docs: sharpen the attribution guarantee to a byte<->activation-key binding
tracker-policy-pr / check-pr (pull_request) Successful in 6s

Revised per PR #480 (#5607 owner clarification + #5608 codex resolution;
#5612 directs the update):

- The audit row no longer implies upstream observation or agent-only
  authorship. Reworded the guarantee: the row cryptographically binds
  commit bytes (control-plane-RECOMPUTED SHA) to access to the activation
  signing key, and binds that key to control-plane-owned activation
  metadata. An agent can sign arbitrary contents but cannot verify as a
  different activation or choose the recorded metadata.
- Control plane accepts gateway-delivered opaque bytes, independently
  recomputes the Git object ID, verifies the embedded signature against
  the activation key, and stamps its own metadata. Trusts no gateway
  SHA/key/verdict/metadata. No upstream fetch.
- Purged overclaims: removed "a compromised gateway cannot fabricate an
  audit binding" (the sidecar holds the signing capability, so it can —
  and that's acceptable under the intended guarantee), plus "accepted
  push" / "introduced upstream" framing.
- Resolved the control-plane-transport open question in-PRD (was left
  open; codex asked to resolve): transport is gateway bytes +
  recompute + verify; mirror-read is no stronger.

Issue: #423

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 02:22:35 +00:00
parent 245f258f20
commit 010e253d66
+110 -67
View File
@@ -8,15 +8,18 @@
## Summary ## Summary
Give each bottled agent a **per-activation signing key** so that every commit it Give each bottled agent a **per-activation signing key** so that every commit it
introduces is signed in the git-gate trust boundary (outside the bottle) and produces is signed in the git-gate trust boundary (outside the bottle) and
recorded in bot-bottle's own **host-owned audit store**, which is the portable recorded in bot-bottle's own **host-owned audit store**, which is the portable
source of truth binding each signed commit to a bottle, host, manifest, agent, source of truth. Each row cryptographically binds a commit's bytes (and their
activation interval, and retained public key — along with the commit's *claimed* control-plane-recomputed SHA) to **access to that activation's signing key**, and
author. The gate mints a short-lived Ed25519 key at spin-up, holds the private binds the key to control-plane-owned activation metadata — bottle, host,
half in the sidecar `ssh-agent`, and forwards only `SSH_AUTH_SOCK` into the manifest, agent, activation interval, retained public key — plus the commit's
bottle. The gate rejects any newly-introduced commit that is not signed by the *claimed* author. The gate mints a short-lived Ed25519 key at spin-up, holds the
activation key; the **control plane** independently re-verifies each signature private half in the sidecar `ssh-agent`, and forwards only `SSH_AUTH_SOCK` into
before recording attribution. the bottle. The gate rejects any commit it forwards that is not signed by the
activation key; separately, the **control plane** independently recomputes each
commit's object ID and verifies its signature before recording attribution — it
never trusts a SHA, key, or verdict asserted by the gate.
This PRD deliberately does **not** enforce or vouch author/committer identity. This PRD deliberately does **not** enforce or vouch author/committer identity.
Author/committer name/email are recorded as claims carried inside the signed Author/committer name/email are recorded as claims carried inside the signed
@@ -43,26 +46,38 @@ Successor to:
The crisp property this feature provides: The crisp property this feature provides:
> Every new commit introduced through this bottle's gate is **signed by the > The **host-owned audit store** binds a set of commit bytes — whose Git object
> activation key**, and the **host-owned audit store** — after the control plane > ID the control plane **recomputes** itself — to **access to this activation's
> independently verifies that signature — records it, binding the SHA to the > signing key**, and binds that key to **control-plane-owned activation
> bottle, host, manifest, agent, activation interval, and retained public key. > metadata**: bottle, host, manifest, agent, activation interval, retained public
> The commit's author/committer identity is **recorded as a claim**, not enforced > key. An agent may author and sign arbitrary commit contents, but it cannot make
> or vouched. The forge remains only the repository transport/capability layer. > that signature verify as a *different* activation, and it cannot choose the
> activation metadata the control plane records. The commit's author/committer
> identity is **recorded as a claim**, not enforced or vouched. The forge remains
> only the repository transport/capability layer.
What the signature does and does not prove (issue #423, comment #5554): What this does and does not prove (issue #423, comments #5554 / #5607 / #5608):
- The signature proves **bottle/activation provenance**: the commit was created - It proves **access to activation *Y*'s signing key**: whoever assembled these
with access to activation *Y*'s signing key, i.e. inside this bottle during commit bytes could sign with that key. Recomputing the object ID and verifying
this activation. Independent verification binds SHA *X* to activation *Y*. the embedded signature binds the SHA to activation *Y*, and the control plane's
- The signature does **not** make the author name/email cryptographically own records bind *Y*'s key to *Y*'s metadata.
vouched. The bottle chooses every byte sent through the forwarded agent, so a - It does **not** prove the commit was ever pushed, observed upstream, kept
raw signature could sign `author Mallory <mallory@example>` just as validly. (vs. later reverted or dropped), or produced by the *agent* rather than by any
Author/committer identity is a claim carried inside the signed object and other holder of the activation signing capability (the sidecar itself). The
recorded as-is. store deliberately makes no claim about publication or sole-agent authorship
- The audit record is a cryptographic binding only because the **control plane** — the owner's requirement is attribution of *what manifest/agent/etc. was in
verifies the signature before recording — not because the data-plane gate use when a commit was signed*, not proof of where the commit went (#5607).
asserts it (see **Trust boundary**). - It does **not** make author/committer identity cryptographically vouched. The
bottle chooses every byte sent through the forwarded agent, so a signature over
`author Mallory <mallory@example>` is just as valid. Those fields are a claim
carried inside the signed object and recorded as-is.
- The binding is trustworthy because the **control plane** supplies the SHA (it
recomputes it), the public key, and the activation metadata from its own state
— never from a value the gateway asserts (see **Trust boundary**). The
residual, by design: anything that holds the activation signing capability can
produce commits that attribute to that activation. That is inherent to a
binding on *activation-key access*, not a defect.
## Problem ## Problem
@@ -94,10 +109,12 @@ tamper-evidence over it:
unsigned or wrong-key new commit is rejected, loudly, with the offending SHA. unsigned or wrong-key new commit is rejected, loudly, with the offending SHA.
This is a **signature** check only — no author/committer matching. This is a **signature** check only — no author/committer matching.
- **Control-plane-owned attribution.** The orchestrator/control plane (sole - **Control-plane-owned attribution.** The orchestrator/control plane (sole
owner of `bot-bottle.db`, PRD 0070) independently verifies each new commit's owner of `bot-bottle.db`, PRD 0070) recomputes each commit's object ID from the
signature and only then writes the attribution row. A gateway assertion alone bytes, verifies the embedded signature against the activation public key it
never creates an audit record. holds, and attaches activation metadata from its own state — accepting no SHA,
- **Host is the source of truth.** The audit record binds each attributed SHA to key, verdict, or metadata asserted by the gateway. No upstream fetch is
required.
- **Host is the source of truth.** The audit record binds each recomputed SHA to
the bottle, host, manifest, agent, activation interval, and retained public the bottle, host, manifest, agent, activation interval, and retained public
key, and records the commit's claimed author/committer. key, and records the commit's claimed author/committer.
- **Verifiable after teardown.** The audit record retains the **full public - **Verifiable after teardown.** The audit record retains the **full public
@@ -166,18 +183,30 @@ and forwards pushes. The orchestrator is the **control plane** and, per PRD
and the audit record must be anchored in the control plane: and the audit record must be anchored in the control plane:
- The gate performs a **synchronous pre-forward signature check** (below) and - The gate performs a **synchronous pre-forward signature check** (below) and
can reject a push before it reaches the upstream. This is a data-plane gate, can reject a push before it reaches the upstream. This is a data-plane gate on
not the audit binding. what leaves the bottle, not the audit binding.
- The **control plane independently verifies** each new commit's signature — - The **control plane** takes the commit bytes to attribute (gateway-delivered
receiving the commit object + signature (or independently reading/fetching the opaque bytes are fine), **recomputes the Git object ID**, **verifies the
objects it is asked to attribute) — and only then writes `attributed_commit`. embedded signature** against the activation public key it minted and holds, and
A gateway assertion alone never produces a row. If the gateway is compromised, writes `attributed_commit` attaching metadata from its own state. It accepts
the worst it can do is forward or reject pushes; it cannot fabricate an audit **no** gateway-supplied `verified` flag, claimed SHA, public key, or activation
binding, because the control plane re-verifies against the activation public identity.
key it minted and holds.
The gate cannot stand in for host-side audit verification (issue #423, review by The precise trust statement (issue #423, review by didericis-codex on d8362ec,
didericis-codex on d8362ec). resolved in #5608): the row binds *these commit bytes / this recomputed SHA* to
*access to this activation's signing key*, and the control plane binds that key
to the recorded activation metadata. It does **not** assert forge observation or
that only the agent (not the signing sidecar) authored the commit — so this PRD
does **not** claim a compromised gateway cannot obtain an attribution row.
Because the sidecar holds the activation signing capability, a compromised
gateway *can* assemble and sign a commit and have it attributed to that
activation; what it cannot do is make the signature verify as a *different*
activation or choose the metadata the control plane records. That residual is
acceptable under the intended guarantee (#5607) and is why the guarantee is
worded as activation-key access, not agent-only authorship or upstream
publication. The gate therefore cannot stand in for host-side verification: the
control plane recomputes the object ID and verifies the signature itself rather
than trusting the gate's word.
### Identity model ### Identity model
@@ -266,20 +295,25 @@ record.
### Control-plane verification & recording ### Control-plane verification & recording
On an accepted push (or asynchronously, driven by the accepted-push event), the For each commit to attribute (the gate hands the control plane the commit bytes;
orchestrator/control plane: opaque gateway-delivered bytes are acceptable because nothing the gateway *says*
about them is trusted), the orchestrator/control plane:
1. Obtains the commit object and its signature for each newly-introduced SHA — 1. **Recomputes the Git object ID** from the bytes itself. The stored `sha` is
either delivered by the gate as opaque bytes to be checked, or read/fetched this recomputed value, never a SHA the gateway claims.
independently from the mirrored repository. Either way the control plane does 2. **Verifies the embedded signature** against the activation public key it
not trust a "verified" flag from the gate. minted and holds for that activation (via a generated allowed-signers file) —
2. **Independently verifies** the signature against the activation public key it ignoring any `verified` flag, key, or activation identity supplied by the
minted and holds for that activation (via a generated allowed-signers file). gateway.
3. Writes `attributed_commit` only for SHAs that pass, recording the commit's 3. Writes `attributed_commit` only for bytes that pass, stamping the activation
claimed author/committer alongside the activation binding. metadata (bottle/manifest/agent/host/interval) from its **own** state — not
from anything the gateway provides — and recording the commit's claimed
author/committer.
A SHA the control plane cannot independently verify is **not** recorded as No upstream fetch is required: the guarantee is a byte↔activation-key binding, so
attributed (it may be logged as an anomaly instead). the object does not need to come from the forge (issue #423, #5608). Bytes that
do not verify against the activation key are **not** recorded as attributed (they
may be logged as an anomaly instead).
### Audit trail ### Audit trail
@@ -307,7 +341,7 @@ CREATE TABLE bottled_agent_activation (
); );
CREATE TABLE attributed_commit ( CREATE TABLE attributed_commit (
sha TEXT NOT NULL, sha TEXT NOT NULL, -- control-plane-RECOMPUTED object ID, not gateway-claimed
bottled_agent_slug TEXT NOT NULL, bottled_agent_slug TEXT NOT NULL,
activation_id TEXT NOT NULL, activation_id TEXT NOT NULL,
repo TEXT NOT NULL, repo TEXT NOT NULL,
@@ -378,11 +412,12 @@ validating signing broker in front of the key would be required for the latter.
**passes the gate** (identity is not enforced here). **passes the gate** (identity is not enforced here).
5. **Control-plane verification + audit.** `bottled_agent_activation` / 5. **Control-plane verification + audit.** `bottled_agent_activation` /
`attributed_commit` tables (PRD 0067 store, control-plane-owned per PRD 0070); `attributed_commit` tables (PRD 0067 store, control-plane-owned per PRD 0070);
the control plane independently verifies each new commit's signature before the control plane recomputes each commit's object ID and verifies the
writing a row; retain full pubkey + fingerprint + principal + validity signature before writing a row; retain full pubkey + fingerprint + principal +
interval; record claimed author/committer; allowed-signers generation + a validity interval; record claimed author/committer; allowed-signers generation
post-teardown `verify-commit` helper. Test that a gateway assertion without an + a post-teardown `verify-commit` helper. Tests: a gateway-claimed SHA/key/
independently-verifiable signature produces **no** attribution row. verdict is ignored — the row's `sha` is the recomputed ID and bytes not signed
by the activation key produce **no** row.
6. **Docs.** Glossary entry ("per-bottle signed commits"); README manifest 6. **Docs.** Glossary entry ("per-bottle signed commits"); README manifest
section; ADR note that signing-enabled bottles gain signed *provenance* and a section; ADR note that signing-enabled bottles gain signed *provenance* and a
host-owned audit record while authorship stays *claimed* (ADR 0002 unchanged). host-owned audit record while authorship stays *claimed* (ADR 0002 unchanged).
@@ -398,9 +433,10 @@ validating signing broker in front of the key would be required for the latter.
the upstream-reachable exclusion (pull + merge human history and push a signed the upstream-reachable exclusion (pull + merge human history and push a signed
merge); a correctly-signed foreign-author commit **passes** (no identity merge); a correctly-signed foreign-author commit **passes** (no identity
enforcement); a clean all-signed push succeeds. enforcement); a clean all-signed push succeeds.
- **Control plane (must):** independent verification records an attribution row - **Control plane (must):** the control plane recomputes the object ID and
for a genuinely-signed SHA; a SHA presented with an invalid/foreign signature records a row for bytes genuinely signed by the activation key; a gateway-
(or a bare gateway assertion) produces **no** row. supplied SHA/key/verdict is ignored (the stored `sha` is the recomputed value);
bytes signed by a foreign/invalid key produce **no** row.
- **Lifecycle:** activation mints the key and writes an `active` row; teardown - **Lifecycle:** activation mints the key and writes an `active` row; teardown
retires it (`valid_until`) and revokes deploy keys fail-loud; a restart retires it (`valid_until`) and revokes deploy keys fail-loud; a restart
re-attaches the same key (no new row); a fresh activation mints a new key and re-attaches the same key (no new row); a fresh activation mints a new key and
@@ -408,13 +444,20 @@ validating signing broker in front of the key would be required for the latter.
- **Post-teardown verification:** regenerate the allowed-signers file from a - **Post-teardown verification:** regenerate the allowed-signers file from a
`retired` row and confirm `verify-commit` still succeeds for an attributed SHA. `retired` row and confirm `verify-commit` still succeeds for an attributed SHA.
## Resolved: control-plane transport
Raised in review and **resolved** (issue #423, #5608): the commit object does not
need to come from the forge, and reading the gateway-owned mirror is no stronger
than accepting gateway-delivered bytes — both are fabricatable, and neither
matters because the control plane trusts nothing the gateway *asserts*. The
transport is therefore: the gate hands the control plane the raw commit bytes,
the control plane **recomputes the object ID** and **verifies the signature**
against the activation key, and stamps its own activation metadata. No upstream
fetch. This is exactly what makes the byte↔activation-key binding sound
regardless of transport.
## Open questions ## Open questions
- **Control-plane transport for verification.** How does the orchestrator obtain
the commit object + signature to verify — pushed to it by the gate as opaque
bytes, or read/fetched independently from the mirror? Independent read is the
stronger boundary; confirm it composes with the mirror layout (PRD 0008) and
the orchestrator's repo access (PRD 0070).
- **Where the gate check slots into PRD 0008 ordering.** Modeled as a - **Where the gate check slots into PRD 0008 ordering.** Modeled as a
pre-forward step after gitleaks; confirm it composes with the existing pre-forward step after gitleaks; confirm it composes with the existing
access-hook / mirror ordering rather than needing a separate hook. access-hook / mirror ordering rather than needing a separate hook.