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
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# 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. |
|
||||
Reference in New Issue
Block a user