docs: redesign forge identity trust boundary
tracker-policy-pr / check-pr (pull_request) Successful in 4s

This commit is contained in:
2026-07-26 18:32:07 +00:00
parent 010e253d66
commit 9c19ce4c58
+265 -77
View File
@@ -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 - **Status:** Draft
- **Author:** didericis-claude - **Author:** didericis-claude
@@ -7,34 +7,47 @@
## Summary ## Summary
Give each bottled agent a **per-activation signing key** so that every commit it Give each **host-trusted agent definition** an author identity and named forge
produces is signed in the git-gate trust boundary (outside the bottle) and accounts, and let a bottle associate each git-gate repository with one of those
recorded in bot-bottle's own **host-owned audit store**, which is the portable accounts. The resolved association provisions authenticated forge API access
source of truth. Each row cryptographically binds a commit's bytes (and their through the egress proxy and injects provider-specific workflow instructions
control-plane-recomputed SHA) to **access to that activation's signing key**, and into the agent's system prompt without exposing the token. Repository-local
binds the key to control-plane-owned activation metadata — bottle, host, agent and bottle definitions are no longer discovered because they would
manifest, agent, activation interval, retained public key — plus the commit's otherwise be able to select host credential references or impersonate an agent.
*claimed* author. The gate mints a short-lived Ed25519 key at spin-up, holds the
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 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 the bottle. The gate rejects any commit it forwards that is not signed by the
activation key; separately, the **control plane** independently recomputes each activation key; separately, the **control plane** independently recomputes each
commit's object ID and verifies its signature before recording attribution — it commit's object ID and verifies its signature before recording attribution — it
never trusts a SHA, key, or verdict asserted by the gate. never trusts a SHA, key, or verdict asserted by the gate.
This PRD deliberately does **not** enforce or vouch author/committer identity. This PRD deliberately does **not** enforce or cryptographically vouch
Author/committer name/email are recorded as claims carried inside the signed author/committer identity. The trusted agent definition supplies the configured
object; making the gate reject a mismatching author/committer is a possible name/email, while the values in each commit remain claims carried inside the
future add (see **Non-goals** and **Deferred: identity enforcement**). Push signed object; making the gate reject a mismatch is a possible future add (see
capability stays exactly as PRD 0048 deploy keys; forge subuser accounts, **Non-goals** and **Deferred: identity enforcement**). Git push capability stays
provisioned API tokens, and forge-side status/"Verified" badges remain out of exactly as PRD 0048 deploy keys. Forge API identity uses an operator-provided,
scope (a future "forge actors" PRD). agent-specific token referenced from the trusted host environment; bot-bottle
does not mint forge users or tokens.
Successor to: Successor to:
- **PRD 0027 (agent git identity, #94)** / **ADR 0002** — established that - **PRD 0027 (agent git identity, #94)** / **ADR 0002** — established that
`git-gate.user` name/email is *claimed, not vouched*. This PRD keeps that agent name/email is *claimed, not vouched*. This PRD moves that identity from
posture: it adds signed **provenance** and a durable host record, not identity the bottle/git-gate overlay to the trusted agent definition and adds signed
enforcement. **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 / - **PRD 0048 (deploy-key provisioning, #169)** — the host-side mint-at-spin-up /
revoke-at-teardown lifecycle the signing key follows. Deploy keys are revoke-at-teardown lifecycle the signing key follows. Deploy keys are
unchanged. unchanged.
@@ -50,11 +63,12 @@ The crisp property this feature provides:
> ID the control plane **recomputes** itself — to **access to this activation's > ID the control plane **recomputes** itself — to **access to this activation's
> signing key**, and binds that key to **control-plane-owned activation > signing key**, and binds that key to **control-plane-owned activation
> metadata**: bottle, host, manifest, agent, activation interval, retained public > metadata**: bottle, host, manifest, agent, activation interval, retained public
> key. An agent may author and sign arbitrary commit contents, but it cannot make > key, configured agent author. An agent may author and sign arbitrary commit
> that signature verify as a *different* activation, and it cannot choose the > contents, but it cannot make that signature verify as a *different*
> activation metadata the control plane records. The commit's author/committer > activation, and it cannot choose the activation metadata the control plane
> identity is **recorded as a claim**, not enforced or vouched. The forge remains > records. The commit's author/committer identity is **recorded separately as a
> only the repository transport/capability layer. > 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): 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: tamper-evidence over it:
- **No provenance.** Nothing ties a pushed commit to the bottle/activation that - **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. (ADR 0002); a commit could be produced anywhere.
- **No durable, portable record.** There is no host-side ledger that says "SHA - **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 *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 *[t0,t1]*, signed by key *K*," independent of any forge and surviving key
rotation. 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 ## Goals / Success Criteria
- **Per-activation signing key.** A fresh Ed25519 keypair is minted host-side at - **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 each activation; the private half lives only in the sidecar `ssh-agent`, never
in the bottle. Only `SSH_AUTH_SOCK` crosses the boundary. 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 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 signed at commit time; the SHA the agent observes is the SHA that reaches the
upstream through the gate. upstream through the gate.
@@ -115,8 +160,9 @@ tamper-evidence over it:
key, verdict, or metadata asserted by the gateway. No upstream fetch is key, verdict, or metadata asserted by the gateway. No upstream fetch is
required. required.
- **Host is the source of truth.** The audit record binds each recomputed SHA to - **Host is the source of truth.** The audit record binds each recomputed SHA to
the bottle, host, manifest, agent, activation interval, and retained public the bottle, host, manifest, agent, configured agent author, activation
key, and records the commit's claimed author/committer. interval, and retained public key, and separately records the commit's claimed
author/committer.
- **Verifiable after teardown.** The audit record retains the **full public - **Verifiable after teardown.** The audit record retains the **full public
key, fingerprint, principal, and validity interval** — enough to regenerate an key, fingerprint, principal, and validity interval** — enough to regenerate an
allowed-signers file and run `git verify-commit` long after the activation 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 once per activation (persists across restarts within that activation) and
discarded at teardown; deploy-key revocation continues to follow PRD 0048's discarded at teardown; deploy-key revocation continues to follow PRD 0048's
fail-loud discipline. fail-loud discipline.
- **Push capability unchanged.** Forge access remains PRD 0048 deploy keys; no - **Push capability unchanged.** Git transport remains PRD 0048 deploy keys.
new forge API dependency beyond 0048's existing deploy-key registration. 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 ## Non-goals
@@ -137,12 +184,9 @@ tamper-evidence over it:
**Deferred: identity enforcement** for what a future add would look like. **Deferred: identity enforcement** for what a future add would look like.
- **Cryptographically-vouched author identity.** Not claimed — see **The - **Cryptographically-vouched author identity.** Not claimed — see **The
guarantee**. guarantee**.
- **Forge subuser accounts / provisioned API tokens / PAT minting.** Dropped. - **Forge account or token minting.** Bot-bottle does not create subusers or
Gitea's `POST /users/:name/tokens` requires Basic Auth *as the target user* PATs. The operator creates the agent-specific account/token out of band and
(an admin PAT cannot mint one for another user; the only server-side path is names the host environment secret in the trusted agent definition.
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-side attribution surfaces.** No commit-status badges, no forge - **Forge-side attribution surfaces.** No commit-status badges, no forge
"Verified" badge. The latter is doubly unsuitable: it renders dynamically "Verified" badge. The latter is doubly unsuitable: it renders dynamically
against a *currently registered* key (so it would lie the moment a 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 dirty-teardown reconciliation.** As before; a separate cleanup/sync pass
handles orphans left by a crash or discarded snapshot. handles orphans left by a crash or discarded snapshot.
## Scope narrowing ## Scope evolution
This PRD started as "forge subroles" (forge subuser accounts + provisioned API This PRD started as "forge subroles" (forge subuser accounts + provisioned API
tokens + optional forge status posting + signing). Review (issue #423, comments tokens + optional forge status posting + signing). Review first narrowed it,
#5518 → #5590) narrowed it in two steps: then restored only the part needed for correct agent operation:
1. **Dropped the forge-account and API-token machinery** (#5518#5556): 1. **Dropped forge account/token provisioning** (#5518#5556): the PAT
the PAT bootstrap is not implementable as sketched (Basic-Auth-as-target-user bootstrap is not implementable as sketched (Basic-Auth-as-target-user
constraint); the signature never vouched the author anyway; and making the constraint), and the signature never vouched the author anyway. The host
host audit store the portable source of truth is a cleaner boundary that audit store remains the portable source of truth.
removes the forge-specific token lifecycle and commit-status dependence.
2. **Dropped author/committer enforcement** (#5590): rely on the audit store of 2. **Dropped author/committer enforcement** (#5590): rely on the audit store of
signed commits and the authors recorded there; gate enforcement of the signed commits and the authors recorded there; gate enforcement of the
identity fields is a possible future add, not part of this slice. 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 The resulting feature is **trusted agent identity + operator-provided forge API
host-owned, independently-verified audit record.** Forge *actors* (a per-bottle access + forge-aware prompting + signed commits + a host-owned,
account that comments/opens PRs) and *identity enforcement* (the gate rejecting a independently-verified audit record.** Identity enforcement (the gate rejecting
foreign author/committer) are each candidate future PRDs. a foreign author/committer) remains a candidate future PRD.
## Design ## Design
@@ -216,19 +269,34 @@ activation:
| Part | Value | Source | Role | | 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 | | 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.<repo>.forge` | selects the actor and generated workflow guidance for that repository |
### Manifest surface ### Manifest surface
No new top-level keys and no `git-forge`/`forge-accounts` blocks. A single Identity belongs to the agent. Repository capability and policy belong to the
opt-in flag under the existing `git-gate` key turns on per-activation signing; bottle. The following files are both host-owned:
`git-gate.user` (PRD 0027) supplies the author string as today.
```yaml ```yaml
git-gate: # ~/.bot-bottle/agents/claude.md
user: # PRD 0027 — author string; recorded, not enforced ---
author:
name: didericis-claude name: didericis-claude
email: eric+claude@dideric.is 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: signing:
enabled: true # NEW — opt-in per-activation signing + audit enabled: true # NEW — opt-in per-activation signing + audit
repos: repos:
@@ -238,15 +306,100 @@ git-gate:
provider: gitea provider: gitea
token_env: GITEA_DEPLOY_TOKEN token_env: GITEA_DEPLOY_TOKEN
host_key: "ssh-ed25519 AAAA..." 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 - `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 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 identity fields, so no knob is needed (and a knob that weakened a guarantee
was flagged as a contradiction in review). was flagged as a contradiction in review).
- `git-gate.signing` is **bottle-only** (home-only policy), rejected at the - `git-gate.signing`, `git-gate.repos`, and their `forge` associations are
agent level with a clear pointer. `git-gate.user` keeps its PRD 0027 bottle-only. `author` and `forge-accounts` are agent-only. Validation errors
agent-overlay semantics. 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 ### Signing: sign at commit time via a forwarded ssh-agent
@@ -331,6 +484,8 @@ CREATE TABLE bottled_agent_activation (
host TEXT NOT NULL, host TEXT NOT NULL,
manifest_digest TEXT NOT NULL, -- ties the record to the sealed manifest manifest_digest TEXT NOT NULL, -- ties the record to the sealed manifest
agent TEXT NOT NULL, 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_pubkey TEXT NOT NULL, -- full ssh-ed25519 public key (for verify-commit)
signing_fpr TEXT NOT NULL, -- SHA256:... fingerprint (stable handle) signing_fpr TEXT NOT NULL, -- SHA256:... fingerprint (stable handle)
principal TEXT NOT NULL, -- allowed-signers principal, e.g. the author email 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
`<principal> <signing_pubkey>` to a temporary allowed-signers file, and `<principal> <signing_pubkey>` to a temporary allowed-signers file, and
`git verify-commit` (or `ssh-keygen -Y verify`) against it. The `git verify-commit` (or `ssh-keygen -Y verify`) against it. The
`(pubkey, principal, valid_from/until)` tuple is exactly what that requires. 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 activation's `configured_author_*` columns preserve the trusted agent
"who says they wrote this" reads them, understanding they are unenforced. 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 ### 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 - **Activation:** mint a fresh Ed25519 signing keypair; load the private half
into the sidecar `ssh-agent`; write the public half into `.gitconfig` and the into the sidecar `ssh-agent`; write the public half into `.gitconfig` and the
`bottled_agent_activation` row (`active`, `valid_from` set). Deploy keys are `bottled_agent_activation` row (`active`, `valid_from` set). Deploy keys are
provisioned exactly as PRD 0048. Minting is **per activation** (a restart provisioned exactly as PRD 0048. Resolve each referenced forge actor token
re-attaches the same key; a new activation mints a new key and retires the old from the host environment and install it only in the egress proxy process.
row), so frozen snapshots don't accumulate live keys. 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 - **Teardown (fail-loud):** revoke provisioned deploy keys via the forge API
(0048); discard the signing key from the sidecar agent and set the activation (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, 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 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 - **Dirty teardown** is assumed handled; a separate cleanup/sync pass reconciles
orphaned deploy keys. 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 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 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 rejecting mismatches — with the same control-plane re-verification before
recording. This is deliberately left out now (issue #423, comment #5590); it is 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 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 ## Implementation chunks
1. **This PRD.** Sets the (narrowed) design. 1. **This PRD.** Sets the revised design and trust boundary.
2. **Manifest surface.** Add `git-gate.signing` (bottle-only; `enabled` only); 2. **Trusted definition boundary.** Remove `$CWD/.bot-bottle/agents` from
reject it at the agent level. Unit tests for parse/validation and the discovery, override, enumeration, lazy loading, and selectors. Keep both
agent-level rejection. agent and bottle definitions home-only; warn on ignored repository files.
3. **Signing pipeline.** Sidecar `ssh-agent` provisioning; forward 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, `SSH_AUTH_SOCK` into the bottle across docker, smolmachines, macOS-container,
and firecracker backends; emit the `commit.gpgsign` / `gpg.format=ssh` / and firecracker backends; emit the `commit.gpgsign` / `gpg.format=ssh` /
`user.signingkey` gitconfig. Integration test: a bottle commit is `user.signingkey` gitconfig. Integration test: a bottle commit is
`verify-commit`-valid and its SHA is unchanged through the gate; the private `verify-commit`-valid and its SHA is unchanged through the gate; the private
key is absent from the bottle. 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 (excluding upstream-reachable commits), verify each against the activation
key, reject unsigned/wrong-key with the offending SHA. Tests: unsigned key, reject unsigned/wrong-key with the offending SHA. Tests: unsigned
rejected; wrong-key rejected; pulled/merged upstream history passes; an rejected; wrong-key rejected; pulled/merged upstream history passes; an
all-signed push succeeds. A foreign-author commit that is correctly signed all-signed push succeeds. A foreign-author commit that is correctly signed
**passes the gate** (identity is not enforced here). **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); `attributed_commit` tables (PRD 0067 store, control-plane-owned per PRD 0070);
the control plane recomputes each commit's object ID and verifies the the control plane recomputes each commit's object ID and verifies the
signature before writing a row; retain full pubkey + fingerprint + principal + 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/ + 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 verdict is ignored — the row's `sha` is the recomputed ID and bytes not signed
by the activation key produce **no** row. by the activation key produce **no** row.
6. **Docs.** Glossary entry ("per-bottle signed commits"); README manifest 8. **Docs.** Glossary entries for forge account and per-bottle signed commits;
section; ADR note that signing-enabled bottles gain signed *provenance* and a README agent/bottle schema and home-only migration; ADR note that
host-owned audit record while authorship stays *claimed* (ADR 0002 unchanged). signing-enabled bottles gain signed *provenance* and a host-owned audit
record while authorship stays *claimed* (ADR 0002 unchanged).
## Testing strategy ## Testing strategy
- **Unit (must):** `git-gate.signing` parse/validation; agent-level `signing` - **Unit — trust boundary (must):** cwd agent files are ignored with a warning,
rejection. 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 - **Integration — signing (must):** end-to-end signed commit verifies with
`git verify-commit`; SHA observed in the bottle equals the SHA upstream; the `git verify-commit`; SHA observed in the bottle equals the SHA upstream; the
private key is absent from the bottle. 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 - **Control plane (must):** the control plane recomputes the object ID and
records a row for bytes genuinely signed by the activation key; a gateway- 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); 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 - **Lifecycle:** activation mints the key and writes an `active` row; teardown
retires it (`valid_until`) and revokes deploy keys fail-loud; a restart 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 re-attaches the same key (no new row); a fresh activation mints a new key and