14c28946a7
tracker-policy-pr / check-pr (pull_request) Successful in 12s
test / integration-docker (pull_request) Successful in 19s
lint / lint (push) Failing after 1m3s
test / unit (pull_request) Successful in 2m6s
test / integration-firecracker (pull_request) Successful in 3m26s
test / coverage (pull_request) Successful in 21s
test / publish-infra (pull_request) Has been skipped
manifest/__init__.py was a facade that eagerly imported every piece (agent, bottle, egress, git, loader, schema, util) and defined the aggregate Manifest / ManifestIndex, so importing a leaf like `manifest.util` paid 19 modules. Move the aggregate model — Manifest, ManifestIndex, and the bottle-resolution helpers — into manifest/index.py (which keeps the framework imports it needs), and make __init__ a thin __getattr__ facade over the 12 __all__ names (mapping each to its submodule), with a TYPE_CHECKING block. The package docstring is preserved. `import bot_bottle.manifest.util` drops 19 -> 3; the package itself is 2. All `from bot_bottle.manifest import Manifest/ManifestIndex/ManifestError/…` call-sites keep working via the lazy facade; no importer changes needed. Full unit suite green (2243). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
4.4 KiB
Python
119 lines
4.4 KiB
Python
"""Manifest dataclasses (PRD 0011 layout).
|
|
|
|
Reads the per-file manifest tree:
|
|
|
|
$HOME/.bot-bottle/bottles/<name>.md — one bottle per file
|
|
$HOME/.bot-bottle/agents/<name>.md — home-resident agents
|
|
$CWD/.bot-bottle/agents/<name>.md — cwd-supplied 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)
|
|
user: { name: <str>, email: <str> } # optional
|
|
repos: { <name>: <git-gate-entry>, ... } # optional
|
|
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> # required
|
|
skills: [ <skill-name>, ... ] # optional
|
|
git-gate:
|
|
user: { name: <str>, email: <str> } # optional; overlays bottle
|
|
# Claude Code subagent passthrough fields — accepted, ignored:
|
|
name, description, model, color, memory
|
|
|
|
The agent file's Markdown body is the system prompt (stripped).
|
|
Unknown top-level frontmatter keys raise ManifestError with a hint.
|
|
|
|
Bottles can ONLY live under $HOME. A 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 .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",
|
|
"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",
|
|
]
|