From cbd92347d3bfdb32a9be0f601039a8022d498c22 Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 26 Jul 2026 18:32:07 +0000 Subject: [PATCH] docs: redesign forge identity trust boundary --- .../prd-new-signed-commits-attribution.md | 344 ++++++++++++++---- 1 file changed, 266 insertions(+), 78 deletions(-) diff --git a/docs/prds/prd-new-signed-commits-attribution.md b/docs/prds/prd-new-signed-commits-attribution.md index 108dad00..37fd0585 100644 --- a/docs/prds/prd-new-signed-commits-attribution.md +++ b/docs/prds/prd-new-signed-commits-attribution.md @@ -1,4 +1,4 @@ -# PRD prd-new: Per-bottle signed commits & audit attribution +# PRD prd-new: Trusted agent forge identity, signed commits & audit attribution - **Status:** Draft - **Author:** didericis-claude @@ -7,34 +7,47 @@ ## Summary -Give each bottled agent a **per-activation signing key** so that every commit it -produces is signed in the git-gate trust boundary (outside the bottle) and -recorded in bot-bottle's own **host-owned audit store**, which is the portable -source of truth. Each row cryptographically binds a commit's bytes (and their -control-plane-recomputed SHA) to **access to that activation's signing key**, and -binds the key to control-plane-owned activation metadata — bottle, host, -manifest, agent, activation interval, retained public key — plus the commit's -*claimed* author. The gate mints a short-lived Ed25519 key at spin-up, holds the +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 vouch author/committer identity. -Author/committer name/email are recorded as claims carried inside the signed -object; making the gate reject a mismatching author/committer is a possible -future add (see **Non-goals** and **Deferred: identity enforcement**). Push -capability stays exactly as PRD 0048 deploy keys; forge subuser accounts, -provisioned API tokens, and forge-side status/"Verified" badges remain out of -scope (a future "forge actors" PRD). +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 - `git-gate.user` name/email is *claimed, not vouched*. This PRD keeps that - posture: it adds signed **provenance** and a durable host record, not identity - enforcement. + 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. @@ -50,11 +63,12 @@ The crisp property this feature provides: > ID the control plane **recomputes** itself — to **access to this activation's > signing key**, and binds that key to **control-plane-owned activation > metadata**: bottle, host, manifest, agent, activation interval, retained public -> key. An agent may author and sign arbitrary commit contents, but it cannot make -> that signature verify as a *different* activation, and it cannot choose the -> activation metadata the control plane records. The commit's author/committer -> identity is **recorded as a claim**, not enforced or vouched. The forge remains -> only the repository transport/capability layer. +> 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): @@ -88,18 +102,49 @@ 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. `git-gate.user` name/email is forgeable and cosmetic + 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. @@ -115,8 +160,9 @@ tamper-evidence over it: key, verdict, or metadata asserted by the gateway. No upstream fetch is required. - **Host is the source of truth.** The audit record binds each recomputed SHA to - the bottle, host, manifest, agent, activation interval, and retained public - key, and records the commit's claimed author/committer. + 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 @@ -125,8 +171,9 @@ tamper-evidence over it: 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.** Forge access remains PRD 0048 deploy keys; no - new forge API dependency beyond 0048's existing deploy-key registration. +- **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 @@ -137,12 +184,9 @@ tamper-evidence over it: **Deferred: identity enforcement** for what a future add would look like. - **Cryptographically-vouched author identity.** Not claimed — see **The guarantee**. -- **Forge subuser accounts / provisioned API tokens / PAT minting.** Dropped. - Gitea's `POST /users/:name/tokens` requires Basic Auth *as the target user* - (an admin PAT cannot mint one for another user; the only server-side path is - the `gitea admin user generate-access-token` CLI), so a token-minting - bootstrap is a design in its own right (issue #423, comments #5518 / #5554). - This PRD needs no subrole API token, so that bootstrap problem does not arise. +- **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 @@ -153,25 +197,34 @@ tamper-evidence over it: dirty-teardown reconciliation.** As before; a separate cleanup/sync pass handles orphans left by a crash or discarded snapshot. -## Scope narrowing +## Scope evolution This PRD started as "forge subroles" (forge subuser accounts + provisioned API -tokens + optional forge status posting + signing). Review (issue #423, comments -#5518 → #5590) narrowed it in two steps: +tokens + optional forge status posting + signing). Review first narrowed it, +then restored only the part needed for correct agent operation: -1. **Dropped the forge-account and API-token machinery** (#5518 → #5556): - the PAT bootstrap is not implementable as sketched (Basic-Auth-as-target-user - constraint); the signature never vouched the author anyway; and making the - host audit store the portable source of truth is a cleaner boundary that - removes the forge-specific token lifecycle and commit-status dependence. +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. -What remains is the core that stands on its own: **signed commits + a -host-owned, independently-verified audit record.** Forge *actors* (a per-bottle -account that comments/opens PRs) and *identity enforcement* (the gate rejecting a -foreign author/committer) are each candidate future PRDs. +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 @@ -216,19 +269,34 @@ 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 | `git-gate.user` (PRD 0027 overlay) | written into commits and **recorded** as a claim; **not** enforced | +| 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 -No new top-level keys and no `git-forge`/`forge-accounts` blocks. A single -opt-in flag under the existing `git-gate` key turns on per-activation signing; -`git-gate.user` (PRD 0027) supplies the author string as today. +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: - user: # PRD 0027 — author string; recorded, not enforced - name: didericis-claude - email: eric+claude@dideric.is signing: enabled: true # NEW — opt-in per-activation signing + audit repos: @@ -238,15 +306,100 @@ git-gate: 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` is **bottle-only** (home-only policy), rejected at the - agent level with a clear pointer. `git-gate.user` keeps its PRD 0027 - agent-overlay semantics. +- `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 @@ -331,6 +484,8 @@ CREATE TABLE bottled_agent_activation ( 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 @@ -359,24 +514,32 @@ 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 -recorded author/committer columns are the *claim*; a consumer that wants to know -"who says they wrote this" reads them, understanding they are unenforced. +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 -Follows PRD 0048, minus the API-token kind (dropped): +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. 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. + 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. + 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. @@ -384,7 +547,8 @@ Follows PRD 0048, minus the API-token kind (dropped): 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 `git-gate.user`, +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 @@ -394,23 +558,35 @@ validating signing broker in front of the key would be required for the latter. ## Implementation chunks -1. **This PRD.** Sets the (narrowed) design. -2. **Manifest surface.** Add `git-gate.signing` (bottle-only; `enabled` only); - reject it at the agent level. Unit tests for parse/validation and the - agent-level rejection. -3. **Signing pipeline.** Sidecar `ssh-agent` provisioning; forward +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. -4. **Gate pre-forward signature check.** Compute the newly-introduced set +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). -5. **Control-plane verification + audit.** `bottled_agent_activation` / +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 + @@ -418,14 +594,24 @@ validating signing broker in front of the key would be required for the latter. + a post-teardown `verify-commit` helper. Tests: a gateway-claimed SHA/key/ verdict is ignored — the row's `sha` is the recomputed ID and bytes not signed by the activation key produce **no** row. -6. **Docs.** Glossary entry ("per-bottle signed commits"); README manifest - section; ADR note that signing-enabled bottles gain signed *provenance* and a - host-owned audit record while authorship stays *claimed* (ADR 0002 unchanged). +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 (must):** `git-gate.signing` parse/validation; agent-level `signing` - rejection. +- **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. @@ -436,7 +622,9 @@ validating signing broker in front of the key would be required for the latter. - **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. + 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