WIP: PRD: Trusted agent forge identity and guidance #480

Draft
didericis-claude wants to merge 7 commits from prd-forge-subroles into main
Showing only changes of commit 010e253d66 - Show all commits
+110 -67
View File
@@ -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.