d0c2642943
test / run tests/run_tests.py (pull_request) Failing after 31s
Add a Gitea Actions test-status badge plus a short README "CI" section covering how to read the check and what to do when it's red. Capture the (out-of-tree) branch-protection rule on `main` in docs/ci.md so the gate that requires the test check is reproducible from the repo alone — covers both the Gitea UI path and the equivalent API call. Refs: PRD 0002 Assisted-by: Claude Code
134 lines
5.1 KiB
Markdown
134 lines
5.1 KiB
Markdown
# claude-bottle
|
|
|
|
[](https://gitea.dideric.is/didericis/claude-bottle/actions?workflow=test.yml)
|
|
|
|
Spins up an isolated container for running Claude Code with a curated set of skills and env vars.
|
|
|
|
## Why "claude-bottle"?
|
|
|
|
Each container is a bottle; Claude is the genie inside. The genie has
|
|
broad powers within the bottle — read, write, run anything — but it
|
|
cannot escape to the host. You uncork one bottle per agent
|
|
(`./cli.sh start <agent>`), many bottles run in parallel, and each
|
|
one's powers are scoped to what the manifest grants it: a curated set
|
|
of skills, env vars, and a starting prompt. When the session ends the
|
|
bottle is destroyed and the genie does not persist.
|
|
|
|
## Goals
|
|
|
|
- Minimize risk of running claude with full permissions
|
|
- Allow me to easily spin up agent tasks in parallel
|
|
- Create isolated, well defined, easily updated, shareable agents
|
|
|
|
## Non-goals
|
|
|
|
- Communicating between agents directly
|
|
- Self hosted VMs (v1 uses local Docker containers, not VMs)
|
|
- Advanced agent auditing (lean on git history for auditing)
|
|
|
|
## Quickstart
|
|
|
|
Requires Docker on the host and a long-lived Claude Code OAuth token in
|
|
your shell env.
|
|
|
|
```sh
|
|
./cli.sh start <agent> # builds the image on first run, drops you into claude
|
|
```
|
|
|
|
The container is removed automatically when the session ends. If the script
|
|
is killed with SIGKILL the exit trap won't fire and the container may be
|
|
left running; remove it with `docker rm -f <container-name>`.
|
|
|
|
## CI
|
|
|
|
Every push to a PR (and every push to `main`) runs the full test suite
|
|
on Gitea Actions via `.gitea/workflows/test.yml`. The status badge at
|
|
the top of this README links to the workflow's run history; the same
|
|
check appears on each PR.
|
|
|
|
Reading the check:
|
|
|
|
- Green — `tests/run_tests.py` exited 0 on the runner.
|
|
- Red — at least one test failed (or the workflow itself errored).
|
|
Click through to the run, expand the failing step, and read the
|
|
unittest output. Reproduce locally with `tests/run_tests.py` (or
|
|
`tests/run_tests.py unit` if you don't have Docker handy);
|
|
integration tests skip cleanly when Docker isn't reachable.
|
|
- Skipped tests — expected when the runner has no Docker daemon. They
|
|
still leave the job green; if you actually want them executed,
|
|
ensure Docker is available on the runner host.
|
|
|
|
Pushing a fix to a red PR re-triggers the workflow automatically — no
|
|
manual rerun needed. Branch protection on `main` requires this check
|
|
to be green before the merge button is enabled; see [`docs/ci.md`](docs/ci.md)
|
|
for how those rules are configured.
|
|
|
|
## Egress
|
|
|
|
Agent containers route HTTP / HTTPS traffic through a per-agent
|
|
[pipelock](https://github.com/luckyPipewrench/pipelock) sidecar
|
|
attached to a Docker `--internal` network. The sidecar enforces a
|
|
hostname allowlist, runs DLP scanning (48 default credential
|
|
patterns), and detects URL-embedded high-entropy secret leaks. Without
|
|
the proxy the agent has no route off-box at all — the internal network
|
|
has no default gateway. The sidecar and network are torn down with the
|
|
agent on session exit.
|
|
|
|
The effective allowlist is the union of a baked-in default for Claude
|
|
Code's required hosts (`api.anthropic.com`, `claude.ai`, ...) and the
|
|
optional `bottles.<name>.egress.allowlist` field in
|
|
`claude-bottle.json`:
|
|
|
|
```jsonc
|
|
{
|
|
"bottles": {
|
|
"default": {
|
|
"env": { },
|
|
"ssh": [ ],
|
|
"egress": { "allowlist": ["github.com"] }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The resolved allowlist is shown in the y/N preflight before launch.
|
|
See `docs/prds/0001-per-agent-egress-proxy-via-pipelock.md` for the
|
|
design and `docs/research/pipelock-assessment.md` for the rationale.
|
|
|
|
## Auth: OAuth token, not API key
|
|
|
|
claude-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.
|
|
|
|
**Why a token instead of mounting `~/.claude.json`:** on macOS, Claude
|
|
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.
|
|
|
|
**One-time setup on the host:**
|
|
|
|
```sh
|
|
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`:
|
|
|
|
```sh
|
|
export CLAUDE_BOTTLE_OAUTH_TOKEN="<token>"
|
|
```
|
|
|
|
`cli.sh` automatically forwards it to every container as
|
|
`CLAUDE_CODE_OAUTH_TOKEN` via `docker run -e` — no manifest wiring
|
|
required, and the value is never written to disk or placed on argv.
|
|
|
|
Inside the container, `claude` picks up `CLAUDE_CODE_OAUTH_TOKEN` and
|
|
authenticates against your subscription. Caveats: the token is bound
|
|
to your subscription tier (Pro/Max/Team/Enterprise), it does not work
|
|
with `claude --bare` (which only reads `ANTHROPIC_API_KEY`), and if it
|
|
leaks, regenerate via `claude setup-token` again. Reference:
|
|
<https://code.claude.com/docs/en/authentication>.
|