Files
bot-bottle/claude_bottle/bottles/__init__.py
T
didericis 2827d9b899
test / run tests/run_tests.py (pull_request) Successful in 19s
refactor(bottles): introduce BottlePlan base + move print onto plan
- 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.
2026-05-10 22:49:57 -04:00

118 lines
3.5 KiB
Python

"""Per-platform bottle factories.
A bottle is a running, isolated environment with claude inside. Each
platform exposes two functions:
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.
launch(plan) -> ContextManager[Bottle]
Brings up the container (or VM, or remote machine), provisions
it, yields a Bottle handle, and tears everything down on exit.
Selection is driven by CLAUDE_BOTTLE_PLATFORM (default "docker"). Per
PRD 0003 the manifest does not carry a platform field; the host
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 ..manifest import Manifest
@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):
"""Handle to a running bottle. Yielded by a platform's launch step.
`exec_claude` runs `claude` inside the bottle and blocks until the
session ends. `cp_in` copies a host path into the bottle. `close`
is an idempotent alias for context-manager teardown.
"""
name: str
def exec_claude(self, argv: list[str], *, tty: bool = True) -> int: ...
def cp_in(self, host_path: str, container_path: str) -> None: ...
def close(self) -> None: ...
@dataclass(frozen=True)
class BottlePlatform:
"""Bundles a platform's two-phase factory under one selectable name."""
name: str
prepare: Callable[..., BottlePlan]
launch: Callable[..., AbstractContextManager[Bottle]]
_PLATFORMS: dict[str, BottlePlatform] = {
"docker": BottlePlatform(
name="docker",
prepare=prepare_docker_bottle,
launch=launch_docker_bottle,
),
}
def get_bottle_platform() -> BottlePlatform:
"""Resolve the bottle platform for the active environment. Dies with
a pointer at the known platforms if CLAUDE_BOTTLE_PLATFORM names an
unimplemented one."""
name = os.environ.get("CLAUDE_BOTTLE_PLATFORM", "docker")
if name not in _PLATFORMS:
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",
]