# Source map Question topic → the function, doc and test that hold the answer. Line numbers move; **names do not** — grep for these in the installed script (`readlink -f "$(command -v ai-sandbox)"`). ## Flags, keys, defaults | question | read | |---|---| | "what does `--x` do / does it exist" | `usage()` (grouped by NETWORK / FILESYSTEM / ENVIRONMENT / IMAGE / PI CONFIG / LIMITS / RUN / ESCAPE HATCHES / SUBCOMMANDS), `parse_args()`, and `KNOWN_OPTS` + `suggest()` for the accepted set | | "which config keys are legal" | `cfg_read_kv()` — the `case` lists them, the accumulate list is `cfg_accum_keys` (`allow env mount install label`). Anything else warns `unknown key` and is ignored | | "what wins, my flag or the file" | `resolve_opts()` — every line is `${OPT_x:-${AISB_X:-$(cfgv x default)}}`: CLI, then env, then config, then default. `load_config()` shows the file order | | "`AISB_*` variables" | the comment block above `usage()` and `--help`'s ENVIRONMENT section | | "why did arg parsing eat my harness flag" | `parse_args()` stops at the first positional/unknown; `--` sets `COMMAND`; `--strict-args`; `default_command()`/`harness_argv()` decide what actually runs | | subcommand behaviour | `main()` dispatch, then `cmd_build` / `cmd_update` / `cmd_doctor` / `cmd_gate` / `cmd_ps` / `cmd_clean` / `cmd_inspect` | ## Networking and enforcement | question | read | prove with | |---|---|---| | how `--net allow` holds at all | `entrypoint_script()` → `gen_rules()`, plus `build_argv()`'s `--cap-add=NET_ADMIN` + the `setpriv` line | `ai-sandbox --print-entrypoint`; `docs/networking.md` | | can the sandboxed process lift the policy | the capabilities the entrypoint drops before exec (`--inh-caps=-all --bounding-set=-all --no-new-privs`) | `tests/e2e/11_net.sh` (flush attempt, `unshare -n`, raw socket, DNS to 8.8.8.8) | | why an entry did nothing | `split_target()` then `classify_allows()`: domains → the gate (CONNECT-time, subdomain match); IP/CIDR/ipv6 → direct netfilter rules, ports default 80,443 | `ai-sandbox --print-policy` → `gate_domains=`, `direct=`, `direct6=` | | which ports the gate relays | `write_squid_conf()`: `Safe_ports` 80 443 8443, `SSL_ports` 443 8443, `http_access deny all` after the allowlist | `ai-sandbox gate logs` | | gate lifecycle / ports / reuse | `pick_port()`, `gate_up()`, `teardown()`, `cmd_gate()`; `AISB_GATE_KEEP=1`; `gate_port`/`gate_addr` | `ai-sandbox gate status` | | `--no-gate` vs `--proxy` vs the gate | `resolve_proxy_endpoints()` — `--proxy` uses your proxy (default port 3128); `--no-gate` resolves domains once on the host, opens DNS and warns; default is the built-in gate | `--dry-run` (the `HTTP(S)_PROXY` args) | | host-loopback / `100.64.0.1` | `build_argv()`'s `--network=pasta:--map-host-loopback,${gate_addr}` and the `NO_PROXY` defaults | `docs/networking.md`'s measurement table | | "is my machine able to do this" | `cmd_doctor()`, `tests/spike_network.sh` | `ai-sandbox doctor --deep` | ## Filesystem, mounts, the working directory | question | read | |---|---| | why the project is at `/project` | `resolve_workdir()` and `mirror_block_of()` — the deny list is what the image or the sandbox owns, and `/tmp` is a measured case (the tmpfs covers a bind of the same path). `workdir_note=` in `--print-policy` gives the reason | | what is mounted, rw or ro | `build_argv()`: the cwd, `/run/aisb:ro` (session policy), the home volume or tmpfs, the `.agents` share, then user `--mount`s | | `--mount` grammar and defaults | `mount_dest()` — `SRC[:DEST][:ro\|rw]`, default `DEST=/mnt/`; the option must be `ro` or `rw`; the source must exist | | skills sharing | `agents_src_of()`, `agents_dest_taken()`, the `.agents` block in `build_argv()`. Read-only, no override; a hand-written `--mount` on the same destination wins and the share is skipped with a log line | | file ownership written by the agent | `--userns keep-id:uid=…,gid=…` in `build_argv()` (files land owned by *you*), checked by `doctor` | | SELinux labels | `set_selinux()`, `POL[selinux]`, `AISB_SELINUX` | ## Environment and secrets | question | read | |---|---| | what is inherited | `AISB_IMPLICIT_ENV` (`TERM LANG LC_ALL LC_CTYPE TZ`) and the explicit `HOME`/`AISB`/`AISB_SESSION` in `build_argv()` | | how a value reaches the container | `build_env_file()` — a `0600` file, `NAME`, `NAME=VALUE` and `@file` forms; unset `--env NAME` **dies** on a real run and reports `env_missing=` in preview | | can a secret leak through output | `print_argv()` (redacts the env file), `print_policy()` (names only), `log_session()` (net, counts, rc, command). `tests/unit/04_env.sh`, `tests/e2e/12_sandbox.sh` | | what lands on disk per session | `session_init()` / `teardown()`: `$AISB_STATE_DIR/sessions/` (`chmod 700`), removed unless `--keep` | ## The pi bridge and thinking policy | question | read | |---|---| | what is copied vs rewritten | `pi_config_bridge()` — `apiKey` → `"$VAR"`, `!command` values dropped and reported, host paths dropped and reported, `auth.json` and `models-store.json` never touched | | why `settings.json` behaves oddly | the per-key merge in the entrypoint's seeding block, and `.aisb-settings.json.wrote` — a key you edit in the sandbox stays yours; keys you never touched follow the host | | when the run is refused for a credential | `pi_config_bridge()` + `cmd_doctor()`: unset **or empty** token variable → refused. `--pi-token-env NAME`, `--no-pi-token-env`, `--no-pi-config` | | "the model endpoint is not in the allowlist" warning | `pi_egress_hint()`, `pi_provider_hosts_of()` | | thinking level | `resolve_opts()` (validation of `off…max\|host`) and the seeding block: `AISB_PI_THINKING` is written over the merged settings every run; pi-only; `--no-pi-config` declines the policy too | ## Image, pinning, supply chain | question | read | |---|---| | what is in the image | `containerfile()` (git curl jq build tools python3 Node pi), `gate_containerfile()` (squid from the same base) | | why the tag is what it is | `image_hash()` (the policy, including the base digest, Node and pi versions) and `image_ref()`; `--install` changes it, so variants coexist | | digest pinning and freshness | `base_pin_digest()`, `base_digest_online()`, `base_age_days()`, `base_ref_of()`, `cmd_update()`; `base_ttl_days`, `base_auto_update`, `AISB_BASE_PULL_FALLBACK`, `AISB_SKOPEO`. Preview modes resolve **cache-only** — that is why `--dry-run` never touches the network | | "what version of squid filtered that" | the `aisb.*` image labels and `/etc/aisb/manifest.txt` inside the image (`image_hash()`/`containerfile()` write them) | | rollback | `cmd_update()` keeps the previous image and prints the `podman rmi` for it | | stale home | `ensure_home_volume()`, `cmd_clean()`'s `--volumes[=GLOB]` | ## The pipeline driver (`bin/ai-sandbox-autophase`) | question | read | |---|---| | where does my launcher come from | `ap_resolve_launcher()`, `ap_probe_rc()` (sources an rc file in a throwaway `bash -ic` and returns the *definition*; it never calls it), `ap_rc_list()`, `AISB_AP_RC_FILES` | | how the child command is built | `ap_build_runner()` / `ap_build_run()` — `printf %q` per argument; `--sandbox-args` defaults `--no-tty --no-interactive`; the prompt is one argument that may contain quotes, newlines and `$` | | what it shadows from the phase library | `ap_install_overrides()`: it first requires the library to provide `find_root next_unit phase_entries phase_next_unit unit_phase is_phase_end count_units sweep_finished_phases execute_unit unit_report notify_task commit_phase push_phase` (plus `first_prompt`/`fix_prompt`, kept as `ap_lib_*`), then shadows `pi`, `first_prompt`, `fix_prompt`, `run_validation`, `push_phase` — and nothing else. Order matters: the launcher resolves *before* `pi()` exists, or a launcher named `pi` would resolve to the shim | | why a push was refused | `ap_git_fingerprint()`, `ap_git_guard()`, `ap_git_env()`, `ap_push_pending()`; `--trust-git-changes` is the (loud) override | | redaction | `ap_redact()` — literal `--env NAME=value` and credentials inside remote URLs | | what runs where | `ap_preflight()`, `ap_gate_preview()` (`--validate-in sandbox\|host`), `ap_run_pipeline()`, `ap_status()` | | the contract without the skill installed | `tests/unit/10_autophase.sh` and `tests/fixtures/phase-lib/` | ## Vocabulary * **gate** — the per-session squid container, `--network=host`, bound to the host loopback on a free port ≥ `gate_port`, run as the `proxy` user with `--cap-drop=ALL`, destroyed with the session. `gate up` is the explicit long-lived exception. * **ruleset** — the `inet aisb` nft table installed inside the container's namespace: output `policy drop` with room for loopback, established/related, ICMP, the direct IP entries, the gate's address:port, and DNS only in `--no-gate` mode. Input `policy drop` too, so nothing is published in. * **`POL[...]` / `P_*` / `OPT_*` / `CFG_*`** — resolved policy / resolved lists / raw CLI flags / config-file values. Reading raw env vars elsewhere is the bug pattern; a helper called as `X="$(helper)"` runs in a subshell, so any `POL[..]` it sets is discarded. * **fail closed** — no allowlist in `allow` mode, missing policy file (78), uninstallable ruleset (77), unset `--env NAME`, unset-or-empty bridged token: all refusals, never degradations. * **escape hatch** — `--allow-unenforced`, `--insecure-keep-root`, `--net full`, `--mount`, `extra_args`, `AISB_EXTRA_ARGS`. Named to be uncomfortable; nothing uses them implicitly. ## FAQ (verify the version before quoting) | question | answer | |---|---| | `--print-policy` says "could not create the home cache volume"? | There is no usable podman (missing, or not rootless, or no `/etc/subuid` range) — `--print-policy` ensures the `aisb-home` volume exists, unlike `--dry-run`, which is the fully side-effect-free preview. Run `ai-sandbox --dry-run` to see the policy and `ai-sandbox doctor` for the host. | | Why exit 77? | The ruleset could not be installed, so the sandbox refused to run unfiltered. `ai-sandbox doctor`; check `podman info \| grep -i rootless` and `/etc/subuid`. Debug with `--allow-unenforced` only to see the error, not to ship it. | | Why "no egress allowlist configured"? | `allow` mode with nothing in it is a hard error. `--allow`, `.ai-sandbox/allow`, or `--net none`/`--net full` if you mean it. | | Can it read `~/.ssh`? | No — that path is not in the mount namespace unless you mounted it. Threat model #1 is what you *do* mount and allow. | | Can it rewrite my skills? | No. `~/.agents` is read-only and there is no flag; but its contents are *readable*, so treat the shared tree as published to the agent. | | Why is my project at `/project`? | Mirroring refused because the host path would bury a path the image or sandbox owns; `workdir_note=` says which, `--workdir` chooses. | | `npm ci` works but a package fails | That ecosystem needs another host (python: `files.pythonhosted.org`; rust: `static.crates.io`/`index.crates.io`; go: `sum.golang.org`). Or the package fetched from an allowlisted host — allowing a domain is not a security review. | | How do I let it push? | You don't, from inside. `--allow github.com` + `--env GH_TOKEN` or `--mount ~/.ssh:ro` does it, and the threat model explains the price. For pipelines, run `ai-sandbox-autophase`, which pushes on the host. | | How do I see what the gate allowed? | `ai-sandbox gate logs` (and `gate status`, `gate down`). Don't put secrets in URLs: the gate sees what flows through it. | | A stale container home after a rebuild? | It is the `aisb-home` volume. `ai-sandbox clean --volumes[=GLOB]`, or `--ephemeral-home` per run. | | How do I refresh the base? | `ai-sandbox update --check` then `ai-sandbox update` (or `build --refresh-base`). TTL re-resolves a digest, it never downloads on a preview. | | Do `--dry-run`/`--print-*` touch the network? | No — that determinism is deliberate. `inspect` is the one that asks the machine now. | | Is the container root? | It starts root with `CAP_NET_ADMIN` just long enough to install the ruleset, then `setpriv` to uid 1000 with no capabilities and `no-new-privileges`. `--insecure-keep-root` is the opposite. | | What is NOT protected? | `docs/threat-model.md`, and answer with it: allowlisted destinations, kernel/runtime escapes, anything you mounted, the cached home volume, DNS in `--no-gate` mode, the fact that a host-side push pushes what the agent wrote. |