a2edaf8694
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>
141 lines
5.4 KiB
Python
141 lines
5.4 KiB
Python
"""Manifest dataclasses (PRD 0011 layout).
|
|
|
|
Reads the per-file manifest tree (home-only —
|
|
PRD prd-new-trusted-agent-forge-identity):
|
|
|
|
$HOME/.bot-bottle/bottles/<name>.md — one bottle per file
|
|
$HOME/.bot-bottle/agents/<name>.md — agents
|
|
|
|
Each file is Markdown with YAML frontmatter. The frontmatter holds
|
|
the structured config (see schema below); for agents the body is
|
|
the system prompt, for bottles the body is human documentation
|
|
(ignored by the parser).
|
|
|
|
Bottle schema (frontmatter):
|
|
extends: <bottle-name> # optional (PRD 0025)
|
|
env: { <NAME>: <env-entry>, ... }
|
|
git-gate: # optional (PRD 0047)
|
|
repos: { <name>: <git-gate-entry>, ... } # optional
|
|
# git-gate-entry keys: url, key, host_key, forge
|
|
# `forge`: optional alias into the selected agent's forge-accounts
|
|
egress: { routes: [ <egress-route>, ... ] }
|
|
# route keys: host, matches, auth, role, dlp
|
|
supervise: <bool> # optional (default true)
|
|
nested_containers: <bool> # optional (default false)
|
|
|
|
Agent schema (frontmatter):
|
|
bottle: <bottle-name> # optional
|
|
skills: [ <skill-name>, ... ] # optional
|
|
author: # optional; agent git identity
|
|
name: <str> # required when author is present
|
|
email: <str> # required when author is present
|
|
forge-accounts: # optional; alias -> forge account
|
|
<alias>:
|
|
url: <https Gitea /api/v1 base>
|
|
auth: { type: token, token_secret: <host env var name> }
|
|
# Claude Code subagent passthrough fields — accepted, ignored:
|
|
name, description, model, color, memory
|
|
|
|
`author` populates the bottle's git user.name/user.email; `forge-accounts`
|
|
maps a forge alias to a Gitea API origin plus a host token reference. Identity
|
|
is agent-owned — `git-gate` is no longer accepted on an agent (git-gate.user
|
|
moved to `author`; git-gate.repos is bottle-only).
|
|
|
|
The agent file's Markdown body is the system prompt (stripped).
|
|
Unknown top-level frontmatter keys raise ManifestError with a hint.
|
|
|
|
Both bottles and agents can ONLY live under $HOME. An agents/ or bottles/
|
|
dir under $CWD is a warn at load time and contributes nothing. The trust
|
|
boundary is expressed as filesystem layout rather than resolver logic.
|
|
|
|
Two types are exported:
|
|
|
|
ManifestIndex — the multi-agent/bottle collection returned by
|
|
resolve() and from_json_obj(). Used for agent
|
|
selection (all_agent_names), validation
|
|
(require_agent), and lazy loading (load_for_agent).
|
|
This is the pre-preflight form.
|
|
|
|
Manifest — a single-agent/bottle value type holding exactly
|
|
one agent: ManifestAgent and one bottle:
|
|
ManifestBottle (with the agent's git-gate.user
|
|
already overlaid). Returned by load_for_agent().
|
|
This is the post-preflight form passed to backends.
|
|
|
|
ManifestIndex.from_json_obj is preserved as a programmatic entry
|
|
point (used by tests) that takes a dict with the same field names —
|
|
useful for building manifests without on-disk files.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
if TYPE_CHECKING:
|
|
from .index import Manifest, ManifestIndex
|
|
from .util import ManifestError
|
|
from .agent import ManifestAgent, ManifestAgentProvider
|
|
from .bottle import ManifestBottle
|
|
from .egress import EGRESS_AUTH_SCHEMES, ManifestEgressConfig, ManifestEgressRoute
|
|
from .forge import (
|
|
ManifestAuthor,
|
|
ManifestForgeAccount,
|
|
ResolvedForgeAssociation,
|
|
)
|
|
from .git import ManifestGitEntry, ManifestGitUser, ManifestKeyConfig
|
|
|
|
|
|
# Facade name -> submodule that defines it. The aggregate model (`Manifest`,
|
|
# `ManifestIndex`) lives in `index`; the piece types in their own modules.
|
|
_LAZY_MODULES: dict[str, str] = {
|
|
"Manifest": "index",
|
|
"ManifestIndex": "index",
|
|
"ManifestError": "util",
|
|
"ManifestAgent": "agent",
|
|
"ManifestAgentProvider": "agent",
|
|
"ManifestBottle": "bottle",
|
|
"EGRESS_AUTH_SCHEMES": "egress",
|
|
"ManifestEgressRoute": "egress",
|
|
"ManifestEgressConfig": "egress",
|
|
"ManifestAuthor": "forge",
|
|
"ManifestForgeAccount": "forge",
|
|
"ResolvedForgeAssociation": "forge",
|
|
"ManifestGitEntry": "git",
|
|
"ManifestGitUser": "git",
|
|
"ManifestKeyConfig": "git",
|
|
}
|
|
|
|
|
|
def __getattr__(name: str) -> Any:
|
|
"""Lazily surface the manifest facade names from their submodules and cache
|
|
them at package level — so importing a leaf (e.g. `manifest.util`) doesn't
|
|
build the whole manifest model, while `from bot_bottle.manifest import
|
|
Manifest` keeps working."""
|
|
mod = _LAZY_MODULES.get(name)
|
|
if mod is None:
|
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
from importlib import import_module
|
|
|
|
value = getattr(import_module(f"{__name__}.{mod}"), name)
|
|
globals()[name] = value
|
|
return value
|
|
|
|
|
|
__all__ = [
|
|
"Manifest",
|
|
"ManifestIndex",
|
|
"ManifestError",
|
|
"ManifestGitEntry",
|
|
"ManifestGitUser",
|
|
"ManifestKeyConfig",
|
|
"ManifestAgentProvider",
|
|
"ManifestAgent",
|
|
"ManifestBottle",
|
|
"EGRESS_AUTH_SCHEMES",
|
|
"ManifestEgressRoute",
|
|
"ManifestEgressConfig",
|
|
"ManifestAuthor",
|
|
"ManifestForgeAccount",
|
|
"ResolvedForgeAssociation",
|
|
]
|