df469b2f47
Both fields were missing from the reference table added in the preceding commit — `role` is visible in examples/bottles/claude.md and `git.fetch` is documented in PRD 0052 but neither appeared in the README table.
162 lines
11 KiB
Markdown
162 lines
11 KiB
Markdown
<p align="center">
|
|
<img src="docs/logo.svg" alt="bot-bottle logo" width="140">
|
|
</p>
|
|
|
|
# bot-bottle
|
|
|
|
[](https://gitea.dideric.is/didericis/bot-bottle/actions?workflow=test.yml)
|
|
[](https://github.com/PyCQA/pylint)
|
|
[](https://github.com/microsoft/pyright)
|
|
|
|
**Problem:** Developer wants to run a coding agent without supervision, but they don't want a prompt injected or misbehaving agent wrecking their environment or exfiltrating sensitive data.
|
|
|
|
**Solution:** Ephemeral, per agent "bottles" the agent cannot modify that scan all traffic for data exfiltration and limit capabilities and egress to only what the agent needs.
|
|
|
|
## Features
|
|
|
|
- **Per-bottle egress allowlist** — TLS-bumped HTTP/HTTPS chokepoint with a per-manifest host allowlist; per-route path/method/header `matches` filtering; outbound DLP scanning for known tokens and secrets, inbound DLP scanning for prompt-injection attempts; DoH and arbitrary hosts blocked by default.
|
|
- **Tokens the agent never sees** — host secrets live in a sidecar; the agent dials `http://sidecar:9099/<path>` and the proxy strips inbound `Authorization` and injects the real token before forwarding. `printenv` in the agent shows proxy URLs only.
|
|
- **Gitleaks-scanned push (git-gate)** — `bottle.git` remotes route through a per-bottle `git daemon` that gitleaks-scans incoming refs pre-receive and forwards clean refs upstream over SSH. The agent never holds the upstream credential.
|
|
- **Manifest-scoped skills + secrets** — each bottle declares its skills, env, git identity, remotes, and egress routes; unknown keys die at load.
|
|
- **Trust boundary at `$HOME`** — bottles (credentials, egress, remotes) live only under `~/.bot-bottle/bottles/`. Repos may ship agents but not bottles, so a cloned repo can't redirect an env var to an attacker host.
|
|
- **Composable bottles (`extends:`)** — keep provider/runtime policy in one base bottle (e.g. `claude.md`) and overlay task bottles on top.
|
|
- **Parallel, isolated bottles** — each bottle runs in its own backend-owned isolation boundary; bottles don't share state or talk to each other.
|
|
- **Provider templates (Claude, Codex)** — `Dockerfile.claude` / `Dockerfile.codex`, or a bottle-supplied Dockerfile. Claude auth via long-lived OAuth token; Codex via opt-in host device-auth forwarding.
|
|
- **gVisor auto-detect** — on Linux hosts where `runsc` is registered with Docker, every bottle launches under it for a userspace syscall barrier; no manifest config required.
|
|
- **Apple Container backend (macOS default when available)** — runs the agent and sidecar bundle with Apple's `container` CLI, using a host-only agent network plus a separate sidecar egress network.
|
|
- **Smolmachines backend** — runs the agent in a libkrun micro-VM while the sidecar bundle stays in Docker. TSI and smolmachines DNS filtering close the raw DNS exfiltration gap that exists in the legacy Docker backend.
|
|
- **Legacy Docker backend** — still available for examples, CI, and hosts without Apple Container via `BOT_BOTTLE_BACKEND=docker` or `--backend=docker`.
|
|
|
|
## Architecture
|
|
|
|
On the default macOS Apple Container backend, a bottle is an agent container on a host-only internal network plus a sidecar bundle attached to both that internal network and a NAT egress network. The agent gets HTTP(S)_PROXY and CA bundle env vars pointing at the sidecar's internal-network IP, so HTTP/HTTPS traffic flows through the sidecar instead of direct egress. `bottle.git` / git-gate is intentionally deferred on this backend until a safe Apple Container key-delivery path exists.
|
|
|
|
On the smolmachines backend, a bottle is an agent micro-VM plus a Docker sidecar bundle for egress, git-gate, and supervise. The VM reaches the sidecars through a per-bottle loopback alias allowed by TSI; smolmachines handles DNS filtering below the guest OS.
|
|
|
|
On the legacy Docker backend, the same logical bottle is two containers per agent: an `agent` container and a `sidecars` container. They share a per-agent Docker `--internal` network; the agent has no default route off-box.
|
|
|
|
The Docker topology looks like this:
|
|
|
|
```
|
|
host ( ./cli.py )
|
|
│
|
|
starts │ stops
|
|
▼
|
|
┌─────────────────────────── bottle ──────────────────────────────────┐
|
|
│ │
|
|
│ ┌──────────────────┐ ┌──────────────────────┐ │
|
|
│ │ agent image │ HTTP(S) proxy │ egress image │ │
|
|
│ │ (claude-code, │ ─────────────────►│ (mitmproxy; TLS bump │ │ HTTPS to
|
|
│ │ codex, etc) │ │ DLP scan, path │───┼──► allowlisted
|
|
│ │ │ │ matching, auth │ │ hosts
|
|
│ │ environ: proxy │ │ injection) │ │
|
|
│ │ URLs only, no │ └──────────────────────┘ │
|
|
│ │ real tokens │ │
|
|
│ │ │ git proxy ┌────────────────┐ │ SSH push/fetch
|
|
│ │ │ ────────────────►│ git-gate image │──────────┼──► to bottle.git
|
|
│ │ │ │ (gitleaks + │ │ upstreams
|
|
│ └──────────────────┘ │ git daemon) │ │ (direct — not
|
|
│ └────────────────┘ │ via egress)
|
|
│ │
|
|
│ agent on internal network (no default route); egress and │
|
|
│ git-gate straddle internal + egress networks. │
|
|
│ egress is the single HTTP/HTTPS chokepoint — all agent HTTP/HTTPS │
|
|
│ traffic flows through it. git-gate's SSH egress is direct │
|
|
│ because egress is HTTP-only. │
|
|
└─────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
When the agent exits, `cli.py` tears down every sidecar and both networks; nothing about a bottle persists between runs.
|
|
|
|
## Quickstart
|
|
|
|
On compatible macOS hosts, the default backend requires Apple's `container` CLI and does not require Docker. The smolmachines backend requires Docker on the host for the sidecar bundle plus smolvm. The legacy Docker backend requires Docker. Claude bottles also need a long-lived Claude Code OAuth token (`claude setup-token`) exported as `BOT_BOTTLE_CLAUDE_OAUTH_TOKEN`.
|
|
|
|
Use `BOT_BOTTLE_BACKEND=docker ./cli.py start <agent>` on hosts where Apple Container is not installed and Docker is the desired backend.
|
|
|
|
```sh
|
|
./cli.py start <agent> # builds the image on first run, drops you into claude
|
|
```
|
|
|
|
## Manifest
|
|
|
|
Bottles and agents are Markdown files with YAML frontmatter under `~/.bot-bottle/`. The Markdown body is the system prompt. Bottles live in `~/.bot-bottle/bottles/`; agents may also be shipped by a repo at `<repo>/.bot-bottle/agents/<name>.md`.
|
|
|
|
**Bottle** (`~/.bot-bottle/bottles/gitea-dev.md`):
|
|
|
|
````markdown
|
|
---
|
|
extends: claude # inherit the Claude provider boundary
|
|
|
|
env:
|
|
GIT_AUTHOR_NAME: didericis
|
|
|
|
git:
|
|
user:
|
|
name: "Eric Bauerfeld"
|
|
email: "eric+claude@dideric.is"
|
|
remotes:
|
|
gitea.dideric.is:
|
|
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...
|
|
|
|
egress:
|
|
routes:
|
|
- host: gitea.dideric.is
|
|
auth:
|
|
scheme: token # Bearer | token
|
|
token_ref: BOT_BOTTLE_GITEA_TOKEN
|
|
matches: # optional — restrict to specific paths/methods/headers
|
|
- paths:
|
|
- {type: prefix, value: /api/v1/}
|
|
methods: [GET, POST, PATCH, DELETE]
|
|
dlp: # optional — per-route detector overrides (default: all on)
|
|
outbound_detectors: [token_patterns, known_secrets]
|
|
inbound_detectors: false # disable response scanning for this host
|
|
---
|
|
|
|
The `gitea-dev` bottle. Provider auth via the inherited Claude route;
|
|
gitea over SSH for push, token over HTTPS for the API.
|
|
````
|
|
|
|
**Agent** (`~/.bot-bottle/agents/gitea-helper.md`):
|
|
|
|
````markdown
|
|
---
|
|
bottle: gitea-dev
|
|
skills:
|
|
- init-prd
|
|
---
|
|
|
|
You help maintain Gitea-hosted projects.
|
|
````
|
|
|
|
**Egress route fields:**
|
|
|
|
| Field | Required | Description |
|
|
|---|---|---|
|
|
| `host` | yes | Hostname to allowlist. One entry per host. |
|
|
| `role` | no | Provider-specific role string (e.g. `claude_code_oauth`). Wires built-in auth flows; set by provider templates, not manually. |
|
|
| `auth.scheme` | when `auth` present | `Bearer` or `token`. Injected by the proxy; the agent never sees the value. |
|
|
| `auth.token_ref` | when `auth` present | Env-var name holding the secret on the host. |
|
|
| `matches` | no | Array of `{paths, methods, headers}` filters. A request must match at least one entry (if any are given) to be forwarded. |
|
|
| `matches[].paths` | no | Array of `{type, value}`. `type` is `prefix` (default), `exact`, or `regex`. |
|
|
| `matches[].methods` | no | Array of HTTP method strings, e.g. `[GET, POST]`. |
|
|
| `matches[].headers` | no | Array of `{name, value, type}`. `type` is `exact` (default) or `regex`. |
|
|
| `dlp` | no | Per-route DLP overrides. Omit to use defaults (all detectors on). |
|
|
| `dlp.outbound_detectors` | no | `false` disables outbound scanning; list restricts to named detectors (`token_patterns`, `known_secrets`). |
|
|
| `dlp.inbound_detectors` | no | `false` disables inbound scanning; list restricts to named detectors (`naive_injection_detection`). |
|
|
| `git.fetch` | no | `true` permits smart HTTP clone/fetch (`git-upload-pack`) for this host. Push (`git-receive-pack`) remains blocked. |
|
|
|
|
More examples in `examples/`. Full design lives under `docs/prds/`; the trust-boundary rationale is in `docs/prds/0011-per-file-md-manifest.md`.
|
|
|
|
## Trademarks
|
|
|
|
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 inside a sandbox.
|
|
|
|
## License
|
|
|
|
Copyright 2026 Eric Bauerfeld. Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full text.
|