feat(manifest): trusted agent forge identity and guidance
test / unit (pull_request) Successful in 49s
lint / lint (push) Failing after 1m3s
test / image-input-builds (pull_request) Successful in 1m3s
test / integration-docker (pull_request) Successful in 1m9s
refresh-image-locks / refresh (push) Successful in 1m26s
test / coverage (pull_request) Failing after 16s
tracker-policy-pr / check-pr (pull_request) Successful in 5s
test / unit (pull_request) Successful in 49s
lint / lint (push) Failing after 1m3s
test / image-input-builds (pull_request) Successful in 1m3s
test / integration-docker (pull_request) Successful in 1m9s
refresh-image-locks / refresh (push) Successful in 1m26s
test / coverage (pull_request) Failing after 16s
tracker-policy-pr / check-pr (pull_request) Successful in 5s
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:
@@ -32,6 +32,7 @@ if TYPE_CHECKING:
|
||||
EGRESS_ROUTES_IN_CONTAINER,
|
||||
Egress,
|
||||
egress_agent_env_entries,
|
||||
egress_forge_routes,
|
||||
egress_gateway_env_entries,
|
||||
egress_manifest_routes,
|
||||
egress_render_routes,
|
||||
@@ -51,6 +52,7 @@ _LAZY: dict[str, str] = {
|
||||
"EGRESS_ROUTES_FILENAME": ".service",
|
||||
"EGRESS_ROUTES_IN_CONTAINER": ".service",
|
||||
"egress_agent_env_entries": ".service",
|
||||
"egress_forge_routes": ".service",
|
||||
"egress_gateway_env_entries": ".service",
|
||||
"egress_manifest_routes": ".service",
|
||||
"egress_render_routes": ".service",
|
||||
@@ -80,6 +82,7 @@ __all__ = [
|
||||
"Egress",
|
||||
"EgressPlan",
|
||||
"EgressRoute",
|
||||
"egress_forge_routes",
|
||||
"egress_manifest_routes",
|
||||
"egress_render_routes",
|
||||
"egress_resolve_token_values",
|
||||
|
||||
@@ -26,7 +26,7 @@ from ..log import die
|
||||
from .plan import EgressPlan, EgressRoute
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..manifest import ManifestBottle
|
||||
from ..manifest import ManifestBottle, ResolvedForgeAssociation
|
||||
|
||||
|
||||
CODEX_HOST_CREDENTIAL_TOKEN_REF = "BOT_BOTTLE_CODEX_HOST_ACCESS_TOKEN"
|
||||
@@ -119,15 +119,56 @@ def egress_manifest_routes(
|
||||
return tuple(out)
|
||||
|
||||
|
||||
def egress_forge_routes(
|
||||
associations: "tuple[ResolvedForgeAssociation, ...]",
|
||||
) -> tuple[EgressRoute, ...]:
|
||||
"""Synthesize one inspected, token-authenticated egress route per distinct
|
||||
forge alias referenced by a selected git-gate repo (PRD
|
||||
prd-new-trusted-agent-forge-identity).
|
||||
|
||||
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
|
||||
the host env var named by the account's `token_secret`, resolved at launch
|
||||
from the host environment — the token never enters the bottle. Aliases that
|
||||
canonicalize to the same host share a single route (deduplicated)."""
|
||||
out: list[EgressRoute] = []
|
||||
seen_hosts: set[str] = set()
|
||||
for assoc in associations:
|
||||
acct = assoc.account
|
||||
host_key = acct.host.lower()
|
||||
if host_key in seen_hosts:
|
||||
continue
|
||||
seen_hosts.add(host_key)
|
||||
out.append(EgressRoute(
|
||||
host=acct.host,
|
||||
matches=(CoreMatchEntry(
|
||||
paths=(CorePathMatch(type="prefix", value=acct.api_prefix),),
|
||||
),),
|
||||
auth_scheme=acct.auth_type,
|
||||
token_ref=acct.token_secret,
|
||||
inspect=True,
|
||||
))
|
||||
return tuple(out)
|
||||
|
||||
|
||||
def egress_routes_for_bottle(
|
||||
bottle: ManifestBottle,
|
||||
provider_routes: tuple[EgressRoute, ...] = (),
|
||||
forge_routes: tuple[EgressRoute, ...] = (),
|
||||
) -> tuple[EgressRoute, ...]:
|
||||
manifest = egress_manifest_routes(bottle)
|
||||
provisioned_hosts = {pr.host.lower() for pr in provider_routes}
|
||||
merged = list(_default_provider_on_match(provider_routes)) + [
|
||||
r for r in manifest if r.host.lower() not in provisioned_hosts
|
||||
]
|
||||
# Provider routes (LLM API) default to redact-on-match; forge routes are
|
||||
# host-injected but keep the default DLP policy. Both take precedence over
|
||||
# a manifest route to the same host.
|
||||
reserved_hosts = (
|
||||
{pr.host.lower() for pr in provider_routes}
|
||||
| {fr.host.lower() for fr in forge_routes}
|
||||
)
|
||||
merged = (
|
||||
list(_default_provider_on_match(provider_routes))
|
||||
+ list(forge_routes)
|
||||
+ [r for r in manifest if r.host.lower() not in reserved_hosts]
|
||||
)
|
||||
return _assign_token_slots(merged)
|
||||
|
||||
|
||||
@@ -367,8 +408,9 @@ class Egress:
|
||||
slug: str,
|
||||
stage_dir: Path,
|
||||
provider_routes: tuple[EgressRoute, ...] = (),
|
||||
forge_routes: tuple[EgressRoute, ...] = (),
|
||||
) -> EgressPlan:
|
||||
routes = egress_routes_for_bottle(bottle, provider_routes)
|
||||
routes = egress_routes_for_bottle(bottle, provider_routes, forge_routes)
|
||||
log = bottle.egress.Log
|
||||
routes_path = stage_dir / EGRESS_ROUTES_FILENAME
|
||||
routes_path.write_text(egress_render_routes(routes, log=log))
|
||||
|
||||
Reference in New Issue
Block a user