Turns a project directory into that project's policy, so a plain `ai-sandbox` in the directory is the correct, reviewable invocation. It writes the two files load_config() actually reads -- .ai-sandbox/allow and .ai-sandbox/config -- and nothing else: no sandbox.yaml, no local override, no invented schema, because a file nobody reads is a policy nobody has. scripts/survey.sh gathers the evidence read-only and prints no values: variable NAMES, remote and registry HOSTS, service PORTS, with URL credentials stripped before anything is printed -- the same rule --dry-run, --print-policy and the session log hold themselves to. The agent decides from that evidence, then verifies with --dry-run and --print-policy rather than assuming an allowlist entry means a working toolchain. references/settings-map.md maps every accepted key and flag to its default, plus per-ecosystem allowlists and the survey-to-policy cheat sheet. references/port-and-loopback.md is the honest answer on ports: outbound to a service on the machine via the mapped loopback address 100.64.0.1 (needs an allow entry and a client that does not hand the request to the egress proxy), and why a port the sandbox listens on is not reachable from the host in --net allow -- with probes, because that half is a reading of gen_rules(), not a measurement. Policy files only. Never builds, never starts a container, and never writes a real credential: it writes .ai-sandbox/env.example and tells the user what to put in the gitignored 0600 file. LLM-Generated: true
14 KiB
name, description
| name | description |
|---|---|
| right-sandbox | Analyzes a project directory and writes that project's ai-sandbox policy — .ai-sandbox/allow plus .ai-sandbox/config (egress allowlist, environment variable names, bind mounts, working directory, resource limits, harness, extra image packages, access to services on the host) — so that a plain `ai-sandbox` run in the directory is the correct, reviewable invocation. Use when the user asks to set up, configure, write, fix, review or tighten ai-sandbox or the sandbox for a project, wants a .ai-sandbox config committed with the code, or asks why a sandboxed agent cannot reach a package registry, a database, a localhost port or its model gateway. Writes policy files only — never builds, never starts a container, never puts a credential value in a file. |
right-sandbox
Produce the two files ai-sandbox reads from a project, and nothing else:
| file | contents | committed |
|---|---|---|
.ai-sandbox/allow |
one egress target per line, # comments ok |
yes — the policy gets reviewed with the code |
.ai-sandbox/config |
key = value: net, harness, limits, mounts, env names, image |
yes |
.ai-sandbox/env |
optional NAME=VALUE, mode 600 |
never — gitignore it |
load_config() reads ./.ai-sandbox/{config,allow} (after ~/.config/ai-sandbox/{config,allow})
and nothing else, so do not invent sandbox.yaml, local, env.json or a phases/ directory in
there — a file nobody reads is a policy nobody has.
Precedence for every key: built-in default < user config < repo config < AISB_* env < CLI flag.
The repeatable keys (allow, env, mount, install, label) accumulate across all five
sources rather than replacing, so a repo config adds to the user's, it does not silence it.
What makes a config right
- Names, never values. Both committed files end up in the repository, in
--print-policyand in a reviewer's diff. Credentials live in the environment or in.ai-sandbox/env(chmod 600, gitignored); the config saysenv = AI_API_TOKEN. A literalNAME=VALUEis only for things that are not secrets, e.g.env = DATABASE_URL=postgresql://dev@100.64.0.1:5432/app. - Every key and flag must be real. An unknown config key is warned about and ignored — a typo
is a silently missing policy, which is worse than an error. The authority is the installed
program:
ai-sandbox --help, pluscfg_read_kv()'s key list in the script.references/settings-map.mdholds the list as of writing — check it, do not trust it. Known trap:project_destis accepted by the parser and read by nothing; the key isworkdir. - The git remote stays out. No
github.com,gitlab.com,bitbucket.orgor ssh host inallowunless the user asks after hearing the cost: an allowlisted remote plus a token or a mounted~/.sshlets a prompt-injected agent ship code nobody reviewed (threat model #1).git status,diff,log,blameall work without it, andai-sandbox-autophasecommits and pushes on the host precisely so that this line can stay out. If a dependency genuinely has to be fetched over a git host, say which dependency in a comment above the line and prefer a read-only, short-lived token. - Escape hatches never go in a committed config:
net = full,extra_args,--allow-unenforced,--insecure-keep-root. They are per-run decisions named to be uncomfortable, and a repo file outlives the run that justified it. A run refusing to start (exit 77) is the policy working — debug the host, do not route around it. - Least mount. The cwd is already mounted read-write at its host path, and
~/.agentsis shared read-only with no flag to change it. Never propose a writable skills mount: a sandbox that can write aSKILL.mdhas written an instruction the user follows later on the host. Every extramountline needs a one-line reason in the file. - Verify before reporting success (§5). "It should work" is not an acceptable result for a
security policy;
--print-policyand--dry-runanswer without building or starting anything.
1. Survey the project
bash scripts/survey.sh [project-dir] # default: $PWD; resolve scripts/ against this skill's directory
It is read-only and prints no secret values — variable names, remote hosts, registry hosts
and service ports. Read its output, then read the files it names: the lockfiles, the compose
file, .env.example, the dev-server config, CI, and the Makefile or task runner. Prefer evidence
over inference: a pyproject.toml with an [[tool.uv.index]] is a private index, an empty
node_modules tells you nothing.
Also read what already exists — .ai-sandbox/config, .ai-sandbox/allow, the user's
~/.config/ai-sandbox/{config,allow} — and extend it rather than replacing a decision the user
made deliberately.
2. Decide each setting from evidence
| need | evidence | setting |
|---|---|---|
| package installs | lockfiles (pnpm-lock.yaml, uv.lock, Cargo.lock, go.sum, Gemfile.lock, pom.xml…) |
allow entries for the registries that ecosystem really contacts — see the table in references/settings-map.md |
| model gateway | ~/.pi/agent/models.json baseUrl host, or the base URL the user's harness is configured with |
allow = <gateway host>; without it a bridged pi starts and cannot answer |
| a database / queue / cache | docker-compose*.yml published ports, .env URLs, DATABASE_URL |
allow = 100.64.0.1:<published port> + an env var that points the app at 100.64.0.1 — see references/port-and-loopback.md |
| a non-HTTP backend | pinned host:port (a LAN inference box, an internal API) | allow = <ip or cidr>:<port> (IP/CIDR become netfilter rules; domains go through the gate) |
| credential the harness needs | .env.example, CI secrets.*, --env NAME in the user's launcher |
env = NAME (name only), and list what the user must export |
| reference material / sibling repo | imports or docs that read ../something |
mount = ../something:/something:ro |
| push over ssh | ~/.ssh and an ssh:// remote |
do not add by default; rule 3 |
| a tool the run needs that the image lacks | build logs, apt-get in the project's Dockerfile, rg, jq, a DB client, ffmpeg |
install = <apt package> (each distinct set gets its own cached image tag). Decide it here: inside, the harness is uid 1000 with no root and no package manager, so apt-get is not something it can do later |
| heavy build / flaky cap | repo size, test suite runtime, the user's machine | cpus, memory, pids, tmpfs_size |
| harness that is not pi | .claude/, codex config, the user's habit |
harness = pi | codex | claude | none | "<your command>" |
| paths must look like the host's | a harness that titles by cwd; logs the user pastes | leave workdir = auto; set it only when the host path buries an image path |
| project-specific skills/prompts | a project .agents/ |
nothing — it arrives with the project mount |
| untrusted-code review | the user says so | net = none, agents = 0, home_cache = 0, read-only on the CLI, not in the file |
Two decisions the tool already makes that you should not repeat in the file: the thinking level is
xhigh policy for a bridged pi, and the pi config bridge is on with the credential by environment
reference. Only write thinking or pi_token_env when the project genuinely differs, with a
comment saying why.
3. Write the files
.ai-sandbox/allow — one target per line, grouped and commented, because this file is the diff a
human reviews:
# model gateway -- without it the sandboxed harness cannot answer
gateway.example.com
# dependencies
registry.npmjs.org
nodejs.org # node-gyp / prebuild downloads
# services on this machine, reached through the mapped loopback address (§ports)
100.64.0.1:5432 # postgres, `docker compose up db`
.ai-sandbox/config — only the keys this project needs; a comment per non-obvious one:
# ai-sandbox policy for <project>. Committed on purpose: review it like code.
# Values belong in .ai-sandbox/env (0600, gitignored) or the environment, never here.
net = allow
harness = pi
# the toolchain needs a bit more than the default 4 cpu / 4g
cpus = 8
memory = 8g
# design docs live beside the repo
mount = ../specs:/specs:ro
# names only; ai-sandbox passes them by name through a 0600 file
env = AI_API_TOKEN
env = @./.ai-sandbox/env
# apt packages the test suite needs that the image lacks
install = postgresql-client
env = @path is the config-file form of --env-file (--env-file F becomes the entry @F), so
it works today and fails closed if the file is missing — note in the file that a newcomer must
cp .ai-sandbox/env.example .ai-sandbox/env && chmod 600 it. If you create env.example, put
names with dummy values in it and add .ai-sandbox/env to .gitignore (never overwrite an
existing ignore file's rules — append).
Never write .ai-sandbox/env yourself with real values: write the example, then tell the user what
to put in it.
4. Egress in one paragraph (get the shape right)
- A domain entry is matched by the gate at CONNECT time, including subdomains. Its
:portdoes not add a netfilter rule and the gate only relays 80, 443, 8443 (Safe_ports/SSL_portsin the generated squid config), so--allow host:8080will not do what it looks like. - An IP, CIDR or
[ipv6]entry becomes a direct netfilter rule, default ports 80,443 unless you write:port— this is how non-HTTP traffic (postgres, redis, a LAN model box) is allowed. --no-gateskips the proxy: domains are resolved once, on the host, at start and DNS is opened; weaker for CDNs and rotating IPs.--proxy host:portuses a proxy you already run (default port 3128). Neither belongs in a committed config without a comment justifying it.- An empty allowlist in
allowmode is a hard error. Never "fix" that by widening net.
5. Verify (no builds, no containers, no network)
cd <project>
ai-sandbox --dry-run # the exact podman invocation + the # egress / # env header lines;
# works with no podman and leaves no session directory behind
ai-sandbox --print-policy # net=, allow=, gate_domains=, direct=, env_names=, env_missing=,
# mounts=, workdir=, agents=, thinking= -- as key=value lines
Start with --dry-run: it is the one preview path with no side effects at all. --print-policy
gives the friendlier key=value form but also ensures the home-cache volume exists, so it needs
podman present — treat a failure there as "this machine has no podman", not as a policy problem.
Check, in this order, and fix the files instead of explaining the output:
- no
warning: --env NAME requested but not setfor a name nobody intended (unset names are fine to ship — say so in the report — but each must be a deliberate line, not a typo). - every allow entry appears in
domains=[…]ordirect=[…](orgate_domains=/direct=under--print-policy) — an entry in neither is a parse problem. workdir=is the host path, not/project, unless the fallback was intended (workdir_note=says why).mounts=andagents=are what you wrote and nothing more.- the dry run shows
--network pasta:--map-host-loopback,100.64.0.1,--cap-drop=ALL,--security-opt no-new-privileges,--userns keep-id:…and no secret values. --print-policy | grep -ifor each credential name from §1: the name may appear, a value must not.
Then, with the user watching (this builds the image the first time and starts a container):
ai-sandbox --shell # egress, mounts, env present
ai-sandbox --net none -- make test # the offline half of the toolchain
ai-sandbox doctor # host readiness
Probe the two or three endpoints the project actually needs from inside (curl -sS -m 5 -o /dev/null -w '%{http_code}\n' https://registry.npmjs.org/) rather than assuming an allowlist entry
means a working toolchain.
6. Report
Finish with exactly these five points, so the user can act without reading the files:
- Files written — paths, plus the
.gitignorechange if any. cd <dir> && ai-sandboxis now the invocation (and the alias note if they use one).- What they must export first (
env =names that are unset), and where values may go. - What is deliberately not allowed — the git remote first, then anything you declined to open because the honest answer was "not without weakening the sandbox" (inbound port publishing is usually on this list).
- What you verified and how — the two preview commands and any live probe, with results.
Ports, localhost, "the app must be reachable"
Read references/port-and-loopback.md before promising anything about ports. Short version: a
sandboxed process reaches services on your machine through the mapped loopback address
100.64.0.1, which needs an allow entry and a client that does not send that request to the
egress proxy; and a port the sandbox listens on is not reachable from the host in --net allow
mode, because the generated ruleset's input chain is policy drop. Verify on the machine rather
than repeating that from memory — the file has the probes.
References
references/settings-map.md— every accepted config key and flag, defaults, per-ecosystem allowlists, the survey→policy cheat sheet.references/port-and-loopback.md— inbound vs outbound,100.64.0.1, the gate's port limits, probes, the three workable dev-server patterns.scripts/survey.sh— the read-only evidence gatherer (prints no values).- The tool's own docs, if this repo is present:
README.md,docs/networking.md(measurements),docs/threat-model.md(what is not protected),docs/recipes.md(invocations).