Files
bot-bottle/claude_bottle/bottles/__init__.py
T
didericis d75cc9325f
test / run tests/run_tests.py (pull_request) Successful in 16s
feat(bottles): implement bottle factory abstraction per PRD 0003
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.
2026-05-10 22:15:05 -04:00

55 lines
1.7 KiB
Python

"""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]