docs: redesign forge identity trust boundary
tracker-policy-pr / check-pr (pull_request) Successful in 4s
tracker-policy-pr / check-pr (pull_request) Successful in 4s
This commit is contained in:
@@ -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
|
||||||
|
# ~/.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:
|
git-gate:
|
||||||
user: # PRD 0027 — author string; recorded, not enforced
|
|
||||||
name: didericis-claude
|
|
||||||
email: eric+claude@dideric.is
|
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user