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
8.3 KiB
Settings map
Check this against the installed program before writing a config: ai-sandbox --help, and
cfg_read_kv() / resolve_opts() in the script. Where they disagree, the script wins and this
file is the bug.
Precedence
built-in default < ~/.config/ai-sandbox/{config,allow} < ./.ai-sandbox/{config,allow}
< AISB_* environment < command-line flag
allow, env, mount, install, label accumulate from every source (deduped); every other
key replaces. So a repo file cannot revoke a user-wide allow line — say so if the user expects a
repo config to tighten their own machine.
The container, for orientation
Anything named "container" below is inside the sandbox: uid/gid 1000, the image's own sandbox
account, its home the value of HOME in there (--userns=keep-id makes files the agent writes
owned by you on the host). So ~/.agents exists on both sides of the wall and they are different
directories — the host one is bind-mounted read-only onto the container one at agents_dest.
/project is the image's own directory and the fallback working directory, not the normal one.
Config keys (key = value, # comments)
| key | default | notes |
|---|---|---|
net |
allow |
none | allow | full. allow requires a non-empty allowlist. |
harness |
pi |
pi | codex | claude | none | a command line, e.g. pi --model turbo |
workdir |
auto |
auto mirrors the host path; or an absolute path. project_dest is parsed and never read — use workdir. |
cpus |
4 |
|
memory |
4g |
512m/4g shape |
pids |
512 |
|
tmpfs_size |
1g |
sizes /tmp and, with home_cache = 0, the container home |
base_image |
docker.io/library/ubuntu:24.04 |
may itself be name@sha256:… |
node_version |
22.23.4 |
checksum-verified against nodejs.org |
pi_version |
1.4.0 |
npm package version baked into the image |
base_ttl_days |
7 |
0 re-resolve every build, -1 never |
base_auto_update |
1 |
0 keeps the recorded digest offline |
selinux |
:z when getenforce exists |
:z, :Z or empty |
home_cache |
1 |
0 = throwaway tmpfs home (also --ephemeral-home) |
gate_addr |
100.64.0.1 |
the address that means the host's loopback inside the container |
gate_port |
18081 |
preferred first port; next free is used |
extra_args |
empty | appended verbatim to podman run, word-split. Escape hatch: do not commit it |
pi_config |
1 |
bridge ~/.pi/agent/{models,settings}.json with the credential as $VAR |
pi_token_env |
AI_API_TOKEN |
the variable the bridged config reads |
pi_token_auto |
1 |
pass that variable automatically when set |
pi_agent_dir |
~/.pi/agent |
where to read the host config from |
thinking |
xhigh |
off minimal low medium high xhigh max host; pi only, re-applied every run |
agents |
1 |
share ~/.agents read-only when the directory exists |
agents_dir |
~/.agents |
which directory; a project's own .agents/ needs nothing |
agents_dest |
the container's ~/.agents |
keep it under the container $HOME — that is the only place a harness looks for user-level skills |
allow |
— | repeatable; same grammar as --allow |
env |
— | repeatable; NAME, NAME=VALUE (no secret in a committed file), or @path = an env file |
mount |
— | repeatable; SRC[:DEST][:ro|rw], default DEST=/mnt/<basename> |
install |
— | repeatable; apt packages, each distinct set is its own image tag |
label |
— | repeatable; K=V on the container |
Anything else warns (unknown key) and is ignored.
Flags
egress --net none|allow|full --allow HOST[:PORT] --allow-file F --proxy HOST:PORT --no-gate
files --mount SRC[:DEST][:ro] --no-cwd --workdir DIR|auto --read-only
--home-cache --ephemeral-home --agents | --no-agents --agents-dir PATH
env --env NAME --env NAME=VALUE --env-file F (F should be 0600)
image --base-image REF --install PKG --no-build
harness --harness NAME --shell --thinking LEVEL --pi-token-env NAME --no-pi-token-env
--no-pi-config
limits --cpus N --memory SIZE --pids N --tmpfs-size SIZE
run --name NAME --label K=V --keep --interactive | --no-interactive --no-tty
preview --dry-run|-n --print-policy --print-entrypoint --print-containerfile
hatches --allow-unenforced --insecure-keep-root --strict-args
Parsing stops at the first positional or unknown argument, which becomes an argument to the
harness — that is what makes alias pi=ai-sandbox behave like the harness. -- replaces the
command entirely. --strict-args makes an unknown flag an error instead (use it in scripts).
Subcommands: build [--rebuild] [--gate] [--refresh-base], update [--check], doctor [--deep],
gate up|down|status|logs, ps, clean [--all] [--volumes[=GLOB]], inspect, version.
Exit status: the command's own code; 77 the egress ruleset could not be installed; 78 the policy file was missing in the container. Both refusals are the sandbox working.
Allow-entry grammar
| entry | effect |
|---|---|
example.com |
gate matches that name and anything below it, at CONNECT time |
example.com:8443 |
same; the port does not create a netfilter rule, and the gate only relays 80/443/8443 |
10.0.0.5:5432 |
direct netfilter rule for that host and port — the way to allow non-HTTP |
10.20.0.0/16 |
direct rule for the subnet, default ports 80,443 unless :port |
[fd00::5]:8443 |
direct ipv6 rule |
localhost |
resolves on the host → a direct rule for 127.0.0.1, i.e. the container's loopback. For your machine's services use 100.64.0.1 |
Per-ecosystem starting points
Add only what the project's own lockfiles and build files prove it contacts. Everything left out fails loudly, which is the feature.
| stack | entries |
|---|---|
| npm | registry.npmjs.org (+ nodejs.org and, for prebuilds, release-assets.githubusercontent.com — read the git-host warning before adding anything from github.com) |
| python | pypi.org, files.pythonhosted.org (the file CDN is separate; both are needed). A private index: whatever host --index-url, PIP_INDEX_URL, [[tool.uv.index]] or tool.poetry.source names |
| rust | crates.io, static.crates.io, index.crates.io (+ static.rust-lang.org if it installs toolchains) |
| go | proxy.golang.org, sum.golang.org (+ the host of a private module path, allow = <host> and GOPRIVATE in the env) |
| ruby | rubygems.org, index.rubygems.org |
| php | repo.packagist.org, api.github.com (composer metadata; check before allowing) |
| jvm | repo1.maven.org, repo.maven.apache.org, plugins.gradle.org, services.gradle.org, dl.google.com |
| dotnet | api.nuget.org |
| terraform | registry.terraform.io, releases.hashicorp.com |
| playwright / puppeteer | their download hosts (cdn.playwright.dev, storage.googleapis.com) — or commit the browsers and go --net none |
| container builds inside the sandbox | registry-1.docker.io, auth.docker.io, production.cloudflare.docker.com |
Gotchas worth a comment in the file
--env NAME=VALUEshows up inps; the name-only and--env-fileforms do not.- An unset variable named with
--envfails the run (in preview it is reported asenv_missing=instead) — that fail-closed behaviour is why preview output is worth reading. - The image's home is a named volume (
aisb-home) that outlives the image: after a rebuild the home may be stale.ai-sandbox clean --volumesreclaims unusedaisb-*volumes. - A project under
/tmp,/usr,/etc,/var,/opt,/run,/homeitself or the container's own home cannot mirror its host path and lands in/project, with a line saying so. Do not "fix" that with--workdir /tmp/...; the tmpfs covers it. - The host path may not contain
:at all — podman cannot parse such a mount spec, and ai-sandbox refuses rather than passing the error along. - Nothing is inherited but
TERM LANG LC_ALL LC_CTYPE TZ(plusHOME, set to the container home,AISB=1,AISB_SESSION). A tool that needsPATH, a dotfile or anything else from your real home needs an explicit grant. .envfiles inside the project are readable by the agent — that is the point of the mount, and it is why the sandbox token should be a narrow one.