refactor(platform): rename claude_bottle/bottles -> claude_bottle/platform
test / run tests/run_tests.py (pull_request) Successful in 13s
test / run tests/run_tests.py (pull_request) Successful in 13s
'bottles' was the package name when it held a single Bottle Protocol; since we added BottlePlatform / BottlePlan / BottleCleanupPlan and made it the home of platform dispatch, 'platform' describes the package better. The 'bottle' concept (and the manifest field) stays. CLI imports update from ..bottles to ..platform; internal relative imports inside the package survive the rename unchanged. Git detected all 7 file renames.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
"""Per-platform bottle factories.
|
||||
|
||||
A bottle is a running, isolated environment with claude inside. Each
|
||||
platform exposes four methods:
|
||||
|
||||
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.
|
||||
|
||||
prepare_cleanup() -> BottleCleanupPlan
|
||||
Enumerates orphaned resources left behind by previous bottles
|
||||
(containers, networks, ...). Idempotent; no side effects.
|
||||
|
||||
cleanup(plan) -> None
|
||||
Actually removes everything described by the cleanup plan.
|
||||
|
||||
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 ..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."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BottleCleanupPlan(ABC):
|
||||
"""Base output of a platform's prepare_cleanup step. Concrete
|
||||
subclasses (e.g. DockerBottleCleanupPlan) carry platform-specific
|
||||
lists of resources to be removed and implement `print` + `empty`."""
|
||||
|
||||
@abstractmethod
|
||||
def print(self) -> None:
|
||||
"""Render the cleanup y/N summary to stderr."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def empty(self) -> bool:
|
||||
"""True iff there is nothing to clean up; the CLI uses this to
|
||||
short-circuit before showing the y/N."""
|
||||
|
||||
|
||||
class Bottle(ABC):
|
||||
"""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
|
||||
|
||||
@abstractmethod
|
||||
def exec_claude(self, argv: list[str], *, tty: bool = True) -> int: ...
|
||||
|
||||
@abstractmethod
|
||||
def cp_in(self, host_path: str, container_path: str) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None: ...
|
||||
|
||||
|
||||
class BottlePlatform(ABC):
|
||||
"""Abstract base for selectable bottle platforms. Concrete subclasses
|
||||
(e.g. DockerBottlePlatform) own their own prepare/launch impls.
|
||||
Symmetric with the BottlePlan → DockerBottlePlan hierarchy."""
|
||||
|
||||
name: str
|
||||
|
||||
@abstractmethod
|
||||
def prepare(self, spec: BottleSpec, *, stage_dir: Path) -> BottlePlan:
|
||||
"""Resolve names, validate host-side prerequisites, write
|
||||
scratch files. No remote/runtime resources created yet."""
|
||||
|
||||
@abstractmethod
|
||||
def launch(self, plan: BottlePlan) -> AbstractContextManager[Bottle]:
|
||||
"""Build/run the bottle and yield a handle; tear down on exit."""
|
||||
|
||||
@abstractmethod
|
||||
def prepare_cleanup(self) -> BottleCleanupPlan:
|
||||
"""Enumerate orphaned resources from previous bottles. No side
|
||||
effects; safe to call before the y/N."""
|
||||
|
||||
@abstractmethod
|
||||
def cleanup(self, plan: BottleCleanupPlan) -> None:
|
||||
"""Remove everything described by the cleanup plan."""
|
||||
|
||||
@abstractmethod
|
||||
def list_active(self) -> None:
|
||||
"""Print every currently-running bottle on this platform to
|
||||
stderr (name + status)."""
|
||||
|
||||
|
||||
# Import concrete platform classes AFTER the base types are defined, so
|
||||
# each platform module can pull BottleSpec / BottlePlan / BottlePlatform
|
||||
# via `from . import ...` without hitting a partially-initialized module.
|
||||
from .docker import DockerBottlePlatform # noqa: E402
|
||||
|
||||
|
||||
_PLATFORMS: dict[str, BottlePlatform] = {
|
||||
"docker": DockerBottlePlatform(),
|
||||
}
|
||||
|
||||
|
||||
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",
|
||||
"BottleCleanupPlan",
|
||||
"BottlePlan",
|
||||
"BottlePlatform",
|
||||
"BottleSpec",
|
||||
"get_bottle_platform",
|
||||
]
|
||||
Reference in New Issue
Block a user