Files
ai-sandbox/.agents/skills/doc-sandbox/references/source-map.md
T
ducoterra 209f167e43 add doc-sandbox: answer from the installed program, not memory
The explainer for questions about a flag, a config key, a refusal, a
mechanism or a security boundary. ai-sandbox generates its own
Containerfile, entrypoint, squid config and netfilter rules from one
script, so the text of the program is the documentation of the version
the user will run: the skill sets a strict ground-truth order -- the
installed script and --help, then the repo docs, then the tests as the
executable spec -- and its own fact sheet last, overridden by all of
them.

scripts/locate.sh resolves that order on an arbitrary machine: the
installed script and its version, the source tree behind it (via
$AISB_REPO, the script's own parent, $PWD, the git root), the docs with
their line counts, and the config/state/cache locations.

references/source-map.md maps a question topic to the function, doc and
test that hold the answer, plus the vocabulary (split_target,
classify_allows, gate vs --no-gate vs --proxy, POL[]/P_*/OPT_*) and the
FAQ. The skill names which safe surface answers what: the --dry-run,
--print-entrypoint, --print-containerfile and --print-policy previews
build nothing and reach no registry, while inspect, doctor --deep,
build, update and gate up do change the machine and are not for an
explanation. It records the two honest exceptions rather than
overclaiming -- --print-policy ensures the home volume, and --no-gate
hits DNS.

Read-only by construction: no build, no container, no network call, and
it reads pi_config_bridge() rather than a models.json holding a live
key. Its worst failure mode is an invented flag, so the answer format
requires the real spelling, the source it came from, what the thing does
not do, and a plain "no" plus the closest real thing when the answer is
no.

LLM-Generated: true
2026-10-29 08:37:51 -04:00

117 lines
12 KiB
Markdown

# 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/<basename>`; 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/<id>` (`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. |