236c4fa50c
test / run tests/run_tests.py (pull_request) Successful in 13s
The spec is intent-only and platform-agnostic — only the plan carries Docker-specific fields. Drop the 'Docker' prefix and re-export from claude_bottle.bottles so callers see it as cross-platform.
75 lines
2.4 KiB
Python
75 lines
2.4 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=...) -> Plan
|
|
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 contextlib import AbstractContextManager
|
|
from dataclasses import dataclass
|
|
from typing import Callable, Protocol
|
|
|
|
from ..log import die
|
|
from .docker import BottleSpec, launch_docker_bottle, prepare_docker_bottle
|
|
|
|
__all__ = ["Bottle", "BottlePlatform", "BottleSpec", "get_bottle_platform"]
|
|
|
|
|
|
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[..., object]
|
|
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]
|