docs(prd): assign PRD number 0082
prd-number-check / require-numbered-prds (pull_request) Successful in 13s
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / integration-docker (pull_request) Successful in 1m5s
test / image-input-builds (pull_request) Successful in 1m22s
test / unit (pull_request) Failing after 10m22s
lint / lint (push) Failing after 10m24s
test / coverage (pull_request) Has been skipped

CI (prd-number-check) rejects unnumbered prd-new-*.md on merge to main.
Rename docs/prds/prd-new-trusted-agent-forge-identity.md to
0082-trusted-agent-forge-identity.md (0081 is claimed by #517/#519) and
update the in-repo 'PRD prd-new-trusted-agent-forge-identity' citations to
'PRD 0082'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 02:07:20 +00:00
committed by didericis
parent 91f6cbabdc
commit ccfb935b19
19 changed files with 31 additions and 31 deletions
+1 -1
View File
@@ -196,7 +196,7 @@ bot-bottle start <agent> # builds the image on first run, drops you into claud
## Manifest ## Manifest
Bottles and agents are Markdown files with YAML frontmatter under `~/.bot-bottle/`. The Markdown body is the system prompt. Both bottles and agents are **home-only**: they live under `~/.bot-bottle/bottles/` and `~/.bot-bottle/agents/`. A `<repo>/.bot-bottle/agents/<name>.md` shipped by a workspace is ignored with a warning — since an agent may select a host identity and forge secret, checked-out content must not define one (PRD prd-new-trusted-agent-forge-identity). Keep repo-specific behavioral instructions in `AGENTS.md` instead. Bottles and agents are Markdown files with YAML frontmatter under `~/.bot-bottle/`. The Markdown body is the system prompt. Both bottles and agents are **home-only**: they live under `~/.bot-bottle/bottles/` and `~/.bot-bottle/agents/`. A `<repo>/.bot-bottle/agents/<name>.md` shipped by a workspace is ignored with a warning — since an agent may select a host identity and forge secret, checked-out content must not define one (PRD 0082). Keep repo-specific behavioral instructions in `AGENTS.md` instead.
Identity is **agent-owned**: the author name/email and named forge accounts live on the agent, not under `git-gate` (which now carries only Git transport policy). A bottle repo may optionally name one of the selected agent's forge aliases via `forge:`. Identity is **agent-owned**: the author name/email and named forge accounts live on the agent, not under `git-gate` (which now carries only Git transport policy). A bottle repo may optionally name one of the selected agent's forge aliases via `forge:`.
+2 -2
View File
@@ -76,7 +76,7 @@ def prepare_agent_state_dir(slug: str, manifest: Manifest) -> tuple[Path, Path]:
For repositories associated with a forge, appends generated, non-secret For repositories associated with a forge, appends generated, non-secret
provider-specific workflow guidance to the prompt (PRD provider-specific workflow guidance to the prompt (PRD
prd-new-trusted-agent-forge-identity). The guidance carries neither the 0082). The guidance carries neither the
token value nor its `token_secret` name.""" token value nor its `token_secret` name."""
agent = manifest.agent agent = manifest.agent
agent_dir = agent_state_dir(slug) agent_dir = agent_state_dir(slug)
@@ -102,7 +102,7 @@ def prepare_egress(
) -> EgressPlan: ) -> EgressPlan:
"""Build the egress plan, adding a scoped, proxy-held Gitea API route for """Build the egress plan, adding a scoped, proxy-held Gitea API route for
each forge alias referenced by a selected git-gate repo (PRD each forge alias referenced by a selected git-gate repo (PRD
prd-new-trusted-agent-forge-identity). The token is resolved from the host 0082). The token is resolved from the host
env at launch and never enters the bottle.""" env at launch and never enters the bottle."""
egress_dir = egress_state_dir(slug) egress_dir = egress_state_dir(slug)
egress_dir.mkdir(parents=True, exist_ok=True) egress_dir.mkdir(parents=True, exist_ok=True)
+1 -1
View File
@@ -383,7 +383,7 @@ def _peek_agent_bottle(manifest: ManifestIndex, agent_name: str) -> str:
from ...manifest.loader import scan_agent_names from ...manifest.loader import scan_agent_names
from ...yaml_subset import YamlSubsetError, parse_frontmatter from ...yaml_subset import YamlSubsetError, parse_frontmatter
# Agents are home-only (PRD prd-new-trusted-agent-forge-identity). # Agents are home-only (PRD 0082).
home_agents = scan_agent_names(manifest.home_md / "agents") home_agents = scan_agent_names(manifest.home_md / "agents")
path = home_agents.get(agent_name) path = home_agents.get(agent_name)
if path is None: if path is None:
+1 -1
View File
@@ -124,7 +124,7 @@ def egress_forge_routes(
) -> tuple[EgressRoute, ...]: ) -> tuple[EgressRoute, ...]:
"""Synthesize one inspected, token-authenticated egress route per distinct """Synthesize one inspected, token-authenticated egress route per distinct
forge alias referenced by a selected git-gate repo (PRD forge alias referenced by a selected git-gate repo (PRD
prd-new-trusted-agent-forge-identity). 0082).
The route is scoped to the canonical forge origin (host) and API prefix The route is scoped to the canonical forge origin (host) and API prefix
(`/api/v1`): the proxy injects the Gitea `token` scheme using the value of (`/api/v1`): the proxy injects the Gitea `token` scheme using the value of
+1 -1
View File
@@ -1,7 +1,7 @@
"""Manifest dataclasses (PRD 0011 layout). """Manifest dataclasses (PRD 0011 layout).
Reads the per-file manifest tree (home-only — Reads the per-file manifest tree (home-only —
PRD prd-new-trusted-agent-forge-identity): PRD 0082):
$HOME/.bot-bottle/bottles/<name>.md — one bottle per file $HOME/.bot-bottle/bottles/<name>.md — one bottle per file
$HOME/.bot-bottle/agents/<name>.md — agents $HOME/.bot-bottle/agents/<name>.md — agents
+2 -2
View File
@@ -119,7 +119,7 @@ class ManifestAgent:
bottle: str = "" bottle: str = ""
skills: tuple[str, ...] = () skills: tuple[str, ...] = ()
prompt: str = "" prompt: str = ""
# Agent-owned identity (PRD prd-new-trusted-agent-forge-identity). # Agent-owned identity (PRD 0082).
# `author` populates the bottle's git user.name/user.email; # `author` populates the bottle's git user.name/user.email;
# `forge_accounts` maps a forge alias to a canonical Gitea API origin and # `forge_accounts` maps a forge alias to a canonical Gitea API origin and
# a host token reference. Both live only on the agent — never under # a host token reference. Both live only on the agent — never under
@@ -138,7 +138,7 @@ class ManifestAgent:
if "git-gate" in d: if "git-gate" in d:
raise ManifestError( raise ManifestError(
f"agent '{name}' has a 'git-gate' block, which is no longer " f"agent '{name}' has a 'git-gate' block, which is no longer "
f"accepted on an agent (PRD prd-new-trusted-agent-forge-identity). " f"accepted on an agent (PRD 0082). "
f"Move git-gate.user name/email into the 'author' block; " f"Move git-gate.user name/email into the 'author' block; "
f"git-gate.repos stays on the bottle." f"git-gate.repos stays on the bottle."
) )
+1 -1
View File
@@ -109,7 +109,7 @@ class ManifestBottle:
# `git_user` is now an internal resolved carrier populated from the # `git_user` is now an internal resolved carrier populated from the
# selected agent's `author` at composition time — it is never parsed # selected agent's `author` at composition time — it is never parsed
# from the bottle manifest (PRD prd-new-trusted-agent-forge-identity). # from the bottle manifest (PRD 0082).
git: tuple[ManifestGitEntry, ...] = () git: tuple[ManifestGitEntry, ...] = ()
git_user = ManifestGitUser() git_user = ManifestGitUser()
git_raw = d.get("git-gate") git_raw = d.get("git-gate")
+1 -1
View File
@@ -1,4 +1,4 @@
"""Agent-owned author identity and forge accounts (PRD prd-new-trusted-agent-forge-identity). """Agent-owned author identity and forge accounts (PRD 0082).
`ManifestAuthor` is the agent's git author identity (name/email) — it moved off `ManifestAuthor` is the agent's git author identity (name/email) — it moved off
`git-gate.user` (PRD 0027 / ADR 0002) and onto the trusted agent definition. `git-gate.user` (PRD 0027 / ADR 0002) and onto the trusted agent definition.
+3 -3
View File
@@ -117,7 +117,7 @@ class ManifestGitEntry:
UpstreamHost: str = "" UpstreamHost: str = ""
UpstreamPort: str = "" UpstreamPort: str = ""
UpstreamPath: str = "" UpstreamPath: str = ""
# Optional forge alias (PRD prd-new-trusted-agent-forge-identity). When # Optional forge alias (PRD 0082). When
# set, it must match a `forge-accounts` alias on the selected agent; the # set, it must match a `forge-accounts` alias on the selected agent; the
# composition enables a scoped proxy-held API credential and forge # composition enables a scoped proxy-held API credential and forge
# workflow guidance for this repo. Empty = no forge association. # workflow guidance for this repo. Empty = no forge association.
@@ -309,12 +309,12 @@ def parse_git_gate_config(
raw: object, raw: object,
) -> tuple[ManifestGitEntry, ...]: ) -> tuple[ManifestGitEntry, ...]:
"""Parse `git-gate` on a bottle. Only `repos` is accepted; `git-gate.user` """Parse `git-gate` on a bottle. Only `repos` is accepted; `git-gate.user`
moved to the agent's `author` block (PRD prd-new-trusted-agent-forge-identity).""" moved to the agent's `author` block (PRD 0082)."""
d = as_json_object(raw, f"bottle '{bottle_name}' git-gate") d = as_json_object(raw, f"bottle '{bottle_name}' git-gate")
if "user" in d: if "user" in d:
raise ManifestError( raise ManifestError(
f"bottle '{bottle_name}' git-gate.user is no longer supported " f"bottle '{bottle_name}' git-gate.user is no longer supported "
f"(PRD prd-new-trusted-agent-forge-identity). Move name/email into " f"(PRD 0082). Move name/email into "
f"the selected home agent's 'author' block:\n" f"the selected home agent's 'author' block:\n"
f" author:\n name: <name>\n email: <email>\n" f" author:\n name: <name>\n email: <email>\n"
f"Identity is agent-owned; git-gate now carries only transport " f"Identity is agent-owned; git-gate now carries only transport "
+6 -6
View File
@@ -52,7 +52,7 @@ def _warn_ignored_cwd_dir(cwd_dir: Path, kind: str, home_path: str) -> None:
warn( warn(
f"ignoring {kind[:-1]} file(s) under {stale}: {names}. " f"ignoring {kind[:-1]} file(s) under {stale}: {names}. "
f"{kind.capitalize()} can only live under {home_path} " f"{kind.capitalize()} can only live under {home_path} "
f"(PRD prd-new-trusted-agent-forge-identity). Move them or delete." f"(PRD 0082). Move them or delete."
) )
@@ -62,7 +62,7 @@ def _compose_manifest(
raw_bottle: "ManifestBottle", raw_bottle: "ManifestBottle",
) -> "Manifest": ) -> "Manifest":
"""Build the single-value Manifest from the selected agent and its """Build the single-value Manifest from the selected agent and its
effective bottle (PRD prd-new-trusted-agent-forge-identity): effective bottle (PRD 0082):
- the agent's `author` populates the bottle's git user.name/user.email; - the agent's `author` populates the bottle's git user.name/user.email;
- each git-gate repo's `forge` alias is resolved against the agent's - each git-gate repo's `forge` alias is resolved against the agent's
@@ -192,7 +192,7 @@ class ManifestIndex:
$HOME/.bot-bottle/agents/<name>.md agents (home-only) $HOME/.bot-bottle/agents/<name>.md agents (home-only)
Both agents and bottles are home-only Both agents and bottles are home-only
(PRD prd-new-trusted-agent-forge-identity): a `bottles/` or `agents/` (PRD 0082): a `bottles/` or `agents/`
subdir under $CWD is logged as a warning and ignored the filesystem subdir under $CWD is logged as a warning and ignored the filesystem
layout IS the trust boundary, since an agent may now select a host layout IS the trust boundary, since an agent may now select a host
identity and forge secret. identity and forge secret.
@@ -248,7 +248,7 @@ class ManifestIndex:
if cwd_dir is not None: if cwd_dir is not None:
_warn_ignored_cwd_dir(cwd_dir, "bottles", "$HOME/.bot-bottle/bottles/") _warn_ignored_cwd_dir(cwd_dir, "bottles", "$HOME/.bot-bottle/bottles/")
# Agents became home-only in # Agents became home-only in
# PRD prd-new-trusted-agent-forge-identity: a cwd agent file that # PRD 0082: a cwd agent file that
# once shadowed a home agent could select a host identity/secret, # once shadowed a home agent could select a host identity/secret,
# so it is now ignored with a migration pointer. # so it is now ignored with a migration pointer.
_warn_ignored_cwd_dir(cwd_dir, "agents", "$HOME/.bot-bottle/agents/") _warn_ignored_cwd_dir(cwd_dir, "agents", "$HOME/.bot-bottle/agents/")
@@ -295,7 +295,7 @@ class ManifestIndex:
filenames without reading their content. In eager mode (from filenames without reading their content. In eager mode (from
from_json_obj) it returns the pre-parsed agents' names. from_json_obj) it returns the pre-parsed agents' names.
Agents are home-only (PRD prd-new-trusted-agent-forge-identity): cwd Agents are home-only (PRD 0082): cwd
agent files never contribute names.""" agent files never contribute names."""
if self.home_md is not None: if self.home_md is not None:
return sorted(scan_agent_names(self.home_md / "agents").keys()) return sorted(scan_agent_names(self.home_md / "agents").keys())
@@ -351,7 +351,7 @@ class ManifestIndex:
"""Lazy path (resolve/from_md_dirs): read and parse the agent file and """Lazy path (resolve/from_md_dirs): read and parse the agent file and
its bottle chain from disk for the first time here.""" its bottle chain from disk for the first time here."""
assert self.home_md is not None # guaranteed by load_for_agent dispatch assert self.home_md is not None # guaranteed by load_for_agent dispatch
# Agents are home-only (PRD prd-new-trusted-agent-forge-identity): # Agents are home-only (PRD 0082):
# a cwd agent file must not select a host identity or forge secret. # a cwd agent file must not select a host identity or forge secret.
home_agents = scan_agent_names(self.home_md / "agents") home_agents = scan_agent_names(self.home_md / "agents")
+1 -1
View File
@@ -23,7 +23,7 @@ BOTTLE_KEYS = frozenset(
) )
AGENT_KEYS_REQUIRED: frozenset[str] = frozenset() AGENT_KEYS_REQUIRED: frozenset[str] = frozenset()
# `author` / `forge-accounts` are agent-owned identity (PRD # `author` / `forge-accounts` are agent-owned identity (PRD
# prd-new-trusted-agent-forge-identity). `git-gate` is no longer accepted on an # 0082). `git-gate` is no longer accepted on an
# agent: `git-gate.user` moved to `author`, and `git-gate.repos` is bottle-only. # agent: `git-gate.user` moved to `author`, and `git-gate.repos` is bottle-only.
AGENT_KEYS_OPTIONAL = frozenset({"bottle", "skills", "author", "forge-accounts"}) AGENT_KEYS_OPTIONAL = frozenset({"bottle", "skills", "author", "forge-accounts"})
+1 -1
View File
@@ -4,7 +4,7 @@
- **Author:** didericis - **Author:** didericis
- **Created:** 2026-05-24 - **Created:** 2026-05-24
> **Superseded in part by PRD prd-new-trusted-agent-forge-identity.** > **Superseded in part by PRD 0082.**
> The `$CWD/.bot-bottle/agents/<name>.md` discovery/override path described > The `$CWD/.bot-bottle/agents/<name>.md` discovery/override path described
> below is removed: agents are now **home-only**, like bottles. Once an agent > below is removed: agents are now **home-only**, like bottles. Once an agent
> definition can select a host identity (`author`) and a host forge secret > definition can select a host identity (`author`) and a host forge secret
@@ -1,4 +1,4 @@
# PRD prd-new: Trusted agent forge identity and guidance # PRD 0082: Trusted agent forge identity and guidance
- **Status:** Accepted - **Status:** Accepted
- **Author:** didericis-claude - **Author:** didericis-claude
+1 -1
View File
@@ -1,5 +1,5 @@
"""Unit: trusted agent forge identity & guidance """Unit: trusted agent forge identity & guidance
(PRD prd-new-trusted-agent-forge-identity). (PRD 0082).
Covers the net-new surface: agent `author`, agent `forge-accounts` (Gitea API Covers the net-new surface: agent `author`, agent `forge-accounts` (Gitea API
URL validation + host token reference), the repoforge association resolved at URL validation + host token reference), the repoforge association resolved at
+1 -1
View File
@@ -1,5 +1,5 @@
"""Unit: agent-owned identity via `author` (PRD """Unit: agent-owned identity via `author` (PRD
prd-new-trusted-agent-forge-identity). 0082).
Identity is agent-only now: an agent file declares an `author` block Identity is agent-only now: an agent file declares an `author` block
(name + email, both required) and at `ManifestIndex.load_for_agent()` (name + email, both required) and at `ManifestIndex.load_for_agent()`
+2 -2
View File
@@ -132,7 +132,7 @@ class TestExtendsEnvMerge(unittest.TestCase):
class TestExtendsGitMerge(unittest.TestCase): class TestExtendsGitMerge(unittest.TestCase):
"""git-gate.repos merges by name, with same-name child entries """git-gate.repos merges by name, with same-name child entries
merging field-by-field (child wins). Bottles no longer carry a user merging field-by-field (child wins). Bottles no longer carry a user
identity (PRD prd-new-trusted-agent-forge-identity), so only repos identity (PRD 0082), so only repos
merging is meaningful across extends chains.""" merging is meaningful across extends chains."""
_GIT_ENTRY_A = {"url": "ssh://git@host-a/a.git", "key": {"provider": "static", "path": "/dev/null"}} _GIT_ENTRY_A = {"url": "ssh://git@host-a/a.git", "key": {"provider": "static", "path": "/dev/null"}}
@@ -339,7 +339,7 @@ class TestExtendsEgressMerge(unittest.TestCase):
class TestExtendsNoBottleUserIdentity(unittest.TestCase): class TestExtendsNoBottleUserIdentity(unittest.TestCase):
"""Bottles no longer carry a user identity (PRD """Bottles no longer carry a user identity (PRD
prd-new-trusted-agent-forge-identity): identity moved to the agent's 0082): identity moved to the agent's
`author` block. `git-gate.user` on a bottle is rejected outright, and `author` block. `git-gate.user` on a bottle is rejected outright, and
`git_user` is always empty across an extends chain.""" `git_user` is always empty across an extends chain."""
+1 -1
View File
@@ -1,5 +1,5 @@
"""Unit: agent `author` identity -> bottle.git_user (PRD """Unit: agent `author` identity -> bottle.git_user (PRD
prd-new-trusted-agent-forge-identity). 0082).
Identity moved off `git-gate.user` (bottle) onto the trusted agent's Identity moved off `git-gate.user` (bottle) onto the trusted agent's
`author` block. At `load_for_agent` the agent's author populates the `author` block. At `load_for_agent` the agent's author populates the
+2 -2
View File
@@ -72,7 +72,7 @@ class _LazyCase(unittest.TestCase):
class TestAllAgentNamesLazy(_LazyCase): class TestAllAgentNamesLazy(_LazyCase):
def test_cwd_agents_ignored_home_only(self) -> None: def test_cwd_agents_ignored_home_only(self) -> None:
# Agents are home-only (PRD prd-new-trusted-agent-forge-identity): # Agents are home-only (PRD 0082):
# a cwd agents/ dir is warned-and-ignored, so only the home agent # a cwd agents/ dir is warned-and-ignored, so only the home agent
# appears in all_agent_names. # appears in all_agent_names.
_write(self.home_cb / "bottles" / "dev.md", _BOTTLE_DEV) _write(self.home_cb / "bottles" / "dev.md", _BOTTLE_DEV)
@@ -100,7 +100,7 @@ class TestRequireAgentLazy(_LazyCase):
self.resolve().require_agent("alpha") # no raise self.resolve().require_agent("alpha") # no raise
def test_cwd_only_agent_not_selectable(self) -> None: def test_cwd_only_agent_not_selectable(self) -> None:
# Agents are home-only (PRD prd-new-trusted-agent-forge-identity): # Agents are home-only (PRD 0082):
# a cwd-only agent file is never selectable, so require_agent raises. # a cwd-only agent file is never selectable, so require_agent raises.
_write(self.home_cb / "agents" / "alpha.md", _AGENT) _write(self.home_cb / "agents" / "alpha.md", _AGENT)
_write(self.cwd_cb / "agents" / "beta.md", _AGENT) _write(self.cwd_cb / "agents" / "beta.md", _AGENT)
+2 -2
View File
@@ -111,7 +111,7 @@ class TestAgentFileParses(_ResolveCase):
class TestCwdAgentIgnoredHomeWins(_ResolveCase): class TestCwdAgentIgnoredHomeWins(_ResolveCase):
"""SC #3 (revised, PRD prd-new-trusted-agent-forge-identity): agents """SC #3 (revised, PRD 0082): agents
are home-only. A cwd agent file with the same name as a home agent no are home-only. A cwd agent file with the same name as a home agent no
longer wins it is warned-and-ignored, so the HOME agent's prompt is longer wins it is warned-and-ignored, so the HOME agent's prompt is
used and the home bottle stays intact.""" used and the home bottle stays intact."""
@@ -141,7 +141,7 @@ class TestCwdAgentIgnoredHomeWins(_ResolveCase):
class TestCwdBottlesIgnored(_ResolveCase): class TestCwdBottlesIgnored(_ResolveCase):
"""SC #4: a bottles/ dir under $CWD is ignored (with a warn). """SC #4: a bottles/ dir under $CWD is ignored (with a warn).
The home bottle still wins. Under The home bottle still wins. Under
PRD prd-new-trusted-agent-forge-identity a cwd agents/ dir is also PRD 0082 a cwd agents/ dir is also
ignored, so $CWD contributes nothing the filesystem layout is the ignored, so $CWD contributes nothing the filesystem layout is the
trust boundary.""" trust boundary."""