A single bash script that runs a coding harness (pi by default) inside a rootless Ubuntu 24.04 container with only the current directory mounted at /project, an explicit egress allowlist and an explicit environment: nothing is passed in that was not named on the command line or in a config file. Network enforcement here is empirical rather than assumed (tests/spike_network.sh, docs/networking.md). The original design -- a sidecar proxy on an internal network -- does not work rootless on this host, so allow mode runs pasta with --map-host-loopback, installs an inet aisb ruleset inside the container that drops every egress path except the gate's, and relays through a squid gate bound to the host's loopback that resolves names itself and refuses anything unlisted. The entrypoint checks that the default-drop rule really exists before setpriv drops to uid 1000 with an empty capability set, and exits 77/78 rather than run unenforced. The Containerfile, the entrypoint, the squid config generator and the ruleset renderer all live in bin/ai-sandbox, and the image is tagged by content hash, so a policy change can never silently reuse a stale image. Tests are pure bash: 161 assertions start no container and touch no network, 256 run real ones, including proof that a raw socket, a stripped proxy env and DNS all stay blocked while an allowlisted host works over both http and CONNECT. LLM-Generated: true
147 lines
4.0 KiB
Markdown
147 lines
4.0 KiB
Markdown
# Recipes
|
|
|
|
Concrete invocations. `pi` below is whatever you aliased to `ai-sandbox`.
|
|
|
|
## A first session
|
|
|
|
```bash
|
|
cd ~/Projects/myapp
|
|
ai-sandbox doctor # is this machine ready?
|
|
pi --allow api.anthropic.com --env ANTHROPIC_API_KEY
|
|
```
|
|
|
|
Same thing, but remembered by the repo (so teammates review the policy, not just
|
|
your shell history):
|
|
|
|
```bash
|
|
mkdir -p .ai-sandbox
|
|
printf 'api.anthropic.com\n' > .ai-sandbox/allow
|
|
pi --env ANTHROPIC_API_KEY
|
|
```
|
|
|
|
## Per harness
|
|
|
|
```bash
|
|
# pi (baked into the image)
|
|
pi --allow api.anthropic.com --env ANTHROPIC_API_KEY
|
|
|
|
# pi with a proxy/harness you pass explicitly
|
|
pi --harness 'pi --model sonnet' --allow api.anthropic.com --env ANTHROPIC_API_KEY
|
|
|
|
# OpenAI Codex CLI (install it into the image with --install or use a custom base)
|
|
pi --allow chatgpt.com --allow api.openai.com --env OPENAI_API_KEY
|
|
|
|
# Claude Code
|
|
pi --allow api.anthropic.com --env ANTHROPIC_API_KEY --env CLAUDE_CODE_OAUTH_TOKEN
|
|
|
|
# just a shell in the sandbox
|
|
pi --shell
|
|
|
|
# any other build/test command, no network needed
|
|
pi --net none -- make test
|
|
```
|
|
|
|
## Per ecosystem
|
|
|
|
Only the endpoints the tool actually needs. Everything else stays blocked, so a
|
|
typosquatted package from an unexpected host fails loudly instead of quietly
|
|
working.
|
|
|
|
```bash
|
|
# npm / node
|
|
pi --allow registry.npmjs.org --allow nodejs.org --allow github.com
|
|
|
|
# python: pypi needs both of these (the CDN that serves the files is separate)
|
|
pi --allow pypi.org --allow files.pythonhosted.org
|
|
|
|
# rust
|
|
pi --allow crates.io --allow static.crates.io --allow index.crates.io
|
|
|
|
# go
|
|
pi --allow proxy.golang.org --allow sum.golang.org
|
|
|
|
# docker/podman images from within a build
|
|
pi --allow registry-1.docker.io --allow auth.docker.io --allow production.cloudflare.docker.com
|
|
|
|
# a project that also needs its dev database (non-HTTP: pinned IP + port)
|
|
pi --allow api.anthropic.com --allow 10.20.0.5:5432 --env ANTHROPIC_API_KEY
|
|
```
|
|
|
|
## Git
|
|
|
|
```bash
|
|
# fetch/push over https with a token in the environment
|
|
pi --allow github.com --env GH_TOKEN -- git push origin main
|
|
|
|
# ssh: the key has to be mounted deliberately (an explicit, reviewable grant)
|
|
pi --allow github.com --mount ~/.ssh:ro -- git push origin main
|
|
|
|
# read-only view of a sibling repo for context
|
|
pi --allow api.anthropic.com --env ANTHROPIC_API_KEY --mount ../docs:/docs:ro
|
|
```
|
|
|
|
## Tightening a run
|
|
|
|
```bash
|
|
# nothing persistent, nothing readable, nothing writable outside the repo
|
|
pi --net none --ephemeral-home --read-only -- make build
|
|
|
|
# an entirely blank sandbox for untrusted code
|
|
pi --net none --no-cwd --ephemeral-home --read-only -- bash
|
|
|
|
# CI-ish: refuse to start if the policy or environment is not exactly right
|
|
pi --strict-args --net allow --allow-file .ai-sandbox/allow --env-file .ci/env -- npm ci
|
|
```
|
|
|
|
## Long-lived gate shared by several sandboxes
|
|
|
|
```bash
|
|
ai-sandbox gate up --allow api.anthropic.com --allow registry.npmjs.org
|
|
# name=aisb-gate
|
|
# port=18081
|
|
# proxy_env=export HTTPS_PROXY=http://100.64.0.1:18081
|
|
|
|
ai-sandbox --proxy 100.64.0.1:18081 --allow api.anthropic.com --shell
|
|
ai-sandbox gate logs # what did it allow/deny?
|
|
ai-sandbox gate down
|
|
```
|
|
|
|
## Debugging
|
|
|
|
```bash
|
|
ai-sandbox --dry-run --allow example.com --env KEY # the exact podman invocation
|
|
ai-sandbox --print-policy # effective policy, no secrets
|
|
ai-sandbox --print-entrypoint | less # the enforcement script
|
|
ai-sandbox --keep --net none -- false # leave the container for forensics
|
|
ai-sandbox ps
|
|
AISB_GATE_KEEP=1 pi --allow example.com --shell # keep the gate after exit
|
|
ai-sandbox clean --all
|
|
|
|
# "the ruleset would not install" (exit 77): check the host
|
|
podman info | grep -i rootless
|
|
grep "$(id -un)" /etc/subuid
|
|
```
|
|
|
|
## Making it the default for a project
|
|
|
|
`.ai-sandbox/config`:
|
|
|
|
```ini
|
|
net = allow
|
|
harness = pi
|
|
cpus = 8
|
|
memory = 8g
|
|
mount = ../specs:/specs:ro
|
|
env = ANTHROPIC_API_KEY
|
|
```
|
|
|
|
`.ai-sandbox/allow`:
|
|
|
|
```
|
|
api.anthropic.com
|
|
registry.npmjs.org
|
|
```
|
|
|
|
Then `pi` alone in that directory is the whole invocation — and because both files
|
|
are in the repository, the sandbox policy gets reviewed with the code.
|