From b098556757f0bdb0ac328b3e6c52e7d123634626 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 8 Jun 2026 06:42:06 +0000 Subject: [PATCH] refactor: prefix all manifest data classes with Manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoids name collisions with same-named runtime/plugin classes (e.g. manifest AgentProvider vs plugin AgentProvider ABC, manifest EgressRoute vs runtime EgressRoute). Renamed: AgentProvider → ManifestAgentProvider (manifest_agent.py) Agent → ManifestAgent (manifest_agent.py) EgressRoute → ManifestEgressRoute (manifest_egress.py) PathMatch → ManifestPathMatch (manifest_egress.py) HeaderMatch → ManifestHeaderMatch (manifest_egress.py) MatchEntry → ManifestMatchEntry (manifest_egress.py) EgressConfig → ManifestEgressConfig (manifest_egress.py) Bottle → ManifestBottle (manifest.py) ProvisionedKeyConfig → ManifestProvisionedKeyConfig (manifest_git.py) GitEntry → ManifestGitEntry (manifest_git.py) GitUser → ManifestGitUser (manifest_git.py) --- bot_bottle/backend/__init__.py | 4 +- bot_bottle/egress.py | 8 +-- bot_bottle/git_gate.py | 14 +++--- bot_bottle/manifest.py | 60 +++++++++++------------ bot_bottle/manifest_agent.py | 16 +++--- bot_bottle/manifest_egress.py | 52 ++++++++++---------- bot_bottle/manifest_extends.py | 42 ++++++++-------- bot_bottle/manifest_git.py | 30 ++++++------ bot_bottle/manifest_loader.py | 12 ++--- tests/unit/test_manifest_git_user.py | 8 +-- tests/unit/test_manifest_runtime.py | 4 +- tests/unit/test_smolmachines_provision.py | 6 +-- 12 files changed, 128 insertions(+), 128 deletions(-) diff --git a/bot_bottle/backend/__init__.py b/bot_bottle/backend/__init__.py index 1912809..348ab40 100644 --- a/bot_bottle/backend/__init__.py +++ b/bot_bottle/backend/__init__.py @@ -43,7 +43,7 @@ from ..agent_provider import AgentProvisionPlan, get_provider from ..egress import EgressPlan from ..git_gate import GitGatePlan from ..log import die, info -from ..manifest import GitEntry, Manifest +from ..manifest import ManifestGitEntry, Manifest from ..supervise import SupervisePlan from ..util import expand_tilde from ..workspace import WorkspacePlan @@ -297,7 +297,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]): f"Create it under ~/.claude/skills/, then re-run." ) - def _validate_git_entries(self, entries: Sequence[GitEntry]) -> None: + def _validate_git_entries(self, entries: Sequence[ManifestGitEntry]) -> None: """Each entry's IdentityFile must exist on the host (after expanding leading ~) — the git-gate copies it in at start time to authenticate the upstream push (PRD 0008). Shape is already diff --git a/bot_bottle/egress.py b/bot_bottle/egress.py index 1b80147..527279e 100644 --- a/bot_bottle/egress.py +++ b/bot_bottle/egress.py @@ -24,7 +24,7 @@ from .egress_addon_core import ( from .log import die if TYPE_CHECKING: - from .manifest import Bottle + from .manifest import ManifestBottle CODEX_HOST_CREDENTIAL_TOKEN_REF = "BOT_BOTTLE_CODEX_HOST_ACCESS_TOKEN" @@ -66,7 +66,7 @@ class EgressPlan: def egress_manifest_routes( - bottle: Bottle, + bottle: ManifestBottle, ) -> tuple[EgressRoute, ...]: out: list[EgressRoute] = [] for r in bottle.egress.routes: @@ -98,7 +98,7 @@ def egress_manifest_routes( def egress_routes_for_bottle( - bottle: Bottle, + bottle: ManifestBottle, provider_routes: tuple[EgressRoute, ...] = (), ) -> tuple[EgressRoute, ...]: manifest = egress_manifest_routes(bottle) @@ -280,7 +280,7 @@ def egress_resolve_token_values( class Egress(ABC): def prepare( self, - bottle: Bottle, + bottle: ManifestBottle, slug: str, stage_dir: Path, provider_routes: tuple[EgressRoute, ...] = (), diff --git a/bot_bottle/git_gate.py b/bot_bottle/git_gate.py index 58427a2..1384341 100644 --- a/bot_bottle/git_gate.py +++ b/bot_bottle/git_gate.py @@ -37,7 +37,7 @@ from dataclasses import dataclass from pathlib import Path from .log import info -from .manifest import Bottle, GitEntry +from .manifest import ManifestBottle, ManifestGitEntry # Short network alias for git-gate inside the sidecar bundle. The @@ -96,9 +96,9 @@ class GitGatePlan: egress_network: str = "" -def git_gate_upstreams_for_bottle(bottle: Bottle) -> tuple[GitGateUpstream, ...]: +def git_gate_upstreams_for_bottle(bottle: ManifestBottle) -> tuple[GitGateUpstream, ...]: """Lift each `bottle.git` entry into a GitGateUpstream. Unique-Name - validation already ran in `manifest.Bottle.from_dict`.""" + validation already ran in `manifest.ManifestBottle.from_dict`.""" return tuple( GitGateUpstream( name=e.Name, @@ -113,7 +113,7 @@ def git_gate_upstreams_for_bottle(bottle: Bottle) -> tuple[GitGateUpstream, ...] def git_gate_render_gitconfig( - entries: tuple[GitEntry, ...], gate_host: str, *, scheme: str = "git", + entries: tuple[ManifestGitEntry, ...], gate_host: str, *, scheme: str = "git", ) -> str: """Render the agent's ~/.gitconfig content for git-gate `insteadOf` rewrites. Pure host-side, no docker / smolvm; @@ -361,7 +361,7 @@ exit 0 def _provision_dynamic_key( - entry: GitEntry, + entry: ManifestGitEntry, slug: str, stage_dir: Path, ) -> str: @@ -402,7 +402,7 @@ def _provision_dynamic_key( return str(key_file) -def revoke_git_gate_provisioned_keys(bottle: Bottle, stage_dir: Path) -> None: +def revoke_git_gate_provisioned_keys(bottle: ManifestBottle, stage_dir: Path) -> None: """Revoke all deploy keys provisioned for `bottle` during prepare. Called at teardown after containers stop. Raises if any revocation @@ -440,7 +440,7 @@ class GitGate(ABC): start/stop lifecycle is backend-specific and lives on concrete subclasses.""" - def prepare(self, bottle: Bottle, slug: str, stage_dir: Path) -> GitGatePlan: + def prepare(self, bottle: ManifestBottle, slug: str, stage_dir: Path) -> GitGatePlan: """Compute the upstream table from `bottle.git` and write the entrypoint, pre-receive hook, and access-hook scripts (mode 600) under `stage_dir`. Pure host-side, no docker subprocess. diff --git a/bot_bottle/manifest.py b/bot_bottle/manifest.py index 0e44347..4f856cf 100644 --- a/bot_bottle/manifest.py +++ b/bot_bottle/manifest.py @@ -50,26 +50,26 @@ from pathlib import Path from typing import Mapping from .manifest_util import ManifestError, as_json_object -from .manifest_agent import Agent, AgentProvider +from .manifest_agent import ManifestAgent, ManifestAgentProvider from .manifest_egress import ( EGRESS_AUTH_SCHEMES, - EgressConfig, - EgressRoute, + ManifestEgressConfig, + ManifestEgressRoute, ) -from .manifest_git import GitEntry, GitUser, parse_git_gate_config +from .manifest_git import ManifestGitEntry, ManifestGitUser, parse_git_gate_config from .manifest_schema import BOTTLE_KEYS # Re-export everything that callers currently import from this module. __all__ = [ "ManifestError", - "GitEntry", - "GitUser", - "AgentProvider", + "ManifestGitEntry", + "ManifestGitUser", + "ManifestAgentProvider", "EGRESS_AUTH_SCHEMES", - "EgressRoute", - "EgressConfig", - "Agent", - "Bottle", + "ManifestEgressRoute", + "ManifestEgressConfig", + "ManifestAgent", + "ManifestBottle", "Manifest", ] @@ -86,16 +86,16 @@ def _section_dict(value: object, label: str) -> dict[str, object]: @dataclass(frozen=True) -class Bottle: +class ManifestBottle: env: Mapping[str, str] = field(default_factory=_empty_str_dict) - agent_provider: AgentProvider = field(default_factory=AgentProvider) - git: tuple[GitEntry, ...] = () + agent_provider: ManifestAgentProvider = field(default_factory=ManifestAgentProvider) + git: tuple[ManifestGitEntry, ...] = () # Per-bottle git identity (issue #86). Empty default — bottles # that don't set `git-gate.user:` in the manifest skip the # `git config --global` step entirely. A bottle can declare a user # identity without any git-gate.repos upstreams, and vice versa. - git_user: GitUser = field(default_factory=GitUser) - egress: EgressConfig = field(default_factory=EgressConfig) + git_user: ManifestGitUser = field(default_factory=ManifestGitUser) + egress: ManifestEgressConfig = field(default_factory=ManifestEgressConfig) # Opt-in per-bottle stuck-recovery sidecar (PRD 0013). When true, # the launch step brings up a supervise sidecar that exposes MCP # tools to the agent (egress-block, capability-block) plus mounts @@ -105,7 +105,7 @@ class Bottle: supervise: bool = False @classmethod - def from_dict(cls, name: str, raw: object) -> "Bottle": + def from_dict(cls, name: str, raw: object) -> "ManifestBottle": d = as_json_object(raw, f"bottle '{name}'") if "runtime" in d: @@ -157,22 +157,22 @@ class Bottle: ) env[var] = value - git: tuple[GitEntry, ...] = () - git_user = GitUser() + git: tuple[ManifestGitEntry, ...] = () + git_user = ManifestGitUser() git_raw = d.get("git-gate") if git_raw is not None: git, git_user = parse_git_gate_config(name, git_raw) agent_provider = ( - AgentProvider.from_dict(name, d["agent_provider"]) + ManifestAgentProvider.from_dict(name, d["agent_provider"]) if "agent_provider" in d - else AgentProvider() + else ManifestAgentProvider() ) egress = ( - EgressConfig.from_dict(name, d["egress"]) + ManifestEgressConfig.from_dict(name, d["egress"]) if "egress" in d - else EgressConfig() + else ManifestEgressConfig() ) supervise_raw = d.get("supervise", False) @@ -190,8 +190,8 @@ class Bottle: @dataclass(frozen=True) class Manifest: - bottles: Mapping[str, Bottle] - agents: Mapping[str, Agent] + bottles: Mapping[str, ManifestBottle] + agents: Mapping[str, ManifestAgent] @classmethod def resolve(cls, cwd: str, *, missing_ok: bool = False) -> "Manifest": @@ -305,8 +305,8 @@ class Manifest: bottles = resolve_bottles(raw_bottles) bottle_names = set(bottles.keys()) - agents: dict[str, Agent] = { - n: Agent.from_dict(n, a, bottle_names) for n, a in raw_agents.items() + agents: dict[str, ManifestAgent] = { + n: ManifestAgent.from_dict(n, a, bottle_names) for n, a in raw_agents.items() } return cls(bottles=bottles, agents=agents) @@ -338,7 +338,7 @@ class Manifest: ) raise ManifestError(f"bottle '{name}' not defined in bot-bottle.json (no bottles defined).") - def _effective_git_user(self, agent_name: str) -> GitUser: + def _effective_git_user(self, agent_name: str) -> ManifestGitUser: """Merge the agent's git.user over the referenced bottle's, per-field, agent-wins-on-non-empty (issue #94). Same overlay the `extends:` resolver applies between bottles @@ -348,12 +348,12 @@ class Manifest: over = agent.git_user if over.is_empty(): return base - return GitUser( + return ManifestGitUser( name=over.name or base.name, email=over.email or base.email, ) - def bottle_for(self, agent_name: str) -> Bottle: + def bottle_for(self, agent_name: str) -> ManifestBottle: """Resolve the Bottle the named agent references, with the agent's git.user overlaid on top. The validator guarantees both lookups succeed for a manifest built via from_json_obj. diff --git a/bot_bottle/manifest_agent.py b/bot_bottle/manifest_agent.py index 7495e35..927df42 100644 --- a/bot_bottle/manifest_agent.py +++ b/bot_bottle/manifest_agent.py @@ -7,12 +7,12 @@ from typing import cast from .agent_provider import PROVIDER_TEMPLATES from .manifest_util import ManifestError, as_json_object -from .manifest_git import GitUser +from .manifest_git import ManifestGitUser from .manifest_schema import AGENT_MODEL_KEYS @dataclass(frozen=True) -class AgentProvider: +class ManifestAgentProvider: """Provider/template for the agent process inside a bottle. `template` selects a built-in launch/runtime contract. `dockerfile` @@ -35,7 +35,7 @@ class AgentProvider: forward_host_credentials: bool = False @classmethod - def from_dict(cls, bottle_name: str, raw: object) -> "AgentProvider": + def from_dict(cls, bottle_name: str, raw: object) -> "ManifestAgentProvider": d = as_json_object(raw, f"bottle '{bottle_name}' agent_provider") for k in d: if k not in {"template", "dockerfile", "auth_token", "forward_host_credentials"}: @@ -98,7 +98,7 @@ class AgentProvider: @dataclass(frozen=True) -class Agent: +class ManifestAgent: bottle: str skills: tuple[str, ...] = () prompt: str = "" @@ -106,10 +106,10 @@ class Agent: # bottle's git-gate.user per-field at `Manifest.bottle_for`. Only # `user` is allowed at the agent level; `repos` stays bottle-only # because it carries credentials and host trust. - git_user: GitUser = GitUser() + git_user: ManifestGitUser = ManifestGitUser() @classmethod - def from_dict(cls, name: str, raw: object, bottle_names: set[str]) -> "Agent": + def from_dict(cls, name: str, raw: object, bottle_names: set[str]) -> "ManifestAgent": d = as_json_object(raw, f"agent '{name}'") unknown = set(d.keys()) - AGENT_MODEL_KEYS if unknown: @@ -164,7 +164,7 @@ class Agent: # git-gate: agents may declare only `git-gate.user` (name/email). # `git-gate.repos` is bottle-only — it carries credentials and host trust. - git_user = GitUser() + git_user = ManifestGitUser() git_raw = d.get("git-gate") if git_raw is not None: gd = as_json_object(git_raw, f"agent '{name}' git-gate") @@ -177,6 +177,6 @@ class Agent: f"(it carries credentials and host trust)." ) if "user" in gd: - git_user = GitUser.from_dict(name, gd["user"]) + git_user = ManifestGitUser.from_dict(name, gd["user"]) return cls(bottle=bottle, skills=skills, prompt=prompt, git_user=git_user) diff --git a/bot_bottle/manifest_egress.py b/bot_bottle/manifest_egress.py index cbdded4..73bda30 100644 --- a/bot_bottle/manifest_egress.py +++ b/bot_bottle/manifest_egress.py @@ -24,7 +24,7 @@ INBOUND_DETECTOR_NAMES = frozenset({"naive_injection_detection"}) def validate_egress_routes( bottle_name: str, - routes: tuple[EgressRoute, ...], + routes: tuple[ManifestEgressRoute, ...], ) -> None: seen_hosts: dict[str, None] = {} for r in routes: @@ -38,29 +38,29 @@ def validate_egress_routes( @dataclass(frozen=True) -class PathMatch: +class ManifestPathMatch: Type: str = "prefix" Value: str = "" @dataclass(frozen=True) -class HeaderMatch: +class ManifestHeaderMatch: Name: str = "" Value: str = "" Type: str = "exact" @dataclass(frozen=True) -class MatchEntry: - Paths: tuple[PathMatch, ...] = () +class ManifestMatchEntry: + Paths: tuple[ManifestPathMatch, ...] = () Methods: tuple[str, ...] = () - Headers: tuple[HeaderMatch, ...] = () + Headers: tuple[ManifestHeaderMatch, ...] = () @dataclass(frozen=True) -class EgressRoute: +class ManifestEgressRoute: Host: str - Matches: tuple[MatchEntry, ...] = () + Matches: tuple[ManifestMatchEntry, ...] = () AuthScheme: str = "" TokenRef: str = "" Role: tuple[str, ...] = () @@ -68,7 +68,7 @@ class EgressRoute: InboundDetectors: tuple[str, ...] | None = None @classmethod - def from_dict(cls, bottle_name: str, idx: int, raw: object) -> "EgressRoute": + def from_dict(cls, bottle_name: str, idx: int, raw: object) -> "ManifestEgressRoute": label = f"bottle '{bottle_name}' egress.routes[{idx}]" d = as_json_object(raw, label) host = d.get("host") @@ -76,7 +76,7 @@ class EgressRoute: raise ManifestError(f"{label} missing required string field 'host'") # --- matches --- - matches: tuple[MatchEntry, ...] = () + matches: tuple[ManifestMatchEntry, ...] = () matches_raw = d.get("matches") if matches_raw is not None: if not isinstance(matches_raw, list): @@ -85,7 +85,7 @@ class EgressRoute: f"(was {type(matches_raw).__name__})" ) matches_list = cast(list[object], matches_raw) - entries: list[MatchEntry] = [] + entries: list[ManifestMatchEntry] = [] for k, entry_raw in enumerate(matches_list): entries.append( _parse_match_entry(label, k, entry_raw) @@ -185,17 +185,17 @@ class EgressRoute: def _parse_match_entry( route_label: str, k: int, raw: object, -) -> MatchEntry: +) -> ManifestMatchEntry: label = f"{route_label} matches[{k}]" d = as_json_object(raw, label) - paths: tuple[PathMatch, ...] = () + paths: tuple[ManifestPathMatch, ...] = () paths_raw = d.get("paths") if paths_raw is not None: if not isinstance(paths_raw, list): raise ManifestError(f"{label} paths must be an array") paths_list = cast(list[object], paths_raw) - parsed_paths: list[PathMatch] = [] + parsed_paths: list[ManifestPathMatch] = [] for j, p_raw in enumerate(paths_list): parsed_paths.append(_parse_path_match(label, j, p_raw)) paths = tuple(parsed_paths) @@ -220,13 +220,13 @@ def _parse_match_entry( normalised.append(upper) methods = tuple(normalised) - headers: tuple[HeaderMatch, ...] = () + headers: tuple[ManifestHeaderMatch, ...] = () headers_raw = d.get("headers") if headers_raw is not None: if not isinstance(headers_raw, list): raise ManifestError(f"{label} headers must be an array") headers_list = cast(list[object], headers_raw) - parsed_headers: list[HeaderMatch] = [] + parsed_headers: list[ManifestHeaderMatch] = [] for j, h_raw in enumerate(headers_list): parsed_headers.append(_parse_header_match(label, j, h_raw)) headers = tuple(parsed_headers) @@ -235,12 +235,12 @@ def _parse_match_entry( if key not in ("paths", "methods", "headers"): raise ManifestError(f"{label} has unknown key {key!r}") - return MatchEntry(Paths=paths, Methods=methods, Headers=headers) + return ManifestMatchEntry(Paths=paths, Methods=methods, Headers=headers) def _parse_path_match( entry_label: str, j: int, raw: object, -) -> PathMatch: +) -> ManifestPathMatch: label = f"{entry_label} paths[{j}]" d = as_json_object(raw, label) ptype = d.get("type", "prefix") @@ -266,12 +266,12 @@ def _parse_path_match( for k in d: if k not in ("type", "value"): raise ManifestError(f"{label} has unknown key {k!r}") - return PathMatch(Type=ptype, Value=value) + return ManifestPathMatch(Type=ptype, Value=value) def _parse_header_match( entry_label: str, j: int, raw: object, -) -> HeaderMatch: +) -> ManifestHeaderMatch: label = f"{entry_label} headers[{j}]" d = as_json_object(raw, label) name = d.get("name") @@ -296,7 +296,7 @@ def _parse_header_match( for k in d: if k not in ("name", "value", "type"): raise ManifestError(f"{label} has unknown key {k!r}") - return HeaderMatch(Name=name, Value=value, Type=htype) + return ManifestHeaderMatch(Name=name, Value=value, Type=htype) def _parse_dlp_block( @@ -350,15 +350,15 @@ LOG_LEVELS = frozenset({0, 1, 2}) @dataclass(frozen=True) -class EgressConfig: - routes: tuple[EgressRoute, ...] = () +class ManifestEgressConfig: + routes: tuple[ManifestEgressRoute, ...] = () Log: int = 0 @classmethod - def from_dict(cls, bottle_name: str, raw: object) -> "EgressConfig": + def from_dict(cls, bottle_name: str, raw: object) -> "ManifestEgressConfig": d = as_json_object(raw, f"bottle '{bottle_name}' egress") routes_raw = d.get("routes") - routes: tuple[EgressRoute, ...] = () + routes: tuple[ManifestEgressRoute, ...] = () if routes_raw is not None: if not isinstance(routes_raw, list): raise ManifestError( @@ -367,7 +367,7 @@ class EgressConfig: ) routes_list = cast(list[object], routes_raw) routes = tuple( - EgressRoute.from_dict(bottle_name, i, entry) + ManifestEgressRoute.from_dict(bottle_name, i, entry) for i, entry in enumerate(routes_list) ) validate_egress_routes(bottle_name, routes) diff --git a/bot_bottle/manifest_extends.py b/bot_bottle/manifest_extends.py index 18d5674..37176f3 100644 --- a/bot_bottle/manifest_extends.py +++ b/bot_bottle/manifest_extends.py @@ -5,12 +5,12 @@ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: - from .manifest import Bottle, GitEntry + from .manifest import ManifestBottle, ManifestGitEntry -def resolve_bottles(raws: dict[str, dict[str, object]]) -> dict[str, Bottle]: - """Apply `extends:` chains and return resolved Bottle objects.""" - cache: dict[str, Bottle] = {} +def resolve_bottles(raws: dict[str, dict[str, object]]) -> dict[str, ManifestBottle]: + """Apply `extends:` chains and return resolved ManifestBottle objects.""" + cache: dict[str, ManifestBottle] = {} for name in raws: if name not in cache: _resolve_one_bottle(name, raws, cache, ()) @@ -20,10 +20,10 @@ def resolve_bottles(raws: dict[str, dict[str, object]]) -> dict[str, Bottle]: def _resolve_one_bottle( name: str, raws: dict[str, dict[str, object]], - cache: dict[str, Bottle], + cache: dict[str, ManifestBottle], seen: tuple[str, ...], -) -> Bottle: - from .manifest import Bottle, ManifestError +) -> ManifestBottle: + from .manifest import ManifestBottle, ManifestError if name in cache: return cache[name] @@ -32,13 +32,13 @@ def _resolve_one_bottle( raise ManifestError(f"bottle '{name}' is in an extends cycle: {chain}") raw = raws[name] parent_name_raw = raw.get("extends") - # Strip `extends:` before passing to Bottle.from_dict so it - # is not accidentally treated as a real Bottle field by future + # Strip `extends:` before passing to ManifestBottle.from_dict so it + # is not accidentally treated as a real ManifestBottle field by future # schema additions. It is only meaningful here. child_raw = {k: v for k, v in raw.items() if k != "extends"} if parent_name_raw is None: - bottle = Bottle.from_dict(name, child_raw) + bottle = ManifestBottle.from_dict(name, child_raw) cache[name] = bottle return bottle @@ -66,27 +66,27 @@ def _resolve_one_bottle( def _merge_bottles( - parent: Bottle, + parent: ManifestBottle, child_raw: dict[str, object], name: str, -) -> Bottle: +) -> ManifestBottle: """Apply PRD 0025 merge rules.""" - from .manifest import Bottle, GitUser + from .manifest import ManifestBottle, ManifestGitUser from .manifest_egress import validate_egress_routes - # Parse the child's declared fields into a Bottle (with the + # Parse the child's declared fields into a ManifestBottle (with the # usual defaults for anything missing). Validation runs the same # way it would for a leaf bottle: typos / wrong types die here. - child = Bottle.from_dict(name, child_raw) + child = ManifestBottle.from_dict(name, child_raw) # env: dict merge, child wins on collision. merged_env = {**parent.env, **child.env} # git-gate.user: per-field overlay. Each non-empty field on child - # wins; empties fall through to parent. The default GitUser() + # wins; empties fall through to parent. The default ManifestGitUser() # is two empty strings, so a child that omits git-gate.user # inherits the parent's user verbatim. - merged_git_user = GitUser( + merged_git_user = ManifestGitUser( name=child.git_user.name or parent.git_user.name, email=child.git_user.email or parent.git_user.email, ) @@ -112,7 +112,7 @@ def _merge_bottles( ) validate_egress_routes(name, merged_egress.routes) - return Bottle( + return ManifestBottle( env=merged_env, agent_provider=merged_agent_provider, git=merged_git, @@ -133,9 +133,9 @@ def _child_declares_git_gate_repos(child_raw: dict[str, object]) -> bool: def _merge_git_remotes( - parent: tuple[GitEntry, ...], - child: tuple[GitEntry, ...], -) -> tuple[GitEntry, ...]: + parent: tuple[ManifestGitEntry, ...], + child: tuple[ManifestGitEntry, ...], +) -> tuple[ManifestGitEntry, ...]: by_host = {entry.UpstreamHost: entry for entry in parent} for entry in child: by_host[entry.UpstreamHost] = entry diff --git a/bot_bottle/manifest_git.py b/bot_bottle/manifest_git.py index 81d845d..8ed0600 100644 --- a/bot_bottle/manifest_git.py +++ b/bot_bottle/manifest_git.py @@ -57,7 +57,7 @@ def parse_git_upstream(url: str, label: str) -> tuple[str, str, str, str]: return (user, host, port, path) -def validate_unique_git_names(bottle_name: str, git: tuple[GitEntry, ...]) -> None: +def validate_unique_git_names(bottle_name: str, git: tuple[ManifestGitEntry, ...]) -> None: seen: dict[str, None] = {} for g in git: if g.Name in seen: @@ -69,7 +69,7 @@ def validate_unique_git_names(bottle_name: str, git: tuple[GitEntry, ...]) -> No @dataclass(frozen=True) -class ProvisionedKeyConfig: +class ManifestProvisionedKeyConfig: """Configuration for automatic deploy-key lifecycle management (PRD 0048). Used when a git-gate.repos entry opts out of a static identity file and instead wants a fresh SSH keypair @@ -87,7 +87,7 @@ class ProvisionedKeyConfig: @dataclass(frozen=True) -class GitEntry: +class ManifestGitEntry: """One upstream the per-agent git-gate (PRD 0008) is allowed to talk to. `Upstream` is the real remote URL the agent would push to if there were no gate; the gate hosts a bare repo at /git/.git @@ -107,7 +107,7 @@ class GitEntry: Upstream: str IdentityFile: str = "" KnownHostKey: str = "" - ProvisionedKey: Optional[ProvisionedKeyConfig] = None + ProvisionedKey: Optional[ManifestProvisionedKeyConfig] = None RemoteKey: str = "" UpstreamUser: str = "" UpstreamHost: str = "" @@ -117,7 +117,7 @@ class GitEntry: @classmethod def from_repos_entry( cls, bottle_name: str, repo_name: str, raw: object - ) -> "GitEntry": + ) -> "ManifestGitEntry": """Parse one entry from `git-gate.repos.`. YAML keys: `url` (required), exactly one of `identity` or @@ -160,7 +160,7 @@ class GitEntry: ) ident = "" - provisioned_key: Optional[ProvisionedKeyConfig] = None + provisioned_key: Optional[ManifestProvisionedKeyConfig] = None if has_identity: raw_ident = d.get("identity") if not isinstance(raw_ident, str) or not raw_ident: @@ -196,7 +196,7 @@ class GitEntry: def _parse_provisioned_key_config( bottle_name: str, label: str, raw: object -) -> ProvisionedKeyConfig: +) -> ManifestProvisionedKeyConfig: d = as_json_object(raw, f"bottle '{bottle_name}' {label}.provisioned_key") for k in d: if k not in {"provider", "token_env", "api_url"}: @@ -221,7 +221,7 @@ def _parse_provisioned_key_config( raise ManifestError( f"bottle '{bottle_name}' {label}.provisioned_key 'api_url' must be a string" ) - return ProvisionedKeyConfig( + return ManifestProvisionedKeyConfig( provider=provider, token_env=token_env, api_url=api_url_raw, @@ -229,7 +229,7 @@ def _parse_provisioned_key_config( @dataclass(frozen=True) -class GitUser: +class ManifestGitUser: """Per-bottle `git config --global user.name` / `user.email` pair (issue #86). The agent's commits inside the bottle are attributed to this identity rather than the agent image's @@ -244,7 +244,7 @@ class GitUser: email: str = "" @classmethod - def from_dict(cls, bottle_name: str, raw: object) -> "GitUser": + def from_dict(cls, bottle_name: str, raw: object) -> "ManifestGitUser": d = as_json_object(raw, f"bottle '{bottle_name}' git-gate.user") for k in d: if k not in {"name", "email"}: @@ -279,7 +279,7 @@ class GitUser: def parse_git_gate_config( bottle_name: str, raw: object, -) -> tuple[tuple[GitEntry, ...], GitUser]: +) -> tuple[tuple[ManifestGitEntry, ...], ManifestGitUser]: d = as_json_object(raw, f"bottle '{bottle_name}' git-gate") for k in d: if k not in {"user", "repos"}: @@ -289,17 +289,17 @@ def parse_git_gate_config( ) git_user = ( - GitUser.from_dict(bottle_name, d["user"]) + ManifestGitUser.from_dict(bottle_name, d["user"]) if "user" in d - else GitUser() + else ManifestGitUser() ) - git: tuple[GitEntry, ...] = () + git: tuple[ManifestGitEntry, ...] = () repos_raw = d.get("repos") if repos_raw is not None: repos = as_json_object(repos_raw, f"bottle '{bottle_name}' git-gate.repos") git = tuple( - GitEntry.from_repos_entry(bottle_name, name, entry) + ManifestGitEntry.from_repos_entry(bottle_name, name, entry) for name, entry in repos.items() ) validate_unique_git_names(bottle_name, git) diff --git a/bot_bottle/manifest_loader.py b/bot_bottle/manifest_loader.py index 81a55f1..67d0e51 100644 --- a/bot_bottle/manifest_loader.py +++ b/bot_bottle/manifest_loader.py @@ -14,7 +14,7 @@ from .manifest_schema import ( from .yaml_subset import YamlSubsetError, parse_frontmatter if TYPE_CHECKING: - from .manifest import Agent, Bottle + from .manifest import ManifestAgent, ManifestBottle def check_stale_json(dir_path: Path, md_dir: Path, label: str) -> None: @@ -34,7 +34,7 @@ def check_stale_json(dir_path: Path, md_dir: Path, label: str) -> None: ) -def load_bottles_from_dir(bottles_dir: Path) -> dict[str, Bottle]: +def load_bottles_from_dir(bottles_dir: Path) -> dict[str, ManifestBottle]: """Walk `/*.md`, parse each as a bottle, and return `{name: Bottle}`. Missing dir returns an empty dict.""" from .manifest import ManifestError @@ -67,13 +67,13 @@ def load_agents_from_dir( bottle_names: set[str], *, source: str, # noqa: F841 — unused, but required by interface -) -> dict[str, Agent]: +) -> dict[str, ManifestAgent]: """Walk `/*.md`, parse each as an agent, and return `{name: Agent}`. The Markdown body becomes the agent's prompt. Missing dir returns an empty dict.""" - from .manifest import Agent, ManifestError + from .manifest import ManifestAgent, ManifestError - out: dict[str, Agent] = {} + out: dict[str, ManifestAgent] = {} if not agents_dir.is_dir(): return out for path in sorted(agents_dir.glob("*.md")): @@ -101,5 +101,5 @@ def load_agents_from_dir( } if "git-gate" in fm: agent_dict["git-gate"] = fm["git-gate"] - out[name] = Agent.from_dict(name, agent_dict, bottle_names) + out[name] = ManifestAgent.from_dict(name, agent_dict, bottle_names) return out diff --git a/tests/unit/test_manifest_git_user.py b/tests/unit/test_manifest_git_user.py index 324f898..fee0091 100644 --- a/tests/unit/test_manifest_git_user.py +++ b/tests/unit/test_manifest_git_user.py @@ -2,7 +2,7 @@ import unittest -from bot_bottle.manifest import ManifestError, GitUser, Manifest +from bot_bottle.manifest import ManifestError, ManifestGitUser, Manifest def _error_message(callable_, *args, **kwargs) -> str: # type: ignore @@ -99,13 +99,13 @@ class TestGitUserDirect(unittest.TestCase): """Direct GitUser dataclass exercises (no manifest wrapper).""" def test_is_empty_default(self): - self.assertTrue(GitUser().is_empty()) + self.assertTrue(ManifestGitUser().is_empty()) def test_is_empty_false_when_name_set(self): - self.assertFalse(GitUser(name="x").is_empty()) + self.assertFalse(ManifestGitUser(name="x").is_empty()) def test_is_empty_false_when_email_set(self): - self.assertFalse(GitUser(email="x@y").is_empty()) + self.assertFalse(ManifestGitUser(email="x@y").is_empty()) if __name__ == "__main__": diff --git a/tests/unit/test_manifest_runtime.py b/tests/unit/test_manifest_runtime.py index 9db3060..42def91 100644 --- a/tests/unit/test_manifest_runtime.py +++ b/tests/unit/test_manifest_runtime.py @@ -7,7 +7,7 @@ silently ignoring.""" import unittest from typing import Any -from bot_bottle.manifest import ManifestError, Bottle, Manifest +from bot_bottle.manifest import ManifestError, ManifestBottle, Manifest def _manifest_with_runtime(value: object) -> dict[str, Any]: @@ -26,7 +26,7 @@ class TestManifestRuntimeRemoved(unittest.TestCase): self.assertIn("dev", m.bottles) def test_bottle_dataclass_has_no_runtime_attribute(self): - self.assertFalse(hasattr(Bottle(), "runtime")) + self.assertFalse(hasattr(ManifestBottle(), "runtime")) def test_any_runtime_value_is_rejected(self): for value in ("runsc", "runc", "kata-runtime", "", 42, None): diff --git a/tests/unit/test_smolmachines_provision.py b/tests/unit/test_smolmachines_provision.py index 951d2a6..5bedc5d 100644 --- a/tests/unit/test_smolmachines_provision.py +++ b/tests/unit/test_smolmachines_provision.py @@ -33,7 +33,7 @@ from bot_bottle.backend.smolmachines.launch import _bundle_launch_spec from bot_bottle.backend.util import AGENT_CA_PATH from bot_bottle.egress import EgressPlan, EgressRoute from bot_bottle.git_gate import GitGatePlan, GitGateUpstream -from bot_bottle.manifest import GitEntry, Manifest +from bot_bottle.manifest import ManifestGitEntry, Manifest from bot_bottle.supervise import SupervisePlan from bot_bottle.workspace import workspace_plan @@ -85,7 +85,7 @@ def _plan( *, agent_prompt: str = "", skills: list[str] | None = None, - git: list[GitEntry] = (), # type: ignore + git: list[ManifestGitEntry] = (), # type: ignore git_user: dict | None = None, # type: ignore copy_cwd: bool = False, user_cwd: str = "/tmp/x", @@ -392,7 +392,7 @@ class TestProvisionGit(unittest.TestCase): # git HTTP port is published on host loopback at launch # time, and the plan carries the discovered host port. plan = _plan( - git=[GitEntry( + git=[ManifestGitEntry( Name="bot-bottle", Upstream="ssh://git@host/repo.git", IdentityFile="~/.ssh/id_ed25519",