feat(manifest): trusted agent forge identity and guidance
Implements PRD prd-new-trusted-agent-forge-identity. Moves author identity and named forge configurations onto the trusted, host-only agent definition, and lets a bottle repository optionally associate a git-gate repo with one of the agent's forge aliases. - Agent-owned identity: new `author` (name/email) and `forge-accounts` (alias -> canonical Gitea /api/v1 origin + host `token_secret` ref) on the agent manifest. `author` populates the bottle's git user.name/user.email. - Remove `git-gate.user` from both agents and bottles; fail with a migration pointer to `author`. `git-gate` is no longer accepted on an agent. - Bottle git-gate repos gain optional `forge: <alias>`, resolved against the selected agent's forge-accounts at composition (fail-closed on unknown). - Fail-closed Gitea API URL validation (https, no userinfo/query/fragment, /api/v1 base, host lowercased, trailing slash normalized, host dedup). - Proxy-held credential: synthesize one inspected, token-authenticated egress route per referenced forge alias, scoped to the origin + API prefix. The token is resolved from the host env at launch into the egress proxy only — never the bottle env, prompt, gitconfig, or workspace. - Generated, non-secret forge workflow guidance appended to the agent prompt for associated repos (API base, branch-backed PR flow, AGit-ref prohibition, mutation verification); omitted when no repo declares a forge. - Agents become home-only: cwd `.bot-bottle/agents` no longer contributes, overrides, or is selectable; warned-and-ignored like cwd bottles. - Docs: README examples, PRD 0011 supersession note, manifest schema docstring. - Tests: new test_forge_identity suite; legacy git-gate.user/cwd tests updated to the agent-owned identity model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+38
-12
@@ -117,6 +117,11 @@ class ManifestGitEntry:
|
||||
UpstreamHost: str = ""
|
||||
UpstreamPort: str = ""
|
||||
UpstreamPath: str = ""
|
||||
# Optional forge alias (PRD prd-new-trusted-agent-forge-identity). When
|
||||
# set, it must match a `forge-accounts` alias on the selected agent; the
|
||||
# composition enables a scoped proxy-held API credential and forge
|
||||
# workflow guidance for this repo. Empty = no forge association.
|
||||
Forge: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_repos_entry(
|
||||
@@ -139,10 +144,10 @@ class ManifestGitEntry:
|
||||
label = f"git-gate.repos[{repo_name!r}]"
|
||||
d = as_json_object(raw, f"bottle '{bottle_name}' {label}")
|
||||
for k in d:
|
||||
if k not in {"url", "key", "host_key"}:
|
||||
if k not in {"url", "key", "host_key", "forge"}:
|
||||
raise ManifestError(
|
||||
f"bottle '{bottle_name}' {label} has unknown key {k!r}; "
|
||||
f"allowed: url, key, host_key"
|
||||
f"allowed: url, key, host_key, forge"
|
||||
)
|
||||
upstream = d.get("url")
|
||||
if not isinstance(upstream, str) or not upstream:
|
||||
@@ -150,6 +155,21 @@ class ManifestGitEntry:
|
||||
f"bottle '{bottle_name}' {label} missing required string field 'url'"
|
||||
)
|
||||
|
||||
forge = d.get("forge", "")
|
||||
if not isinstance(forge, str):
|
||||
raise ManifestError(
|
||||
f"bottle '{bottle_name}' {label} forge must be a string "
|
||||
f"(was {type(forge).__name__})"
|
||||
)
|
||||
if forge and not _GIT_NAME_RE.match(forge):
|
||||
# forge aliases follow the kebab-case identifier grammar; the
|
||||
# cross-check against the agent's forge-accounts happens at
|
||||
# composition time (it needs the resolved agent).
|
||||
raise ManifestError(
|
||||
f"bottle '{bottle_name}' {label} forge {forge!r} is not a "
|
||||
f"valid forge alias; allowed characters: A-Z a-z 0-9 . _ -"
|
||||
)
|
||||
|
||||
if "key" not in d:
|
||||
raise ManifestError(
|
||||
f"bottle '{bottle_name}' {label} missing required 'key' block"
|
||||
@@ -176,6 +196,7 @@ class ManifestGitEntry:
|
||||
UpstreamHost=host,
|
||||
UpstreamPort=port,
|
||||
UpstreamPath=path,
|
||||
Forge=forge,
|
||||
)
|
||||
|
||||
|
||||
@@ -286,21 +307,26 @@ class ManifestGitUser:
|
||||
def parse_git_gate_config(
|
||||
bottle_name: str,
|
||||
raw: object,
|
||||
) -> tuple[tuple[ManifestGitEntry, ...], ManifestGitUser]:
|
||||
) -> tuple[ManifestGitEntry, ...]:
|
||||
"""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)."""
|
||||
d = as_json_object(raw, f"bottle '{bottle_name}' git-gate")
|
||||
if "user" in d:
|
||||
raise ManifestError(
|
||||
f"bottle '{bottle_name}' git-gate.user is no longer supported "
|
||||
f"(PRD prd-new-trusted-agent-forge-identity). Move name/email into "
|
||||
f"the selected home agent's 'author' block:\n"
|
||||
f" author:\n name: <name>\n email: <email>\n"
|
||||
f"Identity is agent-owned; git-gate now carries only transport "
|
||||
f"policy (repos)."
|
||||
)
|
||||
for k in d:
|
||||
if k not in {"user", "repos"}:
|
||||
if k != "repos":
|
||||
raise ManifestError(
|
||||
f"bottle '{bottle_name}' git-gate has unknown key {k!r}; "
|
||||
f"allowed: user, repos"
|
||||
f"allowed: repos"
|
||||
)
|
||||
|
||||
git_user = (
|
||||
ManifestGitUser.from_dict(bottle_name, d["user"])
|
||||
if "user" in d
|
||||
else ManifestGitUser()
|
||||
)
|
||||
|
||||
git: tuple[ManifestGitEntry, ...] = ()
|
||||
repos_raw = d.get("repos")
|
||||
if repos_raw is not None:
|
||||
@@ -311,4 +337,4 @@ def parse_git_gate_config(
|
||||
)
|
||||
validate_unique_git_names(bottle_name, git)
|
||||
|
||||
return git, git_user
|
||||
return git
|
||||
|
||||
Reference in New Issue
Block a user