Files
ai-sandbox/docs/recipes.md
T
ducoterra 03ab9c0cb1 add ai-sandbox: rootless podman sandbox for agentic development
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
2026-10-27 15:11:29 -04:00

4.0 KiB

Recipes

Concrete invocations. pi below is whatever you aliased to ai-sandbox.

A first session

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):

mkdir -p .ai-sandbox
printf 'api.anthropic.com\n' > .ai-sandbox/allow
pi --env ANTHROPIC_API_KEY

Per harness

# 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.

# 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

# 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

# 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

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

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:

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.