#!/usr/bin/env bash # tests/unit/01_args.sh -- argument handling. source "$TESTS/lib.sh" t_name "help / version" sb --help; assert_rc 0 "--help exits 0"; assert_contains "NETWORK" "--help documents the network section" sb --version; assert_rc 0 "--version exits 0" sb version; assert_contains "ai-sandbox" "version subcommand prints the name" t_name "flag -> harness pass-through" A="--allow example.com" # allow mode is the default and fails closed without this sb --dry-run $A --model sonnet; assert_rc 0 "unknown flag is accepted"; assert_contains "pi --model sonnet" "unknown flag and the rest are handed to pi" sb --dry-run $A file.txt --more; assert_contains "pi file.txt --more" "positional starts the command" sb --dry-run $A -- echo hi; assert_contains "echo hi" "after -- the command runs exactly as given" assert_absent "pi echo hi" "after -- the harness is replaced, not prefixed" assert_contains "--network pasta:--map-host-loopback" "and a --net after -- is not mistaken for a sandbox flag" sb --dry-run $A --shell; assert_contains "bash -li" "--shell runs bash" sb --dry-run $A --harness codex; assert_contains "codex" "--harness codex selects the command" sb --dry-run $A --harness none; assert_contains "bash -li" "--harness none drops to a shell" sb --dry-run --net none -- echo hi; assert_contains "echo hi" "--net= form before the command still parses" t_name "strict args" sb --strict-args --model sonnet; assert_rc 1 "strict mode rejects unknown flags" assert_contains "unknown option" "strict mode says 'unknown option'" sb --strict-args --al foo; assert_contains "did you mean --allow" "strict mode suggests a near miss" t_name "value validation" sb --net bogus --dry-run; assert_rc 1 "bad --net rejected"; assert_contains "--net must be none, allow or full" "names the valid values" sb --pids abc --dry-run; assert_rc 1 "bad --pids rejected" sb --cpus x4 --dry-run; assert_rc 1 "bad --cpus rejected" sb --memory huge --dry-run; assert_rc 1 "bad --memory rejected" sb --allow; assert_rc 1 "--allow with no value" sb --allow-file /nope/missing; assert_rc 1 "--allow-file must exist" sb --mount /nope/missing --dry-run; assert_rc 1 "--mount path must exist" t_name "publish takes a port, a pair, or an address and a pair" sb --dry-run $A --publish 8000; assert_rc 0 "a bare port parses" sb --dry-run $A --publish 18080:8000; assert_rc 0 "host:container parses" sb --dry-run $A --publish 0.0.0.0:8000:8000; assert_rc 0 "addr:host:container parses" sb --dry-run $A --publish '[::1]:8000:8000'; assert_rc 0 "bracketed ipv6 parses" sb --dry-run $A --publish=8000; assert_rc 0 "--publish= form parses" sb --strict-args --publish; assert_rc 1 "--publish is a known flag that needs a value" sb --strict-args --pub 8000 --dry-run $A; assert_contains "did you mean --publish" "a near miss suggests --publish" # Every one of these is a port list that *looks* right and is not. Silently reading one as # something else is how a config ends up publishing a port nobody wrote. t_name "publish specs that are refused" for bad in 0 99999 abc 8000/udp 8000:8000:8000:8000 '::1:8000:8000' '8000:' ':8000' '8000::9000' '0.0.0.0:8000' '*:8000:8000' '0.0.0.0:8000:8000:8000'; do sb_timeout 15 --dry-run $A --publish "$bad" assert_rc 1 "refuses '$bad'" done # Three fields where the first is not an address: podman would get 'bind on 8000' and fail with an # error that names none of our words, so the address has to look like an address here. sb_timeout 15 --dry-run $A --publish 8000:8000:8000 assert_rc 1 "refuses a port written where an address belongs" assert_contains "is not an address" "and says the three shapes that are valid" sb_timeout 15 --dry-run $A --publish all:8000:8000 assert_rc 1 "refuses a word for an address" sb --dry-run $A --publish box.internal:8000:8000 assert_contains "--publish box.internal:8000:8000" "a dotted host is taken as an address (and warned: not loopback)" assert_contains "anything that can route to this machine" " ... by the same warning an explicit IP gets" sb_timeout 15 --dry-run $A --publish 18080:8000 --publish 18080:9000 assert_rc 1 "two container ports on one host port is refused" assert_contains "published twice" "and says which" sb_timeout 15 --dry-run --net none --publish 8000 assert_rc 1 "--net none refuses to publish" assert_contains "cannot forward a port" "because podman would drop the mapping and run anyway" # -p is the harness's flag (claude -p prints), so it must stay a pass-through. sb --dry-run $A -p 8000 assert_contains "pi -p 8000" "-p belongs to the harness, not to the sandbox" t_name "fail-closed network policy" sb --dry-run; assert_rc 1 "allow mode with no allowlist refuses to start" assert_contains "Refusing to start wide open" "explains that it refuses to be wide open" sb --dry-run --net none; assert_rc 0 "--net none needs no allowlist"; assert_contains "--network none" "--net none maps to podman --network none" sb --dry-run --net full; assert_rc 0 "--net full is allowed (explicit opt-in)" assert_contains "unrestricted internet" "--net full warns" sb --dry-run --allow example.com; assert_rc 0 "one allow entry is enough"