fix(types): add BottleSpec.loaded_manifest to satisfy pyright on union type

BottleSpec.manifest is ManifestIndex | Manifest (pre/post _validate()).
Downstream code always runs post-validate so it needs Manifest, but
pyright flagged every .agent/.bottle access. The new loaded_manifest
property asserts isinstance and returns Manifest, giving pyright a
narrowed type without scattering type: ignore everywhere.

Also remove unused Manifest imports from test files and annotate the
_index() helper in test_manifest_agent_git_user.
This commit is contained in:
2026-06-23 02:08:27 +00:00
committed by didericis
parent 294a6ed023
commit 56ef71060a
16 changed files with 31 additions and 22 deletions
+1 -1
View File
@@ -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)
+14 -4
View File
@@ -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 <identity>`) 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:
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+1 -2
View File
@@ -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"
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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:
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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)