diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index 20f0bb7..da03221 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -240,7 +240,7 @@ class AgentProvider(ABC): BottleBackend.provision_workspace against the running bottle.""" from .log import info - manifest_bottle = plan.spec.manifest.bottle + manifest_bottle = plan.spec.loaded_manifest.bottle if manifest_bottle.git: from .git_gate import GIT_GATE_HOSTNAME, git_gate_render_gitconfig gate_host = getattr(plan, "git_gate_insteadof_host", GIT_GATE_HOSTNAME) diff --git a/bot_bottle/backend/__init__.py b/bot_bottle/backend/__init__.py index ad32351..6beffc0 100644 --- a/bot_bottle/backend/__init__.py +++ b/bot_bottle/backend/__init__.py @@ -65,6 +65,13 @@ class BottleSpec: agent_name: str copy_cwd: bool user_cwd: str + + @property + def loaded_manifest(self) -> Manifest: + assert isinstance(self.manifest, Manifest), ( + "spec.manifest is still a ManifestIndex — call _validate() first" + ) + return self.manifest # PRD 0016 follow-up: when set, the backend's prepare step uses # this identity instead of minting a fresh one — the resume path # (`cli.py resume `) sets this to continue an existing @@ -112,7 +119,7 @@ class BottlePlan(ABC): """Render the y/N preflight summary to stderr.""" del remote_control spec = self.spec - manifest = spec.manifest # type: ignore[assignment] + manifest = spec.loaded_manifest agent = manifest.agent bottle = manifest.bottle @@ -293,7 +300,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]): self._preflight() - manifest = spec.manifest # type: ignore[assignment] + manifest = spec.loaded_manifest manifest_bottle = manifest.bottle manifest_agent_provider = manifest_bottle.agent_provider agent_provider = get_provider(manifest_agent_provider.template) @@ -364,7 +371,10 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]): selected agent. Subclasses with additional preconditions should override and call `super()._validate(spec)` first, using the returned spec for further checks.""" - manifest = spec.manifest.load_for_agent(spec.agent_name) # type: ignore[union-attr] + assert isinstance(spec.manifest, ManifestIndex), ( + "_validate() called on a spec whose manifest is already loaded" + ) + manifest = spec.manifest.load_for_agent(spec.agent_name) spec = replace(spec, manifest=manifest) agent = manifest.agent self._validate_skills(agent.skills) @@ -384,7 +394,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]): ) def _validate_agent_provider_dockerfile(self, spec: BottleSpec) -> None: - manifest = spec.manifest # type: ignore[assignment] + manifest = spec.loaded_manifest bottle = manifest.bottle dockerfile = bottle.agent_provider.dockerfile if not dockerfile: diff --git a/bot_bottle/backend/docker/launch.py b/bot_bottle/backend/docker/launch.py index f937b30..e0f7f7c 100644 --- a/bot_bottle/backend/docker/launch.py +++ b/bot_bottle/backend/docker/launch.py @@ -75,7 +75,7 @@ def launch( Teardown on exit.""" stack = ExitStack() - _bottle_for_revoke = plan.spec.manifest.bottle + _bottle_for_revoke = plan.spec.loaded_manifest.bottle _git_gate_dir_for_revoke = git_gate_state_dir(plan.slug) def teardown() -> None: diff --git a/bot_bottle/backend/macos_container/launch.py b/bot_bottle/backend/macos_container/launch.py index 9db7411..a0e9d26 100644 --- a/bot_bottle/backend/macos_container/launch.py +++ b/bot_bottle/backend/macos_container/launch.py @@ -68,7 +68,7 @@ def launch( ) -> Generator[MacosContainerBottle, None, None]: """Build, run, provision, and yield an Apple Container bottle.""" stack = ExitStack() - bottle_for_revoke = plan.spec.manifest.bottle + bottle_for_revoke = plan.spec.loaded_manifest.bottle git_gate_dir_for_revoke = git_gate_state_dir(plan.slug) def teardown() -> None: diff --git a/bot_bottle/backend/resolve_common.py b/bot_bottle/backend/resolve_common.py index affb345..21b483d 100644 --- a/bot_bottle/backend/resolve_common.py +++ b/bot_bottle/backend/resolve_common.py @@ -69,8 +69,7 @@ def write_launch_metadata( def prepare_agent_state_dir(slug: str, spec: BottleSpec) -> tuple[Path, Path]: """Create the agent state subdir, write the prompt file. Returns (agent_dir, prompt_file).""" - manifest = spec.manifest # type: ignore[assignment] - agent = manifest.agent + agent = spec.loaded_manifest.agent agent_dir = agent_state_dir(slug) agent_dir.mkdir(parents=True, exist_ok=True) prompt_file = agent_dir / "prompt.txt" diff --git a/bot_bottle/backend/smolmachines/launch.py b/bot_bottle/backend/smolmachines/launch.py index e16b611..93a4d50 100644 --- a/bot_bottle/backend/smolmachines/launch.py +++ b/bot_bottle/backend/smolmachines/launch.py @@ -130,7 +130,7 @@ def _teardown_smolmachines( except BaseException as exc: # noqa: W0718 — teardown must not fail teardown_exc = exc warn(f"smolmachines teardown failed: {exc!r}") - bottle = plan.spec.manifest.bottle + bottle = plan.spec.loaded_manifest.bottle revoke_git_gate_provisioned_keys(bottle, git_gate_state_dir(plan.slug)) if teardown_exc is not None: raise teardown_exc diff --git a/bot_bottle/contrib/claude/agent_provider.py b/bot_bottle/contrib/claude/agent_provider.py index add66a2..91733cd 100644 --- a/bot_bottle/contrib/claude/agent_provider.py +++ b/bot_bottle/contrib/claude/agent_provider.py @@ -211,7 +211,7 @@ class ClaudeAgentProvider(AgentProvider): when the agent has no skills.""" from ...backend.util import host_skill_dir - agent = plan.spec.manifest.agent + agent = plan.spec.loaded_manifest.agent if not agent.skills: return skills_dir = _skills_dir(plan.guest_home) @@ -240,7 +240,7 @@ class ClaudeAgentProvider(AgentProvider): f"chown node:node {prompt_path} && chmod 600 {prompt_path}", user="root", ) - agent = plan.spec.manifest.agent + agent = plan.spec.loaded_manifest.agent return prompt_path if plan.agent_provision.has_prompt or agent.prompt else None def provision(self, plan: "BottlePlan", bottle: "Bottle") -> None: diff --git a/bot_bottle/contrib/codex/agent_provider.py b/bot_bottle/contrib/codex/agent_provider.py index 8b5f485..997a30f 100644 --- a/bot_bottle/contrib/codex/agent_provider.py +++ b/bot_bottle/contrib/codex/agent_provider.py @@ -177,7 +177,7 @@ class CodexAgentProvider(AgentProvider): skills.""" from ...backend.util import host_skill_dir - agent = plan.spec.manifest.agent + agent = plan.spec.loaded_manifest.agent if not agent.skills: return skills_dir = _skills_dir(plan.guest_home) @@ -206,7 +206,7 @@ class CodexAgentProvider(AgentProvider): f"chown node:node {prompt_path} && chmod 600 {prompt_path}", user="root", ) - agent = plan.spec.manifest.agent + agent = plan.spec.loaded_manifest.agent return prompt_path if plan.agent_provision.has_prompt or agent.prompt else None def provision(self, plan: "BottlePlan", bottle: "Bottle") -> None: diff --git a/bot_bottle/contrib/pi/agent_provider.py b/bot_bottle/contrib/pi/agent_provider.py index 83d498d..73b12c3 100644 --- a/bot_bottle/contrib/pi/agent_provider.py +++ b/bot_bottle/contrib/pi/agent_provider.py @@ -232,7 +232,7 @@ class PiAgentProvider(AgentProvider): def provision_skills(self, plan: "BottlePlan", bottle: "Bottle") -> None: from ...backend.util import host_skill_dir - agent = plan.spec.manifest.agent + agent = plan.spec.loaded_manifest.agent if not agent.skills: return skills_dir = _skills_dir(plan.guest_home) diff --git a/tests/unit/test_contrib_claude_provider.py b/tests/unit/test_contrib_claude_provider.py index c8ce4c0..cb806e8 100644 --- a/tests/unit/test_contrib_claude_provider.py +++ b/tests/unit/test_contrib_claude_provider.py @@ -24,7 +24,7 @@ from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan from bot_bottle.contrib.claude.agent_provider import ClaudeAgentProvider from bot_bottle.egress import EgressPlan from bot_bottle.git_gate import GitGatePlan -from bot_bottle.manifest import Manifest, ManifestIndex +from bot_bottle.manifest import ManifestIndex from bot_bottle.supervise import SupervisePlan diff --git a/tests/unit/test_contrib_codex_provider.py b/tests/unit/test_contrib_codex_provider.py index 4e5411c..8ba5b97 100644 --- a/tests/unit/test_contrib_codex_provider.py +++ b/tests/unit/test_contrib_codex_provider.py @@ -24,7 +24,7 @@ from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan from bot_bottle.contrib.codex.agent_provider import CodexAgentProvider from bot_bottle.egress import EgressPlan from bot_bottle.git_gate import GitGatePlan -from bot_bottle.manifest import Manifest, ManifestIndex +from bot_bottle.manifest import ManifestIndex from bot_bottle.supervise import SupervisePlan diff --git a/tests/unit/test_contrib_pi_provider.py b/tests/unit/test_contrib_pi_provider.py index dd0a88c..f9d83c2 100644 --- a/tests/unit/test_contrib_pi_provider.py +++ b/tests/unit/test_contrib_pi_provider.py @@ -16,7 +16,7 @@ from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan from bot_bottle.contrib.pi.agent_provider import PiAgentProvider from bot_bottle.egress import EgressPlan from bot_bottle.git_gate import GitGatePlan -from bot_bottle.manifest import Manifest, ManifestIndex +from bot_bottle.manifest import ManifestIndex _URL = "http://supervise:9100/" diff --git a/tests/unit/test_docker_provision_git_user.py b/tests/unit/test_docker_provision_git_user.py index 6c1220c..4c572df 100644 --- a/tests/unit/test_docker_provision_git_user.py +++ b/tests/unit/test_docker_provision_git_user.py @@ -21,7 +21,7 @@ from bot_bottle.backend import Bottle, BottleSpec, ExecResult from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan from bot_bottle.egress import EgressPlan from bot_bottle.git_gate import GitGatePlan -from bot_bottle.manifest import Manifest, ManifestIndex +from bot_bottle.manifest import ManifestIndex class _Provider(AgentProvider): diff --git a/tests/unit/test_manifest_agent_git_user.py b/tests/unit/test_manifest_agent_git_user.py index 80d1afc..c9ccd26 100644 --- a/tests/unit/test_manifest_agent_git_user.py +++ b/tests/unit/test_manifest_agent_git_user.py @@ -45,7 +45,7 @@ def _manifest(*, bottle_user=None, agent_git=None) -> Manifest: # type: ignore }).load_for_agent("impl") -def _index(*, bottle_user=None, agent_git=None) -> ManifestIndex: +def _index(*, bottle_user: dict[str, object] | None = None, agent_git: dict[str, object] | None = None) -> ManifestIndex: """Build an index with one agent 'impl' without loading it.""" bottle: dict = {} # type: ignore if bottle_user is not None: diff --git a/tests/unit/test_manifest_md_load.py b/tests/unit/test_manifest_md_load.py index 4f04aae..7659469 100644 --- a/tests/unit/test_manifest_md_load.py +++ b/tests/unit/test_manifest_md_load.py @@ -11,7 +11,7 @@ import textwrap import unittest from pathlib import Path -from bot_bottle.manifest import ManifestError, Manifest, ManifestIndex +from bot_bottle.manifest import ManifestError, ManifestIndex def _write(p: Path, text: str) -> None: diff --git a/tests/unit/test_smolmachines_provision.py b/tests/unit/test_smolmachines_provision.py index 3bbfe8c..5864916 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 ManifestGitEntry, ManifestKeyConfig, Manifest, ManifestIndex +from bot_bottle.manifest import ManifestGitEntry, ManifestKeyConfig, ManifestIndex from bot_bottle.supervise import SupervisePlan