refactor(types): move loaded manifest from BottleSpec to BottlePlan
test / integration (pull_request) Successful in 21s
test / unit (pull_request) Successful in 49s
lint / lint (push) Successful in 2m15s
test / unit (push) Successful in 56s
test / integration (push) Successful in 27s
Update Quality Badges / update-badges (push) Successful in 2m37s

BottleSpec.manifest was ManifestIndex | Manifest — a union encoding
two lifecycle stages in one field. The union was unjustifiable:
it forced a type-narrowing workaround (loaded_manifest property)
on every consumer.

Clean split:
- BottleSpec.manifest: ManifestIndex (always; CLI-supplied intent)
- BottlePlan.manifest: Manifest (always; loaded by _validate())

_validate() returns the loaded Manifest directly. prepare() passes
it to _resolve_plan(), which stores it on the plan. All provisioner
code now reads plan.manifest.agent / plan.manifest.bottle — no
union, no asserts, no type: ignore.
This commit was merged in pull request #239.
This commit is contained in:
2026-06-23 02:22:10 +00:00
committed by didericis
parent 56ef71060a
commit da42740156
24 changed files with 112 additions and 94 deletions
+8 -11
View File
@@ -67,16 +67,6 @@ def _manifest(*, supervise: bool, with_git: bool, with_egress: bool) -> Manifest
})
def _spec(*, supervise: bool, with_git: bool, with_egress: bool) -> BottleSpec:
return BottleSpec(
manifest=_manifest(
supervise=supervise, with_git=with_git, with_egress=with_egress,
),
agent_name="demo",
copy_cwd=False,
user_cwd="/tmp/x",
)
def _git_gate_plan(upstreams: tuple[GitGateUpstream, ...] = ()) -> GitGatePlan:
return GitGatePlan(
@@ -146,9 +136,16 @@ def _plan(
roles=(),
),)
spec = _spec(supervise=supervise, with_git=with_git, with_egress=with_egress)
index = _manifest(supervise=supervise, with_git=with_git, with_egress=with_egress)
spec = BottleSpec(
manifest=index,
agent_name="demo",
copy_cwd=False,
user_cwd="/tmp/x",
)
return DockerBottlePlan(
spec=spec,
manifest=index.load_for_agent("demo"),
stage_dir=STAGE,
slug=SLUG,
forwarded_env={"CLAUDE_CODE_OAUTH_TOKEN": "x"},
+5 -3
View File
@@ -55,7 +55,7 @@ def _plan(
bottle_json: dict = {"agent_provider": {"template": "claude"}} # type: ignore
if supervise:
bottle_json["supervise"] = True
manifest = ManifestIndex.from_json_obj({
index = ManifestIndex.from_json_obj({
"bottles": {"dev": bottle_json},
"agents": {
"demo": {
@@ -64,9 +64,10 @@ def _plan(
"bottle": "dev",
},
},
}).load_for_agent("demo")
})
manifest = index.load_for_agent("demo")
spec = BottleSpec(
manifest=manifest, agent_name="demo",
manifest=index, agent_name="demo",
copy_cwd=False, user_cwd="/tmp/x",
)
supervise_plan = None
@@ -78,6 +79,7 @@ def _plan(
)
return DockerBottlePlan(
spec=spec,
manifest=manifest,
stage_dir=Path("/tmp/stage"),
slug="demo-abc12",
forwarded_env={},
+5 -3
View File
@@ -55,7 +55,7 @@ def _plan(
bottle_json: dict = {"agent_provider": {"template": "codex"}} # type: ignore
if supervise:
bottle_json["supervise"] = True
manifest = ManifestIndex.from_json_obj({
index = ManifestIndex.from_json_obj({
"bottles": {"dev": bottle_json},
"agents": {
"demo": {
@@ -64,9 +64,10 @@ def _plan(
"bottle": "dev",
},
},
}).load_for_agent("demo")
})
manifest = index.load_for_agent("demo")
spec = BottleSpec(
manifest=manifest, agent_name="demo",
manifest=index, agent_name="demo",
copy_cwd=False, user_cwd="/tmp/x",
)
supervise_plan = None
@@ -78,6 +79,7 @@ def _plan(
)
return DockerBottlePlan(
spec=spec,
manifest=manifest,
stage_dir=Path("/tmp/stage"),
slug="demo-abc12",
forwarded_env={},
+5 -3
View File
@@ -43,7 +43,7 @@ def _plan(
skills: list[str] | None = None,
agent_provision: AgentProvisionPlan | None = None,
) -> DockerBottlePlan:
manifest = ManifestIndex.from_json_obj({
index = ManifestIndex.from_json_obj({
"bottles": {"dev": {"agent_provider": {"template": "pi"}}},
"agents": {
"demo": {
@@ -52,13 +52,15 @@ def _plan(
"bottle": "dev",
},
},
}).load_for_agent("demo")
})
manifest = index.load_for_agent("demo")
spec = BottleSpec(
manifest=manifest, agent_name="demo",
manifest=index, agent_name="demo",
copy_cwd=False, user_cwd="/tmp/x",
)
return DockerBottlePlan(
spec=spec,
manifest=manifest,
stage_dir=Path("/tmp/stage"),
slug="demo-abc12",
forwarded_env={},
+7 -11
View File
@@ -23,22 +23,17 @@ from bot_bottle.egress import EgressPlan
from bot_bottle.git_gate import GitGatePlan
from bot_bottle.manifest import ManifestIndex
from bot_bottle.manifest import Manifest, ManifestIndex
def _manifest() -> Manifest:
return ManifestIndex.from_json_obj({
"bottles": {"dev": {}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).load_for_agent("demo")
_INDEX = ManifestIndex.from_json_obj({
"bottles": {"dev": {}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
})
def _plan(tmp: str) -> DockerBottlePlan:
stage = Path(tmp)
manifest = _manifest()
manifest = _INDEX.load_for_agent("demo")
spec = BottleSpec(
manifest=manifest,
manifest=_INDEX,
agent_name="demo",
copy_cwd=False,
user_cwd=tmp,
@@ -46,6 +41,7 @@ def _plan(tmp: str) -> DockerBottlePlan:
)
return DockerBottlePlan(
spec=spec,
manifest=manifest,
stage_dir=stage,
git_gate_plan=GitGatePlan(
slug="test-teardown-00001",
+5 -3
View File
@@ -51,16 +51,18 @@ def _plan(*, git_user: dict | None = None, # type: ignore
bottle_json: dict = {} # type: ignore
if git_user is not None:
bottle_json["git-gate"] = {"user": git_user}
manifest = ManifestIndex.from_json_obj({
index = ManifestIndex.from_json_obj({
"bottles": {"dev": bottle_json},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).load_for_agent("demo")
})
manifest = index.load_for_agent("demo")
spec = BottleSpec(
manifest=manifest, agent_name="demo",
manifest=index, agent_name="demo",
copy_cwd=copy_cwd, user_cwd=user_cwd,
)
return DockerBottlePlan(
spec=spec,
manifest=manifest,
stage_dir=stage_dir or Path("/tmp/stage"),
slug="demo-abc12",
forwarded_env={},
@@ -11,6 +11,12 @@ from unittest.mock import patch
from bot_bottle.backend.macos_container import launch
from bot_bottle.backend.macos_container.bottle_plan import MacosContainerBottlePlan
from bot_bottle.manifest import ManifestIndex
_MANIFEST = ManifestIndex.from_json_obj({
"bottles": {"dev": {}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).load_for_agent("demo")
def _plan(
@@ -67,6 +73,7 @@ def _plan(
)
return cast(MacosContainerBottlePlan, SimpleNamespace(
spec=SimpleNamespace(),
manifest=_MANIFEST,
stage_dir=stage_dir,
slug="dev-abc",
container_name="bot-bottle-dev-abc",
@@ -193,6 +200,7 @@ class TestMacosContainerLaunchArgv(unittest.TestCase):
)
plan = MacosContainerBottlePlan(
spec=base.spec,
manifest=base.manifest,
stage_dir=base.stage_dir,
git_gate_plan=base.git_gate_plan,
egress_plan=base.egress_plan,
+18 -17
View File
@@ -22,16 +22,15 @@ from bot_bottle.git_gate import GitGatePlan, GitGateUpstream
from bot_bottle.manifest import Manifest, ManifestIndex
def _manifest() -> Manifest:
return ManifestIndex.from_json_obj({
"bottles": {"dev": {}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).load_for_agent("demo")
_INDEX = ManifestIndex.from_json_obj({
"bottles": {"dev": {}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
})
def _spec(manifest: Manifest, tmp: str) -> BottleSpec:
def _spec(index: ManifestIndex, tmp: str) -> BottleSpec:
return BottleSpec(
manifest=manifest,
manifest=index,
agent_name="demo",
copy_cwd=False,
user_cwd=tmp,
@@ -92,10 +91,11 @@ def _agent_provision(tmp: str) -> AgentProvisionPlan:
)
def _docker_plan(spec: BottleSpec, tmp: str) -> DockerBottlePlan:
def _docker_plan(spec: BottleSpec, manifest: Manifest, tmp: str) -> DockerBottlePlan:
stage = Path(tmp)
return DockerBottlePlan(
spec=spec,
manifest=manifest,
stage_dir=stage,
git_gate_plan=_git_gate_plan(tmp),
egress_plan=_egress_plan(tmp),
@@ -107,10 +107,11 @@ def _docker_plan(spec: BottleSpec, tmp: str) -> DockerBottlePlan:
)
def _smolmachines_plan(spec: BottleSpec, tmp: str) -> SmolmachinesBottlePlan:
def _smolmachines_plan(spec: BottleSpec, manifest: Manifest, tmp: str) -> SmolmachinesBottlePlan:
stage = Path(tmp)
return SmolmachinesBottlePlan(
spec=spec,
manifest=manifest,
stage_dir=stage,
git_gate_plan=_git_gate_plan(tmp),
egress_plan=_egress_plan(tmp),
@@ -140,10 +141,10 @@ class TestGitGatePrintParity(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.mkdtemp(prefix="plan-print-parity-")
manifest = _manifest()
spec = _spec(manifest, self._tmp)
self._docker_lines = _capture_print(_docker_plan(spec, self._tmp))
self._smol_lines = _capture_print(_smolmachines_plan(spec, self._tmp))
manifest = _INDEX.load_for_agent("demo")
spec = _spec(_INDEX, self._tmp)
self._docker_lines = _capture_print(_docker_plan(spec, manifest, self._tmp))
self._smol_lines = _capture_print(_smolmachines_plan(spec, manifest, self._tmp))
def _git_gate_lines(self, lines: list[str]) -> list[str]:
return [ln for ln in lines if "git gate" in ln]
@@ -170,10 +171,10 @@ class TestEgressPrintParity(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.mkdtemp(prefix="plan-print-parity-")
manifest = _manifest()
spec = _spec(manifest, self._tmp)
self._docker_lines = _capture_print(_docker_plan(spec, self._tmp))
self._smol_lines = _capture_print(_smolmachines_plan(spec, self._tmp))
manifest = _INDEX.load_for_agent("demo")
spec = _spec(_INDEX, self._tmp)
self._docker_lines = _capture_print(_docker_plan(spec, manifest, self._tmp))
self._smol_lines = _capture_print(_smolmachines_plan(spec, manifest, self._tmp))
def _egress_section(self, lines: list[str]) -> list[str]:
"""Return lines from the egress label through the last route entry.
+5 -3
View File
@@ -110,7 +110,7 @@ def _plan(
bottle_json["git-gate"] = git_gate_json
if supervise:
bottle_json["supervise"] = True
manifest = ManifestIndex.from_json_obj({
index = ManifestIndex.from_json_obj({
"bottles": {"dev": bottle_json},
"agents": {
"demo": {
@@ -119,9 +119,10 @@ def _plan(
"bottle": "dev",
},
},
}).load_for_agent("demo")
})
manifest = index.load_for_agent("demo")
spec = BottleSpec(
manifest=manifest,
manifest=index,
agent_name="demo",
copy_cwd=copy_cwd,
user_cwd=user_cwd,
@@ -135,6 +136,7 @@ def _plan(
)
return SmolmachinesBottlePlan(
spec=spec,
manifest=manifest,
stage_dir=stage_dir or Path("/tmp/stage"),
slug="demo-abc12",
bundle_subnet="192.168.50.0/24",