diff --git a/docs/architecture/contracts/api-payload-contracts.md b/docs/architecture/contracts/api-payload-contracts.md index 9b66c4ca..dd5312e4 100644 --- a/docs/architecture/contracts/api-payload-contracts.md +++ b/docs/architecture/contracts/api-payload-contracts.md @@ -257,7 +257,7 @@ Plan-008 Phase 1 (Plan-008-bootstrap, Tier 1 carve-out per [`docs/plans/008-cont | `session.read` | `query` | `SessionReadRequestSchema` | `SessionReadResponseSchema` | `directoryService.readSession(...)` | | `session.join` | `mutation` | `SessionJoinRequestSchema` | `SessionJoinResponseSchema` | `directoryService.joinSession(...)` | -The procedure-type assignments follow the tRPC convention: read-only operations use `query` (HTTP GET-like, idempotent); writes / state-changes use `mutation` (HTTP POST-like, non-idempotent). Method-name strings are dotted-lowercase (`session.create`, `session.read`, `session.join`) per the canonical format ratified in §Tier 1 (cont.): Plan-007 below — the same dotted-lowercase convention applies to both Plan-008's tRPC HTTP procedures and Plan-007's JSON-RPC IPC methods so that client SDK call-site shape is symmetric across local IPC and remote control-plane calls. +The procedure-type assignments follow the tRPC convention: read-only operations use `query` (HTTP GET-like, idempotent); writes / state-changes use `mutation` (HTTP POST-like, non-idempotent). Method-name strings are `dotted-camelCase` (`session.create`, `session.read`, `session.join`) per the canonical format ratified in §Tier 1 (cont.): Plan-007 below — the same `dotted-camelCase` convention applies to both Plan-008's tRPC HTTP procedures and Plan-007's JSON-RPC IPC methods so that client SDK call-site shape is symmetric across local IPC and remote control-plane calls. The Tier 1 surface uses all-lowercase segments (`session.create`, `session.read`, `session.join`); within-segment camelCase is permitted in nested namespaces per LSP precedent (e.g. `textDocument.didOpen`, `settings.effectiveRead`). ```ts // session.create — tRPC mutation @@ -313,17 +313,19 @@ The Plan-007 JSON-RPC method-name registry sub-item and the `protocolVersion` fi Closes the BL-102 sub-item "JSON-RPC method-name canonical-format registry (`session.create` vs `session/create`)" and feature ID F-007p-3-01. -**Canonical format**: `dotted-lowercase`. Method-name strings match the regex: +**Canonical format**: `dotted-camelCase`. Method-name strings match the regex: ``` -/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/ +/^[a-z][a-z0-9]*(\.[a-z][a-zA-Z0-9]*)+$/ ``` -The regex accepts the Tier 1 surface (`session.create`, `session.read`, `session.join`, `session.subscribe`) and rejects: +The regex requires a lowercase-starting first segment (the namespace root); subsequent dot-delimited segments may contain camelCase (`[a-z][a-zA-Z0-9]*`). This adopts the dotted-camelCase _segment_ style of the LSP precedent ([Language Server Protocol §General Messages](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/) — e.g. `workspace.executeCommand`) and the MCP precedent ([Model Context Protocol §Protocol Messages](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) — `tools.list`, `tools.call`), but deliberately **tightens the leading segment to lowercase-only**: LSP's own camelCase-rooted names such as `textDocument.didOpen` are _rejected_ by this regex, because every V1 namespace root (`session`, `driver`, `settings`, `daemon`, `event`, `run`, `repo`, `artifact`) is a lowercase identifier. The V1 Tier 1 surface (`session.create`, `session.read`, `session.join`, `session.subscribe`) uses all-lowercase segments; nested-namespace operations like `settings.effectiveRead` and `driver.listCapabilities` (lowercase root + camelCase tail) are permitted under this regex. + +The regex accepts the Tier 1 surface and rejects: - `session/create` — slash-style (visually conflated with HTTP path segments; ambiguous in JSON-RPC contexts where method names appear in the JSON `method` field, not URLs). - `SessionCreate` — PascalCase (collides with the project's TypeScript type-name convention; `SessionCreate` is already a request-payload type symbol per `packages/contracts/src/session.ts`, so a string-form would be ambiguous at every call site). -- `sessionCreate` — camelCase (cannot express the namespace/operation split without a convention-internal delimiter; doesn't scale to nested namespaces such as `session.member.add`, which would have to become `sessionMemberAdd` and lose the structural signal). +- `sessionCreate` — bare camelCase without a namespace dot (cannot express the namespace/operation split without a convention-internal delimiter; doesn't scale to nested namespaces). **Method-name table** (Plan-007 Phase 3 surface, per F-007p-3-01): @@ -334,16 +336,16 @@ The regex accepts the Tier 1 surface (`session.create`, `session.read`, `session | `session.join` | RPC (request/response) | Add member; emit `MembershipCreated`. | | `session.subscribe` | Long-lived (`LocalSubscriptionConsumer`) | Replay-then-tail event stream. | -**Cross-transport consistency**: This same dotted-lowercase format is used by Plan-008's tRPC HTTP procedures (per §Tier 1 (cont.): Plan-008 above). Both transport surfaces share the convention so that client SDK call-site shape is symmetric across local IPC and remote control-plane calls — `client.session.create({ ... })` reads identically whether the underlying transport is local JSON-RPC over Unix domain socket or tRPC HTTP over the control-plane. +**Cross-transport consistency**: This same `dotted-camelCase` format is used by Plan-008's tRPC HTTP procedures (per §Tier 1 (cont.): Plan-008 above). Both transport surfaces share the convention so that client SDK call-site shape is symmetric across local IPC and remote control-plane calls — `client.session.create({ ... })` reads identically whether the underlying transport is local JSON-RPC over Unix domain socket or tRPC HTTP over the control-plane. **Register-time enforcement** (closes [Plan-007 §I-007-9](../../plans/007-local-ipc-and-daemon-control.md) `BLOCKED-ON-C6`): the method registry's `register(method, handler)` call MUST evaluate `method` against this regex and throw on mismatch. This is mechanical validation, not human review — out-of-format names cannot reach the dispatcher. ```ts -const METHOD_NAME_FORMAT = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/; +const METHOD_NAME_FORMAT = /^[a-z][a-z0-9]*(\.[a-z][a-zA-Z0-9]*)+$/; function register(method: string, handler: Handler): void { if (!METHOD_NAME_FORMAT.test(method)) { - throw new Error(`method name "${method}" violates dotted-lowercase canonical format`); + throw new Error(`method name "${method}" violates dotted-camelCase canonical format`); } // ... registry insertion } @@ -522,9 +524,9 @@ interface RuntimeNodeDetachRequest { ### Runtime-Node Method-Name Registry (Tier 3) -Plan-003's runtime-node operations are exposed as four methods. Method-name strings are `dotted-lowercase` per the canonical `METHOD_NAME_FORMAT` ratified in §Tier 1 (cont.): Plan-007 above (the `register(method, …)` guard at the regex constant) — the same convention shared across Plan-007's JSON-RPC daemon IPC (mechanically regex-enforced at register time) and Plan-008's tRPC control-plane procedures, so the SDK call-site shape (`client.runtimenode.attach({ … })`) is symmetric across transports. Plan-003 registers these handlers under the Plan-007-partial daemon IPC substrate, and the attach/heartbeat calls also cross the Plan-008 control-plane transport (per [Plan-003 §Dependencies](../../plans/003-runtime-node-attach.md)). +Plan-003's runtime-node operations are exposed as four methods. Method-name strings are `dotted-camelCase` per the canonical `METHOD_NAME_FORMAT` ratified in §Tier 1 (cont.): Plan-007 above (the `register(method, …)` guard at the regex constant) — the same convention shared across Plan-007's JSON-RPC daemon IPC (mechanically regex-enforced at register time) and Plan-008's tRPC control-plane procedures, so the SDK call-site shape (`client.runtimenode.attach({ … })`) is symmetric across transports. Plan-003 registers these handlers under the Plan-007-partial daemon IPC substrate, and the attach/heartbeat calls also cross the Plan-008 control-plane transport (per [Plan-003 §Dependencies](../../plans/003-runtime-node-attach.md)). -The `runtimenode` namespace token is the concatenated domain noun — distinct from the `runtime_node.*` **event** taxonomy (the 7 lifecycle events in [Spec-006 §Runtime Node Lifecycle](../../specs/006-session-event-taxonomy-and-audit-log.md)). The underscore `runtime_node.*` form is a valid _event_ name but is **rejected** as a _method_ name by `METHOD_NAME_FORMAT` (no underscores). `runtimenode.capabilityupdate` is the system's first multi-word procedure: it uses the 2-segment lowercase run-on form to match the uniform single-verb arity of the `session.*` surface (the regex also permits a 3-segment `noun.sub.verb` form, reserved for a future nested-router need). +The `runtimenode` namespace token is the concatenated domain noun — distinct from the `runtime_node.*` **event** taxonomy (the 7 lifecycle events in [Spec-006 §Runtime Node Lifecycle](../../specs/006-session-event-taxonomy-and-audit-log.md)). The underscore `runtime_node.*` form is a valid _event_ name but is **rejected** as a _method_ name by `METHOD_NAME_FORMAT` (no underscores). `runtimenode.capabilityupdate` is the system's first multi-word procedure: it uses an all-lowercase run-on form within the `dotted-camelCase` regex (the regex permits camelCase in segments — `runtimeNode.capabilityUpdate` would also be legal — but Plan-003 chose the run-on style to match the uniform single-verb arity of the `session.*` surface; the regex also permits a 3-segment `noun.sub.verb` form, reserved for a future nested-router need). | Method | Procedure type | Request schema | Response schema | | --- | --- | --- | --- | @@ -542,10 +544,15 @@ All four are `mutation`s (state-changing, non-idempotent) per the tRPC procedure ### Plan-005 — Provider Driver Contract (Internal Interface) ```ts -// Internal driver interface — TypeScript interfaces, not Zod (internal boundary) +// Internal driver interface — TypeScript interfaces, not Zod (internal boundary). +// `resumeSession` returns the `DriverResumeResult` discriminated union (defined below) +// to make silent-replacement structurally inexpressible per Spec-005:60. +// `getCapabilities` returns the `GetCapabilitiesResult` wrapper (defined below) so the +// per-tool `ProviderToolMetadata[]` rides alongside the flag matrix in a single +// round-trip per Plan-005 Phase 4 ratified design. interface ProviderDriver { createSession(params: CreateSessionParams): Promise; - resumeSession(params: ResumeSessionParams): Promise; + resumeSession(params: ResumeSessionParams): Promise; startRun(params: StartRunParams): Promise; interruptRun(params: InterruptRunParams): Promise; applyIntervention(params: ApplyInterventionParams): Promise; @@ -553,7 +560,7 @@ interface ProviderDriver { closeSession(params: CloseSessionParams): Promise; listModels(): Promise; listModes(): Promise; - getCapabilities(): Promise; + getCapabilities(): Promise; } interface CreateSessionParams { @@ -604,6 +611,22 @@ interface InterventionDriverResult { fallbackAction?: string; // e.g. 'queue_and_interrupt' for degraded steer } +// Return shape of `ProviderDriver.resumeSession()`. Discriminated union over `status` +// makes silent-replacement structurally inexpressible: the failure variant has no +// `bindingId`, so a successful resume cannot be conflated with a failed one. Spec-005:60 +// requires that resume failure "surface `provider failure` detail and a visible +// `recovery-needed` condition; it must not silently create a replacement provider +// session under the same canonical run." Timestamps for the resumed case live on +// `runtime_bindings.updated_at` (Plan-005 T2.1); the result shape carries only the +// discriminated-union semantic payload. +type DriverResumeResult = + | { status: "resumed"; bindingId: string } + | { + status: "failed"; + recoveryCondition: "recovery-needed"; + providerFailureDetail: string; + }; + interface RespondToRequestParams { runId: RunId; requestId: string; @@ -634,11 +657,84 @@ interface DriverCapabilities { flags: Record; contractVersion: string; } + +// Per-tool idempotency classification used by the daemon's two-phase command-receipt +// protocol during crash recovery (Spec-005 §Tool Metadata; Spec-015 §Idempotency +// Protocol). +type IdempotencyClass = "idempotent" | "compensable" | "manual_reconcile_only"; + +// INGRESS shape — what a provider driver DECLARES via `getCapabilities()`. `idempotency_class` +// is OPTIONAL: a driver MAY omit it and an undeclared class is NOT a contract violation. Were the +// field required here, Zod would reject a conformant-but-silent driver at ingress BEFORE the +// default could apply — defeating Spec-005:128. The daemon's capability-normalization seam +// (Plan-005 T2.4 hydration) resolves an omitted class to `manual_reconcile_only` (the conservative +// default per Spec-005:128), producing a `NormalizedProviderToolMetadata`. +interface ProviderToolMetadata { + name: string; + idempotency_class?: IdempotencyClass; + description?: string; +} + +// NORMALIZED shape — the daemon-side projection AFTER the normalization seam has applied the +// `manual_reconcile_only` default. `idempotency_class` is REQUIRED, so the type system forbids +// persisting an un-normalized value into the NOT NULL `driver_tools.idempotency_class` column or +// emitting it on a `runtime_node.capability_*` event. This is the only tool-metadata shape that +// crosses the persistence / event-payload boundary; ingress `ProviderToolMetadata` never does. +interface NormalizedProviderToolMetadata { + name: string; + idempotency_class: IdempotencyClass; + description?: string; +} + +// Return type of `ProviderDriver.getCapabilities()`. Spec-005:116-118 semantically +// separates whole-driver capability flags from per-tool metadata; the wrapper keeps +// `DriverCapabilities` pure (flags + contractVersion only) while still carrying both +// surfaces in a single round-trip. Modern precedent: MCP 2026 separates `initialize` +// server capabilities from `tools/list`; LSP separates `ServerCapabilities` from +// registered tool surfaces. +interface GetCapabilitiesResult { + capabilities: DriverCapabilities; + tools: ProviderToolMetadata[]; +} ``` ### Plan-006 — Session Event Taxonomy ```ts +// CapabilityDetails — wrapper shape carried by `runtime_node.capability_declared` and +// `runtime_node.capability_updated` event payloads (Spec-006:375-376). Bound to the same +// three surfaces a driver advertises via `ProviderDriver.getCapabilities()` (GetCapabilitiesResult +// above): the seven-flag matrix, the negotiated contract version, and the per-tool metadata — +// here as `NormalizedProviderToolMetadata` (post-default), since these payloads cross the event +// boundary and must never carry an un-normalized `idempotency_class`. +// Why flattened (not nested under `capabilities`): in the event-payload context all three +// surfaces compose one capability snapshot; readers (Plan-013 timeline, Plan-020 dashboards, +// Plan-015 replay) discriminate `runtime_node.capability_*` events from the discriminated +// union and consume the snapshot as a single object — there is no driver-method context +// that requires DriverCapabilities to remain pure. Sources: Spec-006:375-376; Plan-005 +// CP-005-5; Plan-006 Phase 1 T1.4 + Phase 3 doc-mirror audit. +interface CapabilityDetails { + flags: Record; + contractVersion: string; + tools: NormalizedProviderToolMetadata[]; +} + +// runtime_node.capability_declared payload (Spec-006:375). Emitted once per driver +// registration with the daemon's runtime-node bootstrap (Plan-003 territory). +interface RuntimeNodeCapabilityDeclaredPayload { + capability: string; // canonical capability identifier (e.g., "provider-driver") + capabilityDetails: CapabilityDetails; +} + +// runtime_node.capability_updated payload (Spec-006:376). Emitted on driver-version +// bump, tool addition/removal, or flag-matrix mutation. `previousState` / `newState` +// carry the same wrapper shape so consumers diff snapshots structurally. +interface RuntimeNodeCapabilityUpdatedPayload { + capability: string; + previousState: CapabilityDetails; + newState: CapabilityDetails; +} + // EventEnvelopeVersion — branded semver "MAJOR.MINOR" string per ADR-018 §Decision #1. // Wire form and persisted form are both string (never numeric). Parsing extracts MAJOR // and MINOR as integers for numeric comparison; lexical string comparison is unsafe @@ -674,14 +770,19 @@ type EventCategory = | "approval_flow" | "usage_telemetry" // Extended per Spec-006 §Runtime Node Lifecycle, §Recovery Events, §Participant Lifecycle, - // §Audit Integrity, §Security Events, §Event Maintenance, §Policy Events (16 categories total). + // §Audit Integrity, §Security Events, §Event Maintenance, §Policy Events, + // §Channel Arbitration, §Onboarding Lifecycle, §Cross-Node Dispatch (19 categories total + // per Spec-006 §Event Type Summary line 506; 123 event types per line 533). | "runtime_node_lifecycle" | "recovery_events" | "participant_lifecycle" | "audit_integrity" | "security_events" | "event_maintenance" - | "policy_events"; + | "policy_events" + | "channel_arbitration" + | "onboarding_lifecycle" + | "cross_node_dispatch"; // Individual event types within each category are enumerated in Spec-006 §Event Type Enumeration. // EventReadAfterCursor @@ -733,20 +834,29 @@ interface DaemonHelloResult { // DaemonStatusRead interface DaemonStatusReadParams {} interface DaemonStatusReadResult { - state: "starting" | "ready" | "degraded" | "shutting_down"; - activeSessions: number; - activeRuns: number; - uptime: number; // seconds + processState: "running" | "starting" | "stopping" | "degraded"; + protocolVersion: string; + transportEndpoint: string; + uptimeMs: number; } -// DaemonStart / DaemonStop / DaemonRestart -interface DaemonLifecycleParams { - action: "start" | "stop" | "restart"; - force?: boolean; +// DaemonStop / DaemonRestart (no DaemonStart: daemon cold-boot is the CLI process-spawn path — `ai-sidekicks daemon start` — not an IPC method; see Plan-007 T-007r-3-4) +// Separate per-method request schemas (NOT a shared `action` discriminator): each carries the idle-drain +// deadline that I-007-12 self-swap refusal + I-007-15 quiesce depend on. The 5000ms default is applied by +// the Zod schema (Plan-007 T-007r-1-2), so the field is input-optional but always present post-parse. +// Refusal is the canonical JSON-RPC error envelope (data.type: "daemon.lifecycle_conflict"), never a +// success-shape discriminator — so both success results are the uniform { accepted: true }. +interface DaemonStopParams { + idleDrainDeadlineMs?: number; // default 5000 +} +interface DaemonStopResult { + accepted: true; +} +interface DaemonRestartParams { + idleDrainDeadlineMs?: number; // default 5000 } -interface DaemonLifecycleResult { - state: string; - message: string; +interface DaemonRestartResult { + accepted: true; } // LocalSubscription @@ -825,7 +935,11 @@ interface InterventionRequestResponse { result?: Record; } -// RunStateChange (event, not request/response) +// RunStateChange (event, not request/response). The `run.failed` variant carries the +// `providerFailureDetail` surface that mirrors `DriverResumeResult.failure.providerFailureDetail` +// (line 620 above) — Spec-005:60 requires resume-failure detail to reach the canonical audit +// log so Plan-015's recovery dispatcher and Plan-013's timeline can render the operator-actionable +// reason for the failure without re-querying the driver. Plan-005 CP-005-5; Plan-006 Phase 3 audit. interface RunStateChangeEvent { runId: RunId; previousState: RunState; @@ -833,6 +947,7 @@ interface RunStateChangeEvent { failureCategory?: RunFailureCategory; recoveryCondition?: "recovery-needed"; healthSignal?: "stuck-suspected"; + providerFailureDetail?: string; // populated on `run.failed` when failureCategory='provider' timestamp: string; } ``` diff --git a/docs/architecture/cross-plan-dependencies.md b/docs/architecture/cross-plan-dependencies.md index 88b00311..00456205 100644 --- a/docs/architecture/cross-plan-dependencies.md +++ b/docs/architecture/cross-plan-dependencies.md @@ -23,6 +23,10 @@ These tables are claimed by multiple plans. The table below resolves each confli | `participant_keys` (SQLite) | Plan-001 (initial migration `0001-initial.ts`) | Plan-022 (schema origin and CRUD code paths) | **Forward-declared split per Plan-022 header.** Plan-022 authors the `participant_keys` schema but forward-declares the `CREATE TABLE` into Plan-001's Tier 1 migration so V1 session-core cannot ship without the GDPR crypto-envelope schema (per ADR-015 V1 scope). Plan-022's implementation code paths (store, rotation, wrap-codec) land at Tier 5. | | `session_events.pii_payload` (SQLite column) | Plan-001 (initial migration `0001-initial.ts`) | Plan-022 (column origin; reader/writer code paths at Tier 5) | **Forward-declared split per Plan-022 header.** Plan-022 adds this BLOB column to Plan-001's `session_events` schema in the Tier 1 migration so the crypto envelope does not require a breaking schema migration after V1 ships. | | `session_events.{monotonic_ns,prev_hash,row_hash,daemon_signature,participant_signature}` (SQLite columns) | Plan-001 (initial migration `0001-initial.ts`) | Plan-006 (column origin; integrity-protocol semantics + writer/verifier code paths at Tier 4) | **Forward-declared split per Spec-006 §Integrity Protocol.** Plan-001 ships the physical CREATE of these columns in the Tier 1 initial migration so the integrity protocol does not require a breaking schema migration after V1 ships. Plan-006 owns the column semantics (monotonic-clock allocation, hash-chain construction, dual-signature mechanics) and the writer/verifier code paths at Tier 4. Plan-003 emits `runtime_node.*` events into this surface at Tier 3, but the events conform to the column shape Plan-001 ships — Plan-003 does not author the integrity-protocol semantics. | +| `daemon_signing_keys` (SQLite table, Plan-006-owned, sealed-Ed25519-private-key store) | Plan-006 (additive migration at Tier 4 Phase 2 — `0NNN-daemon-signing-keys.ts`; new local SQLite table) | Plan-002 (consumer at session-create via amendment-via-extension PR — invokes `DaemonSigningKeySource.create(sessionId)` and registers the resulting public key in the participant roster) | **Plan-006 audit F-006-2-02 resolution (hardened-mode, user-ratified 2026-05-28); revised post-Codex T4 review on PR #124.** Daemon-private signing keys are local-machine secrets and MUST live in local SQLite (per ADR-004 SQLite-Local-State boundary). They are NEVER stored in shared Postgres `sessions` (the original pre-T4-review row mis-located the column there — corrected to a new local-SQLite table here). Plan-006 Phase 2 T2.7 ships the `DaemonSigningKeySource` interface + `OsKeystoreSealedDaemonSigningKeySource` implementation: per-session Ed25519 private key sealed via an OS-keystore-managed master key (Keychain `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` on macOS / `CRED_TYPE_GENERIC` `CRED_PERSIST_LOCAL_MACHINE` on Windows / Secret Service via libsecret + kwallet6 + keyutils fallback on Linux per [Spec-022 §Daemon Master Key :146](../specs/022-data-retention-and-gdpr.md)), accessed through the `@napi-rs/keyring` v1.2.0 cross-platform shim (`keytar` superseded — unmaintained); stored as ciphertext in the new local `daemon_signing_keys.sealed_private_key` BLOB column keyed by `session_id`. Self-contained against Plan-022 (Tier 5) — no tier inversion. Plan-002 (Tier 1, closed) reopens via amendment-via-extension PR per CP-002-N to add the session-create call-site + roster public-key registration; the amendment is scope-limited to call-site + roster row write (no master-key crypto in Plan-002). Spec-006:382 (the public key "registered in the session participant roster at join time, keyed by NodeId") is the governing semantic. | +| `session_events.retention_class` (SQLite column, `TEXT` nullable; `'audit_stub'` discriminator) | Plan-006 (additive migration at Tier 4 Phase 3 — `0NNN-retention-class-and-stub-signature.ts`) | None (Plan-006-internal: compactor T3.2 writes; replay-service T4.3 reads; verifier T4.1 keys compacted-row verification off it) | **Plan-006 audit F-006-3-02 resolution (hardened-mode, Phase 3 Design B).** Compactor (T3.2) sets `retention_class = 'audit_stub'` after compacting a row's payload to the audit-stub form per [Spec-006 §Compacted Event Format](../specs/006-session-event-taxonomy-and-audit-log.md). A typed column (vs JSON-field probe inside `payload`) keeps hot-path replay-window construction fast and lets the compactor selector exclude already-stubbed rows via `WHERE retention_class IS NULL` for re-compaction guards. Partial index `WHERE retention_class IS NULL` recommended for compactor candidate-range queries. | +| `session_events.stub_signature` (SQLite column, `BLOB` nullable; 64-byte Ed25519) | Plan-006 (additive migration at Tier 4 Phase 3 — `0NNN-retention-class-and-stub-signature.ts`, same migration as `retention_class`) | None (Plan-006-internal: compactor T3.2 writes; verifier T4.1 re-verifies) | **Plan-006 post-compaction stub-authenticity resolution (hardened-mode; Codex PR #124 round-2 P1, 2026-05-28).** Per-row commitment to the surviving audit-stub bytes: the compactor (T3.2) signs `canonical_bytes(audit-stub projection)` with the daemon session key and stores the 64-byte signature here; the verifier (T4.1) re-verifies it against the row's CURRENT stub bytes — tampered/replayed/missing → `failureMode: 'stub_signature_invalid'`. Closes the gap where the round-1 anchor (which commits only to the ORIGINAL `row_hash`) could not detect a post-compaction edit of the visible stub. A companion **scalar-binding check** (verifier T4.1; Codex PR #124 round-5 P1, 2026-05-28) additionally binds the surviving scalar columns (`category`/`type`/`actor`/`occurred_at`, plus `id`/`session_id`/`sequence`) to the signed `payload` projection — a scalar edited to diverge from the projection (which `stub_signature` alone cannot catch, since it signs only `payload`) → `failureMode: 'stub_scalar_mismatch'`. NULL for live (`retention_class IS NULL`) rows. See [Spec-006 §Post-Compaction Integrity](../specs/006-session-event-taxonomy-and-audit-log.md#post-compaction-integrity). | +| `session_snapshots.{has_compacted_ranges,compacted_range_count}` (SQLite columns) | Plan-006 (additive migration at Tier 4 Phase 4 — `0NNN-session-snapshots-compaction-cursor.ts`) | Plan-015 (read-only consumer at recovery dispatch — flag tells recovery dispatcher whether replay across this snapshot range will encounter audit stubs) | **Plan-006 audit F-006-4-01 resolution (hardened-mode, Phase 4 Reading (a)).** `has_compacted_ranges BOOLEAN NOT NULL DEFAULT 0` and `compacted_range_count INTEGER NOT NULL DEFAULT 0` are read-side hints that replay output across this snapshot's `as_of_sequence` range will encounter audit stubs. NOT redundant with Plan-015's separate `replay_cursors.last_sequence` (different question: replay_cursors = projector resumption pointer; these flags = compaction-incidence indicator). Plan-006 Phase 4 T4.4 adds the columns. local-sqlite-schema.md:57 already lists Plan-006 as an explicit extender of `session_snapshots`. | | `participants` (Postgres) | Plan-001 (initial migration `0001-initial.ts` — minimal `id UUID PK`, `created_at TIMESTAMPTZ`) | Plan-018 (Identity And Participant State — additive ALTER migrations for `display_name`, `identity_ref`, `metadata` + the `identity_mappings` side table) | **Forward-declared split per [Plan-001 §Cross-Plan Forward-Declared Schema](../plans/001-shared-session-core.md#cross-plan-forward-declared-schema).** Plan-001 owns the physical CREATE of the minimal identity-anchor shape at Tier 1 because `session_memberships.participant_id`, `session_invites.inviter_id`, and `runtime_node_attachments.participant_id` all `REFERENCES participants(id)` (Plans 001/002/003 execute before Plan-018 per §5 Canonical Build Order). Plan-018 extends with identity/profile columns and the `identity_mappings` side table via additive ALTER migrations at Tier 5. | | `sessions.min_client_version` (Postgres column) | Plan-001 (initial migration `0001-initial.ts` — `min_client_version TEXT` forward-decl, NULL = no floor) | Plan-003 (Runtime Node Attach — attach-time version-floor enforcement per ADR-018 §Decision #4) | **Forward-declared split per [Plan-001 §Cross-Plan Forward-Declared Schema](../plans/001-shared-session-core.md#cross-plan-forward-declared-schema).** Plan-001 ships the column shape (TEXT, NULL default = no floor) at Tier 1 so V1 session-core can persist a per-session client-version floor without a breaking schema migration. ADR-018 §Decision #4 routes the floor through Plan-003's runtime-node-attach flow: peers read the floor from this column at attach time and reject below-floor writes with the `VERSION_FLOOR_EXCEEDED` envelope (shipped at Plan-001 T2.3 / `packages/contracts/src/error.ts`). Control-plane is authoritative for session metadata per [ADR-004](../decisions/004-sqlite-local-state-and-postgres-control-plane.md). Plan-001 PR #4 does NOT write the column; Plan-003 picks up read/write semantics at Tier 3 per BL-090. | @@ -36,8 +40,8 @@ All other tables have a single owning plan. See `docs/plans/NNN-*.md` Data And S | Plan-002 | `session_invites` (Postgres) | | Plan-003 | `node_capabilities`, `node_trust_state` (SQLite); `runtime_node_attachments`, `runtime_node_presence` (Postgres) | | Plan-004 | `queue_items`, `interventions`, `command_receipts` (SQLite) | -| Plan-005 | `runtime_bindings`, `driver_capabilities` (SQLite) | -| Plan-006 | No owned tables; extends `session_events` (Plan-001) | +| Plan-005 | `runtime_bindings`, `driver_capabilities`, `driver_tools`, `driver_contract_meta` (SQLite) | +| Plan-006 | `event_log_anchors` (Postgres — anchor metadata only per ADR-017); `daemon_signing_keys`, `pending_anchor_uploads` (SQLite — daemon-private signing-key store + partition-tolerance queue per F-006-2-02 + F-006-3-01); extends `session_events`, `session_snapshots` (Plan-001) with additive Tier-4 column migrations per Contested Tables rows above | | Plan-008 | `session_directory`, `relay_connections` (Postgres) | | Plan-009 | `repo_mounts`, `workspaces` (SQLite) | | Plan-010 | `worktrees`, `ephemeral_clones`, `branch_contexts` (SQLite) | @@ -87,7 +91,7 @@ These directories or files are targeted by multiple plans. The owning plan creat | `packages/control-plane/src/cross-node-dispatch/` | Plan-027 | (no extenders yet) | Plan-027's shared coordination-row service and relay-routing adapter land here; dispatch payloads and ApprovalRecord content remain daemon-local. | | `apps/desktop/src/main/` + `apps/desktop/src/preload/` (Electron main process + preload bridge — trusted Node-side surface) | Plan-023 (creates `apps/desktop/src/{main,preload}/` directory tree at Tier 1 Partial — see Plan-023 carve-out in §5; remainder of main-process surface — daemon supervisor, keystore, WebAuthn, auto-updater, deep-link, crash reporter, etc. — lands at Tier 8) | Plan-001 (`apps/desktop/src/main/sidecar-lifecycle.ts` at Tier 1 Phase 5 per CP-001-1 content-ownership) | Plan-001 Phase 5 authors `sidecar-lifecycle.ts` against the Plan-023 Tier 1 Partial substrate per BL-101 (a) resolution. The directory tree is owned by Plan-023; per-file content ownership crosses plan boundaries (CP-001-1: Plan-001 owns drain orchestration content; Plan-023 Tier 8 owns supervisor / keystore / etc.). The architecture-tier canonical naming (`apps/desktop/src/{main,preload,renderer}/`) follows the electron-vite zero-config convention per [container-architecture.md §Canonical Implementation Topology](./container-architecture.md#canonical-implementation-topology) + [component-architecture-desktop-app.md §Implementation Home](./component-architecture-desktop-app.md#implementation-home), grounded in the [BL-101 orchestrated 5-axis research synthesis (60+ sources)](../archive/backlog-archive.md#bl-101-c-3--plan-023-tier-1-partial-substrate-carve-out-mirrors-plan-007-partial--plan-008-bootstrap). | | `apps/desktop/src/renderer/` | Plan-023 (creates the React + Vite renderer app at Tier 1 Partial — see Plan-023 carve-out in §5; renderer feature-views remainder lands at Tier 8) | Plan-001 (`apps/desktop/src/renderer/src/session-bootstrap/` at Tier 1 Phase 5), Plan-002 (`apps/desktop/src/renderer/src/session-members/`), Plan-003 (`apps/desktop/src/renderer/src/runtime-node-attach/`), Plan-004 (`apps/desktop/src/renderer/src/run-controls/`), Plan-006 (`apps/desktop/src/renderer/src/timeline/` audit-stub), Plan-007 (`apps/desktop/src/renderer/src/daemon-status/`), Plan-008 (`apps/desktop/src/renderer/src/session-join/`), Plan-009 (workspace/repo renderer views), Plan-010 (`apps/desktop/src/renderer/src/execution-mode-picker/`), Plan-011 (`apps/desktop/src/renderer/src/diff-review/`), Plan-012 (approvals renderer views), Plan-013 (`apps/desktop/src/renderer/src/timeline/` live), Plan-014 (artifacts renderer views), Plan-015 (`apps/desktop/src/renderer/src/recovery-status/`), Plan-016 (channels renderer views), Plan-017 (`apps/desktop/src/renderer/src/workflows/`), Plan-018 (`apps/desktop/src/renderer/src/participants/`), Plan-019 (notifications renderer views), Plan-020 (`apps/desktop/src/renderer/src/health-and-recovery/`), Plan-026 (`apps/desktop/src/renderer/src/onboarding/`), Plan-027 (`apps/desktop/src/renderer/src/cross-node-dispatch/`) | Each extending plan adds renderer views as thin projections over the Spec-023 preload-bridge surface (`window.sidekicks`). Extending plans must not bypass the bridge to reach daemon or control-plane state directly. Tier-ordering detail: extensions land at each plan's canonical tier; renderer-tree construction now begins at Plan-023's Tier 1 Partial (per BL-101 (a) resolution), so Plan-001 Phase 5's `session-bootstrap/` extension lands at Tier 1 alongside the substrate. Tier 2+ extender plans ship their renderer subtrees at their own canonical tier (the substrate is already present from Tier 1). Plan-013's live timeline components land under `apps/desktop/src/renderer/src/timeline/` at Plan-013's Tier 8 placement; Plan-006's audit-stub rendering folds into the same subtree at Tier 4. The renderer's nested `src/` reflects the electron-vite renderer-as-Vite-subproject convention. | -| `packages/contracts/src/` | Plan-001 Phase 2 owns the canonical single-file-per-domain contract pattern (`session.ts`, `event.ts`, `error.ts`). Substrate-carve-out plans (per §5) may extend `packages/contracts/src/` with additional contract files within their owned namespace (e.g., Plan-007-partial extends with `jsonrpc-streaming.ts` + `jsonrpc-negotiation.ts` + `jsonrpc.ts` for the JSON-RPC wire substrate). | Plan-024 (`pty-host.ts` precedent), Plan-021 (`rate-limiter.ts`), Plan-027 (`cross-node-dispatch.ts`), Plan-007-partial (substrate-carve-out per §5) | The directory is a shared home for cross-plan contract files. Single-file-per-domain is the default; §5 substrate-carve-out plans MAY extend within their owned namespace. No two plans edit the same file, so no shared-resource conflict exists. | +| `packages/contracts/src/` | Plan-001 Phase 2 owns the canonical single-file-per-domain contract pattern (`session.ts`, `event.ts`, `error.ts`). Substrate-carve-out plans (per §5) may extend `packages/contracts/src/` with additional contract files within their owned namespace (e.g., Plan-007-partial extends with `jsonrpc-streaming.ts` + `jsonrpc-negotiation.ts` + `jsonrpc.ts` for the JSON-RPC wire substrate). | Plan-024 (`pty-host.ts` precedent), Plan-021 (`rate-limiter.ts`), Plan-027 (`cross-node-dispatch.ts`), Plan-007-partial (substrate-carve-out per §5), Plan-005 (`provider-driver.ts` — `ProviderDriver` interface + `DriverCapabilities` + `IdempotencyClass` + `ProviderToolMetadata` + `GetCapabilitiesResult` + `ApplyInterventionParams` + `DriverInterventionResultSchema`) | The directory is a shared home for cross-plan contract files. Single-file-per-domain is the default; §5 substrate-carve-out plans MAY extend within their owned namespace. No two plans edit the same file, so no shared-resource conflict exists. | | `packages/contracts/src/workflows/` | Plan-017 (creates the subdirectory at Tier 8 per §5) | (no extenders yet) | First subdirectory member of `packages/contracts/src/`, diverging from the parent row's "single-file-per-domain" convention. Convention-extension call (single-file-only vs single-file-or-single-subdirectory) deferred per [BL-097 Resolution §7(d)](../archive/backlog-archive.md#bl-097-reconcile-workflow-v1-scope-drift-spec-017-vs-v1-feature-scopemd) — resolution blocked on a second subdirectory candidate surfacing in another plan. | | `packages/crypto-paseto/` | Plan-025 Tier 1 Partial (`packages/crypto-paseto/` substrate — v4.public + v4.local primitives + RFC vectors; substrate-vs-namespace decomposition mirroring Plan-007-partial / Plan-008-bootstrap / Plan-023-partial — see §5 carve-out) **shipped 2026-05-21 via [PR #92](https://github.com/Sawmonabo/ai-sidekicks/pull/92)** — Plan-025 §Decision Log; Plan-025 remainder (Tier 7 — Fastify relay server, Dockerfile, operator runbook) | Plan-002 (Phase 2 — invite-token issuer via v4.local per [CP-002-4](../plans/002-invite-membership-and-presence.md#cross-plan-obligations)); Plan-018 (access-token issuer via v4.public + refresh-token issuer via v4.local per [ADR-010:29](../decisions/010-paseto-webauthn-mls-auth.md)) | **Symmetric co-dep per Plan-025 §Risks And Blockers.** Plan-025 formally depends on Plan-018's issuer key-publication surface, but the shared crypto primitive package must land first (from Plan-025) or Plan-002 Phase 2 + Plan-018 cannot compile. Plan-025's first-four steps ship at Tier 1 Partial; the rest of Plan-025 (relay server, deploy assets) lands at Tier 7. BL-119 resolved 2026-05-20 via Option A (substrate-vs-namespace decomposition). v4.local is **XChaCha20 stream cipher + BLAKE2b-MAC** encrypt-then-MAC per the PASETO v4 spec — **not** XChaCha20-Poly1305 AEAD; doc-drift swept in the same PR (#92 follow-up). | | `packages/sidecar-rust-pty/` (workspace path / Rust crate) + npm umbrella `@ai-sidekicks/pty-sidecar` + 5 platform packages (`@ai-sidekicks/pty-sidecar-{win32-x64,darwin-arm64,darwin-x64,linux-x64,linux-arm64}`) | Plan-024 | Plan-005 (consumes the `PtyHost` contract from `packages/contracts/src/pty-host.ts`) | **Naming reconciliation.** The repo workspace path for Plan-024's Rust crate is `packages/sidecar-rust-pty/` (matches Plan-024 §Scope line 25 and §Target Areas lines 78–82). The published npm umbrella package is `@ai-sidekicks/pty-sidecar`, which is an unscoped shorthand sometimes referenced as `packages/pty-sidecar/` in earlier drafts. Both refer to the same artifact — the workspace path is canonical for source-tree references; the umbrella name is canonical for npm-consumer references. Plan-024 publishes the umbrella + platform packages via the esbuild-precedent `optionalDependencies` + `os`/`cpu` filter pattern. Plan-005 runtime bindings import the `PtyHost` contract, not the binary directly. | @@ -128,10 +132,10 @@ Each dependency is annotated with its type: | Plan-003 | Plan-001 (session model, node attachment to sessions) | spec-declared | | Plan-003 | Plan-006 (`runtime_node.*` event taxonomy — Plan-003 emits 7 events but does not author the `EventEnvelope` schema or the integrity-protocol semantics; per Spec-006 the taxonomy is registered at Tier 4. Tier 3 ships event-shape stubs only; full taxonomy registration lands in Plan-006 at Tier 4 as an additive follow-up), Plan-008-bootstrap (Tier 1 — control-plane attach contracts surfaced via tRPC sessionRouter substrate; Plan-003's runtime-node attach calls cross the same transport) | implementation-derived | | Plan-004 | Plan-001 (session core), Plan-005 (driver capability checks for steer/pause routing) | spec-declared, implementation-derived | -| Plan-005 | Plan-024 (consumes `PtyHost` contract from `packages/contracts/src/pty-host.ts` for runtime-binding provider surface) | implementation-derived | -| Plan-006 | Plan-001 (extends session_events, session_snapshots) | implementation-derived | +| Plan-005 | Plan-024 (consumes `PtyHost` contract from `packages/contracts/src/pty-host.ts` for runtime-binding provider surface); Plan-007-partial (Tier 1 — Phase 4 SDK consumes the JSON-RPC client transport `packages/client-sdk/src/transport/jsonRpcClient.ts` shipped via PR #19; Phase 4 also registers 7 client-facing `driver.*` JSON-RPC method handlers under Plan-007's namespace registry per CP-005-4 / CP-007-6 — 6 capability/intervention/read verbs + `driver.subscribeEvents`; the 4 session/run lifecycle ops stay daemon-internal per Plan-005 §Phase 4) | implementation-derived | +| Plan-006 | Plan-001 (owns the Tier-4 integrity-protocol semantics over Plan-001's forward-declared `session_events` columns — the integrity columns `{monotonic_ns, prev_hash, row_hash, daemon_signature, participant_signature}` + `pii_payload` — AND ships the Plan-006-owned ADDITIVE Tier-4 columns `session_events.{retention_class, stub_signature}` + `session_snapshots.{has_compacted_ranges, compacted_range_count}` as new migrations per Contested Tables rows above + CP-006-6: these are NOT forward-declared, so an implementer must run the Tier-4 compaction migration to create them or the verifier/compactor hits missing columns at runtime; consumes migration runner + `schema_version` table for Phase 3 T3.4 schema-migration emitter); Plan-002 (amendment-via-extension PR at Tier 1+ adds session-create call-site for `DaemonSigningKeySource.create(sessionId)` + participant-roster public-key registration per F-006-2-02 resolution and Spec-006:382; Plan-006 owns the new `daemon_signing_keys` SQLite table — sealed-Ed25519 private-key storage, NOT a shared-Postgres `sessions` column per ADR-004 SQLite-local-state boundary, corrected post-Codex T4 review on PR #124); Plan-005 (consumes `DriverCapabilityFlag` + `ProviderToolMetadata` + `DriverResumeResult` types from `packages/contracts/src/provider-driver.ts` to bind `CapabilityDetails` wrapper on `runtime_node.capability_declared` / `_updated` payloads and `providerFailureDetail` on `run.failed` payload per CP-005-5 carry-forward resolution); Plan-007-partial Tier 1 (consumes `LocalSubscriptionProducer` from `packages/contracts/src/jsonrpc-streaming.ts` for Phase 4 T4.5 live event-subscription stream); Plan-007-remainder Tier 4 (Phase 4 T4.7 registers `event.readAfterCursor`, `event.readWindow`, `event.subscribe` JSON-RPC methods under Plan-007's namespace registry per CP-006-4 / CP-007-N); Plan-008-bootstrap Tier 1 (Phase 3 T3.3 mounts `event-anchors/anchor-router.ts` tRPC procedure on `host.ts` per CP-006-2); Plan-022 (Plan-006 Phase 2 T2.4 declares `PiiEncryptor` interface + ships stub; Plan-022 Tier 5 ships real AES-256-GCM codec implementation per CP-006-1 — Plan-006's `pii-indirection.ts` is sole-write-path enforcer with structural sole-write enforcement via branded `PiiPayloadCiphertext` type) | implementation-derived (Plan-006 audit synthesis 2026-05-28; T4 row-mislocation correction 2026-05-28) | | Plan-007 | None upstream-as-blocker. Partial/remainder split: Plan-007-partial (Tier 1 — Spec-007 §Wire Format substrate + `session.*` namespace + SDK Zod layer) is a build-order prerequisite for Plan-001 Phase 5; Plan-007-remainder (Tier 4 — `run.*` / `repo.*` / `artifact.*` / `settings.*` / `daemon.*` namespaces + Spec-027 SecureDefaults) ships at its original tier. **Tier 1 phase-level imports** (per Plan-007 header): Phase 3 imports Plan-001 Phase 2 (`packages/contracts/src/session.ts` + `packages/contracts/src/event.ts` Zod schemas) — Plan-007 Phase 3 PR cannot open until Plan-001 Phase 2 has merged (already shipped at Tier 1). See §5 Tier 1 carve-out and Plan-007 §Execution Windows. | declared in plan header | -| Plan-008 | Plan-008 bootstrap-deliverable (Tier 1 — tRPC v11 server skeleton + `sessionRouter` HTTP handlers exposing `packages/control-plane/src/sessions/session-directory-service.ts` + SSE substrate for `SessionSubscribe` per `packages/contracts/src/session.ts:408`): Plan-001 Phase 4 (`session-directory-service.ts` already shipped at Tier 1) **AND** Plan-001 Phase 2 schemas (`packages/contracts/src/session.ts` + `packages/contracts/src/event.ts`) as upstream prerequisites. **Tier 1 forward-declared contract dep:** the `EventEnvelope` type at `packages/contracts/src/event.ts` is forward-declared by Plan-001 Phase 2 with semantic ownership at Plan-006 Tier 4 — the SSE substrate consumes the placeholder shape and rebinds at Tier 4 (CP-008-3). Plan-008 remainder (Tier 5 — relay, presence, invites, `session_directory` + `relay_connections` tables): Plan-001 (session core), Plan-002 (invite acceptance, presence register), Spec-024 (implicit cross-node dispatch surface per §Spec-024 Implementation Plan below — consumed by Plan-008-remainder). See §5 Tier 1 carve-out and Plan-008 §Execution Windows. | spec-declared, implementation-derived | +| Plan-008 | Plan-008 bootstrap-deliverable (Tier 1 — tRPC v11 server skeleton + `sessionRouter` HTTP handlers exposing `packages/control-plane/src/sessions/session-directory-service.ts` + SSE substrate for `SessionSubscribe` per `packages/contracts/src/session.ts:408`): Plan-001 Phase 4 (`session-directory-service.ts` already shipped at Tier 1) **AND** Plan-001 Phase 2 schemas (`packages/contracts/src/session.ts` + `packages/contracts/src/event.ts`) as upstream prerequisites. **Tier 1 forward-declared contract dep:** the `EventEnvelope` type at `packages/contracts/src/event.ts` is forward-declared by Plan-001 Phase 2 with semantic ownership at Plan-006 Tier 4 — the SSE substrate consumes the placeholder shape and rebinds at Tier 4 (CP-008-3). Plan-008 remainder (Tier 5 — relay, presence, invites, `session_directory` + `relay_connections` tables): Plan-001 (session core), Plan-002 (invite acceptance, presence register), Spec-024 (implicit cross-node dispatch surface per §Spec-024 Implementation Plan below — consumed by Plan-008-remainder), Plan-006 (Tier 4 — Plan-006 Phase 3 T3.3 mounts `event-anchors/anchor-router.ts` tRPC procedure on `apps/control-plane/src/host.ts` for Merkle-anchor metadata uploads to `event_log_anchors` per ADR-017 + CP-006-3; Plan-008-bootstrap's host substrate is the mount-point host). See §5 Tier 1 carve-out and Plan-008 §Execution Windows. | spec-declared, implementation-derived | | Plan-009 | None | — | | Plan-010 | Plan-009 (workspace infrastructure) | spec-declared | | Plan-011 | Plan-010 (worktree infrastructure), Plan-014 (artifact manifests) | declared in plan header | diff --git a/docs/architecture/schemas/local-sqlite-schema.md b/docs/architecture/schemas/local-sqlite-schema.md index 75be4b60..fd396411 100644 --- a/docs/architecture/schemas/local-sqlite-schema.md +++ b/docs/architecture/schemas/local-sqlite-schema.md @@ -38,29 +38,38 @@ CREATE TABLE session_events ( -- lexical TEXT comparison is unsafe, e.g. "1.10" < "1.9") -- Integrity protocol (BL-050): hash-chain + per-event daemon signature prev_hash BLOB NOT NULL, -- 32 bytes; row_hash of previous row (zero-filled at sequence=0) - row_hash BLOB NOT NULL, -- 32 bytes; BLAKE3(prev_hash || JCS-canonical envelope bytes) - daemon_signature BLOB NOT NULL, -- 64 bytes; Ed25519 over same canonical bytes + row_hash BLOB NOT NULL, -- 32 bytes; BLAKE3(prev_hash || JCS-canonical envelope bytes); frozen pre-compaction + daemon_signature BLOB NOT NULL, -- 64 bytes; Ed25519 over same canonical bytes; frozen pre-compaction participant_signature BLOB, -- 64 bytes; Ed25519 from participant key; NULL for non-sensitive events + -- Compaction (Plan-006 Tier 4 Phase 3): typed retention discriminator + post-compaction stub commitment + retention_class TEXT CHECK (retention_class IS NULL OR retention_class = 'audit_stub'), -- NULL = live row (per-row chain-verified); 'audit_stub' = compacted (anchor + stub_signature verified). Column-level CHECK closes the discriminator domain; it is ALTER-ADD-COLUMN-addable (references only this column; NULL-permitting so pre-migration rows pass). Co-presence (audit_stub ⟺ non-NULL stub_signature) is a two-column invariant that cannot be an ALTER-added table-level CHECK without a 12-step table rebuild; it is instead enforced at the verification layer per Spec-006 §Post-Compaction Integrity (NULL stub_signature on an audit_stub row → stub_signature_invalid; surviving scalar columns category/type/actor/occurred_at bound to the signed payload projection → stub_scalar_mismatch on divergence). + stub_signature BLOB, -- 64 bytes; Ed25519 over canonical_bytes(audit-stub projection); NULL for live rows. Authenticates the post-compaction stub representation per Spec-006 §Post-Compaction Integrity (frozen row_hash/daemon_signature commit only to the now-discarded pre-compaction bytes) UNIQUE(session_id, sequence) ); CREATE INDEX idx_session_events_session_seq ON session_events(session_id, sequence); CREATE INDEX idx_session_events_type ON session_events(session_id, type); CREATE INDEX idx_session_events_correlation ON session_events(correlation_id) WHERE correlation_id IS NOT NULL; +-- Hot-path replay keeps live rows fast; the partial index excludes compacted stubs. +CREATE INDEX idx_session_events_live ON session_events(session_id, sequence) WHERE retention_class IS NULL; ``` -**Integrity protocol.** `prev_hash`, `row_hash`, `daemon_signature` are required; `participant_signature` is NULL-able and present only for sensitive events (approvals, policy changes, membership revocations). The canonical serialization (RFC 8785 JCS) and verification order are specified in [Security Architecture § Audit Log Integrity](../security-architecture.md#audit-log-integrity) and [Spec-006 § Integrity Protocol](../../specs/006-session-event-taxonomy-and-audit-log.md#integrity-protocol). +**Integrity protocol.** `prev_hash`, `row_hash`, `daemon_signature` are required; `participant_signature` is NULL-able and present only for sensitive events (approvals, policy changes, membership revocations). For un-compacted rows (`retention_class IS NULL`) the verifier recomputes the per-row chain hash + signature over the live `payload`. After compaction (`retention_class = 'audit_stub'`) the original canonical bytes are discarded, so `row_hash`/`daemon_signature` freeze as a commitment to the (now-gone) pre-compaction state and the **`stub_signature`** authenticates the surviving audit-stub bytes; range existence is additionally witnessed by the covering Merkle anchor in `pending_anchor_uploads` / `event_log_anchors`. The canonical serialization (RFC 8785 JCS) and the full verification order are specified in [Security Architecture § Audit Log Integrity](../security-architecture.md#audit-log-integrity) and [Spec-006 § Integrity Protocol](../../specs/006-session-event-taxonomy-and-audit-log.md#integrity-protocol) + [§ Post-Compaction Integrity](../../specs/006-session-event-taxonomy-and-audit-log.md#post-compaction-integrity). ## Session Snapshots (Plan-001, extended by Plans 006, 015) ```sql -- Owner: Plan-001 | Extended by: Plan-006, Plan-015 CREATE TABLE session_snapshots ( - id TEXT PRIMARY KEY, - session_id TEXT NOT NULL, - as_of_sequence INTEGER NOT NULL, -- snapshot reflects events up to this sequence - state_blob BLOB NOT NULL, -- serialized session state - created_at TEXT NOT NULL, + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + as_of_sequence INTEGER NOT NULL, -- snapshot reflects events up to this sequence (replay-cursor state) + state_blob BLOB NOT NULL, -- serialized session state + created_at TEXT NOT NULL, + -- Compaction hints (Plan-006 Tier 4 Phase 4, migration 0NNN-session-snapshots-compaction-cursor.ts): + -- let replay/projection detect compacted regions without scanning session_events. + has_compacted_ranges INTEGER NOT NULL DEFAULT 0, -- boolean: 0 or 1; whether [.., as_of_sequence] contains audit_stub rows + compacted_range_count INTEGER NOT NULL DEFAULT 0, -- count of distinct compacted ranges reflected in this snapshot FOREIGN KEY (session_id, as_of_sequence) REFERENCES session_events(session_id, sequence) ); @@ -160,6 +169,91 @@ CREATE TABLE driver_capabilities ( refreshed_at TEXT NOT NULL, PRIMARY KEY (driver_name, capability_flag) ); + +-- Owner: Plan-005 +-- Per-tool metadata for the daemon's two-phase command-receipt protocol at +-- crash-recovery dispatch time (idempotency_class lookup without round-tripping +-- the driver per Spec-005:130-132). Normalized per-tool rows mirror the +-- per-flag-row shape of driver_capabilities. +CREATE TABLE driver_tools ( + driver_name TEXT NOT NULL, + tool_name TEXT NOT NULL, + idempotency_class TEXT NOT NULL + CHECK(idempotency_class IN ( + 'idempotent', 'compensable', 'manual_reconcile_only' + )), + description TEXT, + refreshed_at TEXT NOT NULL, + PRIMARY KEY (driver_name, tool_name) +); + +-- Owner: Plan-005 +-- Per-driver capability-contract metadata. The capability cache is keyed by driver_name +-- (driver_capabilities + driver_tools are per-driver children); this parent row holds the +-- single per-driver contract_version so cold-start hydration can reconstruct +-- GetCapabilitiesResult = { capabilities: { flags, contractVersion }, tools } WITHOUT +-- round-tripping the driver (Spec-005:130-132 cache-as-source-of-truth). Distinct from +-- runtime_bindings.contract_version, which records the version bound to a specific run. +CREATE TABLE driver_contract_meta ( + driver_name TEXT PRIMARY KEY, + contract_version TEXT NOT NULL, -- semver of the driver's advertised capability contract + refreshed_at TEXT NOT NULL -- last capability-refresh write (matches driver_capabilities.refreshed_at cadence) +); +``` + +--- + +## Audit Log Crypto Tables (Plan-006) + +```sql +-- Owner: Plan-006 | Migration: 0NNN-daemon-signing-keys.ts (Tier 4 Phase 2) +-- Per-session daemon Ed25519 signing keypair. Private key is sealed via the +-- OS keystore master key (@napi-rs/keyring v1.2.0 per Spec-022:146 — Keychain +-- kSecAttrAccessibleWhenUnlockedThisDeviceOnly on macOS / CRED_TYPE_GENERIC +-- CRED_PERSIST_LOCAL_MACHINE on Windows / Secret Service via libsecret + +-- kwallet6 + keyutils fallback on Linux). Public key is registered in the +-- session participant roster at join time per Spec-006:382. Sealed-key storage +-- lives in local SQLite (NOT shared-Postgres sessions) per ADR-004 SQLite- +-- local-state boundary — daemon-private secrets are per-machine. +CREATE TABLE daemon_signing_keys ( + session_id TEXT PRIMARY KEY, + public_key BLOB NOT NULL, -- Ed25519 32-byte public key + sealed_private_key BLOB NOT NULL, -- Ed25519 private key sealed via OS keystore master key + created_at TEXT NOT NULL, + rotated_at TEXT -- non-NULL when key has been rotated per ADR-010 +); + +-- Owner: Plan-006 | Migration: 0NNN-pending-anchor-uploads.ts (Tier 4 Phase 3) +-- Durable partition-tolerance queue for Merkle anchors awaiting control-plane +-- upload. Unflushed anchors survive daemon restart without re-signing per +-- Plan-006:151. The (session_id, node_id, start_sequence, end_sequence) UNIQUE +-- constraint makes the T3.3 anchorRange() force-fire path (consumed by T3.2 +-- compactor's anchor-before-compaction protocol per Spec-006 §Post-Compaction +-- Integrity) idempotent against re-entry of an identical range (the key dedups +-- genuine re-fires only — coverage semantics in the constraint comment below). +CREATE TABLE pending_anchor_uploads ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + node_id TEXT NOT NULL, + start_sequence INTEGER NOT NULL, + end_sequence INTEGER NOT NULL, + merkle_root BLOB NOT NULL, -- BLAKE3 binary Merkle tree root (RFC 9162 §2.1 odd-leaf duplication) + root_signature BLOB NOT NULL, -- Ed25519 signature over merkle_root by daemon_signing_keys.sealed_private_key + anchored_at TEXT NOT NULL, -- daemon-local timestamp at anchor computation + uploaded_at TEXT, -- non-NULL once control-plane confirms upload to event_log_anchors + -- Durable retry/backoff state (Plan-006:296 partition-anchor-queue durability decision): survives daemon + -- restart so upload retry resumes and the last failure is queryable for operator triage post-restart. + attempt_count INTEGER NOT NULL DEFAULT 0, -- upload attempts since enqueue; drives exponential backoff + last_attempt_at TEXT, -- daemon-local timestamp of most recent upload attempt; NULL until first attempt + last_error TEXT, -- last upload failure detail (operator triage); NULL on success or before first attempt + -- end_sequence is part of the key: a cadence anchor [1,1000] and a wider compaction-covering anchor [1,5000] share start_sequence=1 and MUST coexist. + -- "Covering anchor exists" (Spec-006 §Post-Compaction Integrity step 1) is a COVERAGE query (start_sequence <= range_start AND end_sequence >= range_end), NOT an exact-start match; the key only dedups genuine re-fires of the identical range. + UNIQUE (session_id, node_id, start_sequence, end_sequence) +); + +CREATE INDEX idx_pending_anchor_uploads_pending + ON pending_anchor_uploads(session_id, anchored_at) + WHERE uploaded_at IS NULL; ``` --- diff --git a/docs/architecture/schemas/shared-postgres-schema.md b/docs/architecture/schemas/shared-postgres-schema.md index 9e9c742a..893d9ee7 100644 --- a/docs/architecture/schemas/shared-postgres-schema.md +++ b/docs/architecture/schemas/shared-postgres-schema.md @@ -370,7 +370,11 @@ CREATE TABLE event_log_anchors ( root_signature BYTEA NOT NULL, -- 64 bytes; Ed25519 signature over merkle_root by emitting daemon anchored_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (end_sequence >= start_sequence), - UNIQUE(session_id, node_id, start_sequence) + -- end_sequence is part of the key (mirrors local pending_anchor_uploads): a cadence anchor [1,1000] and a wider + -- compaction-covering anchor [1,5000] share start_sequence=1 and MUST coexist, so the daemon's ON CONFLICT DO NOTHING + -- upload dedups only genuine re-uploads of the identical range. "Covering anchor" at verify time is a coverage test + -- (start_sequence <= range_start AND end_sequence >= range_end) per Spec-006 §Post-Compaction Integrity, not exact-start. + UNIQUE(session_id, node_id, start_sequence, end_sequence) ); CREATE INDEX idx_event_log_anchors_session ON event_log_anchors(session_id, anchored_at DESC); diff --git a/docs/architecture/security-architecture.md b/docs/architecture/security-architecture.md index 8cc51a4e..6152d77e 100644 --- a/docs/architecture/security-architecture.md +++ b/docs/architecture/security-architecture.md @@ -404,19 +404,20 @@ References: ### Verification Rules -A read-side verifier runs three checks, in order. Any failure halts replay and emits `audit_integrity_failed` per [Spec-006 §Integrity Protocol](../specs/006-session-event-taxonomy-and-audit-log.md): +A read-side verifier runs four checks, in order (the fourth applies only to compacted rows). Any failure halts replay and emits `audit_integrity_failed` per [Spec-006 §Integrity Protocol](../specs/006-session-event-taxonomy-and-audit-log.md): -1. **Chain check.** For each row, recompute `BLAKE3(prev_hash || canonical_bytes(row))` and compare to the stored `row_hash`. Mismatch → `audit_integrity_failed { failureKind: 'chain_break' }`. -2. **Signature check.** Verify `daemon_signature` against `canonical_bytes(row)` using the `NodeId`-resolved Ed25519 public key from the session participant roster. If `participant_signature` is present, verify it with the participant's public key. Failure → `audit_integrity_failed { failureKind: 'signature_invalid' }`. -3. **Anchor check.** For each anchored range, recompute the Merkle root from locally stored `row_hash` values and compare to `event_log_anchors.merkle_root`; verify `root_signature` against the same `NodeId`-resolved key. Failure → `audit_integrity_failed { failureKind: 'anchor_mismatch' }`. +1. **Chain check.** For each row where `retention_class IS NULL` (un-compacted), recompute `BLAKE3(prev_hash || canonical_bytes(row))` and compare to the stored `row_hash`. Mismatch → `audit_integrity_failed { failureMode: 'hash_mismatch', failurePath: 'inclusion' }` per Spec-006:433. For rows where `retention_class = 'audit_stub'`, the original canonical bytes have been discarded by compaction, so per-row chain recomputation against the original is impossible — those rows are instead verified by the anchor check (rule 3, original-existence) AND the stub-signature check (rule 4, stub-authenticity) per [Spec-006 §Post-Compaction Integrity](../specs/006-session-event-taxonomy-and-audit-log.md#post-compaction-integrity). +2. **Signature check.** For each `retention_class IS NULL` row, verify `daemon_signature` against `canonical_bytes(row)` using the `NodeId`-resolved Ed25519 public key from the session participant roster. If `participant_signature` is present, verify it with the participant's public key. Failure → `audit_integrity_failed { failureMode: 'signature_mismatch', failurePath: 'signature' }`. For `retention_class = 'audit_stub'` rows, the ORIGINAL canonical bytes are gone — per-row signature verification against the original is skipped; original-range tamper-evidence comes from the Ed25519-signed Merkle root checked in rule 3, and post-compaction stub authenticity comes from the per-row `stub_signature` checked in rule 4. +3. **Anchor check.** For each anchored range, recompute the Merkle root from locally stored `row_hash` values and compare to `event_log_anchors.merkle_root`; verify `root_signature` against the same `NodeId`-resolved key. Failure → `audit_integrity_failed { failureMode: 'anchor_mismatch', failurePath: 'consistency' }`. For ranges entirely composed of `retention_class = 'audit_stub'` rows the same Merkle-root + signature check applies (the stored `row_hash` was frozen pre-compaction by I-006-3-03 chain-commitment-frozen invariant). A **covering** anchor is one whose `[start_sequence, end_sequence]` spans the range under verification (`anchor.start_sequence ≤ range_start AND anchor.end_sequence ≥ range_end` — the same single-anchor coverage test the compactor applies in [Spec-006 §Post-Compaction Integrity](../specs/006-session-event-taxonomy-and-audit-log.md#post-compaction-integrity) step 1, NOT an exact-`start_sequence` match; a cadence anchor and a wider force-fire anchor may share a `start_sequence`). If a compacted range has no covering anchor, → `audit_integrity_failed { failureMode: 'anchor_missing_for_compacted_range' }`; if the covering anchor's signature fails to verify, → `audit_integrity_failed { failureMode: 'anchor_signature_invalid' }`. Additional `failureMode` values per Spec-006:433: `inclusion_proof_failed`, `consistency_proof_failed`, `log_file_missing`, `log_file_moved` cover anchor-substrate failures beyond the core checks. +4. **Stub-signature check** (compacted rows only). For each `retention_class = 'audit_stub'` row, verify the stored `stub_signature` **directly over the canonical byte string stored in `payload`** (the exact bytes the compactor signed at compaction — not re-canonicalized, not reconstructed from the scalar columns) using the same `NodeId`-resolved Ed25519 public key. The anchor (rule 3) commits to the ORIGINAL pre-compaction `row_hash`, not to the post-compaction stub bytes, so without this check a local tamper of the visible stub (`summary`, `actor`, …) would pass rule 3 undetected. A `stub_signature` that fails to verify OR is absent on an `audit_stub` row → `audit_integrity_failed { failureMode: 'stub_signature_invalid', failurePath: 'signature' }` (the signature is REQUIRED on every compacted row; absence is a failure, never a skip). Because the canonical bytes bind `id` + `sequence`, a `stub_signature` cannot be replayed from another row, and a `retention_class` flip in either direction is caught (NULL→stub fails rules 3+4; stub→NULL fails rule 1's chain recomputation against the now-mismatched frozen `row_hash`). **Scalar-column binding (same rule, compacted rows).** The surviving scalar columns (`id`, `session_id`, `sequence`, `occurred_at`, `category`, `type`, `actor`) are a denormalized cache for SQL filters (`idx_session_events_type`) and envelope reconstruction; they are NOT covered by `stub_signature`, which signs only the `payload` projection bytes. So rule 4 additionally decodes the projection from the just-verified `payload` bytes and asserts each scalar column byte-equals its projection counterpart (`occurred_at` ↔ `occurredAt`, `session_id` ↔ `sessionId`, …) — a divergence (e.g. an at-rest edit of `actor`/`type` while `payload` is untouched, which would otherwise pass rules 3+4 yet forge a filter/reconstruction value) → `audit_integrity_failed { failureMode: 'stub_scalar_mismatch', failurePath: 'signature' }`. The signed `payload` projection is the authoritative source for a compacted row's envelope fields. -**Verifier roles.** The three checks above require access to the local event rows on the emitting daemon (or to a peer that has replicated those rows through the relay). A control-plane-only auditor — seeing only `event_log_anchors` metadata, never event payloads, consistent with [ADR-017](../decisions/017-shared-event-sourcing-scope.md) — cannot perform the chain check or per-row signature check. Its available checks reduce to verifying each anchor's `root_signature` against `merkle_root` (using the `NodeId`-resolved Ed25519 key) and confirming anchor-sequence monotonicity per `(session_id, node_id)`. +**Verifier roles.** The three checks above require access to the local event rows on the emitting daemon (or to a peer that has replicated those rows through the relay). A control-plane-only auditor — seeing only `event_log_anchors` metadata, never event payloads or stub bytes, consistent with [ADR-017](../decisions/017-shared-event-sourcing-scope.md) — cannot perform the chain check, the per-row signature check, or the stub-signature check (rule 4 needs the stub rows). Its available checks reduce to verifying each anchor's `root_signature` against `merkle_root` (using the `NodeId`-resolved Ed25519 key) and confirming anchor-sequence monotonicity per `(session_id, node_id)`. Post-compaction, a LOCAL verifier (holding the `audit_stub` rows) retains TWO tamper-evidence shapes — the range-level anchor (rule 3, original-existence) and the per-row `stub_signature` (rule 4, stub-authenticity) — whereas the control-plane-only auditor is limited to the anchor shape alone. This is why the anchor-before-compaction protocol AND the per-row stub commitment per Spec-006 §Post-Compaction Integrity are BOTH load-bearing: the anchor is the only original-existence proof a remote auditor can check, and the `stub_signature` is the only per-row authenticity proof for the bytes that survive locally. The `audit_integrity_failed` event is itself a `session_events` row and is therefore covered by the chain/signature/anchor protocol going forward — a tampered integrity failure cannot be silently appended after the fact. ### Schema Migration -- **Local SQLite** — `session_events` gains four columns: `prev_hash BLOB(32) NOT NULL`, `row_hash BLOB(32) NOT NULL`, `daemon_signature BLOB(64) NOT NULL`, `participant_signature BLOB(64)` (NULL-able). See [Local SQLite Schema](schemas/local-sqlite-schema.md) § Session Events. +- **Local SQLite** — the integrity protocol lands across two migration waves. (a) Plan-001 forward-declares the four integrity columns on `session_events` in the Tier-1 initial migration: `prev_hash BLOB(32) NOT NULL`, `row_hash BLOB(32) NOT NULL`, `daemon_signature BLOB(64) NOT NULL`, `participant_signature BLOB(64)` (NULL-able). (b) Plan-006 then ships its own ADDITIVE Tier-4 migrations for the post-compaction integrity protocol: `session_events.retention_class TEXT` (NULL-able — the `'audit_stub'` discriminator Verification Rules 1/2/4 branch on), `session_events.stub_signature BLOB(64)` (NULL-able — the per-row stub-authenticity signature Rule 4 verifies), the `daemon_signing_keys` table (the sealed-Ed25519 custody store whose public key resolves the Rule 2/3/4 signature checks), and the `pending_anchor_uploads` table (the local queue of Merkle-anchor uploads awaiting replication to the shared `event_log_anchors` table — partition tolerance for the Rule 3 anchor checks). An implementer must run the Plan-006 Tier-4 migrations — not only the Tier-1 forward-declared columns — before the post-compaction verifier can run. See [Local SQLite Schema](schemas/local-sqlite-schema.md) § Session Events. - **Shared Postgres** — a new `event_log_anchors` table is added. It stores anchor metadata only (Merkle roots + signatures), never event payloads. See [Shared Postgres Schema](schemas/shared-postgres-schema.md) § Event Log Anchors. --- diff --git a/docs/backlog.md b/docs/backlog.md index 67d05e5e..bfc864e4 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -168,6 +168,28 @@ The items below were surfaced by the [plan-readiness-audit Tier 1](./operations/ - Exit Criteria: (a) Spec-002 amended with the non-consuming invite-metadata method contract — request keyed on the invite token (or its opaque main-process reference); response carrying display metadata (at minimum the session name + proposed join mode per Spec-002 §Invite Delivery; inviter + expiry as the shape is designed); explicitly NON-consuming (idempotent — the single-use token is not spent by the read); typed errors reusing the §Invite vocabulary for expired / revoked / already-accepted / not-found; (b) Plan-002 (or the consuming tier's plan) carries an implementation task for the endpoint on the daemon-as-gateway surface (ADR-008), mirroring the `invite.accept` wire registration; (c) Plan-023 Tier 8 deep-link rendering consumes it and the two-client manual smoke (former Plan-002 Phase 6 T6.4) passes end-to-end. - Revisit Trigger: Plan-023 Tier 8 deep-link runtime wiring is sequenced for delivery (this endpoint is its precondition); OR a Spec-002 amendment touches the invite read / resolution surface; OR the desktop invite-accept view is reshaped to the pinned opaque-reference + display-metadata posture (Spec-023 §Deep-Link property (b)). +### BL-134: clipanion stable-v4 upgrade + lockfile bump for Plan-007 R3-PR-a CLI + +- Status: `blocked` (until upstream clipanion ships a stable v4.x release) +- Priority: `P3` +- Owner: `unassigned` +- References: [Plan-007 Phase R3 — R3-PR-a CLI Delivery](./plans/007-local-ipc-and-daemon-control.md), [Spec-007 §Delivery Surfaces](./specs/007-local-ipc-and-daemon-control.md), [clipanion GitHub releases](https://github.com/arcanis/clipanion/releases) (upstream tag stream), [Yarn `yarnpkg-cli` package.json](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-cli/package.json) (the production-precedent dependency line `"clipanion": "^4.0.0-rc.2"` — Yarn ships clipanion v4-RC in its production CLI surface, the largest typed-CLI workload in the JS ecosystem; precedent that justified the exact-RC pin for V1 ship) +- Summary: Plan-007 R3-PR-a (CLI delivery — the `sidekicks` first-class delivery track per Spec-007:41) pins `clipanion@4.0.0-rc.4` as an exact-version dependency because as of 2026-05-28 the v4 line has not shipped a stable release (latest tag is `4.0.0-rc.4`; v3.x is the last stable line). The v4 type-safety improvements over v3 (typed positional-argument inference, typed `Command.Option.String`/`Boolean` decorators, drop of the v3 unsafe `@Command.Path` decorator) are load-bearing for the V1 typed CLI commands (`sidekicks daemon start|stop|status` per F-007r-3-09). The exact-RC pin is the hardened choice — caret ranges (`^4.0.0-rc.4`) would auto-upgrade across RC drops with potentially breaking type-API changes between release candidates; an exact pin freezes the surface against an audited RC. When upstream ships a stable `4.x.x` (or `4.0.0` final), bump the pin, run the R3-T1..R3-T8 CLI test slice against the stable build, and ratify the lockfile shift. +- Exit Criteria: (a) Upstream clipanion ships a stable v4 release tag (`4.0.0` or first stable `4.x.x`); (b) Plan-007 R3-PR-a's `apps/cli/package.json` clipanion pin updated from `4.0.0-rc.4` to the stable release (exact-version pin preserved); (c) `pnpm install` regenerates `pnpm-lock.yaml` with the new resolved version; (d) the R3-T1..R3-T8 CLI test slice (per T-007r-3-15 slice-a) passes against the stable build with no migration-only behavior deltas; (e) Plan-007 §Decision Log entry records the RC-to-stable bump + date + any migration notes from the upstream changelog. +- Revisit Trigger: Upstream clipanion stable v4 release announced (the dominant trigger — file the bump PR within one week); OR a Plan-007 R3-PR-a follow-up surfaces an RC-vs-stable behavior delta during V1 hardening (e.g., a regression report against `4.0.0-rc.4` that an RC bump or stable release would fix); OR a subsequent V1 CLI plan (post-Plan-007) requires a clipanion-API surface only exposed by stable v4 (e.g., an `onError` hook or a typed-environment-variable decorator that the RC does not yet expose). + +--- + +### BL-135: Reconcile Plan-025 relay admin-token path against canonical Spec-027 `./data/admin-token` + +- Status: `todo` +- Priority: `P2` +- Owner: `unassigned` +- References: [Plan-025 §File-By-File Plan-Of-Record](./plans/025-self-hostable-node-relay.md), [Spec-027 §Required Behavior](./specs/027-self-host-secure-defaults.md#required-behavior) (row 3 secret set + the Example 1 first-run banner), [Plan-007 §Phase R2](./plans/007-local-ipc-and-daemon-control.md) (conformed to `./data/admin-token` in PR #124), [operations/self-host-secure-defaults.md](./operations/self-host-secure-defaults.md) (ops mirror), PR #124 Codex round-3 finding F (path divergence surfaced) +- Summary: Plan-025 writes the relay admin token to `./data/trust/relay-admin-token` in six places (file-by-file plan, plan-of-record table, build steps 21 + 26, acceptance tests), but the canonical Spec-027 §Required Behavior row 3 + the Example 1 first-run banner specify `./data/admin-token`, and the ops mirror independently confirms `./data/admin-token`. This is **one** token at a divergent path, not two separate files: Spec-027 row 3 enumerates the _complete_ first-run secret set (daemon master key per Spec-022; session-signing key; relay admin token) and the both-services Example 1 banner lists exactly one `Admin token:` line. Plan-007's Tier-4 audit (PR #124, finding F) conformed the daemon-side references to `./data/admin-token`; Plan-025 remains divergent. Resolution is a Spec-027-level canonicalization decision and is therefore deferred to Plan-025's own plan-readiness audit (later tier, not yet audited) rather than fixed by a drive-by edit from a Tier-4 PR. +- Exit Criteria: (a) Spec-027 records the canonical admin-token path as a single decision — EITHER keep `./data/admin-token` (conform Plan-025's six references to it) OR amend Spec-027 to relocate the token under `./data/trust/` alongside the other trust materials (`fingerprint.txt`, `first-run.complete`), in which case Plan-007 §Phase R2, the Spec-027 Example 1 banner, and the ops mirror are all updated to match; (b) every Plan-025 reference uses the canonical path; (c) `docs/architecture/cross-plan-dependencies.md` shared-resource ownership reflects the single owning plan for the admin-token file path; (d) no daemon/relay doc cites a path the canonical spec contradicts. +- Revisit Trigger: Plan-025 plan-readiness audit reaches the `draft → review` gate (the dominant trigger — resolve as part of that audit); OR any Spec-027 amendment touching the `./data/trust/` first-run-ceremony layout; OR a Plan-025 implementation PR is cut before the audit (block it until the path is canonicalized). + --- _Closed items live in [Backlog Archive](./archive/backlog-archive.md)._ diff --git a/docs/plans/003-runtime-node-attach.md b/docs/plans/003-runtime-node-attach.md index bc3ae5e6..4e4f2b30 100644 --- a/docs/plans/003-runtime-node-attach.md +++ b/docs/plans/003-runtime-node-attach.md @@ -426,7 +426,7 @@ Plan-003 implementation lands as a sequence of small PRs at Tier 3. Phases 1–4 - **Files:** `packages/control-plane/src/runtime-nodes/runtime-node-router.factory.ts` (new). - **Step:** Export `createRuntimeNodeRouter(deps)` built on the shared `t` builder from `packages/control-plane/src/sessions/trpc.ts`, with `runtimenode.attach` / `runtimenode.heartbeat` / `runtimenode.capabilityupdate` / `runtimenode.detach` tRPC procedures. Compose it as a sibling `runtimeNodeRouter` merged into the root router in `packages/control-plane/src/server/host.ts` alongside `createSessionRouter` (resolves the "sessionRouter substrate or sibling" choice in favor of the sibling). -- **Contract dependency:** the runtime-node tRPC procedure names + their request/response schemas, and the daemon-side JSON-RPC method namespace (regex-valid dotted-lowercase, since Plan-007's `ipc/registry.ts` enforces `METHOD_NAME_FORMAT` and rejects the underscore `runtime_node.*` style), are ratified by the Tier-3 audit (see §Preconditions). The `runtime_node.*` **event** namespace is distinct from and unaffected by the JSON-RPC **method** namespace. +- **Contract dependency:** the runtime-node tRPC procedure names + their request/response schemas, and the daemon-side JSON-RPC method namespace (regex-valid `dotted-camelCase`, since Plan-007's `ipc/registry.ts` enforces `METHOD_NAME_FORMAT` and rejects the underscore `runtime_node.*` style), are ratified by the Tier-3 audit (see §Preconditions). The `runtime_node.*` **event** namespace is distinct from and unaffected by the JSON-RPC **method** namespace. - **Test:** (enables P1–P10 transport; no standalone assertion.) - **Spec coverage:** Spec-003 line 52 (control plane coordinates discovery/presence; execution stays local). - **Verifies invariant:** (none; transport wiring per CP-003-2.) @@ -446,7 +446,7 @@ Plan-003 implementation lands as a sequence of small PRs at Tier 3. Phases 1–4 - **Files:** `packages/client-sdk/src/runtimeNodeClient.ts` (new). - **Step:** Mirror `packages/client-sdk/src/sessionClient.ts`. Define a `RuntimeNodeClient` interface exposing `attach`, `heartbeat`, `capabilityUpdate`, `detach`. Export `createDaemonRuntimeNodeClient(client: JsonRpcClient): RuntimeNodeClient` — each method calls `client.call(, request, , )` using the Phase-1 Zod schemas (`RuntimeNodeAttachRequest/Response`, `RuntimeNodeHeartbeat*`, `RuntimeNodeCapabilityUpdate*`, `RuntimeNodeDetach*`). `JsonRpcClient.call` requires a result schema — there is no void overload (see the three `sessionClient.ts` call sites) — so `heartbeat` and `detach` pass their Phase-1 no-content schemas (`RuntimeNodeHeartbeatResponseSchema` / `RuntimeNodeDetachResponseSchema`, both `z.null()`, validating the JSON-RPC `result: null`), while `attach` and `capabilityUpdate` pass their content response schemas. Export `createControlPlaneRuntimeNodeClient(fetcher)` for the tRPC transport, mirroring the `sessionClient.ts` control-plane factory and binding the `runtimenode.*` procedure paths from the sibling `runtimeNodeRouter` (T3.8). Carry a file-header `Spec coverage:` JSDoc block matching the `sessionClient.ts` precedent. -- **Contract dependency:** the daemon-side JSON-RPC method-name constants (regex-valid dotted-lowercase per the Phase-2/Phase-3 registry) and the control-plane tRPC procedure paths are ratified by the Tier-3 audit (see §Preconditions); do not author literal wire strings until that namespace lands. Control-plane router placement is already resolved — the SDK binds the sibling `runtimeNodeRouter` composed in T3.8 (no `sessionRouter` extension). +- **Contract dependency:** the daemon-side JSON-RPC method-name constants (regex-valid `dotted-camelCase` per the Phase-2/Phase-3 registry) and the control-plane tRPC procedure paths are ratified by the Tier-3 audit (see §Preconditions); do not author literal wire strings until that namespace lands. Control-plane router placement is already resolved — the SDK binds the sibling `runtimeNodeRouter` composed in T3.8 (no `sessionRouter` extension). - **Test:** (factory surface; exercised by T4.2–T4.4 — no standalone assertion.) - **Spec coverage:** Spec-003 line 69 (RuntimeNodeAttach fields), line 70 (RuntimeNodeHeartbeat updates presence and health), line 71 (RuntimeNodeCapabilityUpdate add/remove/health variants), line 72 (RuntimeNodeDetach retires a node). - **Verifies invariant:** (none directly; SDK transport wrapper.) diff --git a/docs/plans/005-provider-driver-contract-and-capabilities.md b/docs/plans/005-provider-driver-contract-and-capabilities.md index 5cd57f5c..5bdf5cf0 100644 --- a/docs/plans/005-provider-driver-contract-and-capabilities.md +++ b/docs/plans/005-provider-driver-contract-and-capabilities.md @@ -8,94 +8,453 @@ | **Date** | `2026-04-14` | | **Author(s)** | `Codex` | | **Spec** | [Spec-005: Provider Driver Contract And Capabilities](../specs/005-provider-driver-contract-and-capabilities.md) | -| **Required ADRs** | [ADR-005](../decisions/005-provider-drivers-use-a-normalized-interface.md), [ADR-015](../decisions/015-v1-feature-scope-definition.md) | -| **Dependencies** | [Plan-024](./024-rust-pty-sidecar.md) (consumes `PtyHost` contract from `packages/contracts/src/pty-host.ts`) | +| **Required ADRs** | [ADR-005](../decisions/005-provider-drivers-use-a-normalized-interface.md), [ADR-011](../decisions/011-generic-intervention-dispatch.md), [ADR-015](../decisions/015-v1-feature-scope-definition.md), [ADR-017](../decisions/017-shared-event-sourcing-scope.md) | +| **Dependencies** | [Plan-001](./001-shared-session-core.md) (consumes migration runner substrate from Phase 3, shipped; consumes branded `SessionId` / `RunId` / `ChannelId` from `packages/contracts/src/session.ts` Phase 2, shipped), [Plan-007-partial](./007-local-ipc-and-daemon-control.md) (consumes JSON-RPC client transport `packages/client-sdk/src/transport/jsonRpcClient.ts` Phase 3, shipped; consumes the registry's typed surface for `driver.*` namespace registration per CP-007-6 reciprocal), [Plan-024](./024-rust-pty-sidecar.md) (consumes `PtyHost` contract from `packages/contracts/src/pty-host.ts` Phase 2, shipped) | | **Cross-Plan Deps** | [Cross-Plan Dependency Graph](../architecture/cross-plan-dependencies.md) | -| **References** | [ADR-011](../decisions/011-generic-intervention-dispatch.md) (generic intervention dispatch), [Updated Spec-005](../specs/005-provider-driver-contract-and-capabilities.md) (applyIntervention, 7 capability flags) | +| **References** | [Updated Spec-005](../specs/005-provider-driver-contract-and-capabilities.md) (applyIntervention, 7 capability flags, idempotency_class), [api-payload-contracts.md §Plan-005](../architecture/contracts/api-payload-contracts.md), [error-contracts.md driver-namespace codes](../architecture/contracts/error-contracts.md) | ## Goal -Implement the normalized provider driver contract, capability registry, and runtime binding persistence. +Implement the normalized provider driver contract, capability registry, and runtime binding persistence. Drivers execute within the local daemon as the execution authority (per Spec-005:42); the contract is the boundary between the session domain and provider-specific lifecycles. Phase 4 SDK exposure ships under a dedicated `driver.*` JSON-RPC namespace registered through Plan-007's namespace registry. ## Scope -This plan covers shared driver interfaces, two initial drivers, capability refresh, and recovery binding storage. +This plan covers (a) the shared `ProviderDriver` interface + 7-flag capability schema + per-tool `idempotency_class` metadata, (b) the in-daemon provider registry + runtime-binding persistence (`runtime_bindings`, `driver_capabilities` SQLite tables), (c) two initial drivers (Codex, Claude) implemented against the contract as local-runtime-node integrations, (d) capability refresh + recovery-aware binding storage extension points, and (e) typed SDK exposure of capability + intervention surfaces with degraded-fallback semantics (`status: "applied" | "degraded"`). ## Non-Goals -- Multi-agent workflow semantics -- Provider-specific UI tuning beyond capability exposure -- Support for every future provider in the first pass -- Shared hosted execution drivers +- Multi-agent workflow semantics (Plan-009) +- Provider-specific UI tuning beyond capability exposure (out-of-scope for V1) +- Support for every future provider in the first pass (post-V1) +- Shared hosted execution drivers — drivers execute within the local daemon per Spec-005:42 + ADR-005 ## Preconditions - [x] Paired spec is approved -- [x] Required ADRs are accepted +- [x] Required ADRs are accepted (verified: ADR-005 `accepted`, ADR-011 `accepted`, ADR-015 `accepted`, ADR-017 `accepted`) - [x] Blocking open questions are resolved or explicitly deferred +- [x] Plan-001 Phase 3 migration runner shipped +- [x] Plan-001 Phase 2 branded-id contracts shipped +- [x] Plan-024 Phase 2 `PtyHost` contract shipped +- [x] Plan-007-partial Phase 3 JSON-RPC client transport shipped +- [x] Plan-readiness audit passed (Tier 4 audit, 2026-05-27) Target paths below assume the canonical implementation topology defined in [Container Architecture](../architecture/container-architecture.md). ## Target Areas -- `packages/contracts/src/provider-driver.ts` -- `packages/runtime-daemon/src/provider/provider-registry.ts` -- `packages/runtime-daemon/src/provider/runtime-binding-store.ts` -- `packages/runtime-daemon/src/provider/drivers/codex/` -- `packages/runtime-daemon/src/provider/drivers/claude/` -- `packages/client-sdk/src/providerClient.ts` +Phase 1 (contracts): + +- `packages/contracts/src/provider-driver.ts` — typed `ProviderDriver` interface + 7-flag `DriverCapabilityFlag` literal-union + `DriverCapabilities` shape ({flags, contractVersion}) + `IdempotencyClass` enum + `ProviderToolMetadataSchema` with `ProviderToolMetadata` (ingress `z.input`) + `NormalizedProviderToolMetadata` (`z.output`) shapes + `GetCapabilitiesResult` wrapper ({capabilities, tools}) + `ApplyInterventionParams` + `DriverInterventionResultSchema` (Zod) + `DriverResumeResultSchema` (Zod discriminated union) + +Phase 2 (runtime persistence): + +- `packages/runtime-daemon/src/provider/provider-registry.ts` — in-daemon registry + capability cache +- `packages/runtime-daemon/src/provider/runtime-binding-store.ts` — `runtime_bindings` SQLite CRUD (Plan-005 owns; Plan-015 extends per CP-005-1) +- `packages/runtime-daemon/src/provider/driver-capabilities-writer.ts` — `driver_capabilities` SQLite writer +- `packages/runtime-daemon/src/migrations/NNNN-runtime-bindings.ts` — SQLite migration + +Phase 3 (driver implementations): + +- `packages/runtime-daemon/src/provider/drivers/codex/index.ts` — Codex driver entry +- `packages/runtime-daemon/src/provider/drivers/codex/lifecycle.ts` — createSession / resumeSession / startRun / interruptRun / closeSession +- `packages/runtime-daemon/src/provider/drivers/codex/intervention.ts` — applyIntervention generic dispatcher (steer / interrupt / cancel) +- `packages/runtime-daemon/src/provider/drivers/codex/capabilities.ts` — capability declaration + refresh +- `packages/runtime-daemon/src/provider/drivers/codex/tools.ts` — per-tool `idempotency_class` metadata +- `packages/runtime-daemon/src/provider/drivers/codex/event-normalizer.ts` — provider-native → normalized event mapping +- `packages/runtime-daemon/src/provider/drivers/claude/{index,lifecycle,intervention,capabilities,tools,event-normalizer}.ts` — symmetric file layout + +Phase 4 (SDK exposure): + +- `packages/runtime-daemon/src/ipc/handlers/driver-handlers.ts` — daemon-side `driver.*` JSON-RPC handlers (7 client-facing: 6 non-lifecycle verbs + `driver.subscribeEvents`; the 4 session/run lifecycle ops are daemon-internal, orchestration-invoked — see §Phase 4 decision #2) +- `packages/client-sdk/src/providerClient.ts` — `createDaemonProviderClient(JsonRpcClient): DriverClient` factory +- `packages/client-sdk/src/providerClient.test.ts` — SDK conformance + degraded-fallback unit tests ## Data And Storage Changes -- Add local `runtime_bindings` and `driver_capabilities` persistence. -- See [Local SQLite Schema](../architecture/schemas/local-sqlite-schema.md) for column definitions. +Four SQLite tables ship in Phase 2 (Plan-005-owned per cross-plan-dependencies.md §1): + +- `runtime_bindings` — persists driver-instance ↔ session/run bindings. Plan-015 extends with recovery-aware persistence methods (per CP-005-1; row-level recovery state lives in Plan-015's dedicated `recovery_checkpoints` table, not on `runtime_bindings`). +- `driver_capabilities` — caches the 7-flag capability matrix per active driver instance, refreshed on capability change events. +- `driver_tools` — per-tool metadata keyed `(driver_name, tool_name)`, persisting each tool's `idempotency_class` so the daemon's two-phase command-receipt protocol (Spec-005:130-132) resolves crash-recovery dispatch class without round-tripping the driver. T2.4 hydrates it into `listCapabilities()` after restart. +- `driver_contract_meta` — per-driver parent row (`driver_name` PK) holding the advertised `contract_version` that cold-start cache hydration (T2.4) reconstructs into `GetCapabilitiesResult.capabilities.contractVersion` without round-tripping the driver (the cache-as-source-of-truth path). + +See [Local SQLite Schema §Driver and Runtime Binding Tables](../architecture/schemas/local-sqlite-schema.md#driver-and-runtime-binding-tables-plan-005) for column definitions (all four rows carry the `Owner: Plan-005` annotation). ## API And Transport Changes -- Add typed driver capability and driver runtime events to the client SDK. -- Define internal driver interface for 10 operations: create, resume, start, interrupt, respond, close, applyIntervention, listModels, listModes, getCapabilities. +- Add typed driver capability + driver runtime events to the client SDK under a dedicated `driver.*` JSON-RPC namespace registered through Plan-007's namespace registry (per CP-007-6 reciprocal). +- Define the internal driver interface for 10 operations (canonical long-form names per [api-payload-contracts.md:546](../architecture/contracts/api-payload-contracts.md) `interface ProviderDriver`): `createSession`, `resumeSession`, `startRun`, `interruptRun`, `applyIntervention`, `respondToRequest`, `closeSession`, `listModels`, `listModes`, `getCapabilities`. +- Add three driver-namespace error codes to the JSON-RPC error registry (already present at [error-contracts.md](../architecture/contracts/error-contracts.md) lines 183-185): `driver.unavailable` (503), `driver.capability_unsupported` (400 — note that this is the SDK seam's wire-error for invocation of an undeclared capability, distinct from the per-call `degraded` status returned by `applyIntervention` per Spec-005:44; the two are not in conflict — `driver.capability_unsupported` covers the static "this driver does not support that capability at all" gate, `degraded` covers the dynamic "this intervention type is not supported but a fallback action is available" outcome). +- SDK seam Zod schemas: `DriverInterventionResultSchema = z.object({ status: z.enum(['applied', 'degraded']), fallbackAction: z.string().optional() })` (per Phase 4 ratified envelope shape; see Phase 4 below). +- SDK factory pattern: daemon-only `createDaemonProviderClient(JsonRpcClient): DriverClient`. No control-plane variant per Spec-005:42 (drivers are local-daemon-resident; no V1.1 remote-provider workflow on roadmap). + +## Implementation Phase Sequence + +### Phase 1 — Driver contract + capability schema + idempotency_class + +**Goal:** Author the typed driver-contract surface (10-op interface + 7-flag capability schema + per-tool `idempotency_class` metadata + `ApplyInterventionParams` + Zod-validated `DriverInterventionResult` shape). + +#### Tasks + +- **T1.1** — Author the 10-operation `ProviderDriver` interface in `packages/contracts/src/provider-driver.ts`. + - Files: `packages/contracts/src/provider-driver.ts` (new) + - Spec coverage: Spec-005:41 (normalized contract), :43 (10-op surface), :67 (Required driver operations anchor) + - Verifies invariant: I-005-1 (driver authority remains local) + - Consumes: `SessionId` / `RunId` / `ChannelId` from `packages/contracts/src/session.ts` (Plan-001 Phase 2, Tier 1 — shipped); `PtyHost` contract from `packages/contracts/src/pty-host.ts` (Plan-024 Phase 2, Tier 1 — shipped); typed shape at api-payload-contracts.md:546 (`interface ProviderDriver`) + - Canonical method names (long-form): `createSession`, `resumeSession`, `startRun`, `interruptRun`, `applyIntervention`, `respondToRequest`, `closeSession`, `listModels`, `listModes`, `getCapabilities` + - Non-trivial return shapes (authored in companion tasks): `resumeSession` → `Promise` (T1.6); `applyIntervention` → `Promise` (T1.4); `getCapabilities` → `Promise` (T1.3). + - Estimate: 1 PR + +- **T1.2** — Author the 7-flag `DriverCapabilityFlag` literal-union type and `DriverCapabilities` shape in `packages/contracts/src/provider-driver.ts`. + - Files: `packages/contracts/src/provider-driver.ts` (extend T1.1) + - Spec coverage: Spec-005:46 (7-flag enumeration + `pause` exclusion rationale per ADR-011), :48 (undeclared = unsupported) + - Verifies invariant: I-005-2 (undeclared capability = unsupported) + - Consumes: api-payload-contracts.md:168 (`type DriverCapabilityFlag` anchor), :633 (`interface DriverCapabilities` anchor) + - Flags (7): `resume`, `steer`, `interactive_requests`, `mcp`, `tool_calls`, `reasoning_stream`, `model_mutation`. `pause` is intentionally excluded per Spec-005:46 + ADR-011. + - Estimate: 1 PR (combined with T1.1) + +- **T1.3** — Author the per-tool `IdempotencyClass` enum, `ProviderToolMetadata` shape, and `GetCapabilitiesResult` wrapper in `packages/contracts/src/provider-driver.ts`. Keep `DriverCapabilities` semantically pure (`{flags, contractVersion}`); introduce `GetCapabilitiesResult` as the typed return of `getCapabilities()` so per-tool metadata travels alongside whole-driver capability flags in a single round-trip without conflating the two concepts. + - Files: `packages/contracts/src/provider-driver.ts` (extend T1.1/T1.2); `docs/architecture/contracts/api-payload-contracts.md` (doc-mirror update at line 633 — leave `interface DriverCapabilities` shape unchanged; add new `interface ProviderToolMetadata` (ingress) + `interface NormalizedProviderToolMetadata` (normalized) + `interface GetCapabilitiesResult` shapes immediately after) + - Spec coverage: Spec-005:49 (three-value enum required), :116-118 (semantic separation between Driver capabilities and Tool metadata), :128 (`manual_reconcile_only` conservative default); Spec-015:108 (`### Idempotency Classes and Recovery Behavior` — recovery dispatch consumer), Spec-015:120 (`manual_reconcile_only` recovery-needed handoff) + - Verifies invariant: I-005-3 (undeclared `idempotency_class` defaults to `manual_reconcile_only`) + - Consumes: Spec-005 §Tool Metadata anchor; Spec-015 §Idempotency Protocol + - Provides: `IdempotencyClass = "idempotent" \| "compensable" \| "manual_reconcile_only"`; `ProviderToolMetadataSchema = z.object({ name: z.string(), idempotency_class: IdempotencyClassSchema.optional().default("manual_reconcile_only"), description: z.string().optional() })`; `ProviderToolMetadata = z.input` (INGRESS — `idempotency_class` OPTIONAL: what a driver declares via `getCapabilities()`; an omitting driver is accepted, NOT rejected, since a required field would reject a conformant-but-silent driver before the default could apply per Spec-005:128); `NormalizedProviderToolMetadata = z.output` (NORMALIZED — `idempotency_class` REQUIRED after the schema's `.default("manual_reconcile_only")` applies at parse time; the only tool-metadata shape that crosses the persistence / event-payload boundary, so the type system forbids persisting or emitting an un-normalized value); `GetCapabilitiesResult = { capabilities: DriverCapabilities; tools: ProviderToolMetadata[] }` (ingress wrapper — semantically aligned with Spec-005:116-118 and MCP 2026 / LSP capability-vs-tool-list separation precedent) + - Modern-precedent rationale: MCP 2026-07-28 release candidate separates `initialize` server-capabilities response from `tools/list` request; LSP separates `ServerCapabilities` from registered tool surfaces. The wrapper return preserves the 10-op interface count from Spec-005:67 while honoring the two-concept separation. + - Estimate: 1 PR + +- **T1.4** — Author `ApplyInterventionParams` + Zod-validated `DriverInterventionResultSchema` in `packages/contracts/src/provider-driver.ts`. + - Files: `packages/contracts/src/provider-driver.ts` (extend T1.1) + - Spec coverage: Spec-005:44 (generic dispatcher + degraded-fallback contract); ADR-011 (generic intervention dispatch) + - Verifies invariant: I-005-4 (`applyIntervention` returns `degraded` for unsupported intervention types) + - Consumes: api-payload-contracts.md:581 (`interface ApplyInterventionParams` anchor), :602 (`interface InterventionDriverResult` anchor), :142 (cross-cutting `InterventionType` enum — resolution: co-locate the `InterventionType` re-export in `provider-driver.ts` since Plan-004's `runControl.ts` ships at Tier 5; Plan-004 imports the enum from Plan-005 at Tier 4) + - Provides: `DriverInterventionResultSchema = z.object({ status: z.enum(['applied', 'degraded']), fallbackAction: z.string().optional() })`; `DriverInterventionResult = z.infer` (Zod-validated wire envelope per Phase 4 ratified shape; distinct from orchestration `InterventionState` lifecycle enum per LSP/MCP separation-of-concerns precedent) + - Estimate: 1 PR + +- **T1.5** — Author contract-conformance test scaffolding in `packages/contracts/src/__tests__/provider-driver.test.ts`. + - Files: `packages/contracts/src/__tests__/provider-driver.test.ts` (new) + - Spec coverage: Spec-005 AC1 (Spec-005:155 — type-only conformance: a mock driver implementing the interface should compile and not require session-domain changes); Spec-005 AC2 (Spec-005:156 — capability-flag exhaustiveness: a flag not in the 7-flag enum should produce a type error) + - Verifies invariant: I-005-1, I-005-2, I-005-3, I-005-4, I-005-5 + - Notes: Phase 1 tests are type-system tests + a minimal mock-driver scaffold. Behavioral conformance tests (resume-handle round-trip, capability refresh) ship in Phase 2-3. Type-level enforcement of I-005-5 is verified here: a mock driver whose `resumeSession` returns `void` on the failure path must produce a TS compile error against the T1.6 `DriverResumeResult` discriminated-union return type. + - Estimate: 1 PR + +- **T1.6** — Author the `DriverResumeResult` discriminated-union return shape for `resumeSession()` in `packages/contracts/src/provider-driver.ts`. Pattern matches existing `DriverInterventionResult` (T1.4): a `status`-discriminated union with explicit success and failure variants, validated via Zod. + - Files: `packages/contracts/src/provider-driver.ts` (extend T1.1/T1.4); `docs/architecture/contracts/api-payload-contracts.md` (doc-mirror — add `interface DriverResumeResult` near the existing `DriverInterventionResult` shape) + - Spec coverage: Spec-005:60 (resume-handle failure surfaces `provider failure` detail + `recovery-needed` condition; MUST NOT silently create replacement provider session); Spec-005:157 (AC3) + - Verifies invariant: I-005-5 (the discriminated-union shape makes silent-replacement structurally inexpressible — the failure variant has no `bindingId` field, so a successful resume cannot be conflated with a failed one) + - Consumes: T1.1 `resumeSession` signature anchor; api-payload-contracts.md:602 `interface InterventionDriverResult` precedent (sibling shape) + - Provides: `DriverResumeResultSchema = z.discriminatedUnion('status', [z.object({ status: z.literal('resumed'), bindingId: z.string() }), z.object({ status: z.literal('failed'), recoveryCondition: z.literal('recovery-needed'), providerFailureDetail: z.string() })])`; `DriverResumeResult = z.infer`. `resumeSession()` signature becomes `(handle: string) => Promise` rather than the (Spec-005-silent) implicit throw-on-failure pattern. Timestamps live on `runtime_bindings.updated_at` (T2.1); the result shape carries only the discriminated-union semantic payload. + - Modern-precedent rationale: Discriminated-union result types (success | failure) over thrown exceptions for expected-failure paths — the pattern used by Rust's `Result` and by API SDKs that prefer structured error responses (Stripe SDK, AWS SDK) over throwing. Matches existing `DriverInterventionResult` (`{status: 'applied' | 'degraded'}`) precedent in this contract. + - Estimate: 1 PR (combined with T1.4 since both author Zod-validated result discriminated unions) + +**Phase 1 Acceptance Mapping:** T1.5 → Spec-005:155 (AC1) + Spec-005:156 (AC2). AC3 (Spec-005:157 — recovery-needed) is verified in Phase 3 (T3.1 Codex / T3.6 Claude lifecycle resume-failure tests) where `resumeSession()` returns the structured failure result, and in Phase 4 (T4.7 return-value contract test). + +### Phase 2 — Provider registry + runtime-binding store + SQLite + +**Goal:** Author the in-daemon provider registry, the `RuntimeBindingStore` CRUD class, the `driver_capabilities` cache writer, and the SQLite migration that materializes the two tables. + +#### Tasks + +- **T2.1** — Author the SQLite migration creating `runtime_bindings` + `driver_capabilities`. + - Files: `packages/runtime-daemon/src/migrations/NNNN-runtime-bindings.ts` (new; NNNN is the next free migration number at PR-author time); `docs/architecture/schemas/local-sqlite-schema.md` (verify column definitions match at line 137 `runtime_bindings` block) + - Spec coverage: Spec-005:43 (driver-contract operations imply persistence of provider session handles), :47 (drivers persist provider-owned resume handles separately from canonical ids) + - Verifies invariant: — (migration is structural; invariants apply at runtime) + - Consumes: Plan-001 Phase 3 migration runner (`packages/runtime-daemon/src/migrations/runner.ts`, shipped) + - Provides: `runtime_bindings` table (`id` TEXT PK, `run_id` TEXT NOT NULL, `driver_name` TEXT NOT NULL, `contract_version` TEXT NOT NULL, `resume_handle` TEXT nullable, `runtime_metadata` TEXT NOT NULL DEFAULT `'{}'`, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL; index `idx_runtime_bindings_run` on `run_id`) + `driver_capabilities` table (`driver_name` TEXT NOT NULL, `capability_flag` TEXT NOT NULL CHECK IN (`'resume'`, `'steer'`, `'interactive_requests'`, `'mcp'`, `'tool_calls'`, `'reasoning_stream'`, `'model_mutation'`), `supported` INTEGER NOT NULL DEFAULT 0, `refreshed_at` TEXT NOT NULL, PRIMARY KEY (`driver_name`, `capability_flag`)) + `driver_tools` table (`driver_name` TEXT NOT NULL, `tool_name` TEXT NOT NULL, `idempotency_class` TEXT NOT NULL CHECK IN (`'idempotent'`, `'compensable'`, `'manual_reconcile_only'`), `description` TEXT nullable, `refreshed_at` TEXT NOT NULL, PRIMARY KEY (`driver_name`, `tool_name`)) + `driver_contract_meta` table (`driver_name` TEXT PRIMARY KEY, `contract_version` TEXT NOT NULL, `refreshed_at` TEXT NOT NULL). Migration matches canonical [local-sqlite-schema.md §Driver and Runtime Binding Tables](../architecture/schemas/local-sqlite-schema.md#driver-and-runtime-binding-tables-plan-005) exactly for `runtime_bindings` + `driver_capabilities`; `driver_tools` is the new co-owned per-tool-metadata surface (crash-recovery `idempotency_class` lookup per Spec-005:130-132 cannot round-trip the driver, so per-tool `idempotency_class` is persisted as normalized per-tool rows rather than a JSON blob — `(driver_name, tool_name)` composite PK mirrors the `(driver_name, capability_flag)` shape of `driver_capabilities`); `driver_contract_meta` is the per-driver parent holding the single `contract_version` that cold-start hydration (T2.4) needs to reconstruct `GetCapabilitiesResult.capabilities.contractVersion` without round-tripping the driver — kept out of `driver_capabilities` to avoid denormalizing the version across the per-flag rows, and distinct from the per-run `runtime_bindings.contract_version`. No FK to non-existent local `sessions` table (sessions is shared-Postgres-only per [shared-postgres-schema.md:43](../architecture/schemas/shared-postgres-schema.md)); session-level lookups join through `runs.session_id` at the higher layer. + - Note: `recovery_state` / `recovery_needed` columns do NOT belong on `runtime_bindings` — Plan-015 owns the `recovery_checkpoints` table at `local-sqlite-schema.md:791` for row-level recovery state per CP-005-1 (recovery-aware persistence extension contract). Plan-005's invariant I-005-5 (resume-failure surfaces recovery-needed) is satisfied via a typed return-value contract from `resumeSession()` (T3.1, T3.6), which Plan-015's recovery dispatcher consumes and surfaces on the existing `run.failed` event with `recoveryCondition: 'recovery-needed'` per Spec-006:179 — no separate driver event, no persisted column state. + - Estimate: 1 PR + +- **T2.2** — Author `RuntimeBindingStore` CRUD class. + - Files: `packages/runtime-daemon/src/provider/runtime-binding-store.ts` (new) + - Spec coverage: Spec-005:47 (provider-owned resume handles persisted separately from session/run ids) + - Verifies invariant: I-005-1 (driver authority local — store is daemon-resident) + - Consumes: T2.1 migration; better-sqlite3 client (existing in `packages/runtime-daemon/src/db/`) + - Provides: `RuntimeBindingStore` with methods `create(input)`, `findById(id)`, `findByRun(runId)`, `update(id, patch)`, `delete(id)`. Session-level lookups (when a caller wants every binding for a session) join through `runs.session_id` at the higher layer — `runtime_bindings` has no direct `session_id` column per canonical schema. Extension points for Plan-015 per CP-005-1 are declared as public-but-not-overridden method seams (e.g., `findResumableBindings(...)` is intentionally exposed as a queryable surface that Plan-015 extends with recovery-aware predicates). + - Estimate: 1-2 PRs + +- **T2.3** — Author `ProviderRegistry` class. + - Files: `packages/runtime-daemon/src/provider/provider-registry.ts` (new) + - Spec coverage: Spec-005:41 (every provider integration must implement a normalized driver contract), :48 (runtime treats undeclared capabilities as unsupported) + - Verifies invariant: I-005-2 (undeclared capability = unsupported — enforced by registry capability check) + - Consumes: T1.1-T1.4 contracts; T2.2 RuntimeBindingStore + - Provides: `ProviderRegistry` with `register(driverInstance)`, `lookup(driverId)`, `checkCapability(driverId, flag)`, `listAvailable()`. Registry enforces capability-flag gating ONLY for direct capability-bound methods (e.g. a future `driver.steer` direct entrypoint, or `driver.getCapabilities` itself when called against an unregistered driver) — `applyIntervention` is **excluded from pre-dispatch gating** because its intervention-type-aware degraded-fallback per [ADR-011](../decisions/011-generic-intervention-dispatch.md) must reach the driver to return `{ status: 'degraded', fallbackAction }` (Spec-005:44). Direct capability-bound calls against undeclared flags throw `driver.capability_unsupported` (error-contracts.md:184) before reaching the driver; `applyIntervention` reaches the driver regardless of `(type → capability flag)` mapping so the driver-side dispatcher can return the typed degraded envelope (T3.7 verifies this for Claude `steer: false`). + - Estimate: 1 PR + +- **T2.4** — Author `driver_capabilities` cache writer logic. + - Files: `packages/runtime-daemon/src/provider/driver-capabilities-writer.ts` (new) + - Spec coverage: Spec-005:48 (undeclared capabilities = unsupported — drives the cache-as-source-of-truth pattern); ADR-005 (normalized driver interface) + - Verifies invariant: I-005-2 + - Consumes: T1.2 DriverCapabilities shape; T2.1 `driver_capabilities` + `driver_tools` + `driver_contract_meta` tables; T2.3 ProviderRegistry + - Provides: Writes per-driver capability data on driver registration + on capability-refresh events, in one transaction across THREE per-driver-keyed tables: (1) per-flag rows to `driver_capabilities` (one row per `DriverCapabilityFlag` enum value with `supported INTEGER 0/1` and `refreshed_at`); (2) per-tool rows to `driver_tools` (one row per `NormalizedProviderToolMetadata` — the post-`.default()` shape, since the NOT NULL `idempotency_class` CHECK-enum column structurally rejects an un-normalized value — plus optional `description` and `refreshed_at`) so the daemon's two-phase command-receipt protocol (Spec-005:130-132) can read `idempotency_class` at crash-recovery dispatch time without round-tripping the driver; (3) ONE row to `driver_contract_meta` (`driver_name` PK, `contract_version`, `refreshed_at`) holding the driver's advertised capability-contract version. The contract version lives in its own per-driver table — NOT denormalized across the per-flag `driver_capabilities` rows, and distinct from `runtime_bindings.contract_version` (which is the version bound to a specific RUN, not the driver's advertised cache version). Reads on cold-start cache hydration join all three tables keyed by `driver_name` to reconstruct `GetCapabilitiesResult = { capabilities: { flags, contractVersion }, tools: ProviderToolMetadata[] }` — `contractVersion` comes from `driver_contract_meta`, so `listCapabilities()` returns the advertised version after a daemon restart WITHOUT round-tripping the driver (the cache-as-source-of-truth path). Refresh trigger emits `runtime_node.capability_updated` event per Spec-006:376 (existing event surface; payload `previousState` / `newState` carry the reconstructed wrapper-shape contents) — see CP-005-5. + - Estimate: 1 PR + +- **T2.5** — Author Phase 2 integration tests (RuntimeBindingStore + ProviderRegistry + capability-writer round-trip). + - Files: `packages/runtime-daemon/src/provider/__tests__/phase-2-integration.test.ts` (new) + - Spec coverage: Spec-005:48 (capability gating); Spec-005:47 (resume-handle persistence) + - Verifies invariant: I-005-1, I-005-2 + - Notes: Tests use the real SQLite client (better-sqlite3 in-memory) per the no-mock-database policy (test-engineering memory). Migration applied at test setup. + - Estimate: 1 PR + +**Phase 2 Acceptance Mapping:** T2.5 → Spec-005:156 (AC2 — capability gating verified end-to-end against a real registry + store). + +### Phase 3 — Codex + Claude driver implementations + +**Goal:** Implement the two initial drivers (Codex, Claude) against the Phase 1 contract. Both drivers execute as local-runtime-node integrations per Spec-005:42 (no shared hosted driver service). Codex consumes the Plan-024 `PtyHost` contract for terminal-attached lifecycle. + +**Driver capability matrix (V1 declared values per Spec-005 §Per-Driver Capability Matrix):** + +| Capability | Codex | Claude | +| ---------------------- | ------- | ------- | +| `resume` | `true` | `true` | +| `steer` | `true` | `false` | +| `interactive_requests` | `true` | `true` | +| `mcp` | `true` | `true` | +| `tool_calls` | `true` | `true` | +| `reasoning_stream` | `false` | `true` | +| `model_mutation` | `false` | `true` | + +`pause` is intentionally absent from both — orchestration-layer construct per Spec-005:46. + +#### Tasks + +- **T3.1** — Author Codex driver entry + lifecycle methods. + - Files: `packages/runtime-daemon/src/provider/drivers/codex/index.ts` (new), `packages/runtime-daemon/src/provider/drivers/codex/lifecycle.ts` (new) + - Spec coverage: Spec-005:43 (10-op contract surface); Spec-005:60 (resume-handle failure surfaces recovery-needed condition — AC3) + - Verifies invariant: I-005-5 (resume-failure returns typed `recovery-needed` condition; no silent session replacement) + - Consumes: T1.1 ProviderDriver interface; Plan-024 PtyHost contract from `packages/contracts/src/pty-host.ts` (Tier 1, shipped); Codex /resume substrate (PtyHost-based persistent process) + - Provides: `createSession`, `resumeSession`, `startRun`, `interruptRun`, `closeSession` for Codex + - Estimate: 2 PRs + +- **T3.2** — Author Codex intervention dispatcher. + - Files: `packages/runtime-daemon/src/provider/drivers/codex/intervention.ts` (new) + - Spec coverage: Spec-005:44 (generic dispatcher + degraded-fallback); ADR-011 (generic intervention dispatch) + - Verifies invariant: I-005-4 (degraded result for unsupported intervention types) + - Consumes: T1.4 ApplyInterventionParams + DriverInterventionResultSchema; T1.2 capability flags + - Provides: `applyIntervention(params)` for Codex — routes to native steer/interrupt/cancel handlers; returns `{ status: 'degraded', fallbackAction: 'queue_and_interrupt' }` when an intervention type maps to a capability flag the driver declared `false` (Codex declares all three intervention-relevant flags `true`; the degraded path is exercised more in T3.7 Claude where `steer: false`). + - Estimate: 1 PR + +- **T3.3** — Author Codex capability declaration + refresh. + - Files: `packages/runtime-daemon/src/provider/drivers/codex/capabilities.ts` (new) + - Spec coverage: Spec-005:46 (7-flag declaration); :48 (undeclared = unsupported) + - Verifies invariant: I-005-2 + - Consumes: T1.2 DriverCapabilities; T1.3 GetCapabilitiesResult wrapper; T2.4 driver_capabilities writer + - Provides: `getCapabilities()` for Codex returning the V1 `GetCapabilitiesResult` wrapper with declared capability flags (matrix above) + Codex-declared per-tool metadata array. Refresh trigger emits `runtime_node.capability_updated` event per Spec-006:376 (existing event surface; payload `previousState` / `newState` carry the wrapper-shape contents) — see CP-005-5. + - Estimate: 1 PR + +- **T3.4** — Author Codex per-tool metadata declaration. + - Files: `packages/runtime-daemon/src/provider/drivers/codex/tools.ts` (new) + - Spec coverage: Spec-005:49 (per-tool idempotency_class required), :128 (manual_reconcile_only conservative default) + - Verifies invariant: I-005-3 + - Consumes: T1.3 IdempotencyClass + ProviderToolMetadata + - Provides: Codex tool list with explicit `idempotency_class` annotations (any tool not annotated defaults to `manual_reconcile_only` per I-005-3) + - Estimate: 1 PR + +- **T3.5** — Author Codex event normalizer. + - Files: `packages/runtime-daemon/src/provider/drivers/codex/event-normalizer.ts` (new) + - Spec coverage: Spec-005:45 (drivers emit normalized runtime events, not provider-native types); :78-84 (required normalized event families) + - Verifies invariant: — (normalization is structural; family-level coverage verified by Plan-006 taxonomy tests) + - Consumes: Plan-006 normalized event-family contract (`packages/contracts/src/event.ts`, Tier 1 — shipped) + - Provides: Mapping from Codex-native event types to Plan-006 normalized families (`run_lifecycle`, `assistant_output`, `tool_activity`, `interactive_request`, `artifact_publication`, `usage_telemetry`) + - Estimate: 1-2 PRs + +- **T3.6** through **T3.10** — Symmetric tasks for Claude driver (`drivers/claude/{index,lifecycle,intervention,capabilities,tools,event-normalizer}.ts`). Claude declares `steer: false` per the V1 capability matrix — T3.7 (Claude intervention dispatcher) exercises the degraded-fallback path: `applyIntervention({ type: 'steer', ... })` returns `{ status: 'degraded', fallbackAction: 'queue_and_interrupt' }` per ADR-011's documented fallback for no-native-steer providers. + +**Phase 3 Acceptance Mapping:** T3.1 + T3.6 lifecycle tests → Spec-005:155 (AC1 — driver-integration semantics-preserving). T3.3 + T3.8 capability declaration tests → Spec-005:156 (AC2 — undeclared capabilities not invocable). T3.1 + T3.6 resume-failure tests → Spec-005:157 (AC3 — recovery-needed condition surfaces). + +### Phase 4 — Client SDK exposure + degraded-fallback + +**Goal:** Author the typed SDK surface for capability + intervention controls under a dedicated `driver.*` JSON-RPC namespace (per Plan-007 CP-007-6). Daemon-side IPC handlers + SDK-side Zod-validated wrappers ship together. Subscription primitive reuses `LocalSubscriptionProducer` per Plan-007 CP-007-4 precedent. -## Implementation Steps +**Ratified design decisions (Tier 4 audit, 2026-05-27):** -- Contracts: See [API Payload Contracts](../architecture/contracts/api-payload-contracts.md) for typed schemas this plan consumes. +1. **Factory pattern**: daemon-only `createDaemonProviderClient(JsonRpcClient): DriverClient` — no control-plane variant. Aligns with Spec-005:42 (driver authority is local-daemon-resident; no V1.1 remote-provider workflow on roadmap). Matches MCP/LSP best practice (single-transport for local-only capability servers). +2. **Namespace**: dedicated `driver.*` (added to Plan-007 §Tier 4 namespace registry per CP-007-6). **7 client-facing method names** follow canonical long-form per ADR-009 + Plan-007 I-007-9: `driver.listCapabilities`, `driver.interruptRun`, `driver.applyIntervention`, `driver.respondToRequest`, `driver.listModels`, `driver.listModes`, plus the subscription method `driver.subscribeEvents`. **Reversal-with-rationale (Codex round-3 review, PR #124):** the 2026-05-27 ratified design originally exposed all 10 driver-contract operations + `subscribeEvents` (11) over the client JSON-RPC namespace. The four session/run **lifecycle** operations — `driver.createSession`, `driver.resumeSession`, `driver.startRun`, `driver.closeSession` — are now **daemon-internal**: invoked only by the session/run orchestration layer on the in-daemon `ProviderRegistry` (T2.3), never registered as client-callable JSON-RPC methods. Governing principle: a `driver.*` method is client-facing iff it operates on an already-existing session/run (capability introspection, intervention/control, model/mode reads, event subscription); methods that **establish, restore, start, or tear down** a session-or-run domain object are orchestration-owned, because orchestration must persist the runtime binding and emit the canonical lifecycle event around the driver call. This honors Spec-005:138 (the driver "only sees `interruptRun` followed later by `startRun`" — those lifecycle calls are driven BY orchestration, not by clients) and the Phase-4 goal above ("capability + intervention controls"). Exposing the lifecycle verbs as client-callable would let a renderer/CLI start or destroy a provider session/run while bypassing the orchestration that owns `runtime_bindings` persistence + canonical `run.*` event emission. The in-daemon 10-op `ProviderDriver` interface (T1.1) is unchanged — only its client JSON-RPC exposure narrows. Clients reach lifecycle indirectly through the orchestration namespaces (`session.*` per Plan-001 — `session.create` maps to `directoryService.createSession` per api-payload-contracts.md:256, NOT `driver.createSession`; `run.*` per Plan-004). +3. **Envelope shape**: Zod-validated `DriverInterventionResult` at SDK seam (`{ status: 'applied' | 'degraded'; fallbackAction?: string }`) mirroring driver-internal shape (Spec-005:44, 112) with wire-level validation. Distinct from orchestration `InterventionState` lifecycle enum per LSP/MCP separation-of-concerns precedent. +4. **Subscription primitive**: `driver.subscribeEvents` returns `LocalSubscriptionProducer` per Plan-007 CP-007-4 (the shared streaming primitive). `DriverEvent` is the Plan-006-owned union of existing event-category types relevant to driver runtime — `run_lifecycle`, `assistant_output`, `tool_activity`, `interactive_request`, `artifact_publication`, `usage_telemetry`, and `runtime_node_lifecycle` (the last category supplies `runtime_node.capability_declared` + `runtime_node.capability_updated` for driver-capability events per CP-005-5; no new category is introduced). `usage_telemetry` is the canonical category name per [Spec-006 §Usage Telemetry](../specs/006-session-event-taxonomy-and-audit-log.md#usage-telemetry-usage_telemetry) and `packages/contracts/src/event.ts` literal union. +5. **Recovery-needed surface**: surfaced via a typed return-value contract from `resumeSession()` (T3.1, T3.6), then propagated by Plan-015's recovery dispatcher to the existing `run.failed` event with `recoveryCondition: 'recovery-needed'` per Spec-006:179. Plan-005 does NOT emit a separate `driver.recovery_needed` event — see CP-005-5 for the full handoff sequence. -1. Define contract types and capability schema. The driver contract enumerates 10 operations: `create`, `resume`, `start`, `interrupt`, `respond`, `close`, `applyIntervention`, `listModels`, `listModes`, `getCapabilities`. The capability schema defines 7 flags: `resume`, `steer`, `interactive_requests`, `mcp`, `tool_calls`, `reasoning_stream`, `model_mutation`. -2. Implement registry and runtime binding persistence. -3. Implement initial Codex and Claude drivers against the contract as local-runtime-node integrations rather than shared hosted execution services. -4. Add client SDK exposure for capability-aware controls and diagnostics. Include degraded-fallback behavior: when `applyIntervention` receives an unsupported intervention type the driver returns a structured rejection so the caller can degrade gracefully rather than error. +#### Tasks + +- **T4.1** — Author daemon-side `driver.*` IPC handlers (7 client-facing: 6 non-lifecycle verbs + `driver.subscribeEvents`). + - Files: `packages/runtime-daemon/src/ipc/handlers/driver-handlers.ts` (new); `packages/runtime-daemon/src/ipc/router.ts` (extend registration) + - Spec coverage: Spec-005:67-77 (the 10-op surface is the in-daemon `ProviderDriver` interface; the client `driver.*` namespace exposes only the 6 non-lifecycle verbs + `subscribeEvents` per §Phase 4 decision #2) + - Verifies invariant: I-005-1 (driver authority local — IPC handlers dispatch to in-daemon ProviderRegistry) + - Consumes: T2.3 ProviderRegistry; Plan-007 NamespaceRegistry (Tier 4 — co-tier delivery; CP-007-6 reciprocal); T1.1 ProviderDriver interface + - Provides: 7 typed client-facing handlers (6 non-lifecycle verbs + `driver.subscribeEvents`) registered against Plan-007's NamespaceRegistry as `driver.{method}` entries; the 4 lifecycle ops (`createSession`/`resumeSession`/`startRun`/`closeSession`) are NOT registered as JSON-RPC methods — the session/run orchestration layer invokes them on the in-daemon `ProviderRegistry` (T2.3) + - Estimate: 2 PRs + +- **T4.2** — Author Zod schemas + Zod-validated wire envelopes at SDK seam. + - Files: `packages/contracts/src/provider-driver.ts` (extend T1.4 — add the SDK-seam Zod schemas for the remaining client-facing methods, e.g. `ListCapabilitiesResultSchema`, `InterruptRunParamsSchema`, `ListModelsResultSchema`, etc.; the 4 session/run lifecycle ops `createSession`/`resumeSession`/`startRun`/`closeSession` get NO client-facing SDK schema — they are daemon-internal per §Phase 4 decision #2, so no `CreateSessionParamsSchema`/`ResumeSessionParamsSchema` at the client seam) + - Spec coverage: Spec-005:44 (applyIntervention degraded envelope) + - Verifies invariant: I-005-4 + - Consumes: T1.4 DriverInterventionResultSchema + - Provides: Full SDK-seam Zod schemas for the 7 client-facing driver.\* methods (6 non-lifecycle verbs + `driver.subscribeEvents`; request + response) + - Estimate: 1 PR + +- **T4.3** — Author `createDaemonProviderClient` factory. + - Files: `packages/client-sdk/src/providerClient.ts` (new); `packages/client-sdk/src/index.ts` (export) + - Spec coverage: Spec-005:42 (driver authority local — SDK factory is daemon-only); Spec-005:43 (the 10-op contract surface is the in-daemon driver interface — the SDK exposes the 6 non-lifecycle client verbs + `subscribeEvents` per §Phase 4 decision #2, not the 4 lifecycle ops) + - Verifies invariant: I-005-1 + - Consumes: T1.1 ProviderDriver interface; T1.4 DriverInterventionResultSchema; T4.2 SDK-seam schemas; Plan-007 `JsonRpcClient` transport from `packages/client-sdk/src/transport/jsonRpcClient.ts` (Tier 1 — shipped) + - Provides: `interface DriverClient { listCapabilities(); interruptRun(p); applyIntervention(p); respondToRequest(p); listModels(); listModes(); subscribeEvents(): LocalSubscriptionConsumer }` (the 6 non-lifecycle client verbs + `subscribeEvents` = the 7 client-facing methods ratified at §Phase 4 decision #2; the 4 lifecycle ops `createSession`/`resumeSession`/`startRun`/`closeSession` are daemon-internal and deliberately absent from the client interface — exposing them would let a client bypass `runtime_bindings` persistence + canonical run/session event emission) + `createDaemonProviderClient(client: JsonRpcClient): DriverClient` factory. No `createControlPlaneProviderClient` per ratified decision #1. + - Estimate: 1-2 PRs + +- **T4.4** — Author `driver.subscribeEvents` subscription surface. + - Files: `packages/runtime-daemon/src/ipc/handlers/driver-subscribe.ts` (new); `packages/client-sdk/src/providerClient.ts` (extend T4.3) + - Spec coverage: Spec-005:45 (drivers emit normalized runtime events) + - Verifies invariant: I-005-1 + - Consumes: T1.1 ProviderDriver interface; Plan-007 CP-007-4 `LocalSubscriptionProducer` from `packages/contracts/src/jsonrpc-streaming.ts` (Tier 1 — shipped); T3.5 + T3.10 event normalizers + - Provides: `driver.subscribeEvents(runId)` returns `LocalSubscriptionProducer` on the daemon side and `LocalSubscriptionConsumer` on the SDK side per the Plan-007 producer/consumer split. + - Estimate: 1 PR + +- **T4.5** — Author daemon-side capability cache + invalidation. + - Files: `packages/runtime-daemon/src/provider/capability-cache.ts` (new) + - Spec coverage: Spec-005:48 (undeclared = unsupported) + - Verifies invariant: I-005-2 + - Consumes: T2.4 driver_capabilities writer; T3.3 + T3.8 driver capability declarations + - Provides: Per-driver capability cache + invalidation on `runtime_node.capability_updated` event (Spec-006:376; subscribed via the daemon's audit-log fanout). SDK `listCapabilities()` reads the cache (no provider round-trip per call). + - Estimate: 1 PR + +- **T4.6** — Author degraded-fallback orchestration unit tests. + - Files: `packages/client-sdk/src/providerClient.test.ts` (new) + - Spec coverage: Spec-005:44 (degraded envelope); Spec-005 AC2 (Spec-005:156 — unsupported capability gated) + - Verifies invariant: I-005-4, I-005-2 + - Notes: Tests assert `applyIntervention({ type: 'steer', ...})` against the Claude driver (which declares `steer: false`) returns `{ status: 'degraded', fallbackAction: 'queue_and_interrupt' }` — the client-facing `driver.applyIntervention` path. Tests also assert the in-daemon `ProviderRegistry` capability gate (T2.3) refuses a capability-bound invocation against an undeclared flag BEFORE it reaches the driver (throwing `driver.capability_unsupported`); because the lifecycle ops (`startRun` et al.) are daemon-internal, this gate sits at the orchestration→driver (`ProviderRegistry`) boundary, not at a client JSON-RPC layer. + - Estimate: 1 PR + +- **T4.7** — Author recovery-needed return-value contract tests. + - Files: `packages/client-sdk/src/providerClient.recovery.test.ts` (new). No Plan-006 amendment needed — recovery-needed reuses the existing `run.failed.recoveryCondition` surface from Spec-006:179 per CP-005-5; the test asserts only the driver's return-value contract, not event emission. + - Spec coverage: Spec-005:60 (resume-handle failure → recovery-needed condition); Spec-005 AC3 (Spec-005:157 — explicit recovery-needed condition rather than silent session replacement) + - Verifies invariant: I-005-5 + - Consumes: T3.1 + T3.6 lifecycle resume-failure paths; Spec-006:179 existing `run.failed` event with `recoveryCondition: 'recovery-needed'` + - Provides: Test that simulates Codex resume failure and asserts (a) `resumeSession()` returns a typed failure result carrying `recoveryCondition: 'recovery-needed'` and (b) the driver does NOT silently call `createSession()` to replace the failed binding. The downstream `run.failed` event emission (with `recoveryCondition: 'recovery-needed'`) is Plan-015's responsibility and is covered by Plan-015's tier-7 tests; this Phase-4 test verifies only the driver's return-value contract per Spec-005:60 + AC3. + - Estimate: 1 PR + +**Phase 4 Acceptance Mapping:** T4.6 → Spec-005:156 (AC2). T4.7 → Spec-005:157 (AC3). T4.3 SDK exposure → Spec-005:155 (AC1) for the externally-observable contract. ## Parallelization Notes -- Contract work and binding-store work can start first. -- Codex and Claude driver implementations can proceed in parallel once the contract stabilizes. +- Phase 1 (contracts) must complete before Phase 2-4 begin. +- Phase 2 (registry + persistence) blocks Phase 3 (drivers consume the registry). +- Phase 3 Codex + Claude tasks can proceed in parallel once T2.3 ProviderRegistry is shipped. +- Phase 4 SDK exposure can proceed in parallel with Phase 3 driver implementation once T2.3 + T2.4 ship — Phase 4 daemon-side IPC handlers depend on the registry, not on the specific drivers being implemented. ## Test And Verification Plan -- Contract conformance tests for driver lifecycle methods -- Capability matrix tests for control exposure -- Recovery tests for adopt-existing and resume-handle paths -- Integration tests proving driver lifecycle and policy enforcement stay daemon-local even when the provider endpoint itself is remote +- **Phase 1 contract tests** (T1.5): Type-system conformance via TypeScript compile check + minimal mock driver. Verifies AC1 + AC2. +- **Phase 2 integration tests** (T2.5): Real SQLite (better-sqlite3 in-memory) + ProviderRegistry round-trip. Verifies I-005-1, I-005-2 + AC2. +- **Phase 3 driver tests** (per-driver): Lifecycle (createSession → resumeSession → startRun → interruptRun → closeSession) + capability declaration + per-tool idempotency_class + event normalization. Verifies AC1 + AC2 + AC3. +- **Phase 4 SDK tests** (T4.6, T4.7): Degraded-fallback orchestration + recovery-needed return-value contract. Verifies AC2 + AC3 + I-005-4 + I-005-5. +- **Integration tests across phases**: Proving driver lifecycle and policy enforcement stay daemon-local even when the provider endpoint itself is remote — verifies I-005-1. ## Rollout Order -1. Land shared driver contract and registry -2. Port first driver -3. Port second driver -4. Enable capability-driven UI behavior +1. Land Phase 1 contracts (T1.1-T1.6) — blocks all downstream. +2. Land Phase 2 persistence + registry (T2.1-T2.5) — blocks Phase 3. +3. Land Phase 3 Codex driver (T3.1-T3.5) and Phase 3 Claude driver (T3.6-T3.10) in parallel. +4. Land Phase 4 SDK exposure (T4.1-T4.7) — may proceed in parallel with late-Phase-3 work once T2.3 ships. ## Rollback Or Fallback - Keep one driver behind a compatibility adapter if the full contract rollout regresses. +- If Phase 4 SDK exposure regresses, fall back to direct daemon-internal use of ProviderRegistry (Phase 2 surface) until the SDK surface is restored. ## Risks And Blockers -- Contract churn while both initial drivers are under construction -- Recovery semantics may diverge before enough conformance tests exist -- Remote provider APIs can be mistaken for permission to centralize driver execution unless the local-runtime boundary stays explicit in code and docs +- Contract churn while both initial drivers are under construction → mitigated by Phase 1 contract-stabilization gate before Phase 3 starts. +- Recovery semantics may diverge before enough conformance tests exist → mitigated by I-005-5 invariant + T4.7 explicit recovery-needed return-value contract test. +- Remote provider APIs can be mistaken for permission to centralize driver execution unless the local-runtime boundary stays explicit in code and docs → mitigated by I-005-1 invariant + Spec-005:42 reinforcement throughout this plan. + +## Invariants + +The following load-bearing properties are promoted from Spec-005 and apply across all phases. Each invariant is referenced by Task `Verifies invariant:` fields above. + +- **I-005-1** — Driver authority remains local even when provider endpoint is remote. Source: Spec-005:42, Spec-005:54. Enforced by: ProviderRegistry residing in-daemon (T2.3); SDK factory is daemon-only (T4.3). Test: integration test asserting driver lifecycle stays daemon-local with remote endpoint mock. +- **I-005-2** — Runtime treats undeclared capabilities as unsupported. Source: Spec-005:48. Enforced by: ProviderRegistry capability-check gating (T2.3); 7-flag enum exhaustiveness at the type system (T1.2). Test: SDK test asserts invocation against undeclared flag is rejected with `driver.capability_unsupported` (T4.6). +- **I-005-3** — Per-tool `idempotency_class` defaults to `manual_reconcile_only` when undeclared. Source: Spec-005:128. Enforced by: the `ProviderToolMetadataSchema` ingress field `idempotency_class: IdempotencyClassSchema.optional().default("manual_reconcile_only")` (T1.3) — a driver that OMITS the field is accepted, NOT rejected, at ingress, and parsing yields the `manual_reconcile_only` default in `z.output` (`NormalizedProviderToolMetadata`), so every value persisted to the NOT NULL `driver_tools.idempotency_class` column or emitted on a `runtime_node.capability_*` event is populated; Plan-015's recovery dispatch table reads the normalized value (Spec-015:120). Test: contract test asserts a tool declared WITHOUT `idempotency_class` is accepted at ingress AND resolves to `manual_reconcile_only` in the normalized (persisted / emitted) shape. +- **I-005-4** — Unsupported intervention types return structured `degraded` result; orchestration layer dispatches fallback. Source: Spec-005:44, Spec-005:112. Enforced by: DriverInterventionResultSchema Zod validation at SDK seam (T1.4 + T4.2); Claude driver intervention dispatcher (T3.7) returns `degraded` on `steer` invocation. Test: T4.6 asserts the Zod-validated result shape. +- **I-005-5** — Drivers MUST NOT silently create replacement provider sessions on resume failure; failure surfaces an explicit `recovery-needed` condition. Source: Spec-005:60, Spec-005:157 (AC3). Enforced by: lifecycle `resumeSession()` implementations (T3.1, T3.6) return a typed failure result carrying `recoveryCondition: 'recovery-needed'` rather than calling `createSession()`. Plan-015's recovery dispatcher (Tier 7) receives the typed result and emits the existing `run.failed` event with `recoveryCondition: 'recovery-needed'` per Spec-006:179 — no separate driver event. Test: T4.7 asserts the typed return shape from a simulated Codex resume failure path; verifying that no `createSession()` call is issued is covered by the same test (mock-spy on the driver's `createSession` method). + +## Cross-Plan Obligations + +This section makes Plan-005's reciprocal obligations to other plans visible to a Plan-005 reviewer without requiring them to read every consuming or consumed plan first. Mirrors the bidirectional-citation pattern established by [Plan-001 §Cross-Plan Obligations](./001-shared-session-core.md#cross-plan-obligations) and [Plan-007 §Cross-Plan Obligations](./007-local-ipc-and-daemon-control.md#cross-plan-obligations). + +### CP-005-1 — `runtime-binding-store.ts` recovery-aware extension contract owed to [Plan-015](./015-persistence-recovery-and-replay.md) + +Plan-005 Phase 2 (T2.2) owns `packages/runtime-daemon/src/provider/runtime-binding-store.ts`. Plan-015 at Tier 7 extends the store with recovery-aware persistence methods (per cross-plan-dependencies.md §2 line 80 ownership row). The extension contract: + +- The `RuntimeBindingStore` exposes `findResumableBindings(...)` as a queryable seam where Plan-015 may add recovery-aware predicates (e.g., bindings with stale checkpoints, bindings marked recovery-needed via the dedicated `recovery_checkpoints` table at `local-sqlite-schema.md:791`). +- Row-level recovery state lives in Plan-015's `recovery_checkpoints` table (not on `runtime_bindings`). Plan-005's `resumeSession()` returns the typed failure result (T3.1/T3.6); Plan-015's recovery dispatcher (Tier 7) receives the result and writes the corresponding row to `recovery_checkpoints` via a Plan-015-defined extension method on `RuntimeBindingStore`, then emits the existing `run.failed` event with `recoveryCondition: 'recovery-needed'` per Spec-006:179. +- Plan-005 does NOT add `recovery_state` / `recovery_reason` columns to `runtime_bindings`. The ownership boundary is: `runtime_bindings` carries identity + active binding state; `recovery_checkpoints` carries recovery-state-machine state. (Recovery state belongs to Plan-015's dedicated table.) + +**Why bidirectional.** Plan-015 reviewers see the consumer dependency; Plan-005 reviewers (especially Phase 2 PR authors) must know that the binding-store's public method surface is contractually required to support Plan-015's recovery dispatch without further Plan-005 amendments. + +### CP-005-2 — `idempotency_class` per-tool metadata shape owed to [Plan-015](./015-persistence-recovery-and-replay.md) + +Plan-005 Phase 1 (T1.3) owns the `IdempotencyClass` enum + `ProviderToolMetadata` shape. Plan-015 at Tier 7 consumes these types in its `Spec-015 §Idempotency Protocol` recovery-dispatch table (Spec-015:108-120) — the `manual_reconcile_only` row (Spec-015:120) is load-bearing for Plan-015's recovery dispatcher: Plan-015 reads the `IdempotencyClass` enum to dispatch (replay vs skip vs halt), and on halt emits the existing `run.failed` event with `recoveryCondition: 'recovery-needed'` per Spec-006:179. The driver's `resumeSession()` failure-path return-value is the upstream signal that triggers Plan-015's dispatch; the event emission stays on Plan-015's side. + +**Why bidirectional.** The `IdempotencyClass` symbol must exist in Plan-005's contracts file before Plan-015 can compile. Plan-005 cannot move this enum without coordinating with Plan-015's spec language. + +### CP-005-3 — `PtyHost` contract consumption from [Plan-024](./024-rust-pty-sidecar.md) + +Plan-005 Phase 3 Codex driver (T3.1) consumes the `PtyHost` contract from `packages/contracts/src/pty-host.ts`. Plan-024 Phase 2 ships this contract at Tier 1 (per NS-05). The boundary: + +- Plan-005 imports the `PtyHost` interface; it does NOT depend on the Rust binary directly. The binary is a Plan-024-owned implementation detail behind the contract. +- The Codex /resume substrate (PtyHost-based persistent process for `resumeSession()`) is a Plan-024 contract surface — Plan-005's Codex driver consumes it. If Plan-024 changes the PtyHost contract, Plan-005 amendments may be required. + +**Why bidirectional.** Plan-024's Phase 2 is the canonical landing-point for the `PtyHost` contract; Plan-005 reviewers must know which Plan-024 surface they're consuming so a Plan-024 contract change triggers a Plan-005 review. + +### CP-005-4 — `driver.*` JSON-RPC namespace registration owed to [Plan-007](./007-local-ipc-and-daemon-control.md) Tier 4 + +Plan-005 Phase 4 (T4.1) registers 7 client-facing `driver.*` JSON-RPC method handlers (6 non-lifecycle verbs + `driver.subscribeEvents`; the 4 session/run lifecycle ops — `createSession`/`resumeSession`/`startRun`/`closeSession` — are daemon-internal per §Phase 4 decision #2, invoked by orchestration on the in-daemon `ProviderRegistry`) against Plan-007's NamespaceRegistry (`packages/contracts/src/jsonrpc-registry.ts` per Plan-007 CP-007-3 → no-mirror disposition; canonical source: code). Plan-007 §Tier 4 namespace enumeration must include `driver.*` for Plan-005's registration to land without orphaning. + +**Why bidirectional.** Plan-007 Tier 4 PR authors must know that `driver.*` is part of the registered namespace set; otherwise the Plan-005 SDK seam fails to register at daemon startup. Plan-007 CP-007-6 is the reciprocal entry on Plan-007's side. + +### CP-005-5 — Driver capability event surface owed to [Plan-006](./006-session-event-taxonomy-and-audit-log.md) / [Spec-006](../specs/006-session-event-taxonomy-and-audit-log.md) + +**Capability-declaration + capability-change events reuse existing Spec-006 surface.** Spec-006:375-376 already enumerates two events in the `runtime_node_lifecycle` category that name "provider driver" as a capability type: + +- `runtime_node.capability_declared` — payload `{capability, capabilityDetails}`. Plan-005 emits this on driver registration (T2.3) and on cold-start (T4.5) with `capability: "provider-driver-{codex|claude}"` and `capabilityDetails: { flags: Record, contractVersion: string, tools: NormalizedProviderToolMetadata[] }` (the wrapper-shape contents from T1.3; `tools` carries the normalized shape because the event crosses the persistence / event-payload boundary). +- `runtime_node.capability_updated` — payload `{capability, previousState, newState}`. Plan-005 emits this on capability refresh (T3.3, T3.8; consumed by T4.5 cache invalidation) with `capability: "provider-driver-{codex|claude}"` and previous/new state carrying the same shape as `capabilityDetails` above. + +No new event types, no new category. The 7-flag matrix + per-tool metadata travel inside the existing `capabilityDetails` / `previousState` / `newState` payload fields. Plan-006 audit confirms these payload shapes are admissible under the existing event surface (Spec-006:376 already lists "driver version bump, tool addition" as a documented example of `capability_updated`). + +**Recovery-needed condition reuses existing `run.failed` surface.** Spec-005:60 calls for drivers to "surface a visible `recovery-needed` condition" on resume-handle failure. Spec-006:179 already declares this surface: `run.failed` payload includes `recoveryCondition?: 'recovery-needed'`. Plan-005 does NOT emit a separate `driver.recovery_needed` event. Instead: + +1. Plan-005's `resumeSession()` returns a typed failure result (T3.1, T3.6). +2. Plan-015's recovery dispatcher (Tier 7) receives the typed result. +3. Plan-015 emits the existing `run.failed` event with `recoveryCondition: 'recovery-needed'`. + +This keeps the recovery-needed signal on the canonical run-lifecycle surface where downstream consumers already subscribe; avoids creating a parallel driver-specific recovery event. + +**Why bidirectional.** Plan-006 audit (concurrent Tier 4) confirms the existing `runtime_node.capability_declared` / `runtime_node.capability_updated` payload shapes accept the wrapper-shape contents — no new event types are added. Plan-005 reviewers (especially Phase 2-4 implementers) must know that the driver-capability event surface is the existing Spec-006:375-376 row, not a new category. Plan-005 reviewers must also know that recovery-needed flows through Plan-015 (not directly from the driver) so the driver's `resumeSession()` return-type stays a pure typed failure result. + +**Resolution (Plan-006 audit synthesis 2026-05-28 — closes CP-005-5).** Plan-006 audit ratifies two concrete deliverables that discharge this carry-forward: + +1. **Plan-006 Phase 1 T1.4 binds a typed `CapabilityDetails` interface** in `packages/contracts/src/event.ts` (extends top-level per Plan-001's contracts package layout precedent; resolved as "extend in place"). Shape: `{ flags: Record, contractVersion: string, tools: readonly NormalizedProviderToolMetadata[] }` (`tools` is the normalized shape — `CapabilityDetails` is an event payload, so it crosses the boundary). Plan-006 Phase 1 T1.6 doc-mirrors the same shape into [`api-payload-contracts.md` §Plan-006](../architecture/contracts/api-payload-contracts.md) (between `GetCapabilitiesResult` and `EventEnvelope`) with explicit references to Spec-006:375-376 and this carry-forward. + +2. **Plan-006 Phase 3 ratifies `providerFailureDetail?: string` on `run.failed` payload** ([`api-payload-contracts.md` `RunStateChangeEvent`](../architecture/contracts/api-payload-contracts.md) line ~876). Mirrors `DriverResumeResult.failure.providerFailureDetail` (line 620, shipped by Plan-005 T1.6) — Plan-015's recovery dispatcher reads the typed failure detail from the driver result and emits it on the canonical `run.failed` event so the audit log carries the operator-actionable reason without re-querying the driver. ADR-018-compliant additive-only MINOR addition (no consumer breakage; optional field). + +3. **Plan-006 Phase 4 confirms read-shape generic envelope is sufficient.** Replay endpoints (`EventReadAfterCursorResponse`, `EventReadWindowResponse`, `EventSubscription`) return `EventEnvelope[]` with `payload: Record`. Consumers narrow via Phase 1's discriminated-union taxonomy (the typed `RuntimeNodeCapabilityDeclaredPayload` + `RuntimeNodeCapabilityUpdatedPayload` payload interfaces). No replay-shape tightening required. + +**Status: CP-005-5 RESOLVED.** Discharge artifacts land in Plan-006 audit PR (this PR or its successor); Plan-005 author confirms by re-reading `api-payload-contracts.md` `CapabilityDetails` + `RunStateChangeEvent.providerFailureDetail` definitions and Plan-006 working-copy T1.4 + Phase 3 T3.x integration. + +### CP-005-6 — `InterventionType` enum co-location resolution owed to [Plan-004](./004-queue-steer-pause-resume.md) + +The cross-cutting `InterventionType = "steer" | "interrupt" | "cancel"` enum (api-payload-contracts.md:142) is consumed by both Plan-004's `runControl.ts` (Tier 5) and Plan-005's `provider-driver.ts` (Tier 4). Resolution: Plan-005 Phase 1 (T1.4) co-locates the `InterventionType` re-export in `provider-driver.ts` since Plan-005 ships earlier. Plan-004 Tier 5 imports the enum from Plan-005 (Tier 4 → Tier 5 tier-consistent import). + +**Precedent.** This matches Plan-001's ownership pattern for branded ids (`SessionId`, `RunId`, `ChannelId` co-located in Plan-001's contracts Phase 2 because Plan-001 ships first; downstream plans at higher tiers import from Plan-001). Earlier-tier plan owns cross-cutting symbols it ships first; downstream plans import without inventing parallel definitions. + +**Why bidirectional.** Plan-004 reviewers see the import-from-Plan-005; Plan-005 reviewers must know that the enum is shared and must not relocate it without coordinating with Plan-004. ## Done Checklist -- [ ] Code changes implemented -- [ ] Tests added or updated -- [ ] Verification completed -- [ ] Related docs updated +- [ ] Phase 1 — Driver contract + capability schema + idempotency_class (T1.1-T1.6) +- [ ] Phase 2 — Provider registry + runtime-binding store + SQLite (T2.1-T2.5) +- [ ] Phase 3 — Codex driver (T3.1-T3.5) + Claude driver (T3.6-T3.10) +- [ ] Phase 4 — SDK exposure + degraded-fallback (T4.1-T4.7) +- [ ] All 5 invariants (I-005-1 through I-005-5) verified via tests +- [ ] All 6 cross-plan obligations (CP-005-1 through CP-005-6) discharged +- [ ] Acceptance criteria AC1, AC2, AC3 verified per the Phase Acceptance Mapping rows +- [ ] Required ADRs cited from Tasks/Invariants/Cross-Plan Obligations (ADR-005, ADR-011, ADR-015, ADR-017) +- [ ] Related docs updated (api-payload-contracts.md doc-mirror per T1.3 — `IdempotencyClass` + `ProviderToolMetadata` (ingress) + `NormalizedProviderToolMetadata` (normalized) + `GetCapabilitiesResult` shapes added immediately after the existing `interface DriverCapabilities` declaration without modifying that declaration; api-payload-contracts.md doc-mirror per T1.6 — `DriverResumeResult` discriminated union added immediately after the existing `InterventionDriverResult` shape; cross-plan-dependencies.md §3 row 131 adds Plan-005 → Plan-007-partial edge; cross-plan-dependencies.md §2 row 90 adds `provider-driver.ts` to the Plan-005-owned extenders cell) diff --git a/docs/plans/006-session-event-taxonomy-and-audit-log.md b/docs/plans/006-session-event-taxonomy-and-audit-log.md index 93fc2fa3..cdb8fa05 100644 --- a/docs/plans/006-session-event-taxonomy-and-audit-log.md +++ b/docs/plans/006-session-event-taxonomy-and-audit-log.md @@ -18,9 +18,9 @@ Implement the canonical append-only session-event contract, its tamper-evident i ## Scope -This plan covers the `EventEnvelope` contract and version semantics, the 120-event taxonomy registry across 18 categories, append-only persistence with BLAKE3 hash chain and Ed25519 signatures over RFC 8785 JCS canonical bytes, Merkle anchor emission into the shared `event_log_anchors` table (metadata only per [ADR-017](../decisions/017-shared-event-sourcing-scope.md)), the PII-column indirection pattern (`pii_payload` ciphertext + `pii_ciphertext_digest`), replay reads, live subscriptions, and compaction to audit stubs. +This plan covers the `EventEnvelope` contract and version semantics, the 123-event taxonomy registry across 19 categories per [Spec-006 §Event Type Summary](../specs/006-session-event-taxonomy-and-audit-log.md) (lines 506+533 — canonical count), append-only persistence with BLAKE3 hash chain and Ed25519 signatures over RFC 8785 JCS canonical bytes, Merkle anchor emission into the shared `event_log_anchors` table (metadata only per [ADR-017](../decisions/017-shared-event-sourcing-scope.md)), the PII-column indirection pattern (`pii_payload` ciphertext + `pii_ciphertext_digest`), replay reads, live subscriptions, and compaction to audit stubs. -Plan-006 is the canonical emitter of the `event_maintenance` and `audit_integrity` categories (6 event types total). It is not the emitter of the remaining 16 category entries — see §Event Taxonomy Coverage for the ownership boundary. +Plan-006 is the canonical emitter of the `event_maintenance` and `audit_integrity` categories (6 event types total). It is not the emitter of the remaining 17 categories — see §Event Taxonomy Coverage for the ownership boundary. ## Non-Goals @@ -42,21 +42,24 @@ Target paths below assume the canonical implementation topology defined in [Cont ## Target Areas -- `packages/contracts/src/events/envelope.ts` — `EventEnvelope` shape + `.version` semantics -- `packages/contracts/src/events/taxonomy.ts` — 120-event type enum + 18-category enum -- `packages/contracts/src/errors/version-errors.ts` — `VERSION_FLOOR_EXCEEDED` + `VERSION_CEILING_EXCEEDED` error codes -- `packages/runtime-daemon/src/events/canonicalizer.ts` — RFC 8785 JCS emitter with fixed field ordering and RFC 3339 UTC millisecond `occurredAt` +- `packages/contracts/src/event.ts` (EXTEND in place per Plan-001's contracts package layout — directory-placement decision resolved 2026-05-28) — `EventEnvelope` schema + 19-category `EventCategory` literal union + 123-event `SessionEventType` literal union + `SESSION_EVENT_CATEGORY_BY_TYPE` registry + `CapabilityDetails` wrapper + `RuntimeNodeCapability{Declared,Updated}Payload` interfaces. The `events/` subdirectory does not exist in the contracts package; the top-level co-location pattern (Plan-001 baseline: 16 categories + 3-variant union + factory + registry) is the canonical layout. +- `packages/contracts/src/event-anchor.ts` (NEW Plan-006-owned) — `AnchorPayload` wire shape for daemon → control-plane anchor upload (Phase 3 T3.3). Top-level co-located per the package convention. +- `packages/contracts/src/error.ts` (CROSS-LINK only — JSDoc on `EventEnvelopeVersionSchema` pointing to the existing `VersionFloorExceededError` + `VersionCeilingExceededError` schemas at error.ts:97-101, 306-339, already shipped by Plan-001 T2.3 per the Phase 1 audit forward-completed surface) +- `packages/runtime-daemon/src/events/canonicalizer.ts` — RFC 8785 JCS emitter with mandatory UTF-16 code-unit lex-sort field ordering per the canonicalization resolution + Spec-006:546 amendment + RFC 3339 UTC millisecond `occurredAt` - `packages/runtime-daemon/src/events/signer.ts` — BLAKE3 hash chain + Ed25519 signer +- `packages/runtime-daemon/src/events/signing-key-source.ts` (NEW Plan-006-owned per the signing-key custody resolution) — `DaemonSigningKeySource` interface + `OsKeystoreSealedDaemonSigningKeySource` implementation (per-session Ed25519 sealed via OS-keystore master-key; stored as ciphertext in the new local `daemon_signing_keys` SQLite table per ADR-004 SQLite-local-state boundary — corrected from the pre-Codex-T4-review draft that mis-located the column on shared-Postgres `sessions`) - `packages/runtime-daemon/src/events/pii-indirection.ts` — AES-256-GCM encrypt + BLAKE3 ciphertext digest + payload embed (sole write path for `pii_payload`) - `packages/runtime-daemon/src/events/event-log-service.ts` — append path writing all integrity columns; emits `event.shredded` at Plan-022 Path 1 callback -- `packages/runtime-daemon/src/events/compactor.ts` — audit-stub generator + compaction triggers; emits `event.compacted` -- `packages/runtime-daemon/src/events/merkle-anchor-service.ts` — anchor cadence + upload to shared `event_log_anchors` -- `packages/runtime-daemon/src/events/integrity-verifier.ts` — read-side chain/signature/anchor verifier + observer-pattern key-reuse detector; emits `audit_integrity_verified` / `audit_integrity_failed` / `key_reuse_detected` -- `packages/runtime-daemon/src/events/schema-migration-emitter.ts` — emits `schema.migrated` on batch boundary +- `packages/runtime-daemon/src/events/compactor.ts` — audit-stub generator + compaction triggers; **enforces anchor-before-compaction protocol per Spec-006 §Post-Compaction Integrity (force-fires `MerkleAnchorService.anchorRange()` if the to-be-compacted range is not yet anchor-covered)**; emits `event.compacted`; uses `session_events.retention_class` discriminator per the audit-stub representation resolution +- `packages/runtime-daemon/src/events/merkle-anchor-service.ts` — anchor cadence + upload to shared `event_log_anchors`; durable partition queue via `pending_anchor_uploads` table per the partition-anchor queue resolution +- `packages/runtime-daemon/src/events/integrity-verifier.ts` — read-side chain/signature/anchor/stub-signature/scalar-binding verifier; emits `audit_integrity_verified` / `audit_integrity_failed` with the 11-value `failureMode` enum from Spec-006:433 (post amendment for the four `anchor_missing_for_compacted_range` / `anchor_signature_invalid` / `stub_signature_invalid` / `stub_scalar_mismatch` modes added by the post-compaction integrity protocol per Spec-006 §Post-Compaction Integrity) +- `packages/runtime-daemon/src/events/key-reuse-observer.ts` — observer-pattern Sigstore-precedent key-reuse detector; emits `key_reuse_detected` with `rotationInvariantViolated: 'refuse_on_rotation'` and halts ingest from the colliding signer's NodeId +- `packages/runtime-daemon/src/events/schema-migration-emitter.ts` — emits `schema.migrated` on Flyway-precedent `AFTER_MIGRATE_OPERATION_FINISH` batch boundary - `packages/runtime-daemon/src/events/replay-service.ts` — `EventReadAfterCursor`, `EventReadWindow`, cursor state tracking compacted regions -- `packages/control-plane/src/event-anchors/` — shared `event_log_anchors` write path (metadata only per ADR-017) -- `packages/client-sdk/src/eventClient.ts` — typed SDK methods + `EventSubscription` -- `apps/desktop/src/renderer/src/timeline/` — audit-stub rendering for compacted regions +- `packages/runtime-daemon/src/events/event-subscription.ts` — `EventSubscription` replay-then-live producer (consumes `LocalSubscriptionProducer` from Plan-007 CP-007-4) +- `packages/control-plane/src/event-anchors/` (anchor-router.ts + anchor-store.ts) — shared `event_log_anchors` write path (metadata only per ADR-017); mounted on Plan-008-bootstrap `host.ts` +- `packages/client-sdk/src/eventClient.ts` — typed SDK methods + `EventSubscription` (registers `event.readAfterCursor`, `event.readWindow`, `event.subscribe` under Plan-007 namespace per CP-006-4) +- `apps/desktop/src/renderer/src/timeline/CompactedStubSegment.tsx` — narrow-scope audit-stub renderer (Plan-013 Non-Goal per §Non-Goals; full timeline UI deferred) ## Data And Storage Changes @@ -214,18 +217,144 @@ Plan-006 owns the `EventEnvelope` contract's integration with [ADR-018](../decis ## Implementation Steps +Plan-006 ships in **four phases**, each phase = one PR. The phase boundaries align with the audit-derived dispatch graph (see [`docs/operations/plan-implementation-readiness-audit-runbook.md`](../operations/plan-implementation-readiness-audit-runbook.md)). + +**Phase 1 — Contracts** (T1.1 – T1.7). Widen `packages/contracts/src/event.ts` in place per Plan-001's contracts package layout: `EventCategory` literal union to 19 entries; enumerate all 123 `SessionEventType` literals; author the named `EventEnvelopeSchema`; bind `CapabilityDetails` typed wrapper for `runtime_node.capability_declared` / `runtime_node.capability_updated` payloads (closes Plan-005 CP-005-5); cross-link version-bound errors to Spec-006 + ADR-018; widen api-payload-contracts.md Plan-006 §EventEnvelope block from 16 to 19 categories; confirm envelope-to-`session_events`-column bijection. + +**Phase 2 — Crypto Protocol Core** (T2.1 – T2.7). RFC 8785 JCS canonicalizer with UTF-16 lex-sort field ordering (Spec-006:546 amended in this PR to describe field membership and lex-sort serialization); BLAKE3 hash-chain + Ed25519 signer; golden-vector contract tests (RFC 8785 Appendix-A vectors + project-specific edge cases); PII indirection codec enforcing encrypt → digest → embed → canonicalize → sign sole-write-path; post-shred signature-verification property test; genesis-row backfill migration; **T2.7 — `DaemonSigningKeySource` interface + `OsKeystoreSealedDaemonSigningKeySource` implementation** (user-ratified 2026-05-28; self-contained signing-key custody using OS-keystore-managed master key + sealed BLOB persisted in the new local `daemon_signing_keys` SQLite table per ADR-004 SQLite-local-state boundary — corrected from the pre-Codex-T4-review draft that mis-located the column on shared-Postgres `sessions`). + +**Phase 3 — Persistence + Maintenance** (T3.1 – T3.5). Append-path service writing the forward-declared integrity columns + Plan-022 Path 1 shred callback; compactor with three triggers (50K events / 90 days / 500MB) + audit-stub format + three-layer category-exclusion enforcement + new `session_events.retention_class` discriminator column (typed column over JSON-field probe); Merkle-anchor service with earlier-of-1000-events-or-300s cadence + new `pending_anchor_uploads` table for durable partition queue (Design A — SQLite table with `UNIQUE(session_id, node_id, start_sequence, end_sequence)`); schema-migration emitter on `AFTER_MIGRATE_OPERATION_FINISH` batch boundary with callback-seam primary path + reconcile-on-startup fallback (hybrid resolution); cross-task contract-test suite + end-to-end shred-safety regression (Plan-006:248 acceptance gate). + +**Phase 4 — Read-Side + SDK + Desktop Stub** (T4.1 – T4.9). Three-check integrity verifier (chain → signature → anchor) plus per-row stub-signature and scalar-binding re-verification for compacted rows, emitting the full 11-value `failureMode` enum per Spec-006:433 (post-amendment for the post-compaction integrity protocol — `anchor_missing_for_compacted_range` + `anchor_signature_invalid` + `stub_signature_invalid` + `stub_scalar_mismatch` are additive-MINOR extensions per ADR-018 §Decision #8); `key-reuse-observer.ts` enforcing `refuse_on_rotation` Sigstore-precedent invariant and halting ingest from colliding NodeId; `replay-service.ts` for `EventReadAfterCursor` + `EventReadWindow` honoring `retentionClass: 'audit_stub'` for compacted regions; new `session_snapshots.{has_compacted_ranges, compacted_range_count}` columns (existing `as_of_sequence` already carries cursor-state semantics; only the compaction-incidence flags are added); `EventSubscription` replay-then-live stream consuming `LocalSubscriptionProducer` with sequence-monotonicity gap-detection in the SDK; Zod replay-shape schemas mirrored from api-payload-contracts.md:726-753; client SDK `eventClient.ts` typed entry-points registered under Plan-007 namespace as `event.readAfterCursor` / `event.readWindow` / `event.subscribe` per CP-006-4; desktop `` narrow-scope renderer (Plan-013 Non-Goal compliance); end-to-end integration tests. + +The per-task `Provides` / `Depends` / `IdempotencyClass` / inline-cite block follows in §Tasks. Each task names its file path, exported symbols, intra-phase + cross-plan dependencies, idempotency classification per [Spec-015 §Idempotency Classes and Recovery Behavior](../specs/015-persistence-recovery-and-replay.md#idempotency-classes-and-recovery-behavior), and Spec-006 / ADR-018 cite anchors. The full audit-trail rationale for each ratification (drift class, options considered, hardened-mode recommendation) is surface-forwarded into this plan body + the [api-payload-contracts.md §Plan-006](../architecture/contracts/api-payload-contracts.md) doc-mirror per AGENTS.md Surface-Forward-Then-Delete; transient working artifacts under `.agents/tmp/research/plan-readiness-audit/plan-006/` are gitignored and deleted post-merge. + - Contracts: See [API Payload Contracts](../architecture/contracts/api-payload-contracts.md) for typed schemas this plan consumes. -1. Define the `EventEnvelope` contract, the 120-event type enum, and the 18-category enum in `packages/contracts/src/events/`. Register `VERSION_FLOOR_EXCEEDED` + `VERSION_CEILING_EXCEEDED` in Error Contracts. -2. Implement the RFC 8785 JCS canonicalizer and the BLAKE3 + Ed25519 signer. Ship golden-vector contract tests covering RFC 8785 test vectors, field-ordering edge cases, null-vs-absent, and millisecond-precision `occurredAt`. -3. Implement `pii-indirection.ts` enforcing the encrypt → digest → embed → canonicalize → sign order as the sole write path for `pii_payload`. Contract tests cover the post-shred signature-verification property. -4. Implement `event-log-service.ts` append path writing Plan-001's forward-declared integrity columns (`monotonic_ns`, `prev_hash`, `row_hash`, `daemon_signature`, `participant_signature`, `pii_payload`). Expose a Plan-022 callback entry point for Path 1 completion that emits `event.shredded`. -5. Implement `compactor.ts` with the three triggers (50K events per session / 90 days / 500MB per-session SQLite) and the audit-stub format per [Spec-006 §Compacted Event Format](../specs/006-session-event-taxonomy-and-audit-log.md). Exclude `audit_integrity` and `event_maintenance` categories. Emit `event.compacted` on each pass. -6. Implement `merkle-anchor-service.ts` with the earlier-of-1000-events-or-300-seconds cadence. Upload metadata-only anchors to shared `event_log_anchors` per ADR-017. -7. Implement `integrity-verifier.ts` — read-side chain, signature, and anchor verification plus the observer-pattern key-reuse detector. Emit `audit_integrity_verified` on success; `audit_integrity_failed` (with the full `failureMode` enum per Spec-006 §Audit Integrity) on failure; `key_reuse_detected` when the rotation invariant `refuse_on_rotation` is violated. -8. Implement `schema-migration-emitter.ts` — emit `schema.migrated` on the `AFTER_MIGRATE_OPERATION_FINISH` batch boundary (per-operation granularity per Spec-006 §Event Maintenance, not per-statement). -9. Implement `replay-service.ts` — `EventReadAfterCursor`, `EventReadWindow`, `EventSubscription` with cursor state tracking compacted regions (via `retentionClass: 'audit_stub'`). -10. Wire the client SDK reads and desktop timeline rendering to display compacted stubs as summarized segments per Spec-006 §Replay Interaction with Compacted Regions. +#### Tasks + +##### Co-Tier Phase Ordering Precondition + +Plan-005 and Plan-006 are co-tier (both Tier 4) in the cross-plan DAG. The Plan-006 Phase 1 contract surface **imports** types Plan-005 Phase 1 ships: + +- `DriverCapabilityFlag` (literal union) +- `NormalizedProviderToolMetadata` (the post-`.default()` `z.output` of `ProviderToolMetadataSchema`; `CapabilityDetails.tools` imports the normalized shape because event payloads cross the persistence boundary) + +Both are exported from `packages/contracts/src/provider-driver.ts` by Plan-005 Phase 1 (T1.1 – T1.4). Until that file lands on `develop`, Plan-006 Phase 1 T1.4 cannot compile. The ordering is therefore: **Plan-005 Phase 1 PR → Plan-006 Phase 1 PR**. PR authors MUST cite the Plan-005 Phase 1 merge commit in the Plan-006 Phase 1 PR description so the precondition is auditable. No reciprocal: Plan-005 Phase 1 has no Plan-006 import. + +##### Phase 1 — Contracts (`packages/contracts/src/event.ts` extension in place) + +- **T1.1 — Widen `EventCategory` enum to 19 canonical entries.** File: `packages/contracts/src/event.ts` (EXTEND). Provides: `EventCategory` literal union with all 19 entries adding `channel_arbitration`, `onboarding_lifecycle`, `cross_node_dispatch` to the existing 16. Depends: existing 16-entry baseline at `event.ts:74-90` (Plan-001's contracts package). IdempotencyClass: N/A (type). Cites: Spec-006:506+533. +- **T1.2 — Enumerate 123 `SessionEventType` literals + category registry.** File: `packages/contracts/src/event.ts` (EXTEND). Provides: `SessionEventType` literal union of all 123 strings; `SESSION_EVENT_CATEGORY_BY_TYPE: ReadonlyMap`; per-category const arrays (`SESSION_LIFECYCLE_EVENT_TYPES`, etc.). Depends: T1.1; existing 3-variant `SessionEvent` union; existing registry pattern at `event.ts:431`. IdempotencyClass: N/A (type/data). Cites: Spec-006:506-533; per-category subsections of Spec-006. +- **T1.3 — Author named `EventEnvelopeSchema` with canonical field set.** File: `packages/contracts/src/event.ts` (EXTEND — refactor existing `buildCommonShape()` output to a named export). Provides: `EventEnvelopeSchema: z.ZodType`; `EventEnvelope` inferred interface; JSDoc citing Spec-006:539-549 + ADR-018 §Decision #1/#2/#6 (the field SET is canonical; serialized order is mandated by RFC 8785 §3.2.3 UTF-16 lex-sort — see Spec-006:546 amendment). Depends: T1.1, T1.2, existing `EventEnvelopeVersionSchema` brand. IdempotencyClass: N/A (type). Cites: Spec-006:539-549; ADR-018 §Decision #1, #2, #6. +- **T1.4 — Bind `CapabilityDetails` + `RuntimeNodeCapability*Payload` interfaces.** File: `packages/contracts/src/event.ts` (EXTEND; imports `DriverCapabilityFlag` + `NormalizedProviderToolMetadata` from `./provider-driver`). Provides: `CapabilityDetails = { flags: Record; contractVersion: string; tools: readonly NormalizedProviderToolMetadata[] }` (`tools` is the normalized shape — `CapabilityDetails` is an event payload that crosses the persistence / event boundary); `RuntimeNodeCapabilityDeclaredPayload = { capability, capabilityDetails }`; `RuntimeNodeCapabilityUpdatedPayload = { capability, previousState, newState }`. Depends: **Plan-005 Phase 1 (T1.1–T1.4) MUST ship `packages/contracts/src/provider-driver.ts` exporting `DriverCapabilityFlag` + `NormalizedProviderToolMetadata` before this task can compile** — see §Co-Tier Phase Ordering Precondition above. IdempotencyClass: N/A (type). Cites: Spec-006:375-376; **closes Plan-005 CP-005-5**. +- **T1.5 — Cross-link version-bound errors to Spec-006 + ADR-018 (JSDoc-only).** File: `packages/contracts/src/event.ts` (EXTEND JSDoc on `EventEnvelopeVersionSchema`); optional reciprocal cite in `docs/architecture/contracts/error-contracts.md:218-225`. Provides: JSDoc cross-link pointer; no new schemas (Plan-001 T2.3 already shipped `VersionFloorExceededErrorSchema` + `VersionCeilingExceededErrorSchema` per Phase 1 audit forward-completed surface). Depends: Plan-001 T2.3 shipped artifacts. IdempotencyClass: N/A (docs). Cites: Spec-006:76-90; ADR-018 §Decision #4, #10. +- **T1.6 — Widen api-payload-contracts.md Plan-006 §EventEnvelope block to 19 categories.** File: `docs/architecture/contracts/api-payload-contracts.md` (EDIT Plan-006 §EventEnvelope block, ~line 705-723). Provides: Updated `EventCategory` doc-mirror to 19 entries + comment "16 categories total" → "19 categories total (123 events per Spec-006:506+533)". Already applied in this audit PR. Depends: T1.1. IdempotencyClass: N/A (docs). Cites: Spec-006:506+533. +- **T1.7 — Confirm envelope ↔ `session_events` column bijection.** File: None (verification + JSDoc anchor). Provides: Type-level assertion that every required `EventEnvelope` field maps to a column on `session_events`; phase boundary clarification (Phase 1 does not author integrity-column population — that lands in Phase 2). Depends: Plan-001 forward-declared columns per cross-plan-deps.md:25. IdempotencyClass: N/A (verification). Cites: Spec-006:539-549; local-sqlite-schema.md `session_events`. + +##### Phase 2 — Crypto Protocol Core (`packages/runtime-daemon/src/events/`) + +- **T2.1 — RFC 8785 JCS canonicalizer.** File: `packages/runtime-daemon/src/events/canonicalizer.ts` (NEW). Provides: `canonicalizeEvent(envelope: EventEnvelope): CanonicalBytes` returning phantom-branded `Uint8Array` constructible only inside this module; `canonicalizeJson(value: unknown): CanonicalBytes` for Spec-024 `request_body_hash` reuse per CP-006-3. Depends: `EventEnvelope` + `EventEnvelopeVersion` (T1.3); pinned JCS library (`canonicalize@3.0.0` — Erdtman's RFC 8785 reference implementation, Apache-2.0, zero-deps, bundled TS types; **exact-pinned**, not a caret range: the canonical bytes feed every event `row_hash` + `daemon_signature`, so the canonicalizer version is locked and the T2.3 golden-vector suite binds its output to RFC 8785 §A — a silent minor bump must never change canonical bytes). IdempotencyClass: `idempotent`. Cites: Spec-006:539-549; RFC 8785 §3.2.3 (UTF-16 lex-sort mandate, verbatim); ADR-018 §version brand semantics. +- **T2.2 — BLAKE3 hash-chain + Ed25519 signer.** File: `packages/runtime-daemon/src/events/signer.ts` (NEW). Provides: `signRow(canonical: CanonicalBytes, prevHash: Uint8Array, signingKey: Ed25519PrivateKey): SignedRow`; `verifyRow(...)` for read-side; `GENESIS_PREV_HASH` (32 zero bytes). Depends: T2.1; `@noble/hashes/blake3.js` + `@noble/curves/ed25519.js` (already pinned in crypto-paseto); T2.7 daemon signing-key source. IdempotencyClass: `idempotent` (RFC 8032 Ed25519 determinism). Cites: Spec-006:535-540; Security architecture:378 ("**same** canonical_bytes(row) feeds both row_hash and daemon_signature"); RFC 8032 §5.1; BLAKE3 spec. +- **T2.3 — Golden-vector tests for canonicalizer.** File: `packages/runtime-daemon/src/events/__tests__/canonicalizer.golden.test.ts` (NEW). Provides: Vitest suite asserting byte-stable output across field-order drift, null-vs-undefined, clock-tick boundaries, nested-payload lex-sort, numeric edge cases per ECMA-262 ToString, `version: "1.0"` literal, RFC 8785 Appendix-A vectors. Inline hex fixtures (matches crypto-paseto convention). Depends: T2.1, T2.2. IdempotencyClass: N/A (test). Cites: Spec-006:539-549; RFC 8785 §A. +- **T2.4 — PII indirection codec (sole write path).** File: `packages/runtime-daemon/src/events/pii-indirection.ts` (NEW). Provides: `writeEventWithPii(input: RawEventInput, db, encryptor, signingKey): Promise` as the one-and-only path writing non-null `pii_payload`; branded `PiiPayloadCiphertext` constructible only here; `PiiEncryptor` interface (implementation owned by Plan-022 per CP-006-1). Depends: T2.1, T2.2; `splitPii()` shape from Plan-022 working copy (interface only; runtime injection at composition root). IdempotencyClass: `manual_reconcile_only` per AES-256-GCM random-nonce non-replay-safety (NIST SP 800-38D §8.2). Cites: Spec-022 §Signature Safety Under Shred:323-350 (encrypt→digest→embed→canonicalize→sign verbatim); Spec-022 §PII Payload Column Pattern; Spec-015 §Idempotency Classes. +- **T2.5 — Post-shred signature-verification property test.** File: `packages/runtime-daemon/src/events/__tests__/post-shred-verify.test.ts` (NEW). Provides: Vitest property suite asserting `writeEventWithPii(...)` produces canonical bytes including `pii_ciphertext_digest`; after `pii_payload = NULL` (Path 1), `daemon_signature` still verifies; tampering any envelope field post-shred → verification fails. Depends: T2.4. IdempotencyClass: N/A (test). Cites: Spec-022:323-350; Spec-022 §Shred Fan-Out Path 1. +- **T2.6 — Genesis-row backfill migration.** File: `packages/runtime-daemon/src/migrations/0NNN-genesis-chain-backfill.ts` (NEW; NNN at PR time). Provides: Migration walking placeholder rows (32-byte zero buffers from Plan-001 T2.3) in `sequence` order; computes `row_hash = BLAKE3(prevHash || canonical_bytes)` + `daemon_signature` for each; detect-real-signature skip on re-run. Depends: T2.1, T2.2, CP-006-2 (Plan-001 placeholder convention). IdempotencyClass: `idempotent` (detect-real-signature skip on re-run). Cites: Plan-001 T2.3 placeholder convention; Spec-006:540. +- **T2.7 — DaemonSigningKeySource interface + OsKeystore-sealed implementation + `daemon_signing_keys` SQLite table.** File: `packages/runtime-daemon/src/events/signing-key-source.ts` (NEW). Provides: `DaemonSigningKeySource` interface (`create(sessionId): Promise<{publicKey: Ed25519PublicKey}>` — returns the PUBLIC key ONLY so daemon-private signing material never crosses the Plan-006/Plan-002 boundary; the freshly generated private key is sealed in-place and is reachable solely through the signer-local `read(sessionId): Promise` path); `OsKeystoreSealedDaemonSigningKeySource` implementation generating fresh per-session Ed25519 keypair, sealing the private key via OS-keystore-managed master key (`@napi-rs/keyring` v1.2.0 per [Spec-022 §Daemon Master Key :146](../specs/022-data-retention-and-gdpr.md) — Keychain `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` on macOS / `CRED_TYPE_GENERIC` `CRED_PERSIST_LOCAL_MACHINE` on Windows / Secret Service via libsecret + kwallet6 + keyutils fallback on Linux; supersedes prior `keytar` reference — keytar is unmaintained), **storing ciphertext in the new local `daemon_signing_keys` SQLite table (`session_id` TEXT PK, `public_key` BLOB NOT NULL, `sealed_private_key` BLOB NOT NULL, `created_at` TEXT NOT NULL, `rotated_at` TEXT nullable)** per ADR-004 SQLite-local-state boundary — daemon-private secrets are per-machine and MUST NOT live in shared-Postgres `sessions` (corrected post-Codex T4 review on PR #124; the canonical schema doc-mirror at [local-sqlite-schema.md §Audit Log Crypto Tables](../architecture/schemas/local-sqlite-schema.md#audit-log-crypto-tables-plan-006) is the source of truth); new additive migration `0NNN-daemon-signing-keys.ts` creates the table. Depends: T2.1, T2.2 (consumer); Plan-002 amendment-via-extension adds session-create call-site + roster public-key registration per CP-006-7. IdempotencyClass: `manual_reconcile_only` per per-session key generation non-replay-safety. Cites: Spec-022:146 (canonical keystore library); Spec-006:382 (roster registration); ADR-004 §Decision (SQLite-local-state boundary); signing-key custody resolution user-ratified 2026-05-28; ADR-010 (key-rotation semantics). + +##### Phase 3 — Persistence + Maintenance (`packages/runtime-daemon/src/events/`) + +- **T3.1 — Append-path service writing integrity columns + Plan-022 Path 1 shred callback.** File: `packages/runtime-daemon/src/events/event-log-service.ts` (NEW). Provides: `EventLogService.append(envelope, options): Promise<{id, sequence, rowHash}>` as the sole append path under per-session mutex; `registerShredCallback(handler: ShredCallback): void` invoked by Plan-022's Path 1 orchestrator after `participant_keys` DELETE commits; refuses any `append()` whose `payload` carries a PII-tagged field without `pii_ciphertext_digest` (surfaces `daemon.pii_split_bypass` typed error). Depends: T2.1, T2.2, T2.4, T2.7; Plan-001 forward-declared columns; Plan-001 `I-001-2` (sequence as canonical replay key). IdempotencyClass: `manual_reconcile_only` per chain-append non-retry-safety. Cites: Plan-006:181 (Plan-022 callback); Security Architecture §Audit Log Integrity; Plan-001:128 forward-declared columns. +- **T3.2 — Compactor with three triggers + anchor-before-compaction protocol + audit-stub format + `retention_class` discriminator (Design B — typed column).** File: `packages/runtime-daemon/src/events/compactor.ts` (NEW). Provides: `Compactor.tick(): Promise` invoked by daemon idle scheduler; SQL selector excluding `category IN ('audit_integrity', 'event_maintenance')` (layer 1 of three-layer enforcement); **anchor-before-compaction enforcement per [Spec-006 §Post-Compaction Integrity](../specs/006-session-event-taxonomy-and-audit-log.md#post-compaction-integrity) — before mutating any row in the to-be-compacted range, verify a covering Merkle anchor exists in `pending_anchor_uploads` or `event_log_anchors`; if absent, force-fire via T3.3 `MerkleAnchorService.anchorRange({sessionId, fromSeq, toSeq})` and wait for durable queue insertion before proceeding (refuses to compact if force-fire fails)**; **per-row stub commitment — for each row, build the audit-stub projection (Spec-006 §Compacted Event Format), serialize it once to its canonical byte string `B = canonical_bytes(stub)`, compute `stub_signature = Ed25519(B)` with the daemon signing key, then store that **same** `B` verbatim in `payload` (sign-exact-bytes invariant — no re-serialization between signing and storing, so the verifier checks `stub_signature` directly over the stored `payload` bytes; canonical bytes bind `id`+`sequence`, so the signature is non-replayable across rows)**; audit-stub UPDATE that **REPLACES** `payload` with that canonical byte string `B` (the JCS-canonicalized audit-stub projection) (the column is `NOT NULL` — it is rewritten, NOT nulled — so replay/renderer can still surface the visible stub per Spec-006 §Replay Interaction), NULLs `correlation_id`/`causation_id`/`pii_payload`, sets `retention_class = 'audit_stub'`, and writes `stub_signature`, but NEVER mutates `prev_hash`/`row_hash`/`daemon_signature`/`participant_signature`/`monotonic_ns`/`version` (I-006-3-03); `event.compacted` envelope per Spec-006 §Event Maintenance; **new additive migration `0NNN-retention-class-and-stub-signature.ts` adds `session_events.retention_class TEXT CHECK (retention_class IS NULL OR retention_class = 'audit_stub')` (typed discriminator over JSON-field probe; the column-level CHECK closes the discriminator domain and is ALTER-ADD-COLUMN-addable — it references only the new column and permits NULL, so pre-migration rows pass) + `session_events.stub_signature BLOB` (per-row post-compaction commitment, NULL for live rows) columns. The co-presence invariant (`retention_class = 'audit_stub'` ⟺ non-NULL `stub_signature`) is a two-column constraint that ALTER cannot add as a table-level CHECK without a 12-step rebuild of the append-only audit log; it is enforced at the verification layer instead (T4.1 — NULL `stub_signature` on an `audit_stub` row → `stub_signature_invalid`).** Depends: T3.1 (event.compacted emission); T3.3 (`MerkleAnchorService.anchorRange()` API extension for force-fire); T2.7 daemon signing key (consumer — `stub_signature` minting); Phase 1 contracts. IdempotencyClass: `compensable` per mid-pass replay-safety (re-entry skips already-stubbed rows via `retention_class IS NULL`; anchor force-fire is idempotent per T3.3 `UNIQUE(session_id, node_id, start_sequence, end_sequence)`). Cites: Spec-006 §Event Compaction Policy (three triggers); Spec-006 §Compacted Event Format; Spec-006 §Post-Compaction Integrity (anchor-before-compaction protocol + per-row stub_signature commitment); Plan-006:198-202 three-layer enforcement. +- **T3.3 — Merkle-anchor service with earlier-of-1000-or-300s cadence + durable partition queue (Design A — SQLite-backed queue) + force-fire anchor-range API.** Files: `packages/runtime-daemon/src/events/merkle-anchor-service.ts` (NEW); `packages/contracts/src/event-anchor.ts` (NEW Plan-006-owned); `packages/control-plane/src/event-anchors/anchor-router.ts` + `anchor-store.ts` (NEW). Provides: `MerkleAnchorService.onEventAppended({sessionId, sequence, rowHash}): Promise` cadence-driven hook; `MerkleAnchorService.anchorRange({sessionId, fromSeq, toSeq}): Promise` force-fire entry-point consumed by T3.2 compactor's anchor-before-compaction protocol per Spec-006 §Post-Compaction Integrity — synchronously computes the Merkle root over `[fromSeq, toSeq]`, signs with the daemon Ed25519 key, durably inserts into `pending_anchor_uploads`, and returns once the row is queued (does NOT await control-plane upload — local queue's durable monotonic ordering + daemon signature is sufficient for anchor-before-stub gating); `AnchorPayload` type bound to the seven `event_log_anchors` columns (no `payload` field; structurally enforces ADR-017 metadata-only constraint per I-006-3-02); BLAKE3 binary Merkle tree with RFC-9162 §2.1 odd-leaf duplication; **new additive migration `0NNN-pending-anchor-uploads.ts` adds a durable partition queue table with `UNIQUE(session_id, node_id, start_sequence, end_sequence)` so unflushed anchors survive daemon restart without re-signing per Plan-006:151 AND so `anchorRange()` force-fire is idempotent against re-entry after crash.** Depends: T3.1 hook; T2.2 signer (Ed25519 over merkle root); Plan-006 `event_log_anchors` shipped via shared-postgres-schema.md:363-378; Plan-008-bootstrap `host.ts` for router mount per CP-006-2. IdempotencyClass: `idempotent` (Postgres `ON CONFLICT DO NOTHING`; no re-signing on retry per Plan-006:151; `anchorRange()` is doubly idempotent — its coverage pre-check (`∃ anchor: start_sequence ≤ fromSeq AND end_sequence ≥ toSeq`) short-circuits a force-fire when a wider anchor already covers the range, and the `UNIQUE(session_id, node_id, start_sequence, end_sequence)` key makes a genuine re-fire of the **same** `[fromSeq, toSeq]` return the queued row without re-signing — distinct ranges sharing a `start_sequence` are NOT collapsed). Cites: Spec-006 §Anchoring Cadence; Spec-006 §Post-Compaction Integrity (anchor-before-compaction protocol consumer); ADR-017 §Decision; Security Architecture §Merkle Anchors; Plan-006:151 partition tolerance. +- **T3.4 — Schema-migration emitter on `AFTER_MIGRATE_OPERATION_FINISH` batch boundary (hybrid callback + reconcile design).** File: `packages/runtime-daemon/src/events/schema-migration-emitter.ts` (NEW); Plan-006 contract addition: `SchemaMigratedPayload` shape with `{fromVersion, toVersion, migrationId, description, checksum, appliedBy, executionMs, success}`. Provides: `SchemaMigrationEmitter.emitBatchCompletion(batch: MigrationBatchResult): Promise` invoked via callback seam on the migration runner (primary path) OR via reconcile-on-startup `MAX(sequence) WHERE type = 'schema.migrated'` gap fill (fallback). BLAKE3 `checksum` over concatenated migration file contents defends against silent migration-file divergence. Depends: T3.1; Plan-001 `schema_version` table (Plan-001 owns; Plan-006 reads). IdempotencyClass: `manual_reconcile_only` per crash-between-commit-and-emit reconcile semantics. Cites: Plan-006:226 (Step 8); Spec-006 §Event Maintenance (Flyway-precedent batch granularity); Plan-001 `schema_version` table. +- **T3.5 — Phase 3 contract-test suite + end-to-end shred-safety regression (Plan-006:248 acceptance gate).** Files: Five `__tests__/` files spanning T3.1-T3.4 + shred-safety E2E. Provides: Genesis-and-multi-row chain integrity test; trigger tests for each of the three compaction thresholds; cadence test for the earlier-of rule with the `AnchorPayload` no-`payload`-field TypeScript structural assertion; batch-boundary granularity (1 batch → 1 event) + rollback path + reconcile-on-startup; **E2E: 60+ events with non-null `pii_payload` → compaction pass → Plan-022 Path 1 crypto-shred via `participant_keys` DELETE → `event.shredded` callback → integrity verifier reruns over full chain → all signatures verify, all chain hashes verify, `` markers replace PII fields on read.** Depends: T3.1-T3.4; Phase 2; Plan-022's `splitPii()` (mock via fixture if Plan-022 implementation hasn't landed). IdempotencyClass: N/A (test). Cites: Plan-006:239-248 §Test And Verification Plan; Spec-022 §Signature Safety Under Shred; Plan-022:140-148 five-point proof. + +##### Phase 4 — Read-Side + SDK + Desktop Stub + +- **T4.1 — `integrity-verifier.ts` three-check verifier + full 11-value `failureMode` enum + anchor + stub-signature + scalar-binding compacted-row verification.** Files: `packages/runtime-daemon/src/events/integrity-verifier.ts` (NEW) + test. Provides: `verifyRange(params): Promise` returning `{kind: 'verified' | 'failed', ...}` with the 11-value `failureMode` enum (`'hash_mismatch' | 'signature_mismatch' | 'anchor_mismatch' | 'inclusion_proof_failed' | 'consistency_proof_failed' | 'log_file_missing' | 'log_file_moved' | 'anchor_missing_for_compacted_range' | 'anchor_signature_invalid' | 'stub_signature_invalid' | 'stub_scalar_mismatch'`) verbatim from Spec-006:433 (post-amendment — the four `anchor_missing_for_compacted_range` / `anchor_signature_invalid` / `stub_signature_invalid` / `stub_scalar_mismatch` modes are additive-MINOR extensions per ADR-018 §Decision #8 sanctioning the post-compaction integrity protocol); `VerifierFailureModeSchema` + `VerifierFailurePathSchema` (`'inclusion' | 'consistency' | 'signature'`) Zod schemas; appends `audit_integrity_verified` OR `audit_integrity_failed` per pass (I-006-4-01 exactly-one). **Compacted-row handling per Spec-006 §Post-Compaction Integrity: for any row with `retention_class = 'audit_stub'`, run THREE checks (all must pass): (a) skip per-row chain recomputation against the original (canonical bytes unrecoverable) and instead verify a covering Merkle anchor exists in `pending_anchor_uploads` or `event_log_anchors` AND its daemon Ed25519 `root_signature` verifies — anchor-missing → `failureMode: 'anchor_missing_for_compacted_range'`, anchor-signature-invalid → `failureMode: 'anchor_signature_invalid'`; (b) verify `stub_signature` directly over the canonical byte string stored in `payload` (the exact bytes the compactor signed — not re-canonicalized, not reconstructed from scalar columns) using the `NodeId`-resolved daemon Ed25519 public key — tampered/replayed/missing `stub_signature` → `failureMode: 'stub_signature_invalid'` (the signature is REQUIRED on every `audit_stub` row; absence is a failure, never a skip); (c) decode the audit-stub projection from the just-verified `payload` bytes and assert each surviving scalar column (`id`, `session_id`, `sequence`, `occurred_at`, `category`, `type`, `actor`) byte-equals its projection counterpart (`occurred_at` ↔ `occurredAt`, `session_id` ↔ `sessionId`, …; `compactedAt`/`summary` have no scalar column) — any divergence → `failureMode: 'stub_scalar_mismatch'` (closes the forge-a-scalar-column gap where `stub_signature` over `payload` and the anchor over the frozen `row_hash` both still verify but a filter/reconstruction reads the tampered scalar). Mixed ranges (some uncompacted, some `audit_stub`) are split-verified: chain-recompute the uncompacted prefix + run all three compacted-row checks on the compacted suffix; all must pass.** Depends: Phase 1 contracts; Phase 2 canonicalizer + signer; Plan-001 `session_events` table; Plan-006 P3 `event_log_anchors` + `pending_anchor_uploads`. IdempotencyClass: `compensable` per per-pass new-row append (consumers dedupe on `(verifierNodeId, fromSeq, toSeq)`). Cites: Spec-006:135-141; Spec-006:432-433 (amended to 11-value enum); Spec-006 §Post-Compaction Integrity (anchor + stub-signature + scalar-binding verification); Security Architecture §Audit Log Integrity (verification rule order); **Spec-006:433 supersedes security-architecture.md:409's interim `failureKind` terminology (security-architecture.md fixed in this PR).** +- **T4.2 — `key-reuse-observer.ts` enforcing `refuse_on_rotation`.** Files: `packages/runtime-daemon/src/events/key-reuse-observer.ts` (NEW) + test. Provides: `KeyReuseObserver` class with `start(sessionId)` / `stop(sessionId)` / `getDetectedReuse()`; emits `key_reuse_detected` with `rotationInvariantViolated: 'refuse_on_rotation'` verbatim (Spec-006:434); HALTS ingest of further events from the colliding signer's `NodeId` (I-006-4-03). Depends: Phase 1 `EventEnvelope` + `NodeId`; Phase 2 Ed25519 fingerprint; Plan-007 `LocalSubscriptionConsumer`; Plan-001 participant-roster snapshot. IdempotencyClass: `compensable` per dedupe-by-`(offendingKeyFingerprint, detectorNodeId)`. Cites: Spec-006:434 (Sigstore-precedent observer pattern); ADR-010 §Key rotation; Security Architecture §Per-Event Daemon Signature. +- **T4.3 — `replay-service.ts` for `EventReadAfterCursor` + `EventReadWindow`.** Files: `packages/runtime-daemon/src/events/replay-service.ts` (NEW) + test. Provides: `readAfterCursor(params): Promise` returning `{events, nextCursor, hasMore}`; `readWindow(params): Promise` bounded by `[fromSequence, toSequence]`; events from compacted regions returned as audit-stub envelopes with `payload.retentionClass === 'audit_stub'` (I-006-4-04 — never silent omission). Depends: T4.6 Zod schemas; Phase 1 contracts; Phase 3 T3.2 compactor format; Plan-001 daemon-migration substrate; ADR-018 §Decision #6 + #9 (upcaster chain on read; unknown-MAJOR accept-and-stub). IdempotencyClass: `idempotent` (pure read). Cites: Spec-006:53; Spec-006:69-72; Spec-006:605-609; api-payload-contracts.md:726-746. +- **T4.4 — `session_snapshots` extension with compacted-region cursor state (Reading (a) — additive flag columns only).** Files: `packages/runtime-daemon/src/migrations/0NNN-session-snapshots-compaction-cursor.ts` (NEW) + doc-mirror amendment to local-sqlite-schema.md. Provides: Two additive columns `has_compacted_ranges BOOLEAN NOT NULL DEFAULT 0` + `compacted_range_count INTEGER NOT NULL DEFAULT 0`. Existing `as_of_sequence` already satisfies the "replay-cursor state" clause of Plan-006:65; only the compaction-incidence flags are NEW. Depends: Plan-001 migration runner; existing `session_snapshots` table (Plan-001-owned, Plan-006 extends per local-sqlite-schema.md:57). IdempotencyClass: `idempotent` (migration). Cites: Plan-006:65; local-sqlite-schema.md:57. +- **T4.5 — `EventSubscription` SSE/JSON-RPC notification stream.** Files: `packages/runtime-daemon/src/events/event-subscription.ts` (NEW) + test + IPC handler. Provides: `subscribe(params: EventSubscriptionRequest): LocalSubscriptionProducer` returning daemon-side producer; streams in two halves (replay from `afterCursor` then live append); monotonic cursor handoff at boundary (I-006-4-05); backpressure model verified at design-review against Plan-007 CP-007-4 `LocalSubscriptionProducer` contract (pre-dispatch verification clause). Depends: T4.3; Phase 1; Plan-007 IPC substrate; Plan-008-bootstrap SSE substrate. IdempotencyClass: `idempotent` per open-call (append-only log). Cites: Spec-006:72; api-payload-contracts.md:280-298 SSE wire frame; api-payload-contracts.md:748-753. +- **T4.6 — Zod replay-shape schemas.** File: `packages/contracts/src/event.ts` (EXTEND with replay-shape Zod schemas; co-located per Plan-001's contracts package layout rather than a separate `replay-shapes.ts`). Provides: `EventReadAfterCursorRequestSchema` + `Response`; `EventReadWindowRequestSchema` + `Response`; `EventSubscriptionRequestSchema`; all `z.infer<>` typed exports. Mirrors api-payload-contracts.md:726-753 verbatim. Depends: Phase 1 contracts (`EventEnvelope`, `EventCursor`, `SessionId`). IdempotencyClass: N/A (type). Cites: Spec-006:71-72; api-payload-contracts.md:726-753. +- **T4.7 — Client SDK `eventClient.ts` typed entry points.** Files: `packages/client-sdk/src/eventClient.ts` (NEW) + test; `packages/client-sdk/src/index.ts` (EXTEND). Provides: `EventClient` interface (`readAfterCursor`, `readWindow`, `subscribe`); `createDaemonEventClient(client: JsonRpcClient): EventClient` factory (daemon-only per ADR-017 — no control-plane variant since event reads go through daemon IPC, not control-plane endpoints). Sequence-monotonicity gap-detection on `subscribe` per the live-stream gap-detection resolution. Depends: T4.6 Zod schemas; Plan-007 transport substrate. IdempotencyClass: SDK wrapper inherits transport semantics. Cites: Spec-006:71-72; Plan-006:58 target area; **registers `event.readAfterCursor` / `event.readWindow` / `event.subscribe` namespaces under Plan-007 registry per CP-006-4**. +- **T4.8 — Desktop `` narrow-scope renderer.** Files: `apps/desktop/src/renderer/src/timeline/CompactedStubSegment.tsx` (NEW) + test. Provides: React component consuming `EventEnvelope` carrying audit-stub payload `{id, sessionId, sequence, occurredAt, category, type, actor, compactedAt, retentionClass: 'audit_stub', summary}` per Spec-006:587-599; single-line summary render + dimmed/badged visual distinction from live events; **NEVER renders null or empty** (I-006-4-07: audit stubs are always visibly present). Depends: T4.7 `EventClient`; Phase 1; Phase 3 T3.2 audit-stub format; existing renderer substrate (Plan-002 P5/P6 bridge pattern). IdempotencyClass: N/A (UI). Cites: Spec-006:608-609 (client detection + render); Spec-006:587-599 audit-stub format; Plan-006:27 (full timeline = Plan-013 Non-Goal). +- **T4.9 — Wire client SDK Zod parse + desktop integration E2E.** Files: `packages/client-sdk/src/eventClient.integration.test.ts` + `apps/desktop/src/renderer/src/timeline/CompactedStubSegment.integration.test.tsx` (NEW). Provides: Integration test driving `eventClient.readAfterCursor` against a fixture log with compacted ranges; assert SDK does NOT throw or silently omit audit-stub rows; renderer integration test feeding SDK-parsed envelope into `` and asserting visible DOM segment with summary text + "compacted" indicator. Depends: T4.1, T4.3, T4.5, T4.6, T4.7, T4.8. IdempotencyClass: N/A (test). Cites: Spec-006:63-64; Spec-006:608-609. + +## Open Authoring Decisions (Category 2 — Audit-Surfaced) + +These are decisions the spec or prior plan iterations did not settle, surfaced by the 2026-05-28 Plan-006 audit. Load-bearing decisions are ratified inline; all others are resolved in-PR with hardened-mode defaults. + +- **Event contracts directory placement.** Resolved: Extend `packages/contracts/src/event.ts` top-level per Plan-001's contracts package layout + memory `feedback_extend_dont_fork`. The `events/` subdirectory referenced by prior plan iterations does not exist; no other plan adopts a subdirectory pattern. +- **RFC 8785 lex-sort vs Spec-006:546 listed order.** Resolved: RFC 8785 §3.2.3 UTF-16 code-unit lex-sort is non-negotiable (the canonical-bytes invariant depends on it). Spec-006:546 lists field MEMBERSHIP, not serialized order — amended in this audit PR ("Fields included (the canonical set; serialized order is mandated by RFC 8785 §3.2.3 UTF-16 code-unit lex-sort of member names, not the order listed here)"). T2.3 golden vectors assert the lex-sorted output as ground truth. +- **Daemon Ed25519 signing-key custody.** Resolved (user-ratified 2026-05-28): Plan-006 T2.7 self-contains. Phase 2 ships `DaemonSigningKeySource` interface + `OsKeystoreSealedDaemonSigningKeySource` implementation; per-session Ed25519 sealed via OS-keystore master key (`@napi-rs/keyring` v1.2.0 per [Spec-022:146](../specs/022-data-retention-and-gdpr.md) — Keychain/libsecret/DPAPI; supersedes prior `keytar` reference); stored as ciphertext in new local `daemon_signing_keys` SQLite table per ADR-004 SQLite-local-state boundary (post-Codex-T4-review redesign 2026-05-28; supersedes the pre-review draft that mis-located the column on shared-Postgres `sessions`). Plan-002 amendment-via-extension adds session-create call-site + roster public-key registration per CP-006-7. No Plan-022 (Tier 5) dependency → no tier inversion. +- **JCS library: vendor vs in-house.** Resolved: Vendor — `canonicalize` package (Erdtman's RFC 8785 reference implementation; ships with the RFC). T2.3 lifts RFC 8785 Appendix-A vectors verbatim as a sub-suite to catch version-drift bugs. In-house implementation rejected per ECMA-262 ToString edge-case risk. +- **Golden-vector storage: inline hex vs JSON fixture.** Resolved: Inline hex strings in the test file. Matches crypto-paseto convention (`packages/crypto-paseto/src/__tests__/v4-local.test.ts:100`); PR-diff is the audit trail for byte-stable artifacts. +- **`pii_ciphertext_digest` position + wire shape.** Resolved: Top-level field on `payload` (matches Spec-006:549 + Spec-022:333 literal reading); wire shape is **lowercase hex 64-char string** (matches BLAKE3 reference output convention; T2.3 golden vectors are reviewer-friendly in hex). +- **`participantSignature` minting boundary.** Resolved: Phase 2 signer exports both `mintParticipantSignature(...)` (write-side helper accepting participant private key) and `verifyParticipantSignature(...)` (read-side); the WHICH-events-are-sensitive enum lives in `packages/contracts/src/event.ts` Phase 1 extension; the WHEN-to-mint decision is Plan-002 / Plan-022 territory. +- **Partition-anchor queue durability.** Resolved: Design A — new SQLite table `pending_anchor_uploads` with columns mirroring `AnchorPayload` plus `attempt_count`, `last_attempt_at`, `last_error`; `UNIQUE(session_id, node_id, start_sequence, end_sequence)` constraint. Survives daemon restart; queryable for operator triage; storage footprint bounded at ≤ 1 anchor per 300s per session. Design B (reconstruct from Postgres deltas) fails the no-re-signing constraint; Design C (in-memory) fails durability. +- **Audit-stub representation: column add vs payload discriminator.** Resolved: Design B — new `session_events.retention_class TEXT` column. Partial index `WHERE retention_class IS NULL` keeps hot-path replay queries fast; explicit column makes stub state visible at row inspection; three-layer enforcement easier with typed column than JSON-field probe. +- **Migration-runner integration: callback vs polling.** Resolved: Hybrid — callback seam on the runner (primary path; immediate emission via `onBatchFinish(handler)`) + startup reconciliation fallback (`MAX(sequence) WHERE type = 'schema.migrated'` gap fill, guarding against crash between commit and emit). Callback amendment to Plan-001 migration-runner per cross-plan-deps.md §2 housekeeping exception (Plan-006 may fix-in-place since the change adds a callback seam without altering semantics). +- **`session_snapshots` extension scope.** Resolved: Reading (a) — existing `as_of_sequence` already carries cursor-state semantics per local-sqlite-schema.md:61; T4.4 adds ONLY the compaction-incidence flag columns `has_compacted_ranges` + `compacted_range_count`. Plan-015 owns the separate `replay_cursors` table for projector resumption; the two surfaces answer different questions. +- **`EventSubscription` backpressure model.** Resolved: Phase 4 implementer reads Plan-007 CP-007-4 `LocalSubscriptionProducer` contract first; if Plan-007 ratifies a backpressure model there, T4.5 follows. If Plan-007 CP-007-4 is silent at T4.5 dispatch time, surface as a Plan-007 ratification before T4.5 lands. **Phase 4 owes pre-dispatch verification.** +- **`summary` field max-length and generation rule.** Resolved: Deferred to Phase 3 T3.2 (`compactor.ts` is the emission site). T4.8's `` MUST handle arbitrary lengths gracefully (CSS truncation + tooltip-on-hover); renderer-side resilience does not gate Phase 5's emission rule. +- **Live-stream gap detection mechanism.** Resolved: Sequence-number monotonicity check in the SDK's `EventClient.subscribe` (Option (b)) as canonical gap-detection; SSE reconnect via `Last-Event-ID` header (Option (a)) as reconnect path. Heartbeat-with-cursor (Option (c)) deferred unless Plan-008-bootstrap heartbeat substrate adds it natively. + +## Cross-Plan Obligations + +Each obligation gets a CP-006-N ID. The Cross-Plan Dependency Graph entry for Plan-006 (cross-plan-dependencies.md §3 row 135) mirrors this list. + +- **CP-006-1 — Plan-022 PII codec interface boundary.** Phase 2 T2.4's `pii-indirection.ts` consumes `PiiEncryptor` as an interface; Plan-022 (Tier 5) ships the AES-256-GCM + HKDF-SHA256 + XChaCha20-Poly1305 implementation. Phase 2 ships interface + test-only stub; composition root (Plan-001 / Plan-003 bootstrap) MUST inject the real codec — never the stub — and ships a runtime assertion against the stub outside tests. +- **CP-006-2 — Anchor-router mount on Plan-008-bootstrap.** Phase 3 T3.3 ships `packages/control-plane/src/event-anchors/anchor-router.ts` as a tRPC procedure; mount point is Plan-008-bootstrap `host.ts` per cross-plan-deps.md §2. Plan-006 extends; Plan-008 substrate owns. +- **CP-006-3 — RFC 8785 JCS shared with Spec-024 dispatch.** Spec-024 §request_body_hash computes a BLAKE3 digest over RFC-8785-canonicalized JSON. Phase 2 T2.1's `canonicalizeEvent` + `canonicalizeJson` are the workspace's single source of truth for RFC 8785; Plan-027 (Spec-024 implementer) MUST consume — never re-implement. +- **CP-006-4 — `event.*` JSON-RPC namespace registration with Plan-007.** Phase 4 T4.7 registers `event.readAfterCursor`, `event.readWindow`, `event.subscribe` under Plan-007's namespace registry per Plan-007 I-007-9. Plan-007 Phase 4 (Tier 4) MUST accept these three method-name registrations; without them, the SDK seam fails to register at daemon startup. Reciprocal entry on Plan-007's side per CP-007-N. +- **CP-006-5 — `CapabilityDetails` + `providerFailureDetail` carry-forward with Plan-005 (CLOSES CP-005-5).** Phase 1 T1.4 binds typed `CapabilityDetails` interface for `runtime_node.capability_*` event payloads; Phase 3 audit-mirror adds `providerFailureDetail?: string` to `run.failed` payload (api-payload-contracts.md:868-878 `RunStateChangeEvent`). Both are ADR-018-compliant additive-only MINOR additions. **Discharge artifacts applied in this audit PR.** +- **CP-006-6 — Plan-001 forward-declared schema is immutable.** Plan-006 Phase 3 reads columns shipped by Plan-001 in `0001-initial.ts` but never re-shapes them. Plan-001 `I-001-3` enforces this. New tables + columns Plan-006 needs (`daemon_signing_keys` table, `session_events.retention_class`, `session_snapshots.{has_compacted_ranges, compacted_range_count}`, `pending_anchor_uploads` table) ship as Plan-006-owned additive migrations at Tier 4, never as modifications to `0001-initial.ts`. +- **CP-006-7 — Plan-002 amendment for daemon_signing_key generation at session-create.** Plan-002 (Tier 1, closed) reopens via amendment-via-extension PR to extend session-create with `DaemonSigningKeySource.create(sessionId)` call + participant-roster public-key registration. Amendment is scope-limited to call-site + roster row write; no master-key crypto leaks into Plan-002. Spec-006:382 ("registered in the session participant roster at join time, keyed by NodeId") is the governing semantic. +- **CP-006-8 — Plan-013 audit-stub renderer surface (forward-declared).** Phase 4 T4.8 ships `` as the audit-stub render contract per Spec-006:609. The future Plan-013 (Timeline Rendering And Replay UI) MUST consume — not re-implement. Bidirectional once Plan-013 lands; one-sided forward-declared today (verified via `ls`: `docs/plans/013-*.md` does not exist). +- **CP-006-9 — Plan-015 recovery-dispatcher consumes T4.3 read-after-cursor.** Plan-015 (Tier 7) recovery path calls `eventClient.readAfterCursor` (or daemon-internal `replay-service.ts:readAfterCursor`) to rebuild projections from the last `replay_cursors.last_sequence` checkpoint. T4.6 Zod schemas are the binding interface. +- **CP-006-10 — Plan-020 metrics + dashboard consume audit-integrity events (forward-declared).** Plan-020 (Metrics & Dashboards; Non-Goal of Plan-006 per §Non-Goals) SHOULD consume `audit_integrity_verified`, `audit_integrity_failed`, `key_reuse_detected` for an audit-health dashboard. Binding when Plan-020 is authored. + +## Invariants + +Each invariant gets an I-006-N ID with the enforcement strategy and the test that confirms it. + +- **I-006-1-01 — Category/type bijection.** Every event type belongs to exactly one category; `SESSION_EVENT_CATEGORY_BY_TYPE.size === 123` and `new Set(SESSION_EVENT_CATEGORY_BY_TYPE.values()).size === 19`. Enforced via Phase 1 type-level exhaustiveness check + runtime assertion. Cite: Spec-006:506-533. +- **I-006-1-02 — Event-type-string immutability.** Type strings are immutable wire identifiers; renames are forbidden once a type is registered. Enforced via build-time const-assertion + ADR-018 §Decision #8 (MINOR additive-only). Cite: Spec-006:539-549; ADR-018 §Decision #8. +- **I-006-1-03 — Envelope field set is fixed; serialized order is RFC 8785 lex-sort.** The Zod schema declares the canonical field set; RFC 8785 §3.2.3 mandates UTF-16 code-unit lex-sort serialization. JSDoc on `EventEnvelopeSchema` references the Spec-006:546 amendment. Enforced via Phase 2 T2.3 golden-vector tests + Spec-006:546 prose amendment. Cite: Spec-006:539-549 + amendment; RFC 8785 §3.2.3. +- **I-006-1-04 — `EventEnvelope.version` immutability.** Producer-set at emit; never rewritten on read (upcaster chain mutates in-memory representation only). Enforced via `EventEnvelopeVersion` brand + JSDoc cite to ADR-018 §Decision #2/#6. Cite: ADR-018 §Decision #2, #6; Spec-006:76-90. +- **I-006-2-01 — Encrypt → digest → embed → canonicalize → sign order is load-bearing.** Phantom-branded types per stage make reversing the order a TS compile error: `RawEventInput → EventWithPiiDigest → CanonicalBytes → SignedRow`. T2.5 post-shred property test verifies signature still verifies after `pii_payload` shred. Cite: Spec-022 §Signature Safety Under Shred:323-350. +- **I-006-2-02 — `pii-indirection.ts` is the sole write path for `pii_payload`.** Branded `PiiPayloadCiphertext` constructible only inside this module; Plan-001's persistence-layer INSERT helper types the parameter as `PiiPayloadCiphertext | null`; cross-module construction is a TS compile error. Cite: Plan-006:169. +- **I-006-2-03 — Canonical bytes are byte-stable across implementations.** T2.3 golden-vector tests + RFC 8785 Appendix-A conformance vectors guarantee byte-identical output for semantically-equal envelopes. Cite: RFC 8785; Spec-006:539-549. +- **I-006-2-04 — Genesis `prev_hash` = 32 zero bytes; subsequent `prev_hash[n] = row_hash[n-1]`.** Enforced via T2.6 backfill migration + persistence-layer assertion. Cite: Spec-006:540; Plan-006:132; Plan-001:300. +- **I-006-2-05 — Signed bytes commit to `pii_ciphertext_digest`, not raw ciphertext.** `daemon_signature` covers `canonical_bytes(row)` which includes the digest but excludes `pii_payload`; post-shred verification still succeeds. T2.5 verifies. Cite: Spec-022:323-350; Spec-006:549. +- **I-006-2-06 — One canonicalization per row.** `signRow` takes `CanonicalBytes` once and uses it for both BLAKE3 input and Ed25519 message — no opportunity for drift. Cite: Security Architecture:378. +- **I-006-2-07 — `audit_integrity` and `event_maintenance` categories carry no `pii_payload`.** Discriminated-union type on `RawEventInput`: events in these categories have `pii_payload: never`. Cite: Spec-006:529-531; Spec-006:469. +- **I-006-3-01 — Three-layer `audit_integrity` + `event_maintenance` non-compaction / non-shred enforcement.** Layer 1: compactor selector excludes (T3.2). Layer 2: `pii-indirection.ts` refuses (Phase 2). Layer 3: shred fan-out Path 1 selector excludes (Plan-022). T3.5 E2E test exercises all three. Cite: Plan-006:198-202. +- **I-006-3-02 — `event_log_anchors` upload is metadata-only at the type level.** `AnchorPayload` (T3.3) has no `payload` field, no `events` field, no `pii_payload` field. Structurally enforces ADR-017's "control plane sees only ciphertext-derived hashes". T3.5 structural-type test asserts the exact seven fields. Cite: ADR-017 §Decision; shared-postgres-schema.md:355-380. +- **I-006-3-03 — Compaction is anchor-before-stub and chain-commitment-frozen.** T3.2 enforces the anchor-before-compaction protocol per [Spec-006 §Post-Compaction Integrity](../specs/006-session-event-taxonomy-and-audit-log.md#post-compaction-integrity): before mutating any row in range `[start_seq, end_seq]`, the compactor verifies (or force-fires + durably queues) a covering Merkle anchor with daemon Ed25519 signature, computes a per-row `stub_signature` over the canonical bytes of each audit-stub projection, then in one transaction **replaces** `payload` with the JCS-canonicalized audit-stub projection (column is `NOT NULL` — rewritten, not nulled), NULLs `correlation_id`/`causation_id`/`pii_payload`, sets `retention_class = 'audit_stub'`, and writes `stub_signature`. Chain-commitment columns (`prev_hash`/`row_hash`/`daemon_signature`/`participant_signature`/`monotonic_ns`/`version`) are NEVER mutated. T4.1 verifier handles `retention_class = 'audit_stub'` rows via THREE checks (all must pass): (a) anchor-existence + anchor-signature (NOT chain-recomputation against the original, which is impossible after canonical bytes are discarded) — anchor-missing → `failureMode: 'anchor_missing_for_compacted_range'`, anchor-signature-invalid → `failureMode: 'anchor_signature_invalid'`; (b) `stub_signature` re-verification directly over the canonical bytes stored in `payload` (the exact bytes signed at compaction) — tampered/replayed/missing → `failureMode: 'stub_signature_invalid'`; (c) scalar-binding — decode the projection from the verified `payload` bytes and assert each surviving scalar column (`id`/`session_id`/`sequence`/`occurred_at`/`category`/`type`/`actor`) equals its projection field — divergence → `failureMode: 'stub_scalar_mismatch'` (the surviving scalar columns are not covered by `stub_signature`, so an at-rest scalar edit would otherwise forge a filter/reconstruction value). All four are additive-MINOR enum extensions per ADR-018 §Decision #8 (sanctioned). Cite: Spec-006 §Post-Compaction Integrity (anchor-before-compaction protocol + per-row stub_signature commitment + scalar-binding check + threat model); Spec-006 §Integrity Protocol (two-tier integrity); ADR-018 §Decision #8. +- **I-006-3-04 — Anchor cadence: earlier-of (1000 events) OR (300 seconds).** Constants `ANCHOR_INTERVAL_EVENTS = 1000` and `ANCHOR_INTERVAL_SECONDS = 300`. T3.5 cadence test exercises both thresholds independently. Cite: Spec-006 §Anchoring Cadence. +- **I-006-4-01 — Exactly-one verifier emission per range.** `verifyRange({sessionId, fromSeq, toSeq})` emits exactly ONE of `audit_integrity_verified` or `audit_integrity_failed`; never zero, never two. Consumers dedupe by `(verifierNodeId, fromSeq, toSeq, verifiedAt)`. Cite: Spec-006:432-433. +- **I-006-4-02 — Verifier rows are themselves chain-and-signature-protected.** A tampered-after-the-fact integrity-failure record cannot be silently appended without breaking the chain. T4.1 fault-injection test verifies. Cite: Security Architecture §Audit Log Integrity:415. +- **I-006-4-03 — `refuse_on_rotation` invariant enforcement.** Key-reuse detection halts ingest of further events from the colliding signer's `NodeId`; `key_reuse_detected` payload carries `rotationInvariantViolated: 'refuse_on_rotation'` verbatim. T4.2 test verifies. Cite: Spec-006:434. +- **I-006-4-04 — Replay across compacted regions returns audit stubs, never silent omission.** `readAfterCursor` / `readWindow` return all sequence positions; compacted positions carry `payload.retentionClass === 'audit_stub'`. T4.3 test asserts. Cite: Spec-006:64; Spec-006:605-609. +- **I-006-4-05 — `EventSubscription` replay-then-live cursor handoff is monotonic.** Strict sequence order; no duplicate at boundary; no gap at boundary. T4.5 test verifies. Cite: Spec-006:72; Spec-006:63. +- **I-006-4-06 — Zod-validated wire envelopes at SDK seam.** Malformed responses rejected at the SDK boundary, not silently coerced. T4.6 + T4.7 tests verify. Cite: Plan-005 Phase 4 precedent. +- **I-006-4-07 — Audit stubs render as visible summarized segments.** `` never renders null or empty; empty `summary` still produces a visible "(compacted, no summary)" segment. T4.8 + T4.9 tests verify. Cite: Spec-006:64; Spec-006:608-609. + +## Audit Reconciliation + +Transcription errors and cross-doc inconsistencies discovered during the 2026-05-28 Plan-006 audit, all resolved in this PR or named follow-up. + +- **Plan-006 working-copy 120/18 vs canonical 123/19.** Pre-audit lines 22/47/219 transcribed the wrong count. Reconciled: Spec-006:506 enumerates 19 categories; Spec-006:533 totals 123 events. Working-copy updated in this audit PR. +- **`events/` subdirectory non-existent in `packages/contracts/src/`.** Plan-006 working-copy referenced `packages/contracts/src/events/envelope.ts` + `events/taxonomy.ts`; the directory does not exist (Plan-001's contracts package uses top-level co-location). Reconciled per the directory-placement decision: extend top-level `event.ts` in place. Working-copy Target Areas updated. +- **api-payload-contracts.md Plan-006 §EventEnvelope listed 16 categories.** Reconciled per T1.6: widened to 19 with the three missing entries (`channel_arbitration`, `onboarding_lifecycle`, `cross_node_dispatch`) and the comment updated from "16 categories total" to reference Spec-006:506+533 with 123-event count. Applied in this audit PR. +- **`security-architecture.md:409-411` used superseded `failureKind: 'chain_break'` interim term.** Spec-006:559 explicitly says the prior interim registration is superseded; Spec-006:433 enumerates the canonical 7-value `failureMode` enum (subsequently extended to 11 values in this audit PR — see post-compaction integrity entries below). Fixed in this audit PR (security-architecture.md:409 lines updated to use `failureMode` + `failurePath`). +- **Spec-006:546 prose amendment** — Pre-amendment wording "Fields included, in this order:" was ambiguous against RFC 8785 §3.2.3's mandate. Amended in this audit PR to "Fields included (the canonical set; serialized order is mandated by RFC 8785 §3.2.3 UTF-16 code-unit lex-sort of member names, not the order listed here)" with explicit RFC 8785 cite. +- **Post-compaction integrity gap surfaced by Codex review on PR #124 (T5 P1 finding, 2026-05-28).** Pre-amendment Plan-006 I-006-3-03 stated "Verifier short-circuits chain recomputation on `retention_class = 'audit_stub'` rows" — violating Spec-006 §Integrity Protocol's "append-only AND tamper-evident" guarantee + §Acceptance Criteria #3 ("visible in audit history even after payload compaction"). Resolved by introducing the **anchor-before-compaction protocol**: Spec-006 amended with a new §Post-Compaction Integrity sub-section under §Event Compaction Policy, §Integrity Protocol amended with two-tier integrity statement, `failureMode` enum extended from 7 to 9 values (`anchor_missing_for_compacted_range` + `anchor_signature_invalid`, additive-MINOR per ADR-018 §Decision #8), and a 4th Acceptance Criterion added. Plan-006 I-006-3-03 rewritten; T3.2 compactor + T4.1 verifier updated; Plan-006 line 55 + line 228 enum-count references swept to 9. Hardened-mode path; no V1.1 deferral. Resolution surfaces the compacted-range integrity guarantee at anchor-existence + anchor-signature granularity (per-row recomputation is impossible after canonical bytes are discarded, but the daemon-signed Merkle anchor — durably persisted to both `pending_anchor_uploads` and `event_log_anchors` — provides range-level commitment to the pre-compaction state). +- **Post-compaction stub authenticity gap surfaced by Codex re-review on PR #124 (round-2 P1 finding, 2026-05-28).** The round-1 anchor-before-compaction protocol (above) commits to the ORIGINAL pre-compaction bytes (via the frozen `row_hash` the anchor's Merkle root covers) but left the POST-compaction audit-stub bytes — now the only visible `payload` — unauthenticated: a local tamper of the stub's `summary`/`actor` while leaving `row_hash` + the signed Merkle root untouched still verified. Resolved (hardened-mode, no V1.1 deferral) by adding a per-row **`stub_signature`** — an Ed25519 signature over `canonical_bytes(audit-stub projection)` minted by the compactor at compaction time (T3.2) and re-verified by T4.1 against the row's CURRENT stub bytes. Spec-006 §Compacted Event Format + §Post-Compaction Integrity amended (per-row commitment + full threat model: stub-edit, cross-row replay, signature-strip, and both `retention_class`-flip directions all detected); `failureMode` enum extended 9 → 10 (`stub_signature_invalid`, additive-MINOR per ADR-018 §Decision #8); Spec-006 §Compacted Event Format clarified that `payload` is REPLACED with the stub projection (the column is `NOT NULL`), not nulled — coupled fix for the replay/renderer "never silent omission" contract (Codex round-2 P2 on T3.2). New additive column `session_events.stub_signature BLOB` (migration `0NNN-retention-class-and-stub-signature.ts`); I-006-3-03 + T3.2 + T4.1 + Plan-006 line 55 + line 228 enum-count references swept to 10; security-architecture.md §Verification Rules extended with the stub-signature check. Marked a deliberate security amendment to an approved spec (anchor = external proof of original existence; `stub_signature` = at-rest proof of stub authenticity; both required, neither sufficient alone). +- **Post-compaction scalar-binding gap surfaced by Codex re-review on PR #124 (round-5 P1 finding, 2026-05-28).** The round-2 `stub_signature` (above) authenticates only the `payload` projection bytes, but the surviving scalar columns (`id`/`session_id`/`sequence`/`occurred_at`/`category`/`type`/`actor`) are neither nulled nor frozen nor signed — a denormalized cache that SQL filters (`idx_session_events_type`) and envelope reconstruction read. An at-rest edit to a scalar column (e.g. `actor`, `type`) left `payload` + `stub_signature` + the anchor all verifying while a filter/reconstruction surfaced the forged value. Resolved (hardened-mode, no V1.1 deferral) by a per-row **scalar-binding check**: T4.1 decodes the projection from the verified `payload` bytes and asserts each surviving scalar column byte-equals its projection counterpart — divergence → `stub_scalar_mismatch`. Spec-006 §Post-Compaction Integrity amended (verifier "two checks" → "three checks" + new threat-model scenario + new Acceptance Criterion), `failureMode` enum extended 10 → 11 (`stub_scalar_mismatch`, additive-MINOR per ADR-018 §Decision #8); security-architecture.md Rule 4 + cross-plan-dependencies.md `stub_signature` row + I-006-3-03 + T4.1 + Plan-006 line 55 + line 228 enum-count references swept to 11. The signed `payload` projection is affirmed as the authoritative source for a compacted row's envelope fields; the scalar columns are an index/filter cache trustworthy only post-verification. ## Parallelization Notes @@ -249,12 +378,14 @@ Plan-006 owns the `EventEnvelope` contract's integration with [ADR-018](../decis ## Rollout Order -1. Land envelope contracts + taxonomy enum + error-contract version codes. -2. Enable append-only writes with BLAKE3 chain and Ed25519 signatures behind internal feature gating. -3. Enable PII indirection (encrypt → digest → embed → sign order). -4. Enable replay reads and live subscription catch-up. -5. Enable compaction + Merkle anchor emission. -6. Enable integrity verifier + observer-pattern `key_reuse_detected`. +**Prerequisite (off-plan)** — Plan-002 amendment-via-extension PR must land before step 2 begins. The amendment wires session-create to invoke `DaemonSigningKeySource.create(sessionId)` (T2.7), which writes the per-session sealed keypair into the Plan-006-owned `daemon_signing_keys` local SQLite table (migration ships under Plan-006 T2.7, not Plan-002). Without the call-site, every `EventLogService.append()` from step 2 onward hits a null signing-key lookup and the Ed25519 signer throws. Tracked under CP-006-7. The amendment is sequencing-only — Plan-002's already-shipped phases are untouched. + +1. Land envelope contracts + taxonomy enum + error-contract version codes (Phase 1). +2. Enable append-only writes with BLAKE3 chain and Ed25519 signatures behind internal feature gating (Phase 2 — depends on Prerequisite + step 1). +3. Enable PII indirection (encrypt → digest → embed → sign order) (Phase 2 — depends on step 2). +4. Enable compaction + Merkle anchor emission (Phase 3 — depends on step 3). +5. Enable replay reads and live subscription catch-up (Phase 4 — depends on step 4). +6. Enable integrity verifier + observer-pattern `key_reuse_detected` (Phase 4 — depends on step 5). ## Rollback Or Fallback @@ -270,6 +401,7 @@ Plan-006 owns the `EventEnvelope` contract's integration with [ADR-018](../decis - **Category drift.** Event-type renames or category reassignments on existing wire types violate ADR-018 §Decision #8. Enforced by the taxonomy enum + contract tests; new types are additive-only. - **Anchor cadence under partition.** If the daemon cannot reach the control plane for >300 seconds, anchors queue locally. The per-daemon hash chain + signatures retain tamper-evidence on each local log; the anchor tier provides external cross-observer consistency that catches up on reconnect. - **Invariant enforcement regressions.** If the compactor or shred selector ever mis-categorizes an `audit_integrity` or `event_maintenance` event, the invariant is violated silently. Enforced at three layers (compactor, pii-indirection, shred selector) to prevent single-layer regressions. +- **Plan-002 amendment sequencing.** The signing-key column + session-create call-site live in Plan-002. The amendment-via-extension PR must merge before step 2 of Rollout Order; the §Rollout Order Prerequisite captures this. Late landing surfaces as null-signing-key throws on the first `EventLogService.append()` — a load-bearing cross-plan sequence dependency, not a runtime bug. ## Done Checklist diff --git a/docs/plans/007-local-ipc-and-daemon-control.md b/docs/plans/007-local-ipc-and-daemon-control.md index d9bd1eaa..96c50f88 100644 --- a/docs/plans/007-local-ipc-and-daemon-control.md +++ b/docs/plans/007-local-ipc-and-daemon-control.md @@ -30,9 +30,9 @@ Lands alongside Plan-001 to unblock Plan-001 Phase 5. Scope: Lands at Plan-007's original Tier 4 slot, co-tier with Plan-005 (runtime bindings) and Plan-006 (event taxonomy) — all three are gated only on Tier 1 completion per [cross-plan-dependencies.md §5 Canonical Build Order](../architecture/cross-plan-dependencies.md#5-canonical-build-order). Scope: -- The four other JSON-RPC method namespaces — `run.*`, `repo.*`, `artifact.*`, `settings.*`, `daemon.*` — extending the substrate's namespace registry without re-implementing the wire layer. +- The JSON-RPC method namespaces extending the substrate's namespace registry at Tier 4 — `run.*`, `repo.*`, `artifact.*`, `settings.*`, `daemon.*`, `driver.*`, `event.*`. Of these, **Plan-007-remainder OWNS `daemon.*`** (supervision verbs per Spec-007:74) and **`settings.*`** (effective-settings read-projection per I-007-3); `driver.*` is owned by [Plan-005](./005-provider-driver-contract-and-capabilities.md) Phase 4 (6 client-facing non-lifecycle verbs + `driver.subscribeEvents`; the 4 session/run lifecycle ops are daemon-internal per Plan-005 §Phase 4 decision #2); `event.*` is owned by [Plan-006](./006-session-event-taxonomy-and-audit-log.md) Phase 4 (`event.readAfterCursor`, `event.readWindow`, `event.subscribe`); `run.*` / `repo.*` / `artifact.*` are owned by their respective downstream-tier plans (Plan-004 / Plan-009 / Plan-014) and land at the consuming plan's tier per the Plan-002 `presence.*` precedent ([NS-26](../architecture/cross-plan-dependencies.md)). Registration boundaries declared in CP-007-6 (`driver.*`) and CP-007-7 (`event.*`) below. - The Spec-027 secure-defaults bootstrap surface owned by Plan-007 widens at Tier 4 alongside the additional bind paths — `tls-surface.ts` (row 8 — only load-bearing once a non-loopback / TLS bind enters), `first-run-keys.ts` (row 3 — daemon master key generation; key custody is Plan-022's at Tier 5), `update-notify.ts` (row 7a — periodic poller, not a bind-time gate), and the CLI `self-update` dual-verification command (row 7b — out-of-process). The Tier 1 partial already ships the bind-time `SecureDefaults` validation surface (`secure-defaults.ts` + `secure-defaults-events.ts`) scoped to the loopback OS-local socket bind path it exposes; §Invariants I-007-1 / I-007-2 / I-007-5 hold at every execution window per the validation-surface-widens-with-bind-surface rule. -- The CLI delivery track (`apps/cli/`) and desktop-shell daemon supervision (`apps/desktop/src/main/daemon-supervision/`, `apps/desktop/src/renderer/src/daemon-status/`). +- The CLI delivery track (`apps/cli/`) and desktop-shell **transport-supervision** consumer (`apps/desktop/src/main/daemon-status-projector/`) + renderer status surface (`apps/desktop/src/renderer/src/daemon-status/`). The transport-supervision projector consumes the `SupervisionHooks` surface from `local-ipc-gateway.ts:114-172` and projects `daemon.status.*` events through the preload bridge. Concern-disjoint from Plan-023 Tier 8's `daemon-supervisor.ts` (which owns the `utilityProcess.fork` OS-process lifecycle + Plan-023:208 backoff ladder `100ms/300ms/1s/3s/10s`) per the concern-split resolution (user-ratified 2026-05-28); see CP-007-11 below. ### Substrate-vs-Namespace Decomposition Rule (Methodology) @@ -112,9 +112,9 @@ Errors thrown from handler bodies MUST map to JSON-RPC error codes per [error-co ### I-007-9 — Method names conform to the canonical format declared in api-payload-contracts.md -The registry MUST mechanically validate method-name format at `register(method, ...)` call time, refusing names that don't match the canonical convention. The canonical format is `dotted-lowercase` per [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md) (resolved 2026-04-30). Implementations MUST evaluate `method` against the regex `/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/` and throw on mismatch. +The registry MUST mechanically validate method-name format at `register(method, ...)` call time, refusing names that don't match the canonical convention. The canonical format is `dotted-camelCase` (first segment lowercase, subsequent segments may include uppercase letters internally — e.g. `event.readAfterCursor`, `driver.listCapabilities`, `settings.effectiveRead`) per [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md) (resolved 2026-04-30; convention surfaced in canonical examples like `settings.effectiveRead` since baseline). Implementations MUST evaluate `method` against the regex `/^[a-z][a-z0-9]*(\.[a-z][a-zA-Z0-9]*)+$/` and throw on mismatch. The regex permits camelCase identifiers in non-leading segments (each segment starts lowercase but may include uppercase internally) while still rejecting leading-uppercase, leading-digit, separator-other-than-dot, or non-ASCII forms. -**Why load-bearing.** Without format enforcement, downstream plans may register `session.create`, `session/create`, `Session.create`, and `sessionCreate` simultaneously. The registry would treat these as distinct methods (correctly per the literal string) but the SDK consumer would have no way to choose. Format enforcement at registration time is the only mechanical guarantee. +**Why load-bearing.** Without format enforcement, downstream plans may register `session.create`, `session/create`, `Session.create`, and `sessionCreate` simultaneously. The registry would treat these as distinct methods (correctly per the literal string) but the SDK consumer would have no way to choose. Format enforcement at registration time is the only mechanical guarantee. The dotted-camelCase convention (not snake_case) matches the JSON-RPC method naming used by LSP and MCP, which keeps the wire-format method names symmetric with the TypeScript handler/SDK identifiers (`session.create` → `sessionClient.create(...)`, `event.readAfterCursor` → `eventClient.readAfterCursor(...)`). ### I-007-10 — Subscribe-init response precedes the first notification frame @@ -128,7 +128,55 @@ For any `LocalSubscriptionProducer` instance (the producer-side primitive at Eight semantic sub-invariants are load-bearing (canonical statement: `packages/contracts/src/jsonrpc-streaming.ts:432-454`): (1) registration-after-cancel fires synchronously, (2) registration-after-complete is silently dropped, (3) multi-handler firing in registration order, (4) per-handler error isolation, (5) two-layer try/catch in bulk cleanup, (6) remove-from-maps-then-fire ordering so re-entrant handlers observe post-cancel state, (7) mirrors AbortSignal-style semantics, (8) idempotent registration. -**Why load-bearing.** Plan-007-remainder Tier 4 handlers (`run.*`, `repo.*`, `artifact.*`, `settings.*`, `daemon.*`) and Plan-002 `presence.*` register subscriptions through the same `LocalSubscriptionProducer` shape and must close upstream-watcher leaks the same way `session-subscribe.ts` does (via `sub.onCancel(unsubscribe)`). Without this invariant pinned at plan-body level, every future handler author re-discovers the failure mode the PR #19 F5 patch closed (registered onCancel handlers firing from inside the upstream's `onEvent` call stack — re-entrant unsubscribe). The consumer-side precondition is named on `SessionSubscribeDeps.subscribeToSession`'s returned function: the unsubscribe callback must tolerate snapshot-during-emit / queued-removal. +**Why load-bearing.** Plan-007-remainder Tier 4 handlers (`run.*`, `repo.*`, `artifact.*`, `settings.*`, `daemon.*`) and Plan-002 `presence.*` register subscriptions through the same `LocalSubscriptionProducer` shape and must close upstream-watcher leaks the same way `session-subscribe.ts` does (via `sub.onCancel(unsubscribe)`). Without this invariant pinned at plan-body level, every future handler author re-discovers the failure mode the Plan-007 Phase 3 lifecycle-hook patch closed (registered onCancel handlers firing from inside the upstream's `onEvent` call stack — re-entrant unsubscribe). The consumer-side precondition is named on `SessionSubscribeDeps.subscribeToSession`'s returned function: the unsubscribe callback must tolerate snapshot-during-emit / queued-removal. + +### I-007-12 — Daemon refuses self-swap while IPC has active clients + +The daemon MUST refuse to replace its own running binary while the local-IPC gateway has active client connections. The notify-by-default poller (`update-notify.ts`, Phase R2) MUST NOT download + swap even behind a flag — only the CLI-invoked `ai-sidekicks self-update` (out-of-process, daemon-restart afterward) is sanctioned to perform the swap. + +**Why load-bearing.** Spec-027:63 row 7a verbatim says "The daemon MUST NOT self-swap its binary while IPC is live." Spec-027:187 §Pitfalls reinforces: "row 7a forbids self-swap while IPC is live. Plan-007 MUST NOT implement notify-by-default as 'download + swap' even behind a flag." This is a runtime-state property — at startup, IPC has zero clients, so I-007-1 (load-before-bind) does NOT subsume this. I-007-12 is the live-state property: even AFTER startup, even AFTER all bootstrap invariants have been satisfied, an attempted self-swap is a programmer error and MUST throw / refuse. The invariant is symmetric to I-007-1 in spirit (one guards startup-order; the other guards runtime-state). + +### I-007-13 — CLI workspace package is import-isolated + +`apps/cli/src/**` may import only from `@ai-sidekicks/contracts`, `@ai-sidekicks/client-sdk`, the CLI library (`clipanion@4.0.0-rc.4` per T-007r-3-1 scaffold pin), and Node built-ins. Direct imports of `@ai-sidekicks/runtime-daemon`, `@ai-sidekicks/control-plane`, or any `apps/desktop/**` path are forbidden. + +**Why load-bearing.** The CLI is the first delivery track per Spec-007:41 and ADR-009 — it MUST consume the daemon strictly through the JSON-RPC IPC contract, not by reaching into daemon internals. If the CLI imported daemon code directly, the wire contract would be silently bypassable and the SDK / control-plane / desktop / CLI clients would diverge. Enforcement is mechanical: `apps/cli/eslint.config.mjs` declares the `no-restricted-imports` rule mirroring Plan-023's renderer Tier-1-Partial substrate enforcement; CI build fails on violation. + +### I-007-14 — CLI exit codes are a closed deterministic mapping + +Every CLI exit code is sourced from the closed mapping `JsonRpcErrorCode → PosixExitCode` per Spec-007:32. No ad-hoc exit codes (e.g., `process.exit(1)` for an unmapped error) are permitted. + +**Why load-bearing.** The CLI composes into scripts and CI pipelines; non-deterministic exit codes break automation. A closed mapping makes the failure semantic part of the wire contract: `transport.unavailable` → exit 69, `session.not_found` → exit 78, etc. (final code assignments transcribed at T-007r-3-7's `exit-codes.test.ts`). + +### I-007-15 — Daemon-lifecycle CLI commands ack `DaemonHelloAck` before reporting success + +`ai-sidekicks daemon start` and `ai-sidekicks daemon restart` MUST NOT exit 0 on TCP-accept alone; they MUST wait for `DaemonHelloAck` within a deadline (default 10s, override `--timeout `) before reporting success. + +**Why load-bearing.** A daemon that accepted the connection but failed to complete the handshake (e.g., protocol-version mismatch emitting `invalid_protocol_version` per `local-ipc-gateway.ts:1140`) is not actually usable; reporting success here would mask the failure. Operators rely on the `daemon start` exit status to gate subsequent operations in scripts. + +### I-007-16 — Security-critical override banners cannot be suppressed by `--no-banner` + +Spec-027 row 10 loud banners announcing a security-critical override (TLS floor breach, loopback-fallback enabled, `--insecure` bind, `LEGACY_TLS12=1`, `AUTO_UPDATE_CHECK=off`, etc.) MUST emit regardless of `--no-banner`. Only non-security informational banners (e.g., "first-run welcome") honor `--no-banner`. + +**Why load-bearing.** `--no-banner` is a UX-convenience flag; it must not become a security-mute mechanism. The Spec-027 banner is the operator's audit-visible signal that a security-default override is active; allowing it to be silenced by a generic UX flag would let an attacker (or a careless operator) hide active downgrades from the next operator who runs the binary. Banner-string assembly in §R2 tags each banner with an `is_security_critical: boolean` field; the CLI's `--no-banner` gate consults that field per banner. + +### I-007-17 — Supervision events are single-emit per transport boundary + +For each transport instance (per `SupervisionTransport.id` per `local-ipc-gateway.ts:127-130`), the Phase R3 supervision-hook consumer (T-007r-3-8) emits exactly one `onConnect` event followed by at most one `onError` (terminal) and exactly one `onDisconnect` (terminal). Duplicate emissions for the same transport boundary indicate a state-machine bug. + +**Why load-bearing.** The renderer view (T-007r-3-11) and the Plan-023 Tier 8 process-supervisor (per CP-007-11) both subscribe to the projected `daemon.status.*` stream; double-emit would race the state machine and surface inconsistent UI / restart-counter increments. Enforcement: `status-machine.test.ts` asserts XState v5 guards explicitly preventing duplicate emissions per transport id. + +### I-007-18 — Supervision-status state machine is fail-closed on unknown reasons + +If the Phase R3 supervision consumer receives a `SupervisionDisconnectReason` value outside the canonical 5-value enum at `local-ipc-gateway.ts:150-155` (`"client_close" | "server_close" | "transport_error" | "oversized_body" | "malformed_frame"`) — e.g., a forward-compat future addition — the state machine MUST default to the `degraded` state, never `connected`. The renderer view (T-007r-3-11) MUST render unknown reasons as a generic "Daemon error" rather than fabricating user-readable text. + +**Why load-bearing.** Fail-closed on unknown enum values prevents new-future-value-introduces-a-bypass attacks. The forward-compat surface is real: if a future Plan-007 PR adds a sixth disconnect reason to the closed-union enum, this invariant ensures pre-amendment CLI / desktop binaries degrade safely rather than rendering "connected" against an unrecognized signal. + +### I-007-19 — Mutating-op gate for renderer-issued `daemon.restart` + +A `daemon.restart` call originating from the renderer (via T-007r-3-13's button) MUST be gated by a server-side check that the calling session has a valid `DaemonHelloAck` (per Spec-007 §Mutating-Op Gate). The renderer-side UI debounce (button disabled during `connecting`) is a UX hint, not a security boundary. + +**Why load-bearing.** Trust-boundary discipline per Spec-023 §Trust Stance — the renderer is NOT a direct daemon client. Even though Plan-023's `no-restricted-imports` ESLint rule blocks the renderer from binding a JSON-RPC client, the IPC bridge handler is the only channel and MUST re-validate every mutating call against the daemon's typed-error envelope. Server-side enforcement at the substrate mutating-op gate (`local-ipc-gateway.ts:1096+`) is the authoritative check; T-007r-3-9's bridge handler asserts the renderer-issued call is dropped (and an actionable error envelope returned) if the daemon-status state machine is not `connected`. ## Preconditions @@ -136,9 +184,9 @@ Eight semantic sub-invariants are load-bearing (canonical statement: `packages/c - [x] Required ADRs are accepted - [x] Blocking open questions are resolved or explicitly deferred - [x] [api-payload-contracts.md](../architecture/contracts/api-payload-contracts.md) §Tier 1 (cont.): Plan-007 declares `protocolVersion` field type — closed 2026-05-01 via BL-102 ratification: ISO 8601 `YYYY-MM-DD` date-string per the MCP §Architecture overview precedent; Spec-007:54 amended to match; substrate at `packages/contracts/src/jsonrpc.ts` narrowed from `number | string` to `string`. Sister sub-items closed: method-namespace registry typed surface (`MethodRegistry`) + `LocalSubscriptionProducer` streaming primitive shape — closed via [BL-102](../backlog.md) no-mirror disposition (canonical sources: `packages/contracts/src/jsonrpc-registry.ts` + `packages/contracts/src/jsonrpc-streaming.ts`); JSON-RPC method-name format convention closed via [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md). -- [x] [error-contracts.md §JSON-RPC Wire Mapping](../architecture/contracts/error-contracts.md#json-rpc-wire-mapping) declares JSON-RPC numeric-error-space (`-32700` / `-32600` / `-32601` / `-32602` / `-32603`) ↔ project dotted-namespace `data.type` two-layer envelope per JSON-RPC 2.0 §5.1 + RFC 7807 + LSP 3.17 ResponseError precedent; declares `unknown_setting` (`-32602`), `transport.unavailable` (`-32603`), `transport.message_too_large` (`-32600`), and `transport.invalid_protocol_version` (`-32600`) Tier-1 domain identifiers with structured `data.fields` shapes — closed 2026-05-01 via [BL-103](../backlog.md) ratification. The transport-layer `transport.message_too_large` (HTTP 413 semantic — peer mis-framing the wire layer; `data.fields: { limit, observed }`) is intentionally distinct from Spec-001's domain-quota code `resource.limit_exceeded` (HTTP 429 semantic — control-plane resource saturation; `data.fields: { resource, limit, current }`); the prior conflation was canonicalized in 2026-05-01 round-trip per Codex review on PR #26. The substrate-side envelope-level `transport.invalid_protocol_version` gate enforces Spec-007:54's per-request `protocolVersion` field requirement BEFORE handler dispatch (I-007-7) and is intentionally distinct from `protocol.version_mismatch` (NegotiationError, registry-side mutating-op gate); `data.fields: { reason: "missing" | "wrong_type" | "invalid_format", observedType?: string }`. +- [x] [error-contracts.md §JSON-RPC Wire Mapping](../architecture/contracts/error-contracts.md#json-rpc-wire-mapping) declares JSON-RPC numeric-error-space (`-32700` / `-32600` / `-32601` / `-32602` / `-32603`) ↔ project dotted-namespace `data.type` two-layer envelope per JSON-RPC 2.0 §5.1 + RFC 7807 + LSP 3.17 ResponseError precedent; declares `unknown_setting` (`-32602`), `transport.unavailable` (`-32603`), `transport.message_too_large` (`-32600`), and `transport.invalid_protocol_version` (`-32600`) Tier-1 domain identifiers with structured `data.fields` shapes — closed 2026-05-01 via [BL-103](../backlog.md) ratification. The transport-layer `transport.message_too_large` (HTTP 413 semantic — peer mis-framing the wire layer; `data.fields: { limit, observed }`) is intentionally distinct from Spec-001's domain-quota code `resource.limit_exceeded` (HTTP 429 semantic — control-plane resource saturation; `data.fields: { resource, limit, current }`); the prior conflation was canonicalized in the 2026-05-01 round-trip. The substrate-side envelope-level `transport.invalid_protocol_version` gate enforces Spec-007:54's per-request `protocolVersion` field requirement BEFORE handler dispatch (I-007-7) and is intentionally distinct from `protocol.version_mismatch` (NegotiationError, registry-side mutating-op gate); `data.fields: { reason: "missing" | "wrong_type" | "invalid_format", observedType?: string }`. - [x] [Spec-006 §Security Events (`security_events`)](../specs/006-session-event-taxonomy-and-audit-log.md#security-events-security_events) registers `security.default.override` and `security.update.available` as canonical event types; [Plan-006 §Event Taxonomy Coverage](./006-session-event-taxonomy-and-audit-log.md#event-taxonomy-coverage) emitter table lists Plan-007 as the emitter — closed 2026-05-01 via [BL-105](../backlog.md) ratification. -- [x] `session.subscribe` streaming-primitive shape — closed 2026-04-30 via [BL-102](../backlog.md) no-mirror disposition; canonical source: `packages/contracts/src/jsonrpc-streaming.ts` (`LocalSubscriptionProducer` shape, shipped Plan-007 Phase 3 PR #19). [api-payload-contracts.md §Source-of-Truth Policy](../architecture/contracts/api-payload-contracts.md) governs the relationship; no doc-side mirror is maintained. +- [x] `session.subscribe` streaming-primitive shape — closed 2026-04-30 via [BL-102](../backlog.md) no-mirror disposition; canonical source: `packages/contracts/src/jsonrpc-streaming.ts` (`LocalSubscriptionProducer` shape, shipped at Plan-007 Phase 3). [api-payload-contracts.md §Source-of-Truth Policy](../architecture/contracts/api-payload-contracts.md) governs the relationship; no doc-side mirror is maintained. - [x] [Plan-001](./001-shared-session-core.md) Phase 2 schemas merged (`packages/contracts/src/session.ts` + `packages/contracts/src/event.ts`) — Phase 3 PR cannot open until Plan-001 Phase 2 is at HEAD Target paths below assume the canonical implementation topology defined in [Container Architecture](../architecture/container-architecture.md). @@ -149,25 +197,25 @@ The Tier 1 substrate Plan-007-partial ships at Phase 1-3 carries reciprocal obli ### CP-007-1 — `session.*` namespace contract owed to [Plan-001](./001-shared-session-core.md) Phase 5 -Plan-007-partial Phase 3 owns the typed handlers + SDK Zod wrapper for `SessionCreate` / `SessionRead` / `SessionJoin` / `SessionSubscribe`. The contract surface includes (a) the canonical method-name strings per [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md) (F-007p-3-01 closed 2026-04-30; dotted-lowercase: `session.create` / `session.read` / `session.join` / `session.subscribe`), (b) the request/response Zod schemas re-exported from `packages/contracts/src/session.ts` (Plan-001 Phase 2 ownership; imported transitively), (c) the producer/consumer streaming primitive split: the producer-side shape `LocalSubscriptionProducer` is canonical at `packages/contracts/src/jsonrpc-streaming.ts` (returned by `session.subscribe` as `LocalSubscriptionProducer` per [BL-102](../backlog.md) no-mirror disposition — F-007p-3-02 closed 2026-04-30); the consumer-side shape `LocalSubscriptionConsumer` is re-declared independently at `packages/client-sdk/src/transport/types.ts` (the SDK does NOT import the producer symbol — file-namespace separation is intentional; the canonical rename from the prior shared `LocalSubscription` identifier landed 2026-05-19 via BL-115 — see JSDoc at `jsonrpc-streaming.ts:316-326`), and (d) the re-entrant unsubscribe precondition — the SessionSubscribeDeps.subscribeToSession returned function MUST tolerate snapshot-during-emit / queued-removal because PR #19 F5's `onCancel` handlers fire from inside the upstream's `onEvent` call stack (canonical statement: `packages/runtime-daemon/src/ipc/handlers/session-subscribe.ts:122-132`; load-bearing for §Invariants I-007-11). +Plan-007-partial Phase 3 owns the typed handlers + SDK Zod wrapper for `SessionCreate` / `SessionRead` / `SessionJoin` / `SessionSubscribe`. The contract surface includes (a) the canonical method-name strings per [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md) (closed 2026-04-30; `dotted-camelCase`: `session.create` / `session.read` / `session.join` / `session.subscribe`), (b) the request/response Zod schemas re-exported from `packages/contracts/src/session.ts` (Plan-001 Phase 2 ownership; imported transitively), (c) the producer/consumer streaming primitive split: the producer-side shape `LocalSubscriptionProducer` is canonical at `packages/contracts/src/jsonrpc-streaming.ts` (returned by `session.subscribe` as `LocalSubscriptionProducer` per [BL-102](../backlog.md) no-mirror disposition — closed 2026-04-30); the consumer-side shape `LocalSubscriptionConsumer` is re-declared independently at `packages/client-sdk/src/transport/types.ts` (the SDK does NOT import the producer symbol — file-namespace separation is intentional; the canonical rename from the prior shared `LocalSubscription` identifier landed 2026-05-19 via BL-115 — see JSDoc at `jsonrpc-streaming.ts:316-326`), and (d) the re-entrant unsubscribe precondition — the SessionSubscribeDeps.subscribeToSession returned function MUST tolerate snapshot-during-emit / queued-removal because Plan-007 Phase 3's lifecycle-hook `onCancel` handlers fire from inside the upstream's `onEvent` call stack (canonical statement: `packages/runtime-daemon/src/ipc/handlers/session-subscribe.ts:122-132`; load-bearing for §Invariants I-007-11). **Why bidirectional.** Plan-001 Phase 5 (line 268-269) names `packages/client-sdk/src/sessionClient.ts` as Phase-5-owned and cites Plan-007-partial as the substrate Phase 5 imports. Without CP-007-1 on the Plan-007 side, the obligation is one-directional — Plan-001 reviewers see the dep but Plan-007 reviewers must reverse-search to find it. ### CP-007-2 — `presence.*` namespace contract owed to [Plan-002](./002-invite-membership-and-presence.md) -Plan-007-remainder (Tier 4) owns the substrate's namespace registry; Plan-002 (line 94) registers `presence.*` against that surface. The Tier 1 PR sequence ships the registry's typed surface at `packages/contracts/src/jsonrpc-registry.ts` — F-007p-2-03 closed 2026-04-30 via [BL-102](../backlog.md) no-mirror disposition (canonical source: code; api-payload-contracts.md does not maintain a doc-side mirror per its §Source-of-Truth Policy). +Plan-007-remainder (Tier 4) owns the substrate's namespace registry; Plan-002 (line 94) registers `presence.*` against that surface. The Tier 1 PR sequence ships the registry's typed surface at `packages/contracts/src/jsonrpc-registry.ts` — closed 2026-04-30 via [BL-102](../backlog.md) no-mirror disposition (canonical source: code; api-payload-contracts.md does not maintain a doc-side mirror per its §Source-of-Truth Policy). **Why bidirectional.** Plan-002 reviewers see the dependency on Plan-007's registry; Plan-007 reviewers (especially Tier 4 PR authors) must know that the registry's typed surface is contractually required to support Plan-002's registration before Plan-007-remainder lands. ### CP-007-3 — `router.register(method, handler)` registry surface owed to [Plan-026](./026-first-run-onboarding.md) and Tier 4 namespace plans -Plan-026 (line 236) imports the substrate's registry surface (`router.register(method, handler)`) for first-run-onboarding handlers. Tier 4 namespace plans (`run.*` / `repo.*` / `artifact.*` / `settings.*` / `daemon.*` per Plan-007-remainder) similarly register against the same surface. The typed shape ships at `packages/contracts/src/jsonrpc-registry.ts` (F-007p-2-03 closed 2026-04-30 via [BL-102](../backlog.md) no-mirror disposition; canonical source: code). +Plan-026 (line 236) imports the substrate's registry surface (`router.register(method, handler)`) for first-run-onboarding handlers. Tier 4 namespace plans (`run.*` / `repo.*` / `artifact.*` / `settings.*` / `daemon.*` per Plan-007-remainder) similarly register against the same surface. The typed shape ships at `packages/contracts/src/jsonrpc-registry.ts` (closed 2026-04-30 via [BL-102](../backlog.md) no-mirror disposition; canonical source: code). **Why bidirectional.** Multiple downstream plans cite the surface informally (Plan-026:236, Plan-002:94, Tier 4 namespace remainders); the surface itself must be authoritatively typed once and re-cited. ### CP-007-4 — Typed JSON-RPC client transport (`packages/client-sdk/src/transport/`) owed to all client-SDK consumers -Plan-007-partial Phase 3 owns the transport-layer + Zod-wrapping primitive that every typed-JSON-RPC client surface (`sessionClient`, future `runClient` / `presenceClient` / etc.) consumes. Per [F-007p-3-03] resolution, the Tier 1 file split is: +Plan-007-partial Phase 3 owns the transport-layer + Zod-wrapping primitive that every typed-JSON-RPC client surface (`sessionClient`, future `runClient` / `presenceClient` / etc.) consumes. The Tier 1 file split is: - `packages/client-sdk/src/transport/jsonRpcClient.ts` — Plan-007 CREATE (transport-layer + Zod wrapping) - `packages/client-sdk/src/transport/types.ts` — Plan-007 CREATE (`LocalSubscriptionConsumer` type + `Handler` shape) @@ -181,30 +229,133 @@ Plan-007-partial Phase 1 emits `security.default.override` (Spec-027:81+138+146 **Why bidirectional.** Spec-027 line 146 makes the load-bearing claim ("visible to Spec-006 event taxonomy"); Plan-006 / Spec-006 are the consumers. The CP-007-5 obligation pair is now satisfied at HEAD: the bootstrap emitter writes against a registered taxonomy entry, and the emitter table cites Plan-007 as authority. +### CP-007-6 — `driver.*` JSON-RPC method namespace registration owed to [Plan-005](./005-provider-driver-contract-and-capabilities.md) Phase 4 + +Plan-007-remainder (Tier 4) owns the substrate's namespace registry; Plan-005 Phase 4 (T4.1) registers 7 client-facing `driver.*` method handlers against that surface (6 non-lifecycle verbs + `driver.subscribeEvents` per Plan-005 §Phase 4 decision #2 — narrowed from the 2026-05-27 ratified 11 via Codex round-3 review on PR #124; the 4 session/run lifecycle ops — `createSession`/`resumeSession`/`startRun`/`closeSession` — are daemon-internal, invoked by orchestration on the in-daemon `ProviderRegistry`, never client-callable). The contract surface includes (a) the canonical method-name strings per [ADR-009](../decisions/009-json-rpc-ipc-wire-format.md) + §Invariants I-007-9 dotted long-form convention: `driver.listCapabilities`, `driver.interruptRun`, `driver.applyIntervention`, `driver.respondToRequest`, `driver.listModels`, `driver.listModes`, `driver.subscribeEvents`; (b) the Zod-validated wire envelope `DriverInterventionResultSchema` at the SDK seam per [Plan-005](./005-provider-driver-contract-and-capabilities.md) T4.2 (`{ status: 'applied' | 'degraded'; fallbackAction?: string }` mirroring driver-internal shape per Spec-005:44, 112); (c) the subscription primitive `driver.subscribeEvents` returns `LocalSubscriptionProducer` per CP-007-4 (shared streaming primitive at `packages/contracts/src/jsonrpc-streaming.ts`). + +**Why bidirectional.** Plan-005 reviewers see the dependency on Plan-007's registry (per Plan-005 CP-005-4 reciprocal); Plan-007 reviewers (especially Tier 4 PR authors) must know that the `driver.*` namespace is part of the registered namespace set before Plan-007-remainder lands. The 2026-05-27 Tier 4 audit (Plan-005) surfaces the obligation and adds `driver.*` to the §Tier 4 namespace enumeration above; without this CP entry the registration boundary would be inferred only from Plan-005's side. + +### CP-007-7 — `event.*` JSON-RPC method namespace registration owed to [Plan-006](./006-session-event-taxonomy-and-audit-log.md) Phase 4 + +Plan-007-remainder (Tier 4) owns the substrate's namespace registry; [Plan-006](./006-session-event-taxonomy-and-audit-log.md) Phase 4 (T4.7) registers 3 `event.*` method handlers against that surface — `event.readAfterCursor`, `event.readWindow`, `event.subscribe`. The contract surface includes (a) the canonical method-name strings per [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md) method-name regex (dotted-camelCase: `event.readAfterCursor`, `event.readWindow`, `event.subscribe`); (b) the Zod-validated wire envelope at the SDK seam per [Plan-006 §Phase 4](./006-session-event-taxonomy-and-audit-log.md) (request/response shapes owned by Plan-006); (c) the subscription primitive `event.subscribe` returns `LocalSubscriptionProducer` per CP-007-4 (shared streaming primitive at `packages/contracts/src/jsonrpc-streaming.ts`) and inherits I-007-10 (subscribe-init response precedes first notify) and I-007-11 (`onCancel` lifecycle across all externally-imposed cancel paths). + +**Why bidirectional.** Plan-006 reviewers see the dependency on Plan-007's registry (per Plan-006 CP-006-4 reciprocal); Plan-007 reviewers (especially Phase R1 PR authors) must know that the `event.*` namespace is part of the registered namespace set before Plan-007-remainder lands. The 2026-05-28 Tier 4 audit (Plan-007-remainder R1) surfaces the obligation and adds `event.*` to the §Tier 4 namespace enumeration above; without this CP entry the registration boundary would be inferred only from Plan-006's side and the I-007-6 duplicate-rejection co-dependency would be invisible to Phase R1 reviewers. + +### CP-007-8 — `DaemonKeyStore` interface boundary owed to [Plan-022](./022-data-retention-and-gdpr.md) Tier 5 + +Phase R2 T-007r-2-4 ships `DaemonKeyStore` as an interface at `packages/runtime-daemon/src/bootstrap/daemon-key-store.ts` along with a test-only `InMemoryDaemonKeyStore` stub for Vitest fixtures. [Plan-022](./022-data-retention-and-gdpr.md) Tier 5 ships the real `OsKeystoreSealedDaemonKeyStore` implementation using `@napi-rs/keyring` v1.2.0 (per [Spec-022 §Daemon Master Key :146](../specs/022-data-retention-and-gdpr.md) — Keychain `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` on macOS / `CRED_TYPE_GENERIC` `CRED_PERSIST_LOCAL_MACHINE` on Windows / Secret Service via libsecret + kwallet6 + keyutils fallback on Linux). The composition root (daemon bootstrap) MUST inject the real implementation — NEVER the stub — and MUST emit a runtime assertion against the stub outside tests; this composition-root injection + production-guard assertion lands at Plan-022 Tier 5 together with the real `OsKeystoreSealedDaemonKeyStore` it guards (R2 ships only the interface + the test-only `InMemoryDaemonKeyStore` stub + the first-run ceremony exercised against that stub — it cannot inject or assert against a real implementation that does not yet exist). Mirrors the keystore-boundary precedent established by Plan-006 CP-006-1 (interface + test-only stub + composition-root injection + downstream plan ships real impl). + +**Spec-022 supersedes the prior `keytar` reference.** Plan-006 T2.7 originally named `keytar` as the cross-platform shim; Spec-022:146 ratifies the move to `@napi-rs/keyring` v1.2.0 (keytar is unmaintained). Plan-007 takes the Spec-022 anchor as the load-bearing primary source; cross-reference Plan-006 only for the _interface-boundary pattern_, not the library choice. + +**Why bidirectional.** Plan-022 reviewers see the dependency on Plan-007's interface; Plan-007 reviewers see the dependency on Plan-022's real impl. Without CP-007-8 on the Plan-007 side, the obligation is one-directional — Plan-022 reviewers see it but Plan-007 reviewers must reverse-search. + +### CP-007-9 — Banner content contract extension owed to [Plan-026](./026-first-run-onboarding.md) + +Plan-007 row 10 (Spec-027 banner content) currently delivers banner CONTENT (partial — fields (b) effective bind addresses + `bannerFormat` selector at Tier 1 per the Plan-007 retroactive audit); Phase R2 extends the content contract to ALSO include (a) TLS mode + fingerprint (SPKI-SHA256 + whole-cert hash via `TlsSurface.getFingerprint()` from T-007r-2-3), (c) backup destination + cadence (Plan-022 owned — surface via existing `effectiveSettings()` extension; out of R2 scope but coordinated), (d) admin-token file path (`./data/admin-token` from T-007r-2-4's first-run ceremony), (e) update channel + mode (from `UpdateNotifyPoller.getEffectiveSettings()` extension on T-007r-2-5), (f) any active `security.default.override=*` rows (sourced from `SecureDefaultOverrideEmitter.listEmitted()` — extension to the existing emitter at T-007r-2-2). [Plan-026](./026-first-run-onboarding.md) owns the rendering/format; Plan-007 owns the content extension surface. + +**Why bidirectional.** Plan-026 (first-run-onboarding) cites Plan-007's `effectiveSettings()` as the content source; Plan-007 reviewers must know the contract extension at Tier 4 includes the additional fields. + +### CP-007-10 — Phase R3 CLI banner string consumed from Phase R2 surface + +Phase R3 (CLI) consumes the Spec-027 row 10 loud banner string ship at Phase R2 (Secure Defaults). Phase R3's CLI (T-007r-3-2 + T-007r-3-6) consumes the banner string at startup and at every command invocation (subject to `--no-banner` rules + I-007-16). Direction is intra-plan (R2 → R3); included here for cross-phase reviewer-visibility and to make explicit that the `--no-banner` gate respects the security-critical tag emitted by R2's banner-assembler. + +**Why named.** Phase R3-PR-a's `self-update` command (T-007r-3-6a, relocated from R2 per the 2026-05-28 ratification reversal) consumes the banner string from R2's banner-content surface at runtime (via `settings.effectiveRead`); without this CP, the cross-phase banner consumption is implicit and R3 PR authors would not see the R2 banner-string source-of-truth dependency. R2 merges before R3, so the source is present when R3 consumes it. + +### CP-007-11 — Phase R3 desktop transport-supervision consumer ↔ [Plan-023](./023-desktop-shell-and-renderer.md) Tier 8 daemon-supervisor (bidirectional concern-split) + +Phase R3 ships the JSON-RPC **transport-level** supervision consumer at `apps/desktop/src/main/daemon-status-projector/` — subscribes to the `SupervisionHooks` surface from `local-ipc-gateway.ts:114-172` (`onConnect` / `onDisconnect` / `onError`), translates each event into a domain-level `DaemonStatusEvent`, projects the stream through the preload bridge per Spec-023 §Trust Stance. [Plan-023](./023-desktop-shell-and-renderer.md) Tier 8 ships the **process-level** supervisor at `apps/desktop/src/main/daemon-supervisor.ts` — `utilityProcess.fork(daemonEntryPath, args, { serviceName: 'ai-sidekicks-daemon' })`, exit-reason switch including `"memory-eviction"` carve-out, backoff ladder `100ms, 300ms, 1s, 3s, 10s` with 5-attempt ceiling (Plan-023:208), version-negotiation gate. Both subscribe to each other's projected stream via `LocalSubscriptionConsumer` multiplexing: Plan-023's `daemon-supervisor.ts` consumes `daemon.status.*` (transport state) to enrich its restart-decision logic; Phase R3's `daemon-status-projector/` consumes `daemon.process.*` (process state) to surface process-crash + memory-eviction state in the renderer's daemon-status pill. + +**Why bidirectional.** Phase R3 cannot synthesize "daemon is healthy" from transport-state alone (a transport disconnect during a process restart looks identical to a transport disconnect during a daemon hang); Plan-023's process-supervisor cannot decide "restart vs persistent error" from process-exit-state alone (a clean process exit during a normal `daemon.stop` looks identical to a crash mid-handshake). Each subscribes to the other for the missing half. Concern-disjoint, two state machines, one combined user-visible status — see §Notes Concern-Split paragraph below for the architectural rationale. + +**Resolution lineage.** The concern-split (option (a)) was ratified by the 2026-05-28 Tier 4 audit (Plan-007-remainder §R3) over options (b) Plan-023 Tier-4 partial carve and (c) Plan-007 R3 → Tier 8 deferral; user-ratified 2026-05-28. The `apps/desktop/src/main/daemon-supervision/` directory name in earlier drafts is renamed to `daemon-status-projector/` to remove the apparent path collision with Plan-023's `daemon-supervisor.ts`. + +### CP-007-12 — Phase R3 IPC bridge handler EXTENDS [Plan-023](./023-desktop-shell-and-renderer.md) Tier 8 bridge registry + +[Plan-023](./023-desktop-shell-and-renderer.md) Tier 8 ships `apps/desktop/src/main/bridge/index.ts` as the canonical IPC-handler registration entry. Phase R3's T-007r-3-9 (`apps/desktop/src/main/bridge/daemon-status.ts`) EXTENDS this registry by adding one channel — the `daemon.status` topic under the existing generic `daemon.subscribe` channel per Spec-023 §Preload Bridge Contract. Pre-Plan-023-Tier-8, Phase R3 stands the file alone in `apps/desktop/src/main/bridge/` and is imported by `bridge/index.ts` once Plan-023 ships; Plan-023 Phase 2 Tier-1-Partial substrate provides the initial `bridge/` directory skeleton. + +**Why bidirectional.** Plan-023 reviewers see the dependency on Phase R3 adding one bridge file; Phase R3 reviewers see that the broader registry mount-point is Plan-023's. Without CP-007-12, the boundary is implicit and the two phases risk landing the same files. + +### CP-007-13 — Phase R3 renderer view → [Plan-002](./002-invite-membership-and-presence.md) Phase 6 ambient bridge type + +[Plan-002](./002-invite-membership-and-presence.md) Phase 6 (NS-29 hoist per [cross-plan-dependencies.md](../architecture/cross-plan-dependencies.md)) hoisted `apps/desktop/src/renderer/src/sidekicks-bridge.d.ts` out of `SessionBootstrap.tsx` into a dedicated file. Phase R3's `DaemonStatusView.tsx` consumes the ambient `window.sidekicks` type from this file. Phase R3 EXTENDS the `SidekicksBridge` interface (in `packages/contracts/src/desktop-bridge.ts`, Plan-023 owned) by adding the `daemon.status` topic to the typed subscription map; Phase R3 does NOT modify the `.d.ts` itself. + +**Why bidirectional.** Plan-002 reviewers (retroactive — Phase 6 already shipped) see the contract their hoisted ambient type satisfies; Phase R3 reviewers see that the renderer view typechecks against an already-shipped ambient type and that the `SidekicksBridge` interface extension is the only forward edit Phase R3 owns. + +### CP-007-14 — Phase R3 CLI → [Plan-001](./001-shared-session-core.md) Phase 5 `@ai-sidekicks/client-sdk` package + +Phase R3's CLI imports `@ai-sidekicks/client-sdk` for the typed daemon client (JSON-RPC envelope builder, `DaemonHello`/`DaemonHelloAck` handshake helper, namespace-handler typed callers). [Plan-001](./001-shared-session-core.md) Phase 5 (T5.1) created the package; Phase R3 BLOCKED until Plan-001 Phase 5 is at HEAD (verified — Plan-001 Phase 5 fully shipped per Plan-001:268 Done Checklist). + +**Why bidirectional.** Plan-001 Phase 5 reviewers see the client-SDK surface their package ships; Phase R3 reviewers see the dependency on the typed daemon client and the requirement that the SDK NEVER swallow validation errors (per Plan-007 I-007-3-T4). + ## Target Areas -- `packages/contracts/src/daemon/` -- `packages/client-sdk/src/daemonClient.ts` -- `packages/runtime-daemon/src/ipc/local-ipc-gateway.ts` -- `packages/runtime-daemon/src/ipc/protocol-negotiation.ts` -- `packages/runtime-daemon/src/bootstrap/secure-defaults.ts` — `SecureDefaults` configuration and enforcement layer (Spec-027 daemon-side rows) -- `packages/runtime-daemon/src/bootstrap/secure-defaults-events.ts` — `security.default.override=*` audit event emitters -- `packages/runtime-daemon/src/bootstrap/tls-surface.ts` — daemon TLS 1.3-only listener factory (Spec-027 row 8) -- `packages/runtime-daemon/src/bootstrap/first-run-keys.ts` — daemon first-run key generation (Spec-027 row 3 daemon scope) -- `packages/runtime-daemon/src/bootstrap/update-notify.ts` — Spec-027 row 7a notify-by-default poller -- `apps/cli/src/commands/self-update.ts` — Spec-027 row 7b dual-verification self-update (manifest sig + Sigstore bundle) -- `apps/desktop/src/main/daemon-supervision/` -- `apps/desktop/src/renderer/src/daemon-status/` -- `apps/cli/src/` - -## Data And Storage Changes +### Contract surfaces (`packages/contracts/src/`) + +- `packages/contracts/src/jsonrpc.ts` — JSON-RPC 2.0 envelope + `protocolVersion` (Tier 1, shipped) +- `packages/contracts/src/jsonrpc-registry.ts` — `MethodRegistry` typed surface (Tier 1, shipped) +- `packages/contracts/src/jsonrpc-streaming.ts` — `LocalSubscriptionProducer` primitive (Tier 1, shipped) +- `packages/contracts/src/jsonrpc-negotiation.ts` — `DaemonHello` / `DaemonHelloAck` (Tier 1, shipped) +- `packages/contracts/src/session.ts` — `session.*` namespace request/response Zod schemas (Plan-001 Phase 2, shipped) +- `packages/contracts/src/daemon-status.ts` — `DaemonStatusReadRequest` / `DaemonStatusReadResponse` (NEW — Phase R1 T-007r-1-1) +- `packages/contracts/src/daemon-lifecycle.ts` — `DaemonStopRequest`/`Response` (with `idleDrainDeadlineMs?: number` field) + `DaemonRestartRequest`/`Response` (NEW — Phase R1 T-007r-1-2); no `DaemonStartRequest`/`Response` and no unified `DaemonLifecycleParams` action-discriminator — `daemon.start` is not an IPC method (cold-boot is the CLI process-spawn path per T-007r-3-4), and stop/restart carry separate per-method request schemas rather than a shared `action` union +- `packages/contracts/src/settings.ts` — `SettingsEffectiveReadRequest`/`Response` mirroring `SecureDefaults.effectiveSettings` shape with active-override list (NEW — Phase R1 T-007r-1-5) +- `packages/contracts/src/desktop-bridge.ts` — extends `SidekicksBridge` interface with `daemon.status` topic on the typed subscription map (Plan-023 owned; Phase R3 EXTENDS per CP-007-13) + +### Daemon IPC + bootstrap (`packages/runtime-daemon/src/`) + +- `packages/runtime-daemon/src/ipc/local-ipc-gateway.ts` (Tier 1, shipped — supervision-hook surface at `:114-172`; `SupervisionDisconnectReason` 5-value enum at `:150-155`) +- `packages/runtime-daemon/src/ipc/protocol-negotiation.ts` (Tier 1, shipped) +- `packages/runtime-daemon/src/ipc/handlers/daemon-status-read.ts` (NEW — Phase R1 T-007r-1-1) +- `packages/runtime-daemon/src/ipc/handlers/daemon-stop.ts` (NEW — Phase R1 T-007r-1-3) +- `packages/runtime-daemon/src/ipc/handlers/daemon-restart.ts` (NEW — Phase R1 T-007r-1-4) — no `daemon-start.ts` IPC handler: `daemon.start` cold-boot is owned by the CLI process-spawn path (`ai-sidekicks daemon start`, T-007r-3-4), not an in-daemon handler (a stopped daemon has no IPC server to receive the call) +- `packages/runtime-daemon/src/ipc/handlers/settings-effective-read.ts` (NEW — Phase R1 T-007r-1-5) +- `packages/runtime-daemon/src/ipc/handlers/index.ts` (EXTEND — Phase R1 T-007r-1-6; per-method `register*` re-exports) +- `packages/runtime-daemon/src/bootstrap/secure-defaults.ts` — `SecureDefaults` configuration and enforcement layer (Tier 1 substrate at HEAD; Phase R2 T-007r-2-1 widens schema for `tlsMode` / `firstRunKeysPolicy` / `nonLoopbackHost`) +- `packages/runtime-daemon/src/bootstrap/secure-defaults-events.ts` — `security.default.override=*` audit event emitters (Tier 1 substrate; Phase R2 T-007r-2-2 widens row coverage) +- `packages/runtime-daemon/src/bootstrap/tls-surface.ts` — daemon TLS 1.3-only listener factory (NEW — Phase R2 T-007r-2-3; Spec-027 row 8) +- `packages/runtime-daemon/src/bootstrap/first-run-keys.ts` — daemon first-run key generation (NEW — Phase R2 T-007r-2-4; Spec-027 row 3 daemon scope) +- `packages/runtime-daemon/src/bootstrap/daemon-key-store.ts` — `DaemonKeyStore` interface + test-only `InMemoryDaemonKeyStore` stub (NEW — Phase R2 T-007r-2-4; real `OsKeystoreSealedDaemonKeyStore` ships at Plan-022 Tier 5 per CP-007-8) +- `packages/runtime-daemon/src/bootstrap/update-notify.ts` — Spec-027 row 7a notify-by-default poller (NEW — Phase R2 T-007r-2-5) +- `packages/runtime-daemon/src/bootstrap/index.ts` (EXTEND — Phase R1 T-007r-1-7 wires owned-namespace `register*` calls after `SecureDefaults.load`; Phase R2 T-007r-2-8 orchestrates `FirstRunKeys` + `TlsSurface` + `UpdateNotifyPoller`) + +### Client SDK + CLI (`packages/client-sdk/`, `apps/cli/`) + +- `packages/client-sdk/src/transport/jsonRpcClient.ts` (Tier 1, shipped) +- `packages/client-sdk/src/transport/types.ts` (Tier 1, shipped) +- `apps/cli/` workspace package (NEW — Phase R3 T-007r-3-1 scaffolds the entire workspace; `package.json` declares `name: "@ai-sidekicks/cli"`, `bin: { "ai-sidekicks": "./dist/main.js" }`, CLI parser `clipanion@4.0.0-rc.4`, single-file ESM bundle via `tsup`) +- `apps/cli/src/main.ts` — single entrypoint; instantiates the clipanion `Cli` builder (which hosts `--version` / `--help` / global flags as built-ins on the `Cli` instance — not a separate command file) and surfaces the `settings.effectiveRead` banner subject to `--no-banner` + I-007-16 (Phase R3 T-007r-3-2) +- `apps/cli/src/exit-codes.ts` — closed deterministic `JsonRpcErrorCode → PosixExitCode` mapping; `UnmappedExitCodeError` thrown on drift (Phase R3 T-007r-3-3; I-007-14; Spec-007:32) +- `apps/cli/src/commands/daemon-start.ts` / `daemon-stop.ts` / `daemon-restart.ts` — `ai-sidekicks daemon start` / `stop` / `restart` (Phase R3 T-007r-3-4; three files per the post-F9 lifecycle split — `daemon start` is a process-spawn while `stop`/`restart` are IPC calls on the running daemon, so they do not share a single module) +- `apps/cli/src/commands/daemon-status.ts` — `ai-sidekicks daemon status` (Phase R3 T-007r-3-5; git-style subcommand; renders human-readable text or `--json`) +- `apps/cli/src/commands/settings.ts` — `ai-sidekicks settings effective-read` (Phase R3 T-007r-3-6; I-007-3 — the command surface adds no secret-bearing fields to the response shape) +- `apps/cli/src/commands/self-update.ts` — Spec-027 row 7b dual-verification self-update (NEW — Phase R3-PR-a T-007r-3-6a; manifest sig + `@sigstore/verify` v3.x bundle; ships in the R3-owned `apps/cli/` scaffold alongside the other CLI commands; relocated from R2 per the 2026-05-28 ratification reversal — Codex PR #124) +- `apps/cli/src/update/manifest-verifier.ts` / `sigstore-verifier.ts` / `anti-rollback.ts` / `atomic-swap.ts` (NEW — Phase R3-PR-a T-007r-3-6a sub-modules) + +### Desktop main process (`apps/desktop/src/main/`) + +- `apps/desktop/src/main/daemon-status-projector/` — Phase R3 transport-supervision consumer subtree (NEW; renamed from prior `daemon-supervision/` per the concern-split resolution; concern-disjoint from Plan-023 Tier 8's `daemon-supervisor.ts` per CP-007-11): + - `index.ts` — `attachSupervisionConsumer(gateway, projector)` (Phase R3 T-007r-3-8) + - `status-machine.ts` — XState v5 state machine projecting `SupervisionHooks` events to `daemon.status.*` (Phase R3 T-007r-3-8) +- `apps/desktop/src/main/bridge/daemon-status.ts` — `daemon.status.subscribe` IPC bridge handler (NEW — Phase R3 T-007r-3-9; EXTENDS Plan-023 Tier 8's `bridge/index.ts` registry per CP-007-12) +- `apps/desktop/src/main/index.ts` (EXTEND — Phase R3 T-007r-3-10 wires `attachSupervisionConsumer` after `app.whenReady()`) + +### Desktop renderer (`apps/desktop/src/renderer/src/`) + +- `apps/desktop/src/renderer/src/daemon-status/` — Phase R3 chrome-level renderer subtree (NEW; deliberately placed outside `features/` because daemon-status is always-visible chrome, not a navigable feature-route — see §Notes Renderer Chrome-vs-Features Placement paragraph below): + - `DaemonStatusView.tsx` — function component subscribing via `window.sidekicks.daemon.subscribe('daemon.status', ...)` (Phase R3 T-007r-3-11) + - `disconnect-reason-copy.ts` — pure mapping from `SupervisionDisconnectReason` 5-value enum (per `local-ipc-gateway.ts:150-155`) to humanized strings (Phase R3 T-007r-3-11) +- `apps/desktop/src/renderer/src/App.tsx` (EXTEND — Phase R3 T-007r-3-12 mounts `` in always-visible bottom-bar chrome) + +### Notes — Renderer Chrome-vs-Features Placement + +Phase R3's `apps/desktop/src/renderer/src/daemon-status/` subtree is **deliberately placed outside `apps/desktop/src/renderer/src/features/`** because daemon-status is always-visible chrome (a persistent bottom-bar pill surfacing supervision state at all times), NOT a navigable feature route. This contrasts with Plan-002 Phase 6's `apps/desktop/src/renderer/src/features/session-members/` (a navigable feature subtree). The chrome-vs-features distinction is a project-level convention codified here as a documentation note (per the 2026-05-28 Tier 4 audit demotion — originally proposed as an invariant in R3-completeness.md but demoted to convention status per advisor's "convenience, not load-bearing" call). Phase R3's `DaemonStatusView.tsx` cross-links to this paragraph via the §Target Areas forward-reference above; Plan-023 §Renderer Structure (per Plan-023:131) is amended to acknowledge the convention without elevating it to a Plan-023-level invariant. - Persist daemon version-compatibility diagnostics and reconnect metadata only where needed for actionable client status. - No new shared control-plane storage is required for the local IPC contract itself. ## API And Transport Changes -- Add `DaemonHello`, `DaemonHelloAck`, `DaemonStatusRead`, `DaemonStart`, `DaemonStop`, `DaemonRestart`, and shared subscription primitives to the typed client SDK. +- Add `DaemonHello`, `DaemonHelloAck`, `DaemonStatusRead`, `DaemonStop`, `DaemonRestart`, and shared subscription primitives to the typed client SDK. (`DaemonStart` is intentionally absent: cold-boot is the `ai-sidekicks daemon start` process-spawn path per T-007r-3-4 — the SDK's `DaemonHello`/`DaemonHelloAck` handshake is what that spawn path awaits, not a `daemon.start` JSON-RPC call.) - Implement OS-local socket or pipe transport as the default client path, with explicit loopback fallback hooks. ## CLI Delivery Track @@ -234,15 +385,15 @@ Plan-007 owns the daemon-side enforcement of [Spec-027 §Required Behavior](../s ## Implementation Steps - Contracts: See [API Payload Contracts](../architecture/contracts/api-payload-contracts.md) for typed schemas this plan consumes. -- Tier markers below correspond to §Execution Windows (V1 Carve-Out): `[Tier 1]` = ships in Plan-007-partial alongside Plan-001; `[Tier 4]` = ships in Plan-007-remainder. +- Tier markers below correspond to §Execution Windows (V1 Carve-Out): `[Tier 1]` = ships in Plan-007-partial alongside Plan-001; `[Tier 4]` = ships in Plan-007-remainder. Phase markers `[R1]` / `[R2]` / `[R3]` correspond to the §Tier 4 Phase Decomposition section. -1. **[Tier 1: `session.*` only; Tier 4: rest]** Define daemon handshake, lifecycle, and subscription contracts in shared packages. The `session.*` namespace contracts (`SessionCreate`, `SessionRead`, `SessionJoin`, `SessionSubscribe`) ship in the Tier 1 partial — they are Plan-001 vertical-slice contracts that already live in `packages/contracts/src/session.ts`. The `run.*`, `repo.*`, `artifact.*`, `settings.*`, and `daemon.*` namespace contracts ship in Tier 4 alongside the corresponding handlers. -2. **[Tier 1: `secure-defaults.ts` + `secure-defaults-events.ts` (validation scope limited to loopback OS-local socket binding; Tier-4-scope keys refused with `unknown_setting` per fail-closed invariant); Tier 4: `tls-surface.ts` + `first-run-keys.ts` + `update-notify.ts` + Spec-027 row coverage extension as bind paths widen]** Implement `SecureDefaults` configuration + enforcement layer covering Spec-027 rows 2, 3, 4, 7a, 8, 10 (split per the bracketed Tier 1 / Tier 4 file scopes above; row-by-row tier mapping in [`cross-plan-dependencies.md` §Plan-007 Substrate-vs-Namespace Carve-Out](../architecture/cross-plan-dependencies.md#plan-007-substrate-vs-namespace-carve-out-tier-1--tier-4)); wire it as the first step of daemon bootstrap before any listener binds. The Tier 1 partial validates the bind paths it actually exposes (loopback OS-local socket only); Tier 4 widens validation as additional bind paths (HTTP, non-loopback, TLS) and Spec-027 surfaces (`tls-surface.ts`, `first-run-keys.ts`, `update-notify.ts`) are introduced. -3. **[Tier 4]** Implement daemon TLS 1.3-only listener factory (`tls-surface.ts`) and first-run key ceremony (`first-run-keys.ts`). -4. **[Tier 4]** Implement update-notify poller (`update-notify.ts`, row 7a) and CLI self-update dual-verification command (`apps/cli/src/commands/self-update.ts`, row 7b). -5. **[Tier 1: substrate + loopback-bind validation via `effectiveSettings`; Tier 4: extended bind-path validation]** Implement OS-local IPC gateway (`local-ipc-gateway.ts`) and protocol-version negotiation (`protocol-negotiation.ts`) in the Local Runtime Daemon. The Tier 1 partial ships the substrate (JSON-RPC 2.0 + LSP-style Content-Length framing, 1MB max-message-size, error model, supervision hooks, OS-local socket / named-pipe transport, gated loopback fallback) plus the SDK Zod layer (~500–1000 LOC per [Spec-007 §Wire Format](../specs/007-local-ipc-and-daemon-control.md#wire-format)) and the `session.*` namespace handlers; the gateway consumes `SecureDefaults.effectiveSettings` for the loopback OS-local bind path. Tier 4 widens what `effectiveSettings` exposes (TLS mode, non-loopback bind, additional override flags) as the corresponding bind paths enter the daemon. -6. **[Tier 4]** Implement the CLI on top of the same client SDK and daemon contract rather than embedding daemon logic directly. -7. **[Tier 4]** Implement desktop-shell daemon supervision and actionable startup or reconnect status surfaces on the same stabilized contract. +1. **[Tier 1: `session.*` only; Tier 4 / R1: daemon.* + settings.* + namespace registration for run.*/repo.*/artifact.*/driver.*/event.* per NS-26 precedent]** Define daemon handshake, lifecycle, and subscription contracts in shared packages. The `session.*` namespace contracts (`SessionCreate`, `SessionRead`, `SessionJoin`, `SessionSubscribe`) ship in the Tier 1 partial — they are Plan-001 vertical-slice contracts that already live in `packages/contracts/src/session.ts`. The `daemon.*` and `settings.*` namespace contracts ship at Tier 4 Phase R1 (T-007r-1-1..3); `run.*` (Plan-004), `repo.*` (Plan-009), `artifact.*` (Plan-014), `driver.*` (Plan-005 Phase 4), `event.*` (Plan-006 Phase 4) attach from their owning plans per the explicit ownership map under §Execution Windows. +2. **[Tier 1: `secure-defaults.ts` + `secure-defaults-events.ts` (validation scope limited to loopback OS-local socket binding; Tier-4-scope keys refused with `unknown_setting` per fail-closed invariant); Tier 4 / R2: `tls-surface.ts` + `first-run-keys.ts` + `daemon-key-store.ts` (interface boundary per CP-007-8) + `update-notify.ts` + Spec-027 row coverage extension as bind paths widen]** Implement `SecureDefaults` configuration + enforcement layer covering Spec-027 rows 2, 3, 4, 7a, 7b, 8, 10 (split per the bracketed Tier 1 / Tier 4 file scopes above; row-by-row tier mapping in [`cross-plan-dependencies.md` §Plan-007 Substrate-vs-Namespace Carve-Out](../architecture/cross-plan-dependencies.md#plan-007-substrate-vs-namespace-carve-out-tier-1--tier-4)); wire it as the first step of daemon bootstrap before any listener binds. The Tier 1 partial validates the bind paths it actually exposes (loopback OS-local socket only); Tier 4 Phase R2 widens validation as additional bind paths (HTTP, non-loopback, TLS) and Spec-027 surfaces (`tls-surface.ts`, `first-run-keys.ts`, `daemon-key-store.ts`, `update-notify.ts`) are introduced. +3. **[Tier 4 / R2]** Implement daemon TLS 1.3-only listener factory (`tls-surface.ts`) and first-run key ceremony (`first-run-keys.ts`) backed by the `DaemonKeyStore` interface (`daemon-key-store.ts`) — interface + test-only `InMemoryDaemonKeyStore` stub at R2 per CP-007-8; production `OsKeystoreSealedDaemonKeyStore` ships at Plan-022 Tier 5 using `@napi-rs/keyring` v1.2.0 per [Spec-022:146](../specs/022-data-retention-and-gdpr.md). +4. **[Tier 4 / R2]** Implement the daemon-side update-notify poller (`update-notify.ts`, row 7a). The CLI self-update dual-verification command (`apps/cli/src/commands/self-update.ts`, row 7b) is implemented in **R3-PR-a (T-007r-3-6a)**, NOT R2 — relocated per the 2026-05-28 ratification reversal (a CLI command cannot land in the R2 PR before R3 scaffolds the `apps/cli/` workspace; see T-007r-2-6). +5. **[Tier 1: substrate + loopback-bind validation via `effectiveSettings`; Tier 4 / R1: extended bind-path validation + namespace registration]** Implement OS-local IPC gateway (`local-ipc-gateway.ts`) and protocol-version negotiation (`protocol-negotiation.ts`) in the Local Runtime Daemon. The Tier 1 partial ships the substrate (JSON-RPC 2.0 + LSP-style Content-Length framing, 1MB max-message-size, error model, supervision hooks at `:114-172`, OS-local socket / named-pipe transport, gated loopback fallback) plus the SDK Zod layer (~500–1000 LOC per [Spec-007 §Wire Format](../specs/007-local-ipc-and-daemon-control.md#wire-format)) and the `session.*` namespace handlers; the gateway consumes `SecureDefaults.effectiveSettings` for the loopback OS-local bind path. Tier 4 Phase R1 widens what `effectiveSettings` exposes (TLS mode, non-loopback bind, additional override flags) as the corresponding bind paths enter the daemon and registers Plan-007's own `daemon.*` + `settings.*` namespaces into the substrate registry — the other five Tier 4 namespaces (`run.*` / `repo.*` / `artifact.*` / `driver.*` / `event.*`) attach from their owning plans per the NS-26 precedent, not from R1. +6. **[Tier 4 / R3]** Stand up the `apps/cli/` workspace package (`@ai-sidekicks/cli` with clipanion@4.0.0-rc.4 + tsup single-file ESM bundle) and implement CLI commands on top of the shared `@ai-sidekicks/client-sdk` and daemon contracts rather than embedding daemon logic directly. CLI commands MUST acknowledge `DaemonHelloAck` BEFORE reporting success per I-007-15 and MUST honor I-007-13 import isolation (the CLI workspace can only import `@ai-sidekicks/contracts`, `@ai-sidekicks/client-sdk`, `clipanion`, and Node built-ins). +7. **[Tier 4 / R3]** Implement desktop-shell daemon transport-supervision consumer at `apps/desktop/src/main/daemon-status-projector/` — concern-disjoint from Plan-023 Tier 8's process-supervisor `apps/desktop/src/main/daemon-supervisor.ts` per CP-007-11 — and actionable startup or reconnect status surfaces (chrome-level `DaemonStatusView.tsx` outside `apps/desktop/src/renderer/src/features/` per the chrome-vs-features placement convention documented in §Target Areas Notes) on the same stabilized contract. ## Tier 1 Partial PR Sequence @@ -294,7 +445,7 @@ preconditions: - **T-007p-2-1** (Files: `packages/runtime-daemon/src/ipc/local-ipc-gateway.ts` + `packages/contracts/src/jsonrpc.ts` (CREATE if not present); Verifies invariant: I-007-7 + I-007-8; Spec coverage: Spec-007 §Wire Format + ADR-009) — Implement JSON-RPC 2.0 dispatcher with LSP-style Content-Length framing (`Content-Length: \r\n\r\n`) per [ADR-009](../decisions/009-json-rpc-ipc-wire-format.md). The handshake `protocolVersion` field type is ratified at [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md) (BL-102 closed 2026-05-01) as ISO 8601 `YYYY-MM-DD` date-string; substrate narrowed accordingly. The substrate gate at `#dispatchFrame` enforces the per-request envelope-level `protocolVersion` field (Spec-007:54) BEFORE handler dispatch per I-007-7; non-conforming envelopes (missing / wrong type / invalid format) project as `-32600 InvalidRequest` + `data.type: "transport.invalid_protocol_version"` + `data.fields: { reason, observedType? }` per [error-contracts.md §Plan-007 Tier 1 Domain Identifiers](../architecture/contracts/error-contracts.md#plan-007-tier-1-domain-identifiers); the `daemon.hello` method is exempt because the negotiation parameter rides in `params.protocolVersion` (canonical exempt set: `ENVELOPE_PROTOCOL_VERSION_EXEMPT_METHODS` at `packages/contracts/src/jsonrpc.ts`). The 1MB max-message-size limit is **hard-coded in the substrate** (per F-007p-2-11 conservative resolution) — changes require a Phase 2 amendment + Spec-007 update. Per F-007p-2-12, the supervision-hook surface is `{ onConnect(transport): void; onDisconnect(transport, reason): void; onError(transport, err): void }` exported from the gateway for the Tier 4 desktop-shell supervision surface to consume (Plan-007-remainder picks up the consumer side). - **T-007p-2-2** (Files: `packages/runtime-daemon/src/ipc/jsonrpc-error-mapping.ts` (CREATE) + `packages/runtime-daemon/src/ipc/local-ipc-gateway.ts` consumer wiring; Verifies invariant: I-007-7 + I-007-8) — Implement the JSON-RPC numeric-error-space (`-32700` parse error / `-32600` invalid request / `-32601` method not found / `-32602` invalid params / `-32603` internal error) ↔ project dotted-namespace two-layer envelope per [error-contracts.md §JSON-RPC Wire Mapping](../architecture/contracts/error-contracts.md#json-rpc-wire-mapping) (BL-103 closed 2026-05-01). Mapping per the canonical table: parse failure → `-32700`; missing/malformed JSON-RPC envelope → `-32600`; unregistered method → `-32601`; Zod validation failure → `-32602` with the project dotted code carried in `data.type`; handler-thrown registered domain error → preserves dotted code in `data.type`, JSON-RPC `code` selected per error-contracts §Plan-007 Tier 1 Domain Identifiers; unhandled exception → `-32603` with sanitized message. Oversized-body rejection (per F-007p-2-05): close the connection with a JSON-RPC error frame matching `-32600 InvalidRequest` and emit `data.type: "transport.message_too_large"` with `data.fields: { limit, observed }` (HTTP 413 semantic — peer mis-framing the wire layer). This is intentionally distinct from Spec-001's domain-quota code `resource.limit_exceeded` (HTTP 429 semantic, `data.fields: { resource, limit, current }`); a 1MB transport-frame overflow is a wire-layer failure, not a control-plane resource saturation. Envelope-level per-request `protocolVersion` rejection (Spec-007:54): map the synthetic FramingError code `invalid_protocol_version` to `-32600 InvalidRequest` + `data.type: "transport.invalid_protocol_version"` + `data.fields: { reason, observedType? }` (`reason` ∈ `"missing" | "wrong_type" | "invalid_format"`); the connection STAYS OPEN (envelope-level violation, not framing-level), distinct from oversized-body which tears down the connection. -- **T-007p-2-3** (Files: `packages/runtime-daemon/src/ipc/registry.ts` (CREATE) + `packages/contracts/src/jsonrpc-registry.ts` (CREATE if not present); Verifies invariant: I-007-6 + I-007-7 + I-007-9; Spec coverage: §Cross-Plan Obligations CP-007-3) — Implement the method-namespace registry typed surface. The `MethodRegistry` interface is canonical at `packages/contracts/src/jsonrpc-registry.ts` per [BL-102](../backlog.md) no-mirror disposition (F-007p-2-03 closed 2026-04-30; method-name format closed via [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md)). Inline shape: `interface MethodRegistry { register(method: string, paramsSchema: ZodSchema

, resultSchema: ZodSchema, handler: (params: P, ctx: HandlerContext) => Promise, opts?: { mutating?: boolean }): void; dispatch(method: string, params: unknown, ctx: HandlerContext): Promise; has(method: string): boolean; }`. Per F-007p-2-06, the read-vs-mutating classification is the optional `mutating: boolean` flag at registration time; the substrate uses this flag for the version-mismatch gate (refuse mutating ops when `DaemonHelloAck.compatible === false`; allow read-only methods through). The canonical method-name regex `/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/` MUST be enforced at register-time per I-007-9 (governance source: api-payload-contracts.md §Tier 1 (cont.): Plan-007). Tests: T-007p-2-T1 duplicate registration rejected (I-007-6); T-007p-2-T2 schema-validates-before-dispatch (I-007-7); T-007p-2-T3 method-name regex validation (I-007-9). +- **T-007p-2-3** (Files: `packages/runtime-daemon/src/ipc/registry.ts` (CREATE) + `packages/contracts/src/jsonrpc-registry.ts` (CREATE if not present); Verifies invariant: I-007-6 + I-007-7 + I-007-9; Spec coverage: §Cross-Plan Obligations CP-007-3) — Implement the method-namespace registry typed surface. The `MethodRegistry` interface is canonical at `packages/contracts/src/jsonrpc-registry.ts` per [BL-102](../backlog.md) no-mirror disposition (F-007p-2-03 closed 2026-04-30; method-name format closed via [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md)). Inline shape: `interface MethodRegistry { register(method: string, paramsSchema: ZodSchema

, resultSchema: ZodSchema, handler: (params: P, ctx: HandlerContext) => Promise, opts?: { mutating?: boolean }): void; dispatch(method: string, params: unknown, ctx: HandlerContext): Promise; has(method: string): boolean; }`. Per F-007p-2-06, the read-vs-mutating classification is the optional `mutating: boolean` flag at registration time; the substrate uses this flag for the version-mismatch gate (refuse mutating ops when `DaemonHelloAck.compatible === false`; allow read-only methods through). The canonical method-name regex `/^[a-z][a-z0-9]*(\.[a-z][a-zA-Z0-9]*)+$/` MUST be enforced at register-time per I-007-9 (governance source: api-payload-contracts.md §Tier 1 (cont.): Plan-007 — note the **non-leading** segments permit camelCase (`[a-zA-Z0-9]`) so the repo's own method names like `driver.listCapabilities` / `settings.effectiveRead` validate; the **leading** namespace segment stays lowercase-only by design). Tests: T-007p-2-T1 duplicate registration rejected (I-007-6); T-007p-2-T2 schema-validates-before-dispatch (I-007-7); T-007p-2-T3 method-name regex validation (I-007-9). - **T-007p-2-4** (Files: `packages/runtime-daemon/src/ipc/protocol-negotiation.ts`; Verifies invariant: I-007-7; Spec coverage: Spec-007 §Required Behavior line 47 + §Fallback Behavior line 67-68) — Implement `DaemonHello` / `DaemonHelloAck` exchange. Negotiation algorithm (per F-007p-2-10 resolution): daemon selects the lex-max element of `client.supportedProtocols ∩ daemon.supported` (ISO 8601 date-strings; lex-order ≡ chronological per BL-102 ratification); if intersection is empty, surface `version.floor_exceeded` (client too old) or `version.ceiling_exceeded` (client too new) on `DaemonHelloAck.reason` per [error-contracts.md §Negotiation Refusals](../architecture/contracts/error-contracts.md#negotiation-refusals) and refuse all subsequent mutating ops via the registry's `mutating: boolean` flag check. The per-request `protocolVersion` field is an ISO 8601 `YYYY-MM-DD` date-string (per Spec-007:54 + [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md), BL-102 ratified 2026-05-01) that echoes the negotiated handshake version selected from `daemon.supportedProtocols`. Loopback-fallback gate (per F-007p-2-09): the gate is the SecureDefaults validation surface — Tier 1 only allows loopback OS-local transport; loopback-fallback transport requires explicit operator opt-in via a config key NOT YET DEFINED at Tier 1 (deferred to Tier 4 with non-loopback bind). At Tier 1, attempting loopback-fallback fails with the canonical `transport.unavailable` envelope (`-32603 InternalError` + `data.type: "transport.unavailable"` + `data.fields: { requested, reason }`) per [error-contracts.md §Plan-007 Tier 1 Domain Identifiers](../architecture/contracts/error-contracts.md#plan-007-tier-1-domain-identifiers). - **T-007p-2-5** (Files: `packages/runtime-daemon/src/ipc/streaming-primitive.ts` (CREATE) + `packages/contracts/src/jsonrpc-streaming.ts` (CREATE); Verifies invariant: I-007-7) — Implement the `LocalSubscriptionProducer` JSON-RPC streaming primitive at Phase 2 (per F-007p-2-14 — primitive ships with substrate, handler binding ships at Phase 3). The streaming primitive shape is canonical at `packages/contracts/src/jsonrpc-streaming.ts` per [BL-102](../backlog.md) no-mirror disposition (F-007p-3-02 closed 2026-04-30; api-payload-contracts.md does not maintain a doc-side mirror per its §Source-of-Truth Policy). Inline shape: initial response `{ subscriptionId: string }`; notification frame `{ jsonrpc: "2.0", method: "$/subscription/notify", params: { subscriptionId: string, value: T } }` (LSP `$/cancelRequest` pattern); cancel via `$/subscription/cancel` method (params: `{ subscriptionId }`). Server-side cleanup on transport disconnect. Tests: T-007p-2-T4 subscribe round-trip (initial response + N notifications + cancel + cleanup verified). This file is the authoritative streaming primitive Plan-001 Phase 5 imports. - **T-007p-2-6** (Files: `packages/runtime-daemon/src/ipc/__tests__/local-ipc-gateway.test.ts` + `protocol-negotiation.test.ts` + `registry.test.ts` + `streaming-primitive.test.ts`) — Author Phase 2 test suite: W-007p-2-T1 handshake + version-negotiation compatibility (Spec-007 line 47); W-007p-2-T2 transport for Unix domain socket; W-007p-2-T3 transport for Windows named pipe; W-007p-2-T4 transport: gated loopback fallback fails at Tier 1 with `transport.unavailable` (per F-007p-2-09 Tier 1 conservative gate); W-007p-2-T5 1MB max-message-size enforcement → connection close + `-32600` error frame (per F-007p-2-05); W-007p-2-T6 Content-Length framing parser correctness (single message, multi-message buffer, malformed framing → connection close); W-007p-2-T7 method-not-found → `-32601` (per F-007p-2-04 + I-007-9 namespace-isolation); W-007p-2-T8 mutating-op gate when `DaemonHelloAck.compatible === false` (per Spec-007:67-68); W-007p-2-T9 schema-validates-before-dispatch → handler not invoked on malformed payload, `-32602` returned (I-007-7); W-007p-2-T10 handler-thrown error → `-32603` (no stack/secret leak, I-007-8); W-007p-2-T11 streaming primitive round-trip + cancel cleanup (I-007-7 streaming variant). Cite [ADR-009](../decisions/009-json-rpc-ipc-wire-format.md) inline in the gateway test file's header comment per F-007p-2-08. @@ -317,7 +468,7 @@ preconditions: #### Tasks -- **T-007p-3-1** (Files: `packages/runtime-daemon/src/ipc/handlers/session-create.ts` + `session-read.ts` + `session-join.ts` + `session-subscribe.ts`; Verifies invariant: I-007-7 + I-007-8; Spec coverage: §Cross-Plan Obligations CP-007-1) — Implement the four `session.*` handlers binding into the registry from T-007p-2-3. Method-name strings closed 2026-04-30 per [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md); subscribe streaming-primitive shape closed 2026-04-30 via [BL-102](../backlog.md) no-mirror disposition (canonical source: `packages/contracts/src/jsonrpc-streaming.ts`). Each handler imports the request/response Zod schemas from `packages/contracts/src/session.ts` (Plan-001 Phase 2 ownership; precondition per §Dependencies). Canonical method-name strings: `session.create` / `session.read` / `session.join` / `session.subscribe` (dotted-lowercase per api-payload-contracts.md §Tier 1 (cont.): Plan-007 method-name table). Handler signatures per F-007p-3-08 code-surface example: +- **T-007p-3-1** (Files: `packages/runtime-daemon/src/ipc/handlers/session-create.ts` + `session-read.ts` + `session-join.ts` + `session-subscribe.ts`; Verifies invariant: I-007-7 + I-007-8; Spec coverage: §Cross-Plan Obligations CP-007-1) — Implement the four `session.*` handlers binding into the registry from T-007p-2-3. Method-name strings closed 2026-04-30 per [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md); subscribe streaming-primitive shape closed 2026-04-30 via [BL-102](../backlog.md) no-mirror disposition (canonical source: `packages/contracts/src/jsonrpc-streaming.ts`). Each handler imports the request/response Zod schemas from `packages/contracts/src/session.ts` (Plan-001 Phase 2 ownership; precondition per §Dependencies). Canonical method-name strings: `session.create` / `session.read` / `session.join` / `session.subscribe` (`dotted-camelCase` per api-payload-contracts.md §Tier 1 (cont.): Plan-007 method-name table). Handler signatures per F-007p-3-08 code-surface example: ```typescript // Daemon side @@ -380,6 +531,139 @@ preconditions: After Phase 3 merges (and Plan-008 bootstrap Phase 1 also merges), [Plan-001 Phase 5](./001-shared-session-core.md#phase-5--client-sdk-and-desktop-bootstrap) consumer can begin. +## Tier 4 Phase Decomposition + +The Tier 4 remainder (Plan-007-remainder) lands as **3 PRs** following the substrate-vs-namespace decomposition rule and the explicit ownership map under §Execution Windows. Each PR is reviewable in isolation. The 2026-05-28 Tier 4 audit (per [the plan-readiness-audit runbook](../operations/plan-implementation-readiness-audit-runbook.md)) ratifies the phase boundaries below; per-phase preconditions reference the same audit semantics as the Tier 1 partial. + +### Phase R1 — Namespace Handlers (Tier 4) + +**Goal:** Daemon-side JSON-RPC namespace handlers ship for `daemon.*` (read + lifecycle) and `settings.*` (effective-read only); namespace registration completes for ALL Tier 4 namespaces (daemon._, settings._, run._, repo._, artifact._, driver._, event.\*) per §Execution Windows ownership map; downstream plans (Plan-004/005/006/009/014) attach their handlers to the same registry. + +**Precondition:** Plan-007 Tier 1 partial (Phases 1-3) merged + Plan-001 vertical-slice migrated. Phase R1 entry point. + + +```yaml +preconditions: + - { type: audit_status, status: complete, evidence_pr: 124, baseline_tag: "plan-readiness-audit-tier-4" } +``` + +- `packages/contracts/src/daemon-status.ts` (CREATE) — Zod schemas for `DaemonStatusReadRequest` / `DaemonStatusReadResponse`; the response shape carries process state (`running` | `starting` | `stopping` | `degraded`), version-negotiation state, transport endpoint, and uptime ms. +- `packages/contracts/src/daemon-lifecycle.ts` (CREATE) — Zod schemas for `DaemonStopRequest` / `DaemonStopResponse` / `DaemonRestartRequest` / `DaemonRestartResponse`; the stop/restart request payloads carry `idleDrainDeadlineMs?: number` (default 5000) per the hardened-resolution decision + I-007-12 self-swap-refusal coordination. **No `DaemonStart*` schema:** `daemon.start` is NOT an IPC method — a stopped daemon has no IPC server to receive it, so cold-boot is owned by the `ai-sidekicks daemon start` CLI command (process spawn + `DaemonHelloAck` wait, T-007r-3-4), not a JSON-RPC handler. +- `packages/contracts/src/settings.ts` (CREATE) — Zod schemas for `SettingsEffectiveReadRequest` / `SettingsEffectiveReadResponse`; the response shape projects `SecureDefaults.effectiveSettings()` minus secret-bearing fields (TLS fingerprint surfaced as opaque string, admin-token file path surfaced, backup destination surfaced, update channel surfaced) per CP-007-9 banner-content extension. +- `packages/runtime-daemon/src/ipc/handlers/daemon-status-read.ts` (CREATE) — handler for `daemon.status.read` method. +- `packages/runtime-daemon/src/ipc/handlers/daemon-stop.ts` + `daemon-restart.ts` (CREATE × 2) — handlers for `daemon.stop` / `daemon.restart`; mutating: true. There is NO `daemon-start.ts` handler: `daemon.start` (cold-boot) cannot be an IPC method because a stopped daemon hosts no JSON-RPC endpoint — the `ai-sidekicks daemon start` CLI command spawns the daemon process out-of-band and awaits `DaemonHelloAck` (T-007r-3-4). `daemon.stop` / `daemon.restart` operate on an already-running daemon, so they remain in-daemon IPC methods. (This reverses the prior 3-handler lifecycle design, which specified `daemon.start` as an IPC handler — Tier-4 audit, PR #124.) +- `packages/runtime-daemon/src/ipc/handlers/settings-effective-read.ts` (CREATE) — handler for `settings.effectiveRead`; mutating: false. +- `packages/runtime-daemon/src/bootstrap/index.ts` (EXTEND) — registers the seven Tier 4 namespaces' handlers into the registry from T-007p-2-3 (Tier 1 partial Phase 2). `daemon.*` and `settings.*` register their own handlers here; `run.*` (Plan-004), `repo.*` (Plan-009), `artifact.*` (Plan-014), `driver.*` (Plan-005 P4), `event.*` (Plan-006 P4) register from their owning plans per the NS-26 precedent. +- [`docs/architecture/contracts/api-payload-contracts.md`](../architecture/contracts/api-payload-contracts.md) (EXTEND) — append the Tier 4 method-name table covering `daemon.status.read`, `daemon.stop`, `daemon.restart`, `settings.effectiveRead` (no `daemon.start` row — cold-boot is the CLI process-spawn path, not an IPC method); cross-link to Plan-004/005/006/009/014 method-name tables. +- [`docs/architecture/contracts/error-contracts.md`](../architecture/contracts/error-contracts.md) (EXTEND) — register `daemon.lifecycle_conflict` (the I-007-12 self-swap refusal envelope) in the Plan-007 Tier 4 Domain Identifiers section; map to JSON-RPC `-32603 InternalError` + `data.type: "daemon.lifecycle_conflict"` + `data.fields: { activeClientCount, observedClientIds }`. The `-32603` selection matches the existing `transport.unavailable` precedent at [error-contracts.md §Plan-007 Tier 1 Domain Identifiers](../architecture/contracts/error-contracts.md#plan-007-tier-1-domain-identifiers) — wire-level envelope is well-formed, the daemon refuses for state-based reasons (not a JSON-RPC §5.1 `-32600 InvalidRequest` wire-malformation surface). +- Tests: `daemon.*` and `settings.*` handler integration tests; registry membership test scoped to the two R1-owned namespaces (`daemon.*` + `settings.*` register; the registry REJECTS a duplicate registration per I-007-6, verified via a deliberate `daemon.*` re-register — the no-collision mechanism the other five namespaces rely on when they attach from their owning plans); namespace isolation test (cross-namespace `daemon.foo` ≠ `settings.foo`) per I-007-9. The `run.*` / `repo.*` / `artifact.*` / `driver.*` / `event.*` registrations land + are tested from their owning plans (Plan-004/009/014/005/006), not R1 — R1 ships before those handlers exist, so an all-seven assertion here would either block the PR or force ownership-violating placeholder registrations. + +#### Tasks + +- **T-007r-1-1** (Files: `packages/contracts/src/daemon-status.ts`; Verifies invariant: I-007-7; Spec coverage: §CP-007-9 + Spec-027 row 10 banner content) — Author Zod schemas for daemon status read. Inline shape: `DaemonStatusReadRequest = z.object({})`; `DaemonStatusReadResponse = z.object({ processState: z.enum(["running","starting","stopping","degraded"]), protocolVersion: z.string(), transportEndpoint: z.string(), uptimeMs: z.number().int().nonnegative() })`. The contract surface is canonical at this file per [BL-102](../backlog.md) no-mirror disposition; the method-name `daemon.status.read` is registered in api-payload-contracts.md Tier 4 table by T-007r-1-8. +- **T-007r-1-2** (Files: `packages/contracts/src/daemon-lifecycle.ts`; Verifies invariant: I-007-7 + I-007-12 + I-007-15; Spec coverage: Spec-007 §Daemon Lifecycle Methods + Spec-027:63 row 7a) — Author Zod schemas for `daemon.stop` / `daemon.restart` (no `DaemonStart*` — `daemon.start` is not an IPC method; cold-boot is the CLI process-spawn path, T-007r-3-4). Inline shape: `DaemonStopRequest = z.object({ idleDrainDeadlineMs: z.number().int().nonnegative().default(5000) })`; `DaemonRestartRequest = z.object({ idleDrainDeadlineMs: z.number().int().nonnegative().default(5000) })`; success-response shape is `{ accepted: true }` (the I-007-12 self-swap refusal is surfaced ONLY through the canonical JSON-RPC error envelope per T-007r-1-9, NOT through a success-response discriminator — this avoids dual-surface incoherence where one condition projects across both response and error wires). The `idleDrainDeadlineMs` field uses `.default(5000)` ALONE (NOT `.default(5000).optional()`): in Zod a bare `.default()` already makes the input key omittable while guaranteeing the PARSED output is `5000`, whereas wrapping it in `.optional()` makes the outer optional accept an omitted key as `undefined` BEFORE the default applies — so a normal `daemon-stop`/`daemon-restart` request that omits the field would yield `undefined` and the documented 5000ms drain deadline would never reach the handler. This is the hardened authoring decision (no SIGKILL fallback for orphaned requests — the caller surfaces a quiesce error if the deadline is exceeded), and it depends on the contract schema — not ad-hoc handler fallback — supplying the deadline. +- **T-007r-1-3** (Files: `packages/contracts/src/settings.ts`; Verifies invariant: I-007-3 + I-007-7; Spec coverage: §CP-007-9) — Author Zod schemas for `settings.effectiveRead`. Inline shape: `SettingsEffectiveReadRequest = z.object({})`; `SettingsEffectiveReadResponse = z.object({ bindAddress: z.string(), bindPort: z.number().int().nullable(), localIpcPath: z.string(), tlsMode: z.enum(["off","required","required-and-pinned"]), tlsFingerprint: z.string().nullable(), bannerFormat: z.enum(["text","json"]), updateChannel: z.string(), updateMode: z.string(), adminTokenPath: z.string(), backupDestination: z.string().nullable(), backupCadence: z.string().nullable(), securityOverrides: z.array(z.object({ row: z.string(), effective_value: z.unknown(), behavior: z.string(), securityCritical: z.boolean() })) })`. The shape is the canonical content contract for Spec-027 row 10's banner; CP-007-9 owns the bidirectional dependency on Plan-026 rendering. The `securityCritical` flag is load-bearing: R2 marks security-critical override rows with `securityCritical: true`, and the CLI MUST query it before honoring `--no-banner` (I-007-16 — `--no-banner` does NOT suppress security-critical rows per Spec-027). Without it on the contract, the client cannot distinguish critical from non-critical banners and could silently suppress the Spec-027 security-override warning. +- **T-007r-1-4** (Files: `packages/runtime-daemon/src/ipc/handlers/daemon-status-read.ts`; Verifies invariant: I-007-7) — Implement `daemon.status.read` handler. Read-only; `mutating: false`. Projects daemon process state DIRECTLY from the gateway-level `SupervisionHooks` surface (`local-ipc-gateway.ts:114-172`, a Plan-007-partial Tier-1 substrate shipped by T-007p-2-1), which is self-contained within R1 — R1 is a precondition for R3 and MUST NOT depend on any R3 surface. The R3 desktop-side supervision-status state machine (T-007r-3-13) is a downstream CONSUMER of this handler's output (it subscribes the desktop `daemon-status-projector` to `SupervisionHooks` and projects renderer-facing state), NOT a source this handler reads from. This keeps the `daemon.status.read` contract independently shippable + testable in R1. +- **T-007r-1-5** (Files: `packages/runtime-daemon/src/ipc/handlers/daemon-stop.ts` + `daemon-restart.ts` + `packages/runtime-daemon/src/ipc/handlers/lifecycle-conflict-error.ts` (CREATE); Verifies invariant: I-007-12 + I-007-15) — Implement the two daemon-lifecycle handlers (`daemon.stop` + `daemon.restart`; `daemon.start` is not an IPC handler — cold-boot is the CLI process-spawn path, T-007r-3-4). Both mark `mutating: true`. `daemon.stop` and `daemon.restart` honor `idleDrainDeadlineMs` per Spec-027:63 + Spec-027:187. The I-007-12 restriction is about self-swapping the binary while OTHER IPC clients are live — NOT about refusing a normal stop/restart merely because the requesting client is connected. Since the invoking CLI/renderer is ITSELF an active IPC client until the handler returns, the handler MUST EXCLUDE its own connection from the count: it resolves the caller's connection id from the request context, then drains the OTHER clients (signals quiesce + waits up to `idleDrainDeadlineMs` for them to disconnect). Only if non-caller clients REMAIN after the drain deadline does it refuse, throwing `LifecycleConflictError(otherActiveClientCount, observedOtherClientIds)` (typed class extending `Error`; the JSON-RPC error mapper at `packages/runtime-daemon/src/ipc/jsonrpc-error-mapping.ts` projects it as the canonical envelope per T-007r-1-9). The I-007-12 check is therefore `ClientRegistry.getActiveCount({ excluding: callerConnectionId }) > 0` evaluated AFTER the drain window — never a bare handler-entry `getActiveCount() > 0` (which would always be true because the caller is itself counted, so the lifecycle command could never succeed). On success the handlers return `{ accepted: true }`; refusal is the JSON-RPC error path (the consumer discriminates on `error.data.type === "daemon.lifecycle_conflict"`). +- **T-007r-1-6** (Files: `packages/runtime-daemon/src/ipc/handlers/settings-effective-read.ts`; Verifies invariant: I-007-3 + I-007-7) — Implement `settings.effectiveRead`. Projects `SecureDefaults.effectiveSettings()` plus the Tier 4 additions (TLS fingerprint, admin-token path, banner-content fields per CP-007-9). NEVER exposes raw secret material; `tlsFingerprint` is the SPKI-SHA256 derived projection from `TlsSurface.getFingerprint()` (R2-owned dependency). +- **T-007r-1-7** (Files: `packages/runtime-daemon/src/bootstrap/index.ts` consumer EXTEND; Verifies invariant: I-007-6 + I-007-9) — Extend daemon bootstrap to register the `daemon.*` + `settings.*` namespace handlers from T-007r-1-4..6 into the registry from T-007p-2-3 (Tier 1 partial Phase 2 substrate). Ordering matters: registration MUST complete before the IPC gateway accepts its first frame (per I-007-1). The seven-namespace coordination dependency: this task registers ONLY `daemon.*` + `settings.*`; the other five namespaces (run/repo/artifact/driver/event) register from their owning plans' handler files per the NS-26 precedent. +- **T-007r-1-8** (Files: `docs/architecture/contracts/api-payload-contracts.md` EXTEND; Verifies invariant: I-007-9 method-name regex governance) — Append the Tier 4 method-name table. Canonical method names: `daemon.status.read`, `daemon.stop`, `daemon.restart`, `settings.effectiveRead` (no `daemon.start` — cold-boot is the CLI process-spawn path per T-007r-3-4, not an IPC method). Each row cites the request-schema-file (per T-007r-1-1..3) and the handler-file (per T-007r-1-4..6). The Tier 4 table is the doc-mirror for namespace ownership; the contract source-of-truth remains the `.ts` schema files per BL-102. +- **T-007r-1-9** (Files: `docs/architecture/contracts/error-contracts.md` EXTEND; Verifies invariant: I-007-12) — Register the `daemon.lifecycle_conflict` envelope in the Plan-007 Tier 4 Domain Identifiers section. JSON-RPC mapping: `-32603 InternalError` + `data.type: "daemon.lifecycle_conflict"` + `data.fields: { activeClientCount: number, observedClientIds: string[] }` — where `activeClientCount` / `observedClientIds` count and list ONLY the OTHER (non-caller) clients still connected after the `idleDrainDeadlineMs` drain window, NOT the requesting client itself (per the T-007r-1-5 caller-exclusion semantics). The `-32603` selection matches the existing `transport.unavailable` envelope's precedent (state-based refusal where the wire envelope is well-formed) — JSON-RPC §5.1 reserves `-32600 InvalidRequest` for wire-level malformation only, which this envelope is not. The envelope is fired by `daemon.stop` and `daemon.restart` handlers (T-007r-1-5) when self-swap-refusal trips per Spec-027:63 + :187; the `LifecycleConflictError` class throws from the handler and the JSON-RPC error mapper projects it through the canonical envelope. +- **T-007r-1-10** (Files: `packages/runtime-daemon/src/ipc/handlers/__tests__/daemon-handlers.test.ts` + `settings-handlers.test.ts`; Verifies all I-007-12 + I-007-15 + I-007-7 + I-007-9 at Tier 4 scope) — Author the R1 test suite. Tests: R1-T1 `daemon.status.read` round-trip projects correct state under each `processState` enum value; R1-T2 the daemon-lifecycle namespace registers exactly `daemon.stop` + `daemon.restart` as `mutating: true` and registers NO `daemon.start` IPC method (cold-boot is the CLI process-spawn path per T-007r-3-4 — a `daemon.start` handler must not exist on the registry); R1-T3 `daemon.stop` SUCCEEDS (`{ accepted: true }`) when the requesting client is the only active client (caller-exclusion — the bare `getActiveCount() > 0` bug would wrongly refuse here) AND refuses with `lifecycle_conflict` when a SECOND (non-caller) client remains connected past the drain deadline (I-007-12 verification; asserts `data.fields.observedClientIds` excludes the caller); R1-T4 `daemon.restart` honors `idleDrainDeadlineMs` (5000 default; sub-second test override) — a non-caller client that disconnects WITHIN the drain window yields success, one that remains AFTER the window yields `lifecycle_conflict`; R1-T5 `settings.effectiveRead` response excludes raw secret material (I-007-3); R1-T6 the two R1-owned namespaces (`daemon.*` + `settings.*`) register and a deliberate duplicate `daemon.*` registration is REJECTED per I-007-6 (the no-collision mechanism; the other five namespaces register + are tested from their owning plans, not R1); R1-T7 `daemon.foo` ≠ `settings.foo` (namespace isolation, I-007-9). + +### Phase R2 — Secure Defaults (Tier 4) + +**Goal:** Spec-027 rows 2/3/7a/8 ship daemon-side (TLS surface + first-run keys ceremony + update-notify poller + Spec-027 banner content extension). Row 7b — the `apps/cli/` `self-update` command — is NOT an R2 deliverable: it ships in R3-PR-a (T-007r-3-6a), relocated per the 2026-05-28 ratification reversal (see T-007r-2-6). R2's row-7b contribution is the daemon-side update-notify poller (row 7a) plus the bundled update trust root the R3 command verifies against at runtime. + +**Precondition:** Phase R1 merged. R2 depends on R1's `settings.effectiveRead` projection surface (CP-007-9 banner content extension). + + +```yaml +preconditions: + - { type: audit_status, status: complete, evidence_pr: 124, baseline_tag: "plan-readiness-audit-tier-4" } + - { type: phase_merged, phase: R1 } +``` + +- `packages/runtime-daemon/src/bootstrap/secure-defaults.ts` (EXTEND) — accept additional config keys: `tlsMode` (Spec-027 row 2), `firstRunKeysPolicy` (row 3), `nonLoopbackHost` (row 4 widening), `updateChannel` + `updateMode` (row 7a + 7b), `adminTokenPath` (row 3 ceremony output), `backupDestination` + `backupCadence` (row coordinated with Plan-022). +- `packages/runtime-daemon/src/bootstrap/secure-defaults-events.ts` (EXTEND) — add `listEmitted(): Array<{ row, effective_value, behavior }>` projection for banner-content extension consumption (CP-007-9 + T-007r-2-7). +- `packages/runtime-daemon/src/bootstrap/tls-surface.ts` (CREATE) — `TlsSurface` class encapsulating TLS 1.3-only listener factory (per Spec-027 row 8 + Spec-007 §Transport); exposes `getFingerprint(): { spkiSha256: string, certHash: string }` for banner content surface. +- `packages/runtime-daemon/src/bootstrap/first-run-keys.ts` (CREATE) — `FirstRunKeyCeremony` orchestrator (per Spec-027 row 3): generates daemon master key, seals via DaemonKeyStore, writes `./data/admin-token` (admin-token file path surfaced via T-007r-1-6). +- `packages/runtime-daemon/src/bootstrap/daemon-key-store.ts` (CREATE) — `DaemonKeyStore` interface boundary + `InMemoryDaemonKeyStore` test stub per CP-007-8; production implementation `OsKeystoreSealedDaemonKeyStore` ships at Plan-022 Tier 5 using `@napi-rs/keyring` v1.2.0 per [Spec-022:146](../specs/022-data-retention-and-gdpr.md). +- `packages/runtime-daemon/src/bootstrap/update-notify.ts` (CREATE) — `UpdateNotifyPoller` for Spec-027 row 7a (passive update notification with `effectiveSettings` projection for banner-content extension via T-007r-2-7). +- `apps/cli/src/commands/self-update.ts` — Spec-027 row 7b dual-verification self-update command. Implemented in **R3-PR-a (T-007r-3-6a)**, not R2 — relocated per the 2026-05-28 ratification reversal (see T-007r-2-6). Listed here only to flag the R2→R3 runtime dependency (daemon-bundled trust root + CP-007-9/10 banner string consumed via `settings.effectiveRead`); R2 ships no `apps/cli/` file. +- `packages/runtime-daemon/src/bootstrap/index.ts` (EXTEND) — wire the new R2 subsystems into bootstrap ordering: `SecureDefaults.load` → `TlsSurface.maybeStart` → `FirstRunKeyCeremony.maybeRun` → `UpdateNotifyPoller.start`; banner-content extension assembles after all four complete per CP-007-9. +- Tests: TLS-surface integration tests; first-run-keys ceremony test (with `InMemoryDaemonKeyStore`); update-notify poller test; banner-content-extension projection test; I-007-12 self-swap-refusal integration test; I-007-16 `--no-banner` security-critical bypass refusal test. + +#### Tasks + +- **T-007r-2-1** (Files: `packages/runtime-daemon/src/bootstrap/secure-defaults.ts` EXTEND; Verifies invariant: I-007-1 + I-007-2 + I-007-3 + I-007-5; Spec coverage: Spec-027 rows 2/3/7a/7b/8) — Extend `SecureDefaults` config-key acceptance to recognize the Tier 4 keys (`tlsMode`, `firstRunKeysPolicy`, `nonLoopbackHost`, `updateChannel`, `updateMode`, `adminTokenPath`, `backupDestination`, `backupCadence`). Tier 1 partial refused these with `unknown_setting` (per T-007p-1-1); Tier 4 accepts them. Validation per Spec-027 row constraints (`tlsMode ∈ ["off","required","required-and-pinned"]`; `updateMode ∈ ["passive","interactive","silent"]`; etc.); fail closed on invalid value (I-007-2). +- **T-007r-2-2** (Files: `packages/runtime-daemon/src/bootstrap/secure-defaults-events.ts` EXTEND; Verifies invariant: I-007-4) — Add `listEmitted(): Array<{ row, effective_value, behavior, banner_printed_at? }>` projection. Per CP-007-9, the banner-content assembly consumes this list at startup (or, if banner has not yet printed, at first banner-print). The single-emit-per-startup invariant from T-007p-1-2 (I-007-4) holds. +- **T-007r-2-3** (Files: `packages/runtime-daemon/src/bootstrap/tls-surface.ts`; Verifies invariant: I-007-1 + I-007-7; Spec coverage: Spec-027 row 8) — Implement `TlsSurface` class. TLS 1.3-only enforcement (Node TLS context options `{ minVersion: "TLSv1.3", maxVersion: "TLSv1.3" }` per current Node TLS API). `getFingerprint(): { spkiSha256: string, certHash: string }` derives the SPKI-SHA256 fingerprint from the daemon's serving certificate (banner content extension via T-007r-2-7). +- **T-007r-2-4** (Files: `packages/runtime-daemon/src/bootstrap/first-run-keys.ts` + `packages/runtime-daemon/src/bootstrap/daemon-key-store.ts`; Verifies invariant: I-007-1 + I-007-2 + I-007-3; Spec coverage: Spec-027 row 3 + CP-007-8) — Implement (a) `DaemonKeyStore` interface boundary + `InMemoryDaemonKeyStore` test stub (CP-007-8 governs the Plan-022 Tier 5 real implementation contract — keystore-boundary pattern matches Plan-006 CP-006-1's interface + stub + composition-root injection precedent); (b) the `FirstRunKeyCeremony` orchestrator generating the daemon master key + sealing via the injected store + writing `./data/admin-token`. R2 ships + tests the ceremony ORCHESTRATION against the `InMemoryDaemonKeyStore` stub (key generation, admin-token write, idempotency-given-an-already-populated-store). The real `OsKeystoreSealedDaemonKeyStore` implementation, its composition-root injection, the production-guard runtime assertion (refuse to boot a non-test daemon when the stub is detected), and the cross-platform OS-keystore sealing verification (permissions + sentinel + fingerprint on Linux/macOS/Windows) are **Plan-022 Tier 5** deliverables per CP-007-8 — NOT R2 (the real impl does not exist until Tier 5, so R2 cannot inject it or assert against it; the Tier-4→Tier-5 window is development-only, with no production deployment until V1 ships post-Tier-5, so the stub is safe there and the production-guard lands together with the real impl it guards). +- **T-007r-2-5** (Files: `packages/runtime-daemon/src/bootstrap/update-notify.ts`; Verifies invariant: I-007-1; Spec coverage: Spec-027 row 7a) — Implement `UpdateNotifyPoller`. Passive polling against the update-channel endpoint (per Spec-027 row 7a). Exposes `getEffectiveSettings(): { channel: string, mode: string, lastChecked: Date | null, pendingVersion: string | null }` for banner-content extension consumption (CP-007-9 via T-007r-2-7). +- **T-007r-2-6** — **RELOCATED to R3-PR-a as T-007r-3-6a (2026-05-28 ratification reversal — Codex review PR #124).** The prior "R2/R3 co-shipping ratification" ("self-update is R2's responsibility but it ships in R3's scaffold") created an unsatisfiable cross-PR ordering constraint: an R2-numbered task whose `apps/cli/src/commands/self-update.ts` file cannot compile until R3's `T-007r-3-1` scaffolds the `apps/cli/` workspace, while R2 merges BEFORE R3. **Reversal rationale (checked against the original intent per advisor guidance):** the ratification's OWN stated target was that self-update "co-ships with R3 commands in R3 PR" — so the coherent fix honors that intent by relocating the _task_ to R3-PR-a (where the scaffold + the other CLI commands already live), NOT by moving the scaffold into the secure-defaults phase (which would mis-assign CLI-delivery infrastructure to R2 AND contradict the R3-PR-shipping intent). A CLI self-update command is intrinsically a CLI-delivery (R3) concern; R2 owns no `apps/cli/` file. The command's R2-originated security inputs — Spec-027 row 7b dual-verification + the daemon-bundled update trust root + the CP-007-9/10 banner string — are RUNTIME dependencies (trust root bundled with the daemon; banner string read via `settings.effectiveRead`), satisfied because R2 merges before R3, NOT compile-time/PR-order dependencies. The R2 task slot is retained as a stable numbering anchor (T-007r-2-7/2-8/2-9 unchanged). +- **T-007r-2-7** (Files: `packages/runtime-daemon/src/bootstrap/index.ts` EXTEND; Verifies invariant: I-007-1 + I-007-3 + I-007-4; Spec coverage: CP-007-9 banner content extension) — Wire R2 bootstrap ordering. `SecureDefaults.load(config)` → `TlsSurface.maybeStart(...)` → `FirstRunKeyCeremony.maybeRun(...)` → `UpdateNotifyPoller.start(...)` → banner-content assembly. Banner-content extension wires `effectiveSettings()` → `{ tlsMode, tlsFingerprint, updateChannel, updateMode, adminTokenPath, backupDestination, backupCadence, securityOverrides }` from the four subsystems above. Per I-007-4, the security-overrides projection emits each row ONCE per startup. +- **T-007r-2-8** (Files: `packages/runtime-daemon/src/bootstrap/secure-defaults.ts` EXTEND for `bannerCanBeSuppressed` flag enforcement; Verifies invariant: I-007-16) — Implement I-007-16 enforcement: security-critical override banner rows cannot be suppressed by `--no-banner`. The R2 banner-content extension marks each security-critical row with `securityCritical: true`; the CLI banner-rendering layer (T-007r-3-2) honors `--no-banner` ONLY for non-critical rows. R2's responsibility: tag the security-critical rows at emission time so R3's CLI respects the boundary. +- **T-007r-2-9** (Files: `packages/runtime-daemon/src/bootstrap/__tests__/tls-surface.test.ts` + `first-run-keys.test.ts` + `update-notify.test.ts` + `banner-content-extension.test.ts`) — Author the R2 test suite. Tests: R2-T1 TLS surface refuses non-TLS-1.3 client; R2-T2 first-run-keys ceremony is idempotent: given a `DaemonKeyStore` (the `InMemoryDaemonKeyStore` stub seeded with an existing key) the ceremony does NOT regenerate the master key (at-rest sealing persistence across real OS restarts is verified with the real `OsKeystoreSealedDaemonKeyStore` at Plan-022 Tier 5); R2-T3 update-notify poller polls passively + does not auto-apply; R2-T4 banner-content extension projects all R2-owned fields from `effectiveSettings()`; R2-T5 I-007-12 self-swap refusal integration (covered by R1-T3/T4 + this additional integration assertion); R2-T6 I-007-16 emission-side enforcement: the R2 banner-content extension (T-007r-2-8) tags every security-critical override row with `securityCritical: true` in the `effectiveSettings.securityOverrides` projection — the daemon-side boundary R3's CLI `--no-banner` honors. (The actual CLI `--no-banner` suppression behavior is verified in R3 by R3-T8, since R2 owns no `apps/cli/` file and the flag is scaffolded at T-007r-3-2 — testing it here would force the CLI scaffold into R2, contradicting the relocation rationale.) + +### Phase R3 — Client Delivery (Tier 4) + +**Goal:** CLI workspace package (`apps/cli/`) ships as a first-class client (per Spec-007:41); desktop main process daemon-status projector subscribes to the `SupervisionHooks` surface and bridges into the renderer per Spec-023 §Trust Stance; renderer DaemonStatusView surfaces actionable daemon-status state. The `daemon-status-projector/` directory is concern-disjoint from Plan-023 Tier 8's process-level `daemon-supervisor.ts` per the concern-split ratification + CP-007-11. + +**Precondition:** Phase R2 merged (R3's CLI banner consumption requires the R2 banner-content surface per CP-007-9 + CP-007-10). Plan-001 Phase 5 merged (`@ai-sidekicks/client-sdk` package availability per CP-007-14). Plan-023 Phase 2 bootstrap merged (preload bridge skeleton + bridge registry mount-point per CP-007-12). + + +```yaml +preconditions: + - { type: audit_status, status: complete, evidence_pr: 124, baseline_tag: "plan-readiness-audit-tier-4" } + - { type: phase_merged, phase: R2 } + - { type: external_plan_phase_merged, plan: 001, phase: 5 } + - { type: external_plan_phase_merged, plan: 023, phase: 2 } +``` + +- `apps/cli/` (CREATE workspace package) — `package.json` (`name: "@ai-sidekicks/cli"`, `bin: { "ai-sidekicks": "./dist/main.js" }`, `type: "module"`, deps: `clipanion@4.0.0-rc.4`, `@ai-sidekicks/client-sdk@workspace:*`, `@ai-sidekicks/contracts@workspace:*`); `tsconfig.json` extending `tsconfig.base.json`; `tsup.config.ts` producing a single-file ESM bundle (`format: ["esm"]`, `bundle: true`, `noExternal: ["clipanion", "@ai-sidekicks/client-sdk", "@ai-sidekicks/contracts"]`). +- `apps/cli/src/main.ts` (CREATE) — entry point; instantiates clipanion CLI; surfaces the daemon's banner from `settings.effectiveRead` (R1-owned) for commands that run against an already-running daemon, honoring `--no-banner` for non-security-critical rows only (I-007-16). The banner read MUST tolerate an unavailable daemon: `daemon start` (cold-boot) surfaces the freshly-spawned daemon's own first-run banner AFTER `DaemonHelloAck` rather than reading over IPC first, and any command issued while the daemon is unreachable degrades to a local "daemon unavailable" notice instead of failing on the banner read. +- `apps/cli/src/exit-codes.ts` (CREATE) — closed deterministic `JsonRpcErrorCode → PosixExitCode` mapping per I-007-14 + Spec-007:32. +- `apps/cli/src/commands/daemon-start.ts` + `daemon-stop.ts` + `daemon-restart.ts` + `daemon-status.ts` + `settings.ts` (CREATE × 5; kebab filenames implementing the nested clipanion paths `daemon start` / `daemon stop` / `daemon restart` / `daemon status` / `settings effective-read` per I-007-15) — `daemon stop` / `daemon restart` / `daemon status` / `settings effective-read` invoke the R1 IPC handlers via the `@ai-sidekicks/client-sdk` typed daemon client; `daemon start` instead SPAWNS the daemon process (there is no `daemon.start` IPC method — a stopped daemon hosts no JSON-RPC endpoint). Per I-007-15, `daemon start` awaits the spawned daemon's `DaemonHelloAck` and `daemon restart` awaits the re-exec'd daemon's `DaemonHelloAck` BEFORE reporting success; `daemon stop` awaits clean drain + disconnect (NOT a `DaemonHelloAck` — I-007-15 covers start + restart only). +- `apps/desktop/src/main/daemon-status-projector/index.ts` (CREATE) — TRANSPORT-level supervision consumer; subscribes to the `SupervisionHooks` surface (`onConnect` / `onDisconnect` / `onError`) from `local-ipc-gateway.ts:114-172`; translates each event into a domain-level `DaemonStatusEvent`. Concern-disjoint from `apps/desktop/src/main/daemon-supervisor.ts` (Plan-023 Tier 8 — PROCESS-level supervision: `utilityProcess.fork` + backoff ladder `100ms/300ms/1s/3s/10s` per Plan-023:208 + version-negotiation gate) per the concern-split ratification + CP-007-11. +- `apps/desktop/src/main/daemon-status-projector/supervision-status-machine.ts` (CREATE) — XState v5 state machine modeling the supervision-status. States: `connected` / `transient_disconnect` (re-enterable; resolves on reconnect; trips to `degraded` after backoff exhaustion) / `degraded` (terminal until manual restart) / `unknown` (fail-closed default for unrecognized `SupervisionDisconnectReason` values per I-007-18). Inputs: `SupervisionDisconnectReason` canonical 5-value enum from `local-ipc-gateway.ts:145-160`. +- `apps/desktop/src/main/bridge/daemon-status.ts` (CREATE; EXTENDs Plan-023's `bridge/index.ts` registry per CP-007-12) — registers the `daemon.status` topic on the existing generic `daemon.subscribe` channel; projects the supervision-status stream through the preload bridge per Spec-023 §Trust Stance. +- `packages/contracts/src/desktop-bridge.ts` (EXTEND) — adds the `daemon.status` topic to the typed `SidekicksBridge` subscription map; the ambient `window.sidekicks` type (already hoisted to `apps/desktop/src/renderer/src/sidekicks-bridge.d.ts` at Plan-002 Phase 6 NS-29 hoist per CP-007-13) automatically picks up the type extension at typecheck time. +- `apps/desktop/src/renderer/src/daemon-status/DaemonStatusView.tsx` (CREATE; CHROME-level subtree deliberately OUTSIDE `apps/desktop/src/renderer/src/features/`) — React function component consuming the `window.sidekicks.daemon.subscribe('daemon.status', ...)` typed subscription; renders the supervision-status pill in the desktop chrome. +- `apps/desktop/src/renderer/src/daemon-status/disconnect-reason-copy.ts` (CREATE) — canonical mapping from the 5-value `SupervisionDisconnectReason` enum to user-facing copy + remediation guidance. +- `apps/desktop/src/main/index.ts` (EXTEND) — wires `daemon-status-projector` into the desktop main bootstrap; subscribes the projector to the local-ipc-gateway's `SupervisionHooks` before any renderer window opens. +- Tests: CLI command tests (start / stop / restart / status / settings — each verifies exit-code mapping per I-007-14, `DaemonHelloAck`-before-success per I-007-15, banner-non-suppression per I-007-16); desktop main projector tests; supervision-status state-machine tests (XState v5 model-test pattern + I-007-18 fail-closed verification); renderer view tests (Vitest + jsdom). + +**PR Boundary Mapping (R3-PR-a / R3-PR-b / R3-PR-c):** Phase R3 ships as 3 sequential PRs (per the 2026-05-28 audit advisor's quality-over-speed split — 15 tasks across 3 distinct review surfaces would form a mega-PR that exceeds staff-level review depth; matches the Tier 1 Partial PR Sequence pattern of PRs #16/#17/#19). Task → PR boundary mapping: + +- **R3-PR-a — CLI Delivery** (T-007r-3-1..6 + T-007r-3-6a + R3-T1..R3-T8 tests). Surface: `apps/cli/` workspace package — scaffold + 6 commands (daemon start/stop/restart/status + settings effective-read + self-update) + exit-code mapping + CLI entry point + banner consumption. Independent of desktop main/renderer. +- **R3-PR-b — Desktop Main + Supervision** (T-007r-3-7..10 + T-007r-3-13..14 + R3-T9..R3-T11 + R3-T14 tests). Surface: `apps/desktop/src/main/daemon-status-projector/` (transport-level supervision consumer + XState v5 state machine) + `apps/desktop/src/main/bridge/daemon-status.ts` (IPC bridge handler + `DaemonHelloAck` mutating-op gate) + `packages/contracts/src/desktop-bridge.ts` (typed bridge contract EXTEND) + `apps/desktop/src/main/index.ts` (bootstrap wiring). +- **R3-PR-c — Renderer Chrome** (T-007r-3-11..12 + R3-T12..R3-T13 tests). Surface: `apps/desktop/src/renderer/src/daemon-status/` (chrome-level subtree deliberately outside `features/` per §Notes Renderer Chrome-vs-Features Placement) — DaemonStatusView + disconnect-reason-copy + App.tsx mount. + +PR-b BLOCKS PR-c (PR-c's renderer consumes the bridge contract ratified at PR-b's `desktop-bridge.ts` EXTEND); PR-a stands alone (CLI is independent of desktop main/renderer). Each PR ships its own slice of T-007r-3-15 — the rollup test-suite task is sharded across the 3 PRs. + +#### Tasks + +- **T-007r-3-1** (Files: `apps/cli/package.json` + `apps/cli/tsconfig.json` + `apps/cli/tsup.config.ts`; Verifies invariant: I-007-13; Spec coverage: Spec-007:41) — Author the `apps/cli/` workspace package scaffold. `package.json` declares `name: "@ai-sidekicks/cli"`, `bin: { "ai-sidekicks": "./dist/main.js" }`, `type: "module"`. Dependencies pinned: `clipanion@4.0.0-rc.4` (exact-version pin; v4 is the current major-version line but has not yet shipped a stable release as of 2026-05-28 — pin justified by the [Yarn package manager precedent](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-cli/package.json) which ships `clipanion@4` in production across the JS ecosystem's largest CLI surface; the v4 type-safety improvements over v3 are load-bearing for V1's typed CLI commands; [BL-134](../backlog.md#bl-134-clipanion-stable-v4-upgrade--lockfile-bump-for-plan-007-r3-pr-a-cli) tracks the stable-v4 release + lockfile bump when upstream stables), `@ai-sidekicks/client-sdk@workspace:*`, `@ai-sidekicks/contracts@workspace:*`. `tsup.config.ts` produces single-file ESM bundle (`format: ["esm"]`, `bundle: true`, `noExternal: ["clipanion"]`, `external: ["node:*"]`). Import-isolation invariant I-007-13 enforced via ESLint config (`apps/cli/.eslintrc.json` extends repo root + adds `import/no-restricted-paths` blocking imports from packages outside the allow-list). +- **T-007r-3-2** (Files: `apps/cli/src/main.ts`; Verifies invariant: I-007-15 + I-007-16; Spec coverage: CP-007-10) — Implement CLI entry point. Instantiates clipanion's `Cli` builder; for commands that run against an already-running daemon, surfaces the banner from `settings.effectiveRead` (R1-owned) before the command body, honoring `--no-banner` ONLY for non-security-critical banner rows (I-007-16 enforcement — query the R2 `effectiveSettings.securityOverrides` array's `securityCritical` flag at render time). The banner read is daemon-availability-tolerant: it is NOT issued before the daemon-establishing `daemon start` (which has no IPC endpoint yet — that command surfaces the freshly-spawned daemon's own banner after `DaemonHelloAck`), and a command run while the daemon is unreachable degrades to a typed "daemon unavailable" notice rather than failing on the banner read. The banner-string consumption is the R2 → R3 dependency per CP-007-10. +- **T-007r-3-3** (Files: `apps/cli/src/exit-codes.ts`; Verifies invariant: I-007-14; Spec coverage: Spec-007:32) — Implement closed deterministic `JsonRpcErrorCode → PosixExitCode` mapping. Inline shape: `const JSON_RPC_TO_EXIT_CODE: Record = { [-32700]: 65, [-32600]: 64, [-32601]: 64, [-32602]: 64, [-32603]: 70, default: 1 }` (sysexits-style mapping per [POSIX sysexits.h](https://man.openbsd.org/sysexits.3)). Per I-007-14, the mapping is total (no untyped fallthrough); unmapped codes throw a typed `UnmappedExitCodeError` at CLI exit-translation time to surface drift. +- **T-007r-3-4** (Files: `apps/cli/src/commands/daemon-start.ts` + `daemon-stop.ts` + `daemon-restart.ts`; Verifies invariant: I-007-15 + I-007-12) — Implement the clipanion daemon-lifecycle commands (nested paths `daemon start` / `daemon stop` / `daemon restart` per I-007-15). **`daemon start` is a process-spawn, NOT an IPC call** (a stopped daemon hosts no JSON-RPC endpoint): the command resolves the daemon executable at RUNTIME (the V1 install-layout co-located daemon artifact, overridable via `--daemon-path`; a runtime spawn target, NOT a static import, so the I-007-13 import-isolation allow-list is preserved), spawns it detached (`child_process.spawn` with `detached: true` and decoupled stdio so the daemon outlives the CLI invocation), then connects to the daemon's local-IPC endpoint (the `localIpcPath` projected by `settings.effectiveRead`) and awaits `DaemonHelloAck` within the I-007-15 deadline (default 10s, `--timeout `) before reporting success. It is idempotent — if a live daemon already answers the hello on that endpoint, it reports already-running and exits 0 WITHOUT spawning a second process. This CLI one-shot spawn is concern-disjoint from Plan-023 Tier 8's desktop `daemon-supervisor.ts` (`utilityProcess.fork` + backoff ladder): the CLI does not supervise; it spawns-and-acks. `daemon stop` and `daemon restart` ARE IPC calls on the already-running daemon (`daemon.stop` / `daemon.restart`, R1-owned) and accept `--idle-drain-deadline ` (default 5000 per T-007r-1-2 schema default); per I-007-15 `daemon restart` awaits the re-exec'd daemon's `DaemonHelloAck` before reporting success, while `daemon stop` awaits clean drain + disconnect (NOT a hello). On `daemon.lifecycle_conflict` error envelope (I-007-12 self-swap refusal — JSON-RPC `-32603 InternalError` + `data.type` discriminator), the command exits with the typed exit-code mapped from JSON-RPC `-32603` per T-007r-3-3 (POSIX `EX_SOFTWARE = 70`). Discrimination is `error.data.type === "daemon.lifecycle_conflict"`, NOT the numeric code alone (other `-32603` codes — e.g. unhandled exceptions — surface as generic software failure). +- **T-007r-3-5** (Files: `apps/cli/src/commands/daemon-status.ts`; Verifies invariant: I-007-14) — Implement `ai-sidekicks daemon status` clipanion command. Calls `daemon.status.read` (R1-owned) via the typed client SDK; renders the response as either human-readable text (default) or JSON (`--json` flag) per Spec-007:32. Exit code maps the daemon `processState` to a POSIX exit code: `running` → 0, `starting`/`stopping` → 0, `degraded` → 1. +- **T-007r-3-6** (Files: `apps/cli/src/commands/settings.ts`; Verifies invariant: I-007-3 + I-007-14) — Implement `ai-sidekicks settings effective-read` clipanion command. Calls `settings.effectiveRead` (R1-owned); renders the response (text or JSON). Per I-007-3, the command surface does NOT extend the response shape to include any secret-bearing fields. +- **T-007r-3-6a** (Files: `apps/cli/src/commands/self-update.ts` + `apps/cli/src/update/manifest-verifier.ts` + `sigstore-verifier.ts` + `anti-rollback.ts` + `atomic-swap.ts` (all CREATE); Verifies invariant: I-007-16; Spec coverage: Spec-027 row 7b + CP-007-9 + CP-007-10) — Implement the Spec-027 row 7b dual-verification self-update command (RELOCATED from R2's T-007r-2-6 per the 2026-05-28 ratification reversal; see that slot for rationale). Verifies (i) checksum against the update channel's manifest; (ii) signature against the daemon's bundled trust root (`@sigstore/verify` v3.x bundle). Refuses to apply the update if EITHER verification fails; enforces anti-rollback/version-freeze before atomic swap. Ships in R3-PR-a alongside the scaffold (T-007r-3-1) and the other CLI commands, so its `apps/cli/` imports resolve at compile time. R2-originated inputs are consumed at RUNTIME: the daemon-bundled update trust root and the CP-007-10 banner string (via `settings.effectiveRead`) — R2 merges before R3, so both are present. Honors I-007-16 (`--no-banner` does NOT suppress the security-critical update banner). Depends: T-007r-3-1 scaffold; R1 `settings.effectiveRead`; R2 banner-content surface (runtime). +- **T-007r-3-7** (Files: `apps/desktop/src/main/daemon-status-projector/index.ts`; Verifies invariant: I-007-17 + I-007-18; Spec coverage: CP-007-11) — Implement TRANSPORT-level supervision consumer. Subscribes to `SupervisionHooks` from `local-ipc-gateway.ts:114-172`; translates each `SupervisionDisconnectReason` value (canonical 5: `"client_close" | "server_close" | "transport_error" | "oversized_body" | "malformed_frame"` per `local-ipc-gateway.ts:145-160`) into a domain-level `DaemonStatusEvent`. Per I-007-17, exactly ONE supervision event fires per transport boundary (no duplicates). Concern-disjoint from Plan-023 Tier 8's process-level `daemon-supervisor.ts` per CP-007-11; the two subscribe to each other for the missing half of state per CP-007-11. +- **T-007r-3-8** (Files: `apps/desktop/src/main/daemon-status-projector/supervision-status-machine.ts`; Verifies invariant: I-007-18) — Implement supervision-status XState v5 machine. States: `connected` / `transient_disconnect` / `degraded` / `unknown`. Transitions: `onDisconnect(reason)` where `reason ∈ canonical 5` → `transient_disconnect`; backoff exhaustion → `degraded`; reconnect → `connected`. Per I-007-18, an unrecognized `SupervisionDisconnectReason` value (forwards-compat with future Plan-007 substrate amendments) fails CLOSED to `degraded` (NOT silently to `connected`); the unknown-reason branch is explicit and surfaces a logged warning. +- **T-007r-3-9** (Files: `apps/desktop/src/main/bridge/daemon-status.ts`; Verifies invariant: I-007-19; Spec coverage: CP-007-12) — EXTEND Plan-023 Tier 8's bridge registry (`bridge/index.ts`) by adding the `daemon.status` topic to the existing generic `daemon.subscribe` channel. Per Spec-023 §Trust Stance, the renderer NEVER speaks directly to the daemon — all daemon-status traffic flows through the preload bridge. Per I-007-19, renderer-issued `daemon.restart` (a mutating operation) is gated by the `DaemonHelloAck` mutating-op gate (NOT an admin-token) at the bridge layer + substrate (`local-ipc-gateway.ts:1096+`), never in the renderer. +- **T-007r-3-10** (Files: `packages/contracts/src/desktop-bridge.ts` EXTEND; Verifies invariant: I-007-7; Spec coverage: CP-007-13) — Extend the `SidekicksBridge` interface with the `daemon.status` topic in the typed subscription map. The ambient `window.sidekicks` type at `apps/desktop/src/renderer/src/sidekicks-bridge.d.ts` (hoisted to its own file at Plan-002 Phase 6 NS-29 hoist) automatically picks up the extension via TypeScript declaration merging. +- **T-007r-3-11** (Files: `apps/desktop/src/renderer/src/daemon-status/DaemonStatusView.tsx`; Verifies invariant: I-007-7; Spec coverage: CP-007-11) — React function component (per project's React-views-stay-functional norm). Consumes `window.sidekicks.daemon.subscribe('daemon.status', ...)` typed subscription. Renders the supervision-status pill in the desktop chrome. The chrome-level placement (`daemon-status/` outside `features/`) is the CONVENTION documented in §Notes Renderer Chrome-vs-Features Placement (NOT a load-bearing invariant per advisor demotion). +- **T-007r-3-12** (Files: `apps/desktop/src/renderer/src/daemon-status/disconnect-reason-copy.ts`; Verifies invariant: I-007-18) — Canonical mapping of the 5-value `SupervisionDisconnectReason` enum to user-facing copy + remediation guidance. The mapping table is total: every enum value has a copy entry. Per I-007-18 (fail-closed on unknown), an unrecognized reason falls to a generic `"daemon disconnected"` copy + `"check daemon logs"` remediation. +- **T-007r-3-13** (Files: `apps/desktop/src/main/index.ts` EXTEND; Verifies invariant: I-007-17) — Wire `daemon-status-projector` into desktop main bootstrap. Subscribes the projector to the `local-ipc-gateway` `SupervisionHooks` BEFORE any renderer window opens. Per I-007-17, single-emit-per-transport-boundary is enforced at the gateway level (T-007p-2-1); the projector trusts that contract. +- **T-007r-3-14** (Files: `apps/desktop/src/main/bridge/daemon-status.ts` EXTEND for the mutating-op gate; Verifies invariant: I-007-19) — Implement the renderer-issued `daemon.restart` mutating-op gate per I-007-19. The bridge handler refuses every renderer-originated mutating call (`daemon.stop`, `daemon.restart` — there is no renderer-issuable `daemon.start`: cold-boot has no IPC/bridge path, and the desktop daemon is brought up by Plan-023 Tier 8's `daemon-supervisor.ts`, not a renderer call) unless the daemon-status state machine is `connected` — i.e. a valid `DaemonHelloAck` session exists (Spec-007 §Mutating-Op Gate); the authoritative server-side enforcement is the substrate mutating-op gate at `local-ipc-gateway.ts:1096+`, with the bridge handler as the renderer-facing checkpoint. This is deliberately **NOT** an admin-token check: `./data/admin-token` is the self-host **relay** admin credential (surfaced in the banner per CP-007-9 row d / Spec-027 row 10), not a local-IPC authority — gating a local renderer call on the relay-admin secret would conflate the relay-admin trust boundary with the local-IPC handshake (the renderer's authority to mutate comes from completing the local `DaemonHelloAck`, per Spec-023 §Trust Stance). Per I-007-19, a renderer-issued mutating call lacking a valid `DaemonHelloAck` is refused with a typed bridge error (NOT silently dropped); the refusal surfaces to the renderer's typed subscription error channel. +- **T-007r-3-15** (Files: `apps/cli/src/commands/__tests__/*.test.ts` + `apps/desktop/src/main/daemon-status-projector/__tests__/*.test.ts` + `apps/desktop/src/renderer/src/daemon-status/__tests__/*.test.tsx`) — Author the R3 test suite, sharded across the 3 PR boundaries (R3-PR-a / R3-PR-b / R3-PR-c) per the §PR Boundary Mapping above. Each shard ships in its own PR: + - **T-007r-3-15 (slice a) — R3-PR-a tests:** R3-T1 `daemon start` spawns the daemon process and waits for the spawned daemon's `DaemonHelloAck` before reporting success (I-007-15; process-spawn, not an IPC call); R3-T2 `daemon stop` honors `--idle-drain-deadline` (default 5000) and awaits clean drain + disconnect; R3-T3 `daemon restart` maps the `lifecycle_conflict` error envelope to its POSIX exit code when a non-caller client remains after the drain deadline (I-007-12 + I-007-14); R3-T4 `daemon status --json` produces canonical JSON output; R3-T5 `settings effective-read` excludes secret-bearing fields (I-007-3); R3-T6 import-isolation ESLint rule fails CI if a CLI module imports outside the allow-list (I-007-13); R3-T7 exit-code mapping is total (I-007-14); R3-T8 `--no-banner` does NOT suppress security-critical rows (I-007-16); R3-T8a `self-update` refuses to apply when EITHER the manifest checksum OR the trust-root signature fails, and enforces anti-rollback before atomic swap (Spec-027 row 7b; T-007r-3-6a). + - **T-007r-3-15 (slice b) — R3-PR-b tests:** R3-T9 `SupervisionDisconnectReason` 5-value enum exhaustively mapped (I-007-18); R3-T10 unknown reason fails-closed to `degraded` (I-007-18); R3-T11 single-emit-per-transport-boundary verified (I-007-17); R3-T14 renderer-issued `daemon.restart` is gated by a valid `DaemonHelloAck` (daemon-status `connected`) at the bridge + substrate mutating-op gate (I-007-19). + - **T-007r-3-15 (slice c) — R3-PR-c tests:** R3-T12 `DaemonStatusView` renders all 4 state-machine states; R3-T13 disconnect-reason copy table is total (I-007-18). + ## Parallelization Notes - IPC contract work and shell supervision scaffolding can proceed in parallel once handshake semantics are fixed. @@ -411,10 +695,10 @@ Spec-027 row coverage at Tier 1 (rows 4 + 10 only — bind-path-relevant rows th - **W-007p-2-T1** (Spec-007 line 47) Handshake + version-negotiation compatibility. `DaemonHello` / `DaemonHelloAck` exchange; mutating-operation gate when `compatible === false`. - **W-007p-2-T2** Transport: Unix domain socket round-trip. - **W-007p-2-T3** Transport: Windows named pipe round-trip. -- **W-007p-2-T4** (per F-007p-2-09 Tier 1 conservative gate) Transport: gated loopback fallback. At Tier 1, attempting loopback-fallback fails with the canonical `transport.unavailable` envelope per [error-contracts.md §Plan-007 Tier 1 Domain Identifiers](../architecture/contracts/error-contracts.md#plan-007-tier-1-domain-identifiers) (`-32603 InternalError` + `data.type: "transport.unavailable"` + `data.fields: { requested, reason }`). The mapping itself ships post-BL-103 (closed 2026-05-01) — at Phase 2 this test sits as a Tier-4 deferral of the _bind-path surface_, not the envelope. When the gateway-time gate lands at Tier 4, the assertion projects through `mapJsonRpcError` exactly like the `unknown_setting` envelope test in `secure-defaults.test.ts`. -- **W-007p-2-T5** (per F-007p-2-05) 1MB max-message-size enforcement. Body > 1MB → connection close + `-32600` error frame; subsequent reconnect succeeds. +- **W-007p-2-T4** (Tier 1 conservative gate) Transport: gated loopback fallback. At Tier 1, attempting loopback-fallback fails with the canonical `transport.unavailable` envelope per [error-contracts.md §Plan-007 Tier 1 Domain Identifiers](../architecture/contracts/error-contracts.md#plan-007-tier-1-domain-identifiers) (`-32603 InternalError` + `data.type: "transport.unavailable"` + `data.fields: { requested, reason }`). The mapping itself ships post-BL-103 (closed 2026-05-01) — at Phase 2 this test sits as a Tier-4 deferral of the _bind-path surface_, not the envelope. When the gateway-time gate lands at Tier 4, the assertion projects through `mapJsonRpcError` exactly like the `unknown_setting` envelope test in `secure-defaults.test.ts`. +- **W-007p-2-T5** 1MB max-message-size enforcement. Body > 1MB → connection close + `-32600` error frame; subsequent reconnect succeeds. - **W-007p-2-T6** Content-Length framing parser correctness. Single message; multi-message buffer; partial read; malformed framing → connection close. -- **W-007p-2-T7** (per F-007p-2-04 + I-007-9) Method-not-found namespace-isolation. Invoking an unregistered method (e.g. `not.registered`) returns JSON-RPC `-32601` per F-007p-2-02 mapping; never falls through to a generic dispatch path. +- **W-007p-2-T7** (Verifies I-007-9) Method-not-found namespace-isolation. Invoking an unregistered method (e.g. `not.registered`) returns JSON-RPC `-32601` per the substrate's numeric-error-space mapping; never falls through to a generic dispatch path. - **W-007p-2-T8** (per Spec-007:67-68) Mutating-op gate when version-mismatch. Read methods pass through; mutating methods refused per the registry's `mutating: boolean` flag. - **W-007p-2-T9** (Verifies I-007-7) Schema-validates-before-dispatch. Malformed payload returns JSON-RPC `-32602`; handler is NEVER invoked. - **W-007p-2-T10** (Verifies I-007-8) Handler-thrown error mapping. Thrown unhandled exception → JSON-RPC `-32603` with sanitized message; no stack/secret leak. @@ -432,11 +716,11 @@ Spec-027 row coverage at Tier 1 (rows 4 + 10 only — bind-path-relevant rows th - **I-007-3-T8** (Verifies AC-N2 + I-007-8) `session.read` round-trip. Known sessionId returns `SessionRead`-shape projection (happy path); unknown sessionId throws `SessionNotFoundError` (canonical class at `packages/runtime-daemon/src/ipc/session-errors.ts`) which `mapJsonRpcError` discriminates to `-32602 InvalidParams` + `data.type: "session.not_found"` per [error-contracts.md §JSON-RPC Wire Mapping](../architecture/contracts/error-contracts.md#json-rpc-wire-mapping) (`session.not_found` HTTP-404 equivalent). Closes [BL-117](../backlog.md). - **I-007-3-T9** (Verifies AC-N3 + Spec-001 AC4) `session.join` round-trip. Happy path: handler invocation drives a `membership.created` event (the canonical V1 join-admission variant per `SessionEventSchema` at `packages/contracts/src/event.ts`) to a same-session subscribe-side observer through the streaming-primitive plumbing (mock `joinSession` invokes the test-scope subscription primitive's `next(fixture)` mirroring `I-007-3-T3`'s emit pattern). AC4 replay shape: second-client `SessionJoin` returns the same sessionId + the existing membership set at the handler boundary using mocked deps. Closes [BL-117](../backlog.md). -**Coverage scope note (retroactive, BL-113 audit; amended BL-114; BL-117 direct tests landed 2026-05-19 via PR #79).** Spec-007 §Acceptance Criteria per-method ACs AC-N1..N4 (`session.create` / `session.read` / `session.join` / `session.subscribe`) are now explicit at [Spec-007 §Acceptance Criteria](../specs/007-local-ipc-and-daemon-control.md#acceptance-criteria). Honest coverage map (verified by direct test-file inspection of `packages/runtime-daemon/src/ipc/handlers/__tests__/session-handlers.test.ts` + `packages/client-sdk/src/transport/__tests__/jsonRpcClient.test.ts`): **AC-N1 (`session.create`) → I-007-3-T1** (round-trip identity) **+ I-007-3-T2** (malformed-payload rejection + I-007-7 enforcement) — DIRECT. **AC-N4 (`session.subscribe`) → I-007-3-T3** (subscribe round-trip + cancel idempotency) **+ I-007-3-T6** (daemon-side `setImmediate`-buffered response-before-notify wire-ordering, I-007-10) **+ I-007-3-T7** (SDK-side synchronous `#subscriptions` registration in `#handleResponse`) — DIRECT. **AC-N2 (`session.read`) → I-007-3-T8** (happy-path round-trip + unknown-id `SessionNotFoundError` → `-32602 InvalidParams` + `data.type: "session.not_found"` wire shape, implemented via the `SessionNotFoundError` discriminator branch in `mapJsonRpcError` per [error-contracts.md §JSON-RPC Wire Mapping](../architecture/contracts/error-contracts.md#json-rpc-wire-mapping)) — DIRECT. **AC-N3 (`session.join`) → I-007-3-T9** (`membership.created` event emission to a same-session subscribe-side observer via the test-scope subscription primitive — canonical V1 join-admission variant per `SessionEventSchema` at `packages/contracts/src/event.ts`; second-client `SessionJoin` AC4 replay shape at the handler boundary using mocked deps per [Spec-001 AC4](../specs/001-shared-session-core.md#acceptance-criteria)) — DIRECT. Structural invariants (I-007-6 + I-007-7) additionally inherit via the shared `router.register` substrate proven by `I-007-3-T5` against `session.create`'s identical binding shape — a secondary corroboration of the per-method bindings now backed by their own direct dispatch tests; full session-service integration (SQLite-backed event log + control-plane replay path) ships with Plan-001 Phase 5. +**Coverage scope note (retroactive, BL-113 audit; amended BL-114; BL-117 direct tests landed 2026-05-19).** Spec-007 §Acceptance Criteria per-method ACs AC-N1..N4 (`session.create` / `session.read` / `session.join` / `session.subscribe`) are now explicit at [Spec-007 §Acceptance Criteria](../specs/007-local-ipc-and-daemon-control.md#acceptance-criteria). Honest coverage map (verified by direct test-file inspection of `packages/runtime-daemon/src/ipc/handlers/__tests__/session-handlers.test.ts` + `packages/client-sdk/src/transport/__tests__/jsonRpcClient.test.ts`): **AC-N1 (`session.create`) → I-007-3-T1** (round-trip identity) **+ I-007-3-T2** (malformed-payload rejection + I-007-7 enforcement) — DIRECT. **AC-N4 (`session.subscribe`) → I-007-3-T3** (subscribe round-trip + cancel idempotency) **+ I-007-3-T6** (daemon-side `setImmediate`-buffered response-before-notify wire-ordering, I-007-10) **+ I-007-3-T7** (SDK-side synchronous `#subscriptions` registration in `#handleResponse`) — DIRECT. **AC-N2 (`session.read`) → I-007-3-T8** (happy-path round-trip + unknown-id `SessionNotFoundError` → `-32602 InvalidParams` + `data.type: "session.not_found"` wire shape, implemented via the `SessionNotFoundError` discriminator branch in `mapJsonRpcError` per [error-contracts.md §JSON-RPC Wire Mapping](../architecture/contracts/error-contracts.md#json-rpc-wire-mapping)) — DIRECT. **AC-N3 (`session.join`) → I-007-3-T9** (`membership.created` event emission to a same-session subscribe-side observer via the test-scope subscription primitive — canonical V1 join-admission variant per `SessionEventSchema` at `packages/contracts/src/event.ts`; second-client `SessionJoin` AC4 replay shape at the handler boundary using mocked deps per [Spec-001 AC4](../specs/001-shared-session-core.md#acceptance-criteria)) — DIRECT. Structural invariants (I-007-6 + I-007-7) additionally inherit via the shared `router.register` substrate proven by `I-007-3-T5` against `session.create`'s identical binding shape — a secondary corroboration of the per-method bindings now backed by their own direct dispatch tests; full session-service integration (SQLite-backed event log + control-plane replay path) ships with Plan-001 Phase 5. ### [Tier 4] Plan-007-Remainder Tests -Validation surface widens at Tier 4 alongside the additional bind paths (HTTP, non-loopback, TLS) and the four other JSON-RPC namespaces. Per §Invariants I-007-5, the row coverage extends here. +Validation surface widens at Tier 4 alongside the additional bind paths (HTTP, non-loopback, TLS) and the namespace-handler-registration scope. Per §Execution Windows ownership map, Phase R1 registers ALL Tier 4 JSON-RPC namespaces into the substrate registry: `daemon.*` + `settings.*` ship their handlers here, while `run.*` (Plan-004), `repo.*` (Plan-009), `artifact.*` (Plan-014), `driver.*` (Plan-005 Phase 4), and `event.*` (Plan-006 Phase 4) attach their handlers from their owning plans per the NS-26 precedent. Per §Invariants I-007-5 and I-007-6, the row coverage extends here and per-namespace duplicate-registration rejection is verified at the registry-membership integration boundary (R1-T6). - Spec-027 row coverage at Tier 4 (rows 2, 3, 7a, 7b, 8 — the bind-path-widening rows): - Row 2: daemon refuses to start when ` + `; `--insecure` override boots with banner + `security.default.override=insecure_bind` event emitted exactly once. @@ -450,9 +734,20 @@ Validation surface widens at Tier 4 alongside the additional bind paths (HTTP, n ## Rollout Order -1. Land shared daemon contracts and SDK surface -2. Ship the first CLI against the same local daemon contract -3. Enable desktop-shell supervision and daemon status reads +**Tier 1 (Plan-007-Partial — shipped):** Phase 1 (SecureDefaults Bootstrap) → Phase 2 (Wire Substrate) → Phase 3 (`session.*` Handlers + SDK Zod Layer). + +**Tier 4 (Plan-007-Remainder):** + +1. **Phase R1 — Namespace Handlers** — Ship daemon-side `daemon.*` + `settings.*` JSON-RPC namespace handlers; complete namespace-registration for ALL Tier 4 namespaces (Plan-004/005/006/009/014 owning plans attach their handlers from their own plan files per NS-26 precedent); ship api-payload-contracts.md Tier 4 method-name table + error-contracts.md `daemon.lifecycle_conflict` envelope registration. +2. **Phase R2 — Secure Defaults** — Ship Spec-027 rows 2/3/7a/8 daemon-side surfaces (TLS 1.3-only listener factory + first-run-keys ceremony + `DaemonKeyStore` interface + update-notify poller + banner content extension). The row-7b `apps/cli/src/commands/self-update.ts` command ships in **R3-PR-a (T-007r-3-6a)**, not R2 — relocated per the 2026-05-28 ratification reversal (see T-007r-2-6). +3. **Phase R3 — Client Delivery** (ships as **3 sequential PRs** per advisor recommendation 2026-05-28 — quality-over-speed split rationalizes 15 tasks across 3 distinct review surfaces: CLI workspace, desktop main process, renderer chrome; matches the [Tier 1 Partial PR Sequence](#tier-1-partial-pr-sequence) precedent where Phase 3 was sharded to three sequential PRs for the same reason): + - **R3-PR-a — CLI Delivery** — Stand up `apps/cli/` workspace package (`clipanion@4.0.0-rc.4` exact-RC pin + `tsup` single-file ESM bundle per the CLI workspace-scaffold contract); ship 5 CLI commands (`daemon start` / `daemon stop` / `daemon restart` / `daemon status` + `settings effective-read` — `daemon start` spawns + handshakes the daemon process per T-007r-3-4, the rest are JSON-RPC calls to a running daemon); exit-code mapping per I-007-14 + Spec-007:32; CLI entry point + banner consumption (T-007r-3-1..6). PR-scoped tests: R3-T1..R3-T8. + - **R3-PR-b — Desktop Main + Supervision** — Ship `apps/desktop/src/main/daemon-status-projector/` (transport-level supervision consumer + supervision-status XState v5 machine — concern-disjoint from [Plan-023](./023-desktop-shell-and-renderer.md) Tier 8's process-level `daemon-supervisor.ts` per the concern-split ratification + CP-007-11); ship `apps/desktop/src/main/bridge/daemon-status.ts` IPC bridge handler + `DaemonHelloAck` mutating-op gate per I-007-19; EXTEND `packages/contracts/src/desktop-bridge.ts` typed bridge contract (CP-007-13); wire `apps/desktop/src/main/index.ts` bootstrap to attach the supervision consumer before any renderer window opens (T-007r-3-7..10 + T-007r-3-13..14). PR-scoped tests: R3-T9..R3-T11 + R3-T14. + - **R3-PR-c — Renderer Chrome** — Ship `apps/desktop/src/renderer/src/daemon-status/DaemonStatusView.tsx` chrome-level React component + `disconnect-reason-copy.ts` total mapping over the 5-value `SupervisionDisconnectReason` enum (per I-007-18 fail-closed convention); mount `` in `App.tsx` always-visible bottom-bar chrome per the chrome-level placement convention; renderer subtree placed deliberately OUTSIDE `features/` per §Notes Renderer Chrome-vs-Features Placement convention (T-007r-3-11..12). PR-scoped tests: R3-T12..R3-T13. + +**Inter-phase blocking dependencies:** R1 → R2 → R3 (sequential; R3 depends on R1's `settings.effectiveRead` projection per CP-007-9 banner content consumption + R2's banner-string ship per CP-007-10). R3 additionally blocks on Plan-001 Phase 5 (`@ai-sidekicks/client-sdk` package per CP-007-14, shipped) and Plan-023 Phase 2 bootstrap (preload bridge skeleton + bridge registry mount-point per CP-007-12). + +**Within-R3 blocking dependencies:** R3-PR-a → R3-PR-b → R3-PR-c (sequential; R3-PR-b's `daemon-status-projector/` consumes the bridge-contract extension from R3-PR-b's own `desktop-bridge.ts` EXTEND; R3-PR-c's `DaemonStatusView.tsx` consumes the typed subscription channel ratified by R3-PR-b's bridge contract). PR-a stands alone (CLI is independent of desktop main/renderer surfaces); PR-c BLOCKED on PR-b's bridge-contract ship. ## Rollback Or Fallback @@ -567,7 +862,7 @@ shipped: - **PR #16** (squash-commit `49f1116` on `develop`, merged 2026-04-28): Phase 1 — SecureDefaults Bootstrap. Tasks `T-007p-1-1` (SecureDefaults.load + effectiveSettings), `T-007p-1-2` (security.default.override audit emitter), `T-007p-1-3` (bootstrap orchestrator + load-before-bind guard), `T-007p-1-4` (W-007p-1-T1..T5 invariant test suite) delivered. Acceptance criteria green: W-007p-1-T1..T5 (18 cases, 49/49 package tests pass). Plan-doc path amendment landed in same PR (T-007p-1-4 target_path corrected from `test/bootstrap/` to `src/bootstrap/__tests__/` to match repo's universal vitest include glob). BLOCKED-ON-C6 / -C7 / -C9 markers carried forward without premature shape pre-commit. Post-merge polish surfaced: ~30 OBSERVATIONs aggregated (see PR #16 Review Notes; key items: `localhost`/`bindPort`-negative coverage gaps, T-T4 regex-from-loop-var, citation-symmetry across bootstrap files). -- **PR #17** (squash-commit `3d8ef0e` on `develop`, merged 2026-04-29): Phase 2 — Wire Substrate. Tasks `T-007p-2-1` (JSON-RPC 2.0 + LSP Content-Length framing + supervision hooks), `T-007p-2-2` (numeric-error-space ↔ dotted-namespace mapping), `T-007p-2-3` (method-namespace registry with mutating flag), `T-007p-2-4` (DaemonHello/DaemonHelloAck negotiation + mutating-op gate), `T-007p-2-5` (`LocalSubscriptionProducer` streaming primitive), `T-007p-2-6` (W-007p-2-T1..T11 invariant test suite) delivered. Acceptance criteria green: 129 passed / 1 skipped (Linux-runner Windows-pipe transport) / 1 todo (Tier-4 `transport.unavailable` deferred — _bind-path gate_, not envelope shape; the canonical envelope ships in BL-103 closure) across 7 test files in 2.18s. Plan-doc path amendment landed in same PR (T-007p-2-6 target*path corrected from `test/ipc/` to `src/ipc/__tests__/` to match repo's universal vitest include glob — same defect class as PR #16). BLOCKED-ON-C6 / -C7 markers carried forward across all five touched files with explicit replacement comments at each boundary. Round-trips: `86df812` reclassified `oversized_body` from `-32603 InternalError` to `-32600 InvalidRequest` per Plan-007:268 mapping contract (T-2 ACTIONABLE); `bf74902` folded round-trip-1 — non-idempotent test `close()` helper diagnosed via standalone Node repro as the W-007p-2-T5 5000ms-hang root cause (substrate gateway verified correct, fix landed pre-Phase-C); `3795d1f` round-trip-2 — three Phase-C ACTIONABLE findings (stale comment cleanup, silent-failure-pattern fix in `malformed_header` framing test, outer `expect(caught).toBeInstanceOf(...)` guard at registry.test.ts:387); `66099bf` round-trip-3 (Codex external review on PR head `0907f59`) — three P1/P2 ACTIONABLE findings on `local-ipc-gateway.ts` addressed inline (start() now rolls back `#server`/`#started` on listen failure preventing bootstrap-retry wedge; dispatch validates `id` shape (`string | number | null`) BEFORE handler dispatch per JSON-RPC §4 + I-007-7, malformed ids emit `-32600 Invalid Request` with `id: null` per §5; parseFrame applies the 1 KB header-section cap symmetrically whether or not CRLFCRLF is present, closing a header-bypass DoS surface) + 6 new RT-codex-1 invariant tests covering each fix's contract directly (regex-anchored wedge probe, `vi.fn()` spy asserting handler non-execution, byte-count assertions on header cap). Phase D (PR-scope, full-diff review) returned 0 ACTIONABLE / 3 OBSERVATIONS deferred to a polish PR: O-D-1 Plan-007 §CP-007-3 prose drift (`router.add` vs canonical `router.register`), O-D-2 test-fixture duplication (`passthroughSchema` × 4, `rejectingSchema` × 2 — past rule-of-three on a non-blocked surface), O-D-3 review-process attribution leaked into production source comments (~16 references to "advisor's #N", "orchestrator pre-brief"; rationale prose load-bearing, attribution prefixes are not). CI required multiple pushes — the lint-staged hook runs `eslint --fix` + `tsc -b` but not `prettier --write`, so format drift escapes locally; sealed twice with `0907f59` (PR #17 substrate) and `8e55500` (RT-codex-1 test additions). The repeating-defect pattern is added to the polish-PR scope for a hook-config follow-up (lint-staged should pipe `prettier --write` on staged paths so CI's `prettier --check` becomes a redundancy gate, not a discovery surface). \_BL-113 retroactive audit notes (2026-05-18):* post-merge BL-103 sanitizeFields hardening landed at `packages/runtime-daemon/src/ipc/__tests__/jsonrpc-error-mapping.test.ts` (595 LOC `mapJsonRpcError` HEAD-revision coverage); added to Shipment Manifest Phase 2 `files:`. Tier-1 envelope-version-exempt set is the singleton `{daemon.hello}` per `ENVELOPE_PROTOCOL_VERSION_EXEMPT_METHODS` at `packages/contracts/src/jsonrpc.ts`; Spec-007:54 plural "health checks" is a Tier-4 deferral. T-007p-2-4 scope-extension created `packages/contracts/src/jsonrpc-negotiation.ts` (not declared in Phase 2 Tasks) — retroactively justified by the no-zod-in-daemon constraint (negotiation types live in contracts; daemon imports types-only). Shipment Manifest Phase 2 `spec_coverage` narrowed: "Spec-007 §Required Behavior" → wire-substrate bullets (OS-local default transport, loopback fallback gate, protocol-version negotiation); namespace handlers (`run.*` / `repo.*` / `artifact.*` / `settings.*` / `daemon.*` — Tier-4) and supervision consumer (`apps/desktop/src/main/daemon-supervision/`, `apps/cli/src/` — Tier-4) are explicitly out-of-Phase-2. Test-ID nomenclature note: Phase 2 audit uses `T-007p-2-T*` while plan-body §Test And Verification Plan uses canonical `W-007p-2-T*` — future plans pick one convention. Spec-007:75 "replay-capable subscription envelopes" is a forward-dep: the wire substrate is value-agnostic at Phase 2; replay semantics ship at Phase 3 handler level via subscribe-init-response-before-first-notify (I-007-10). `mapJsonRpcError` is cross-phase reused — single source of truth at `packages/runtime-daemon/src/ipc/jsonrpc-error-mapping.ts` consumed by Phase 3 handlers + `session-subscribe.ts` for envelope assembly. `SupervisionDisconnectReason` closed-union enumeration per F-007p-2-12: `'transport-closed' | 'transport-error' | 'gateway-shutdown' | 'protocol-version-mismatch'` — Tier 4 desktop-shell supervision consumer references this surface (§CP-007-3 reciprocal). BL-102 + BL-103 + BL-105 closures (all 2026-05-01) post-date merge — see Shipment Manifest Phase 2 `notes:` cross-reference. +- **PR #17** (squash-commit `3d8ef0e` on `develop`, merged 2026-04-29): Phase 2 — Wire Substrate. Tasks `T-007p-2-1` (JSON-RPC 2.0 + LSP Content-Length framing + supervision hooks), `T-007p-2-2` (numeric-error-space ↔ dotted-namespace mapping), `T-007p-2-3` (method-namespace registry with mutating flag), `T-007p-2-4` (DaemonHello/DaemonHelloAck negotiation + mutating-op gate), `T-007p-2-5` (`LocalSubscriptionProducer` streaming primitive), `T-007p-2-6` (W-007p-2-T1..T11 invariant test suite) delivered. Acceptance criteria green: 129 passed / 1 skipped (Linux-runner Windows-pipe transport) / 1 todo (Tier-4 `transport.unavailable` deferred — _bind-path gate_, not envelope shape; the canonical envelope ships in BL-103 closure) across 7 test files in 2.18s. Plan-doc path amendment landed in same PR (T-007p-2-6 target*path corrected from `test/ipc/` to `src/ipc/__tests__/` to match repo's universal vitest include glob — same defect class as PR #16). BLOCKED-ON-C6 / -C7 markers carried forward across all five touched files with explicit replacement comments at each boundary. Round-trips: `86df812` reclassified `oversized_body` from `-32603 InternalError` to `-32600 InvalidRequest` per Plan-007:268 mapping contract (T-2 ACTIONABLE); `bf74902` folded round-trip-1 — non-idempotent test `close()` helper diagnosed via standalone Node repro as the W-007p-2-T5 5000ms-hang root cause (substrate gateway verified correct, fix landed pre-Phase-C); `3795d1f` round-trip-2 — three Phase-C ACTIONABLE findings (stale comment cleanup, silent-failure-pattern fix in `malformed_header` framing test, outer `expect(caught).toBeInstanceOf(...)` guard at registry.test.ts:387); `66099bf` round-trip-3 (Codex external review on PR head `0907f59`) — three P1/P2 ACTIONABLE findings on `local-ipc-gateway.ts` addressed inline (start() now rolls back `#server`/`#started` on listen failure preventing bootstrap-retry wedge; dispatch validates `id` shape (`string | number | null`) BEFORE handler dispatch per JSON-RPC §4 + I-007-7, malformed ids emit `-32600 Invalid Request` with `id: null` per §5; parseFrame applies the 1 KB header-section cap symmetrically whether or not CRLFCRLF is present, closing a header-bypass DoS surface) + 6 new RT-codex-1 invariant tests covering each fix's contract directly (regex-anchored wedge probe, `vi.fn()` spy asserting handler non-execution, byte-count assertions on header cap). Phase D (PR-scope, full-diff review) returned 0 ACTIONABLE / 3 OBSERVATIONS deferred to a polish PR: O-D-1 Plan-007 §CP-007-3 prose drift (`router.add` vs canonical `router.register`), O-D-2 test-fixture duplication (`passthroughSchema` × 4, `rejectingSchema` × 2 — past rule-of-three on a non-blocked surface), O-D-3 review-process attribution leaked into production source comments (~16 references to "advisor's #N", "orchestrator pre-brief"; rationale prose load-bearing, attribution prefixes are not). CI required multiple pushes — the lint-staged hook runs `eslint --fix` + `tsc -b` but not `prettier --write`, so format drift escapes locally; sealed twice with `0907f59` (PR #17 substrate) and `8e55500` (RT-codex-1 test additions). The repeating-defect pattern is added to the polish-PR scope for a hook-config follow-up (lint-staged should pipe `prettier --write` on staged paths so CI's `prettier --check` becomes a redundancy gate, not a discovery surface). \_BL-113 retroactive audit notes (2026-05-18):* post-merge BL-103 sanitizeFields hardening landed at `packages/runtime-daemon/src/ipc/__tests__/jsonrpc-error-mapping.test.ts` (595 LOC `mapJsonRpcError` HEAD-revision coverage); added to Shipment Manifest Phase 2 `files:`. Tier-1 envelope-version-exempt set is the singleton `{daemon.hello}` per `ENVELOPE_PROTOCOL_VERSION_EXEMPT_METHODS` at `packages/contracts/src/jsonrpc.ts`; Spec-007:54 plural "health checks" is a Tier-4 deferral. T-007p-2-4 scope-extension created `packages/contracts/src/jsonrpc-negotiation.ts` (not declared in Phase 2 Tasks) — retroactively justified by the no-zod-in-daemon constraint (negotiation types live in contracts; daemon imports types-only). Shipment Manifest Phase 2 `spec_coverage` narrowed: "Spec-007 §Required Behavior" → wire-substrate bullets (OS-local default transport, loopback fallback gate, protocol-version negotiation); namespace handlers (`run.*` / `repo.*` / `artifact.*` / `settings.*` / `daemon.*` — Tier-4) and supervision consumer (`apps/desktop/src/main/daemon-supervision/`, `apps/cli/src/` — Tier-4) are explicitly out-of-Phase-2. Test-ID nomenclature note: Phase 2 audit uses `T-007p-2-T*` while plan-body §Test And Verification Plan uses canonical `W-007p-2-T*` — future plans pick one convention. Spec-007:75 "replay-capable subscription envelopes" is a forward-dep: the wire substrate is value-agnostic at Phase 2; replay semantics ship at Phase 3 handler level via subscribe-init-response-before-first-notify (I-007-10). `mapJsonRpcError` is cross-phase reused — single source of truth at `packages/runtime-daemon/src/ipc/jsonrpc-error-mapping.ts` consumed by Phase 3 handlers + `session-subscribe.ts` for envelope assembly. `SupervisionDisconnectReason` closed-union enumeration per F-007p-2-12 — Tier 4 desktop-shell supervision consumer references this surface (§CP-007-3 reciprocal). _2026-05-28 Tier 4 audit correction (F-007r-3-08):_ the canonical shape at `packages/runtime-daemon/src/ipc/local-ipc-gateway.ts:150-155` is the 5-value underscore-form `"client_close" | "server_close" | "transport_error" | "oversized_body" | "malformed_frame"` (NOT the dash-form 4-value enumeration cited at the time of PR #17 review). The Phase R3 supervision consumer (T-007r-3-7..3-13) MUST treat the 5-value underscore form as canonical; `invalid_protocol_version` is a JSON-RPC error envelope at `local-ipc-gateway.ts:1140` (NOT a disconnect reason) — it surfaces via the typed-error mapper, not the supervision hook surface. BL-102 + BL-103 + BL-105 closures (all 2026-05-01) post-date merge — see Shipment Manifest Phase 2 `notes:` cross-reference. - **PR #19** (squash-commit `0e5599d` on `develop`, merged 2026-04-30): Phase 3 — Handlers + SDK. Tasks `T-007p-3-1` (four `session.*` JSON-RPC handlers — `session-create.ts` / `session-read.ts` / `session-join.ts` / `session-subscribe.ts` + `handlers/index.ts` barrel binding into the T-007p-2-3 registry per §Cross-Plan Obligations CP-007-1), `T-007p-3-2` (SDK transport — `packages/client-sdk/src/transport/jsonRpcClient.ts` + `transport/types.ts` per CP-007-4, MCP TypeScript SDK pattern: synchronous-resolution test transport + Zod-validated `call` and `subscribe`), `T-007p-3-4` (W-007p-3 invariant test suite — `session-handlers.test.ts` (18 tests) + `jsonRpcClient.test.ts` (13 tests)) delivered. T-007p-3-3 verification deferred — the `sessionClient.ts` import-surface check executes when Plan-001 Phase 5 lands its consumer (the transport surface is shipped here; the verification is a one-shot integration check at consume time, not a Phase 3 deliverable). Acceptance criteria green: 158 runtime-daemon tests pass (1 skipped Linux-runner Windows-pipe transport, 1 todo Tier-4 `transport.unavailable` _bind-path gate_ — envelope itself ships in BL-103 closure) + 13 client-sdk tests pass + all 7 packages typecheck clean (0e5599d HEAD). I-007-3-T1..T5 invariants enforced (round-trip `session.create` per T1, malformed-payload rejection per T2 + I-007-7, `session.subscribe` streaming-primitive integration per T3, server-corruption `JsonRpcSchemaError(phase: "result")` per T4, params-phase fail-fast per T5). Phase D (PR-scope, full-diff review) ran 7 rounds of Codex external review against successive HEADs, each landing F-fixes that closed contract-level gaps surfaced on the SDK/handler boundary: `9f47116` ordered the `session.subscribe` response BEFORE replay notifies (sequencing fix discovered pre-Phase-D); F1 (`replay-flush exception guard`) wrapped `setImmediate(() => sub.next(event))` in try/catch so handler crashes during replay flush complete the subscription with an error envelope rather than silently leaking; F2 (`subscribeInitResultSchema`) tightened the SDK's init-result schema to validate `subscriptionId` against `SubscriptionId` per Plan-007 contract + added `#handleResponse` defensive check; F3 (`cancel idempotency under in-flight RPC`) added `state.cancelInFlight` field synchronously registered before first await — concurrent `cancel()` callers share one wire frame and resolve to the same outcome (clean teardown OR local error if daemon nacks); F4 (`thenable detection`) replaced `instanceof Promise` with duck-typed `then` check supporting cross-realm Promises and non-native thenables; F5 (`LocalSubscriptionProducer.onCancel lifecycle hook`) added a contract-level addition to `LocalSubscriptionProducer` — handlers fire across all three teardown paths (`cancel()` / `cleanupTransport` / `cancelSubscription`) with AbortSignal-style semantics (registration-after-cancel fires synchronously, registration-after-complete is silently dropped, multi-handler firing in registration order, per-handler error isolation, two-layer try/catch in bulk cleanup, remove-from-maps-then-fire ordering so re-entrant handlers observe post-cancel state) — and wires `session-subscribe.ts` to register the upstream `unsubscribe` via `sub.onCancel(unsubscribe)`, closing the upstream-watcher leak that would have held the projector listener for the connection's lifetime; F6 (`Promise.resolve absorbs thenable`) routed the duck-typed thenable rejection handler through `Promise.resolve(...).catch(...)` so the absorption logic the existing comment cited matches the implementation — direct `.catch` access broke for valid `PromiseLike` values omitting `.catch` (TC39 spec only mandates `.then`), surfacing a synthetic `TypeError` while the actual wire write may have succeeded; same commit widened `ClientTransport.send` return type from `void | Promise` to `void | PromiseLike` so the type contract aligns with the runtime contract (backward compatible — Promise extends PromiseLike) + added a literal-thenable regression test (`ThenableSendRejectingTransport` whose `send` returns a thenable WITHOUT `.catch`) locking the fix; the re-entrant unsubscribe precondition was named on `SessionSubscribeDeps.subscribeToSession`'s returned function (Plan-001 Phase 5's projector implementation MUST tolerate snapshot-during-emit / queued-removal because the F1 live-tail catch's `sub.cancel()` now fires registered `onCancel` handlers from inside the upstream's `onEvent` call stack). Final state: all 6 review threads resolved via GraphQL `resolveReviewThread` (R1 P1 replay-flush, R2 P2 subscribe init schema, R3 P2 cancel idempotency, R3 P2 thenable detection, R6 P2 retain unsubscribe, R7 P2 Promise.resolve thenable absorption); branch-protection's `required_conversation_resolution: true` gate cleared; `mergeStateStatus: CLEAN`. Round 7→Round 8 cap-exception framing applied per the three-label review framework + ACTIONABLE-deferral discipline — F5 (lifecycle-contract API completion) and F6 (fix-uncovers-fix on the same contract surface) justified rounds beyond the budgeted final adversarial pass; Codex Round 8 returned no new findings, confirming convergence. Plan-001 Phase 5 (`sessionClient.ts` consumer + Desktop Bootstrap) is now unblocked — the CP-007-4 transport surface ships at `0e5599d` and the re-entrant precondition on `subscribeToSession` is documented for the projector wire-up. _BL-113 retroactive audit notes (2026-05-18):_ F6 widened `ClientTransport.send` return type from `void | Promise` → `void | PromiseLike` — backward compatible (Promise extends PromiseLike). `packages/client-sdk/` carries a `@trpc/server` devDep (type-only inference for HTTP/SSE-aware error mapping per the Plan-008-bootstrap co-tier substrate); this devDep is undisclosed in Plan-007 header line 12. Plan-008-bootstrap is co-tier so no build-order break, but single-line plan-header amendment recommended for plan-isolation-readability (tracked retroactively here per D-007-1). ### Retroactive Audit Memo (BL-113, 2026-05-18) @@ -609,23 +904,70 @@ Plan-007 partial Phases 1-3 shipped (PRs #16/#17/#19, merged 2026-04-29/2026-04- ### Tier 1 (Plan-007-Partial) -- [x] All Tier 1 W-007p-1-T1..T5 + W-007p-2-T1..T11 + I-007-3-T1..T9 tests pass (PR #16 Phase 1 W-007p-1 suite; PR #17 Phase 2 W-007p-2 suite (129 passed, 1 skipped Linux-runner Windows-pipe, 1 todo Tier-4 deferred); PR #19 Phase 3 I-007-3-T1..T5 suite; PR #79 BL-117 I-007-3-T8 + T9 direct round-trip tests added — coverage range extended T1..T7 -> T1..T9 per BL-117 closure) -- [x] Invariants I-007-1 through I-007-11 enforced and individually tested at Tier 1 scope (I-007-10 / I-007-11 promoted retroactively by the BL-113 audit; I-007-10 daemon-side covered by I-007-3-T6, I-007-10 SDK-side + I-007-11 covered by I-007-3-T7; daemon-side W-007p-2 framing + registry invariants per PR #17; F1-F6 contract hardening per PR #19's seven Codex round-trips) -- [x] §Cross-Plan Obligations CP-007-1..5 surface ships verified (CP-007-1 `session.*` handlers + SDK via PR #19; CP-007-3 `router.register` registry via PR #17; CP-007-4 `transport/jsonRpcClient.ts` + `transport/types.ts` via PR #19; CP-007-5 `security.*` event-type taxonomy registration at HEAD per Plan-007:182 — bootstrap emitter writes against the registered Spec-006 §Security Events row from BL-105 closure 2026-05-01; the stale BLOCKED-ON-C9 marker drift in `secure-defaults-events.ts` was closed via NS-11 — see [cross-plan-dependencies.md NS-11](../architecture/cross-plan-dependencies.md)) +- [x] All Tier 1 W-007p-1-T1..T5 + W-007p-2-T1..T11 + I-007-3-T1..T9 tests pass (Phase 1 W-007p-1 suite; Phase 2 W-007p-2 suite (129 passed, 1 skipped Linux-runner Windows-pipe, 1 todo Tier-4 deferred); Phase 3 I-007-3-T1..T5 suite; BL-117 I-007-3-T8 + T9 direct round-trip tests added — coverage range extended T1..T7 -> T1..T9 per BL-117 closure) +- [x] Invariants I-007-1 through I-007-11 enforced and individually tested at Tier 1 scope (I-007-10 / I-007-11 promoted retroactively by the BL-113 audit; I-007-10 daemon-side covered by I-007-3-T6, I-007-10 SDK-side + I-007-11 covered by I-007-3-T7; daemon-side W-007p-2 framing + registry invariants ship with the Phase 2 wire substrate; F1-F6 contract hardening rides Phase 3's seven Codex round-trips) +- [x] §Cross-Plan Obligations CP-007-1..5 surface ships verified (CP-007-1 `session.*` handlers + SDK shipped at Phase 3; CP-007-3 `router.register` registry shipped at Phase 2; CP-007-4 `transport/jsonRpcClient.ts` + `transport/types.ts` shipped at Phase 3; CP-007-5 `security.*` event-type taxonomy registration at HEAD per Plan-007:182 — bootstrap emitter writes against the registered Spec-006 §Security Events row from BL-105 closure 2026-05-01; the stale BLOCKED-ON-C9 marker drift in `secure-defaults-events.ts` was closed via NS-11 — see [cross-plan-dependencies.md NS-11](../architecture/cross-plan-dependencies.md)) - [x] BLOCKED-ON-C6 governance pickup tracked: JSON-RPC handshake `protocolVersion` type closed 2026-05-01 via BL-102 ratification at [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md) — ISO 8601 `YYYY-MM-DD` date-string per MCP precedent; Spec-007:54 amended; substrate narrowed at `packages/contracts/src/jsonrpc.ts` + `packages/contracts/src/jsonrpc-negotiation.ts` + `packages/runtime-daemon/src/ipc/protocol-negotiation.ts`. [`MethodRegistry` + `LocalSubscriptionProducer` shapes closed 2026-04-30 via [BL-102](../backlog.md) no-mirror disposition — canonical sources: `packages/contracts/src/jsonrpc-registry.ts` + `packages/contracts/src/jsonrpc-streaming.ts`. Method-name format convention closed via [api-payload-contracts.md §Tier 1 (cont.): Plan-007](../architecture/contracts/api-payload-contracts.md).] -- [x] BLOCKED-ON-C7 closed 2026-05-01 via [BL-103](../backlog.md) ratification: [error-contracts.md §JSON-RPC Wire Mapping](../architecture/contracts/error-contracts.md#json-rpc-wire-mapping) declares the numeric ↔ dotted-namespace two-layer envelope (JSON-RPC 2.0 §5.1 `code` + `data.type` + `data.fields`) per RFC 7807 + LSP 3.17 ResponseError precedent; `unknown_setting`, `transport.unavailable`, `transport.message_too_large`, and `transport.invalid_protocol_version` Tier-1 domain identifiers shipped with structured `data.fields` shapes (the transport-layer 413-semantic `transport.message_too_large` is registered distinct from Spec-001's domain-quota 429-semantic `resource.limit_exceeded` per the canonicalization round-trip closed 2026-05-01; the substrate-side `transport.invalid_protocol_version` envelope-level gate is registered distinct from registry-side `protocol.version_mismatch` NegotiationError to disambiguate Spec-007:54 wire-shape rejection from F-007p-2-10 negotiation-incompatibility rejection). +- [x] BLOCKED-ON-C7 closed 2026-05-01 via [BL-103](../backlog.md) ratification: [error-contracts.md §JSON-RPC Wire Mapping](../architecture/contracts/error-contracts.md#json-rpc-wire-mapping) declares the numeric ↔ dotted-namespace two-layer envelope (JSON-RPC 2.0 §5.1 `code` + `data.type` + `data.fields`) per RFC 7807 + LSP 3.17 ResponseError precedent; `unknown_setting`, `transport.unavailable`, `transport.message_too_large`, and `transport.invalid_protocol_version` Tier-1 domain identifiers shipped with structured `data.fields` shapes (the transport-layer 413-semantic `transport.message_too_large` is registered distinct from Spec-001's domain-quota 429-semantic `resource.limit_exceeded` per the canonicalization round-trip closed 2026-05-01; the substrate-side `transport.invalid_protocol_version` envelope-level gate is registered distinct from registry-side `protocol.version_mismatch` NegotiationError to disambiguate Spec-007:54 wire-shape rejection from registry-level negotiation-incompatibility rejection). - [x] BLOCKED-ON-C9 closed 2026-05-01 via [BL-105](../backlog.md) ratification: [Spec-006 §Security Events (`security_events`)](../specs/006-session-event-taxonomy-and-audit-log.md#security-events-security_events) registers `security.default.override` + `security.update.available`; [Plan-006 §Event Taxonomy Coverage](./006-session-event-taxonomy-and-audit-log.md#event-taxonomy-coverage) emitter table lists Plan-007. - [x] BLOCKED-ON-C6 governance pickup tracked: subscribe streaming-primitive shape — closed 2026-04-30 via [BL-102](../backlog.md) no-mirror disposition; canonical source: `packages/contracts/src/jsonrpc-streaming.ts`. -- [x] Plan-001 Phase 5's `sessionClient.ts` consumes the transport surface from CP-007-4 without modification (verified via T-007p-3-3 — Plan-001 PR #30 squash-commit `7e4ae47` on `develop`, merged 2026-05-05; the post-Phase-D Codex round-trips C1-C8 hardened the transport import surface without modifying the Plan-007-owned `transport/jsonRpcClient.ts` + `transport/types.ts` contracts) +- [x] Plan-001 Phase 5's `sessionClient.ts` consumes the transport surface from CP-007-4 without modification (verified via T-007p-3-3 — Plan-001 Phase 5 squash-commit `7e4ae47` on `develop`, merged 2026-05-05; the post-Phase-D Codex round-trips C1-C8 hardened the transport import surface without modifying the Plan-007-owned `transport/jsonRpcClient.ts` + `transport/types.ts` contracts) ### Tier 4 (Plan-007-Remainder) -- [ ] Code changes implemented -- [ ] Tests added or updated -- [ ] Verification completed -- [ ] Related docs updated -- [ ] `SecureDefaults` module lands with all Spec-027-owned rows enforced (2, 3, 4, 7a, 7b, 8, 10) and every override path emits its `security.default.override=*` log event -- [ ] First-run key ceremony verified on Linux, macOS, and Windows (permissions + sentinel + fingerprint display) -- [ ] TLS 1.3-only listener factory verified to reject TLS 1.2 without `LEGACY_TLS12=1` and reject TLS ≤ 1.1 even with the legacy flag -- [ ] Self-update CLI verified against dual-verification (manifest-sig + Sigstore) and anti-rollback/freeze checks on all three platforms -- [ ] First-run-banner content contract verified: every Spec-027-row-10 field renders in stdout text and in `BANNER_FORMAT=json` single-line form +**Phase R1 — Namespace Handlers (Tier 4):** + +- [ ] R1 contract surfaces ship: `packages/contracts/src/daemon-status.ts` + `daemon-lifecycle.ts` (`daemon.stop` + `daemon.restart` only, with `idleDrainDeadlineMs?: number`; no `DaemonStart*` — cold-boot is the CLI process-spawn path) + `settings.ts` (T-007r-1-1 + T-007r-1-2 + T-007r-1-3) +- [ ] R1 daemon handlers ship: `daemon-status-read.ts` + `daemon-stop.ts` + `daemon-restart.ts` + `settings-effective-read.ts` bind into the substrate registry (no `daemon-start.ts` — cold-boot is the CLI process-spawn path) (T-007r-1-4 + T-007r-1-5 + T-007r-1-6) +- [ ] R1 namespace-registration extends bootstrap: `daemon.*` and `settings.*` register from `runtime-daemon/src/bootstrap/index.ts`; cross-plan namespaces (`run.*` Plan-004, `repo.*` Plan-009, `artifact.*` Plan-014, `driver.*` Plan-005 P4, `event.*` Plan-006 P4) attach from owning plans per NS-26 precedent (T-007r-1-7) +- [ ] R1 doc-mirrors land: api-payload-contracts.md Tier 4 method-name table appended (T-007r-1-8); error-contracts.md `daemon.lifecycle_conflict` envelope registered in Plan-007 Tier 4 Domain Identifiers (T-007r-1-9) +- [ ] R1 test suite passes: R1-T1..R1-T7 (`daemon.status.read` projection per processState enum; daemon-lifecycle namespace registers `daemon.stop` + `daemon.restart` and NO `daemon.start` IPC method — cold-boot is CLI-spawn-owned; `daemon.stop` and `daemon.restart` I-007-12 self-swap refusal with `lifecycle_conflict`; `idleDrainDeadlineMs` honored with 5000 default; `settings.effectiveRead` excludes secrets per I-007-3; the two R1-owned namespaces register + a duplicate registration is rejected per I-007-6, with the other five namespaces verified by their owning plans; cross-namespace isolation per I-007-9) (T-007r-1-10) + +**Phase R2 — Secure Defaults (Tier 4):** + +- [ ] R2 SecureDefaults extension lands: Tier 4 config keys recognized (`tlsMode`, `firstRunKeysPolicy`, `nonLoopbackHost`, `updateChannel`, `updateMode`, `adminTokenPath`, `backupDestination`, `backupCadence`) per Spec-027 rows 2/3/7a/7b/8 (T-007r-2-1) +- [ ] R2 override emitter `listEmitted()` projection lands for banner-content extension consumption per CP-007-9 (T-007r-2-2) +- [ ] R2 TLS surface ships: `bootstrap/tls-surface.ts` enforces TLS 1.3-only (`minVersion: "TLSv1.3"`); `getFingerprint()` derives SPKI-SHA256 for banner content (T-007r-2-3) +- [ ] R2 first-run-keys ceremony ships: `bootstrap/first-run-keys.ts` + `bootstrap/daemon-key-store.ts` (`DaemonKeyStore` interface + `InMemoryDaemonKeyStore` test stub per CP-007-8); R2 tests the ceremony orchestration against the stub (key generation + admin-token write + idempotency-given-an-already-populated-store). The real `OsKeystoreSealedDaemonKeyStore` (`@napi-rs/keyring` v1.2.0 per Spec-022:146), its composition-root injection, the production-guard assertion (refuse the stub outside tests), and cross-platform sealing verification (permissions + sentinel + fingerprint on Linux/macOS/Windows) are Plan-022 Tier 5 per CP-007-8 — not R2 (the real impl does not exist until Tier 5) (T-007r-2-4) +- [ ] R2 update-notify poller lands: `bootstrap/update-notify.ts` passively polls; `getEffectiveSettings()` projection exposes channel + mode + lastChecked + pendingVersion for banner content (T-007r-2-5) +- [ ] R3-PR-a self-update CLI lands at `apps/cli/src/commands/self-update.ts` (in the R3-owned scaffold, alongside the other CLI commands): dual-verification (manifest-sig + Sigstore) + anti-rollback/freeze checks verified on Linux, macOS, and Windows; I-007-16 security-critical banner non-suppression honored (T-007r-3-6a; relocated from R2 per the 2026-05-28 ratification reversal) +- [ ] R2 bootstrap-ordering wires the new surfaces: `SecureDefaults.load` → `TlsSurface.maybeStart` → `FirstRunKeyCeremony.maybeRun` → `UpdateNotifyPoller.start` → banner-content assembly per CP-007-9 (T-007r-2-7) +- [ ] R2 I-007-16 enforcement lands: security-critical override-banner rows tagged at emission time so the R3 CLI cannot suppress them via `--no-banner` (T-007r-2-8) +- [ ] R2 test suite passes: R2-T1..R2-T6 (TLS surface rejects non-TLS-1.3; first-run-keys ceremony is idempotent given an already-populated `DaemonKeyStore` — the `InMemoryDaemonKeyStore` stub at R2, with real sealed-store at-rest persistence verified at Plan-022 Tier 5; update-notify polls passively + does not auto-apply; banner-content extension projects all R2-owned fields from `effectiveSettings()`; I-007-12 self-swap integration; `--no-banner` does NOT suppress security-critical rows per I-007-16) (T-007r-2-9) + +**Phase R3 — Client Delivery (Tier 4):** ships as 3 sequential PRs (R3-PR-a → R3-PR-b → R3-PR-c) per the 2026-05-28 audit advisor's quality-over-speed split. Each PR boundary is reviewable in isolation; PR-c BLOCKED on PR-b's bridge-contract ship per §Rollout Order within-R3 blocking dependencies. + +**R3-PR-a — CLI Delivery:** + +- [ ] R3-PR-a `apps/cli/` workspace package scaffold lands: `package.json` (`@ai-sidekicks/cli`, `bin: { "ai-sidekicks": "./dist/main.js" }`, exact-version pin `clipanion@4.0.0-rc.4` with Yarn precedent justification + [BL-134](../backlog.md#bl-134-clipanion-stable-v4-upgrade--lockfile-bump-for-plan-007-r3-pr-a-cli) tracking for stable-v4 upgrade, `@ai-sidekicks/client-sdk@workspace:*`, `@ai-sidekicks/contracts@workspace:*`); tsup single-file ESM bundle; ESLint `import/no-restricted-paths` enforces I-007-13 import isolation (T-007r-3-1) +- [ ] R3-PR-a CLI entry point lands at `apps/cli/src/main.ts`: clipanion CLI; banner surfaced from `settings.effectiveRead` for commands run against a running daemon (daemon-availability-tolerant — `daemon start` surfaces the freshly-spawned daemon's own banner after `DaemonHelloAck`, and an unreachable daemon degrades to a typed "daemon unavailable" notice rather than a banner-read failure); `--no-banner` honored only for non-security-critical rows per CP-007-10 + I-007-16 (T-007r-3-2) +- [ ] R3-PR-a exit-code mapping closed-deterministic per I-007-14 + Spec-007:32 (sysexits-style: -32700→65, -32600→64, -32601→64, -32602→64, -32603→70); `UnmappedExitCodeError` thrown for drift (T-007r-3-3) +- [ ] R3-PR-a daemon-lifecycle CLI commands land (nested paths `daemon start` / `daemon stop` / `daemon restart` per I-007-15): `daemon start` spawns the daemon process and `daemon restart` awaits the re-exec'd daemon's `DaemonHelloAck` before reporting success, `daemon stop` awaits clean drain + disconnect (no `daemon.start` IPC method — cold-boot is the CLI process-spawn path); `--idle-drain-deadline ` flag on stop/restart (default 5000); `daemon.lifecycle_conflict` error envelope (JSON-RPC `-32603 InternalError` + `data.type` discriminator) maps to POSIX exit code 70 per T-007r-3-3 (T-007r-3-4) +- [ ] R3-PR-a `daemon status` CLI command lands (T-007r-3-5) +- [ ] R3-PR-a `settings effective-read` CLI command lands (T-007r-3-6) +- [ ] R3-PR-a test suite passes: R3-T1..R3-T8 (CLI lifecycle commands honor the I-007-15 readiness handshake — `daemon start`/`daemon restart` await `DaemonHelloAck`, `daemon stop` awaits clean drain + disconnect; idle-drain-deadline honored; `lifecycle_conflict` mapped per I-007-12 + I-007-14; settings excludes secrets per I-007-3; import-isolation enforced by ESLint per I-007-13; exit-code mapping total per I-007-14; `--no-banner` honors security-critical per I-007-16) (T-007r-3-15 slice a) + +**R3-PR-b — Desktop Main + Supervision:** + +- [ ] R3-PR-b desktop main transport-supervision projector lands at `apps/desktop/src/main/daemon-status-projector/`: subscribes to the `SupervisionHooks` surface from `local-ipc-gateway.ts:114-172`; translates 5-value `SupervisionDisconnectReason` enum (`client_close` | `server_close` | `transport_error` | `oversized_body` | `malformed_frame`) into `DaemonStatusEvent`; concern-disjoint from Plan-023 Tier 8's `daemon-supervisor.ts` per the concern-split ratification + CP-007-11; I-007-17 single-emit-per-transport-boundary enforced (T-007r-3-7) +- [ ] R3-PR-b supervision-status XState v5 state machine lands: states `connected` / `transient_disconnect` / `degraded` / `unknown` with I-007-18 fail-closed-to-degraded on unrecognized reasons (T-007r-3-8) +- [ ] R3-PR-b desktop main IPC bridge EXTENDS Plan-023 Tier 8's `bridge/index.ts` registry per CP-007-12: `bridge/daemon-status.ts` registers the `daemon.status` topic on the existing `daemon.subscribe` channel (T-007r-3-9) +- [ ] R3-PR-b desktop bridge contract extension lands: `packages/contracts/src/desktop-bridge.ts` extends `SidekicksBridge` typed subscription map to include the `daemon.status` topic; ambient `window.sidekicks` type at `sidekicks-bridge.d.ts` (Plan-002 Phase 6's NS-29 hoist per CP-007-13) picks up via declaration merging (T-007r-3-10) +- [ ] R3-PR-b desktop main bootstrap subscribes the projector to `SupervisionHooks` BEFORE any renderer window opens (T-007r-3-13) +- [ ] R3-PR-b renderer-issued `daemon.restart` mutating-op gate lands: bridge handler refuses every renderer mutating call lacking a valid `DaemonHelloAck` (daemon-status `connected`), enforced authoritatively at the substrate mutating-op gate (`local-ipc-gateway.ts:1096+`) per I-007-19 + Spec-007 §Mutating-Op Gate — NOT an admin-token check (`./data/admin-token` is the relay-admin credential, not a local-IPC authority); refusals return a typed bridge error (T-007r-3-14) +- [ ] R3-PR-b test suite passes: R3-T9..R3-T11 + R3-T14 (`SupervisionDisconnectReason` 5-value enum exhaustively mapped per I-007-18; unknown reason fails-closed to `degraded` per I-007-18; single-emit-per-transport-boundary per I-007-17; renderer-issued `daemon.restart` `DaemonHelloAck`-gated at the bridge + substrate mutating-op gate per I-007-19) (T-007r-3-15 slice b) + +**R3-PR-c — Renderer Chrome:** + +- [ ] R3-PR-c renderer chrome-level `DaemonStatusView.tsx` lands at `apps/desktop/src/renderer/src/daemon-status/` (deliberately outside `features/` per the chrome-vs-features placement convention documented in §Notes): React function component consumes `window.sidekicks.daemon.subscribe('daemon.status', ...)` and renders the supervision-status pill (T-007r-3-11) +- [ ] R3-PR-c disconnect-reason copy table lands at `apps/desktop/src/renderer/src/daemon-status/disconnect-reason-copy.ts`: total mapping for all 5 canonical enum values; unknown reason falls to generic copy + remediation per I-007-18 (T-007r-3-12) +- [ ] R3-PR-c test suite passes: R3-T12..R3-T13 (renderer view renders all 4 state-machine states; disconnect-reason copy total per I-007-18) (T-007r-3-15 slice c) + +**Tier-spanning verifications:** + +- [ ] Spec-027 row coverage extends Tier-4: rows 2 (refuse-to-start without encryption), 3 (first-run keys), 7a (update-notify), 7b (self-update CLI), 8 (TLS 1.3-only) — every override path emits its `security.default.override=*` log event single-emit-per-startup per I-007-4 +- [ ] First-run key ceremony: R2 verifies the ceremony orchestration (key generation + admin-token write + idempotency) against the `InMemoryDaemonKeyStore` stub; the cross-platform OS-keystore sealing verification on Linux, macOS, and Windows (permissions + sentinel + fingerprint display per Spec-027 row 3) is a Plan-022 Tier 5 deliverable with the real `OsKeystoreSealedDaemonKeyStore` per CP-007-8 (the sealing behavior under test does not exist until the real store ships) +- [ ] TLS 1.3-only listener factory verified to reject TLS 1.2 without `LEGACY_TLS12=1` and reject TLS ≤ 1.1 even with the legacy flag (Spec-027 row 8) +- [ ] Self-update CLI verified against dual-verification (manifest-sig + Sigstore) and anti-rollback/freeze checks on all three platforms (Spec-027 row 7b) +- [ ] First-run-banner content contract verified: every Spec-027-row-10 field (TLS mode + fingerprint, effective bind addresses, backup destination + cadence, admin-token file path, update channel + mode, active security overrides) renders in stdout text and in `bannerFormat: "json"` single-line form +- [ ] §Cross-Plan Obligations CP-007-7..CP-007-14 surface ships verified: CP-007-7 `event.*` namespace handlers register via Plan-006 P4 against the substrate registry; CP-007-8 `DaemonKeyStore` interface + `InMemoryDaemonKeyStore` stub land at R2 with Plan-022 Tier 5 owning real impl; CP-007-9 banner content extension wires `effectiveSettings()` for CP-007-10 R3 banner-string consumption; CP-007-11 concern-disjoint transport-supervision + process-supervision land per the concern-split ratification; CP-007-12 R3 bridge extends Plan-023 Tier 8 registry; CP-007-13 R3 renderer view typechecks against Plan-002 Phase 6 ambient bridge type; CP-007-14 R3 CLI imports `@ai-sidekicks/client-sdk` from Plan-001 Phase 5 diff --git a/docs/plans/008-control-plane-relay-and-session-join.md b/docs/plans/008-control-plane-relay-and-session-join.md index af851f4a..e273f032 100644 --- a/docs/plans/008-control-plane-relay-and-session-join.md +++ b/docs/plans/008-control-plane-relay-and-session-join.md @@ -194,7 +194,7 @@ The `audit_status: substrate_exempt` declaration is documentary — Phase 1 alre | `session.read` | `query` | `SessionReadRequestSchema` | `SessionReadResponseSchema` | `directoryService.readSession(...)` | request-id stamping; Querier-injection context | | `session.join` | `mutation` | `SessionJoinRequestSchema` | `SessionJoinResponseSchema` | `directoryService.joinSession(...)` | request-id stamping; Querier-injection context (Tier 1 stub: rejects non-self joins until invite/presence land at Tier 5 — see I-008-2) | - The procedure type assignments follow the tRPC convention: read-only operations use `query` (HTTP GET-like, idempotent); writes/state-changes use `mutation` (HTTP POST-like, non-idempotent). Closed 2026-04-30: procedure-type assignments and canonical dotted-lowercase method-name strings (`session.create`, `session.read`, `session.join`) are ratified at [api-payload-contracts.md §Tier 1 (cont.): Plan-008](../architecture/contracts/api-payload-contracts.md) (cross-transport consistency with Plan-007 JSON-RPC IPC per the same source). + The procedure type assignments follow the tRPC convention: read-only operations use `query` (HTTP GET-like, idempotent); writes/state-changes use `mutation` (HTTP POST-like, non-idempotent). Closed 2026-04-30: procedure-type assignments and canonical `dotted-camelCase` method-name strings (`session.create`, `session.read`, `session.join`) are ratified at [api-payload-contracts.md §Tier 1 (cont.): Plan-008](../architecture/contracts/api-payload-contracts.md) (cross-transport consistency with Plan-007 JSON-RPC IPC per the same source). - `packages/control-plane/src/sessions/session-subscribe-sse.ts` — SSE transport plumbing for `SessionSubscribe`. The contract is request-only on the wire; the response is an `AsyncIterable` SSE stream per `packages/contracts/src/session.ts:408`. Bootstrap supplies only the transport — event sourcing into the stream remains Plan-006's domain. Wire frame ratified at [api-payload-contracts.md §SSE Wire Frame (Tier 1 Ratified)](../architecture/contracts/api-payload-contracts.md) (closed 2026-04-30): `Content-Type: text/event-stream; charset=utf-8`; `Cache-Control: no-store`; `X-Accel-Buffering: no`; one `EventEnvelope` per SSE event encoded as `data: `; `id:` carries the `EventCursor` value from Plan-006 (or placeholder string at Tier 1 pending Plan-006 widening); `retry: 5000`; on reconnect with `Last-Event-ID`, server emits all events strictly after that cursor; `event: heartbeat\ndata: {}\n\n` every 15s. **SSE adapter selection is settled by BL-104 resolution:** tRPC v11's shared HTTP resolver (`packages/server/src/unstable-core-do-not-import/http/resolveResponse.ts` upstream) detects subscription procedures and produces the SSE-streaming `Response` natively when invoked through `@trpc/server/adapters/fetch`'s `fetchRequestHandler` on Cloudflare Workers. No separate SSE adapter is required. @@ -227,7 +227,7 @@ The `audit_status: substrate_exempt` declaration is documentary — Phase 1 alre Tests: T-008b-1-T1 handler refuses without flag (I-008-1 gate #1); T-008b-1-T2 handler refuses on every non-`'development'` `ENVIRONMENT` value even with flag set (I-008-1 gate #2 allow-list — table-driven over `undefined`, `'production'`, `'staging'`, `'test'`, `''`); T-008b-1-T3 handler serves with both keys set under `wrangler dev` (`.dev.vars` supplies `ENVIRONMENT=development` AND `CONTROL_PLANE_BOOTSTRAP_ENABLED=1`). (Audit trail for prior `[env.dev]`, top-level-`[vars]`-flag, and deny-list-gate task drafts: see §Decision Log.) -- **T-008b-1-2** (Files: `packages/control-plane/src/sessions/session-router.ts` (CREATE) + `packages/control-plane/src/sessions/session-router.factory.ts` (CREATE); Verifies invariant: I-008-3 enforcement #1 (constructor injection); Spec coverage: §Cross-Plan Obligations CP-008-1) — Implement the typed tRPC router via `createSessionRouter(directoryService: SessionDirectoryService): TRPCRouter` factory. The 3 procedures (per the table above) bind to `directoryService.createSession` / `readSession` / `joinSession` exclusively; the factory does NOT instantiate `Querier` or `pg.Pool` directly. Procedure-type assignments + canonical method-name strings (dotted-lowercase `session.create` / `session.read` / `session.join`) are ratified at [api-payload-contracts.md §Tier 1 (cont.): Plan-008](../architecture/contracts/api-payload-contracts.md) (closed 2026-04-30). Tests: T-008b-1-T4 round-trip `session.create` end-to-end against `pg.Pool`-backed `Querier` (I-001-1 lock-ordering inherited via directory service); T-008b-1-T5 round-trip `session.read`; T-008b-1-T6 round-trip `session.join` (Tier 1 stub: self-joins succeed; non-self joins reject with `auth.not_authorized` until Tier 5 invite/presence lands). +- **T-008b-1-2** (Files: `packages/control-plane/src/sessions/session-router.ts` (CREATE) + `packages/control-plane/src/sessions/session-router.factory.ts` (CREATE); Verifies invariant: I-008-3 enforcement #1 (constructor injection); Spec coverage: §Cross-Plan Obligations CP-008-1) — Implement the typed tRPC router via `createSessionRouter(directoryService: SessionDirectoryService): TRPCRouter` factory. The 3 procedures (per the table above) bind to `directoryService.createSession` / `readSession` / `joinSession` exclusively; the factory does NOT instantiate `Querier` or `pg.Pool` directly. Procedure-type assignments + canonical method-name strings (`dotted-camelCase` `session.create` / `session.read` / `session.join`) are ratified at [api-payload-contracts.md §Tier 1 (cont.): Plan-008](../architecture/contracts/api-payload-contracts.md) (closed 2026-04-30). Tests: T-008b-1-T4 round-trip `session.create` end-to-end against `pg.Pool`-backed `Querier` (I-001-1 lock-ordering inherited via directory service); T-008b-1-T5 round-trip `session.read`; T-008b-1-T6 round-trip `session.join` (Tier 1 stub: self-joins succeed; non-self joins reject with `auth.not_authorized` until Tier 5 invite/presence lands). - **T-008b-1-3** (Files: `packages/control-plane/src/sessions/session-subscribe-sse.ts` (CREATE) + `packages/control-plane/src/sessions/session-subscribe-sse.factory.ts` (CREATE); Verifies invariant: I-008-3 enforcement #1 (constructor injection); Spec coverage: §Cross-Plan Obligations CP-008-1 + CP-008-3) — Implement SSE substrate via `createSessionSubscribeSse(directoryService: SessionDirectoryService): SseHandler` factory. The SSE-streaming `Response` is produced natively by tRPC's shared HTTP resolver when the subscription procedure is invoked through `fetchRequestHandler`; the factory wires the directory-service into the subscription procedure's async-generator body. Wire frame primitive ratified at [api-payload-contracts.md §SSE Wire Frame (Tier 1 Ratified)](../architecture/contracts/api-payload-contracts.md) (closed 2026-04-30); frame-shaping logic centralized in the factory. Tests: T-008b-1-T7 SSE connection lifecycle (open + send synthetic EventEnvelope + close on disconnect); T-008b-1-T8 `Last-Event-ID` resumption (reconnect with header → server emits events strictly after cursor); T-008b-1-T9 heartbeat cadence (15s `event: heartbeat` frames in absence of data). - **T-008b-1-4** (Files: `packages/control-plane/.eslintrc.js` (EXTEND with `no-restricted-imports`) + `packages/control-plane/test/sessions/router-no-sql.test.ts` (CREATE); Verifies invariant: I-008-3 enforcement #2 (ESLint rule) + #3 (unit-test introspection)) — Land the I-008-3 enforcement mechanism: ESLint `no-restricted-imports` rule forbidding `pg`, `pg-pool`, `@databases/pg` imports from `session-router.ts` and `session-subscribe-sse.ts`; CI fails on rule violation. Unit-test introspection asserts the two files' exported symbols call only `directoryService.*` methods (TypeScript symbol introspection or AST walker pinned in the test runner). Tests: T-008b-1-T10 ESLint rule trips on direct `pg` import in router; T-008b-1-T11 introspection assertion catches new SQL via direct `Querier` instantiation in router. - **T-008b-1-5** (Files: `packages/client-sdk/test/transport/sse-roundtrip.test.ts` (CREATE) — stub-side test) — Per F-008b-1-09, Phase 1's _raison d'être_ is to unblock Plan-001 Phase 5; the integration handoff must be tested in this PR. T-008b-1-T12 round-trip integration test: a stub `sessionClient.subscribe` (using the contracts-side schema from Plan-001 Phase 2) connects to the Phase 1 SSE substrate, receives a synthetic `EventEnvelope` (sourced via test fixture, since Plan-006 event sourcing is out of Phase 1 scope), and verifies cursor-based resumption via `Last-Event-ID`. Frame shape canonical at [api-payload-contracts.md §SSE Wire Frame (Tier 1 Ratified)](../architecture/contracts/api-payload-contracts.md) (closed 2026-04-30); test asserts on that ratified shape. **Spec coverage:** Spec-008 §Required Behavior — control-plane transport SSE substrate integration with Plan-001 Phase 5 (cursor-based resumption via `Last-Event-ID`). diff --git a/docs/specs/006-session-event-taxonomy-and-audit-log.md b/docs/specs/006-session-event-taxonomy-and-audit-log.md index 207f0833..bb29abc2 100644 --- a/docs/specs/006-session-event-taxonomy-and-audit-log.md +++ b/docs/specs/006-session-event-taxonomy-and-audit-log.md @@ -430,7 +430,7 @@ Payload shape: `{sessionId, anchorId?, verifierNodeId}` (base). Per-event payloa | Type | Description | Payload Extension | | --- | --- | --- | | `audit_integrity_verified` | A read-side verifier has completed hash, signature, and anchor checks successfully over a range. Promoted from §Integrity Events with category corrected from `session_lifecycle` → `audit_integrity`. | base + `{treeSize, rootHash, fromSeq, toSeq, verifiedAt, signatureAlgorithm}` | -| `audit_integrity_failed` | A read-side verifier detected a chain break, signature failure, or anchor mismatch. Halts replay at the affected row and must be surfaced to operators. Promoted from §Integrity Events with category corrected from `session_lifecycle` → `audit_integrity`. | base + `{treeSize, expectedRootHash, observedRootHash, failureMode ∈ ['hash_mismatch','signature_mismatch','anchor_mismatch','inclusion_proof_failed','consistency_proof_failed','log_file_missing','log_file_moved'], failurePath ∈ ['inclusion','consistency','signature'], offendingSeq?, detail}` | +| `audit_integrity_failed` | A read-side verifier detected a chain break, signature failure, anchor mismatch, or post-compaction integrity failure. Halts replay at the affected row and must be surfaced to operators. Promoted from §Integrity Events with category corrected from `session_lifecycle` → `audit_integrity`. The four `anchor_missing_for_compacted_range` / `anchor_signature_invalid` / `stub_signature_invalid` / `stub_scalar_mismatch` modes are additive-MINOR extensions for the post-compaction integrity protocol per §Post-Compaction Integrity. | base + `{treeSize, expectedRootHash, observedRootHash, failureMode ∈ ['hash_mismatch','signature_mismatch','anchor_mismatch','inclusion_proof_failed','consistency_proof_failed','log_file_missing','log_file_moved','anchor_missing_for_compacted_range','anchor_signature_invalid','stub_signature_invalid','stub_scalar_mismatch'], failurePath ∈ ['inclusion','consistency','signature'], offendingSeq?, detail}` | | `key_reuse_detected` | An observer/monitor detected an event signed by a `NodeId` whose Ed25519 public key collides with a prior rotated-out key — the rotation invariant `refuse_on_rotation` has been violated. Security-grade signal: an attacker may be replaying a compromised key, or a legitimate key-rotation bug has reused a retired public key. | `{offendingKeyFingerprint, observedPeerIds[], firstSeenAt, rotationInvariantViolated: 'refuse_on_rotation', detectorNodeId}` | **Precedent — envelope and failure vocabulary.** [RFC 9162 — Certificate Transparency v2.0 (December 2021)](https://datatracker.ietf.org/doc/html/rfc9162) (accessed 2026-04-19) _"obsoletes RFC 6962"_ and publishes the canonical envelope: `SignedTreeHeadDataV2 { LogID log_id; TreeHeadDataV2 tree_head; opaque signature<1..2^16-1>; }` (§4.10 verbatim); `TreeHeadDataV2 { uint64 timestamp; uint64 tree_size; NodeHash root_hash; Extension sth_extensions<0..2^16-1>; }` (§4.9 verbatim); `InclusionProofDataV2 { LogID log_id; uint64 tree_size; uint64 leaf_index; NodeHash inclusion_path<...>; }` (§4.11 verbatim); `ConsistencyProofDataV2 { LogID log_id; uint64 tree_size_1; uint64 tree_size_2; NodeHash consistency_path<...>; }` (§4.12 verbatim). Failure vocabulary from RFC 9162 §§2.1.3.2 and 2.1.4.2 (verbatim): _"If `leaf_index` is greater than or equal to `tree_size`, then fail the proof verification"_; _"If `sn` is 0, then stop the iteration and fail the proof verification"_; _"If `consistency_path` is an empty array, stop and fail the proof verification"_. Our `failureMode` enum ports this vocabulary and extends it with log-file-level failures (missing, moved) observed in production systems. @@ -536,6 +536,8 @@ Total enumerated event types: **123** Canonical events are append-only AND tamper-evident. Every `session_events` row is chained to its predecessor via a BLAKE3 hash and signed by the emitting daemon with Ed25519 over the **same** canonical byte string. On a bounded cadence, a Merkle root over contiguous ranges is anchored to the control plane's `event_log_anchors` table as metadata only — the control plane does not store event payloads, consistent with [ADR-017 Shared Event-Sourcing Scope](../decisions/017-shared-event-sourcing-scope.md). Full protocol, including schema additions and verification order, is specified in [Security Architecture § Audit Log Integrity](../architecture/security-architecture.md#audit-log-integrity). +Tamper-evidence persists across compaction via a two-tier protocol: per-row chain + signature verification for uncompacted rows (`retention_class IS NULL`), and anchor-based range integrity for compacted rows (`retention_class = 'audit_stub'`). Per §Post-Compaction Integrity below, the compactor MUST force-fire and durably persist a covering Merkle anchor BEFORE stripping any row's canonical bytes — verifiers on compacted rows fall through to anchor-existence + anchor-signature checks instead of per-row recomputation. + ### Canonical Serialization Rules Both the `row_hash` input and the Ed25519-signed bytes are computed over the **same** canonical form. Two honest implementations that diverge here produce incompatible hashes and signatures for identical events, so the rules below are mandatory. @@ -543,7 +545,7 @@ Both the `row_hash` input and the Ed25519-signed bytes are computed over the **s - Canonicalization standard: [RFC 8785 — JSON Canonicalization Scheme (JCS)](https://datatracker.ietf.org/doc/html/rfc8785). Re-used identically from [Spec-024 Cross-Node Dispatch And Approval](024-cross-node-dispatch-and-approval.md) so the daemon runs one canonicalization rule across integrity and dispatch. - Hash function: [BLAKE3](https://github.com/BLAKE3-team/BLAKE3-specs/blob/master/blake3.pdf). Same digest used for Spec-024's `request_body_hash`. - Signature scheme: [RFC 8032 §5.1 — Ed25519](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1). -- Fields included, in this order: `id`, `sessionId`, `sequence`, `occurredAt`, `category`, `type`, `actor`, `payload`, `correlationId`, `causationId`, `version`. +- Fields included (the canonical set; serialized order is mandated by RFC 8785 §3.2.3 UTF-16 code-unit lex-sort of member names, not the order listed here): `id`, `sessionId`, `sequence`, `occurredAt`, `category`, `type`, `actor`, `payload`, `correlationId`, `causationId`, `version`. RFC 8785's lex-sort is non-negotiable — it is what makes "two honest implementations" above produce byte-identical canonical forms; this list documents membership only. - Fields with value `null` MUST be included (so "present-but-null" and "absent" are distinguishable after serialization). - `occurredAt` MUST be RFC 3339 UTC with millisecond precision (`YYYY-MM-DDTHH:MM:SS.sssZ`) so ordering is byte-stable. - `pii_payload` is NOT included in the canonical form. Events whose `pii_payload` column is non-NULL MUST embed a `pii_ciphertext_digest` field in `payload` — BLAKE3 over the ciphertext bytes of `pii_payload` — so a [Spec-022](022-data-retention-and-gdpr.md) crypto-shred of `pii_payload` does not break the hash chain. The digest sits inside the canonical bytes and is never shredded; shredding clears only the ciphertext column. @@ -598,7 +600,42 @@ An audit stub retains: } ``` -The full `payload`, `pii_payload`, `correlationId`, and `causationId` are removed. The `summary` field is generated at compaction time from the original payload. +At compaction the `payload` column is **replaced** (not nulled — the column is `NOT NULL`) by the JCS-canonicalized audit-stub projection above; `pii_payload`, `correlationId`, and `causationId` are removed (set NULL). The `summary` field is generated at compaction time from the original payload. The compactor then writes a per-row **`stub_signature`** — an Ed25519 signature over the **exact canonical byte string it stores in `payload`** (the RFC 8785 JCS serialization of the audit-stub projection) using the daemon's session signing key. **Sign-exact-bytes invariant:** the compactor signs the identical byte string it persists to `payload`, with no re-serialization between signing and storing; consequently a verifier authenticates the stub by checking `stub_signature` **directly over the stored `payload` bytes** — it never re-canonicalizes or reconstructs the projection from the scalar columns (`compactedAt` and `summary` are not columns; they exist only inside the stored projection). See [§ Post-Compaction Integrity](#post-compaction-integrity). The replay and renderer contracts read this stub from `payload` and MUST surface it with `retentionClass: 'audit_stub'` (never a silent omission) per [§ Replay Interaction with Compacted Regions](#replay-interaction-with-compacted-regions). + +### Post-Compaction Integrity + +Once an event is compacted to an audit stub, the daemon discards the original `payload` content, `correlationId`, and `causationId`. The row's `daemon_signature` and `row_hash` commit to canonical bytes derived from those (now-discarded) fields, so per-row recomputation **against the original** is no longer possible. Two complementary commitments preserve integrity instead: (1) a covering Merkle **anchor** witnesses that the original pre-compaction range existed and was internally consistent at anchor time; (2) a per-row **`stub_signature`** authenticates the surviving audit-stub bytes against post-compaction tampering. The anchor is the external (control-plane-witnessed) proof of original existence; the `stub_signature` is the at-rest proof that the visible stub is the daemon's genuine compaction output. Both are required — neither alone is sufficient (the anchor cannot detect a tampered stub, and a `stub_signature` alone cannot prove the original range was not fabricated wholesale). + +**Anchor-before-compaction protocol (load-bearing).** Before mutating any row's payload in a range `[start_sequence, end_sequence]`, the compactor MUST: + +1. Verify that a **covering** Merkle anchor exists for the range — one whose `[start_sequence, end_sequence]` spans the entire to-be-compacted range, i.e. `anchor.start_sequence ≤ range_start AND anchor.end_sequence ≥ range_end` (a single-anchor coverage test, NOT an exact-`start_sequence` match) — in the local `pending_anchor_uploads` queue OR already durably persisted to the control-plane's `event_log_anchors` table. +2. If no covering anchor exists, force-fire one — compute `merkle_root = BLAKE3-merkle(row_hash for row in range)` using the pre-existing anchoring path (the leaves are the rows' frozen `row_hash` values — the **same** leaf basis the cadence anchor commits via `MerkleAnchorService.onEventAppended({…rowHash})` and the verifier recomputes per [Security architecture §Audit Log Integrity](../architecture/security-architecture.md) Rule 3, NOT `canonical_bytes`: the canonical bytes are discarded at compaction, so a `canonical_bytes`-leaved root could never be reproduced at verify time, whereas `row_hash` is frozen by the I-006-3-03 chain-commitment-frozen invariant and survives; RFC 9162 §2.1 odd-leaf duplication), sign with the daemon's Ed25519 key, and queue for upload. +3. Wait for the anchor row to land in `pending_anchor_uploads` with a durable monotonic sequence (does NOT require successful control-plane upload — the local queue's durable ordering plus daemon signature is sufficient, since the queue's `UNIQUE(session_id, node_id, start_sequence, end_sequence)` makes re-anchoring the **same** range after partition recovery idempotent while still letting a wider covering anchor that shares a `start_sequence` coexist — the step-1 coverage test, not an exact-`start_sequence` match, decides whether a force-fire is needed). +4. For each row in the range, build the audit-stub projection (the field set in [§ Compacted Event Format](#compacted-event-format)) and serialize it **once** to its canonical byte string `B = canonical_bytes(stub)` (RFC 8785 JCS), then compute `stub_signature = Ed25519-sign(B, daemon_signing_key)`. Because `B` includes the row's `id` and `sequence`, the signature is bound to the specific row and cannot be replayed onto another. `B` itself — not a re-serialization of it — is the value step 5 stores in `payload`. +5. ONLY THEN mutate the rows in one transaction: **replace** `payload` with the exact canonical byte string `B` from step 4 (the column is `NOT NULL`; persisting `B` verbatim — never a re-serialization — is load-bearing so that checking `stub_signature` over the stored bytes at verify time is sound); set `correlation_id` / `causation_id` / `pii_payload` to NULL; set `retention_class = 'audit_stub'`; write the `stub_signature` from step 4. + +The compactor MUST refuse to proceed with payload mutation if step 1 returns false and step 2 fails. Chain-commitment columns (`prev_hash`, `row_hash`, `daemon_signature`, `participant_signature`, `monotonic_ns`, `version`) are NEVER mutated — they remain frozen as the commitment to the pre-compaction state, even though per-row recomputation against the (now-stub) `payload` would fail. + +**Verifier semantics on compacted rows.** For rows with `retention_class = 'audit_stub'`, the verifier runs **three** checks (all must pass): + +- _Original-existence (anchor) check._ Per-row chain recomputation against the original is **skipped** — those bytes are unrecoverable. The verifier instead reads the covering anchor from `pending_anchor_uploads` (local) or `event_log_anchors` (control-plane) and verifies its daemon Ed25519 `root_signature` over the Merkle root. Anchor absent → `audit_integrity_failed` with `failureMode: 'anchor_missing_for_compacted_range'`. Anchor present but `root_signature` verification fails → `failureMode: 'anchor_signature_invalid'`. +- _Stub-authenticity (stub_signature) check._ The verifier checks `stub_signature` **directly over the canonical byte string stored in `payload`** — the exact bytes the compactor signed and persisted (step 5) — using the same `NodeId`-resolved daemon Ed25519 public key from the participant roster. It does **not** re-canonicalize the stub or reconstruct it from the scalar columns (`compactedAt` and `summary` are not columns; they live only inside the stored projection): verifying the stored bytes verbatim is strictly stronger (it catches **any** byte-level edit, not only semantic ones) and avoids a JCS round-trip at verify time. A `stub_signature` that is absent OR fails to verify on an `audit_stub` row → `audit_integrity_failed` with `failureMode: 'stub_signature_invalid'` (the signature is REQUIRED on every compacted row; its absence is a verification failure, never a skip). +- _Scalar-binding check._ The surviving scalar columns on the `audit_stub` row (`id`, `session_id`, `sequence`, `occurred_at`, `category`, `type`, `actor`) are a denormalized cache that SQL filters (`idx_session_events_type`) and envelope reconstruction read — they are NOT covered by `stub_signature`, which signs only the `payload` projection bytes. The verifier therefore decodes the projection from the just-verified `payload` bytes and asserts each scalar column **byte-equals** its projection counterpart (column `occurred_at` ↔ field `occurredAt`, `session_id` ↔ `sessionId`, and so on; `compactedAt` and `summary` have no scalar column and are exempt). Any divergence → `audit_integrity_failed` with `failureMode: 'stub_scalar_mismatch'`. Without it an actor with at-rest DB write access could edit a scalar column (e.g. `actor`, `type`) while leaving `payload` intact: `stub_signature` and the anchor over the frozen `row_hash` would both still verify, yet a filter or reconstruction reading the scalar column would surface the forged value. The signed `payload` projection is the authoritative source for a compacted row's envelope fields — the scalar columns are trustworthy only once this check passes. + +A range verifies only when every compacted row passes all three checks. For mixed ranges (some uncompacted, some `audit_stub`), the verifier performs per-row chain recomputation on the uncompacted prefix and the three compacted-row checks on the compacted suffix; all must pass for the range as a whole to verify. + +**Tamper-evidence guarantees post-compaction (threat model).** The threat is an actor with write access to the at-rest SQLite database but WITHOUT the daemon's sealed signing key (the daemon itself is the trusted compaction authority). Against that actor: + +- _Edit the visible stub_ (`summary`, `actor`, …) inside `payload`: the modified `payload` bytes no longer match `stub_signature` → `stub_signature_invalid`. (This is the gap the prior anchor-only design missed: the anchor commits to the original `row_hash`, not to the post-compaction stub bytes.) +- _Edit a surviving scalar column_ (`actor`, `type`, `category`, `occurred_at`) while leaving `payload` intact: the scalar no longer equals the signed projection → `stub_scalar_mismatch`. Both `stub_signature` over `payload` and the anchor over the frozen `row_hash` still verify, so the scalar-binding check is the only thing that catches a forged value surfaced through a filter or envelope reconstruction. +- _Replay a valid stub+signature from another row_: the canonical bytes bind `id` + `sequence`, so a row-A signature fails to verify against row-B bytes → `stub_signature_invalid`. +- _Strip the `stub_signature`_ to dodge the check: a NULL `stub_signature` on an `audit_stub` row is itself a failure → `stub_signature_invalid`. +- _Flip `retention_class` `'audit_stub'` → NULL_ to force the row down the live-chain path: per-row recomputation then runs over the current (stub) `payload`, which does not match the frozen `row_hash` → `hash_mismatch`. +- _Flip `retention_class` NULL → `'audit_stub'`_ on a live row to dodge per-row checks: the row has no covering compaction anchor (`anchor_missing_for_compacted_range`) and no `stub_signature` (`stub_signature_invalid`). + +Anchors remain immutable once queued (the durable `pending_anchor_uploads` queue plus the control-plane `event_log_anchors` table hold two independent copies of the signed commitment), so the original-existence proof is independently durable. Stub authenticity is now asserted at **per-row** granularity by `stub_signature`, not merely at anchor-presence granularity. + +The four `failureMode` enum values added for the post-compaction protocol (`'anchor_missing_for_compacted_range'`, `'anchor_signature_invalid'`, `'stub_signature_invalid'`, `'stub_scalar_mismatch'`) are additive-MINOR extensions to the canonical 7-value enum at [§Audit Integrity (`audit_integrity`)](#audit-integrity-audit_integrity) per [ADR-018 §Decision #8](../decisions/018-cross-version-compatibility.md) (MINOR additive enum-value extension is sanctioned), taking the enum to 11 values. **Security amendment (approved spec):** the `stub_signature` per-row commitment was added 2026-05-28 in response to a P1 review finding (PR #124) that the prior anchor-only post-compaction design left the visible stub bytes unauthenticated. A follow-on P1 finding the same date (PR #124 round-5) showed the surviving scalar columns (`category`, `type`, `actor`, `occurred_at`) remained unbound to `stub_signature` — a forged scalar could survive verification — so the per-row **scalar-binding check** above was added (`stub_scalar_mismatch`). Both are recorded here as deliberate amendments to an approved spec, not routine editorial changes. ### Replay Interaction with Compacted Regions @@ -637,6 +674,9 @@ The full `payload`, `pii_payload`, `correlationId`, and `causationId` are remove - [ ] Every run lifecycle transition results in one or more canonical session events. - [ ] A client can recover missed state by replaying events after its last known cursor. - [ ] Approval, membership, and artifact changes are visible in audit history even after payload compaction. +- [ ] Post-compaction range integrity is preserved via the anchor-before-compaction protocol: every compacted range has a covering Merkle anchor with valid daemon signature, and a verifier that observes an `audit_stub` row WITHOUT a covering anchor emits `audit_integrity_failed` with `failureMode: 'anchor_missing_for_compacted_range'`. +- [ ] Post-compaction stub authenticity is preserved per-row: every `audit_stub` row carries a `stub_signature` (Ed25519 over its canonical stub bytes), and a verifier that observes a tampered, replayed, or missing `stub_signature` on an `audit_stub` row emits `audit_integrity_failed` with `failureMode: 'stub_signature_invalid'`. +- [ ] Post-compaction scalar binding is enforced per-row: a verifier that observes an `audit_stub` row whose surviving scalar columns (`category`, `type`, `actor`, `occurred_at`) diverge from the signed `payload` projection emits `audit_integrity_failed` with `failureMode: 'stub_scalar_mismatch'`. ## ADR Triggers diff --git a/docs/specs/007-local-ipc-and-daemon-control.md b/docs/specs/007-local-ipc-and-daemon-control.md index 2711f2f8..48b97e6b 100644 --- a/docs/specs/007-local-ipc-and-daemon-control.md +++ b/docs/specs/007-local-ipc-and-daemon-control.md @@ -71,12 +71,14 @@ This spec covers transport choice, version negotiation, request and stream seman ## Interfaces And Contracts - `DaemonHello` and `DaemonHelloAck` must perform version negotiation. -- `DaemonStatusRead`, `DaemonStart`, `DaemonStop`, and `DaemonRestart` must exist for supervised environments. +- `DaemonStatusRead`, `DaemonStop`, and `DaemonRestart` must exist as IPC methods for supervised environments. Daemon **start** is a supervisor capability realized by **process spawn** — the `ai-sidekicks daemon start` CLI command (and the desktop shell's auto-start per § Default Behavior and § Example Flows) launches the daemon process and awaits `DaemonHelloAck` — NOT a JSON-RPC method on the daemon: a stopped daemon has no IPC server to receive a `daemon.start` call. See [Plan-007 Phase R1/R3](../plans/007-local-ipc-and-daemon-control.md) (`daemon.stop` / `daemon.restart` are R1 IPC handlers; `daemon start` is the R3 CLI process-spawn path, T-007r-3-4). - `LocalSubscriptionConsumer` must support replay-capable event streams where appropriate. - The typed client SDK must expose the same semantic surface to Desktop Shell and CLI callers. The renderer consumes a narrower preload bridge API per [Spec-023 §Trust Stance](./023-desktop-shell-and-renderer.md), not this SDK directly. - See [API Payload Contracts](../architecture/contracts/api-payload-contracts.md) for typed request/response schemas. - See [Error Contracts](../architecture/contracts/error-contracts.md) for error response schemas and error codes. +> **Clarifying amendment (approved spec, 2026-05-28, PR #124).** The prior wording listed `DaemonStart` alongside the IPC methods, which a conformance reader could mis-read as mandating a `daemon.start` JSON-RPC handler. Daemon start is a process-spawn capability (CLI / desktop shell), not an IPC method — clarified above to match § Default Behavior and § Example Flows (both already model start as the shell launching the daemon). No capability change; this records a defect-fix to an approved spec, not a routine editorial change. + ## State And Data Implications - Client cache must not become the daemon's state store.