feat(bottles): implement bottle factory abstraction per PRD 0003
test / run tests/run_tests.py (pull_request) Successful in 16s
test / run tests/run_tests.py (pull_request) Successful in 16s
Introduce claude_bottle/bottles/ with a Bottle Protocol and a get_bottle_factory() that dispatches on CLAUDE_BOTTLE_PLATFORM (default "docker"). Move every Docker-specific subprocess.run call from cli/start.py, plus the orchestration of build, networks, the pipelock sidecar, container launch, and per-container provisioning (prompt, skills, ssh, .git), into create_docker_bottle. Drop bottles[].runtime from the manifest schema. Auto-detect whether gVisor is registered with the daemon and pass --runtime=runsc when it is; the preflight shows the resolved runtime so the choice is visible. Manifests still carrying 'runtime' get a clear error pointing at the auto-detect behavior, rather than silent ignore. Out of scope: cli/cleanup.py and cli/list.py still call docker directly. They enumerate active bottles across the host, which is a separate concern from "create a bottle" and is left for a follow-up that introduces a list_active/cleanup primitive on the factory.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""Per-platform bottle factories.
|
||||
|
||||
A bottle is a running, isolated environment with claude inside. Each
|
||||
platform exposes a factory (currently only Docker) that owns the
|
||||
end-to-end lifecycle: image build, container/sidecar launch, file
|
||||
provisioning, and teardown.
|
||||
|
||||
Selection is driven by the CLAUDE_BOTTLE_PLATFORM env var (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 typing import Callable, Protocol
|
||||
|
||||
from ..log import die
|
||||
from .docker import create_docker_bottle
|
||||
|
||||
|
||||
class Bottle(Protocol):
|
||||
"""Handle to a running bottle. Yielded by a factory's context manager.
|
||||
|
||||
`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: ...
|
||||
|
||||
|
||||
BottleFactory = Callable[..., AbstractContextManager[Bottle]]
|
||||
|
||||
|
||||
_FACTORIES: dict[str, BottleFactory] = {
|
||||
"docker": create_docker_bottle,
|
||||
}
|
||||
|
||||
|
||||
def get_bottle_factory() -> BottleFactory:
|
||||
"""Resolve the bottle factory for the active platform. 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 _FACTORIES:
|
||||
known = ", ".join(sorted(_FACTORIES))
|
||||
die(f"unknown CLAUDE_BOTTLE_PLATFORM={name!r}; known platforms: {known}")
|
||||
return _FACTORIES[name]
|
||||
Reference in New Issue
Block a user