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:
@@ -6,10 +6,10 @@
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
considered a low-level `Backend` protocol (`run`, `exec`, `cp`,
|
||||
`network_connect`, ...) and rejected it as the wrong layer — it would
|
||||
have forced fly.io to pretend it's Docker.
|
||||
considered a low-level runtime-primitive protocol (`run`, `exec`,
|
||||
`cp`, `network_connect`, ...) and rejected it as the wrong layer —
|
||||
it would have forced fly.io to pretend it's Docker.
|
||||
|
||||
## 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:
|
||||
|
||||
- A new `claude_bottle/bottles/` package exists with
|
||||
- A new `claude_bottle/backend/` package exists with
|
||||
`__init__.py` (factory selection) and `docker.py`
|
||||
(`create_docker_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.
|
||||
- Every existing `subprocess.run(["docker", ...])` call in
|
||||
`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.
|
||||
- `bottles[].runtime` is removed from the manifest schema, the
|
||||
dataclass in `manifest.py`, the example manifest, and any README /
|
||||
docs references. `require_runsc()` in `claude_bottle/docker.py` is
|
||||
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
|
||||
known platforms.
|
||||
known backends.
|
||||
- The y/N preflight in `cli.py` includes the resolved Docker runtime
|
||||
alongside the allowlist summary.
|
||||
|
||||
## 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
|
||||
`bottles/__init__.py` ships with one entry.
|
||||
`backend/__init__.py` ships with one entry.
|
||||
- No retries, async, or streaming exec. The current code is
|
||||
synchronous `subprocess.run`; the `Bottle` handle matches.
|
||||
- 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.
|
||||
- No `--require-runsc` CLI escape hatch. If a user later wants "fail
|
||||
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).
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- New `claude_bottle/bottles/` package containing `__init__.py` and
|
||||
- New `claude_bottle/backend/` package containing `__init__.py` and
|
||||
`docker.py`.
|
||||
- The `Bottle` Protocol definition and `create_docker_bottle` 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
|
||||
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
|
||||
`bottles/docker.py`.
|
||||
`backend/docker.py`.
|
||||
- Rewriting `pipelock.py`'s YAML generation. The allowlist→YAML
|
||||
translation stays where it is and is called by the Docker factory.
|
||||
- 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
|
||||
|
||||
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
|
||||
module-level dict mapping platform name → factory function.
|
||||
Selection reads `CLAUDE_BOTTLE_PLATFORM` (default `"docker"`).
|
||||
Unknown values call `die()` with the list of known platforms.
|
||||
module-level dict mapping backend name → factory function.
|
||||
Selection reads `CLAUDE_BOTTLE_BACKEND` (default `"docker"`).
|
||||
Unknown values call `die()` with the list of known backends.
|
||||
|
||||
```python
|
||||
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:
|
||||
- probing for `runsc` availability (`docker info --format
|
||||
'{{json .Runtimes}}'`),
|
||||
@@ -178,19 +178,19 @@ A new package, `claude_bottle/bottles/`:
|
||||
consumes.
|
||||
- **`claude_bottle/pipelock.py`** — keep all the allowlist resolution
|
||||
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
|
||||
internal concept.
|
||||
- **`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
|
||||
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
|
||||
`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
|
||||
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
|
||||
field.
|
||||
|
||||
@@ -241,9 +241,9 @@ they're about to run under before approving.
|
||||
|
||||
- **Where the pipelock sidecar lifecycle lives.** Two reasonable
|
||||
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
|
||||
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
|
||||
diff; (b) makes pipelock-as-an-implementation-detail more obvious.
|
||||
Decide during implementation.
|
||||
|
||||
Reference in New Issue
Block a user