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
12 KiB
12 KiB
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 --mounts |
--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 theproxyuser with--cap-drop=ALL, destroyed with the session.gate upis the explicit long-lived exception. - ruleset — the
inet aisbnft table installed inside the container's namespace: outputpolicy dropwith room for loopback, established/related, ICMP, the direct IP entries, the gate's address:port, and DNS only in--no-gatemode. Inputpolicy droptoo, 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 asX="$(helper)"runs in a subshell, so anyPOL[..]it sets is discarded.- fail closed — no allowlist in
allowmode, 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. |