refactor(bottles): introduce BottlePlan base + move print onto plan
test / run tests/run_tests.py (pull_request) Successful in 19s

- Add BottlePlan (frozen dataclass + ABC) with spec, stage_dir, and an
  abstract `print(*, remote_control)` method.
- DockerBottlePlan now inherits from BottlePlan; spec/stage_dir come
  from the base, Docker-specific fields stay on the subclass.
- Move BottleSpec from bottles/docker.py to bottles/__init__.py so the
  cross-platform types live together. docker.py pulls them via
  `from . import ...`.
- Move show_plan from cli/start.py to `DockerBottlePlan.print`. Caller
  becomes `plan.print(remote_control=...)`. The CLI no longer reads
  any Docker-specific fields.
- BottlePlatform.prepare is now typed `Callable[..., BottlePlan]`.

cmd_start drops ~46 more lines.
This commit is contained in:
2026-05-10 22:49:57 -04:00
parent 236c4fa50c
commit 2827d9b899
3 changed files with 94 additions and 70 deletions
+47 -4
View File
@@ -3,7 +3,7 @@
A bottle is a running, isolated environment with claude inside. Each
platform exposes two functions:
prepare(spec, stage_dir=...) -> Plan
prepare(spec, stage_dir=...) -> BottlePlan
Resolves names, validates host-side prerequisites, and writes
scratch files. No remote/runtime resources are created yet.
Safe to call before the y/N preflight.
@@ -20,14 +20,48 @@ environment picks.
from __future__ import annotations
import os
from abc import ABC, abstractmethod
from contextlib import AbstractContextManager
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Protocol
from ..log import die
from .docker import BottleSpec, launch_docker_bottle, prepare_docker_bottle
from ..manifest import Manifest
__all__ = ["Bottle", "BottlePlatform", "BottleSpec", "get_bottle_platform"]
@dataclass(frozen=True)
class BottleSpec:
"""CLI-supplied intent. Platform-agnostic — each platform's prepare
step consumes it and produces its own platform-specific plan.
Resolved values (image names, container name, scratch paths, runsc
availability) live on the plan, not the spec."""
manifest: Manifest
agent_name: str
copy_cwd: bool
user_cwd: str
forward_oauth_token: bool
@dataclass(frozen=True)
class BottlePlan(ABC):
"""Base output of a platform's prepare step. Concrete subclasses
(e.g. DockerBottlePlan) add platform-specific resolved fields and
implement `print`."""
spec: BottleSpec
stage_dir: Path
@abstractmethod
def print(self, *, remote_control: bool) -> None:
"""Render the y/N preflight summary to stderr."""
# Import concrete platform factories AFTER the base types are defined,
# so each platform module can pull BottleSpec / BottlePlan via
# `from . import ...` without hitting a partially-initialized module.
from .docker import launch_docker_bottle, prepare_docker_bottle # noqa: E402
class Bottle(Protocol):
@@ -50,7 +84,7 @@ class BottlePlatform:
"""Bundles a platform's two-phase factory under one selectable name."""
name: str
prepare: Callable[..., object]
prepare: Callable[..., BottlePlan]
launch: Callable[..., AbstractContextManager[Bottle]]
@@ -72,3 +106,12 @@ def get_bottle_platform() -> BottlePlatform:
known = ", ".join(sorted(_PLATFORMS))
die(f"unknown CLAUDE_BOTTLE_PLATFORM={name!r}; known platforms: {known}")
return _PLATFORMS[name]
__all__ = [
"Bottle",
"BottlePlan",
"BottlePlatform",
"BottleSpec",
"get_bottle_platform",
]