docs: name bot-bottle, not ./cli.py, in every instruction
prd-number-check / require-numbered-prds (pull_request) Successful in 11s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / unit (pull_request) Successful in 59s
test / integration-docker (pull_request) Successful in 1m6s
test / coverage (push) Successful in 21s
test / image-input-builds (push) Successful in 44s
test / image-input-builds (pull_request) Successful in 1m15s
test / unit (push) Successful in 57s
test / coverage (pull_request) Successful in 20s
lint / lint (push) Successful in 1m3s
Update Quality Badges / update-badges (push) Successful in 1m12s
test / integration-docker (push) Successful in 1m0s
prd-number-check / require-numbered-prds (pull_request) Successful in 11s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / unit (pull_request) Successful in 59s
test / integration-docker (pull_request) Successful in 1m6s
test / coverage (push) Successful in 21s
test / image-input-builds (push) Successful in 44s
test / image-input-builds (pull_request) Successful in 1m15s
test / unit (push) Successful in 57s
test / coverage (pull_request) Successful in 20s
lint / lint (push) Successful in 1m3s
Update Quality Badges / update-badges (push) Successful in 1m12s
test / integration-docker (push) Successful in 1m0s
`doctor` told users to run `./cli.py backend setup`. cli.py is a four-line wrapper at the repo root that calls bot_bottle.cli:main, and it does not ship — [tool.setuptools.packages.find] includes only bot_bottle*, so anyone who installed rather than cloned has no such file. Confirmed against a real install: the user's tree contains exactly one executable, `bot-bottle`, and no cli.py anywhere, while doctor recommended `./cli.py` three times. Invisible in development, where ./cli.py works fine from a checkout, which is why only a clean-install test surfaced it. The two entry points are the same code, so the fix is to name the one that always exists. 141 replacements across 45 files: the runtime messages that caused this, plus README, docs, PRDs, research notes and test prose, so nothing teaches the invocation a user cannot run. scripts/demo.sh is deliberately untouched — it *executes* ./cli.py from a checkout, where that is the correct and available path. Verified end to end: a sandbox install now reports "Run: bot-bottle backend setup --backend=firecracker", and bot-bottle is on that user's PATH. Unit suite unchanged against the pre-existing baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WEfZZhakx13bxTfXcZCoS5
This commit was merged in pull request #529.
This commit is contained in:
@@ -15,7 +15,7 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Per-bottle egress allowlist** — TLS-bumped HTTP/HTTPS chokepoint with a per-manifest host allowlist; per-route path/method/header `matches` filtering; outbound DLP scanning for known tokens and secrets, inbound DLP scanning for prompt-injection attempts; DoH and arbitrary hosts blocked by default.
|
- **Per-bottle egress allowlist** — TLS-bumped HTTP/HTTPS chokepoint with a per-manifest host allowlist; per-route path/method/header `matches` filtering; outbound DLP scanning for known tokens and secrets, inbound DLP scanning for prompt-injection attempts; DoH and arbitrary hosts blocked by default.
|
||||||
- **Per-route token-match policy** — each egress route picks what happens when the outbound DLP catches a token via `dlp.outbound_on_match`: `supervise` (default) holds the request and surfaces it in `./cli.py supervise` for approval (an approved value is remembered for the life of the proxy); `redact` scrubs the value and forwards; `block` is a hard `403`. Cuts false-positive friction without weakening default-deny.
|
- **Per-route token-match policy** — each egress route picks what happens when the outbound DLP catches a token via `dlp.outbound_on_match`: `supervise` (default) holds the request and surfaces it in `bot-bottle supervise` for approval (an approved value is remembered for the life of the proxy); `redact` scrubs the value and forwards; `block` is a hard `403`. Cuts false-positive friction without weakening default-deny.
|
||||||
- **Tokens the agent never sees** — host secrets live in a gateway; the agent dials `http://gateway:9099/<path>` and the proxy strips inbound `Authorization` and injects the real token before forwarding. `printenv` in the agent shows proxy URLs only.
|
- **Tokens the agent never sees** — host secrets live in a gateway; the agent dials `http://gateway:9099/<path>` and the proxy strips inbound `Authorization` and injects the real token before forwarding. `printenv` in the agent shows proxy URLs only.
|
||||||
- **Gitleaks-scanned push (git-gate)** — `bottle.git` remotes route through a per-bottle `git daemon` that gitleaks-scans incoming refs pre-receive and forwards clean refs upstream over SSH. The agent never holds the upstream credential.
|
- **Gitleaks-scanned push (git-gate)** — `bottle.git` remotes route through a per-bottle `git daemon` that gitleaks-scans incoming refs pre-receive and forwards clean refs upstream over SSH. The agent never holds the upstream credential.
|
||||||
- **Manifest-scoped skills + secrets** — each bottle declares its skills, env, git identity, remotes, and egress routes; unknown keys die at load.
|
- **Manifest-scoped skills + secrets** — each bottle declares its skills, env, git identity, remotes, and egress routes; unknown keys die at load.
|
||||||
@@ -39,7 +39,7 @@ On the legacy Docker backend, the same logical bottle is two containers per agen
|
|||||||
The Docker topology looks like this:
|
The Docker topology looks like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
host ( ./cli.py )
|
host ( bot-bottle )
|
||||||
│
|
│
|
||||||
starts │ stops
|
starts │ stops
|
||||||
▼
|
▼
|
||||||
@@ -87,7 +87,7 @@ The installer is a bootstrapper: it finds a suitable Python, installs bot-bottle
|
|||||||
|
|
||||||
**A backend**, which the installer deliberately does *not* install for you — `doctor` reports what's missing afterwards. On compatible macOS hosts, the default backend requires Apple's `container` CLI and does not require Docker. The Firecracker backend (Linux) requires Docker on the host for the gateway plus the `firecracker` binary and KVM. The legacy Docker backend requires Docker. Claude bottles also need a long-lived Claude Code OAuth token (`claude setup-token`) exported as `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`.
|
**A backend**, which the installer deliberately does *not* install for you — `doctor` reports what's missing afterwards. On compatible macOS hosts, the default backend requires Apple's `container` CLI and does not require Docker. The Firecracker backend (Linux) requires Docker on the host for the gateway plus the `firecracker` binary and KVM. The legacy Docker backend requires Docker. Claude bottles also need a long-lived Claude Code OAuth token (`claude setup-token`) exported as `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`.
|
||||||
|
|
||||||
Use `BOT_BOTTLE_BACKEND=docker ./cli.py start <agent>` on hosts where neither Apple Container nor KVM is available and Docker is the desired backend.
|
Use `BOT_BOTTLE_BACKEND=docker bot-bottle start <agent>` on hosts where neither Apple Container nor KVM is available and Docker is the desired backend.
|
||||||
|
|
||||||
> **CI (macOS Apple Container):** the advisory `integration-macos` job in `.gitea/workflows/pre-release-test.yml` runs only on manual dispatch. It targets a self-hosted host-mode runner labelled `macos`; Apple Container cannot run inside the Linux pull-request runner. Provision an Apple Silicon host with the `container` CLI running and Python ≥ 3.11 plus `coverage` on the launchd service's explicit `PATH`. The infra container is a singleton (`bot-bottle-mac-infra`), so keep runner concurrency at 1. Its coverage is reported separately and never feeds the required pull-request gate.
|
> **CI (macOS Apple Container):** the advisory `integration-macos` job in `.gitea/workflows/pre-release-test.yml` runs only on manual dispatch. It targets a self-hosted host-mode runner labelled `macos`; Apple Container cannot run inside the Linux pull-request runner. Provision an Apple Silicon host with the `container` CLI running and Python ≥ 3.11 plus `coverage` on the launchd service's explicit `PATH`. The infra container is a singleton (`bot-bottle-mac-infra`), so keep runner concurrency at 1. Its coverage is reported separately and never feeds the required pull-request gate.
|
||||||
|
|
||||||
@@ -180,10 +180,10 @@ On Linux, a KVM-capable host defaults to the Firecracker backend. It needs:
|
|||||||
- **`/dev/kvm`** present and accessible. Load `kvm-intel` or `kvm-amd` (and enable virtualization in BIOS/firmware). The invoking user must be in the `kvm` group: `sudo usermod -aG kvm "$USER"` then re-login. bot-bottle preflights this and reports exactly what's missing.
|
- **`/dev/kvm`** present and accessible. Load `kvm-intel` or `kvm-amd` (and enable virtualization in BIOS/firmware). The invoking user must be in the `kvm` group: `sudo usermod -aG kvm "$USER"` then re-login. bot-bottle preflights this and reports exactly what's missing.
|
||||||
- **`firecracker`** on `PATH`: grab a release from <https://github.com/firecracker-microvm/firecracker/releases>. Start flows print this pointer when the binary is missing.
|
- **`firecracker`** on `PATH`: grab a release from <https://github.com/firecracker-microvm/firecracker/releases>. Start flows print this pointer when the binary is missing.
|
||||||
- **Docker** for the gateway and image build.
|
- **Docker** for the gateway and image build.
|
||||||
- **A one-time privileged network setup** — the per-bottle TAP pool plus the fail-closed `nftables` isolation table. Run `./cli.py backend setup --backend=firecracker` for the host-appropriate config (a NixOS module, a `sudo` script elsewhere); `./cli.py backend status --backend=firecracker` reports what's present, including whether the pool range collides with an existing route. The pool defaults to `10.243.0.0/16` (an obscure RFC-1918 block that dodges docker/libvirt/LAN and, deliberately, Tailscale's `100.64.0.0/10` CGNAT range); override with `BOT_BOTTLE_FC_IP_BASE` if it clashes on your host.
|
- **A one-time privileged network setup** — the per-bottle TAP pool plus the fail-closed `nftables` isolation table. Run `bot-bottle backend setup --backend=firecracker` for the host-appropriate config (a NixOS module, a `sudo` script elsewhere); `bot-bottle backend status --backend=firecracker` reports what's present, including whether the pool range collides with an existing route. The pool defaults to `10.243.0.0/16` (an obscure RFC-1918 block that dodges docker/libvirt/LAN and, deliberately, Tailscale's `100.64.0.0/10` CGNAT range); override with `BOT_BOTTLE_FC_IP_BASE` if it clashes on your host.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
BOT_BOTTLE_BACKEND=firecracker ./cli.py start <agent>
|
BOT_BOTTLE_BACKEND=firecracker bot-bottle start <agent>
|
||||||
```
|
```
|
||||||
|
|
||||||
> **NixOS:** enable `virtualisation.docker`, ensure the KVM module is loaded (`boot.kernelModules = [ "kvm-intel" ];` or `kvm-amd`), and add your user to the `kvm` and `docker` groups. For the network pool, consume the flake module — `imports = [ inputs.bot-bottle.nixosModules.firecracker-netpool ]; services.bot-bottle-firecracker = { enable = true; owner = "you"; };` — then `nixos-rebuild switch` (imperative nft/TAP rules don't survive a rebuild; channel users can `imports = [ <bot-bottle>/nix/firecracker-netpool.nix ]`). `firecracker` isn't in nixpkgs by default as a user binary — install the release binary (pin the version) and put it on `PATH`.
|
> **NixOS:** enable `virtualisation.docker`, ensure the KVM module is loaded (`boot.kernelModules = [ "kvm-intel" ];` or `kvm-amd`), and add your user to the `kvm` and `docker` groups. For the network pool, consume the flake module — `imports = [ inputs.bot-bottle.nixosModules.firecracker-netpool ]; services.bot-bottle-firecracker = { enable = true; owner = "you"; };` — then `nixos-rebuild switch` (imperative nft/TAP rules don't survive a rebuild; channel users can `imports = [ <bot-bottle>/nix/firecracker-netpool.nix ]`). `firecracker` isn't in nixpkgs by default as a user binary — install the release binary (pin the version) and put it on `PATH`.
|
||||||
@@ -191,7 +191,7 @@ BOT_BOTTLE_BACKEND=firecracker ./cli.py start <agent>
|
|||||||
> **CI:** Firecracker integration runs in the manually dispatched `.gitea/workflows/pre-release-test.yml` on a self-hosted runner labelled `kvm`; privileged KVM hosts never execute unreviewed PR code automatically. Provision it like a normal Firecracker host: `firecracker` on `PATH`, `/dev/kvm`, the cached guest kernel and static dropbear, and the persistent TAP/nft pool. The required pull-request workflow runs unit plus the complete Docker integration suite on `ubuntu-latest`; see `docs/ci.md`.
|
> **CI:** Firecracker integration runs in the manually dispatched `.gitea/workflows/pre-release-test.yml` on a self-hosted runner labelled `kvm`; privileged KVM hosts never execute unreviewed PR code automatically. Provision it like a normal Firecracker host: `firecracker` on `PATH`, `/dev/kvm`, the cached guest kernel and static dropbear, and the persistent TAP/nft pool. The required pull-request workflow runs unit plus the complete Docker integration suite on `ubuntu-latest`; see `docs/ci.md`.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./cli.py start <agent> # builds the image on first run, drops you into claude
|
bot-bottle start <agent> # builds the image on first run, drops you into claude
|
||||||
```
|
```
|
||||||
|
|
||||||
## Manifest
|
## Manifest
|
||||||
@@ -267,7 +267,7 @@ You help maintain Gitea-hosted projects.
|
|||||||
| `dlp.outbound_on_match` | no | What to do when an outbound token is detected: `supervise` (default for manifest routes — hold for operator approval), `redact` (scrub the value and forward), or `block` (hard 403). Agent-provider routes (e.g. `api.anthropic.com`) default to `redact`. |
|
| `dlp.outbound_on_match` | no | What to do when an outbound token is detected: `supervise` (default for manifest routes — hold for operator approval), `redact` (scrub the value and forward), or `block` (hard 403). Agent-provider routes (e.g. `api.anthropic.com`) default to `redact`. |
|
||||||
| `git.fetch` | no | `true` permits smart HTTP clone/fetch (`git-upload-pack`) for this host. Push (`git-receive-pack`) remains blocked. |
|
| `git.fetch` | no | `true` permits smart HTTP clone/fetch (`git-upload-pack`) for this host. Push (`git-receive-pack`) remains blocked. |
|
||||||
|
|
||||||
When an outbound DLP detector matches a token, the route's `dlp.outbound_on_match` policy decides what happens. Under the default `supervise`, the proxy queues an `egress-token-allow` proposal for the operator's `./cli.py supervise` TUI and holds the request open until it is answered (or `EGRESS_TOKEN_ALLOW_TIMEOUT_SECONDS`, default 300s, elapses — after which it fails closed). The operator never sees the raw token, only the host, method, path, and a redacted snippet; approving adds the value to an in-memory safelist for the life of the egress proxy. Under `redact`, the matched value is scrubbed from the body, headers, and path and the request is forwarded (failing closed if a match lands somewhere unredactable, like the hostname). Under `block` it stays a hard `403`. Structural blocks (CRLF injection) and not-in-allowlist host blocks are always hard `403`s regardless of policy.
|
When an outbound DLP detector matches a token, the route's `dlp.outbound_on_match` policy decides what happens. Under the default `supervise`, the proxy queues an `egress-token-allow` proposal for the operator's `bot-bottle supervise` TUI and holds the request open until it is answered (or `EGRESS_TOKEN_ALLOW_TIMEOUT_SECONDS`, default 300s, elapses — after which it fails closed). The operator never sees the raw token, only the host, method, path, and a redacted snippet; approving adds the value to an in-memory safelist for the life of the egress proxy. Under `redact`, the matched value is scrubbed from the body, headers, and path and the request is forwarded (failing closed if a match lands somewhere unredactable, like the hostname). Under `block` it stays a hard `403`. Structural blocks (CRLF injection) and not-in-allowlist host blocks are always hard `403`s regardless of policy.
|
||||||
|
|
||||||
More examples in `examples/`. Full design lives under `docs/prds/`; the trust-boundary rationale is in `docs/prds/0011-per-file-md-manifest.md`.
|
More examples in `examples/`. Full design lives under `docs/prds/`; the trust-boundary rationale is in `docs/prds/0011-per-file-md-manifest.md`.
|
||||||
|
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ class AgentProvider(ABC):
|
|||||||
initial task in a non-interactive (headless) session.
|
initial task in a non-interactive (headless) session.
|
||||||
|
|
||||||
Called only when ``--prompt`` is passed to
|
Called only when ``--prompt`` is passed to
|
||||||
``./cli.py start --headless``; the returned args are appended
|
``bot-bottle start --headless``; the returned args are appended
|
||||||
after the provider's ``bypass_args`` and ``startup_args``."""
|
after the provider's ``bypass_args`` and ``startup_args``."""
|
||||||
|
|
||||||
def provision_ca(self, bottle: "Bottle", plan: "BottlePlan") -> None:
|
def provision_ca(self, bottle: "Bottle", plan: "BottlePlan") -> None:
|
||||||
|
|||||||
@@ -531,7 +531,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
|||||||
host-appropriate config or commands. Prints to stdout/stderr and
|
host-appropriate config or commands. Prints to stdout/stderr and
|
||||||
returns a shell exit code (0 = nothing to report / success). A
|
returns a shell exit code (0 = nothing to report / success). A
|
||||||
backend that needs no host setup prints a short note and returns
|
backend that needs no host setup prints a short note and returns
|
||||||
0. Invoked generically by `./cli.py backend setup [--backend=…]`
|
0. Invoked generically by `bot-bottle backend setup [--backend=…]`
|
||||||
so operators can provision any backend without a
|
so operators can provision any backend without a
|
||||||
backend-specific command. Classmethod (like `is_available`) —
|
backend-specific command. Classmethod (like `is_available`) —
|
||||||
it's a host query, not per-bottle state."""
|
it's a host query, not per-bottle state."""
|
||||||
@@ -548,14 +548,14 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
|||||||
stderr. When quiet=True returns the status code silently —
|
stderr. When quiet=True returns the status code silently —
|
||||||
useful for cheap programmatic checks.
|
useful for cheap programmatic checks.
|
||||||
|
|
||||||
Invoked by `./cli.py backend status [--backend=…]` (quiet=False)
|
Invoked by `bot-bottle backend status [--backend=…]` (quiet=False)
|
||||||
and by is_backend_ready() (caller-controlled)."""
|
and by is_backend_ready() (caller-controlled)."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def teardown(cls) -> int:
|
def teardown(cls) -> int:
|
||||||
"""Undo `setup()` — the inverse operation, surfaced as
|
"""Undo `setup()` — the inverse operation, surfaced as
|
||||||
`./cli.py backend teardown [--backend=…]` (uninstall). Symmetric
|
`bot-bottle backend teardown [--backend=…]` (uninstall). Symmetric
|
||||||
with setup: where setup is advisory (prints the privileged
|
with setup: where setup is advisory (prints the privileged
|
||||||
commands / declarative config to apply), teardown prints the
|
commands / declarative config to apply), teardown prints the
|
||||||
commands / config change to remove the host prerequisites. A
|
commands / config change to remove the host prerequisites. A
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pointer, and `status()` reports whether docker is usable.
|
|||||||
This is intentionally minimal; a richer version (daemon config checks,
|
This is intentionally minimal; a richer version (daemon config checks,
|
||||||
gVisor/runsc install guidance, rootless-docker hints) is tracked
|
gVisor/runsc install guidance, rootless-docker hints) is tracked
|
||||||
separately. Reached via `DockerBottleBackend.setup` / `.status`, which
|
separately. Reached via `DockerBottleBackend.setup` / `.status`, which
|
||||||
the generic `./cli.py backend {setup,status}` dispatches to.
|
the generic `bot-bottle backend {setup,status}` dispatches to.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -69,7 +69,7 @@ def teardown() -> int:
|
|||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
"Docker backend: nothing to undo — it provisions no privileged host "
|
"Docker backend: nothing to undo — it provisions no privileged host "
|
||||||
"state (networks and the gateway are per-launch and are "
|
"state (networks and the gateway are per-launch and are "
|
||||||
"removed by `./cli.py cleanup`). Docker itself is left installed.\n"
|
"removed by `bot-bottle cleanup`). Docker itself is left installed.\n"
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -89,5 +89,5 @@ def status() -> int:
|
|||||||
runsc = _docker_on_path() and _util.runsc_available()
|
runsc = _docker_on_path() and _util.runsc_available()
|
||||||
sys.stderr.write(f"gVisor runsc runtime: {'registered' if runsc else 'not registered (optional)'}\n")
|
sys.stderr.write(f"gVisor runsc runtime: {'registered' if runsc else 'not registered (optional)'}\n")
|
||||||
if not ok:
|
if not ok:
|
||||||
sys.stderr.write("\nRun: ./cli.py backend setup --backend=docker\n")
|
sys.stderr.write("\nRun: bot-bottle backend setup --backend=docker\n")
|
||||||
return 0 if ok else 1
|
return 0 if ok else 1
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ def boot_vm(
|
|||||||
Records the PID."""
|
Records the PID."""
|
||||||
if not netpool.tap_present(slot.iface):
|
if not netpool.tap_present(slot.iface):
|
||||||
die(f"infra link {slot.iface} not present.\n"
|
die(f"infra link {slot.iface} not present.\n"
|
||||||
f" ./cli.py backend setup --backend=firecracker")
|
f" bot-bottle backend setup --backend=firecracker")
|
||||||
|
|
||||||
run_dir.mkdir(parents=True, exist_ok=True)
|
run_dir.mkdir(parents=True, exist_ok=True)
|
||||||
rootfs = run_dir / "rootfs.ext4"
|
rootfs = run_dir / "rootfs.ext4"
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ def verify_isolation(private_key: Path, guest_ip: str) -> None:
|
|||||||
die(f"ISOLATION FAILURE: the VM reached the host canary "
|
die(f"ISOLATION FAILURE: the VM reached the host canary "
|
||||||
f"{canary_ip}:{canary_port}. The egress boundary is not in "
|
f"{canary_ip}:{canary_port}. The egress boundary is not in "
|
||||||
f"force — refusing to run the agent (fail-closed). Verify the "
|
f"force — refusing to run the agent (fail-closed). Verify the "
|
||||||
f"nft table with: ./cli.py backend setup --backend=firecracker")
|
f"nft table with: bot-bottle backend setup --backend=firecracker")
|
||||||
if result.returncode == 2:
|
if result.returncode == 2:
|
||||||
die("isolation probe inconclusive: the guest has no python3/bash/nc "
|
die("isolation probe inconclusive: the guest has no python3/bash/nc "
|
||||||
"to run the connectivity test. Refusing to continue "
|
"to run the connectivity test. Refusing to continue "
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ config renderers (shell command + NixOS module) shown to operators.
|
|||||||
|
|
||||||
The Firecracker backend needs a privileged one-time network setup:
|
The Firecracker backend needs a privileged one-time network setup:
|
||||||
a pool of point-to-point TAP devices (owned by the invoking user, so
|
a pool of point-to-point TAP devices (owned by the invoking user, so
|
||||||
`./cli.py start` never needs root) and a dedicated nftables table that
|
`bot-bottle start` never needs root) and a dedicated nftables table that
|
||||||
isolates every VM. The pool parameters live in exactly one place —
|
isolates every VM. The pool parameters live in exactly one place —
|
||||||
`netpool.defaults.env`, a plain KEY=VALUE file next to this module —
|
`netpool.defaults.env`, a plain KEY=VALUE file next to this module —
|
||||||
and every consumer reads *that*: this module (below), the shell script
|
and every consumer reads *that*: this module (below), the shell script
|
||||||
@@ -315,11 +315,11 @@ def allocate(slug: str) -> tuple[Slot, IO[str]]:
|
|||||||
return s, handle
|
return s, handle
|
||||||
die(f"Firecracker TAP pool exhausted ({pool_size()} slots, all in "
|
die(f"Firecracker TAP pool exhausted ({pool_size()} slots, all in "
|
||||||
f"use). Stop a running bottle or raise BOT_BOTTLE_FC_POOL_SIZE "
|
f"use). Stop a running bottle or raise BOT_BOTTLE_FC_POOL_SIZE "
|
||||||
f"and re-run `./cli.py backend setup --backend=firecracker`.")
|
f"and re-run `bot-bottle backend setup --backend=firecracker`.")
|
||||||
raise AssertionError("unreachable")
|
raise AssertionError("unreachable")
|
||||||
|
|
||||||
|
|
||||||
# --- config renderers (shown by `./cli.py backend setup`) -----------
|
# --- config renderers (shown by `bot-bottle backend setup`) -----------
|
||||||
|
|
||||||
# The persistent unit is the portable install: the same systemd oneshot
|
# The persistent unit is the portable install: the same systemd oneshot
|
||||||
# on every systemd distro (Debian/Ubuntu/Fedora/RHEL/Arch/…).
|
# on every systemd distro (Debian/Ubuntu/Fedora/RHEL/Arch/…).
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ bundled setup script. `status()` reports what's present, including
|
|||||||
whether the pool range collides with an existing route.
|
whether the pool range collides with an existing route.
|
||||||
|
|
||||||
Called through `FirecrackerBottleBackend.setup` / `.status`, which the
|
Called through `FirecrackerBottleBackend.setup` / `.status`, which the
|
||||||
generic `./cli.py backend {setup,status}` command dispatches to.
|
generic `bot-bottle backend {setup,status}` command dispatches to.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -34,7 +34,7 @@ _UNIT_PATH = Path("/etc/systemd/system") / netpool.SYSTEMD_UNIT
|
|||||||
|
|
||||||
def _owner() -> str:
|
def _owner() -> str:
|
||||||
# Under `sudo`, USER is root but SUDO_USER is the real invoker — the
|
# Under `sudo`, USER is root but SUDO_USER is the real invoker — the
|
||||||
# TAPs must be owned by them so `./cli.py start` stays rootless.
|
# TAPs must be owned by them so `bot-bottle start` stays rootless.
|
||||||
return os.environ.get("SUDO_USER") or os.environ.get("USER") or "youruser"
|
return os.environ.get("SUDO_USER") or os.environ.get("USER") or "youruser"
|
||||||
|
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ def _setup_systemd() -> None:
|
|||||||
if rc == 0:
|
if rc == 0:
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
f"Installed and started {netpool.SYSTEMD_UNIT}. Verify with "
|
f"Installed and started {netpool.SYSTEMD_UNIT}. Verify with "
|
||||||
f"`./cli.py backend status --backend=firecracker`.\n"
|
f"`bot-bottle backend status --backend=firecracker`.\n"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
@@ -181,7 +181,7 @@ def _setup_systemd() -> None:
|
|||||||
)
|
)
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
f"\n(Or re-run this as root to install it directly: "
|
f"\n(Or re-run this as root to install it directly: "
|
||||||
f"sudo ./cli.py backend setup --backend=firecracker)\n"
|
f"sudo bot-bottle backend setup --backend=firecracker)\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -334,7 +334,7 @@ def status() -> int:
|
|||||||
sys.stderr.write(f"range overlap: none (base {netpool.ip_base()})\n")
|
sys.stderr.write(f"range overlap: none (base {netpool.ip_base()})\n")
|
||||||
_report_persistence()
|
_report_persistence()
|
||||||
if not ok:
|
if not ok:
|
||||||
sys.stderr.write("\nRun: ./cli.py backend setup --backend=firecracker\n")
|
sys.stderr.write("\nRun: bot-bottle backend setup --backend=firecracker\n")
|
||||||
return 0 if ok else 1
|
return 0 if ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ generation.
|
|||||||
|
|
||||||
The privileged network setup (TAP pool + nft table) is a one-time
|
The privileged network setup (TAP pool + nft table) is a one-time
|
||||||
operator step — see `netpool.py`, `scripts/firecracker-netpool.sh`,
|
operator step — see `netpool.py`, `scripts/firecracker-netpool.sh`,
|
||||||
and `./cli.py backend setup --backend=firecracker`.
|
and `bot-bottle backend setup --backend=firecracker`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -132,18 +132,18 @@ def _require_network_pool() -> None:
|
|||||||
f"{netpool.pool_size()} slots) overlaps existing routes: "
|
f"{netpool.pool_size()} slots) overlaps existing routes: "
|
||||||
f"{detail}. This can shadow or be shadowed by that route; "
|
f"{detail}. This can shadow or be shadowed by that route; "
|
||||||
f"set BOT_BOTTLE_FC_IP_BASE to a free range and re-run "
|
f"set BOT_BOTTLE_FC_IP_BASE to a free range and re-run "
|
||||||
f"./cli.py backend setup --backend=firecracker.")
|
f"bot-bottle backend setup --backend=firecracker.")
|
||||||
missing = netpool.missing_taps()
|
missing = netpool.missing_taps()
|
||||||
if missing:
|
if missing:
|
||||||
die(f"network pool incomplete — missing TAP devices: "
|
die(f"network pool incomplete — missing TAP devices: "
|
||||||
f"{', '.join(missing)}.\n ./cli.py backend setup --backend=firecracker")
|
f"{', '.join(missing)}.\n bot-bottle backend setup --backend=firecracker")
|
||||||
if shutil.which("nft") is not None and not netpool.nft_table_present():
|
if shutil.which("nft") is not None and not netpool.nft_table_present():
|
||||||
# nft is queryable and says the table is absent — that's a
|
# nft is queryable and says the table is absent — that's a
|
||||||
# definite, catchable misconfiguration; fail early.
|
# definite, catchable misconfiguration; fail early.
|
||||||
warn(f"isolation table `inet {netpool.NFT_TABLE}` not found via nft. "
|
warn(f"isolation table `inet {netpool.NFT_TABLE}` not found via nft. "
|
||||||
"If this is a permissions issue it will be re-checked "
|
"If this is a permissions issue it will be re-checked "
|
||||||
"empirically after boot; otherwise run: "
|
"empirically after boot; otherwise run: "
|
||||||
"./cli.py backend setup --backend=firecracker")
|
"bot-bottle backend setup --backend=firecracker")
|
||||||
|
|
||||||
|
|
||||||
# --- rootfs pipeline (rootless) -------------------------------------
|
# --- rootfs pipeline (rootless) -------------------------------------
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class Freezer(ABC):
|
|||||||
|
|
||||||
Calls _freeze for the backend-specific snapshot, then writes the
|
Calls _freeze for the backend-specific snapshot, then writes the
|
||||||
committed image reference to per-bottle state and marks the bottle
|
committed image reference to per-bottle state and marks the bottle
|
||||||
preserved so the next `./cli.py resume` boots from the snapshot.
|
preserved so the next `bot-bottle resume` boots from the snapshot.
|
||||||
|
|
||||||
Raises CommitCancelled if the user declines an interactive
|
Raises CommitCancelled if the user declines an interactive
|
||||||
confirmation prompt (e.g. the macos-container stop prompt).
|
confirmation prompt (e.g. the macos-container stop prompt).
|
||||||
@@ -51,7 +51,7 @@ class Freezer(ABC):
|
|||||||
image_ref = self._freeze(agent)
|
image_ref = self._freeze(agent)
|
||||||
write_committed_image(agent.slug, image_ref)
|
write_committed_image(agent.slug, image_ref)
|
||||||
mark_preserved(agent.slug)
|
mark_preserved(agent.slug)
|
||||||
info(f"to resume from this snapshot: ./cli.py resume {agent.slug}")
|
info(f"to resume from this snapshot: bot-bottle resume {agent.slug}")
|
||||||
self._export_hint(agent.slug, image_ref)
|
self._export_hint(agent.slug, image_ref)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Like Docker, this backend needs no privileged network-pool provisioning
|
|||||||
running. `setup()` points at the install/`container system start` steps;
|
running. `setup()` points at the install/`container system start` steps;
|
||||||
`status()` reports readiness. Reached via
|
`status()` reports readiness. Reached via
|
||||||
`MacosContainerBottleBackend.setup` / `.status`, dispatched from the
|
`MacosContainerBottleBackend.setup` / `.status`, dispatched from the
|
||||||
generic `./cli.py backend {setup,status}`.
|
generic `bot-bottle backend {setup,status}`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -75,5 +75,5 @@ def status() -> int:
|
|||||||
if not _service_running():
|
if not _service_running():
|
||||||
ok = False
|
ok = False
|
||||||
if not ok:
|
if not ok:
|
||||||
sys.stderr.write("\nRun: ./cli.py backend setup --backend=macos-container\n")
|
sys.stderr.write("\nRun: bot-bottle backend setup --backend=macos-container\n")
|
||||||
return 0 if ok else 1
|
return 0 if ok else 1
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ def _print_vm_install_instructions() -> None:
|
|||||||
info("Then start the service: container system start")
|
info("Then start the service: container system start")
|
||||||
else:
|
else:
|
||||||
info("Install Firecracker: https://github.com/firecracker-microvm/firecracker/releases")
|
info("Install Firecracker: https://github.com/firecracker-microvm/firecracker/releases")
|
||||||
info("Configure the host: ./cli.py backend setup")
|
info("Configure the host: bot-bottle backend setup")
|
||||||
|
|
||||||
|
|
||||||
def _auto_select_backend(prompt: bool = True) -> str:
|
def _auto_select_backend(prompt: bool = True) -> str:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""`backend` CLI command — generic host setup/status across backends.
|
"""`backend` CLI command — generic host setup/status across backends.
|
||||||
|
|
||||||
`./cli.py backend setup [--backend=NAME]` provisions (or points at how
|
`bot-bottle backend setup [--backend=NAME]` provisions (or points at how
|
||||||
to provision) the chosen backend's one-time host prerequisites.
|
to provision) the chosen backend's one-time host prerequisites.
|
||||||
`./cli.py backend status [--backend=NAME]` reports readiness.
|
`bot-bottle backend status [--backend=NAME]` reports readiness.
|
||||||
`./cli.py backend teardown [--backend=NAME]` undoes setup (uninstall).
|
`bot-bottle backend teardown [--backend=NAME]` undoes setup (uninstall).
|
||||||
|
|
||||||
All dispatch to the backend's `setup()` / `status()` / `teardown()`
|
All dispatch to the backend's `setup()` / `status()` / `teardown()`
|
||||||
classmethods, so there are no backend-specific commands — swapping
|
classmethods, so there are no backend-specific commands — swapping
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""cleanup: stop and remove all orphaned bot-bottle resources.
|
"""cleanup: stop and remove all orphaned bot-bottle resources.
|
||||||
|
|
||||||
Walks every registered backend (docker, firecracker, macos-container)
|
Walks every registered backend (docker, firecracker, macos-container)
|
||||||
so a single `./cli.py cleanup` reaps every backend's leftovers — a
|
so a single `bot-bottle cleanup` reaps every backend's leftovers — a
|
||||||
firecracker bottle's VM processes and run dirs won't survive a
|
firecracker bottle's VM processes and run dirs won't survive a
|
||||||
docker-only cleanup pass (issue addressed alongside #77).
|
docker-only cleanup pass (issue addressed alongside #77).
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Docker bottles are committed to a local Docker image. Macos-container
|
|||||||
bottles are exported and rebuilt as a local Apple Container image.
|
bottles are exported and rebuilt as a local Apple Container image.
|
||||||
Firecracker bottles stream the guest rootfs out over SSH and rebuild a
|
Firecracker bottles stream the guest rootfs out over SSH and rebuild a
|
||||||
local Docker image. The resulting reference is stored in per-bottle
|
local Docker image. The resulting reference is stored in per-bottle
|
||||||
state so the next `./cli.py resume <slug>` boots from the snapshot
|
state so the next `bot-bottle resume <slug>` boots from the snapshot
|
||||||
instead of rebuilding from the Dockerfile.
|
instead of rebuilding from the Dockerfile.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ def cmd_commit(argv: list[str]) -> int:
|
|||||||
if slug is None:
|
if slug is None:
|
||||||
active = enumerate_active_agents()
|
active = enumerate_active_agents()
|
||||||
if not active:
|
if not active:
|
||||||
die("no active bottles; start one with `./cli.py start`")
|
die("no active bottles; start one with `bot-bottle start`")
|
||||||
choices = [a.slug for a in active]
|
choices = [a.slug for a in active]
|
||||||
slug = tui.filter_select(choices, title="Select bottle to commit")
|
slug = tui.filter_select(choices, title="Select bottle to commit")
|
||||||
if slug is None:
|
if slug is None:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ override and transcript snapshot under the same state dir.
|
|||||||
|
|
||||||
Use case: an interrupted or preserved bottle needs to be relaunched;
|
Use case: an interrupted or preserved bottle needs to be relaunched;
|
||||||
the operator runs
|
the operator runs
|
||||||
./cli.py resume <identity>
|
bot-bottle resume <identity>
|
||||||
to bring up the replacement from the recorded state.
|
to bring up the replacement from the recorded state.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -203,19 +203,19 @@ def _start_headless(
|
|||||||
if not os.isatty(stdin_fd):
|
if not os.isatty(stdin_fd):
|
||||||
die(
|
die(
|
||||||
"--headless requires a PTY on stdin; run via:\n"
|
"--headless requires a PTY on stdin; run via:\n"
|
||||||
" script -q /dev/null ./cli.py start ..."
|
" script -q /dev/null bot-bottle start ..."
|
||||||
)
|
)
|
||||||
|
|
||||||
agent_name = args.name
|
agent_name = args.name
|
||||||
if not agent_name:
|
if not agent_name:
|
||||||
die("--headless requires an agent name: ./cli.py start <agent> --headless")
|
die("--headless requires an agent name: bot-bottle start <agent> --headless")
|
||||||
manifest.require_agent(agent_name) # raises ManifestError if unknown
|
manifest.require_agent(agent_name) # raises ManifestError if unknown
|
||||||
|
|
||||||
prompt = args.prompt
|
prompt = args.prompt
|
||||||
if not prompt:
|
if not prompt:
|
||||||
die(
|
die(
|
||||||
"--headless requires --prompt: "
|
"--headless requires --prompt: "
|
||||||
"./cli.py start <agent> --headless --prompt 'Do the thing'"
|
"bot-bottle start <agent> --headless --prompt 'Do the thing'"
|
||||||
)
|
)
|
||||||
|
|
||||||
if args.bottle:
|
if args.bottle:
|
||||||
@@ -319,9 +319,9 @@ def attach_agent(
|
|||||||
|
|
||||||
`resume=True` adds `--continue` so claude picks up its most
|
`resume=True` adds `--continue` so claude picks up its most
|
||||||
recent session non-interactively (no session-picker prompt).
|
recent session non-interactively (no session-picker prompt).
|
||||||
First-attach paths (`./cli.py start`) leave it False.
|
First-attach paths (`bot-bottle start`) leave it False.
|
||||||
|
|
||||||
Used as the inner step of `./cli.py start`."""
|
Used as the inner step of `bot-bottle start`."""
|
||||||
runtime = runtime_for(agent_provider_template)
|
runtime = runtime_for(agent_provider_template)
|
||||||
info(
|
info(
|
||||||
f"attaching interactive {agent_provider_template} session "
|
f"attaching interactive {agent_provider_template} session "
|
||||||
@@ -354,7 +354,7 @@ def settle_state(identity: str) -> None:
|
|||||||
if not identity:
|
if not identity:
|
||||||
return
|
return
|
||||||
if is_preserved(identity):
|
if is_preserved(identity):
|
||||||
info(f"to resume this bottle: ./cli.py resume {identity}")
|
info(f"to resume this bottle: bot-bottle resume {identity}")
|
||||||
return
|
return
|
||||||
cleanup_state(identity)
|
cleanup_state(identity)
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ def discover_pending() -> list[QueuedProposal]:
|
|||||||
def _approval_status(qp: QueuedProposal, verb: str) -> str:
|
def _approval_status(qp: QueuedProposal, verb: str) -> str:
|
||||||
"""Status-line text after a successful approval."""
|
"""Status-line text after a successful approval."""
|
||||||
base = f"{verb} {qp.proposal.tool} for [{qp.label}]"
|
base = f"{verb} {qp.proposal.tool} for [{qp.label}]"
|
||||||
return f"{base}; resume: ./cli.py resume {qp.label}"
|
return f"{base}; resume: bot-bottle resume {qp.label}"
|
||||||
|
|
||||||
|
|
||||||
def _detail_lines(
|
def _detail_lines(
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ PY
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
echo "git-gate: queued # gitleaks:allow supervisor approval $proposal_id" >&2
|
echo "git-gate: queued # gitleaks:allow supervisor approval $proposal_id" >&2
|
||||||
echo "git-gate: approve with './cli.py supervise' to continue this push" >&2
|
echo "git-gate: approve with 'bot-bottle supervise' to continue this push" >&2
|
||||||
waited=0
|
waited=0
|
||||||
while [ "$waited" -lt "$timeout" ]; do
|
while [ "$waited" -lt "$timeout" ]; do
|
||||||
status=$(PYTHONPATH="/app${PYTHONPATH:+:$PYTHONPATH}" python3 - "$proposal_id" <<'PY'
|
status=$(PYTHONPATH="/app${PYTHONPATH:+:$PYTHONPATH}" python3 - "$proposal_id" <<'PY'
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# VHS tape — drives `./cli.py start demo` interactively and asks
|
# VHS tape — drives `bot-bottle start demo` interactively and asks
|
||||||
# claude (the AI) to run four probes via natural-language prompts.
|
# claude (the AI) to run four probes via natural-language prompts.
|
||||||
# Setup (manifest + dummy SSH key + image pre-warm) and teardown
|
# Setup (manifest + dummy SSH key + image pre-warm) and teardown
|
||||||
# happen outside the tape; record via `bash scripts/demo-record.sh`,
|
# happen outside the tape; record via `bash scripts/demo-record.sh`,
|
||||||
@@ -29,7 +29,7 @@ Show
|
|||||||
# defaults), one git upstream (unreachable on purpose so gitleaks runs
|
# defaults), one git upstream (unreachable on purpose so gitleaks runs
|
||||||
# before the gate would forward), and a FAKE_TOKEN env var shaped like
|
# before the gate would forward), and a FAKE_TOKEN env var shaped like
|
||||||
# a GitHub PAT.
|
# a GitHub PAT.
|
||||||
Type "./cli.py start demo"
|
Type "bot-bottle start demo"
|
||||||
Enter
|
Enter
|
||||||
Sleep 8s
|
Sleep 8s
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`:
|
|||||||
can revisit, but the v1 of this PRD is one file = one bottle.
|
can revisit, but the v1 of this PRD is one file = one bottle.
|
||||||
|
|
||||||
- **Hot-reload.** Changes to manifest files take effect at next
|
- **Hot-reload.** Changes to manifest files take effect at next
|
||||||
`./cli.py start`; we do not watch the directory.
|
`bot-bottle start`; we do not watch the directory.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ After this PRD:
|
|||||||
|
|
||||||
### Cleanup CLI
|
### Cleanup CLI
|
||||||
|
|
||||||
`./cli.py cleanup` switches from "list every container with prefix
|
`bot-bottle cleanup` switches from "list every container with prefix
|
||||||
`bot-bottle-` and every network with prefix `bot-bottle-net-`
|
`bot-bottle-` and every network with prefix `bot-bottle-net-`
|
||||||
or `bot-bottle-egress-`" to:
|
or `bot-bottle-egress-`" to:
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ Sized for one PR each, in order.
|
|||||||
`docker compose up -d` + attach + teardown. Per-sidecar `start()`/
|
`docker compose up -d` + attach + teardown. Per-sidecar `start()`/
|
||||||
`stop()` lifecycle methods deleted in the same chunk. Compose-
|
`stop()` lifecycle methods deleted in the same chunk. Compose-
|
||||||
log dump on teardown added.
|
log dump on teardown added.
|
||||||
4. **Cleanup CLI on compose.** Switch `./cli.py cleanup` to
|
4. **Cleanup CLI on compose.** Switch `bot-bottle cleanup` to
|
||||||
`docker compose ls`-based discovery; keep prefix-scan as
|
`docker compose ls`-based discovery; keep prefix-scan as
|
||||||
fallback for one release.
|
fallback for one release.
|
||||||
5. **Dashboard.** Decide on the discovery question (open question
|
5. **Dashboard.** Decide on the discovery question (open question
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Two rough edges in the current dashboard:
|
|||||||
shows only pending proposals. If no agent has called a tool,
|
shows only pending proposals. If no agent has called a tool,
|
||||||
the screen reads "no pending proposals" — even when five
|
the screen reads "no pending proposals" — even when five
|
||||||
bottles are quietly working. The operator has to `docker
|
bottles are quietly working. The operator has to `docker
|
||||||
compose ls` (or `./cli.py cleanup -n` to see the y/N preview)
|
compose ls` (or `bot-bottle cleanup -n` to see the y/N preview)
|
||||||
to find out what's actually live.
|
to find out what's actually live.
|
||||||
|
|
||||||
2. **`e` / `p` re-discover-and-disambiguate every invocation.**
|
2. **`e` / `p` re-discover-and-disambiguate every invocation.**
|
||||||
@@ -82,12 +82,12 @@ the "operator wants to make an unprompted change" case.
|
|||||||
global across bottles. Filtering ("show me only this agent's
|
global across bottles. Filtering ("show me only this agent's
|
||||||
proposals") might be a follow-up but isn't this PRD.
|
proposals") might be a follow-up but isn't this PRD.
|
||||||
- **Agent lifecycle from the dashboard.** Starting / stopping
|
- **Agent lifecycle from the dashboard.** Starting / stopping
|
||||||
agents stays in `./cli.py start` / `./cli.py cleanup`. The
|
agents stays in `bot-bottle start` / `bot-bottle cleanup`. The
|
||||||
dashboard reads state; it doesn't change it.
|
dashboard reads state; it doesn't change it.
|
||||||
- **Preserved-but-not-running bottles.** The active-agents list
|
- **Preserved-but-not-running bottles.** The active-agents list
|
||||||
is strictly "what's running now" (cross-referenced from
|
is strictly "what's running now" (cross-referenced from
|
||||||
`docker compose ls`). Preserved state dirs without a live
|
`docker compose ls`). Preserved state dirs without a live
|
||||||
project don't appear — `./cli.py resume <identity>` is the
|
project don't appear — `bot-bottle resume <identity>` is the
|
||||||
path for those.
|
path for those.
|
||||||
- **A separate per-agent detail view.** The agent rows are
|
- **A separate per-agent detail view.** The agent rows are
|
||||||
one-line summaries. Pressing Enter on a proposal still drops
|
one-line summaries. Pressing Enter on a proposal still drops
|
||||||
@@ -125,7 +125,7 @@ the "operator wants to make an unprompted change" case.
|
|||||||
- Changes to proposal handling (`a` / `m` / `r` / Enter all
|
- Changes to proposal handling (`a` / `m` / `r` / Enter all
|
||||||
unchanged).
|
unchanged).
|
||||||
- Changes to the queue-dir / supervise sidecar protocol.
|
- Changes to the queue-dir / supervise sidecar protocol.
|
||||||
- New CLI surface beyond what's in `./cli.py dashboard`.
|
- New CLI surface beyond what's in `bot-bottle dashboard`.
|
||||||
- Touching the manifest, compose renderer, launch lifecycle.
|
- Touching the manifest, compose renderer, launch lifecycle.
|
||||||
|
|
||||||
## Proposed design
|
## Proposed design
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
Today the dashboard is read-only: it surfaces pending proposals
|
Today the dashboard is read-only: it surfaces pending proposals
|
||||||
and active agents (PRD 0019) but can't *start* an agent or
|
and active agents (PRD 0019) but can't *start* an agent or
|
||||||
*re-enter* one. The operator's path is split — they launch
|
*re-enter* one. The operator's path is split — they launch
|
||||||
agents from one terminal (`./cli.py start <name>`), and watch
|
agents from one terminal (`bot-bottle start <name>`), and watch
|
||||||
them from another (`./cli.py dashboard`).
|
them from another (`bot-bottle dashboard`).
|
||||||
|
|
||||||
This PRD collapses that split. The dashboard becomes the
|
This PRD collapses that split. The dashboard becomes the
|
||||||
operator's single surface: pressing a key opens an agent picker,
|
operator's single surface: pressing a key opens an agent picker,
|
||||||
@@ -31,7 +31,7 @@ claude session AND the dashboard process. Exit claude → back to
|
|||||||
dashboard, bottle still running. Start another agent → two
|
dashboard, bottle still running. Start another agent → two
|
||||||
bottles up at once. Quit the dashboard → bottles continue
|
bottles up at once. Quit the dashboard → bottles continue
|
||||||
running. Teardown is **always explicit**: the operator presses
|
running. Teardown is **always explicit**: the operator presses
|
||||||
`x` on an agent, or runs `./cli.py cleanup` later.
|
`x` on an agent, or runs `bot-bottle cleanup` later.
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ Two real frictions today:
|
|||||||
open and the dashboard's "active agents" pane is hopelessly
|
open and the dashboard's "active agents" pane is hopelessly
|
||||||
behind reality because they just spawned three in a row.
|
behind reality because they just spawned three in a row.
|
||||||
|
|
||||||
2. **`./cli.py start` ties the bottle to a single claude
|
2. **`bot-bottle start` ties the bottle to a single claude
|
||||||
session.** The start command's `ExitStack` brings the bottle
|
session.** The start command's `ExitStack` brings the bottle
|
||||||
up, runs claude, and tears down on Ctrl-D — fine for a one-
|
up, runs claude, and tears down on Ctrl-D — fine for a one-
|
||||||
shot session, wrong for "let me bounce in and out of this
|
shot session, wrong for "let me bounce in and out of this
|
||||||
@@ -60,7 +60,7 @@ captures full-merged logs per bottle (PRD 0018). It already
|
|||||||
|
|
||||||
## Goals / Success Criteria
|
## Goals / Success Criteria
|
||||||
|
|
||||||
1. From inside `./cli.py dashboard`, pressing `n` (new) opens
|
1. From inside `bot-bottle dashboard`, pressing `n` (new) opens
|
||||||
an agent picker listing every agent defined in the manifest.
|
an agent picker listing every agent defined in the manifest.
|
||||||
Selecting one runs `prepare → preflight → launch`.
|
Selecting one runs `prepare → preflight → launch`.
|
||||||
2. The preflight Y/N summary renders cleanly — either as a
|
2. The preflight Y/N summary renders cleanly — either as a
|
||||||
@@ -83,7 +83,7 @@ captures full-merged logs per bottle (PRD 0018). It already
|
|||||||
state cleanup) without quitting the dashboard.
|
state cleanup) without quitting the dashboard.
|
||||||
7. Quitting the dashboard (`q`) leaves every running bottle
|
7. Quitting the dashboard (`q`) leaves every running bottle
|
||||||
running. Bottle teardown is always explicit (per-bottle `x`
|
running. Bottle teardown is always explicit (per-bottle `x`
|
||||||
or `./cli.py cleanup`). The next `./cli.py dashboard`
|
or `bot-bottle cleanup`). The next `bot-bottle dashboard`
|
||||||
invocation re-discovers them via `list_active_slugs()` and
|
invocation re-discovers them via `list_active_slugs()` and
|
||||||
surfaces re-attach for any it can reconstruct context for
|
surfaces re-attach for any it can reconstruct context for
|
||||||
(see "Cross-dashboard re-attach" below).
|
(see "Cross-dashboard re-attach" below).
|
||||||
@@ -94,13 +94,13 @@ captures full-merged logs per bottle (PRD 0018). It already
|
|||||||
embedded-emulator option from the research doc is out of
|
embedded-emulator option from the research doc is out of
|
||||||
scope. The handoff (option 1) is the v1; option 2 is a
|
scope. The handoff (option 1) is the v1; option 2 is a
|
||||||
separate PRD if and when handoff is observably insufficient.
|
separate PRD if and when handoff is observably insufficient.
|
||||||
- **Adopting bottles started by an out-of-dashboard `./cli.py
|
- **Adopting bottles started by an out-of-dashboard `bot-bottle
|
||||||
start` invocation.** Those have their own ExitStack-owner and
|
start` invocation.** Those have their own ExitStack-owner and
|
||||||
the dashboard treats them as read-only-watch (already does
|
the dashboard treats them as read-only-watch (already does
|
||||||
today). Re-attach only applies to bottles the *current
|
today). Re-attach only applies to bottles the *current
|
||||||
dashboard process* started.
|
dashboard process* started.
|
||||||
- **Resurrecting an out-of-process bottle into a new dashboard
|
- **Resurrecting an out-of-process bottle into a new dashboard
|
||||||
with full re-attach.** A bottle started by `./cli.py start`
|
with full re-attach.** A bottle started by `bot-bottle start`
|
||||||
in another terminal — or by a previous dashboard run, now
|
in another terminal — or by a previous dashboard run, now
|
||||||
exited — appears in the agents pane (already does, PRD 0019)
|
exited — appears in the agents pane (already does, PRD 0019)
|
||||||
and can be re-attached via `docker exec -it claude` because
|
and can be re-attached via `docker exec -it claude` because
|
||||||
@@ -109,12 +109,12 @@ captures full-merged logs per bottle (PRD 0018). It already
|
|||||||
context object to drive teardown — e.g., the
|
context object to drive teardown — e.g., the
|
||||||
ExitStack-tracked CA + state cleanup `_settle_state` performs
|
ExitStack-tracked CA + state cleanup `_settle_state` performs
|
||||||
today. Cross-dashboard re-attach uses the existing
|
today. Cross-dashboard re-attach uses the existing
|
||||||
`./cli.py cleanup` for teardown, not an `x` keypress (see
|
`bot-bottle cleanup` for teardown, not an `x` keypress (see
|
||||||
open questions).
|
open questions).
|
||||||
- **Multi-window UI.** Single curses window, two existing
|
- **Multi-window UI.** Single curses window, two existing
|
||||||
panes (proposals + agents); the agent picker is a modal, not
|
panes (proposals + agents); the agent picker is a modal, not
|
||||||
a third pane.
|
a third pane.
|
||||||
- **Removing `./cli.py start`.** Stays as the script-friendly /
|
- **Removing `bot-bottle start`.** Stays as the script-friendly /
|
||||||
legacy entry point. The dashboard is the new default.
|
legacy entry point. The dashboard is the new default.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
@@ -140,7 +140,7 @@ captures full-merged logs per bottle (PRD 0018). It already
|
|||||||
|
|
||||||
### Out of scope
|
### Out of scope
|
||||||
|
|
||||||
- Changes to `./cli.py start` itself. It keeps its current
|
- Changes to `bot-bottle start` itself. It keeps its current
|
||||||
shape; the dashboard reuses its internal pieces (backend.
|
shape; the dashboard reuses its internal pieces (backend.
|
||||||
prepare / backend.launch) without reaching through the CLI
|
prepare / backend.launch) without reaching through the CLI
|
||||||
layer.
|
layer.
|
||||||
@@ -157,7 +157,7 @@ captures full-merged logs per bottle (PRD 0018). It already
|
|||||||
Today's flow:
|
Today's flow:
|
||||||
|
|
||||||
```
|
```
|
||||||
./cli.py start agent
|
bot-bottle start agent
|
||||||
└─ with backend.launch(plan) as bottle: ← bottle alive while inside `with`
|
└─ with backend.launch(plan) as bottle: ← bottle alive while inside `with`
|
||||||
bottle.exec_agent([...], tty=True) ← blocks until claude exits
|
bottle.exec_agent([...], tty=True) ← blocks until claude exits
|
||||||
# context exits → compose down → state cleanup
|
# context exits → compose down → state cleanup
|
||||||
@@ -166,7 +166,7 @@ Today's flow:
|
|||||||
The proposed dashboard-driven flow:
|
The proposed dashboard-driven flow:
|
||||||
|
|
||||||
```
|
```
|
||||||
./cli.py dashboard
|
bot-bottle dashboard
|
||||||
└─ bottles: dict[str, tuple[ContextManager, DockerBottle]] = {}
|
└─ bottles: dict[str, tuple[ContextManager, DockerBottle]] = {}
|
||||||
|
|
||||||
# operator presses `n`, picks agent
|
# operator presses `n`, picks agent
|
||||||
@@ -205,7 +205,7 @@ Two shifts:
|
|||||||
evaluation, state-dir reap) doesn't fire on a quit-while-
|
evaluation, state-dir reap) doesn't fire on a quit-while-
|
||||||
running bottle. It DOES fire when the operator explicitly
|
running bottle. It DOES fire when the operator explicitly
|
||||||
stops via `x`, because that calls `cm.__exit__`. For
|
stops via `x`, because that calls `cm.__exit__`. For
|
||||||
bottles a previous dashboard quit on, `./cli.py cleanup`
|
bottles a previous dashboard quit on, `bot-bottle cleanup`
|
||||||
is the path — its compose-down + state-reap logic
|
is the path — its compose-down + state-reap logic
|
||||||
already covers the case.
|
already covers the case.
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ Two shifts:
|
|||||||
|
|
||||||
When the dashboard discovers a bottle in `discover_active_agents`
|
When the dashboard discovers a bottle in `discover_active_agents`
|
||||||
that it didn't itself start (a previous-dashboard or external
|
that it didn't itself start (a previous-dashboard or external
|
||||||
`./cli.py start` bottle), Enter still attaches via `docker exec
|
`bot-bottle start` bottle), Enter still attaches via `docker exec
|
||||||
-it … claude` — the agent container is running `sleep infinity`
|
-it … claude` — the agent container is running `sleep infinity`
|
||||||
exactly the same way regardless of who started it. The only
|
exactly the same way regardless of who started it. The only
|
||||||
thing the current dashboard lacks for those bottles is the
|
thing the current dashboard lacks for those bottles is the
|
||||||
@@ -221,8 +221,8 @@ launch-context object needed to drive a clean teardown via
|
|||||||
`x`.
|
`x`.
|
||||||
|
|
||||||
For v1 we surface this honestly: pressing `x` on a non-owned
|
For v1 we surface this honestly: pressing `x` on a non-owned
|
||||||
agent shows a status hint pointing at `./cli.py cleanup` (or
|
agent shows a status hint pointing at `bot-bottle cleanup` (or
|
||||||
`./cli.py cleanup` targeted at the slug if we add that flag
|
`bot-bottle cleanup` targeted at the slug if we add that flag
|
||||||
later). The agent stays alive; the operator handles teardown
|
later). The agent stays alive; the operator handles teardown
|
||||||
out-of-band. Enter (re-attach) works for both owned and
|
out-of-band. Enter (re-attach) works for both owned and
|
||||||
non-owned bottles.
|
non-owned bottles.
|
||||||
@@ -288,7 +288,7 @@ agents pane.
|
|||||||
|
|
||||||
`x` on a non-owned agent (discovered via `list_active_slugs`
|
`x` on a non-owned agent (discovered via `list_active_slugs`
|
||||||
but not in `bottles` dict): no-op with status hint pointing
|
but not in `bottles` dict): no-op with status hint pointing
|
||||||
at `./cli.py cleanup` (the existing path that tears down
|
at `bot-bottle cleanup` (the existing path that tears down
|
||||||
ANY bot-bottle compose project plus reaps state dirs).
|
ANY bot-bottle compose project plus reaps state dirs).
|
||||||
|
|
||||||
### Dashboard quit
|
### Dashboard quit
|
||||||
@@ -300,7 +300,7 @@ the `docker compose` project keeps running. The next dashboard
|
|||||||
invocation discovers the bottles via `list_active_slugs` and
|
invocation discovers the bottles via `list_active_slugs` and
|
||||||
surfaces re-attach.
|
surfaces re-attach.
|
||||||
|
|
||||||
This is a real departure from today's `./cli.py start`
|
This is a real departure from today's `bot-bottle start`
|
||||||
semantics (which couples bottle lifetime to the process via
|
semantics (which couples bottle lifetime to the process via
|
||||||
ExitStack). It's intentional: the dashboard is a watching +
|
ExitStack). It's intentional: the dashboard is a watching +
|
||||||
acting surface, not a lifetime owner.
|
acting surface, not a lifetime owner.
|
||||||
@@ -322,7 +322,7 @@ Sized for one PR each.
|
|||||||
dashboard's ExitStack; handoff invokes `attach_agent`.
|
dashboard's ExitStack; handoff invokes `attach_agent`.
|
||||||
3. **Re-attach via Enter on owned agents-pane row.** Looks up
|
3. **Re-attach via Enter on owned agents-pane row.** Looks up
|
||||||
the slug in the dashboard's `bottles` map; if present →
|
the slug in the dashboard's `bottles` map; if present →
|
||||||
handoff; else → status-line hint pointing at `./cli.py
|
handoff; else → status-line hint pointing at `bot-bottle
|
||||||
resume`.
|
resume`.
|
||||||
4. **Explicit per-bottle stop (`x` keybinding).** Pop the
|
4. **Explicit per-bottle stop (`x` keybinding).** Pop the
|
||||||
bottle's `close` callback off the stack, call it, refresh.
|
bottle's `close` callback off the stack, call it, refresh.
|
||||||
@@ -369,7 +369,7 @@ Sized for one PR each.
|
|||||||
bottles dict goes out of scope without invoking `__exit__`,
|
bottles dict goes out of scope without invoking `__exit__`,
|
||||||
so the `docker compose` projects keep running. Bottle
|
so the `docker compose` projects keep running. Bottle
|
||||||
teardown is always explicit: per-bottle `x` (for
|
teardown is always explicit: per-bottle `x` (for
|
||||||
dashboard-owned), or `./cli.py cleanup` (for everything).
|
dashboard-owned), or `bot-bottle cleanup` (for everything).
|
||||||
|
|
||||||
## Open questions
|
## Open questions
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ window, two panes, no terminal handoff.
|
|||||||
|
|
||||||
## Goals / Success Criteria
|
## Goals / Success Criteria
|
||||||
|
|
||||||
1. When the operator runs `./cli.py dashboard` from inside a
|
1. When the operator runs `bot-bottle dashboard` from inside a
|
||||||
tmux session (`$TMUX` set), the dashboard establishes a
|
tmux session (`$TMUX` set), the dashboard establishes a
|
||||||
two-pane layout: dashboard in the left pane, an initially-
|
two-pane layout: dashboard in the left pane, an initially-
|
||||||
empty right pane reserved for claude sessions.
|
empty right pane reserved for claude sessions.
|
||||||
@@ -313,7 +313,7 @@ Sized small.
|
|||||||
3. **Dashboard launched OUTSIDE tmux but tmux is installed.**
|
3. **Dashboard launched OUTSIDE tmux but tmux is installed.**
|
||||||
Should the dashboard auto-exec itself inside a fresh tmux
|
Should the dashboard auto-exec itself inside a fresh tmux
|
||||||
session to get the split-pane experience? Convenient but
|
session to get the split-pane experience? Convenient but
|
||||||
surprising (`./cli.py dashboard` shouldn't silently
|
surprising (`bot-bottle dashboard` shouldn't silently
|
||||||
change what session you're in). v1 leaves this off —
|
change what session you're in). v1 leaves this off —
|
||||||
operators who want split-pane mode start tmux themselves
|
operators who want split-pane mode start tmux themselves
|
||||||
and then run the dashboard.
|
and then run the dashboard.
|
||||||
@@ -332,7 +332,7 @@ Sized small.
|
|||||||
PRD-0019 focus indicator?
|
PRD-0019 focus indicator?
|
||||||
|
|
||||||
6. **Concurrent dashboards in different tmux windows.**
|
6. **Concurrent dashboards in different tmux windows.**
|
||||||
Multiple `./cli.py dashboard` invocations in different
|
Multiple `bot-bottle dashboard` invocations in different
|
||||||
tmux windows would each create their own right pane —
|
tmux windows would each create their own right pane —
|
||||||
probably fine, each has its own state, but worth
|
probably fine, each has its own state, but worth
|
||||||
verifying that `tmux list-panes` is scoped to the right
|
verifying that `tmux list-panes` is scoped to the right
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Today bot-bottle is hard-wired around Claude Code assumptions. When Claude runs
|
|||||||
|
|
||||||
## Goals / Success Criteria
|
## Goals / Success Criteria
|
||||||
|
|
||||||
- A Codex agent can be started from the dashboard and via `./cli.py start` alongside a Claude agent.
|
- A Codex agent can be started from the dashboard and via `bot-bottle start` alongside a Claude agent.
|
||||||
- The manifest can express the agent provider/template and, where needed, a custom agent Dockerfile.
|
- The manifest can express the agent provider/template and, where needed, a custom agent Dockerfile.
|
||||||
- Claude-specific default egress/auth behavior is no longer implicit; provider-specific auth is expressed through explicit bottle egress routes and roles.
|
- Claude-specific default egress/auth behavior is no longer implicit; provider-specific auth is expressed through explicit bottle egress routes and roles.
|
||||||
- The launcher preserves required infrastructure behavior for sidecars, egress, pipelock, supervisor MCP, CA handling, git, and shell basics.
|
- The launcher preserves required infrastructure behavior for sidecars, egress, pipelock, supervisor MCP, CA handling, git, and shell basics.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ across every bottle spin-up. This has several consequences:
|
|||||||
to grant that access.
|
to grant that access.
|
||||||
- **Manual rotation burden.** Operators must manage key files on disk, keeping
|
- **Manual rotation burden.** Operators must manage key files on disk, keeping
|
||||||
them secure, rotating them on a schedule, and distributing them across hosts
|
them secure, rotating them on a schedule, and distributing them across hosts
|
||||||
that run `./cli.py start`.
|
that run `bot-bottle start`.
|
||||||
|
|
||||||
## Goals / Success Criteria
|
## Goals / Success Criteria
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
The `./cli.py dashboard` command has grown from its PRD 0013 roots
|
The `bot-bottle dashboard` command has grown from its PRD 0013 roots
|
||||||
(triage supervise proposals) into a parallel-agent control surface
|
(triage supervise proposals) into a parallel-agent control surface
|
||||||
(PRDs 0019/0020/0021): an active-agents pane, agent picker + start,
|
(PRDs 0019/0020/0021): an active-agents pane, agent picker + start,
|
||||||
re-attach, per-bottle stop, tmux split-pane handoff, operator-
|
re-attach, per-bottle stop, tmux split-pane handoff, operator-
|
||||||
@@ -21,7 +21,7 @@ proposals, approve / modify / reject each one, write audit entries,
|
|||||||
deliver the response that unblocks the agent's tool call. Everything
|
deliver the response that unblocks the agent's tool call. Everything
|
||||||
that's about *starting / re-entering / stopping* bottles, or about
|
that's about *starting / re-entering / stopping* bottles, or about
|
||||||
*operator-initiated* config edits, comes out. The command is renamed
|
*operator-initiated* config edits, comes out. The command is renamed
|
||||||
`./cli.py supervise` so the name matches what it does after the cut.
|
`bot-bottle supervise` so the name matches what it does after the cut.
|
||||||
|
|
||||||
Future agent-management UX is explicitly punted: if and when a
|
Future agent-management UX is explicitly punted: if and when a
|
||||||
control surface for parallel agents resurfaces, the working
|
control surface for parallel agents resurfaces, the working
|
||||||
@@ -41,9 +41,9 @@ Three concrete pains, all downstream of the dashboard's growth:
|
|||||||
ExitStack-free bottle ownership are intricate enough that
|
ExitStack-free bottle ownership are intricate enough that
|
||||||
shipping the next polish increment costs more than it returns.
|
shipping the next polish increment costs more than it returns.
|
||||||
2. **No clear ownership of "starts and stops bottles".** Today
|
2. **No clear ownership of "starts and stops bottles".** Today
|
||||||
that responsibility is split: `./cli.py start` owns one-shot
|
that responsibility is split: `bot-bottle start` owns one-shot
|
||||||
sessions; the dashboard owns multi-session bottles it started
|
sessions; the dashboard owns multi-session bottles it started
|
||||||
itself; `./cli.py cleanup` owns everything else. The dashboard
|
itself; `bot-bottle cleanup` owns everything else. The dashboard
|
||||||
tracking its own `bottles: dict[str, (cm, bottle, identity)]`
|
tracking its own `bottles: dict[str, (cm, bottle, identity)]`
|
||||||
that doesn't survive a quit is a confusing third lane.
|
that doesn't survive a quit is a confusing third lane.
|
||||||
3. **Wrong target shape for a "manage many agents" UI.** The
|
3. **Wrong target shape for a "manage many agents" UI.** The
|
||||||
@@ -97,12 +97,12 @@ problem is everything that got bolted onto that core after.
|
|||||||
dashboard. After this PRD they don't exist anywhere — operators
|
dashboard. After this PRD they don't exist anywhere — operators
|
||||||
who need ad-hoc edits use the same path the agents do (call the
|
who need ad-hoc edits use the same path the agents do (call the
|
||||||
supervise tool from inside the bottle) or hand-edit the host-
|
supervise tool from inside the bottle) or hand-edit the host-
|
||||||
side files and restart the sidecar. Adding a `./cli.py routes
|
side files and restart the sidecar. Adding a `bot-bottle routes
|
||||||
edit <slug>` verb is a follow-up if the loss bites.
|
edit <slug>` verb is a follow-up if the loss bites.
|
||||||
- **Removing `./cli.py start` or changing its semantics.** Start
|
- **Removing `bot-bottle start` or changing its semantics.** Start
|
||||||
remains the one-shot launch path. PRD 0020's bottle-outlives-
|
remains the one-shot launch path. PRD 0020's bottle-outlives-
|
||||||
process model is removed; the only path to a long-running
|
process model is removed; the only path to a long-running
|
||||||
bottle is `./cli.py start` (foreground) plus `cli.py cleanup`
|
bottle is `bot-bottle start` (foreground) plus `cli.py cleanup`
|
||||||
for teardown.
|
for teardown.
|
||||||
- **Removing the supervise-sidecar protocol or any of the three
|
- **Removing the supervise-sidecar protocol or any of the three
|
||||||
block-remediation engines.** PRDs 0013–0016 stay Active. The
|
block-remediation engines.** PRDs 0013–0016 stay Active. The
|
||||||
@@ -122,8 +122,8 @@ problem is everything that got bolted onto that core after.
|
|||||||
|
|
||||||
### In scope
|
### In scope
|
||||||
|
|
||||||
- **Rename the subcommand.** `./cli.py dashboard` becomes
|
- **Rename the subcommand.** `bot-bottle dashboard` becomes
|
||||||
`./cli.py supervise`. The module moves from `bot_bottle/cli/
|
`bot-bottle supervise`. The module moves from `bot_bottle/cli/
|
||||||
dashboard.py` to `bot_bottle/cli/supervise.py`. The dispatcher
|
dashboard.py` to `bot_bottle/cli/supervise.py`. The dispatcher
|
||||||
in `bot_bottle/cli/__init__.py` and the help text both update.
|
in `bot_bottle/cli/__init__.py` and the help text both update.
|
||||||
- **Strip the curses loop to proposal-only.** The remaining
|
- **Strip the curses loop to proposal-only.** The remaining
|
||||||
@@ -167,7 +167,7 @@ problem is everything that got bolted onto that core after.
|
|||||||
|
|
||||||
- Any new feature in the supervise TUI. The cut is purely
|
- Any new feature in the supervise TUI. The cut is purely
|
||||||
subtractive (except for the rename).
|
subtractive (except for the rename).
|
||||||
- Behavior changes in `./cli.py start`, `cli.py cleanup`,
|
- Behavior changes in `bot-bottle start`, `cli.py cleanup`,
|
||||||
`cli.py resume`, `cli.py list`, `cli.py info`, `cli.py edit`,
|
`cli.py resume`, `cli.py list`, `cli.py info`, `cli.py edit`,
|
||||||
`cli.py init` — unchanged.
|
`cli.py init` — unchanged.
|
||||||
- Changes to the supervise sidecar (`supervise_server.py`,
|
- Changes to the supervise sidecar (`supervise_server.py`,
|
||||||
@@ -181,7 +181,7 @@ problem is everything that got bolted onto that core after.
|
|||||||
|
|
||||||
### Final shape of the TUI
|
### Final shape of the TUI
|
||||||
|
|
||||||
After this PRD the `./cli.py supervise` curses surface is:
|
After this PRD the `bot-bottle supervise` curses surface is:
|
||||||
|
|
||||||
```
|
```
|
||||||
bot-bottle supervise (3 pending)
|
bot-bottle supervise (3 pending)
|
||||||
@@ -307,8 +307,8 @@ The PR closes issue #174.
|
|||||||
|
|
||||||
1. **`e` / `p` operator-initiated edits — gone for good or
|
1. **`e` / `p` operator-initiated edits — gone for good or
|
||||||
moved to a separate CLI verb?** The PRD removes them with no
|
moved to a separate CLI verb?** The PRD removes them with no
|
||||||
replacement. The simplest replacement is `./cli.py routes
|
replacement. The simplest replacement is `bot-bottle routes
|
||||||
edit <slug>` and `./cli.py pipelock edit <slug>`, sharing
|
edit <slug>` and `bot-bottle pipelock edit <slug>`, sharing
|
||||||
the existing `apply_routes_change` / `apply_allowlist_change`
|
the existing `apply_routes_change` / `apply_allowlist_change`
|
||||||
engines. If the loss is felt within the first parallel
|
engines. If the loss is felt within the first parallel
|
||||||
run after this lands, that follow-up is a small PR. Leaving
|
run after this lands, that follow-up is a small PR. Leaving
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
When `./cli.py start` is run without an agent name, or without a backend
|
When `bot-bottle start` is run without an agent name, or without a backend
|
||||||
explicitly specified, the user currently gets an argparse error (missing
|
explicitly specified, the user currently gets an argparse error (missing
|
||||||
positional) or falls through to the `docker` default silently. This PRD
|
positional) or falls through to the `docker` default silently. This PRD
|
||||||
adds a terminal UI that appears in those gaps: a filter-select screen
|
adds a terminal UI that appears in those gaps: a filter-select screen
|
||||||
built with `curses` that lets the operator pick the agent and/or backend
|
built with `curses` that lets the operator pick the agent and/or backend
|
||||||
interactively rather than memorising names or consulting `./cli.py list`.
|
interactively rather than memorising names or consulting `bot-bottle list`.
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
@@ -29,15 +29,15 @@ visible.
|
|||||||
|
|
||||||
## Goals / Success Criteria
|
## Goals / Success Criteria
|
||||||
|
|
||||||
1. `./cli.py start` (no arguments) shows an interactive agent selector;
|
1. `bot-bottle start` (no arguments) shows an interactive agent selector;
|
||||||
the selected name is used exactly as if it had been passed on the
|
the selected name is used exactly as if it had been passed on the
|
||||||
command line.
|
command line.
|
||||||
2. `./cli.py start <name>` (no `--backend`, no `BOT_BOTTLE_BACKEND`)
|
2. `bot-bottle start <name>` (no `--backend`, no `BOT_BOTTLE_BACKEND`)
|
||||||
shows an interactive backend selector; the selected backend is used
|
shows an interactive backend selector; the selected backend is used
|
||||||
exactly as if `--backend=<selected>` had been passed.
|
exactly as if `--backend=<selected>` had been passed.
|
||||||
3. `./cli.py start <name> --backend=<b>` (both explicit) shows neither
|
3. `bot-bottle start <name> --backend=<b>` (both explicit) shows neither
|
||||||
screen — no behavioural change from today.
|
screen — no behavioural change from today.
|
||||||
4. `./cli.py start` (no arguments, no env backend) shows the agent
|
4. `bot-bottle start` (no arguments, no env backend) shows the agent
|
||||||
selector first, then the backend selector.
|
selector first, then the backend selector.
|
||||||
5. The filter-select widget is a standalone utility
|
5. The filter-select widget is a standalone utility
|
||||||
(`bot_bottle/cli/tui.py`) shared by both selectors.
|
(`bot_bottle/cli/tui.py`) shared by both selectors.
|
||||||
@@ -57,7 +57,7 @@ visible.
|
|||||||
- No pagination beyond what fits in the terminal window (scroll via
|
- No pagination beyond what fits in the terminal window (scroll via
|
||||||
cursor movement is sufficient for typical agent counts).
|
cursor movement is sufficient for typical agent counts).
|
||||||
- No multi-select; exactly one item is chosen per invocation.
|
- No multi-select; exactly one item is chosen per invocation.
|
||||||
- No changes to `./cli.py resume`, `./cli.py list`, or any other
|
- No changes to `bot-bottle resume`, `bot-bottle list`, or any other
|
||||||
subcommand.
|
subcommand.
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
@@ -83,7 +83,7 @@ def filter_select(
|
|||||||
|
|
||||||
The widget renders to the tty file descriptor opened via `curses.initscr`
|
The widget renders to the tty file descriptor opened via `curses.initscr`
|
||||||
(or `curses.newterm` on the tty fd so stdout remains clean for callers
|
(or `curses.newterm` on the tty fd so stdout remains clean for callers
|
||||||
that pipe `./cli.py`).
|
that pipe `bot-bottle`).
|
||||||
|
|
||||||
Layout (full-width, minimal):
|
Layout (full-width, minimal):
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ agent picker can populate itself from the real manifest. The same
|
|||||||
`filter_select` opens `/dev/tty` and feeds it as the input file to
|
`filter_select` opens `/dev/tty` and feeds it as the input file to
|
||||||
`curses.wrapper`-equivalent code (using `curses.newterm` to avoid
|
`curses.wrapper`-equivalent code (using `curses.newterm` to avoid
|
||||||
clobbering the caller's stdout/stderr). This keeps the picker
|
clobbering the caller's stdout/stderr). This keeps the picker
|
||||||
composable — callers can pipe `./cli.py` output without the curses
|
composable — callers can pipe `bot-bottle` output without the curses
|
||||||
draw sequences contaminating the pipe.
|
draw sequences contaminating the pipe.
|
||||||
|
|
||||||
## Implementation chunks
|
## Implementation chunks
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ snapshot before a planned host reboot or hardware migration.
|
|||||||
|
|
||||||
## Goals / Success Criteria
|
## Goals / Success Criteria
|
||||||
|
|
||||||
- `./cli.py commit [<slug>]` takes a snapshot of the running agent and
|
- `bot-bottle commit [<slug>]` takes a snapshot of the running agent and
|
||||||
stores it as a local artifact.
|
stores it as a local artifact.
|
||||||
- Without a slug argument the command shows the same interactive picker
|
- Without a slug argument the command shows the same interactive picker
|
||||||
as `start` (the list of active slugs).
|
as `start` (the list of active slugs).
|
||||||
- The committed artifact reference is stored in per-bottle state so
|
- The committed artifact reference is stored in per-bottle state so
|
||||||
that the next `./cli.py resume <slug>` automatically uses the
|
that the next `bot-bottle resume <slug>` automatically uses the
|
||||||
snapshot instead of rebuilding from the Dockerfile.
|
snapshot instead of rebuilding from the Dockerfile.
|
||||||
- `mark_preserved` is called so the state dir survives the normal
|
- `mark_preserved` is called so the state dir survives the normal
|
||||||
session-end cleanup.
|
session-end cleanup.
|
||||||
@@ -81,7 +81,7 @@ to the committed `.smolmachine` artifact.
|
|||||||
### `commit` command
|
### `commit` command
|
||||||
|
|
||||||
```
|
```
|
||||||
./cli.py commit [<slug>]
|
bot-bottle commit [<slug>]
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Resolve slug (arg or interactive picker from `enumerate_active_agents`).
|
1. Resolve slug (arg or interactive picker from `enumerate_active_agents`).
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ egress policy), the operator must duplicate the agent file and change the
|
|||||||
selection order, as the effective bottle for the session.
|
selection order, as the effective bottle for the session.
|
||||||
5. Confirming with an empty selection falls back to the agent's `bottle:` field.
|
5. Confirming with an empty selection falls back to the agent's `bottle:` field.
|
||||||
If neither is set, a ManifestError is raised pointing the operator at the fix.
|
If neither is set, a ManifestError is raised pointing the operator at the fix.
|
||||||
6. The ordered bottle list is stored in launch metadata so `./cli.py resume`
|
6. The ordered bottle list is stored in launch metadata so `bot-bottle resume`
|
||||||
uses the same bottles.
|
uses the same bottles.
|
||||||
7. The preflight summary (`y/N` screen) shows the effective bottle name(s).
|
7. The preflight summary (`y/N` screen) shows the effective bottle name(s).
|
||||||
8. The multi-select picker supports incremental filtering, Space/Enter to toggle
|
8. The multi-select picker supports incremental filtering, Space/Enter to toggle
|
||||||
@@ -52,7 +52,7 @@ egress policy), the operator must duplicate the agent file and change the
|
|||||||
- Reordering the selection list from within the picker (order = insertion order;
|
- Reordering the selection list from within the picker (order = insertion order;
|
||||||
drag-and-drop is out of scope).
|
drag-and-drop is out of scope).
|
||||||
- Storing bottle selection history / MRU.
|
- Storing bottle selection history / MRU.
|
||||||
- Changes to `./cli.py edit`, `./cli.py list`, or `./cli.py info`.
|
- Changes to `bot-bottle edit`, `bot-bottle list`, or `bot-bottle info`.
|
||||||
- Removing the `bottle:` key from the agent schema (it stays, now optional).
|
- Removing the `bottle:` key from the agent schema (it stays, now optional).
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ macOS-only for v1. Three concrete blockers:
|
|||||||
|
|
||||||
## Goals / Success Criteria
|
## Goals / Success Criteria
|
||||||
|
|
||||||
- `BOT_BOTTLE_BACKEND=smolmachines ./cli.py start <agent>` launches,
|
- `BOT_BOTTLE_BACKEND=smolmachines bot-bottle start <agent>` launches,
|
||||||
runs, and tears down a bottle on a Linux host with `/dev/kvm`.
|
runs, and tears down a bottle on a Linux host with `/dev/kvm`.
|
||||||
- The TSI allowlist is enforced on Linux: PRD 0022's
|
- The TSI allowlist is enforced on Linux: PRD 0022's
|
||||||
`tests/integration/test_sandbox_escape.py` passes against
|
`tests/integration/test_sandbox_escape.py` passes against
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ end-to-end runner that would catch the *next* macOS-only launch regression.)
|
|||||||
PR #470 (#414) already made the integration suite backend-agnostic:
|
PR #470 (#414) already made the integration suite backend-agnostic:
|
||||||
`skip_unless_selected_backend_available()` gates on the *selected* backend's
|
`skip_unless_selected_backend_available()` gates on the *selected* backend's
|
||||||
own `is_backend_ready()` rather than `docker_available()`, and each
|
own `is_backend_ready()` rather than `docker_available()`, and each
|
||||||
integration job runs `./cli.py backend status --backend=<name>` as a preflight
|
integration job runs `bot-bottle backend status --backend=<name>` as a preflight
|
||||||
that fails loudly when the backend is missing. That is the machinery this job
|
that fails loudly when the backend is missing. That is the machinery this job
|
||||||
plugs into; this PRD supplies the runner and the job.
|
plugs into; this PRD supplies the runner and the job.
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ Modeled on `integration-firecracker`:
|
|||||||
- `concurrency: { group: integration-macos-infra, cancel-in-progress: false }`
|
- `concurrency: { group: integration-macos-infra, cancel-in-progress: false }`
|
||||||
to serialize runs against the singleton.
|
to serialize runs against the singleton.
|
||||||
- **Preflight** — `command -v container`, `container system status`, then
|
- **Preflight** — `command -v container`, `container system status`, then
|
||||||
`./cli.py backend status --backend=macos-container`; any failure exits
|
`bot-bottle backend status --backend=macos-container`; any failure exits
|
||||||
non-zero so a misprovisioned runner fails loudly instead of silently
|
non-zero so a misprovisioned runner fails loudly instead of silently
|
||||||
skipping.
|
skipping.
|
||||||
- Run the integration suite under coverage with
|
- Run the integration suite under coverage with
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ verifies host prerequisites after install.
|
|||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
There is currently no install path for new users. The only way to run
|
There is currently no install path for new users. The only way to run
|
||||||
bot-bottle is to clone the repo and invoke `./cli.py`. This blocks any
|
bot-bottle is to clone the repo and invoke `bot-bottle`. This blocks any
|
||||||
public demo: readers want `curl | sh` or `pipx install`, not a manual
|
public demo: readers want `curl | sh` or `pipx install`, not a manual
|
||||||
clone-and-configure flow. There is also no single command that tells a
|
clone-and-configure flow. There is also no single command that tells a
|
||||||
user whether their host is actually ready to run a bottle.
|
user whether their host is actually ready to run a bottle.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Can bot-bottle grow a built-in supervisor — TUI inventory plus PR-feedback rou
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
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.
|
bot-bottle today is a fleet *executor*: `bot-bottle 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 bot-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.
|
||||||
|
|
||||||
@@ -25,13 +25,13 @@ A supervisor doesn't have to be heavy. A TUI built into the existing Python CLI,
|
|||||||
|
|
||||||
Three layers, each independently useful, in order of ambition:
|
Three layers, each independently useful, in order of ambition:
|
||||||
|
|
||||||
### 1. `./cli.py status` — read-only inventory
|
### 1. `bot-bottle status` — read-only inventory
|
||||||
|
|
||||||
Reads `docker ps` filtered by a bottle label and tails each bottle's session log. Reports per bottle: name, agent, uptime, last-activity timestamp, token spend if available, associated PR/branch if recorded.
|
Reads `docker ps` filtered by a bottle label and tails each bottle's session log. Reports per bottle: name, agent, uptime, last-activity timestamp, token spend if available, associated PR/branch if recorded.
|
||||||
|
|
||||||
No new daemons. No new ports. No new credentials. ~100 lines.
|
No new daemons. No new ports. No new credentials. ~100 lines.
|
||||||
|
|
||||||
### 2. `./cli.py watch` — TUI over the same data
|
### 2. `bot-bottle watch` — TUI over the same data
|
||||||
|
|
||||||
Same data as `status`, rendered with auto-refresh and keyboard shortcuts that shell out to the existing `cli.py attach / stop / start` commands.
|
Same data as `status`, rendered with auto-refresh and keyboard shortcuts that shell out to the existing `cli.py attach / stop / start` commands.
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Library choice: prefer the stdlib `curses` module to stay stdlib-first; fall bac
|
|||||||
|
|
||||||
This is the Claude Squad / tmux-agent-status pattern, applied to bottles instead of tmux sessions. The whole category exists *because* a TUI is the lightweight shape that doesn't require what the SPA tier requires.
|
This is the Claude Squad / tmux-agent-status pattern, applied to bottles instead of tmux sessions. The whole category exists *because* a TUI is the lightweight shape that doesn't require what the SPA tier requires.
|
||||||
|
|
||||||
### 3. `./cli.py supervise` — PR feedback router
|
### 3. `bot-bottle supervise` — PR feedback router
|
||||||
|
|
||||||
The optional, more ambitious layer. The bottle manifest gains an optional field:
|
The optional, more ambitious layer. The bottle manifest gains an optional field:
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ pr_watch:
|
|||||||
branch: agent/task-42
|
branch: agent/task-42
|
||||||
```
|
```
|
||||||
|
|
||||||
`./cli.py supervise` polls the named upstream for new review comments and CI failures on `branch`. When one fires, it surfaces as a desktop notification or a flash in the TUI. The human decides what to do with the feedback — there is no autonomous loop that feeds the comment back into a bottle's next prompt (see "Where to be conservative" for why).
|
`bot-bottle supervise` polls the named upstream for new review comments and CI failures on `branch`. When one fires, it surfaces as a desktop notification or a flash in the TUI. The human decides what to do with the feedback — there is no autonomous loop that feeds the comment back into a bottle's next prompt (see "Where to be conservative" for why).
|
||||||
|
|
||||||
The polling token is a **host** token (the same `GH_PAT` / Gitea token the host already keeps in shell env), not a bottle credential. The supervisor never holds bottle secrets.
|
The polling token is a **host** token (the same `GH_PAT` / Gitea token the host already keeps in shell env), not a bottle credential. The supervisor never holds bottle secrets.
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ The load-bearing question is whether the supervisor introduces the privileged-ch
|
|||||||
| Reaching into running bottles | Supervisor reads `docker ps` and host-side log files. The host already sees both — Docker is the trust boundary, the supervisor is on the host side of it. |
|
| Reaching into running bottles | Supervisor reads `docker ps` and host-side log files. The host already sees both — Docker is the trust boundary, the supervisor is on the host side of it. |
|
||||||
| Holding bottle credentials | The polling token is a host token. The supervisor never receives `bottle.cred_proxy.routes` entries; it has no path to them. |
|
| Holding bottle credentials | The polling token is a host token. The supervisor never receives `bottle.cred_proxy.routes` entries; it has no path to them. |
|
||||||
| Bridging between bottles | The supervisor does not relay state from bottle A to bottle B. It relays *upstream PR state* to a bottle's next prompt — and only if the manifest opts in. |
|
| Bridging between bottles | The supervisor does not relay state from bottle A to bottle B. It relays *upstream PR state* to a bottle's next prompt — and only if the manifest opts in. |
|
||||||
| New attack surface | All "control" actions go through `./cli.py start <agent>`, which already enforces the manifest. The supervisor is an automated caller of the existing CLI, not a parallel control plane. |
|
| New attack surface | All "control" actions go through `bot-bottle start <agent>`, which already enforces the manifest. The supervisor is an automated caller of the existing CLI, not a parallel control plane. |
|
||||||
|
|
||||||
The boundary stays at the bottle wall. The supervisor looks outward at git/PR state and downward at Docker; it does not look *inward* through pipelock.
|
The boundary stays at the bottle wall. The supervisor looks outward at git/PR state and downward at Docker; it does not look *inward* through pipelock.
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ What's the cheapest path to that, and where does it bottom out?
|
|||||||
|
|
||||||
## What "interact" means
|
## What "interact" means
|
||||||
|
|
||||||
Today the flow is bimodal. `./cli.py start <agent>` brings the
|
Today the flow is bimodal. `bot-bottle start <agent>` brings the
|
||||||
bottle up and immediately drops you into an interactive
|
bottle up and immediately drops you into an interactive
|
||||||
`docker exec -it bot-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
|
owns the whole terminal until you Ctrl-D out, at which point the
|
||||||
bottle tears down. The dashboard (`./cli.py dashboard`) is a
|
bottle tears down. The dashboard (`bot-bottle dashboard`) is a
|
||||||
*separate* invocation that watches across bottles but never
|
*separate* invocation that watches across bottles but never
|
||||||
exposes the claude TUI itself.
|
exposes the claude TUI itself.
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ What's not good:
|
|||||||
|
|
||||||
This is the v1 the project's existing code-shape strongly
|
This is the v1 the project's existing code-shape strongly
|
||||||
prefers. It clears the bar of "let me talk to claude-code
|
prefers. It clears the bar of "let me talk to claude-code
|
||||||
without quitting `./cli.py dashboard`."
|
without quitting `bot-bottle dashboard`."
|
||||||
|
|
||||||
## Option 2: Embedded emulator
|
## Option 2: Embedded emulator
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ dangerous changes before they left*.
|
|||||||
(`supervise_gitleaks_allow`, [`git_gate_render.py`](../../bot_bottle/git_gate_render.py)).
|
(`supervise_gitleaks_allow`, [`git_gate_render.py`](../../bot_bottle/git_gate_render.py)).
|
||||||
Extend the same flow to **high-risk file classes**: any commit touching
|
Extend the same flow to **high-risk file classes**: any commit touching
|
||||||
CI/build/deploy scripts, auth/crypto code, egress config, or
|
CI/build/deploy scripts, auth/crypto code, egress config, or
|
||||||
adding/changing dependencies → route to `./cli.py supervise`. This is
|
adding/changing dependencies → route to `bot-bottle supervise`. This is
|
||||||
attribution/policy, not detection, and it's the strongest thing here —
|
attribution/policy, not detection, and it's the strongest thing here —
|
||||||
a human on exactly the temporal-escape surfaces.
|
a human on exactly the temporal-escape surfaces.
|
||||||
4. **LLM semantic diff-review — the behavioral backstop.** The only
|
4. **LLM semantic diff-review — the behavioral backstop.** The only
|
||||||
@@ -146,7 +146,7 @@ preserves the bottom-up distribution funnel.
|
|||||||
**governed code-egress review**, not "we resell inference" (the
|
**governed code-egress review**, not "we resell inference" (the
|
||||||
monetization notes explicitly warn against reselling compute).
|
monetization notes explicitly warn against reselling compute).
|
||||||
- **The web-console supervise/review flow — the strongest anchor.** Turn
|
- **The web-console supervise/review flow — the strongest anchor.** Turn
|
||||||
the CLI `./cli.py supervise` approval into a real review surface:
|
the CLI `bot-bottle supervise` approval into a real review surface:
|
||||||
rendered diff + finding context, approve/reject, **who-approved audit
|
rendered diff + finding context, approve/reject, **who-approved audit
|
||||||
trail, RBAC on approvers, mobile/phone-control** (ties to the
|
trail, RBAC on approvers, mobile/phone-control** (ties to the
|
||||||
dashboard/vault north star). This is "central enforcement +
|
dashboard/vault north star). This is "central enforcement +
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ resolver globs each directory.
|
|||||||
becomes file ops (mkdir, mv, rm) instead of editing one file.
|
becomes file ops (mkdir, mv, rm) instead of editing one file.
|
||||||
Power users prefer that; new users may not.
|
Power users prefer that; new users may not.
|
||||||
- Discovery requires `ls`, not "grep one file." Tooling helps
|
- Discovery requires `ls`, not "grep one file." Tooling helps
|
||||||
(e.g. `./cli.py list`) but the manifest is no longer a single
|
(e.g. `bot-bottle list`) but the manifest is no longer a single
|
||||||
artifact to email or ship.
|
artifact to email or ship.
|
||||||
- Atomicity: swapping a bottle name across agents touches
|
- Atomicity: swapping a bottle name across agents touches
|
||||||
multiple files. Git handles this fine; a one-shot text editor
|
multiple files. Git handles this fine; a one-shot text editor
|
||||||
@@ -364,7 +364,7 @@ wins; none of the body-prose or dependency story.
|
|||||||
warns / ignores / breaks. If it warns, we'd want a different
|
warns / ignores / breaks. If it warns, we'd want a different
|
||||||
field name (e.g. `bot-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
|
- **Migration story.** Is the project willing to ship a one-shot
|
||||||
`./cli.py migrate-manifest` command that does the JSON → MD
|
`bot-bottle migrate-manifest` command that does the JSON → MD
|
||||||
conversion? Or do users just rewrite by hand from the new docs?
|
conversion? Or do users just rewrite by hand from the new docs?
|
||||||
- **Bottle file body content.** If most bottle .md files have an
|
- **Bottle file body content.** If most bottle .md files have an
|
||||||
empty body, is the MD-with-frontmatter format still warranted?
|
empty body, is the MD-with-frontmatter format still warranted?
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ on top of working onboarding.
|
|||||||
|
|
||||||
A first-time user today goes through five steps: install Docker,
|
A first-time user today goes through five steps: install Docker,
|
||||||
install `uv`, set `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`, write
|
install `uv`, set `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`, write
|
||||||
`bot-bottle.json`, run `./cli.py start`. One of those is
|
`bot-bottle.json`, run `bot-bottle start`. One of those is
|
||||||
"author a JSON manifest." Polished tools in this category let
|
"author a JSON manifest." Polished tools in this category let
|
||||||
users skip that step on day one. The fix is an `init` subcommand
|
users skip that step on day one. The fix is an `init` subcommand
|
||||||
that drops a working `bot-bottle.json` with a default `coder`
|
that drops a working `bot-bottle.json` with a default `coder`
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ The minimum-viable workflow, no bot-bottle code changes:
|
|||||||
3. SSH in.
|
3. SSH in.
|
||||||
4. `git clone` bot-bottle on the VM, drop a manifest in place,
|
4. `git clone` bot-bottle on the VM, drop a manifest in place,
|
||||||
inject `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` via the provider's secrets path.
|
inject `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN` via the provider's secrets path.
|
||||||
5. `./cli.py start <agent>` — the existing launcher handles the rest.
|
5. `bot-bottle start <agent>` — the existing launcher handles the rest.
|
||||||
6. On exit: destroy the VM. No host artifacts persist.
|
6. On exit: destroy the VM. No host artifacts persist.
|
||||||
|
|
||||||
For the "VPN pivot" failure mode, see
|
For the "VPN pivot" failure mode, see
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#
|
#
|
||||||
# Why a pool + one-time setup: creating a TAP and assigning it an IP
|
# Why a pool + one-time setup: creating a TAP and assigning it an IP
|
||||||
# needs CAP_NET_ADMIN. Pre-creating user-owned, pre-addressed TAPs
|
# needs CAP_NET_ADMIN. Pre-creating user-owned, pre-addressed TAPs
|
||||||
# means `./cli.py start` never needs root. The nft table is static
|
# means `bot-bottle start` never needs root. The nft table is static
|
||||||
# (keyed on the `bbfc*` interface wildcard), so it covers every slot
|
# (keyed on the `bbfc*` interface wildcard), so it covers every slot
|
||||||
# without per-launch changes.
|
# without per-launch changes.
|
||||||
#
|
#
|
||||||
|
|||||||
+2
-2
@@ -95,7 +95,7 @@ BOT_BOTTLE_RUN_CANARIES=1 python -m scripts.unittest_gate \
|
|||||||
```
|
```
|
||||||
4. Skip guards live in `tests._backend` and gate on the backend's own
|
4. Skip guards live in `tests._backend` and gate on the backend's own
|
||||||
readiness check, `bot_bottle.backend.has_backend` — the same probe
|
readiness check, `bot_bottle.backend.has_backend` — the same probe
|
||||||
behind `./cli.py backend status`:
|
behind `bot-bottle backend status`:
|
||||||
- Backend-agnostic tests (go through `get_bottle_backend()`) decorate
|
- Backend-agnostic tests (go through `get_bottle_backend()`) decorate
|
||||||
the class with `@skip_unless_selected_backend_available()` — the test
|
the class with `@skip_unless_selected_backend_available()` — the test
|
||||||
runs against whichever backend `BOT_BOTTLE_BACKEND` selects and skips
|
runs against whichever backend `BOT_BOTTLE_BACKEND` selects and skips
|
||||||
@@ -105,7 +105,7 @@ BOT_BOTTLE_RUN_CANARIES=1 python -m scripts.unittest_gate \
|
|||||||
`backend.docker.*`, …) decorate with `@skip_unless_backend("docker")`
|
`backend.docker.*`, …) decorate with `@skip_unless_backend("docker")`
|
||||||
so they no-op under a run targeting a different backend.
|
so they no-op under a run targeting a different backend.
|
||||||
|
|
||||||
Each CI integration job runs `./cli.py backend status --backend=<name>`
|
Each CI integration job runs `bot-bottle backend status --backend=<name>`
|
||||||
as a preflight, which prints a clear per-check summary and exits non-zero
|
as a preflight, which prints a clear per-check summary and exits non-zero
|
||||||
when the backend is missing — so absent infrastructure fails the job
|
when the backend is missing — so absent infrastructure fails the job
|
||||||
instead of hiding among per-test `unittest.skip` lines.
|
instead of hiding among per-test `unittest.skip` lines.
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Each integration test targets the backend named by ``BOT_BOTTLE_BACKEND``
|
Each integration test targets the backend named by ``BOT_BOTTLE_BACKEND``
|
||||||
(default ``docker``) and gates on that backend's full readiness check —
|
(default ``docker``) and gates on that backend's full readiness check —
|
||||||
``is_backend_ready()`` (equivalent to ``./cli.py backend status``), not just
|
``is_backend_ready()`` (equivalent to ``bot-bottle backend status``), not just
|
||||||
a binary-on-PATH probe. When the backend is not ready, diagnostic output is
|
a binary-on-PATH probe. When the backend is not ready, diagnostic output is
|
||||||
printed during test discovery so the operator sees a concrete reason for each
|
printed during test discovery so the operator sees a concrete reason for each
|
||||||
skip.
|
skip.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Tests for the backend-aware skip guards in ``tests/_backend.py``.
|
"""Tests for the backend-aware skip guards in ``tests/_backend.py``.
|
||||||
|
|
||||||
The guards delegate their readiness check to
|
The guards delegate their readiness check to
|
||||||
``bot_bottle.backend.is_backend_ready`` (the probe behind ``./cli.py backend
|
``bot_bottle.backend.is_backend_ready`` (the probe behind ``bot-bottle backend
|
||||||
status``); here that probe is mocked so the unit job asserts the
|
status``); here that probe is mocked so the unit job asserts the
|
||||||
selection/skip logic without either backend present on the runner.
|
selection/skip logic without either backend present on the runner.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Unit: the generic `backend` CLI command.
|
"""Unit: the generic `backend` CLI command.
|
||||||
|
|
||||||
`./cli.py backend {setup,status} [--backend=NAME]` resolves a backend
|
`bot-bottle backend {setup,status} [--backend=NAME]` resolves a backend
|
||||||
and dispatches to its `setup()` / `status()` classmethods — no
|
and dispatches to its `setup()` / `status()` classmethods — no
|
||||||
backend-specific commands. Also covers the docker backend's own
|
backend-specific commands. Also covers the docker backend's own
|
||||||
setup/status logic (the firecracker path is exercised by
|
setup/status logic (the firecracker path is exercised by
|
||||||
|
|||||||
Reference in New Issue
Block a user