Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ jobs:

- name: Quality gate
run: pnpm check

- name: Coverage
run: pnpm test:coverage
25 changes: 14 additions & 11 deletions docs/CURRENT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ The following facts are supported by repository state and executed GitHub Action
- `@mindrail/contracts` provides deterministic generated TypeScript bindings. Generated drift, strict schema validation, fixtures, schema invariants, formatting, lint, and TypeScript checks are part of the repository quality gate.
- The Cloudflare reference persistence mapping is documented under `docs/architecture/02_CLOUDFLARE_RUNTIME_PERSISTENCE.md`; it is a design, not a deployed persistence implementation.
- A deterministic **in-memory local reference runtime vertical slice** exists under `src/runtime/` and consumes the canonical contracts package rather than redefining domain records.
- The runtime requires a `CanonicalDomainValidator` admission seam. Workspace, Agent, Session, Goal, Task, Lease, and Checkpoint records are validated before authoritative insertion; external `Reason` values used by fail/cancel transitions are validated before state mutation. Reference tests wire this seam to the actual strict Draft 2020-12 schemas through the repository's existing dev-only Ajv tooling, so no third-party runtime dependency was added.
- The local runtime currently supports direct Workspace bootstrap plus Agent registration, Session start, Goal/Task creation, Task claim/release/recovery, Checkpoints, Task completion/failure/retry/cancellation, Goal cancellation, dependency release, and automatic Goal success when all Tasks succeed.
- The runtime requires a `CanonicalDomainValidator` admission seam. Workspace, Agent, Session, Goal, Task, Lease, Checkpoint, PermissionRequest, and PermissionDecision records are validated before authoritative insertion; external `Reason` values used by fail/cancel transitions are validated before state mutation. Reference tests wire this seam to the actual strict Draft 2020-12 schemas through the repository's existing dev-only Ajv tooling, so no third-party runtime dependency was added.
- The local runtime currently supports direct Workspace bootstrap plus Agent registration, Session start, Goal/Task creation, Task claim/release/recovery, Checkpoints, Task completion/failure/retry/cancellation, Goal cancellation, dependency release, automatic Goal success when all Tasks succeed, deterministic permission requests, and human permission follow-up decisions.
- Session authority now has an explicit configurable `sessionTimeoutMs` policy using authoritative server time and the ADR-0004 half-open boundary. A stale Session is materialized as `expired`; its active Leases are revoked and cease to authorize checkpoint/completion or new claims. Durable running Task state remains recoverable by a replacement Session with a higher fence.
- Lease authority is separated from Task state. A running Task may temporarily have no effective Lease and may be recovered by a new Session.
- Same-Session duplicate claim returns the current Lease without minting a new fencing token even when a semantic duplicate still carries the pre-claim Task revision. Recovery after Lease release/expiry/session loss grants a strictly higher per-Task fencing token. Rejected Lease admission does not advance the fencing counter. Stale/revoked Lease authority cannot checkpoint or complete work.
- Same-Session duplicate claim returns the current Lease without minting a new fencing token even when a semantic duplicate still carries the pre-claim Task revision. Recovery after Lease release/expiry/session loss grants a strictly higher per-Task fencing token. Rejected Lease admission does not advance the fencing counter. Stale/revoked Lease authority cannot checkpoint, complete work, or create permission requests.
- Capability requirements are checked at claim time, mutable operations use expected revisions where defined by the slice, and Task creation is rejected under a terminal Goal.
- The implemented protocol dispatcher covers `CreateGoal`, `CreateTask`, `ClaimTask`, `RecordCheckpoint`, `CompleteTask`, `FailTask`, `RetryTask`, `CancelTask`, and `CancelGoal`.
- The implemented protocol dispatcher covers `CreateGoal`, `CreateTask`, `ClaimTask`, `RecordCheckpoint`, `CompleteTask`, `FailTask`, `RequestPermission`, `RecordPermissionDecision`, `RetryTask`, `CancelTask`, and `CancelGoal`.
- Protocol commands pass structural pre-admission validation before workspace admission, semantic fingerprinting, dispatch, or receipt insertion. Invalid protocol version/discriminator, malformed EntityIds/ActorRef, malformed command shapes, and invalid revision/fencing fields return `INVALID_INPUT` without mutation or idempotency-key reservation. Unknown Workspace returns a protocol `NOT_FOUND` error envelope rather than escaping as an exception and is likewise not admitted as a receipt.
- Controller-only protocol commands `RetryTask`, `CancelTask`, and `CancelGoal` admit human/system actors and reject agent actors with `ACTOR_NOT_AUTHORIZED` before mutation.
- Implemented protocol mutations use `(workspaceId, commandId)` as the in-memory idempotency key. Semantic fingerprints exclude `correlationId` and `causationId`; exact replay returns an immutable stored result/error snapshot while reflecting the current retry's correlation id; command-id reuse with different semantic intent returns `IDEMPOTENCY_CONFLICT`.
- Controller-only protocol commands `RetryTask`, `CancelTask`, and `CancelGoal` admit human/system actors and reject agent actors with `ACTOR_NOT_AUTHORIZED` before mutation. `RecordPermissionDecision` is stricter: only a human actor may append a follow-up and the only public follow-up outcomes are `ALLOW` or `DENY`.
- Implemented protocol mutations use `(workspaceId, commandId)` as the in-memory idempotency key. Semantic fingerprints exclude `correlationId` and `causationId`; exact replay returns an immutable stored result/error snapshot while reflecting the current retry's correlation id; command-id reuse with different semantic intent returns `IDEMPOTENCY_CONFLICT`. Exact `RequestPermission` replay therefore returns the original PermissionRequest/PermissionDecision IDs without duplicate records.
- The default deterministic permission policy is `PolicyRef { id: "mindrail.permission", version: "0.1.0" }`. Its explicit v0.1 demonstration rules are `workspace.read -> ALLOW`, `external.publish -> DENY`, and `repository.write -> HUMAN_REQUIRED`; unmatched permissions fail closed to `DENY` with `policy.no_matching_rule`. Policy exceptions or structurally invalid policy state return `POLICY_UNAVAILABLE` and append no permission records.
- Policy decisions are sequence 1, system-authored by `system:mindrail.permission-policy`, carry the exact PolicyRef, and never supersede a predecessor. `HUMAN_REQUIRED` grants nothing. Human follow-up requires the expected latest decision ID and a latest `HUMAN_REQUIRED`, derives basis/sequence/supersession, and rejects stale predecessors, cross-workspace request references, non-human actors, or repeated follow-up after a terminal human decision.
- A MindRail `ALLOW` is effective only for the exact PermissionRequest while its original Task/Session/Lease/fencing authority remains current. A late human decision may complete audit history after authority loss, but it cannot revive the old Lease or transfer the grant to replacement execution authority. The permission engine does not mint credentials or override host, IAM, sandbox, or tool approval.
- `CancelTask` revokes its effective Lease when present. `CancelGoal` terminalizes the Goal, cancels its nonterminal Tasks, and revokes their effective Leases; stale completion is rejected afterward.
- Root runtime code explicitly links to `@mindrail/contracts` with `workspace:*`; the frozen pnpm lockfile resolves it as the local workspace package.
- GitHub Actions full-verification run `33254737970` on commit `57491f9b153a8163b927b0a811edabe4083068cb` passed frozen installation, Prettier, ESLint, strict TypeScript checks, generated-contract drift detection, the complete Vitest suite, `pnpm check`, and `pnpm test:coverage`.
Expand All @@ -35,19 +38,19 @@ The following facts are supported by repository state and executed GitHub Action
## Implemented but not yet durable / externally integrated

- Local runtime correctness is currently in-memory and single-process. It proves state-machine and protocol semantics but does not survive process restart.
- Command receipts, Lease counters, Tasks, Goals, Checkpoints, Sessions, and other runtime state are not persisted yet.
- Command receipts, Lease counters, Tasks, Goals, Checkpoints, Sessions, PermissionRequests, PermissionDecisions, and other runtime state are not persisted yet.
- The canonical validator is an injected core boundary; the current executable reference composition proving it against the real schemas lives in test tooling. Production/deployed composition still needs to provide the same canonical validation boundary.
- Session timeout is enforced, but public `HeartbeatSession` and `EndSession` commands are not implemented yet; without heartbeat support, a long-lived real client cannot extend Session liveness through the protocol.
- The v0.1 permission policy is intentionally hard-coded and versioned. It is not a policy DSL, IAM system, credential manager, model judge, or arbitrary-code policy runtime.
- Goal-level ordering is deterministic inside the synchronous local runtime. The future D1/Durable Objects reference implementation must independently prove the concurrency guarantees from ADR-0004 and the persistence design.
- `Quality` is executable and green on verified branches, but issue #3 still tracks enabling the repository-level required merge gate on `main`.

## Next implementation slices

- Complete the remaining protocol/runtime command surface needed for v0.1, including Session heartbeat/end, Lease renewal, block/resume, and permission commands.
- Implement deterministic permission-policy evaluation and human decision handling.
- Complete the remaining protocol/runtime command surface needed for v0.1, including Session heartbeat/end, Lease renewal, and block/resume.
- Add a persistence interface and durable local/reference storage implementation with command receipts, audit events, revision/fencing guards, canonical admission, and restart recovery.
- Implement the Cloudflare Workers/Durable Objects/D1 reference deployment behind the vendor-neutral runtime interfaces.
- Add transport adapters for HTTP and MCP without changing core lifecycle semantics.
- Add transport adapters for HTTP and MCP without changing core lifecycle or permission semantics.
- Add GitHub integration and minimal Codex/ChatGPT/generic agent bootstrap paths.
- Add optional human-facing projections only after runtime state is durable.

Expand All @@ -58,4 +61,4 @@ The following facts are supported by repository state and executed GitHub Action

## Explicit non-capabilities

MindRail does **not yet** provide a production control plane. It does not persist operational state, expose a deployed HTTP/MCP service, issue runtime permissions, run the Cloudflare reference deployment, integrate with real Codex/ChatGPT sessions, or continue agents unattended across process/runtime termination. The current executable milestone is a verified deterministic in-memory control-plane slice.
MindRail does **not yet** provide a production control plane. It does not persist operational state, expose a deployed HTTP/MCP service, issue credentials or external host/IAM authority, run the Cloudflare reference deployment, integrate with real Codex/ChatGPT sessions, or continue agents unattended across process/runtime termination. The current executable milestone is a deterministic in-memory control-plane slice.
48 changes: 48 additions & 0 deletions src/policy/permission-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { PermissionDecision, PolicyRef, ResourceRef } from '@mindrail/contracts';

export interface PermissionPolicyInput {
permission: string;
resource?: ResourceRef;
}

export interface PermissionPolicyEvaluation {
outcome: PermissionDecision['outcome'];
reasonCode: string;
}

export interface PermissionPolicy {
readonly ref: PolicyRef;
evaluate(input: Readonly<PermissionPolicyInput>): PermissionPolicyEvaluation;
}

export const PERMISSION_POLICY_V0_1_REF = {
id: 'mindrail.permission',
version: '0.1.0',
} as const satisfies PolicyRef;

const DEFAULT_DENY: PermissionPolicyEvaluation = {
outcome: 'DENY',
reasonCode: 'policy.no_matching_rule',
};

const RULES: Readonly<Record<string, PermissionPolicyEvaluation>> = {
'workspace.read': {
outcome: 'ALLOW',
reasonCode: 'policy.automatic_allow',
},
'external.publish': {
outcome: 'DENY',
reasonCode: 'policy.denied',
},
'repository.write': {
outcome: 'HUMAN_REQUIRED',
reasonCode: 'policy.human_required',
},
};

export const permissionPolicyV01: PermissionPolicy = {
ref: PERMISSION_POLICY_V0_1_REF,
evaluate(input) {
return { ...(RULES[input.permission] ?? DEFAULT_DENY) };
},
};
11 changes: 10 additions & 1 deletion src/runtime/domain-validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
export type CanonicalDomainTarget =
'Workspace' | 'Agent' | 'Session' | 'Goal' | 'Task' | 'Lease' | 'Checkpoint' | 'Reason';
| 'Workspace'
| 'Agent'
| 'Session'
| 'Goal'
| 'Task'
| 'Lease'
| 'Checkpoint'
| 'PermissionRequest'
| 'PermissionDecision'
| 'Reason';

export interface CanonicalDomainValidationResult {
readonly valid: boolean;
Expand Down
5 changes: 4 additions & 1 deletion src/runtime/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ export type RuntimeErrorCode =
| 'IDEMPOTENCY_CONFLICT'
| 'ACTOR_NOT_AUTHORIZED'
| 'SESSION_NOT_ACTIVE'
| 'CAPABILITY_MISMATCH';
| 'CAPABILITY_MISMATCH'
| 'PERMISSION_DENIED'
| 'HUMAN_DECISION_REQUIRED'
| 'POLICY_UNAVAILABLE';

export class RuntimeError extends Error {
readonly code: RuntimeErrorCode;
Expand Down
72 changes: 72 additions & 0 deletions src/runtime/in-memory-control-plane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@ import type {
EvidenceRef,
Goal,
Lease,
PermissionDecision,
PermissionRequest,
Reason,
Session,
Task,
Workspace,
} from '@mindrail/contracts';

import type { PermissionPolicy } from '../policy/permission-policy.ts';
import { permissionPolicyV01 } from '../policy/permission-policy.ts';
import type { CanonicalDomainTarget, CanonicalDomainValidator } from './domain-validation.ts';
import { RuntimeError } from './errors.ts';
import {
InMemoryPermissionService,
type PermissionGrantAuthority,
type RecordPermissionDecisionInput,
type RequestPermissionInput,
type RequestPermissionResult,
} from './permission-service.ts';
import {
semanticFingerprint,
type CancelGoalCommand,
Expand All @@ -25,7 +36,9 @@ import {
type ProtocolFailure,
type ProtocolResponse,
type ProtocolSuccess,
type RecordPermissionDecisionCommand,
type RecordCheckpointCommand,
type RequestPermissionCommand,
type RetryTaskCommand,
} from './protocol.ts';
import { isProtocolEntityId, validateProtocolCommand } from './protocol-validation.ts';
Expand All @@ -38,6 +51,7 @@ export interface InMemoryControlPlaneOptions {
leaseDurationMs: number;
sessionTimeoutMs: number;
validateCanonicalDomainRecord: CanonicalDomainValidator;
permissionPolicy?: PermissionPolicy;
}

export interface RegisterAgentInput {
Expand Down Expand Up @@ -167,6 +181,7 @@ export class InMemoryControlPlane {
private readonly leaseDurationMs: number;
private readonly sessionTimeoutMs: number;
private readonly validateCanonicalDomainRecord: CanonicalDomainValidator;
private readonly permissionService: InMemoryPermissionService;

private readonly agents = new Map<string, Agent>();
private readonly sessions = new Map<string, Session>();
Expand All @@ -191,6 +206,15 @@ export class InMemoryControlPlane {
this.leaseDurationMs = options.leaseDurationMs;
this.sessionTimeoutMs = options.sessionTimeoutMs;
this.validateCanonicalDomainRecord = options.validateCanonicalDomainRecord;
this.permissionService = new InMemoryPermissionService({
now: this.now,
idFactory: this.idFactory,
validateCanonicalDomainRecord: this.validateCanonicalDomainRecord,
policy: options.permissionPolicy ?? permissionPolicyV01,
assertExecutionAuthority: (authority) => {
this.requireExecutorAuthority(authority);
},
});
const timestamp = this.timestamp();
const workspace: Workspace = {
id: options.workspaceId,
Expand All @@ -210,6 +234,8 @@ export class InMemoryControlPlane {
execute(command: RecordCheckpointCommand): ProtocolResponse<Checkpoint>;
execute(command: CompleteTaskCommand): ProtocolResponse<CompleteTaskResult>;
execute(command: FailTaskCommand): ProtocolResponse<FailTaskResult>;
execute(command: RequestPermissionCommand): ProtocolResponse<RequestPermissionResult>;
execute(command: RecordPermissionDecisionCommand): ProtocolResponse<PermissionDecision>;
execute(command: RetryTaskCommand): ProtocolResponse<Task>;
execute(command: CancelTaskCommand): ProtocolResponse<CancelTaskResult>;
execute(command: CancelGoalCommand): ProtocolResponse<CancelGoalResult>;
Expand Down Expand Up @@ -630,6 +656,31 @@ export class InMemoryControlPlane {
};
}

requestPermission(input: RequestPermissionInput): RequestPermissionResult {
this.assertWorkspace(input.workspaceId);
return this.permissionService.requestPermission(input);
}

recordPermissionDecision(input: RecordPermissionDecisionInput): PermissionDecision {
this.assertWorkspace(input.workspaceId);
return this.permissionService.recordPermissionDecision(input);
}

getPermissionRequest(workspaceId: string, requestId: string): PermissionRequest {
this.assertWorkspace(workspaceId);
return this.permissionService.getPermissionRequest(workspaceId, requestId);
}

listPermissionDecisions(workspaceId: string, requestId: string): PermissionDecision[] {
this.assertWorkspace(workspaceId);
return this.permissionService.listPermissionDecisions(workspaceId, requestId);
}

isPermissionGrantEffective(input: PermissionGrantAuthority): boolean {
this.assertWorkspace(input.workspaceId);
return this.permissionService.isPermissionGrantEffective(input);
}

getGoal(workspaceId: string, goalId: string): Goal {
return clone(this.requireGoal(workspaceId, goalId));
}
Expand Down Expand Up @@ -716,6 +767,27 @@ export class InMemoryControlPlane {
summary: command.summary,
evidence: command.evidence,
});
case 'RequestPermission':
return this.requestPermission({
workspaceId: command.workspaceId,
taskId: command.taskId,
sessionId: command.sessionId,
leaseId: command.leaseId,
fencingToken: command.fencingToken,
permission: command.permission,
justification: command.justification,
...(command.resource === undefined ? {} : { resource: command.resource }),
});
case 'RecordPermissionDecision':
return this.recordPermissionDecision({
workspaceId: command.workspaceId,
requestId: command.requestId,
actor: command.actor,
outcome: command.outcome,
expectedPreviousDecisionId: command.expectedPreviousDecisionId,
reasonCode: command.reasonCode,
...(command.reason === undefined ? {} : { reason: command.reason }),
});
case 'RetryTask':
this.assertControllerActor(command);
return this.retryTask({
Expand Down
Loading