CLI and dashboard now share one cross-backend abstraction for
listing + launching bottles, so adding a backend (docker /
smolmachines) lights up in both places without separate wiring.
Backend abstraction:
- New `ActiveBottle` dataclass (`backend_name`, `slug`,
`agent_name`, `started_at`, `services`) replaces the
docker-specific `ActiveAgent`. Same field surface for the
existing dashboard consumers; `ActiveAgent` becomes a typed
alias for source-compat.
- New `BottleBackend.enumerate_active() -> Sequence[ActiveBottle]`
replaces the old `list_active() -> None` (which printed and
returned nothing). Docker implements it via the existing
compose query; smolmachines implements it via `smolvm machine
ls --json` cross-referenced with each bundle container's
`CLAUDE_BOTTLE_SIDECAR_DAEMONS` env (`backend/smolmachines/
enumerate.py`).
- New `enumerate_active_bottles()` and `known_backend_names()`
module-level helpers fold every backend into one call.
- `get_bottle_backend(name=None)` takes an optional explicit
name (precedence: arg > $CLAUDE_BOTTLE_BACKEND > "docker").
CLI:
- `./cli.py list active` enumerates every backend, prints
tab-separated `<backend>\t<slug>\t<agent>\t<services>`. The
smolmachines bottle the user was looking for now shows up.
- `./cli.py start` grows `--backend=<docker|smolmachines>`
(choices pulled live from `known_backend_names()`). Threaded
through `prepare_with_preflight(backend_name=...)` so the
resume path picks up the flag too.
Dashboard:
- Active agents pane lists both backends (the row formatter now
prefixes `[docker]` / `[smolmachines]`).
- New-agent flow inserts a backend picker modal between agent
pick and preflight (`_backend_picker_modal`). Short-circuits
when only one backend is registered.
- `discover_active_agents()` collapses to
`enumerate_active_bottles()`; `_parse_services_by_project` and
`_query_services_by_project` move to
`backend/docker/cleanup.py` where the docker enumerator owns
them.
Tests: parser + enumerate-active tests relocated to
`test_docker_enumerate_active.py`. New
`test_backend_selection.py` covers `get_bottle_backend`,
`known_backend_names`, `enumerate_active_bottles`. New
`test_cli_start_backend_flag.py` covers `--backend`'s argparse
shape + the explicit-over-env precedence.
605 unit tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
'bottles' was the package name when it held a single Bottle Protocol;
since we added BottlePlatform / BottlePlan / BottleCleanupPlan and
made it the home of platform dispatch, 'platform' describes the
package better. The 'bottle' concept (and the manifest field) stays.
CLI imports update from ..bottles to ..platform; internal relative
imports inside the package survive the rename unchanged. Git
detected all 7 file renames.
Add list_active() abstract method on BottlePlatform; DockerBottlePlatform
implements it with the existing docker ps logic. cli/list.py no longer
calls docker directly — it just dispatches the active branch to the
platform.
Replace the TypedDict + 14 manifest_* free functions with frozen
dataclasses (SshEntry, BottleEgress, Bottle, Agent, Manifest) carrying
their own validators and constructors. Call sites import Manifest and
chain attribute access; the manifest_* helpers and manifest_validate
are gone.
Behavior changes worth flagging:
- Agent.bottle is now required (was optional with a "(none)" fallback).
Manifest.from_json_obj dies if any agent lacks a 'bottle' field or
references an undefined bottle, where previously start.py raised the
error lazily for the specific agent being launched.
- ssh.py now takes SshEntry instances; Host/IdentityFile shape checks
moved upstream into Manifest construction, leaving only the IdentityFile
filesystem-existence check in ssh_validate_entries.
- pipelock_bottle_allowlist's per-element string check is dropped — the
Manifest validator enforces it at load.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
One file per subcommand under claude_bottle/cli/, with shared constants
and the tty helper in _common.py and dispatch in __init__.py. The
public import (from claude_bottle.cli import main) is unchanged, so
the root cli.py entrypoint and the test suite see no surface change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>