Files
bot-bottle/bot_bottle/manifest/__init__.py
T
didericis-claude e16efc86ad
test / integration-docker (pull_request) Successful in 38s
lint / lint (push) Successful in 56s
test / unit (pull_request) Successful in 52s
test / integration-firecracker (pull_request) Successful in 3m48s
test / coverage (pull_request) Successful in 15s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Failing after 24s
feat(manifest): git-gate.signing opt-in (PRD chunk 2)
Implements the manifest surface for the signed-commits & audit-attribution
PRD (#423, PR #480): a bottle opts into per-activation commit signing with

    git-gate:
      signing:
        enabled: true

- New ManifestGitSigning value type (frozen, enabled: bool = False) parsed
  under git-gate.signing; unknown sub-keys and non-bool `enabled` die with
  targeted errors. parse_git_gate_config now returns (repos, user, signing).
- ManifestBottle gains git_signing; defaults to off, so bottles that omit
  the block are unchanged.
- Bottle-only: git-gate.signing on an agent is rejected by the existing
  agent-level non-`user` guard (like git-gate.repos).
- extends/runtime merges thread git_signing with an enable-wins overlay
  (a child enabling signing wins; omitting inherits the parent), mirroring
  the git_user non-empty-wins overlay.
- Facade exports ManifestGitSigning.

Tests: tests/unit/test_manifest_git_signing.py (parse, validation,
agent-level rejection, extends overlay). Full unit suite green.

This is the first stacked chunk; signing pipeline, gate signature check,
and control-plane verification + audit tables follow per the PRD.

Issue: #423

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 02:52:47 +00:00

126 lines
4.5 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,
ManifestGitSigning,
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",
"ManifestGitSigning": "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",
"ManifestGitSigning",
"ManifestGitUser",
"ManifestKeyConfig",
"ManifestAgentProvider",
"ManifestAgent",
"ManifestBottle",
"EGRESS_AUTH_SCHEMES",
"ManifestEgressRoute",
"ManifestEgressConfig",
]