# AGENTS.md Working notes for anyone (human or agent) changing this repository. ## What this project is One bash script, `bin/ai-sandbox`, that runs another program — usually an LLM coding harness — inside a rootless podman container with a strict, explicit policy: only the current directory mounted, no inherited environment, no root or capabilities inside the container, resource caps, and egress limited to an allowlist that the sandboxed process cannot lift. ## Non-negotiables 1. **Single script.** The Containerfile, the container entrypoint, the squid config and the nftables rules are all generated from `bin/ai-sandbox` (`containerfile()`, `entrypoint_script()`, `write_squid_conf()`, `gen_rules()` inside the entrypoint). Keep it that way; do not split policy into files that can drift from the script. 2. **Fail closed.** Any path where the sandbox cannot guarantee its policy must refuse to start rather than degrade: no allowlist in `allow` mode, missing policy file in the entrypoint (exit 78), ruleset that cannot be installed (exit 77), `--env NAME` where NAME is unset. Escape hatches exist but must be explicitly named (`--allow-unenforced`, `--insecure-keep-root`, `--net full`). 3. **Never leak a secret.** Values go through a `0600` env file, never argv. Nothing that prints (`--dry-run`, `--print-policy`, the session log, error messages) may echo a value. If you add output, add a test that a canary value does not appear in it. 4. **No hardcoded machine paths, usernames or ports.** Everything is overridable via `AISB_*` or a config file; defaults must work for any user on any host. 5. **Measure, don't assume, about podman.** Rootless networking varies by version and distro. Run `tests/spike_network.sh` on a new host and update `docs/networking.md` when reality disagrees with the design. ## Layout ``` bin/ai-sandbox the application install.sh symlink + optional alias config/*.example documented examples (never loaded directly) docs/networking.md the measurements behind the enforcement model docs/threat-model.md what is and is not protected -- keep it honest docs/recipes.md per-harness and per-language setups tests/run.sh runner: unit (no containers) | --e2e (real containers) tests/spike_network.sh re-measure the host ``` ## Commands ```bash make test # unit tests -- fast, no containers, no network make e2e # plus real container/egress tests (needs network + the image) make lint # shellcheck in a container make style # bash -n on every shell file make check # lint + unit ./bin/ai-sandbox --dry-run --allow example.com --print-policy # introspection ``` Before finishing a change: `make check` green, and if you touched enforcement, networking or the image, `make e2e` green too. ## Conventions * bash ≥ 4.4, `set -Eeuo pipefail` in the main script; `set -uo pipefail` in the test runner (assertions report, they do not abort). * Option flow is always: defaults → config files → `AISB_*` env → CLI flags. The built-in defaults live in `resolve_opts()`; the top of the script only sets variables that have no config-file counterpart. Breaking this is the single easiest way to introduce a precedence bug. * `POL[...]` holds resolved policy, `P_*` arrays resolved lists, `OPT_*` raw flags, `CFG_*` config-file values. Do not read raw env vars deep in the code. * Preview paths (`--dry-run`, `--print-policy`, `--print-entrypoint`, `--print-containerfile`) must not build images, start containers or write state. * Tests use `t_name` + `assert_*` from `tests/lib.sh`; each file gets its own `AISB_STATE_DIR`/`AISB_CACHE_DIR`/`AISB_CONFIG_DIR`. Never let a test touch the real `~/.config`, `~/.local/state` or `~/.cache`, and never override `HOME` (podman storage lives there). ## Areas that deserve the most care * `entrypoint_script()` — the enforcement half. It is rendered into the image and runs as root there. A bug here is a security bug. `tests/unit/06_entrypoint.sh` checks the generated rule shape; `tests/e2e/11_net.sh` checks it in a container. * `split_target()` / `classify_allows()` — the mapping from what the user typed to what the firewall and the proxy enforce. * `build_argv()` — every isolation flag lives here.