From a20b53c99d6691eb8e726fa6f41680071d7a7ee4 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Wed, 6 May 2026 21:06:00 +1200 Subject: [PATCH 1/7] docs(adr-0010): add amendments for OQ2, OQ3, OQ4 resolutions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three amendments to ADR-0010 resolving open design questions: OQ2 — Existing chain migration: Abandon v1 chains on upgrade. No in-place migration or import-chain script. Cost: auditors must preserve old SQLite DBs offline for long-term pre-Phase-2 audit. Rationale: pre-1.0 adoption stage, no production audit dependencies blocking a clean break, and migration logic compounds the Section 3 refactor burden. OQ3 — SDK cutover sequencing: Single-shot PR/release (all SDKs + OpenClaw + mcp-proxy at once). No phased rollout. Rationale: phased introduction creates a mixed-state window (v1 and v2 emitters writing separate chains), violating the daemon's single-unified-chain property. Single-shot on day one ensures all emitters speak the same schema. OQ4 — session_id rule: UUID generated at emitter startup, reused across all tool calls within process lifetime, persistent across daemon reconnects but retired on emitter exit. Uniform rule across all three SDKs. Cardinality: few long-lived sessions per deployment (~1–10 unique IDs/day). Indexing: add non-unique index on Issuer.SessionID for filtering by session. All three are design decisions only; no spec changes, no code changes. The amendments establish normative rules for the Section 3 emitter refactor and SDK major version bump. --- docs/adr/0010-daemon-process-separation.md | 56 ++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/adr/0010-daemon-process-separation.md b/docs/adr/0010-daemon-process-separation.md index 31b57af..50fd315 100644 --- a/docs/adr/0010-daemon-process-separation.md +++ b/docs/adr/0010-daemon-process-separation.md @@ -108,3 +108,59 @@ Phase 1 (#322) implemented `SOCK_STREAM` with a 4-byte big-endian length-prefix ### 2026-05-05: Default socket paths — per-user defaults instead of system paths Phase 1 (#322) defaults to per-user socket paths because MVP has no launchd- or systemd-managed system install yet. macOS uses `$TMPDIR/agentreceipts/events.sock` (the originally-specified `/var/run/agentreceipts/events.sock` is not produced by `daemon.DefaultSocketPath()`, only by explicit configuration); Linux uses `$XDG_RUNTIME_DIR/agentreceipts/events.sock` when that variable is set, falling back to `/run/agentreceipts/events.sock` only when it is not. Both originally-specified system paths can still be selected explicitly via `AGENTRECEIPTS_SOCKET` (or `--socket`) and will become the packaging-managed defaults once launchd / systemd integration lands. The *IPC transport* section above describes the current resolution. + +### 2026-05-06: OQ2 — Existing chain migration policy — abandon old chains + +**Decision:** v1 users have per-emitter SQLite databases. Phase 2 (Section 3, thin-emitter refactor) will abandon existing v1 chains and start a fresh daemon-managed chain at `seq=1`. No in-place migration or `import-chain` script. + +**Rationale:** Agent-Receipts is pre-1.0 with early-stage adoption (solo dev and lab usage). No production audit dependencies exist that would prohibit a clean break, and the cost of migration logic (either in-process DB surgery during daemon startup or a separate import tool) compounds the already-substantial emitter refactor burden of Section 3. + +**Consequences:** +- Auditors must preserve v1 SQLite databases offline if they require long-term audit of pre-Phase-2 events. This is a one-time notice; v2 chains persist continuously under daemon supervision. +- v1 receipt verification becomes impossible post-upgrade (no daemon will hold v1 public keys or databases). +- On daemon startup with a fresh database, the chain starts at `seq=1` with no previous-receipt hash. + +**Spec/code changes:** +- No migration logic in daemon startup. +- No schema migrations or data import tooling in `sdk/go/store`. +- Documentation MUST include a deprecation notice: "v1 in-process receipts are not migrated; preserve offline copies if long-term verification is required." + +### 2026-05-06: OQ3 — SDK cutover sequencing — single-shot release + +**Decision:** All three SDKs (Go, TS, Py), mcp-proxy, and OpenClaw ship in a single PR/release. No phased rollout per channel. + +**Rationale:** Phased cutover introduces a mixed-state window where v1 emitters (in-process signing/storage) and v2 emitters (daemon socket, fire-and-forget) write to two separate chains — breaking the core property of daemon-process-separation: a single unified chain. Single-shot release on day one ensures all emitters speak the same schema. The large PR is one cohesive unit; reviewability is not materially worse than five parallel reviews, and readers see the full story at once. + +**What mixed-state means for chain integrity (the scenario we avoid by choosing single-shot):** +- During a phased cutover, v1 emitters create receipts in their own per-process SQLite DBs (one per process, no shared `seq` space) while v2 emitters send to the daemon socket (one shared chain, monotonic `seq`). +- Auditors query two independent chains: the daemon chain (v2, authoritative for post-cutover) and N orphaned v1 chains (v1, final state at the moment each module upgraded). +- Cross-channel correlation (e.g., "all receipts for this agent session") requires application logic to coalesce results from both chains, violating the single-chain guarantee. +- Phased rollout would surface this as a gap in audit coverage; single-shot eliminates the window. + +**Consequences:** +- Section 3 (thin-emitter refactor) produces one large, multi-module PR (likely 1500+ lines). Structured as one commit per module for clarity, each with test coverage. +- v1 is the final in-process release; v2 is the first daemon-backed release. No intermediate release or beta channel for the cutover itself. + +### 2026-05-06: OQ4 — session_id allocation rule — UUID at startup, persistent across reconnects + +**Decision:** Each emitter process MUST generate a unique `session_id` (UUID v4 or v5) at startup and include it in every frame sent to the daemon. The `session_id` remains constant across daemon reconnects and process-local (lifetime of the emitter process). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary. + +**Rationale:** +- **At startup (not per-run):** Agents invoke multiple tool calls within a single logical session. One `session_id` per agent-run would fragment a logical audit session into N receipts with N identifiers. Grouping by emitter-process lifetime naturally clusters tool calls. +- **Persistent across daemon reconnect:** The emitter holds the session_id in memory. If the daemon restarts or the network drops and reconnects, the emitter retransmits with the same `session_id`, keeping receipts logically grouped. No persist-to-disk is required (the session_id dies with the emitter process). +- **Uniform across SDKs:** All three SDK emitters (Go, TS, Py) and integration points (mcp-proxy, OpenClaw) initialize `session_id` at construction time; never generate a new one per emit(). + +**Cardinality and indexing:** +- **Expected cardinality:** Few long-lived sessions per deployment. An agent session lasts minutes to hours; an emitter process lasts the lifetime of the agent (or MCP proxy). Database sees ~1–10 unique `session_id` values per day in typical usage. +- **Index:** Add a non-unique index on `Issuer.SessionID` in the receipts table to support queries like `SELECT * FROM receipts WHERE issuer_session_id = ?`. + +**Normative spec line:** +> "Each emitter process MUST generate a unique `session_id` (UUID) at startup and include it in every frame sent to the daemon. The `session_id` remains constant across daemon reconnects, process-local (survives only the lifetime of the emitter process). The daemon makes no guarantee that `session_id` values are unique across deployments or across time, only that it records the value faithfully." + +**SDK author guideline:** +> "Initialize `session_id` once per emitter/SDK instance at construction time using a UUID (v4 or v5). Do not generate a new session_id on each emit(). Reuse the same session_id across all tool calls and daemon reconnects within the process lifetime. No persistence to disk is required." + +**Spec/code changes:** +- All three SDKs emit the same `session_id` for their process lifetime; no SDK-specific logic. +- Daemon: no new logic (session_id is already captured in `receipt.Issuer.SessionID`). Add the `session_id` index on the next SQLite schema version. +- Agent-receipts verify CLI can filter by session_id (e.g., `--session `), building on the index. From 145cd2d8b419cecbd422cc36dac046d8ffc45a42 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Wed, 6 May 2026 21:22:56 +1200 Subject: [PATCH 2/7] docs(adr-0010): address Copilot review on session_id scope and indexing - Line 53 (Schema split): Clarify that session_id is generated by emitter at startup and persists for emitter-process lifetime (not per-agent-run), with detailed allocation rule in OQ4 amendment. - Line 155 (OQ4 Cardinality and indexing): Clarify that schema extraction and indexing of session_id are deferred to Phase 2 Section 3, not blocking the design decision. For now, session_id is only in the receipt JSON. - Line 166 (OQ4 Spec/code changes): Update to note that CLI filtering is deferred pending the schema extraction. Resolves Copilot comments on inconsistent session_id scoping and unclear indexing strategy. --- docs/adr/0010-daemon-process-separation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/adr/0010-daemon-process-separation.md b/docs/adr/0010-daemon-process-separation.md index 50fd315..6be29ab 100644 --- a/docs/adr/0010-daemon-process-separation.md +++ b/docs/adr/0010-daemon-process-separation.md @@ -50,7 +50,7 @@ Emitter sends the minimum faithful representation: - `v` (schema version) - `ts_emit` (RFC 3339, advisory) -- `session_id` (UUID, scopes one agent run) +- `session_id` (UUID, generated by emitter at startup, persists for emitter-process lifetime; detailed allocation rule in the *Amendments* section, OQ4) - `channel` (`openclaw` | `mcp_proxy` | `sdk` | ...) - `tool` (`{ server, name }` for MCP; equivalent shape for other channels) - `input`, `output`, `error` (raw, no normalisation) @@ -152,7 +152,7 @@ Phase 1 (#322) defaults to per-user socket paths because MVP has no launchd- or **Cardinality and indexing:** - **Expected cardinality:** Few long-lived sessions per deployment. An agent session lasts minutes to hours; an emitter process lasts the lifetime of the agent (or MCP proxy). Database sees ~1–10 unique `session_id` values per day in typical usage. -- **Index:** Add a non-unique index on `Issuer.SessionID` in the receipts table to support queries like `SELECT * FROM receipts WHERE issuer_session_id = ?`. +- **Indexing strategy:** Phase 2 will extract `session_id` into a dedicated (or generated) column and add a non-unique index to support efficient queries like `SELECT * FROM receipts WHERE session_id = ?`. For now, session_id is only in the receipt JSON; extraction is deferred to the Section 3 schema evolution. **Normative spec line:** > "Each emitter process MUST generate a unique `session_id` (UUID) at startup and include it in every frame sent to the daemon. The `session_id` remains constant across daemon reconnects, process-local (survives only the lifetime of the emitter process). The daemon makes no guarantee that `session_id` values are unique across deployments or across time, only that it records the value faithfully." @@ -162,5 +162,5 @@ Phase 1 (#322) defaults to per-user socket paths because MVP has no launchd- or **Spec/code changes:** - All three SDKs emit the same `session_id` for their process lifetime; no SDK-specific logic. -- Daemon: no new logic (session_id is already captured in `receipt.Issuer.SessionID`). Add the `session_id` index on the next SQLite schema version. -- Agent-receipts verify CLI can filter by session_id (e.g., `--session `), building on the index. +- Daemon: no new logic (session_id is already captured in `receipt.Issuer.SessionID`). Schema extraction and indexing deferred to Section 3 (Phase 2). +- Agent-receipts verify CLI can filter by session_id (e.g., `--session `) once the schema extraction lands. From bd304929b7e588a79fc86aeb30565ffeff2a96f3 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Wed, 6 May 2026 22:13:42 +1200 Subject: [PATCH 3/7] docs(adr-0010): address Copilot review on OQ2 and OQ4 design details OQ2 (existing chain migration): - Clarify that v2 daemon/emitters use new default DB/key paths to prevent accidental resume of v1 chains. Operators managing v1/v2 coexistence must keep DBs in separate directories. - Fix consequences: v1 receipts remain cryptographically verifiable offline with preserved v1 DB + public key. v2 tooling will not verify/import v1 chains, not that verification is cryptographically impossible. OQ4 (session_id allocation): - Update normative spec and SDK guideline to accommodate channels with upstream session identifiers (e.g., claude_code_hook forwards Claude Code's session_id per ADR-0013). Flexible rule: forward host-provided session_id if available, otherwise generate UUID v4 at startup. - Specify UUID v4 explicitly (v5 is deterministic and requires namespace). Resolves Copilot comments on OQ2 DB path operational rule, OQ2 v1 verification wording, and OQ4 conflict with ADR-0013. --- docs/adr/0010-daemon-process-separation.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/adr/0010-daemon-process-separation.md b/docs/adr/0010-daemon-process-separation.md index 6be29ab..5e47bde 100644 --- a/docs/adr/0010-daemon-process-separation.md +++ b/docs/adr/0010-daemon-process-separation.md @@ -111,13 +111,13 @@ Phase 1 (#322) defaults to per-user socket paths because MVP has no launchd- or ### 2026-05-06: OQ2 — Existing chain migration policy — abandon old chains -**Decision:** v1 users have per-emitter SQLite databases. Phase 2 (Section 3, thin-emitter refactor) will abandon existing v1 chains and start a fresh daemon-managed chain at `seq=1`. No in-place migration or `import-chain` script. +**Decision:** v1 users have per-emitter SQLite databases. Phase 2 (Section 3, thin-emitter refactor) will abandon existing v1 chains and start a fresh daemon-managed chain at `seq=1`. No in-place migration or `import-chain` script. The daemon and emitters use new default DB/key paths (separate from v1) to ensure accidental resume does not happen; operators who want to preserve v1 chains for verification must keep v1 and v2 DBs in separate directories. **Rationale:** Agent-Receipts is pre-1.0 with early-stage adoption (solo dev and lab usage). No production audit dependencies exist that would prohibit a clean break, and the cost of migration logic (either in-process DB surgery during daemon startup or a separate import tool) compounds the already-substantial emitter refactor burden of Section 3. **Consequences:** -- Auditors must preserve v1 SQLite databases offline if they require long-term audit of pre-Phase-2 events. This is a one-time notice; v2 chains persist continuously under daemon supervision. -- v1 receipt verification becomes impossible post-upgrade (no daemon will hold v1 public keys or databases). +- Auditors must preserve v1 SQLite databases and matching public keys offline if they require long-term audit of pre-Phase-2 events. V1 receipts remain cryptographically verifiable with those artifacts; v2 tooling and daemon will not verify or import v1 chains. +- v1 chains are not resumed on v2 daemon startup (separate DB/key paths prevent accidental coexistence). - On daemon startup with a fresh database, the chain starts at `seq=1` with no previous-receipt hash. **Spec/code changes:** @@ -155,10 +155,10 @@ Phase 1 (#322) defaults to per-user socket paths because MVP has no launchd- or - **Indexing strategy:** Phase 2 will extract `session_id` into a dedicated (or generated) column and add a non-unique index to support efficient queries like `SELECT * FROM receipts WHERE session_id = ?`. For now, session_id is only in the receipt JSON; extraction is deferred to the Section 3 schema evolution. **Normative spec line:** -> "Each emitter process MUST generate a unique `session_id` (UUID) at startup and include it in every frame sent to the daemon. The `session_id` remains constant across daemon reconnects, process-local (survives only the lifetime of the emitter process). The daemon makes no guarantee that `session_id` values are unique across deployments or across time, only that it records the value faithfully." +> "Each emitter process MUST provide a stable `session_id` (UUID) for its logical session. If the host or parent process provides a session identifier (e.g., Claude Code's session ID, an agent-loop context ID), forward it unchanged. Otherwise, generate a new UUID v4 at emitter startup. The `session_id` remains constant across daemon reconnects, process-local (survives only the lifetime of the emitter process). The daemon makes no guarantee that `session_id` values are unique across deployments or across time, only that it records the value faithfully." **SDK author guideline:** -> "Initialize `session_id` once per emitter/SDK instance at construction time using a UUID (v4 or v5). Do not generate a new session_id on each emit(). Reuse the same session_id across all tool calls and daemon reconnects within the process lifetime. No persistence to disk is required." +> "Initialize `session_id` once per emitter/SDK instance at construction time. If the host provides a session identifier, use it; otherwise generate a new UUID v4. Do not generate a new session_id on each emit(). Reuse the same session_id across all tool calls and daemon reconnects within the process lifetime. No persistence to disk is required." **Spec/code changes:** - All three SDKs emit the same `session_id` for their process lifetime; no SDK-specific logic. From 0e01ebf2bf3ecc6209e4cc29af60d2261eb4c190 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Wed, 6 May 2026 22:24:06 +1200 Subject: [PATCH 4/7] docs(adr-0010): fix 4 Copilot review issues on consistency and clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Line 5 (Status): Update amendment date to include 2026-05-06 for OQ2–OQ4 - Line 146 (OQ4 Decision): Change UUID version to v4 only (was inconsistently v4 or v5), and update decision to reflect forward-or-generate rule (not just generate). Also change scope from process-local to instance-local for clarity. - PR description: Clarify that v1 receipts remain cryptographically verifiable offline with preserved artifacts; v2 tooling simply won't verify them (not that verification becomes impossible). Align with amendment wording. - Lines 149–154 (OQ4 Rationale/Cardinality): Change process-scope language to instance-scope; clarify that multiple SDK instances in same process should coordinate on a single session_id; update cardinality description to note typical one-instance-per-run usage. Resolves Copilot issues on UUID inconsistency, scope ambiguity, status line date, and v1 verification contradiction. --- docs/adr/0010-daemon-process-separation.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/adr/0010-daemon-process-separation.md b/docs/adr/0010-daemon-process-separation.md index 5e47bde..831d961 100644 --- a/docs/adr/0010-daemon-process-separation.md +++ b/docs/adr/0010-daemon-process-separation.md @@ -2,7 +2,7 @@ ## Status -Accepted (2026-05-03), amended 2026-05-05 (see *Amendments*) +Accepted (2026-05-03), amended 2026-05-05 and 2026-05-06 (see *Amendments*) ## Implementation status @@ -143,15 +143,15 @@ Phase 1 (#322) defaults to per-user socket paths because MVP has no launchd- or ### 2026-05-06: OQ4 — session_id allocation rule — UUID at startup, persistent across reconnects -**Decision:** Each emitter process MUST generate a unique `session_id` (UUID v4 or v5) at startup and include it in every frame sent to the daemon. The `session_id` remains constant across daemon reconnects and process-local (lifetime of the emitter process). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary. +**Decision:** Each emitter process MUST provide a stable `session_id` (UUID v4) for its logical session. If the host provides a session identifier, forward it unchanged. Otherwise, generate a new UUID v4 at startup. The `session_id` remains constant across daemon reconnects and instance-local (lifetime of the emitter instance). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary. **Rationale:** -- **At startup (not per-run):** Agents invoke multiple tool calls within a single logical session. One `session_id` per agent-run would fragment a logical audit session into N receipts with N identifiers. Grouping by emitter-process lifetime naturally clusters tool calls. -- **Persistent across daemon reconnect:** The emitter holds the session_id in memory. If the daemon restarts or the network drops and reconnects, the emitter retransmits with the same `session_id`, keeping receipts logically grouped. No persist-to-disk is required (the session_id dies with the emitter process). -- **Uniform across SDKs:** All three SDK emitters (Go, TS, Py) and integration points (mcp-proxy, OpenClaw) initialize `session_id` at construction time; never generate a new one per emit(). +- **At startup (not per-run):** Agents invoke multiple tool calls within a single logical session. One `session_id` per agent-run would fragment a logical audit session into N receipts with N identifiers. Grouping by emitter-instance lifetime naturally clusters tool calls. +- **Persistent across daemon reconnect:** The emitter holds the session_id in memory. If the daemon restarts or the network drops and reconnects, the emitter retransmits with the same `session_id`, keeping receipts logically grouped. No persist-to-disk is required (the session_id dies with the emitter instance). +- **Uniform across SDKs:** All three SDK emitters (Go, TS, Py) and integration points (mcp-proxy, OpenClaw) initialize `session_id` once at construction time; never generate a new one per emit(). If multiple SDK instances exist in the same process, they should share or coordinate on a single session_id. **Cardinality and indexing:** -- **Expected cardinality:** Few long-lived sessions per deployment. An agent session lasts minutes to hours; an emitter process lasts the lifetime of the agent (or MCP proxy). Database sees ~1–10 unique `session_id` values per day in typical usage. +- **Expected cardinality:** Few long-lived sessions per deployment. An agent run typically has one emitter instance (hence one session_id), lasting minutes to hours. Database sees ~1–10 unique `session_id` values per day in typical usage. - **Indexing strategy:** Phase 2 will extract `session_id` into a dedicated (or generated) column and add a non-unique index to support efficient queries like `SELECT * FROM receipts WHERE session_id = ?`. For now, session_id is only in the receipt JSON; extraction is deferred to the Section 3 schema evolution. **Normative spec line:** From 3a5d250cadf9313c6db5fa35d09e4add823d5365 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Thu, 7 May 2026 07:20:04 +1200 Subject: [PATCH 5/7] docs(adr-0010): change session_id type to opaque string Resolve Copilot's type consistency issue: session_id was defined as UUID in Schema split and OQ4 Decision, but OQ4 also allows forwarding host-provided IDs that may not be UUIDs. Solution: Define session_id as an opaque string. When emitter-generated, recommend UUID v4 (future-proof, matches current systems like Claude Code). When host-provided, forward unchanged (forward-compatible for channels with custom session identifier formats). Changes: - Line 53 (Schema split): opaque string, UUID v4 recommended - Line 146 (OQ4 Decision): opaque string, forward or generate v4 - Lines 158, 161 (normative spec + SDK guideline): opaque string, UUID v4 recommended for generated IDs This maintains type safety for generated IDs while remaining flexible for host-provided identifiers. --- docs/adr/0010-daemon-process-separation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/adr/0010-daemon-process-separation.md b/docs/adr/0010-daemon-process-separation.md index 831d961..b82df1a 100644 --- a/docs/adr/0010-daemon-process-separation.md +++ b/docs/adr/0010-daemon-process-separation.md @@ -50,7 +50,7 @@ Emitter sends the minimum faithful representation: - `v` (schema version) - `ts_emit` (RFC 3339, advisory) -- `session_id` (UUID, generated by emitter at startup, persists for emitter-process lifetime; detailed allocation rule in the *Amendments* section, OQ4) +- `session_id` (opaque string; UUID v4 recommended when emitter-generated; persists for emitter-instance lifetime; detailed allocation rule in the *Amendments* section, OQ4) - `channel` (`openclaw` | `mcp_proxy` | `sdk` | ...) - `tool` (`{ server, name }` for MCP; equivalent shape for other channels) - `input`, `output`, `error` (raw, no normalisation) @@ -143,7 +143,7 @@ Phase 1 (#322) defaults to per-user socket paths because MVP has no launchd- or ### 2026-05-06: OQ4 — session_id allocation rule — UUID at startup, persistent across reconnects -**Decision:** Each emitter process MUST provide a stable `session_id` (UUID v4) for its logical session. If the host provides a session identifier, forward it unchanged. Otherwise, generate a new UUID v4 at startup. The `session_id` remains constant across daemon reconnects and instance-local (lifetime of the emitter instance). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary. +**Decision:** Each emitter process MUST provide a stable `session_id` (opaque string) for its logical session. If the host provides a session identifier, forward it unchanged. Otherwise, generate a new UUID v4 at startup (recommended form for generated IDs). The `session_id` remains constant across daemon reconnects and instance-local (lifetime of the emitter instance). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary. **Rationale:** - **At startup (not per-run):** Agents invoke multiple tool calls within a single logical session. One `session_id` per agent-run would fragment a logical audit session into N receipts with N identifiers. Grouping by emitter-instance lifetime naturally clusters tool calls. @@ -155,10 +155,10 @@ Phase 1 (#322) defaults to per-user socket paths because MVP has no launchd- or - **Indexing strategy:** Phase 2 will extract `session_id` into a dedicated (or generated) column and add a non-unique index to support efficient queries like `SELECT * FROM receipts WHERE session_id = ?`. For now, session_id is only in the receipt JSON; extraction is deferred to the Section 3 schema evolution. **Normative spec line:** -> "Each emitter process MUST provide a stable `session_id` (UUID) for its logical session. If the host or parent process provides a session identifier (e.g., Claude Code's session ID, an agent-loop context ID), forward it unchanged. Otherwise, generate a new UUID v4 at emitter startup. The `session_id` remains constant across daemon reconnects, process-local (survives only the lifetime of the emitter process). The daemon makes no guarantee that `session_id` values are unique across deployments or across time, only that it records the value faithfully." +> "Each emitter process MUST provide a stable `session_id` (opaque string) for its logical session. If the host or parent process provides a session identifier (e.g., Claude Code's session ID, an agent-loop context ID), forward it unchanged. Otherwise, generate a new UUID v4 at emitter startup (recommended form for generated IDs). The `session_id` remains constant across daemon reconnects, instance-local (survives only the lifetime of the emitter instance). The daemon makes no guarantee that `session_id` values are unique across deployments or across time, only that it records the value faithfully." **SDK author guideline:** -> "Initialize `session_id` once per emitter/SDK instance at construction time. If the host provides a session identifier, use it; otherwise generate a new UUID v4. Do not generate a new session_id on each emit(). Reuse the same session_id across all tool calls and daemon reconnects within the process lifetime. No persistence to disk is required." +> "Initialize `session_id` once per emitter/SDK instance at construction time. If the host provides a session identifier, use it unchanged; otherwise generate a new UUID v4. Do not generate a new session_id on each emit(). Reuse the same session_id across all tool calls and daemon reconnects within the instance lifetime. No persistence to disk is required." **Spec/code changes:** - All three SDKs emit the same `session_id` for their process lifetime; no SDK-specific logic. From 24a146f7a0cab5a357ba648207ec861a725b0c87 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Thu, 7 May 2026 07:56:12 +1200 Subject: [PATCH 6/7] docs(adr-0010): clarify OQ2 schema migration language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Line 125: Change "No schema migrations or data import tooling in sdk/go/store" to "No v1→v2 chain migration or data import tooling in sdk/go/store (schema migrations for other purposes are unaffected)". The original wording sounded like the store avoids all migrations, when the intent is specifically to avoid v1-to-v2 chain migration logic. Store schema migrations (like migrateToolName) continue unchanged. --- docs/adr/0010-daemon-process-separation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0010-daemon-process-separation.md b/docs/adr/0010-daemon-process-separation.md index b82df1a..2fd2aa2 100644 --- a/docs/adr/0010-daemon-process-separation.md +++ b/docs/adr/0010-daemon-process-separation.md @@ -122,7 +122,7 @@ Phase 1 (#322) defaults to per-user socket paths because MVP has no launchd- or **Spec/code changes:** - No migration logic in daemon startup. -- No schema migrations or data import tooling in `sdk/go/store`. +- No v1→v2 chain migration or data import tooling in `sdk/go/store` (schema migrations for other purposes are unaffected). - Documentation MUST include a deprecation notice: "v1 in-process receipts are not migrated; preserve offline copies if long-term verification is required." ### 2026-05-06: OQ3 — SDK cutover sequencing — single-shot release From ff066cf99ecc3999c270baf93dd5e8fe0210f26a Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Thu, 7 May 2026 08:14:55 +1200 Subject: [PATCH 7/7] docs: fix final Copilot review issues (capitalization, grammar, status, wording) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0010 fixes: - Line 116 (OQ2 rationale): Change "Agent-Receipts" to "agent-receipts" for consistency with rest of doc. - Line 146 (OQ4 Decision): Fix grammar "and instance-local" → "and is instance-local". - Line 119 (OQ2 Consequences): Clarify that v2 daemon does not automatically resume/import v1 chains (separate DB paths prevent coexistence), not that cryptographic verification is impossible. V1 receipts remain verifiable offline with preserved artifacts. - Line 149 (OQ4 Rationale): Rephrase "One session_id per agent-run..." to "Generating per tool call would fragment..." for clarity. Make the constraint about instance-lifetime obviously correct, not backwards. Status updates (ADR-0010 is now Accepted, Phase 1 shipped): - docs/adr/README.md line 26: Change ADR-0010 status from Proposed → Accepted - docs/threat-model.md line 3: Update "Both ADRs are Proposed" to "ADR-0010 Accepted, ADR-0015 Proposed" to reflect current status. - docs/threat-model.md line 130: Update roadmap table to "Accepted; Phase 1 shipped, Phase 2+ pending" for ADR-0010. Resolves all 5 Copilot review issues from latest pass. --- docs/adr/0010-daemon-process-separation.md | 8 ++++---- docs/adr/README.md | 2 +- docs/threat-model.md | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/adr/0010-daemon-process-separation.md b/docs/adr/0010-daemon-process-separation.md index 2fd2aa2..857dd83 100644 --- a/docs/adr/0010-daemon-process-separation.md +++ b/docs/adr/0010-daemon-process-separation.md @@ -113,10 +113,10 @@ Phase 1 (#322) defaults to per-user socket paths because MVP has no launchd- or **Decision:** v1 users have per-emitter SQLite databases. Phase 2 (Section 3, thin-emitter refactor) will abandon existing v1 chains and start a fresh daemon-managed chain at `seq=1`. No in-place migration or `import-chain` script. The daemon and emitters use new default DB/key paths (separate from v1) to ensure accidental resume does not happen; operators who want to preserve v1 chains for verification must keep v1 and v2 DBs in separate directories. -**Rationale:** Agent-Receipts is pre-1.0 with early-stage adoption (solo dev and lab usage). No production audit dependencies exist that would prohibit a clean break, and the cost of migration logic (either in-process DB surgery during daemon startup or a separate import tool) compounds the already-substantial emitter refactor burden of Section 3. +**Rationale:** agent-receipts is pre-1.0 with early-stage adoption (solo dev and lab usage). No production audit dependencies exist that would prohibit a clean break, and the cost of migration logic (either in-process DB surgery during daemon startup or a separate import tool) compounds the already-substantial emitter refactor burden of Section 3. **Consequences:** -- Auditors must preserve v1 SQLite databases and matching public keys offline if they require long-term audit of pre-Phase-2 events. V1 receipts remain cryptographically verifiable with those artifacts; v2 tooling and daemon will not verify or import v1 chains. +- Auditors must preserve v1 SQLite databases and matching public keys offline if they require long-term audit of pre-Phase-2 events. V1 receipts remain cryptographically verifiable with those artifacts; v2 daemon does not automatically resume or import v1 chains (separate DB/key paths prevent accidental coexistence). - v1 chains are not resumed on v2 daemon startup (separate DB/key paths prevent accidental coexistence). - On daemon startup with a fresh database, the chain starts at `seq=1` with no previous-receipt hash. @@ -143,10 +143,10 @@ Phase 1 (#322) defaults to per-user socket paths because MVP has no launchd- or ### 2026-05-06: OQ4 — session_id allocation rule — UUID at startup, persistent across reconnects -**Decision:** Each emitter process MUST provide a stable `session_id` (opaque string) for its logical session. If the host provides a session identifier, forward it unchanged. Otherwise, generate a new UUID v4 at startup (recommended form for generated IDs). The `session_id` remains constant across daemon reconnects and instance-local (lifetime of the emitter instance). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary. +**Decision:** Each emitter process MUST provide a stable `session_id` (opaque string) for its logical session. If the host provides a session identifier, forward it unchanged. Otherwise, generate a new UUID v4 at startup (recommended form for generated IDs). The `session_id` remains constant across daemon reconnects and is instance-local (lifetime of the emitter instance). The daemon records `session_id` faithfully; verifiers MUST treat it as an advisory grouping hint, not a cryptographic boundary. **Rationale:** -- **At startup (not per-run):** Agents invoke multiple tool calls within a single logical session. One `session_id` per agent-run would fragment a logical audit session into N receipts with N identifiers. Grouping by emitter-instance lifetime naturally clusters tool calls. +- **At startup (not per-tool-call):** Agents invoke multiple tool calls within a single logical session. Generating a new `session_id` per tool call would fragment a logical agent session into N receipts with N identifiers. Grouping by emitter-instance lifetime (one session_id for all tool calls within the instance's lifetime) naturally clusters them. - **Persistent across daemon reconnect:** The emitter holds the session_id in memory. If the daemon restarts or the network drops and reconnects, the emitter retransmits with the same `session_id`, keeping receipts logically grouped. No persist-to-disk is required (the session_id dies with the emitter instance). - **Uniform across SDKs:** All three SDK emitters (Go, TS, Py) and integration points (mcp-proxy, OpenClaw) initialize `session_id` once at construction time; never generate a new one per emit(). If multiple SDK instances exist in the same process, they should share or coordinate on a single session_id. diff --git a/docs/adr/README.md b/docs/adr/README.md index 8f6ab55..b46e63e 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -23,7 +23,7 @@ Use `0000-template.md` as the starting point for new ADRs. Name each new ADR fil | [ADR-0007](0007-did-method-strategy.md) | DID Method Strategy | Proposed | | [ADR-0008](0008-response-hashing-and-chain-completeness.md) | Response Hashing and Chain Completeness | Accepted | | [ADR-0009](0009-canonicalization-and-schema-consistency.md) | Canonicalisation Profile and VC Field Name Commitment | Accepted | -| [ADR-0010](0010-daemon-process-separation.md) | Daemon Process Separation for Signing and Storage | Proposed | +| [ADR-0010](0010-daemon-process-separation.md) | Daemon Process Separation for Signing and Storage | Accepted | | [ADR-0011](0011-zod-for-runtime-validation.md) | Zod for runtime schema validation | Accepted | | [ADR-0012](0012-payload-disclosure-policy.md) | Payload Disclosure Policy (`parameterDisclosure`) | Proposed | | [ADR-0013](0013-claude-code-hook-channel.md) | claude_code_hook Emission Channel | Proposed | diff --git a/docs/threat-model.md b/docs/threat-model.md index de3194e..ef66748 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -1,6 +1,6 @@ # Threat Model -**Status:** Forward-looking. The body of this document describes the v2 architecture introduced by [ADR-0010](adr/0010-daemon-process-separation.md) (daemon process separation) and [ADR-0015](adr/0015-key-rotation-byok-anchoring.md) (key rotation, BYOK, external anchoring). Both ADRs are Proposed at the time of writing. The shipped v1 architecture differs in load-bearing ways; those differences are captured in [v1 caveats (current state)](#v1-caveats-current-state) below. +**Status:** Forward-looking. The body of this document describes the v2 architecture introduced by [ADR-0010](adr/0010-daemon-process-separation.md) (daemon process separation, Accepted) and [ADR-0015](adr/0015-key-rotation-byok-anchoring.md) (key rotation, BYOK, external anchoring, Proposed). The shipped v1 architecture differs in load-bearing ways; those differences are captured in [v1 caveats (current state)](#v1-caveats-current-state) below. **Audience:** Security leads evaluating Agent Receipts for their environment, white-hat reviewers probing the system, and compliance teams writing audit narratives. The intent is that trust assumptions are stated upfront — not discovered. @@ -127,7 +127,7 @@ The v1-to-v2 transition is a hard breaking change for emitters, by deliberate de | Concern | ADR | Issue | Status | |---|---|---|---| -| Daemon process separation (signing/storage isolation from agent) | [ADR-0010](adr/0010-daemon-process-separation.md) | [#236](https://github.com/agent-receipts/ar/issues/236) | Proposed; implementation pending | +| Daemon process separation (signing/storage isolation from agent) | [ADR-0010](adr/0010-daemon-process-separation.md) | [#236](https://github.com/agent-receipts/ar/issues/236) | Accepted; Phase 1 shipped, Phase 2+ pending | | Key rotation, BYOK, external anchoring | [ADR-0015](adr/0015-key-rotation-byok-anchoring.md) | [#307](https://github.com/agent-receipts/ar/issues/307) (PR [#319](https://github.com/agent-receipts/ar/pull/319)) | Proposed; implementation phased (rotation anchoring → checkpoint anchoring) | | DID-based identity for issuer/key resolution | [ADR-0007](adr/0007-did-method-strategy.md) | [#46](https://github.com/agent-receipts/ar/issues/46) | Proposed | | Algorithm agility (PQ-ready signing) | — (`KeySource` interface in ADR-0015 is algorithm-agnostic by design) | [#32](https://github.com/agent-receipts/ar/issues/32) | Tracked |