ADR-009: Authorship Binding in the ζ Decision Trail

Status: Accepted Date: 2026-05-06 Accepted: 2026-05-06 (R84) Round: post-R83 (accepted in R84 ADR sweep) Supersedes: None Superseded by: None


Context

A whole-system code review surfaced Finding #2: the ζ Decision Trail’s SHA-256 hash chain proves what was recorded in what order but does not cryptographically bind who authored each record.

Concretely, computeHash at src/domains/trail/schema.ts:170-188 hashes a 6-field SUBSET:

{id, type, task_id, content, timestamp, prev_hash}

The agent_id field is excluded by design. The schema’s docstring at src/domains/trail/schema.ts:153-159 makes the exclusion explicit:

agent_id is author metadata, not a chain-integrity input. A record rewritten with a corrected agent_id MUST NOT cascade a chain-break through every subsequent record.

The intent — accommodating legitimate authorship corrections without invalidating downstream records — is sound. The structural consequence is that whoever can write to thought_records.agent_id can rewrite authorship without breaking audit_verify_chain. The verifier at src/domains/trail/verifier.ts:131-132 recomputes computeHash(record) and compares to record.hash; a mutated agent_id does not enter that recomputation, so the chain stays “valid” while authorship has been rewritten under it.

Practical attack surfaces:

  • Direct DB write. Anyone with filesystem access to data/colibri.db can UPDATE thought_records SET agent_id = '...' WHERE id = '...'. The chain still verifies.
  • Audit-sink replacement. A future MCP authoring tool, if mis-scoped, could overwrite agent_id on existing rows. The chain still verifies.
  • Future tooling. A correction-by-edit tool (the very pattern the docstring optimizes for) would, in its current form, allow silent re-attribution.

Why this matters. CLAUDE.md §10 states that Colibri is “execution of work + management of intelligence + legitimacy of action”, and that “not only the result matters, but the right to the result”. A chain whose authorship is asserted but not cryptographically bound has a structural gap in the third axis. ADR-006’s executable-meaning contract treats colibri_code: complete for the ζ concept as gated on the contract being verified — and this gap is in that contract. As long as authorship is unbound, ζ cannot honestly graduate to complete, and Phase 2 λ Reputation (which reads agent_id as truth when scoring agent quality) inherits a hole at its data layer.

Authorship is asserted but not cryptographically bound. This ADR picks how to fix it.


Decision

Recommend Option A — include agent_id in the hash (breaking change).

computeHash becomes a 7-field hash over {id, type, task_id, agent_id, content, timestamp, prev_hash}. The schema docstring at :153-159 is rewritten: authorship correction now goes through correction-by-append (a new thought_type="decision" record stating “Record X was authored by Y, not Z”), not through edit-in-place. Edit-in-place was already disallowed by the chain for every other field; Option A simply extends that property to authorship.

Why now, why this option:

  • Migration cost is bounded. At R83 close the largest task chain in data/colibri.db is in the low hundreds of rows. A one-shot recomputation walk per task_id completes in seconds.
  • Security gain is decisive. The legitimacy axis’s third invariant — that the chain bind authorship — is restored. Future λ Reputation reads agent_id from rows whose hash now depends on it.
  • Defers no decision Phase 1.5+ would need to undo. Option B (HMAC signing) introduces per-agent key material that has no delivery story until multi-model lands per ADR-005. Option B can layer on top of Option A later if cross-host external verifiability is ever required.
  • Option C is rejected as documenting acceptance of a hole the legitimacy axis cannot tolerate by Phase 2.

The accept/reject call is the PM’s per docs/architecture/decisions/README.md §”Who can accept ADRs”. Until accepted, this ADR remains Proposed.


Alternatives Considered

Option A — Include agent_id in the hash (breaking change)

Modify computeHash to hash 7 fields: {id, type, task_id, agent_id, content, timestamp, prev_hash}. Every existing thought_record’s stored hash becomes wrong relative to the new function: audit_verify_chain would report all existing chains broken until they are rebuilt.

Cost. A 1-line schema change at src/domains/trail/schema.ts:178-186 (the canonicalized subset). A migration script that walks every task_id, recomputes hash and prev_hash in insertion order, and writes back the rebuilt chain. A test rewrite that re-fixtures the 6-field assumption. The schema migration counter advances by one (007_authorship_binding.sql). Total LOC delta is small; migration runtime is bounded by row count.

Security property. Authorship becomes a chain-integrity input. Any post-hoc mutation of agent_id breaks verification at the mutated row and at every subsequent row in that task chain. This is the strongest of the three options for chain-internal authorship binding.

Key material. None. Hashing is keyless; verification is open to anyone with the chain.

Fit with future λ Reputation. Direct. Phase 2 λ reads agent_id from rows whose hash binds it; reputation scores derived from these rows inherit the binding for free.

Trade-off. Authorship correction now requires inserting a new thought_type="decision" record rather than editing the old. This is consistent with the chain’s existing posture for every other field — content, timestamp, task_id are already immutable-by-chain — and does not introduce new patterns the corpus doesn’t already use.

Option B — Sign records with an agent-scoped key (heavier)

Add a signature field to thought_records. Computed at insert time as HMAC-SHA256(agent_secret, content_hash). The verifier checks both the chain hash AND the signature; rows whose signature does not validate against any registered key are flagged.

Cost. Schema migration adding signature TEXT NULL (legacy rows pass through with NULL signatures and a logged warning). A signing helper module. A verifier extension. A agent_keys table or equivalent for per-agent key material. Rotation policy. None of these is large in isolation; collectively they are a small new subsystem.

Security property. Stronger than Option A in one dimension and weaker in another. Stronger: a mutation of agent_id that does not also produce a valid signature is detectable even if the attacker can also rewrite the chain hash, because they would also need the agent’s secret. Weaker: the verifier now has to trust the key registry; a compromised key registry is a single point of failure.

Key material — flagged as out-of-scope-but-non-trivial. Where do agent_secret values live? File on disk? Environment variable? KMS? Phase 0 is single-host single-process per CLAUDE.md and ADR-005; per-agent keys have no delivery story until Phase 1.5 multi-model lands. This ADR does NOT specify a key-management approach; if Option B is chosen, key management becomes its own ADR. Pretending key management is a one-line addition would be dishonest.

Fit with future λ Reputation. Higher than Option A: λ can directly verify each record was authored by the holder of a specific key, not just attributed by an unauthenticated string. But this fit only materializes once the key-management ADR is also accepted and implemented.

Trade-off. Highest implementation cost. Highest legitimacy story. Best layered ON TOP OF Option A rather than instead of it — Option B without Option A’s chain-integrity binding leaves a chain that proves “this signed record exists in some order” without proving “the signed record is in this order”.

Option C — Accept the gap and document the legitimacy weakness

No code change. Update CLAUDE.md §10 and ADR-006 to acknowledge: “agent_id is asserted but not cryptographically bound.” The legitimacy story explicitly narrows to chain-content integrity, not authorship integrity.

Cost. Documentation only. ~30 lines across CLAUDE.md, ADR-006, and the ζ concept doc.

Security property. None gained. The structural gap remains; only its visibility changes.

Key material. None.

Fit with future λ Reputation. Direct conflict. λ will read agent_id as truth — that’s the assumption it operates under. Documenting the gap as acceptable hard-codes the rule that λ must independently corroborate authorship before scoring, which is a Phase 2 redesign that ADR-009 would have forced. The redesign is more expensive than Option A, and its cost lands at the worst possible time (Phase 2 implementation, not Phase 1 hardening).

Trade-off. Cheapest now; most expensive later. A reasonable choice only if the round budget is genuinely zero.


Consequences

Positive

  • Authorship becomes chain-integrity. Under Option A, mutating agent_id post-insert breaks the chain at that row and every downstream row in the task. The verifier needs no new logic — the existing computeHash recomputation does the work.
  • Correction-by-append is enforced. The chain stops being a maybe-mutable record of authorship and becomes an immutable record. The pattern (“insert a new decision record stating the correction”) is already canonical for content corrections.
  • ζ can graduate to complete. Per ADR-006 the gating predicate “100% of the spec’s public API is implemented” includes the spec’s authorship-binding promise. With Option A the predicate is satisfiable.

Negative

  • All existing chains require migration. Option A’s recomputation pass touches every row. The migration is bounded in cost but is not free, and the migration itself is a moment of vulnerability — a partial run leaves the DB in a state where some rows hash 7 fields and some hash 6.
  • Authorship correction loses convenience. Edit-in-place on agent_id was the docstring’s intended path. Under Option A that path is removed; corrections route through new records, costing rows.
  • Test rewrites. Every existing test that fixtures a computeHash value must regenerate the fixture under the 7-field function. The test count is bounded but real.

Neutral

  • The verifier is unchanged in interface, only in input. verifyChain(records[]) still walks the array and recomputes hashes; the only thing that changes is what computeHash hashes. The MCP tool signature is stable.
  • prev_hash linkage is unchanged. The chain topology stays identical. Only the hash-input width changes.

Chain-validity table

Option Invalidates data/colibri.db chains? Invalidates data/ams.db chains? Migration approach
A — include agent_id in hash Yes. Every existing record’s stored hash becomes wrong relative to the new function. audit_verify_chain would report all chains broken until rebuilt. Yes, same reason. One-shot recomputation pass per task_id: walk in insertion order, recompute hash and prev_hash, write back atomically. Migration 007_authorship_binding.sql bumps user_version to 7. data/ams.db chains MAY instead be archived (frozen) and read-only after migration, since ams.db is heritage per CLAUDE.md §1 and is not the source of truth for any post-R78 work.
B — HMAC signature column No. Hash inputs unchanged; existing rows pass audit_verify_chain as-is. Signature column is NULL for legacy rows; new rows fill it. No, same reason. Schema migration adds signature TEXT NULL. Verifier check is gated on signature IS NOT NULL; legacy rows skip the signature check with a logged warning. Per-agent key material lands in a new agent_keys table. Key-management ADR is a prerequisite.
C — document the gap, no code change No. Zero code change. No, same reason. None. Legitimacy claim narrows to chain-content integrity only.

Implementation

For the recommended Option A:

Source changes

File Change
src/domains/trail/schema.ts computeHash signature widens to include agent_id: string. The internal subset object adds agent_id: record.agent_id. The docstring at lines 153–159 is rewritten: authorship correction goes through thought_type="decision" records, not edit-in-place.
src/domains/trail/repository.ts createThoughtRecord (lines 235–242) passes agent_id: parsed.agent_id into the computeHash call. The repository’s CreateThoughtRecordInput shape is unchanged.
src/domains/trail/verifier.ts No interface change. verifyChain still recomputes computeHash(record) and compares to record.hash; the new computeHash includes agent_id automatically.

Migration

File Change
src/db/migrations/007_authorship_binding.sql One-shot recomputation walk. Iterates each task_id, walks rows in rowid ASC order, recomputes prev_hash and hash under the 7-field rule, writes back. user_version advances to 7. Idempotent: re-running on an already-migrated DB is a no-op.

Tests

File Change
src/__tests__/trail-schema.test.ts Expected-hash fixtures regenerated. New tests covering agent_id mutation breaking the chain.
src/__tests__/domains/trail/repository.test.ts Hash assertions regenerated. New tests for the correction-by-append pattern.
src/__tests__/domains/trail/verifier.test.ts Mutation-detection tests extended to cover agent_id mutation.
src/__tests__/domains/trail/migration-007.test.ts New migration smoke test: insert legacy rows under the 6-field rule, run migration, verify all chains pass verifyChain under the 7-field rule.

Doc updates

File Change
docs/architecture/decisions/ADR-006-executable-meaning.md Footnote in §Implementation noting that ζ’s partial → complete predicate now also requires Option A landed.
docs/3-world/execution/decision-trail.md Concept doc updated to describe the 7-field hash.
CLAUDE.md §7 Writeback section’s chain example may want a note about authorship binding.

If Option B is reversed-into instead: the implementation pivot is signing helpers + key-management ADR. The Option A migration may be retained as a prerequisite or skipped entirely depending on whether B layers on top.

If Option C is reversed-into instead: only the CLAUDE.md §10 + ADR-006 + concept-doc edits land; no source change.


Verification

This decision is verified iff:

  • src/domains/trail/schema.ts:178-186’s subset object contains agent_id: record.agent_id as a literal property.
  • src/__tests__/trail-schema.test.ts contains a regression test that asserts computeHash({...record, agent_id: 'attacker'}) differs from computeHash(record) for the same other 6 fields.
  • npm test -- --testPathPattern trail passes, including the new migration-007 smoke test.
  • data/colibri.db post-migration: every record’s verifyChain result is {valid: true}; select count(*) from thought_records where length(hash) != 64 returns 0.
  • ADR-006’s footnote (or equivalent) cross-references this ADR.

A future Sigma round may treat ADR-009-implementation as a single round and land all changes atomically (schema + migration + verifier-extension + doc + ADR-006-footnote) under a single round seal.


References

  • src/domains/trail/schema.ts:170-188computeHash over the 6-field subset (the gap).
  • src/domains/trail/schema.ts:153-159 — the deliberate-exclusion docstring.
  • src/domains/trail/repository.ts:243-253 — INSERT writing agent_id to row column 4 without binding it cryptographically.
  • src/domains/trail/verifier.ts:119-153verifyChain recomputation loop.
  • src/db/migrations/003_thought_records.sql — table definition; agent_id TEXT NOT NULL at line 33.
  • CLAUDE.md §10 — legitimacy promise (“not only the result matters, but the right to the result”).
  • ADR-006 — executable-meaning contract; partial → complete predicate that this ADR’s outcome interacts with.
  • ADR-005 — multi-model defer; the basis for treating per-agent key management (Option B) as a Phase 1.5+ topic.

post-R83 round. Answers “is the ζ chain authoritative about who authored each record?” — not under the current 6-field hash; Option A is the recommended fix.


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.