Files
bot-bottle/docs/prds/prd-new-signed-commits-attribution.md
T
didericis-codex 9c19ce4c58
tracker-policy-pr / check-pr (pull_request) Successful in 4s
docs: redesign forge identity trust boundary
2026-07-26 18:32:07 +00:00

35 KiB

PRD prd-new: Trusted agent forge identity, signed commits & audit attribution

  • Status: Draft
  • Author: didericis-claude
  • Created: 2026-07-25
  • Issue: #423

Summary

Give each host-trusted agent definition an author identity and named forge accounts, and let a bottle associate each git-gate repository with one of those accounts. The resolved association provisions authenticated forge API access through the egress proxy and injects provider-specific workflow instructions into the agent's system prompt without exposing the token. Repository-local agent and bottle definitions are no longer discovered because they would otherwise be able to select host credential references or impersonate an agent.

For bottles that opt into signing, give each bottled agent a per-activation signing key so every commit it produces is signed in the git-gate trust boundary (outside the bottle) and recorded in bot-bottle's host-owned audit store, which is the portable 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, configured agent author — and separately records the commit's claimed author/committer. 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 cryptographically vouch author/committer identity. The trusted agent definition supplies the configured name/email, while the values in each commit remain claims carried inside the signed object; making the gate reject a mismatch is a possible future add (see Non-goals and Deferred: identity enforcement). Git push capability stays exactly as PRD 0048 deploy keys. Forge API identity uses an operator-provided, agent-specific token referenced from the trusted host environment; bot-bottle does not mint forge users or tokens.

Successor to:

  • PRD 0027 (agent git identity, #94) / ADR 0002 — established that agent name/email is claimed, not vouched. This PRD moves that identity from the bottle/git-gate overlay to the trusted agent definition and adds signed provenance plus a durable host record, not identity enforcement.
  • PRD 0011 (per-file manifests) — allowed repository-local agent files to override home agents. This PRD removes that trust path: agents and bottles are loaded only from the host-owned ~/.bot-bottle tree.
  • PRD 0048 (deploy-key provisioning, #169) — the host-side mint-at-spin-up / revoke-at-teardown lifecycle the signing key follows. Deploy keys are unchanged.
  • PRD 0070 (per-host orchestrator, #351) — the orchestrator/control plane is the sole owner of bot-bottle.db; audit verification and recording live there, not in the data-plane gate (see Trust boundary).

The guarantee

The crisp property this feature provides:

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, configured agent author. 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 separately as a claim, not enforced or vouched. The forge is an external transport and collaboration surface, not the source of attribution truth.

What this does and does not prove (issue #423, comments #5554 / #5607 / #5608):

  • 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

An agent runs on the developer's machine as a subrole, scoped down per role. Locally that is fine because the machine is single-tenant. The git history an agent produces, however, is a durable artifact that outlives the session and can be pushed to shared repositories, and today bot-bottle offers no tamper-evidence over it:

  • No provenance. Nothing ties a pushed commit to the bottle/activation that actually produced it. The configured name/email is forgeable and cosmetic (ADR 0002); a commit could be produced anywhere.
  • No durable, portable record. There is no host-side ledger that says "SHA X was produced by agent A in bottle B on host H during interval [t0,t1], signed by key K," independent of any forge and surviving key rotation.
  • Forge workflow context is missing. The agent prompt does not know which forge backs a git-gate repository, which API base URL to use, or that authenticated requests must go through the egress proxy. Bespoke prompt text has drifted between agents, causing incorrect PR creation behavior such as using Gitea AGit review refs instead of a branch-backed pull request.
  • Identity is owned by the wrong layer. git-gate.user puts an agent property on a repository transport component. The same author identity and forge actor should follow the agent across bottles and repositories.
  • Repository-local agents are a trust escalation. Today $CWD/.bot-bottle/agents/*.md can override a host agent. Once an agent definition may reference a forge token, allowing the checked-out repository to choose that definition would let untrusted workspace content select host identities and credentials.

Goals / Success Criteria

  • Per-activation signing key. A fresh Ed25519 keypair is minted host-side at each activation; the private half lives only in the sidecar ssh-agent, never in the bottle. Only SSH_AUTH_SOCK crosses the boundary.
  • Agent-owned identity. Author name/email and named forge accounts live on the agent definition, not under git-gate.
  • Bottle-owned repository policy. Signing remains an opt-in property of the bottle, and each bottle repository may associate itself with one named forge account from the selected agent.
  • Forge-aware prompting. The resolved agent+bottle manifest contributes a generated, provider-specific system-prompt section describing the forge API URL, proxy-authenticated access path, repository mapping, and safe PR workflow. No token value or token environment-variable name appears in the prompt.
  • Proxy-held forge credential. The host resolves the forge account's token reference and gives it only to the egress proxy, which injects authentication for the configured forge origin. The bottle receives neither the token nor a credential file containing it.
  • Trusted definitions only. Agent and bottle files are discovered only under the host-owned ~/.bot-bottle/{agents,bottles} directories. $CWD/.bot-bottle/{agents,bottles} never contributes definitions or overrides.
  • Signed commits with no SHA divergence. Commits produced in the bottle are signed at commit time; the SHA the agent observes is the SHA that reaches the upstream through the gate.
  • Gate rejects unsigned commits. Before the gate forwards a push, every newly-introduced commit (those not already reachable from the advertised upstream refs) must verify against the activation public key; a push with any 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) 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, configured agent author, activation interval, and retained public key, and separately records the commit's claimed author/committer.
  • Verifiable after teardown. The audit record retains the full public key, fingerprint, principal, and validity interval — enough to regenerate an allowed-signers file and run git verify-commit long after the activation ends and the key is gone.
  • Reprovision-per-activation, fail-loud teardown. The signing key is minted once per activation (persists across restarts within that activation) and discarded at teardown; deploy-key revocation continues to follow PRD 0048's fail-loud discipline.
  • Push capability unchanged. Git transport remains PRD 0048 deploy keys. The forge account token is for forge API actions such as opening and commenting on pull requests; it is not used for Git push.

Non-goals

  • Author/committer enforcement. Explicitly out of scope for this PRD (issue #423, comment #5590). The gate does not reject a commit for carrying a foreign author or committer; those fields are recorded as claims. We rely on the cross-forge audit store of signed commits and the authors recorded there. See Deferred: identity enforcement for what a future add would look like.
  • Cryptographically-vouched author identity. Not claimed — see The guarantee.
  • Forge account or token minting. Bot-bottle does not create subusers or PATs. The operator creates the agent-specific account/token out of band and names the host environment secret in the trusted agent definition.
  • Forge-side attribution surfaces. No commit-status badges, no forge "Verified" badge. The latter is doubly unsuitable: it renders dynamically against a currently registered key (so it would lie the moment a reprovisioned key is revoked), and on Gitea registering a signing key also grants push. Attribution lives in the host record and local git verify-commit, not the forge.
  • Non-Gitea forges, dashboard UI for orphan cleanup, mid-session rotation, dirty-teardown reconciliation. As before; a separate cleanup/sync pass handles orphans left by a crash or discarded snapshot.

Scope evolution

This PRD started as "forge subroles" (forge subuser accounts + provisioned API tokens + optional forge status posting + signing). Review first narrowed it, then restored only the part needed for correct agent operation:

  1. Dropped forge account/token provisioning (#5518 → #5556): the PAT bootstrap is not implementable as sketched (Basic-Auth-as-target-user constraint), and the signature never vouched the author anyway. The host audit store remains the portable source of truth.
  2. Dropped author/committer enforcement (#5590): rely on the audit store of signed commits and the authors recorded there; gate enforcement of the identity fields is a possible future add, not part of this slice.
  3. Restored declarative forge accounts, not provisioning (#6002): agents still need an operator-supplied API identity and forge-specific system instructions to open and update PRs correctly. The trusted agent definition therefore references an existing host secret; bot-bottle neither creates nor rotates that credential.
  4. Moved identity to the agent trust domain (#6002): author and forge accounts are agent properties. Bottle repositories select an account by name, while git-gate remains transport-only. Because these references grant access to host credentials, repository-local agent/bottle discovery is removed.

The resulting feature is trusted agent identity + operator-provided forge API access + forge-aware prompting + signed commits + a host-owned, independently-verified audit record. Identity enforcement (the gate rejecting a foreign author/committer) remains a candidate future PRD.

Design

Trust boundary (control plane vs data plane)

git-gate is the data plane: it parses hostile bytes from inside the bottle and forwards pushes. The orchestrator is the control plane and, per PRD 0070, is the sole owner of bot-bottle.db. These are different trust boundaries, 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 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 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

Per bottled agent (agent definition ∘ sealed bottle), realized per activation:

Part Value Source Role
Signing key one Ed25519 keypair minted host-side per activation signs every commit; private half sidecar-only; the anchor of provenance
Author/committer name + email trusted agent definition author written into commits and recorded as a claim; not enforced
Forge actor named account + API origin + token reference trusted agent definition forge-accounts authenticates forge API actions through the egress proxy
Repository/forge association forge account name bottle git-gate.repos.<repo>.forge selects the actor and generated workflow guidance for that repository

Manifest surface

Identity belongs to the agent. Repository capability and policy belong to the bottle. The following files are both host-owned:

# ~/.bot-bottle/agents/claude.md
---
author:
  name: didericis-claude
  email: eric+claude@dideric.is
forge-accounts:
  didericis-gitea:
    auth:
      type: token
      token_secret: GITEA_CLAUDE_TOKEN
    url: https://gitea.dideric.is/api/v1
---
# ~/.bot-bottle/bottles/dev.md
---
git-gate:
  signing:
    enabled: true                # NEW — opt-in per-activation signing + audit
  repos:
    bot-bottle:
      url: ssh://git@100.78.141.42:30009/didericis/bot-bottle.git
      provisioned_key:           # PRD 0048 — push capability, UNCHANGED
        provider: gitea
        token_env: GITEA_DEPLOY_TOKEN
      host_key: "ssh-ed25519 AAAA..."
      forge: didericis-gitea     # account from the selected agent definition
---
  • author is agent-only and replaces the git-gate.user agent/bottle overlay. It is required when git-gate.signing.enabled is true or when any selected repository has a forge association. The resolved values populate user.name and user.email.
  • forge-accounts is an agent-only map keyed by the existing manifest kebab-case identifier grammar ([a-z][a-z0-9-]*). Each entry contains:
    • url: an HTTPS forge API base URL. This PRD supports Gitea API URLs; a future provider must add explicit typed validation and prompt generation rather than accepting arbitrary prompt text supplied by a repository.
    • auth.type: token in this slice.
    • auth.token_secret: the name of a host environment variable containing the operator-provided, agent-specific API token. The value is resolved only by host provisioning and passed only to the egress proxy.
  • git-gate.repos.<name>.forge is bottle-only and must resolve to an account in the selected agent definition. An unknown account, an unsupported forge URL, a non-HTTPS URL, or a missing/empty host secret fails launch before creating the bottle.
  • git-gate.signing.enabled: true opts a bottle in. Without it, behavior is exactly as today. There is no enforce sub-key — this PRD does not enforce identity fields, so no knob is needed (and a knob that weakened a guarantee was flagged as a contradiction in review).
  • git-gate.signing, git-gate.repos, and their forge associations are bottle-only. author and forge-accounts are agent-only. Validation errors point to the correct file/type instead of silently ignoring misplaced keys.
  • provisioned_key.token_env remains the deploy-key administration credential from PRD 0048. It is separate from the forge actor's token_secret: the former provisions Git push capability, while the latter performs API actions as the agent.
  • Existing git-gate.user fields fail with migration guidance to move the values into the selected home agent's author block. There is no period where bottle identity silently overrides agent identity.

Definition trust and discovery

Only the host-owned manifest tree is authoritative:

  • Agents: ~/.bot-bottle/agents/*.md
  • Bottles: ~/.bot-bottle/bottles/*.md

$CWD/.bot-bottle/agents/*.md no longer contributes new agents and no longer overrides a home agent. $CWD/.bot-bottle/bottles/*.md remains unusable. If either repository-local directory contains manifest files, bot-bottle emits a warning that they are ignored and points to the home paths. Agent enumeration, require_agent, lazy loading, and dashboard selectors all use the same home-only index so there is no alternate path that can still select a workspace definition.

This intentionally supersedes PRD 0011's repository-agent overlay. Workspace instructions remain repository content (for example AGENTS.md), but executable runtime policy, host secret references, and actor identity do not.

Programmatic in-memory manifests remain available for tests and internal composition; they are already supplied by trusted host code and are not a filesystem discovery path.

Forge API provisioning and generated prompt

For each distinct forge account referenced by the selected bottle's repos, the host:

  1. Parses and canonicalizes the HTTPS API origin and rejects credentials in the URL, fragments, and unsupported path shapes.
  2. Resolves auth.token_secret from the host environment. The secret value is copied only into the egress proxy's credential environment.
  3. Adds an inspected egress route scoped to that forge origin/API prefix with the provider's authentication scheme (token for Gitea). Authentication is injected by the proxy; the bottle sends an unauthenticated request to the configured URL.
  4. Appends a generated, non-secret section to bot-bottle's existing system prompt file. The section is derived from validated typed fields, not copied Markdown from a repository.

For the example above, the generated guidance communicates:

  • account alias didericis-gitea and API base https://gitea.dideric.is/api/v1;
  • repository bot-bottle uses that account;
  • forge API calls must use the configured HTTPS URL through the proxy and must not read, print, or manually attach an authorization token;
  • Git pushes still use the bottle's git-gate remote;
  • create/update a normal refs/heads/<branch> and open a branch-backed Gitea pull request through the API; do not push refs/for/*, refs/draft/*, or refs/for-review/*;
  • use the API for review/comment operations and verify the returned object/state before claiming the action completed.

The prompt includes neither the token value nor GITEA_CLAUDE_TOKEN. Keeping even the environment-variable name out of the bottle reduces accidental credential probing and prevents bespoke agent prompts from needing secret implementation details.

Signing: sign at commit time via a forwarded ssh-agent

The reason SHAs never diverge:

  • The sidecar (the git-gate trust boundary) runs an ssh-agent holding the short-lived signing private key.

  • Only SSH_AUTH_SOCK is forwarded into the bottle — a bounded signing capability, not the key.

  • The provisioner writes the bottle .gitconfig:

    [commit]
        gpgsign = true
    [gpg]
        format = ssh
    [user]
        name = didericis-claude
        email = eric+claude@dideric.is
        signingkey = ssh-ed25519 AAAA...   # activation signing PUBLIC key
    
  • git commit asks the forwarded agent to sign; the signature is embedded at object creation, so the agent-space SHA equals the pushed SHA. No transcoder, no SHA translation table.

Gate pre-forward signature check (data plane)

The gate already fetches from upstream before every upload-pack and mirrors bidirectionally (PRD 0008). When git-gate.signing.enabled is set, after gitleaks and before forwarding a push upstream:

  1. Compute the newly-introduced set. Commits reachable from the pushed ref tips but not reachable from any ref already advertised by the upstream (which the gate knows because it fetches upstream first) — equivalent to git rev-list <new-tips> --not <all-known-upstream-refs>. This excludes pulled/merged existing history; a merge commit the bottle creates is itself new and is checked, its already-upstream ancestors are not.
  2. Verify each new commit's signature against the activation public key. A commit that is unsigned or signed by any other key causes the push to be rejected with the offending SHA.
  3. No author/committer matching is performed.

This is a synchronous safety gate on what leaves the bottle; it is not the audit record.

Control-plane verification & recording

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. 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.

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

The host SQLite store (PRD 0067, ~/.bot-bottle/bot-bottle.db, owned by the control plane per PRD 0070) records the signing-key lifecycle and per-commit attribution. Retention is the full public key, fingerprint, principal, and validity interval — enough to regenerate an allowed-signers file and verify commits after teardown (issue #423, comment #5554, resolution 3). Never any private key material.

CREATE TABLE bottled_agent_activation (
    bottled_agent_slug TEXT NOT NULL,
    activation_id      TEXT NOT NULL,     -- one per activation cycle
    host              TEXT NOT NULL,
    manifest_digest   TEXT NOT NULL,      -- ties the record to the sealed manifest
    agent             TEXT NOT NULL,
    configured_author_name  TEXT NOT NULL, -- trusted agent definition value
    configured_author_email TEXT NOT NULL, -- trusted agent definition value
    signing_pubkey    TEXT NOT NULL,      -- full ssh-ed25519 public key (for verify-commit)
    signing_fpr       TEXT NOT NULL,      -- SHA256:... fingerprint (stable handle)
    principal         TEXT NOT NULL,      -- allowed-signers principal, e.g. the author email
    valid_from        TEXT NOT NULL,
    valid_until       TEXT,               -- NULL while active; set at teardown
    status            TEXT NOT NULL,      -- active | retired
    PRIMARY KEY (bottled_agent_slug, activation_id)
);

CREATE TABLE attributed_commit (
    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,
    author_name        TEXT NOT NULL,     -- CLAIMED, recorded as-is (not enforced)
    author_email       TEXT NOT NULL,     -- CLAIMED
    committer_name     TEXT NOT NULL,     -- CLAIMED
    committer_email    TEXT NOT NULL,     -- CLAIMED
    observed_at        TEXT NOT NULL,
    PRIMARY KEY (sha, repo)
);

Verification/allowed-signers generation is a stated part of the design: for a given SHA, join attributed_commit → bottled_agent_activation, emit <principal> <signing_pubkey> to a temporary allowed-signers file, and git verify-commit (or ssh-keygen -Y verify) against it. The (pubkey, principal, valid_from/until) tuple is exactly what that requires. The activation's configured_author_* columns preserve the trusted agent configuration. The attributed commit's author/committer columns are the commit's claim; consumers compare the two if useful, understanding that a mismatch is recorded but not rejected.

Credential lifecycle

Signing follows PRD 0048's lifecycle discipline. The operator-provided forge actor token is referenced, not provisioned:

  • Activation: mint a fresh Ed25519 signing keypair; load the private half into the sidecar ssh-agent; write the public half into .gitconfig and the bottled_agent_activation row (active, valid_from set). Deploy keys are provisioned exactly as PRD 0048. Resolve each referenced forge actor token from the host environment and install it only in the egress proxy process. Minting is per activation (a restart re-attaches the same key; a new activation mints a new key and retires the old row), so frozen snapshots don't accumulate live keys.
  • Teardown (fail-loud): revoke provisioned deploy keys via the forge API (0048); discard the signing key from the sidecar agent and set the activation row to retired with valid_until. The signing key was never on the forge, so there is nothing to revoke there — only the local retire. Deploy-key revocation failure halts teardown (0048); 404 = already-gone = success. Stop the egress proxy to discard its copy of the forge actor token. The operator-owned token itself is not revoked because bot-bottle did not mint it and it may be reused by later activations of the same trusted agent.
  • Dirty teardown is assumed handled; a separate cleanup/sync pass reconciles orphaned deploy keys.

Deferred: identity enforcement

If a future PRD wants the gate to enforce that new commits carry the manifest identity, the natural shape is: extend the gate pre-forward check to also require each new commit's author and committer name/email to equal the resolved agent author, rejecting mismatches — with the same control-plane re-verification before recording. This is deliberately left out now (issue #423, comment #5590); it is noted so the door stays open and the current schema (which records the claimed author/committer) already carries what such a check would compare against. Note that even then the property would be gate-enforced, not signature-vouched; a validating signing broker in front of the key would be required for the latter.

Implementation chunks

  1. This PRD. Sets the revised design and trust boundary.
  2. Trusted definition boundary. Remove $CWD/.bot-bottle/agents from discovery, override, enumeration, lazy loading, and selectors. Keep both agent and bottle definitions home-only; warn on ignored repository files. Update PRD 0011-facing docs and migration guidance.
  3. Identity and forge manifest surface. Add agent-only author and forge-accounts; remove git-gate.user; add bottle-only git-gate.signing (enabled only) and git-gate.repos.<name>.forge. Validate account references after composing the selected agent+bottle and fail closed on missing host secrets or unsupported URLs/providers.
  4. Forge proxy + prompt provisioning. Resolve referenced actor tokens into scoped egress proxy routes and generate provider-specific, non-secret system instructions from typed manifest fields. Gitea guidance covers API usage, branch-backed PRs, prohibited AGit refs, and verifying mutations. Test that neither token values nor token secret names enter the bottle or prompt.
  5. Signing pipeline. Sidecar ssh-agent provisioning; forward SSH_AUTH_SOCK into the bottle across docker, smolmachines, macOS-container, and firecracker backends; emit the commit.gpgsign / gpg.format=ssh / user.signingkey gitconfig. Integration test: a bottle commit is verify-commit-valid and its SHA is unchanged through the gate; the private key is absent from the bottle.
  6. Gate pre-forward signature check. Compute the newly-introduced set (excluding upstream-reachable commits), verify each against the activation key, reject unsigned/wrong-key with the offending SHA. Tests: unsigned rejected; wrong-key rejected; pulled/merged upstream history passes; an all-signed push succeeds. A foreign-author commit that is correctly signed passes the gate (identity is not enforced here).
  7. Control-plane verification + audit. bottled_agent_activation / attributed_commit tables (PRD 0067 store, control-plane-owned per PRD 0070); 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.
  8. Docs. Glossary entries for forge account and per-bottle signed commits; README agent/bottle schema and home-only migration; ADR note that signing-enabled bottles gain signed provenance and a host-owned audit record while authorship stays claimed (ADR 0002 unchanged).

Testing strategy

  • Unit — trust boundary (must): cwd agent files are ignored with a warning, cannot override a home agent, are absent from enumeration/selectors, and cannot be loaded by name. Cwd bottle behavior remains home-only.
  • Unit — manifest (must): author, forge-accounts, git-gate.signing, and repo forge parsing/validation; misplaced-field rejection; kebab-case account names; unknown account references; HTTPS/API URL validation; missing token-secret environment values.
  • Unit — prompt/proxy (must): only referenced forge accounts produce proxy routes and guidance; Gitea instructions name the API/repo and branch-backed workflow; token values and token_secret names are absent from the prompt and bottle environment; auth is scoped to the validated forge API origin.
  • Integration — signing (must): end-to-end signed commit verifies with git verify-commit; SHA observed in the bottle equals the SHA upstream; the private key is absent from the bottle.
  • Integration — gate check (must): unsigned rejected; wrong-key rejected; 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): 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; the activation retains the configured agent author while a differing commit author is recorded separately as an unenforced claim.
  • 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 retires the old.
  • 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

  • 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.