docs: sharpen the attribution guarantee to a byte<->activation-key binding
tracker-policy-pr / check-pr (pull_request) Successful in 6s
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:
@@ -8,15 +8,18 @@
|
||||
## Summary
|
||||
|
||||
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
|
||||
source of truth binding each signed commit to a bottle, host, manifest, agent,
|
||||
activation interval, and retained public key — along with the commit's *claimed*
|
||||
author. The gate mints a short-lived Ed25519 key at spin-up, holds the private
|
||||
half in the sidecar `ssh-agent`, and forwards only `SSH_AUTH_SOCK` into the
|
||||
bottle. The gate rejects any newly-introduced commit that is not signed by the
|
||||
activation key; the **control plane** independently re-verifies each signature
|
||||
before recording attribution.
|
||||
source of truth. Each row cryptographically binds a commit's bytes (and their
|
||||
control-plane-recomputed SHA) to **access to that activation's signing key**, and
|
||||
binds the key to control-plane-owned activation metadata — bottle, host,
|
||||
manifest, agent, activation interval, retained public key — plus the commit's
|
||||
*claimed* author. The gate mints a short-lived Ed25519 key at spin-up, holds the
|
||||
private half in the sidecar `ssh-agent`, and forwards only `SSH_AUTH_SOCK` into
|
||||
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.
|
||||
Author/committer name/email are recorded as claims carried inside the signed
|
||||
@@ -43,26 +46,38 @@ Successor to:
|
||||
|
||||
The crisp property this feature provides:
|
||||
|
||||
> Every new commit introduced through this bottle's gate is **signed by the
|
||||
> activation key**, and the **host-owned audit store** — after the control plane
|
||||
> independently verifies that signature — records it, binding the SHA to the
|
||||
> bottle, host, manifest, agent, activation interval, and retained public key.
|
||||
> The commit's author/committer identity is **recorded as a claim**, not enforced
|
||||
> or vouched. The forge remains only the repository transport/capability layer.
|
||||
> The **host-owned audit store** binds a set of commit bytes — whose Git object
|
||||
> ID the control plane **recomputes** itself — to **access to this activation's
|
||||
> signing key**, and binds that key to **control-plane-owned activation
|
||||
> metadata**: bottle, host, manifest, agent, activation interval, retained public
|
||||
> key. An agent may author and sign arbitrary commit contents, but it cannot make
|
||||
> 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
|
||||
with access to activation *Y*'s signing key, i.e. inside this bottle during
|
||||
this activation. Independent verification binds SHA *X* to activation *Y*.
|
||||
- The signature does **not** make the author name/email cryptographically
|
||||
vouched. The bottle chooses every byte sent through the forwarded agent, so a
|
||||
raw signature could sign `author Mallory <mallory@example>` just as validly.
|
||||
Author/committer identity is a claim carried inside the signed object and
|
||||
recorded as-is.
|
||||
- The audit record is a cryptographic binding only because the **control plane**
|
||||
verifies the signature before recording — not because the data-plane gate
|
||||
asserts it (see **Trust boundary**).
|
||||
- It proves **access to activation *Y*'s signing key**: whoever assembled these
|
||||
commit bytes could sign with that key. Recomputing the object ID and verifying
|
||||
the embedded signature binds the SHA to activation *Y*, and the control plane's
|
||||
own records bind *Y*'s key to *Y*'s metadata.
|
||||
- It does **not** prove the commit was ever pushed, observed upstream, kept
|
||||
(vs. later reverted or dropped), or produced by the *agent* rather than by any
|
||||
other holder of the activation signing capability (the sidecar itself). The
|
||||
store deliberately makes no claim about publication or sole-agent authorship
|
||||
— the owner's requirement is attribution of *what manifest/agent/etc. was in
|
||||
use when a commit was signed*, not proof of where the commit went (#5607).
|
||||
- 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
|
||||
|
||||
@@ -94,10 +109,12 @@ tamper-evidence over it:
|
||||
unsigned or wrong-key new commit is rejected, loudly, with the offending SHA.
|
||||
This is a **signature** check only — no author/committer matching.
|
||||
- **Control-plane-owned attribution.** The orchestrator/control plane (sole
|
||||
owner of `bot-bottle.db`, PRD 0070) independently verifies each new commit's
|
||||
signature and only then writes the attribution row. A gateway assertion alone
|
||||
never creates an audit record.
|
||||
- **Host is the source of truth.** The audit record binds each attributed SHA to
|
||||
owner of `bot-bottle.db`, PRD 0070) recomputes each commit's object ID from the
|
||||
bytes, verifies the embedded signature against the activation public key it
|
||||
holds, and attaches activation metadata from its own state — accepting no SHA,
|
||||
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
|
||||
key, and records the commit's claimed author/committer.
|
||||
- **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:
|
||||
|
||||
- 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,
|
||||
not the audit binding.
|
||||
- The **control plane independently verifies** each new commit's signature —
|
||||
receiving the commit object + signature (or independently reading/fetching the
|
||||
objects it is asked to attribute) — and only then writes `attributed_commit`.
|
||||
A gateway assertion alone never produces a row. If the gateway is compromised,
|
||||
the worst it can do is forward or reject pushes; it cannot fabricate an audit
|
||||
binding, because the control plane re-verifies against the activation public
|
||||
key it minted and holds.
|
||||
can reject a push before it reaches the upstream. This is a data-plane gate on
|
||||
what leaves the bottle, not the audit binding.
|
||||
- The **control plane** takes the commit bytes to attribute (gateway-delivered
|
||||
opaque bytes are fine), **recomputes the Git object ID**, **verifies the
|
||||
embedded signature** against the activation public key it minted and holds, and
|
||||
writes `attributed_commit` attaching metadata from its own state. It accepts
|
||||
**no** gateway-supplied `verified` flag, claimed SHA, public key, or activation
|
||||
identity.
|
||||
|
||||
The gate cannot stand in for host-side audit verification (issue #423, review by
|
||||
didericis-codex on d8362ec).
|
||||
The precise trust statement (issue #423, review by 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
|
||||
|
||||
@@ -266,20 +295,25 @@ record.
|
||||
|
||||
### Control-plane verification & recording
|
||||
|
||||
On an accepted push (or asynchronously, driven by the accepted-push event), the
|
||||
orchestrator/control plane:
|
||||
For each commit to attribute (the gate hands the control plane the commit bytes;
|
||||
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 —
|
||||
either delivered by the gate as opaque bytes to be checked, or read/fetched
|
||||
independently from the mirrored repository. Either way the control plane does
|
||||
not trust a "verified" flag from the gate.
|
||||
2. **Independently verifies** the signature against the activation public key it
|
||||
minted and holds for that activation (via a generated allowed-signers file).
|
||||
3. Writes `attributed_commit` only for SHAs that pass, recording the commit's
|
||||
claimed author/committer alongside the activation binding.
|
||||
1. **Recomputes the Git object ID** from the bytes itself. The stored `sha` is
|
||||
this recomputed value, never a SHA the gateway claims.
|
||||
2. **Verifies the embedded signature** against the activation public key it
|
||||
minted and holds for that activation (via a generated allowed-signers file) —
|
||||
ignoring any `verified` flag, key, or activation identity supplied by the
|
||||
gateway.
|
||||
3. Writes `attributed_commit` only for bytes that pass, stamping the activation
|
||||
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
|
||||
attributed (it may be logged as an anomaly instead).
|
||||
No upstream fetch is required: the guarantee is a byte↔activation-key binding, so
|
||||
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
|
||||
|
||||
@@ -307,7 +341,7 @@ CREATE TABLE bottled_agent_activation (
|
||||
);
|
||||
|
||||
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,
|
||||
activation_id 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).
|
||||
5. **Control-plane verification + audit.** `bottled_agent_activation` /
|
||||
`attributed_commit` tables (PRD 0067 store, control-plane-owned per PRD 0070);
|
||||
the control plane independently verifies each new commit's signature before
|
||||
writing a row; retain full pubkey + fingerprint + principal + validity
|
||||
interval; record claimed author/committer; allowed-signers generation + a
|
||||
post-teardown `verify-commit` helper. Test that a gateway assertion without an
|
||||
independently-verifiable signature produces **no** attribution row.
|
||||
the control plane recomputes each commit's object ID and verifies the
|
||||
signature before writing a row; retain full pubkey + fingerprint + principal +
|
||||
validity interval; record claimed author/committer; allowed-signers generation
|
||||
+ a post-teardown `verify-commit` helper. Tests: a gateway-claimed SHA/key/
|
||||
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
|
||||
section; ADR note that signing-enabled bottles gain signed *provenance* and a
|
||||
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
|
||||
merge); a correctly-signed foreign-author commit **passes** (no identity
|
||||
enforcement); a clean all-signed push succeeds.
|
||||
- **Control plane (must):** independent verification records an attribution row
|
||||
for a genuinely-signed SHA; a SHA presented with an invalid/foreign signature
|
||||
(or a bare gateway assertion) produces **no** row.
|
||||
- **Control plane (must):** the control plane recomputes the object ID and
|
||||
records a row for bytes genuinely signed by the activation key; a gateway-
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
`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
|
||||
|
||||
- **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
|
||||
pre-forward step after gitleaks; confirm it composes with the existing
|
||||
access-hook / mirror ordering rather than needing a separate hook.
|
||||
|
||||
Reference in New Issue
Block a user