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
87 lines
4.2 KiB
Markdown
87 lines
4.2 KiB
Markdown
# 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.
|