refactor: rename platform abstraction to backend
test / run tests/run_tests.py (pull_request) Successful in 21s
test / run tests/run_tests.py (pull_request) Successful in 21s
Across the package: - claude_bottle/platform/ -> claude_bottle/backend/ - platform/docker/platform.py -> backend/docker/backend.py - class BottlePlatform -> BottleBackend - class DockerBottlePlatform -> DockerBottleBackend - get_bottle_platform() -> get_bottle_backend() - env var CLAUDE_BOTTLE_PLATFORM -> CLAUDE_BOTTLE_BACKEND - dict _PLATFORMS -> _BACKENDS "Backend" is shorter and more established as the term for a pluggable strategy-pattern implementation. "Platform" was vague (could mean OS, hardware, cloud) and mildly redundant — Docker is itself a platform. The previous PRD section claiming "the Backend protocol was rejected" referred to a low-level run/exec/cp/network_connect protocol; the name was never the reason. The PRD is updated to describe that rejected design by shape rather than by name. The bottle/agent concepts and the manifest schema are unchanged.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"""Per-platform bottle factories.
|
"""Per-backend bottle factories.
|
||||||
|
|
||||||
A bottle is a running, isolated environment with claude inside. Each
|
A bottle is a running, isolated environment with claude inside. Each
|
||||||
platform exposes four methods:
|
backend exposes five methods:
|
||||||
|
|
||||||
prepare(spec, stage_dir=...) -> BottlePlan
|
prepare(spec, stage_dir=...) -> BottlePlan
|
||||||
Resolves names, validates host-side prerequisites, and writes
|
Resolves names, validates host-side prerequisites, and writes
|
||||||
@@ -19,8 +19,11 @@ platform exposes four methods:
|
|||||||
cleanup(plan) -> None
|
cleanup(plan) -> None
|
||||||
Actually removes everything described by the cleanup plan.
|
Actually removes everything described by the cleanup plan.
|
||||||
|
|
||||||
Selection is driven by CLAUDE_BOTTLE_PLATFORM (default "docker"). Per
|
list_active() -> None
|
||||||
PRD 0003 the manifest does not carry a platform field; the host
|
Print every currently-running bottle on this backend to stderr.
|
||||||
|
|
||||||
|
Selection is driven by CLAUDE_BOTTLE_BACKEND (default "docker"). Per
|
||||||
|
PRD 0003 the manifest does not carry a backend field; the host
|
||||||
environment picks.
|
environment picks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -38,8 +41,8 @@ from ..manifest import Manifest
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BottleSpec:
|
class BottleSpec:
|
||||||
"""CLI-supplied intent. Platform-agnostic — each platform's prepare
|
"""CLI-supplied intent. Backend-agnostic — each backend's prepare
|
||||||
step consumes it and produces its own platform-specific plan.
|
step consumes it and produces its own backend-specific plan.
|
||||||
Resolved values (image names, container name, scratch paths, runsc
|
Resolved values (image names, container name, scratch paths, runsc
|
||||||
availability) live on the plan, not the spec."""
|
availability) live on the plan, not the spec."""
|
||||||
|
|
||||||
@@ -52,8 +55,8 @@ class BottleSpec:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BottlePlan(ABC):
|
class BottlePlan(ABC):
|
||||||
"""Base output of a platform's prepare step. Concrete subclasses
|
"""Base output of a backend's prepare step. Concrete subclasses
|
||||||
(e.g. DockerBottlePlan) add platform-specific resolved fields and
|
(e.g. DockerBottlePlan) add backend-specific resolved fields and
|
||||||
implement `print`."""
|
implement `print`."""
|
||||||
|
|
||||||
spec: BottleSpec
|
spec: BottleSpec
|
||||||
@@ -66,8 +69,8 @@ class BottlePlan(ABC):
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class BottleCleanupPlan(ABC):
|
class BottleCleanupPlan(ABC):
|
||||||
"""Base output of a platform's prepare_cleanup step. Concrete
|
"""Base output of a backend's prepare_cleanup step. Concrete
|
||||||
subclasses (e.g. DockerBottleCleanupPlan) carry platform-specific
|
subclasses (e.g. DockerBottleCleanupPlan) carry backend-specific
|
||||||
lists of resources to be removed and implement `print` + `empty`."""
|
lists of resources to be removed and implement `print` + `empty`."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -82,7 +85,7 @@ class BottleCleanupPlan(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class Bottle(ABC):
|
class Bottle(ABC):
|
||||||
"""Handle to a running bottle. Yielded by a platform's launch step.
|
"""Handle to a running bottle. Yielded by a backend's launch step.
|
||||||
|
|
||||||
`exec_claude` runs `claude` inside the bottle and blocks until the
|
`exec_claude` runs `claude` inside the bottle and blocks until the
|
||||||
session ends. `cp_in` copies a host path into the bottle. `close`
|
session ends. `cp_in` copies a host path into the bottle. `close`
|
||||||
@@ -101,9 +104,9 @@ class Bottle(ABC):
|
|||||||
def close(self) -> None: ...
|
def close(self) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
class BottlePlatform(ABC):
|
class BottleBackend(ABC):
|
||||||
"""Abstract base for selectable bottle platforms. Concrete subclasses
|
"""Abstract base for selectable bottle backends. Concrete subclasses
|
||||||
(e.g. DockerBottlePlatform) own their own prepare/launch impls.
|
(e.g. DockerBottleBackend) own their own prepare/launch impls.
|
||||||
Symmetric with the BottlePlan → DockerBottlePlan hierarchy."""
|
Symmetric with the BottlePlan → DockerBottlePlan hierarchy."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
@@ -128,37 +131,37 @@ class BottlePlatform(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def list_active(self) -> None:
|
def list_active(self) -> None:
|
||||||
"""Print every currently-running bottle on this platform to
|
"""Print every currently-running bottle on this backend to
|
||||||
stderr (name + status)."""
|
stderr (name + status)."""
|
||||||
|
|
||||||
|
|
||||||
# Import concrete platform classes AFTER the base types are defined, so
|
# Import concrete backend classes AFTER the base types are defined, so
|
||||||
# each platform module can pull BottleSpec / BottlePlan / BottlePlatform
|
# each backend module can pull BottleSpec / BottlePlan / BottleBackend
|
||||||
# via `from . import ...` without hitting a partially-initialized module.
|
# via `from . import ...` without hitting a partially-initialized module.
|
||||||
from .docker import DockerBottlePlatform # noqa: E402
|
from .docker import DockerBottleBackend # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
_PLATFORMS: dict[str, BottlePlatform] = {
|
_BACKENDS: dict[str, BottleBackend] = {
|
||||||
"docker": DockerBottlePlatform(),
|
"docker": DockerBottleBackend(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_bottle_platform() -> BottlePlatform:
|
def get_bottle_backend() -> BottleBackend:
|
||||||
"""Resolve the bottle platform for the active environment. Dies with
|
"""Resolve the bottle backend for the active environment. Dies with
|
||||||
a pointer at the known platforms if CLAUDE_BOTTLE_PLATFORM names an
|
a pointer at the known backends if CLAUDE_BOTTLE_BACKEND names an
|
||||||
unimplemented one."""
|
unimplemented one."""
|
||||||
name = os.environ.get("CLAUDE_BOTTLE_PLATFORM", "docker")
|
name = os.environ.get("CLAUDE_BOTTLE_BACKEND", "docker")
|
||||||
if name not in _PLATFORMS:
|
if name not in _BACKENDS:
|
||||||
known = ", ".join(sorted(_PLATFORMS))
|
known = ", ".join(sorted(_BACKENDS))
|
||||||
die(f"unknown CLAUDE_BOTTLE_PLATFORM={name!r}; known platforms: {known}")
|
die(f"unknown CLAUDE_BOTTLE_BACKEND={name!r}; known backends: {known}")
|
||||||
return _PLATFORMS[name]
|
return _BACKENDS[name]
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Bottle",
|
"Bottle",
|
||||||
|
"BottleBackend",
|
||||||
"BottleCleanupPlan",
|
"BottleCleanupPlan",
|
||||||
"BottlePlan",
|
"BottlePlan",
|
||||||
"BottlePlatform",
|
|
||||||
"BottleSpec",
|
"BottleSpec",
|
||||||
"get_bottle_platform",
|
"get_bottle_backend",
|
||||||
]
|
]
|
||||||
+6
-5
@@ -1,28 +1,29 @@
|
|||||||
"""Docker bottle platform.
|
"""Docker bottle backend.
|
||||||
|
|
||||||
The bulk of the implementation lives in sibling modules:
|
The bulk of the implementation lives in sibling modules:
|
||||||
|
|
||||||
- util: thin Docker subprocess wrappers
|
- util: thin Docker subprocess wrappers
|
||||||
|
- network: Docker network plumbing
|
||||||
- bottle_plan: DockerBottlePlan
|
- bottle_plan: DockerBottlePlan
|
||||||
- bottle_cleanup_plan: DockerBottleCleanupPlan
|
- bottle_cleanup_plan: DockerBottleCleanupPlan
|
||||||
- bottle: DockerBottle handle
|
- bottle: DockerBottle handle
|
||||||
- platform: DockerBottlePlatform
|
- backend: DockerBottleBackend
|
||||||
|
|
||||||
This file only re-exports the public names so
|
This file only re-exports the public names so
|
||||||
`from claude_bottle.platform.docker import DockerBottlePlatform` keeps
|
`from claude_bottle.backend.docker import DockerBottleBackend` keeps
|
||||||
working.
|
working.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .backend import DockerBottleBackend
|
||||||
from .bottle import DockerBottle
|
from .bottle import DockerBottle
|
||||||
from .bottle_cleanup_plan import DockerBottleCleanupPlan
|
from .bottle_cleanup_plan import DockerBottleCleanupPlan
|
||||||
from .bottle_plan import DockerBottlePlan
|
from .bottle_plan import DockerBottlePlan
|
||||||
from .platform import DockerBottlePlatform
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DockerBottle",
|
"DockerBottle",
|
||||||
|
"DockerBottleBackend",
|
||||||
"DockerBottleCleanupPlan",
|
"DockerBottleCleanupPlan",
|
||||||
"DockerBottlePlan",
|
"DockerBottlePlan",
|
||||||
"DockerBottlePlatform",
|
|
||||||
]
|
]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""DockerBottlePlatform — the Docker implementation of BottlePlatform.
|
"""DockerBottleBackend — the Docker implementation of BottleBackend.
|
||||||
|
|
||||||
Methods:
|
Methods:
|
||||||
.prepare(spec, stage_dir=...) -> DockerBottlePlan
|
.prepare(spec, stage_dir=...) -> DockerBottlePlan
|
||||||
@@ -22,7 +22,7 @@ from ... import skills as skills_mod
|
|||||||
from ... import ssh as ssh_mod
|
from ... import ssh as ssh_mod
|
||||||
from ...env_resolve import env_resolve
|
from ...env_resolve import env_resolve
|
||||||
from ...log import die, info
|
from ...log import die, info
|
||||||
from .. import BottleCleanupPlan, BottlePlan, BottlePlatform, BottleSpec
|
from .. import BottleBackend, BottleCleanupPlan, BottlePlan, BottleSpec
|
||||||
from . import network as network_mod
|
from . import network as network_mod
|
||||||
from . import util as docker_mod
|
from . import util as docker_mod
|
||||||
from .bottle import DockerBottle
|
from .bottle import DockerBottle
|
||||||
@@ -34,8 +34,8 @@ from .bottle_plan import DockerBottlePlan
|
|||||||
_REPO_DIR = str(Path(__file__).resolve().parent.parent.parent.parent)
|
_REPO_DIR = str(Path(__file__).resolve().parent.parent.parent.parent)
|
||||||
|
|
||||||
|
|
||||||
class DockerBottlePlatform(BottlePlatform):
|
class DockerBottleBackend(BottleBackend):
|
||||||
"""Docker platform implementation. Selected by CLAUDE_BOTTLE_PLATFORM
|
"""Docker backend implementation. Selected by CLAUDE_BOTTLE_BACKEND
|
||||||
(default)."""
|
(default)."""
|
||||||
|
|
||||||
name = "docker"
|
name = "docker"
|
||||||
@@ -131,7 +131,7 @@ class DockerBottlePlatform(BottlePlatform):
|
|||||||
def launch(self, plan: BottlePlan) -> Iterator[DockerBottle]:
|
def launch(self, plan: BottlePlan) -> Iterator[DockerBottle]:
|
||||||
"""Build, launch, and provision a Docker bottle. Teardown on exit."""
|
"""Build, launch, and provision a Docker bottle. Teardown on exit."""
|
||||||
assert isinstance(plan, DockerBottlePlan), (
|
assert isinstance(plan, DockerBottlePlan), (
|
||||||
f"DockerBottlePlatform.launch expects DockerBottlePlan, "
|
f"DockerBottleBackend.launch expects DockerBottlePlan, "
|
||||||
f"got {type(plan).__name__}"
|
f"got {type(plan).__name__}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -358,7 +358,7 @@ class DockerBottlePlatform(BottlePlatform):
|
|||||||
Containers first; networks would refuse to delete while
|
Containers first; networks would refuse to delete while
|
||||||
containers are still attached."""
|
containers are still attached."""
|
||||||
assert isinstance(plan, DockerBottleCleanupPlan), (
|
assert isinstance(plan, DockerBottleCleanupPlan), (
|
||||||
f"DockerBottlePlatform.cleanup expects DockerBottleCleanupPlan, "
|
f"DockerBottleBackend.cleanup expects DockerBottleCleanupPlan, "
|
||||||
f"got {type(plan).__name__}"
|
f"got {type(plan).__name__}"
|
||||||
)
|
)
|
||||||
for name in plan.containers:
|
for name in plan.containers:
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""DockerBottle — concrete Bottle handle yielded by
|
"""DockerBottle — concrete Bottle handle yielded by
|
||||||
DockerBottlePlatform.launch.
|
DockerBottleBackend.launch.
|
||||||
|
|
||||||
Holds the container name plus the in-container prompt path so
|
Holds the container name plus the in-container prompt path so
|
||||||
exec_claude can transparently add --append-system-prompt-file when a
|
exec_claude can transparently add --append-system-prompt-file when a
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
"""DockerBottleCleanupPlan — concrete subclass of BottleCleanupPlan.
|
"""DockerBottleCleanupPlan — concrete subclass of BottleCleanupPlan.
|
||||||
|
|
||||||
Holds the tuples of container and network names that
|
Holds the tuples of container and network names that
|
||||||
DockerBottlePlatform.cleanup will remove. The y/N preflight reads
|
DockerBottleBackend.cleanup will remove. The y/N preflight reads
|
||||||
these via `print`; the CLI short-circuits via `empty`.
|
these via `print`; the CLI short-circuits via `empty`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ from .. import BottleCleanupPlan
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class DockerBottleCleanupPlan(BottleCleanupPlan):
|
class DockerBottleCleanupPlan(BottleCleanupPlan):
|
||||||
"""Resources DockerBottlePlatform.cleanup will remove. Produced by
|
"""Resources DockerBottleBackend.cleanup will remove. Produced by
|
||||||
`prepare_cleanup` from a snapshot of `docker ps -a` + `docker
|
`prepare_cleanup` from a snapshot of `docker ps -a` + `docker
|
||||||
network ls`; sorted so the y/N output is stable."""
|
network ls`; sorted so the y/N output is stable."""
|
||||||
|
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
"""DockerBottlePlan — concrete subclass of BottlePlan.
|
"""DockerBottlePlan — concrete subclass of BottlePlan.
|
||||||
|
|
||||||
Carries the Docker-specific resolved fields produced by
|
Carries the Docker-specific resolved fields produced by
|
||||||
DockerBottlePlatform.prepare. The launch step consumes it without
|
DockerBottleBackend.prepare. The launch step consumes it without
|
||||||
further resolution; show_plan-style rendering is the `print` method.
|
further resolution; show_plan-style rendering is the `print` method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ from .. import BottlePlan
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class DockerBottlePlan(BottlePlan):
|
class DockerBottlePlan(BottlePlan):
|
||||||
"""Docker-specific resolved fields produced by
|
"""Docker-specific resolved fields produced by
|
||||||
DockerBottlePlatform.prepare. Inherits `spec` and `stage_dir` from
|
DockerBottleBackend.prepare. Inherits `spec` and `stage_dir` from
|
||||||
BottlePlan."""
|
BottlePlan."""
|
||||||
|
|
||||||
slug: str
|
slug: str
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Docker host-side primitives used by DockerBottlePlatform: probing
|
"""Docker host-side primitives used by DockerBottleBackend: probing
|
||||||
for docker on PATH, slugifying agent names, checking image/container
|
for docker on PATH, slugifying agent names, checking image/container
|
||||||
existence, and building images."""
|
existence, and building images."""
|
||||||
|
|
||||||
@@ -5,14 +5,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ..platform import get_bottle_platform
|
from ..backend import get_bottle_backend
|
||||||
from ..log import info
|
from ..log import info
|
||||||
from ._common import read_tty_line
|
from ._common import read_tty_line
|
||||||
|
|
||||||
|
|
||||||
def cmd_cleanup(_argv: list[str]) -> int:
|
def cmd_cleanup(_argv: list[str]) -> int:
|
||||||
platform = get_bottle_platform()
|
backend = get_bottle_backend()
|
||||||
plan = platform.prepare_cleanup()
|
plan = backend.prepare_cleanup()
|
||||||
|
|
||||||
if plan.empty:
|
if plan.empty:
|
||||||
info("no claude-bottle resources to clean up")
|
info("no claude-bottle resources to clean up")
|
||||||
@@ -26,6 +26,6 @@ def cmd_cleanup(_argv: list[str]) -> int:
|
|||||||
info("aborted")
|
info("aborted")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
platform.cleanup(plan)
|
backend.cleanup(plan)
|
||||||
info("done")
|
info("done")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from ..platform import get_bottle_platform
|
from ..backend import get_bottle_backend
|
||||||
from ..manifest import Manifest
|
from ..manifest import Manifest
|
||||||
from ._common import PROG, USER_CWD
|
from ._common import PROG, USER_CWD
|
||||||
|
|
||||||
@@ -20,5 +20,5 @@ def cmd_list(argv: list[str]) -> int:
|
|||||||
print(name)
|
print(name)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
get_bottle_platform().list_active()
|
get_bottle_backend().list_active()
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..platform import BottleSpec, get_bottle_platform
|
from ..backend import BottleSpec, get_bottle_backend
|
||||||
from ..log import info
|
from ..log import info
|
||||||
from ..manifest import Manifest
|
from ..manifest import Manifest
|
||||||
from ._common import PROG, USER_CWD, read_tty_line
|
from ._common import PROG, USER_CWD, read_tty_line
|
||||||
@@ -38,8 +38,8 @@ def cmd_start(argv: list[str]) -> int:
|
|||||||
|
|
||||||
stage_dir = Path(tempfile.mkdtemp(prefix="claude-bottle-stage."))
|
stage_dir = Path(tempfile.mkdtemp(prefix="claude-bottle-stage."))
|
||||||
try:
|
try:
|
||||||
platform = get_bottle_platform()
|
backend = get_bottle_backend()
|
||||||
plan = platform.prepare(spec, stage_dir=stage_dir)
|
plan = backend.prepare(spec, stage_dir=stage_dir)
|
||||||
plan.print(remote_control=args.remote_control)
|
plan.print(remote_control=args.remote_control)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
@@ -53,7 +53,7 @@ def cmd_start(argv: list[str]) -> int:
|
|||||||
info("aborted by user")
|
info("aborted by user")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
with platform.launch(plan) as bottle:
|
with backend.launch(plan) as bottle:
|
||||||
info(
|
info(
|
||||||
"attaching interactive claude session "
|
"attaching interactive claude session "
|
||||||
"(Ctrl-D or 'exit' to leave; container will be removed)"
|
"(Ctrl-D or 'exit' to leave; container will be removed)"
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Introduce a per-platform factory function that owns the end-to-end
|
Introduce a per-backend factory function that owns the end-to-end
|
||||||
lifecycle of a "bottle" (a running, isolated environment with claude
|
lifecycle of a "bottle" (a running, isolated environment with claude
|
||||||
inside). The first and only implementation lands as
|
inside). The first and only implementation lands as
|
||||||
`create_docker_bottle`. No second platform ships in this PRD.
|
`create_docker_bottle`. No second backend ships in this PRD.
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
@@ -33,11 +33,11 @@ Today, "how to launch a bottle" is spread across roughly six modules
|
|||||||
and exists only because the current code can't decide on its own.
|
and exists only because the current code can't decide on its own.
|
||||||
|
|
||||||
The shape that fits the project's actual goals (isolated agent runs
|
The shape that fits the project's actual goals (isolated agent runs
|
||||||
across multiple platforms) is "one factory per platform," not "one
|
across multiple backends) is "one factory per backend," not "one
|
||||||
container-runtime SDK with N drivers." A previous draft of this PRD
|
container-runtime SDK with N drivers." A previous draft of this PRD
|
||||||
considered a low-level `Backend` protocol (`run`, `exec`, `cp`,
|
considered a low-level runtime-primitive protocol (`run`, `exec`,
|
||||||
`network_connect`, ...) and rejected it as the wrong layer — it would
|
`cp`, `network_connect`, ...) and rejected it as the wrong layer —
|
||||||
have forced fly.io to pretend it's Docker.
|
it would have forced fly.io to pretend it's Docker.
|
||||||
|
|
||||||
## Goals / Success Criteria
|
## Goals / Success Criteria
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ The feature works when all of the following are observable:
|
|||||||
|
|
||||||
The feature is **done** when all of the following ship:
|
The feature is **done** when all of the following ship:
|
||||||
|
|
||||||
- A new `claude_bottle/bottles/` package exists with
|
- A new `claude_bottle/backend/` package exists with
|
||||||
`__init__.py` (factory selection) and `docker.py`
|
`__init__.py` (factory selection) and `docker.py`
|
||||||
(`create_docker_bottle`).
|
(`create_docker_bottle`).
|
||||||
- `create_docker_bottle` returns a context manager yielding a `Bottle`
|
- `create_docker_bottle` returns a context manager yielding a `Bottle`
|
||||||
@@ -65,23 +65,23 @@ The feature is **done** when all of the following ship:
|
|||||||
and teardown on context exit.
|
and teardown on context exit.
|
||||||
- Every existing `subprocess.run(["docker", ...])` call in
|
- Every existing `subprocess.run(["docker", ...])` call in
|
||||||
`cli/start.py`, `pipelock.py`, `network.py`, `ssh.py`, and
|
`cli/start.py`, `pipelock.py`, `network.py`, `ssh.py`, and
|
||||||
`skills.py` either moves into `bottles/docker.py` or is called from
|
`skills.py` either moves into `backend/docker.py` or is called from
|
||||||
it. No top-level CLI code references `docker` directly.
|
it. No top-level CLI code references `docker` directly.
|
||||||
- `bottles[].runtime` is removed from the manifest schema, the
|
- `bottles[].runtime` is removed from the manifest schema, the
|
||||||
dataclass in `manifest.py`, the example manifest, and any README /
|
dataclass in `manifest.py`, the example manifest, and any README /
|
||||||
docs references. `require_runsc()` in `claude_bottle/docker.py` is
|
docs references. `require_runsc()` in `claude_bottle/docker.py` is
|
||||||
deleted.
|
deleted.
|
||||||
- A single env var, `CLAUDE_BOTTLE_PLATFORM` (default `"docker"`),
|
- A single env var, `CLAUDE_BOTTLE_BACKEND` (default `"docker"`),
|
||||||
selects the factory. Unknown values die at startup with a list of
|
selects the factory. Unknown values die at startup with a list of
|
||||||
known platforms.
|
known backends.
|
||||||
- The y/N preflight in `cli.py` includes the resolved Docker runtime
|
- The y/N preflight in `cli.py` includes the resolved Docker runtime
|
||||||
alongside the allowlist summary.
|
alongside the allowlist summary.
|
||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
- No second platform implementation. `create_container_bottle` and
|
- No second backend implementation. `create_container_bottle` and
|
||||||
`create_flyio_bottle` are not in this PRD. The factory dict in
|
`create_flyio_bottle` are not in this PRD. The factory dict in
|
||||||
`bottles/__init__.py` ships with one entry.
|
`backend/__init__.py` ships with one entry.
|
||||||
- No retries, async, or streaming exec. The current code is
|
- No retries, async, or streaming exec. The current code is
|
||||||
synchronous `subprocess.run`; the `Bottle` handle matches.
|
synchronous `subprocess.run`; the `Bottle` handle matches.
|
||||||
- No behavior change beyond the runsc auto-detect. Pipelock topology,
|
- No behavior change beyond the runsc auto-detect. Pipelock topology,
|
||||||
@@ -89,14 +89,14 @@ The feature is **done** when all of the following ship:
|
|||||||
provisioning all stay byte-identical.
|
provisioning all stay byte-identical.
|
||||||
- No `--require-runsc` CLI escape hatch. If a user later wants "fail
|
- No `--require-runsc` CLI escape hatch. If a user later wants "fail
|
||||||
rather than silently downgrade," that's a follow-up.
|
rather than silently downgrade," that's a follow-up.
|
||||||
- No `bottles[].platform` manifest field. Platform is a property of
|
- No `bottles[].backend` manifest field. Backend is a property of
|
||||||
the host environment, not the bottle definition (at least for now).
|
the host environment, not the bottle definition (at least for now).
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
### In scope
|
### In scope
|
||||||
|
|
||||||
- New `claude_bottle/bottles/` package containing `__init__.py` and
|
- New `claude_bottle/backend/` package containing `__init__.py` and
|
||||||
`docker.py`.
|
`docker.py`.
|
||||||
- The `Bottle` Protocol definition and `create_docker_bottle` factory.
|
- The `Bottle` Protocol definition and `create_docker_bottle` factory.
|
||||||
- Moving Docker-specific subprocess calls into the factory.
|
- Moving Docker-specific subprocess calls into the factory.
|
||||||
@@ -113,9 +113,9 @@ The feature is **done** when all of the following ship:
|
|||||||
|
|
||||||
- Apple `container` and fly.io factories (separate PRDs, deferred
|
- Apple `container` and fly.io factories (separate PRDs, deferred
|
||||||
until the Docker factory is the only thing shipping).
|
until the Docker factory is the only thing shipping).
|
||||||
- Generalizing the pipelock sidecar to other platforms. Pipelock
|
- Generalizing the pipelock sidecar to other backends. Pipelock
|
||||||
topology is, after this PRD, an implementation detail private to
|
topology is, after this PRD, an implementation detail private to
|
||||||
`bottles/docker.py`.
|
`backend/docker.py`.
|
||||||
- Rewriting `pipelock.py`'s YAML generation. The allowlist→YAML
|
- Rewriting `pipelock.py`'s YAML generation. The allowlist→YAML
|
||||||
translation stays where it is and is called by the Docker factory.
|
translation stays where it is and is called by the Docker factory.
|
||||||
- Changes to `env_resolve.py`, `manifest.py` (beyond the `runtime`
|
- Changes to `env_resolve.py`, `manifest.py` (beyond the `runtime`
|
||||||
@@ -126,13 +126,13 @@ The feature is **done** when all of the following ship:
|
|||||||
|
|
||||||
### New services / components
|
### New services / components
|
||||||
|
|
||||||
A new package, `claude_bottle/bottles/`:
|
A new package, `claude_bottle/backend/`:
|
||||||
|
|
||||||
- **`claude_bottle/bottles/__init__.py`** — Defines the `Bottle`
|
- **`claude_bottle/backend/__init__.py`** — Defines the `Bottle`
|
||||||
Protocol and `get_bottle_factory()`. The factory registry is a
|
Protocol and `get_bottle_factory()`. The factory registry is a
|
||||||
module-level dict mapping platform name → factory function.
|
module-level dict mapping backend name → factory function.
|
||||||
Selection reads `CLAUDE_BOTTLE_PLATFORM` (default `"docker"`).
|
Selection reads `CLAUDE_BOTTLE_BACKEND` (default `"docker"`).
|
||||||
Unknown values call `die()` with the list of known platforms.
|
Unknown values call `die()` with the list of known backends.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class Bottle(Protocol):
|
class Bottle(Protocol):
|
||||||
@@ -145,7 +145,7 @@ A new package, `claude_bottle/bottles/`:
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
- **`claude_bottle/bottles/docker.py`** — `create_docker_bottle(...)`,
|
- **`claude_bottle/backend/docker.py`** — `create_docker_bottle(...)`,
|
||||||
the only factory implementation in this PRD. Owns:
|
the only factory implementation in this PRD. Owns:
|
||||||
- probing for `runsc` availability (`docker info --format
|
- probing for `runsc` availability (`docker info --format
|
||||||
'{{json .Runtimes}}'`),
|
'{{json .Runtimes}}'`),
|
||||||
@@ -178,19 +178,19 @@ A new package, `claude_bottle/bottles/`:
|
|||||||
consumes.
|
consumes.
|
||||||
- **`claude_bottle/pipelock.py`** — keep all the allowlist resolution
|
- **`claude_bottle/pipelock.py`** — keep all the allowlist resolution
|
||||||
and YAML generation. Remove `pipelock_start` / `pipelock_stop` (or
|
and YAML generation. Remove `pipelock_start` / `pipelock_stop` (or
|
||||||
inline them into `bottles/docker.py` — decide during
|
inline them into `backend/docker.py` — decide during
|
||||||
implementation). Pipelock-the-sidecar becomes a Docker-factory
|
implementation). Pipelock-the-sidecar becomes a Docker-factory
|
||||||
internal concept.
|
internal concept.
|
||||||
- **`claude_bottle/network.py`** — same call-sites moved into
|
- **`claude_bottle/network.py`** — same call-sites moved into
|
||||||
`bottles/docker.py`. The module either becomes a thin set of pure
|
`backend/docker.py`. The module either becomes a thin set of pure
|
||||||
name-derivation helpers (`network_name_for_slug`, etc.) or folds
|
name-derivation helpers (`network_name_for_slug`, etc.) or folds
|
||||||
entirely into `bottles/docker.py`. Decide during implementation.
|
entirely into `backend/docker.py`. Decide during implementation.
|
||||||
- **`claude_bottle/ssh.py`** and **`claude_bottle/skills.py`** — the
|
- **`claude_bottle/ssh.py`** and **`claude_bottle/skills.py`** — the
|
||||||
`docker cp` and `docker exec` calls move into / are called from
|
`docker cp` and `docker exec` calls move into / are called from
|
||||||
`bottles/docker.py`. The host-side file-tree generation stays put.
|
`backend/docker.py`. The host-side file-tree generation stays put.
|
||||||
- **`claude-bottle.example.json`** — remove the `runtime` field from
|
- **`claude-bottle.example.json`** — remove the `runtime` field from
|
||||||
any example bottle.
|
any example bottle.
|
||||||
- **`README.md`** — note `CLAUDE_BOTTLE_PLATFORM` and the runsc
|
- **`README.md`** — note `CLAUDE_BOTTLE_BACKEND` and the runsc
|
||||||
auto-detect; remove any mention of `runtime: "runsc"` as a manifest
|
auto-detect; remove any mention of `runtime: "runsc"` as a manifest
|
||||||
field.
|
field.
|
||||||
|
|
||||||
@@ -241,9 +241,9 @@ they're about to run under before approving.
|
|||||||
|
|
||||||
- **Where the pipelock sidecar lifecycle lives.** Two reasonable
|
- **Where the pipelock sidecar lifecycle lives.** Two reasonable
|
||||||
splits: (a) `pipelock.py` keeps `pipelock_start` / `pipelock_stop`
|
splits: (a) `pipelock.py` keeps `pipelock_start` / `pipelock_stop`
|
||||||
and `bottles/docker.py` calls them; (b) the sidecar
|
and `backend/docker.py` calls them; (b) the sidecar
|
||||||
`docker create/cp/network connect/start` sequence moves entirely
|
`docker create/cp/network connect/start` sequence moves entirely
|
||||||
into `bottles/docker.py` and `pipelock.py` shrinks to the YAML +
|
into `backend/docker.py` and `pipelock.py` shrinks to the YAML +
|
||||||
allowlist helpers. (a) keeps git blame intact and is the smaller
|
allowlist helpers. (a) keeps git blame intact and is the smaller
|
||||||
diff; (b) makes pipelock-as-an-implementation-detail more obvious.
|
diff; (b) makes pipelock-as-an-implementation-detail more obvious.
|
||||||
Decide during implementation.
|
Decide during implementation.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from claude_bottle.platform.docker.network import (
|
from claude_bottle.backend.docker.network import (
|
||||||
network_create_egress,
|
network_create_egress,
|
||||||
network_create_internal,
|
network_create_internal,
|
||||||
network_remove,
|
network_remove,
|
||||||
|
|||||||
Reference in New Issue
Block a user