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:
+11
-23
@@ -7,8 +7,7 @@ Schema (see CLAUDE.md "Intended design"):
|
||||
"<bottle-name>": {
|
||||
"env": { "<NAME>": <env-entry>, ... },
|
||||
"ssh": [ <ssh-entry>, ... ],
|
||||
"egress": { "allowlist": [ "<hostname>", ... ] },
|
||||
"runtime": "runc" | "runsc"
|
||||
"egress": { "allowlist": [ "<hostname>", ... ] }
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
@@ -33,15 +32,11 @@ import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Literal, Mapping, cast
|
||||
from typing import Mapping, cast
|
||||
|
||||
from .log import die
|
||||
|
||||
|
||||
Runtime = Literal["runc", "runsc"]
|
||||
_SUPPORTED_RUNTIMES: tuple[Runtime, ...] = ("runc", "runsc")
|
||||
|
||||
|
||||
def _empty_str_dict() -> dict[str, str]:
|
||||
return {}
|
||||
|
||||
@@ -116,12 +111,19 @@ class Bottle:
|
||||
env: Mapping[str, str] = field(default_factory=_empty_str_dict)
|
||||
ssh: tuple[SshEntry, ...] = ()
|
||||
egress: BottleEgress = field(default_factory=BottleEgress)
|
||||
runtime: Runtime = "runc"
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, name: str, raw: object) -> "Bottle":
|
||||
d = _as_json_object(raw, f"bottle '{name}'")
|
||||
|
||||
if "runtime" in d:
|
||||
die(
|
||||
f"bottle '{name}' has a 'runtime' field, which is no longer "
|
||||
f"supported. gVisor (runsc) is now auto-detected when "
|
||||
f"registered with Docker; remove the 'runtime' field from "
|
||||
f"the bottle definition."
|
||||
)
|
||||
|
||||
env: dict[str, str] = {}
|
||||
env_raw = d.get("env")
|
||||
if env_raw is not None:
|
||||
@@ -152,21 +154,7 @@ class Bottle:
|
||||
else BottleEgress()
|
||||
)
|
||||
|
||||
runtime_raw = d.get("runtime")
|
||||
runtime: Runtime
|
||||
if runtime_raw is None:
|
||||
runtime = "runc"
|
||||
else:
|
||||
if not isinstance(runtime_raw, str):
|
||||
die(f"bottle '{name}' runtime must be a string (was {type(runtime_raw).__name__})")
|
||||
if runtime_raw not in _SUPPORTED_RUNTIMES:
|
||||
die(
|
||||
f"bottle '{name}' runtime '{runtime_raw}' is not supported. "
|
||||
f"Use one of: {', '.join(_SUPPORTED_RUNTIMES)}."
|
||||
)
|
||||
runtime = runtime_raw
|
||||
|
||||
return cls(env=env, ssh=ssh, egress=egress, runtime=runtime)
|
||||
return cls(env=env, ssh=ssh, egress=egress)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
Reference in New Issue
Block a user