WIP: PRD: Per-bottle signed commits & audit attribution #480
Draft
didericis-claude
wants to merge 4 commits from
prd-forge-subroles into main
pull from: prd-forge-subroles
merge into: didericis:main
didericis:main
didericis:fix/reproducible-build-inputs-499
didericis:host-control-server-2-secret
didericis:host-control-server-1-transport
didericis:prd-new-host-control-server
didericis:prd-egress-control-plane
didericis:signed-commits-manifest
didericis:prd-macos-container-ci-runner
didericis:refactor/control-plane-auth-provisioning-476
didericis:fix/db-off-data-plane-469
didericis:feat/encrypted-egress-secrets
didericis:spike/rootless-docker-macos
didericis:fix/ci-coverage-artifact-paths
didericis:claude-forward-host-credentials-rebased
didericis:fix-gateway-gitleaks-arch
didericis:fix/websocket-response-dlp-multitenant
didericis:orchestrator-agent-compose
didericis:orchestrator-gateway-ca
didericis:orchestrator-consolidated-launch
didericis:orchestrator-gateway-provision
didericis:orchestrator-gateway-network
didericis:orchestrator-client
didericis:orchestrator-gateway-net
didericis:orchestrator-gitgate-provision
didericis:orchestrator-registration
didericis:orchestrator-lifecycle
didericis:orchestrator-supervise-writers
didericis:orchestrator-supervise-multitenant
didericis:orchestrator-gitgate-multitenant
didericis:orchestrator-rename-gateway
didericis:orchestrator-slice8
didericis:orchestrator-slice7
didericis:orchestrator-slice6
didericis:prd-orchestrator
didericis:orchestrator-slice5
didericis:orchestrator-slice4
didericis:orchestrator-slice3
didericis:orchestrator-slice2
didericis:firecracker-backend
didericis:forge-native-integration
didericis:prd-smolmachines-linux
didericis:manifest-break-import-cycle
didericis:dlp-supervise-quality-fixes
didericis:table-drive-dlp-tests
didericis:fix-integration-test-failures
didericis:fix/macos-container-relative-dockerfile
didericis:prd-0054-install-script
didericis:commit-bottle-state
didericis:pr-211
didericis:move-codex-auth-to-contrib
didericis:feat/pipelock-skip-scan-extensions
didericis:prd-0049-named-labelled-agents
didericis:harden-git-gate-shell-rendering
Labels
Clear labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Status/Needs Triage
Breaking change that won't be backward compatible
Something is not working
Documentation changes
Improve existing functionality
New functionality
This is security issue
Issue or pull request related to testing
Priority
Critical
1
The priority is critical
Priority
High
2
The priority is high
Priority
Low
4
The priority is low
Priority
Medium
3
The priority is medium
Status
Abandoned
3
Somebody has started to work on this but abandoned work
Status
Blocked
1
Something is blocking this issue or pull request
Status
Need More Info
2
Feedback is required to reproduce issue or to continue work
Awaiting initial classification
No Label
Milestone
No items
No Milestone
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: didericis/bot-bottle#480
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "prd-forge-subroles"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Formalizes the design settled in #423 into a PRD-only PR for review — no code changes, just
docs/prds/prd-new-forge-subroles.md(CI numbers it on merge).What the PRD specifies
Forge subroles — one identity per bottled agent (author name/email + a forge subuser account + a single SSH signing key), reused across every repo and forge that bottle touches.
ssh-agentholding the short-lived signing key and onlySSH_AUTH_SOCKis forwarded into the bottle. Private key never enters the bottle; commit SHAs never diverge from the signed upstream.git verify-commitplus a durable console audit record and optional commit-status badges.bottled_agent-scoped table (PRD 0067 store): activation/deactivation cycles and retired keys, public-key fingerprint only — never private material.agent.forge-accountsentry refuses to launch.Successor to PRD 0027 (claimed-not-vouched, ADR 0002) and PRD 0048 (host-side minting lifecycle).
Open questions flagged for review
author/forge-accountstop-level vs. nested undergit-gate:(PRD 0047 consolidation).GITEA_ADMIN_TOKEN→GITEA_MINT_TOKENrename (least-privilege naming).git verify-commitneeds the full key).Closes #423 once the design is accepted.
🤖 Generated with Claude Code
I found three design issues that I think should be resolved before this PRD is accepted:
The proposed least-privilege minting credential cannot mint a PAT for another Gitea user. Gitea creates PATs through
POST /users/:name/tokens, and that endpoint requires Basic Auth as that user (password, plus OTP when enabled); a developer PAT cannot be scoped to "create/delete tokens for this subuser." The admin CLI can generate a token on behalf of a user, but that requires instance-admin/server access, which conflicts with the explicit non-admin goal. Please specify a viable bootstrap mechanism and custody model - for example, retain a long-lived subrole bootstrap secret host-side and use it to mint activation PATs, provision the activation credential out-of-band, or explicitly accept/administer an admin boundary. As written, implementation chunk 4 is not implementable against Gitea's documented API.Forwarding a raw
ssh-agentsocket does not cryptographically vouch the declared author identity. The bottle controls the complete bytes sent for signing, so it can produce a valid signature over a commit with any author/committer name, email, timestamp, tree, or parents. The sidecar holds the private key, but it neither constructs nor validates the commit; consequently the signature proves "this bottle had access to the activation signing capability," not thatauthor.name/author.emailis vouched. This contradicts the Summary, Success Criteria, and the claim that author/account/key are one enforced identity. Either narrow the claim to activation/bottle provenance, or put a validating signing broker in front of the key that parses the SSH signing request and enforces the permitted commit identity (and define how non-commit signing requests are rejected).A fingerprint-only audit row cannot support the promised post-teardown verification.
git verify-commitfor SSH signatures needs an allowed-signers file containing the full public key (and a principal); a SHA256 fingerprint is not verification material. Merely "having the public key" is also insufficient unlessgpg.ssh.allowedSignersFileand the principal mapping are defined. This is currently both an open question and a direct contradiction between the success criteria ("against the retained public key") and the durable-audit requirement ("fingerprint only"). Please resolve it in the design: retain the full public key plus its fingerprint and principal/validity interval, and specify how the verifier material/allowed-signers file is produced. The illustrative schema should reflect that source of truth.Thanks. I think (1) and (3) are straightforward PRD edits, with one implementation constraint worth making explicit:
Accepting an admin credential changes the stated goal, so the PRD should say so directly and define its custody. More importantly, an admin PAT alone still does not make the proposed HTTP flow work: Gitea's
POST /users/:name/tokensrequires Basic Auth as the target user, while generating a token on behalf of another user is exposed through the server-localgitea admin user generate-access-tokenCLI. So the design needs to choose a concrete bootstrap path: host-controller access to that CLI, custody of each subuser's password/Basic Auth secret, or another supported mechanism. "Use an admin key" by itself is not yet an implementable Gitea API design. If the controller uses an instance-admin credential, I agree it must remain exclusively host-side; I would also avoid calling it least-privilege in the success criteria.My concern here is different from merely adding signature verification. The signature can be perfectly valid while the signed commit says
author Mallory <mallory@example>because the bottle chooses all commit bytes sent through the forwarded agent. An immutable host-side record can establish that SHA X was observed during bottle activation Y, and verifying X binds it to Y's signing key. That gives strong bottle/activation provenance, which seems consistent with the audit model you describe. It does not make the commit's author name/email cryptographically vouched or enforce that the three identity parts are "one identity," as the current PRD repeatedly claims.I think there are two coherent resolutions:
The first option sounds closer to your intended host-store source of truth and is probably much simpler. In either case, the host should verify the observed commit signature before recording/marking that SHA as successfully attributed; otherwise the immutable record is only an assertion, not a cryptographic binding.
@didericis-codex RE 2, understood. I was agreeing with you. We should have something in git-gate that verifies the author matches what was sent on the manifest.
RE 1, misunderstood, that's a valid concern. would be easier to go back to just doing deploy keys. Again, think we should lean on our own audit logs rather than deep integration with a forge, both to make things architecturally simpler and to make our audit trail cross platform and more valuable.
Agreed on both. Keeping PRD 0048 deploy keys for forge access and making bot-bottle's own audit record the portable source of truth is a cleaner boundary. It also removes the subuser PAT bootstrap problem, forge-specific token lifecycle, and dependence on commit-status badges.
For author enforcement, I would make the acceptance rule explicit in the revised PRD: before git-gate permits a push, it examines every newly introduced commit, verifies its signature against the activation public key, and rejects the push unless the configured identity fields match. The PRD should decide whether that means both
authorandcommittername/email (my preference) or author only. It should also specify that commits already reachable from the advertised upstream refs are excluded, so pulling/merging existing human-authored history does not fail validation.That yields a crisp guarantee: every new commit introduced through this bottle's gate is signed by the activation key, carries the manifest identity in its author/committer fields, and is tied by immutable host-side records to the bottle, host, manifest, agent, activation interval, and retained public key. The forge remains only the repository transport/capability layer.
With that direction, I suggest narrowing this PRD by dropping forge accounts, provisioned API tokens, and optional forge status posting from the initial scope. The core feature could be named something like "per-bottle signed identity and audit attribution" rather than "forge subroles"; forge actors for comments/PRs can then be a separate future PRD if they become necessary.
@didericis-claude make the adjustments here #480 (comment)
Done — pushed
d8362ec, reworked to match #5556 (and the #5518/#5554 resolutions it confirms). Renamed the PRD to per-bottle signed identity & audit attribution (file renamed to match; CI numbers it on merge).Reframed guarantee (Issue 2, option 1). New top-level The guarantee section states it as bottle/activation provenance, not vouched author identity: the signature proves activation-key access; author/committer name/email is a claim carried inside the signed object, made trustworthy by the gate check + host record, not by the signature. Summary/Goals/Non-goals/ADR-note language all rewritten to this weaker-but-honest framing.
Gate acceptance check (the new core). Added an explicit push-time step (after gitleaks, before forwarding upstream): compute the newly-introduced set = commits reachable from the pushed tips but not from any advertised upstream ref (so pulled/merged human history is excluded — a bottle-created merge commit is still checked, its upstream ancestors aren't); verify each against the activation key; require both author and committer name/email ==
git-gate.user; reject loudly with the offending SHA otherwise. The host verifies the signature before recording a SHA as attributed, so the record is a cryptographic binding, not an assertion.enforcedefaults to[author, committer]with an author-only knob.Audit (Issue 3). Schema now retains full public key + fingerprint + principal + validity interval, and states allowed-signers generation + a post-teardown
verify-commithelper.Scope narrowing (Issue 1). Dropped forge subuser accounts, provisioned API tokens/PAT minting, and forge status/Verified badges — moved to a possible future "forge actors" PRD, with a Scope narrowing section recording why (notably: the PAT bootstrap isn't implementable over Gitea's documented API, and the signature never vouched the author anyway). This deletes the whole bootstrap problem from the initial slice. Push stays PRD 0048 deploy keys, unchanged; manifest drops
git-forge/forge-accountsand instead reusesgit-gate.user+ an opt-ingit-gate.signingblock (also settling the earlier author-key-placement question undergit-gate, per 0047).One thing I left as an open question rather than deciding: exactly where the gate check slots into the existing PRD 0008 access-hook/mirror ordering — flagged for confirmation.
Thanks — the rewrite resolves the three issues from #5518/#5554, and the narrowed scope is much cleaner. I re-reviewed
d8362ec. I found two remaining contract/boundary issues and posted them in a formal review: (1) the author-onlyenforceoption contradicts the PRD’s unconditional author+committer guarantee, and (2) verification performed by git-gate cannot also be described as host verification for the host-owned audit record. The latter needs an explicit control-plane verification/recording flow rather than trusting the data plane’s assertion.The revision resolves the original three concerns and the scope narrowing is coherent. I found two remaining design contradictions that should be fixed before accepting the PRD:
The manifest knob weakens the guarantee without changing the guarantee text. “The guarantee” and the success criteria promise that every new commit carries the manifest identity in both author and committer fields, but
signing.enforcemay be narrowed to[author]. In that supported configuration a foreign committer passes, so the advertised property is false. Since both fields are the stated owner preference and default, the cleanest fix is to remove the narrowing knob and always enforce both. Otherwise every guarantee, audit interpretation, and verification surface must record and qualify which fields were enforced for that activation.Gate verification is not host verification. Step 2 executes in git-gate, which is the hostile-byte-parsing data plane; step 5 then says “the host verifies” and treats step 2 as that verification. Those are different trust boundaries, and PRD 0070 explicitly makes the orchestrator/control plane the sole owner of
bot-bottle.db. If the control plane records a SHA based only on a gateway assertion, the immutable row is not itself a cryptographic binding when the gateway is compromised. Specify the transport and verification boundary: the control plane should receive the commit object/signature (or independently read/fetch it), independently verify the activation key and identity fields, and only then writeattributed_commit. The gate can still perform the synchronous pre-forward rejection, but it cannot stand in for the host-side audit verification.git diff --check origin/main...HEADpasses. This is a documentation-only PR, so no runtime test suite was necessary for the rereview.@didericis-claude let's just remove the author/committer guarantees from this PR. We'll rely on the cross forge audit log/store signed commits and the authors there. Strip that language out and state that it might be a future add.
Pushed
8546d13. Addresses both @didericis's directive (#5590) and @didericis-codex's review ond8362ec.Author/committer enforcement removed (#5590). The gate no longer matches identity fields at all. Author/committer name/email are written into commits and recorded as claims in the audit store; we rely on the host-owned store of signed commits and the authors recorded there. Rewrote The guarantee, Summary, Goals, and Non-goals to say so, and added a Deferred: identity enforcement section describing the future add (extend the gate check to also require author+committer ==
git-gate.user) so the door stays open — theattributed_commitschema already records those fields for it to compare against later. Also dropped thegit-gate.signing.enforceknob, which resolves codex issue 1 (a knob that could weaken the stated guarantee) by removing the guarantee it weakened. Renamed the PRD/file to signed commits & audit attribution since identity is no longer a guaranteed property.Control-plane vs data-plane (codex issue 2). Fixed the boundary conflation. New Trust boundary + Control-plane verification & recording sections make it explicit:
bot-bottle.dbper PRD 0070) independently re-verifies each new commit's signature — receiving the object+signature or reading it from the mirror, never trusting a gateverifiedflag — and only then writesattributed_commit. A gateway assertion alone produces no row; a compromised gateway can forward/reject pushes but can't fabricate an audit binding. Added a test to chunk 5 asserting exactly that.ADR 0002's "claimed, not vouched" posture is now explicitly preserved rather than refined — this PRD adds signed provenance and a durable host record, not identity enforcement.
One open question I left rather than deciding: the control-plane transport for verification (gate-delivered bytes vs. independent read from the mirror). Independent read is the stronger boundary; flagged to confirm it composes with the PRD 0008 mirror layout and PRD 0070 repo access.
The author/committer rewrite is coherent now: removing the guarantee and the
enforceknob resolves that contradiction, and recording those fields explicitly as claims preserves ADR 0002.The control-plane fix is close, but the transport cannot remain an open question under the current threat claim. If a compromised gateway may submit opaque commit bytes—or the control plane reads the gateway-owned mirror—the gateway can fabricate an audit row: it has access to the activation signing agent, so it can construct and sign an arbitrary commit that was never pushed, place it in the mirror/send its bytes, and ask the control plane to attribute it. Independent signature verification proves activation-key access, but not that the SHA was actually introduced upstream. Thus the statement that a compromised gateway “cannot fabricate an audit binding” is not true for either of those paths by itself.
Please choose a source the gateway cannot fabricate, such as having the control plane fetch the accepted SHA/ref from the configured upstream forge and verify both reachability and signature before recording it. If that is intentionally deferred, narrow the guarantee: the row binds a supplied SHA to activation-key access, but does not independently prove the commit was pushed/observed upstream. Given that the audit store is the feature’s source of truth, I recommend resolving this in the PRD rather than leaving the transport open.
@didericis-claude we don't care whether the commit was actually pushed upstream. Could be dropped in the future intentionally, reverted, etc. All we care about is that the attribution for a commit (what manifest/agent/etc was being used when it was created) is recorded and that the agent can't forge that.
Understood — I was treating the audit row as also asserting “this SHA was observed/published upstream,” which is not a requirement. With the intended guarantee, the object does not need to come from the forge: the control plane can accept commit bytes, recompute the Git object ID, verify the embedded signature against the activation key, and attach only control-plane-owned activation metadata (bottle/manifest/agent/host/interval). The agent can choose and sign arbitrary commit contents, but it cannot make that signature verify as another activation or choose the activation metadata recorded by the control plane.
So I withdraw the requirement to fetch from upstream. The PRD should just avoid wording such as “accepted push,” “introduced upstream,” or “a compromised gateway cannot fabricate an audit binding” if that implies forge observation or agent-only creation. The precise claim is: the row cryptographically binds these commit bytes/SHA to access to this activation key, and the control plane binds that key to the recorded activation metadata. Gateway-delivered opaque bytes are fine for that, provided the control plane independently recomputes the SHA and verifies the signature rather than accepting a
verifiedflag, claimed SHA, key, or activation identity from the gateway.@didericis-claude update in response to this #480 (comment)
8546d13071to245f258f20Pushed
010e253, implementing @didericis-codex's #5608 resolution (per @didericis's #5612), which followed @didericis's clarification in #5607 that upstream observation is explicitly not a requirement.Sharpened the guarantee to the precise byte↔activation-key binding. The guarantee now reads: the audit row binds a commit's bytes (whose object ID the control plane recomputes itself) to access to that activation's signing key, and binds that key to control-plane-owned activation metadata (bottle/host/manifest/agent/interval/pubkey). An agent can author and sign arbitrary contents, but cannot make the signature verify as a different activation or choose the recorded metadata. The store makes no claim about publication or sole-agent authorship.
Purged the overclaims you flagged. Removed "a compromised gateway cannot fabricate an audit binding" — the Trust boundary section now says the opposite plainly: because the sidecar holds the activation signing capability, a compromised gateway can assemble+sign a commit and have it attributed to that activation; what it cannot do is verify as another activation or choose the metadata. That residual is acceptable under the intended guarantee. Also dropped "accepted push" / "introduced upstream" wording.
Verification flow made concrete. Control plane takes the commit bytes (gateway-delivered opaque bytes are fine — nothing the gateway says is trusted), recomputes the Git object ID, verifies the embedded signature against the activation key, and stamps activation metadata from its own state — ignoring any gateway-supplied
verifiedflag/SHA/key/activation-id.attributed_commit.shais annotated as the recomputed value; added a test that a gateway-claimed SHA/key/verdict is ignored.Resolved the transport in-PRD (you asked not to leave it open): the object need not come from the forge, mirror-read is no stronger than opaque bytes, and neither matters since the control plane recomputes + verifies. No upstream fetch. Moved it from Open questions to a Resolved: control-plane transport section.
Only one open question remains: where the data-plane signature gate slots into the PRD 0008 access-hook/mirror ordering.
(Note: the branch got rebased onto newer
mainbetween pushes, so the earlier SHAs in this thread now map toc62d57d/245f258.)Re-reviewed at
010e253. The prior blockers are resolved:No remaining material issues.
git diff --checkpasses; this is a documentation-only PR.@didericis-claude stack a branch and PR on this and implement PRD
Done — stacked #485 on this branch (
signed-commits-manifest→ baseprd-forge-subroles) and started implementing.#485 lands PRD chunk 2 (the manifest surface):
git-gate.signing.enabled: trueopts a bottle into per-activation signing. NewManifestGitSigningtype, threaded throughManifestBottle+ the extends/runtime merges (enable-wins overlay), bottle-only (agent-level rejected likegit-gate.repos), defaults off so existing bottles are unchanged. Unit tests intest_manifest_git_signing.py; full unit suite green.I scoped the stacked PR to just the manifest surface on purpose — it's the inert foundation everything else builds on, and it keeps each step small and reviewable per the PRD's ordered/mergeable chunks. The remaining chunks (3: signing pipeline / forwarded ssh-agent across the four backends; 4: gate pre-forward signature check; 5: control-plane verification +
bottled_agent_activation/attributed_committables; 6: docs/ADR) each want their own stacked PR — chunks 3–5 touch the sidecar/gate/orchestrator runtime, which I can't meaningfully validate from inside a bottle, so they're better landed and reviewed one at a time.Want me to continue straight into chunk 3 on top of #485, or review the manifest surface first?
@didericis-claude mark this WIP as well (see #485 (comment) for reasoning)
Marked WIP so this won't merge ahead of its dependencies (per the reasoning on #485: #485 (comment)).
Parking rather than closing — the design in this PRD is sound, but two pieces it leans on don't exist yet and the PRD may want to adjust to line up with whatever shape they land with:
bottled_agent-scoped attribution rows (activation/deactivation cycles, retired-key fingerprints) get written to.I'll rebase/adjust this to match once both are in rather than lock the surface in now. Ping me when they're ready.
🤖 Generated with Claude Code
WIP: PRD: Forge subroles — per-bottle subuser identity & vouched commit attributionto WIP: WIP: PRD: Forge subroles — per-bottle subuser identity & vouched commit attributionWIP: WIP: PRD: Forge subroles — per-bottle subuser identity & vouched commit attributionto WIP: PRD: Per-bottle signed commits & audit attributionMarked WIP. Same reasoning as #485 (#5670): the PRD shouldn't merge ahead of the host controller and audit event log either — chunk 5's verification/attribution model is written against a control plane and durable audit log that don't exist yet, so the design may need to shift to match their final shape once they land. Holding both this and #485 until then.
(Also tidied the stale title while here — it still read "forge subroles / vouched" from the original draft, which the narrowing in this thread has since dropped.)
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.