# 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`, `publish` **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. It also cannot *un*publish a port the user's own config opens. ## 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 | | `publish_addr` | `127.0.0.1` | the host address a bare `publish = 8000` binds. Machine-wide setting: put it in the user config, never in a repo file, and do not widen it for the user's convenience | | `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/` | | `install` | — | repeatable; apt packages, each distinct set is its own image tag | | `label` | — | repeatable; `K=V` on the container | | `publish` | — | repeatable; **ingress**: `[ADDR:]HOST:CONTAINER` forwarded into the container. One port per entry, no ranges, no `/udp`; an address needs three parts. `8000`, `18080:3000`, `0.0.0.0:18080:3000` (LAN-visible: warned, do not commit) | 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 ingress --publish [ADDR:]HOST:CONTAINER (no `-p`: that flag belongs to the harness) 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 = ` 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=VALUE` shows up in `ps`; the name-only and `--env-file` forms do not. * An unset variable named with `--env` **fails the run** (in preview it is reported as `env_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 --volumes` reclaims unused `aisb-*` volumes. * A project under `/tmp`, `/usr`, `/etc`, `/var`, `/opt`, `/run`, `/home` itself 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. * `publish` is the only **ingress** setting there is, and it is not an allowlist entry. `allow = 100.64.0.1:5432` lets the sandbox reach a service on your machine; `publish = 5432:5432` lets your machine reach a port inside the container. Confusing the two is how a database ends up exposed. Read `references/port-and-loopback.md` before writing either. * Nothing is inherited but `TERM LANG LC_ALL LC_CTYPE TZ` (plus `HOME`, set to the container home, `AISB=1`, `AISB_SESSION`). A tool that needs `PATH`, a dotfile or anything else from your real home needs an explicit grant. * `.env` files 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.