diff --git a/docs/prds/prd-new-signed-commits-attribution.md b/docs/prds/prd-new-signed-commits-attribution.md deleted file mode 100644 index 37fd0585..00000000 --- a/docs/prds/prd-new-signed-commits-attribution.md +++ /dev/null @@ -1,651 +0,0 @@ -# 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 ` 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..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: - -```yaml -# ~/.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 ---- -``` - -```yaml -# ~/.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..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/` 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`: - - ```ini - [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 --not `. 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. - -```sql -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 -` ` 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..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. diff --git a/docs/prds/prd-new-trusted-agent-forge-identity.md b/docs/prds/prd-new-trusted-agent-forge-identity.md new file mode 100644 index 00000000..4201438c --- /dev/null +++ b/docs/prds/prd-new-trusted-agent-forge-identity.md @@ -0,0 +1,387 @@ +# PRD prd-new: Trusted agent forge identity and guidance + +- **Status:** Draft +- **Author:** didericis-claude +- **Created:** 2026-07-25 +- **Issue:** #423 + +## Summary + +Move author identity and named forge accounts into host-trusted agent +definitions. A bottle may optionally associate a git-gate repository with one +of the selected agent's forge accounts. When associated, bot-bottle gives the +agent non-secret, provider-specific system guidance for that repository and +routes authenticated forge API calls through the egress proxy without exposing +the account token to the bottle. + +Repository-local agent and bottle definitions are no longer discovered. Once +agent definitions can select a host forge credential, allowing checked-out +repository content to define or override an agent would let untrusted workspace +content select host identities and secrets. + +This PRD is deliberately limited to identity ownership, manifest trust, forge +API access, and generated guidance. Per-activation signing, signature +enforcement, commit attribution, and the commit audit model move to a follow-up +PRD. + +Successor to: + +- **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 0027 (agent git identity, #94)** / **ADR 0002** — put name/email in + `git-gate.user` while keeping them claimed rather than vouched. This PRD moves + those agent properties out of git-gate and into the trusted agent definition. +- **PRD 0048 (deploy-key provisioning, #169)** — remains the Git push + capability. A forge actor token is a separate API credential and never + replaces a deploy key. + +## Problem + +### 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. Missing guidance has +already caused incorrect PR behavior, including attempts to use Gitea AGit +review refs instead of a normal branch-backed pull request. + +### Identity is owned by the wrong layer + +`git-gate.user` puts an agent property on a repository transport component. An +author identity and forge actor should follow the agent across bottles and +repositories. Git-gate should own Git transport policy, not decide who the +agent is. + +### Repository-local agents become a credential-selection path + +Today `$CWD/.bot-bottle/agents/*.md` can define new agents and override +home-resident agents. If an agent definition may reference an operator-provided +forge token, a malicious repository could select a host credential merely by +being the current workspace. Repository-local bottles are already ignored; +agents need the same host-only boundary. + +## Goals / Success Criteria + +- **Agent-owned identity.** Author name/email and named forge accounts live on + the agent definition, not under `git-gate`. +- **Trusted definitions only.** Agent and bottle files are discovered only + under `~/.bot-bottle/{agents,bottles}`. Repository-local definitions never + contribute names, defaults, or overrides. +- **Optional repository association.** A bottle repository may name one forge + account from the selected agent. Repositories without `forge` retain current + behavior and generate no forge guidance or actor credential route. +- **Proxy-held forge credential.** The host resolves the selected forge + account's token reference and gives the value only to the egress proxy. The + bottle receives neither the token nor a credential file containing it. +- **Forge-aware system guidance.** For associated repositories, bot-bottle + generates provider-specific instructions describing the API base URL, + repository/account association, proxy-authenticated access, and correct PR + workflow. +- **No secret prompt material.** Neither token values nor token + environment-variable names appear in the prompt or bottle environment. +- **Fail closed.** Unknown account references, unsupported/non-HTTPS URLs, + missing host secrets, and misplaced agent/bottle fields fail before creating + the bottle. +- **Push capability unchanged.** Git transport remains PRD 0048 deploy keys. + The forge actor token is only for API actions such as opening, reviewing, and + commenting on pull requests. + +## Non-goals + +- **Commit signing or signature enforcement.** No signing key is minted and + git-gate does not verify commit signatures in this PRD. +- **Commit attribution or audit tables.** There is no trustworthy commit + observation event in this slice. A follow-up signing PRD owns activation keys, + control-plane verification, and any configured-author-versus-claimed-author + audit model. +- **Cryptographically vouched author identity.** `author` configures Git and + identifies the agent actor, but commit author/committer fields remain claims + under ADR 0002. +- **Forge account or token minting.** The operator creates the agent-specific + account/token out of band. Bot-bottle references an existing host secret; it + does not create, rotate, or revoke that credential. +- **Forge-side commit attribution surfaces.** No commit status, signing-key + registration, or "Verified" badge. +- **Provider-generic arbitrary prompts.** Gitea is the first supported forge. + A future provider adds typed validation and generated guidance in code rather + than accepting repository-supplied prompt text. + +## Design + +### Ownership model + +The resolved bottled agent is an agent definition composed with a bottle: + +| Part | Source | Role | +|------|--------|------| +| Author identity | agent `author` | configures Git name/email and identifies the agent's claimed author | +| Forge accounts | agent `forge-accounts` | names API origins and host token references available to this agent | +| Git repository | bottle `git-gate.repos` | configures Git transport, host verification, and push capability | +| Repository/forge association | bottle repo `forge` | optionally selects an agent forge account for guidance and API access | + +This boundary keeps identity on the agent, capability/policy on the bottle, and +transport enforcement in git-gate. + +### Agent manifest + +Agent identity and forge accounts live only in +`~/.bot-bottle/agents/.md`: + +```yaml +--- +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 +--- +``` + +`author` contains: + +- `name`: non-empty string; +- `email`: non-empty string passing the existing Git identity validation. + +The resolved values populate `user.name` and `user.email`. Existing +`git-gate.user` fields fail with migration guidance to move the values into the +selected home agent's `author` block. There is no compatibility period where a +bottle identity silently overrides the agent identity. + +`forge-accounts` is a map whose keys follow the manifest's existing kebab-case +identifier grammar (`[a-z][a-z0-9-]*`). Each account contains: + +- `url`: a canonical HTTPS Gitea API base URL; +- `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 token secret name is host configuration, not bottle configuration. The +token value is resolved only if a selected bottle repository references the +account. + +### Bottle manifest + +Repository policy remains in `~/.bot-bottle/bottles/.md`: + +```yaml +--- +git-gate: + repos: + bot-bottle: + url: ssh://git@100.78.141.42:30009/didericis/bot-bottle.git + provisioned_key: + provider: gitea + token_env: GITEA_DEPLOY_TOKEN + host_key: "ssh-ed25519 AAAA..." + forge: didericis-gitea +--- +``` + +`git-gate.repos..forge` is optional: + +- When absent, the repository behaves exactly as it does today. Bot-bottle does + not resolve a forge actor token and does not add forge-specific instructions + for that repository. +- When present, it must match a `forge-accounts` entry on the selected agent. + The resolved association enables a scoped proxy credential route and adds the + repository/forge relationship to generated system guidance. + +`provisioned_key.token_env` remains the deploy-key administration credential +from PRD 0048. It is separate from `forge-accounts.*.auth.token_secret`: the +former provisions Git push capability, while the latter performs API actions as +the agent. + +`author` and `forge-accounts` are agent-only. `git-gate.repos`, including +`forge`, is bottle-only. Validation errors point to the correct file and trust +domain rather than ignoring misplaced keys. + +### 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 corresponding home path. + +Every discovery and resolution surface uses the same home-only index: + +- agent enumeration and selectors; +- `require_agent`; +- lazy `load_for_agent`; +- default-agent/default-bottle resolution; +- dashboard and headless launch paths. + +There must be no alternate direct-path load that can still select a workspace +definition. + +Workspace instructions remain repository content (for example `AGENTS.md`), +but runtime policy, host secret references, and actor identity do not. +Programmatic in-memory manifests remain available for tests and trusted internal +composition; they are not a filesystem discovery path. + +### Forge URL validation + +The host parses and canonicalizes each referenced forge account URL before +creating any runtime resources: + +- scheme must be `https`; +- userinfo, query, and fragment are forbidden; +- hostname must be present; +- the path must be a supported Gitea API base (initially `/api/v1`, with + normalization of a trailing slash); +- visually different inputs that canonicalize to the same origin/prefix are + deduplicated; +- unsupported providers or path shapes fail closed. + +The provider is determined by typed support in bot-bottle, not by prompt text +from a repository. Adding another provider requires a validator, auth scheme, +and guidance renderer. + +### Proxy credential provisioning + +For each distinct forge account referenced by at least one selected bottle +repository, the host: + +1. Resolves `auth.token_secret` from the host environment and rejects a missing + or empty value. +2. Copies the token value only into the egress proxy's credential environment. +3. Adds an inspected route scoped to the canonical forge origin and API prefix. +4. Configures the provider authentication scheme (`token` for Gitea) so the + proxy injects authentication. + +The bottle makes an unauthenticated request to the configured HTTPS API URL. +The token is not copied into the bottle, `.gitconfig`, generated prompt, +workspace, or process environment visible to the agent. + +If several repositories reference the same forge account, they share one +credential route. Unreferenced forge accounts resolve no secret and create no +route. + +### Generated system guidance + +Bot-bottle appends a generated, non-secret section to its existing system +prompt file. It is derived from validated typed fields, not copied Markdown from +a repository. + +For each associated repository, Gitea guidance includes: + +- account alias (for example `didericis-gitea`); +- API base URL (for example `https://gitea.dideric.is/api/v1`); +- the git-gate repository name tied to that forge; +- the instruction to call the configured HTTPS API through the proxy without + reading, printing, or manually attaching an authorization token; +- the distinction that Git pushes still use the git-gate remote; +- the requirement to create/update a normal `refs/heads/` and open a + branch-backed pull request through the API; +- the prohibition on pushing `refs/for/*`, `refs/draft/*`, or + `refs/for-review/*`; +- the instruction to use the API for reviews/comments and verify returned + object state before claiming completion. + +The prompt contains neither the token value nor its `token_secret` name. +Repositories without `forge` are omitted from this section. If no selected +repository has a forge association, no forge guidance section is generated. + +### Failure and lifecycle behavior + +Forge configuration is validated before bottle creation. A bad association or +credential must not leave a partially-created bottle or proxy. + +The operator-owned token is not minted, rotated, or revoked by bot-bottle. On +teardown, stopping the egress proxy discards the activation's in-memory/runtime +copy. Later activations resolve the current host secret again. + +Logs may include the account alias, canonical API origin, and repository name. +They must never contain the token value. Errors for missing secrets name the +configuration field and host environment variable, but do not print any value. + +## Migration + +This change is intentionally breaking at the manifest trust boundary: + +1. Move each home bottle/agent `git-gate.user.name` and `.email` into the + corresponding home agent's `author`. +2. Add agent-specific `forge-accounts` only to home agent definitions. +3. Add optional `forge` associations to home bottle repository entries. +4. Move any `$CWD/.bot-bottle/agents/*.md` that should remain selectable into + `~/.bot-bottle/agents/`. Repository copies are ignored thereafter. +5. Keep repository-specific behavioral instructions in `AGENTS.md` or another + workspace instruction file; do not put runtime identity or secret references + there. + +Errors and warnings link to this migration rather than silently changing which +identity or definition is active. + +## Follow-up: signed commits and attribution + +A separate PRD will consume the trusted agent `author` introduced here and own: + +- per-activation signing key minting and sidecar isolation; +- commit-time signing through a forwarded signing capability; +- git-gate rejection of unsigned/wrong-key new commits; +- independent control-plane object-ID recomputation and signature verification; +- activation and attributed-commit audit tables; +- storage of configured agent author separately from each commit's unenforced + claimed author/committer; +- post-teardown verification and key-retention policy. + +That PRD must not reintroduce identity under git-gate or expand repository-local +manifest trust. + +## Implementation chunks + +1. **This PRD.** Establish the identity, forge, and filesystem trust boundary. +2. **Home-only definitions.** Remove cwd agents from discovery, override, + enumeration, lazy loading, defaults, and selectors. Warn on ignored cwd + agent/bottle files and provide migration guidance. +3. **Manifest schema.** Add agent-only `author` and `forge-accounts`; remove + `git-gate.user`; add optional bottle-only + `git-gate.repos..forge`. Validate account names, composition + references, field placement, and Gitea API URLs. +4. **Proxy provisioning.** Lazily resolve only referenced token secrets and + create scoped authenticated Gitea API routes without putting credentials in + the bottle. +5. **Prompt generation.** Render typed Gitea/repository workflow guidance into + the existing bot-bottle prompt path for associated repositories only. +6. **Docs and migration.** Update README examples, PRD 0011-facing discovery + documentation, agent/bottle schema docs, and error guidance. + +## Testing strategy + +- **Trust boundary:** cwd agent files are ignored with a warning, cannot + override a home agent, are absent from enumeration/selectors/defaults, and + cannot be loaded by name. Cwd bottle behavior remains home-only. +- **Agent schema:** `author` and `forge-accounts` parse; malformed identities, + non-kebab account names, unknown fields, invalid auth types, and misplaced + git-gate fields fail clearly. +- **Bottle schema:** `forge` is optional; absent associations preserve current + behavior; present associations resolve against the selected agent; unknown or + misplaced associations fail before launch. +- **URL validation:** HTTPS Gitea API bases pass and canonicalize; HTTP, + userinfo, query, fragment, missing host, unsupported paths, and unsupported + providers fail closed. +- **Secret handling:** only referenced accounts resolve environment secrets; + missing/empty secrets fail before runtime creation; token values are absent + from bottle env, prompt, generated config, logs, and workspace; token secret + names are absent from the bottle and prompt. +- **Proxy behavior:** associated API requests receive proxy-injected Gitea + authentication scoped to the configured origin/prefix; unrelated hosts and + paths receive no credential. +- **Prompt behavior:** guidance names account/API/repository associations, + branch-backed PR workflow, prohibited AGit refs, and mutation verification; + repositories without `forge` are omitted; no associations means no section. +- **Migration:** legacy `git-gate.user` fails with an `author` migration pointer; + ignored cwd definitions warn with the target home path. + +## Open questions + +- None.