#!/usr/bin/env bash # tests/unit/09_agents.sh -- sharing the host ~/.agents (skills, prompts, AGENTS.md). # # The bridge binds and never copies, so the two things worth pinning down are that the # mount is there when it should be, and that it is read-only: a harness that could edit # your skills could edit the instructions you follow tomorrow on the host. Everything is # preview, so no container is started -- the real mount is checked in e2e/12_sandbox.sh. source "$TESTS/lib.sh" FIX="$WORK/agents" mkdir -p "$FIX/skills/demo-skill" "$FIX/prompts" CANARY_SKILL='SKILLCANARY-not-a-file-ai-sandbox-writes' printf 'name: demo-skill\n%s\n' "$CANARY_SKILL" > "$FIX/skills/demo-skill/SKILL.md" printf '# global instructions\n' > "$FIX/AGENTS.md" export AISB_AGENTS_DIR="$FIX" unset AISB_AGENTS AISB_AGENTS_DEST PROJ="$WORK/proj"; mkdir -p "$PROJ"; cd "$PROJ" || exit 1 MOUNT_RE="$FIX:/home/sandbox/.agents:ro" # The last line of --dry-run is the podman invocation; the order of the -v/--mount # arguments inside it is what decides whether the mount survives being nested in the # home volume, so it is part of what these tests check. inv() { printf '%s' "$OUT" | tail -1; } has_mount() { inv | grep -qF -- "$MOUNT_RE"; } t_name "shared by default, read-only, where a harness looks for user skills" sb --dry-run --net none assert_rc 0 "dry run" assert_contains "$FIX:/home/sandbox/.agents" "the host directory is mounted at \$HOME/.agents" assert_contains "$MOUNT_RE" "read-only" assert_absent "$FIX:/home/sandbox/.agents:rw" "and there is no rw form of it" if has_mount; then after="$(inv | sed "s|.*type=volume,source=[^ ]*target=/home/sandbox||")" [[ "$after" == *"$MOUNT_RE"* ]] && _ok "mounted after /home/sandbox itself, so it is not shadowed" \ || _bad "mounted after /home/sandbox itself, so it is not shadowed" else _bad "mounted after /home/sandbox itself, so it is not shadowed" "no agents mount at all" fi assert_absent "$FIX/skills" "the directory itself is mounted, not a cherry-picked skills/" assert_absent "$FIX/AGENTS.md:" "nor an AGENTS.md singled out" t_name "it is a bind mount, never a copy" sb --keep --print-policy --net none if grep -rlF "$CANARY_SKILL" "$AISB_STATE_DIR" "$AISB_CACHE_DIR" > /dev/null 2>&1; then _bad "no skill content is copied into ai-sandbox state" \ "found the canary under $AISB_STATE_DIR or $AISB_CACHE_DIR" else _ok "no skill content is copied into ai-sandbox state" fi if find "$AISB_STATE_DIR/sessions" -name 'SKILL.md' | grep -q .; then _bad "nothing from the tree lands in the session directory" else _ok "nothing from the tree lands in the session directory" fi assert_contains "agents=$FIX" "the policy reports the source" assert_contains "agents_dest=/home/sandbox/.agents" "and the destination" t_name "the same for every harness, cwd mount or not" sb --dry-run --net none --harness codex; assert_contains "$MOUNT_RE" "codex" sb --dry-run --net none --shell; assert_contains "$MOUNT_RE" "a shell" sb --dry-run --net none -- make test; assert_contains "$MOUNT_RE" "a command after --" sb --dry-run --net none --no-cwd; assert_contains "$MOUNT_RE" "--no-cwd does not take it away" sb --dry-run --net none --ephemeral-home assert_contains "$MOUNT_RE" "an ephemeral (tmpfs) home still gets it" assert_contains "/home/sandbox:rw,nosuid,nodev" "--ephemeral-home is still a tmpfs home" t_name "--no-agents does not share it" sb --dry-run --net none --no-agents assert_absent "$FIX:/home/sandbox/.agents" "--no-agents" sb_env AISB_AGENTS=0 --dry-run --net none assert_absent "$FIX:/home/sandbox/.agents" "AISB_AGENTS=0" printf 'agents = 0\n' > "$WORK/cfg/config" sb --print-policy --net none assert_absent "$FIX" "agents = 0 in the config file" assert_contains "agents=off" "and the policy says off" sb --dry-run --net none --agents assert_contains "$MOUNT_RE" "the CLI wins over the config file" rm -f "$WORK/cfg/config" t_name "AISB_AGENTS_DEST and agents_dest move the mount point" sb_env AISB_AGENTS_DEST=/srv/agents --dry-run --net none assert_contains "$FIX:/srv/agents:ro" "environment" printf 'agents_dest = /opt/dot-agents\n' > "$WORK/cfg/config" sb --dry-run --net none assert_contains "$FIX:/opt/dot-agents:ro" "config file" rm -f "$WORK/cfg/config" t_name "an explicit --mount on the same destination wins" mkdir -p "$WORK/hand-mounted" sb --dry-run --net none --mount "$WORK/hand-mounted:/home/sandbox/.agents:ro" assert_contains "$WORK/hand-mounted:/home/sandbox/.agents" "the hand-written mount is kept" assert_absent "$FIX:/home/sandbox/.agents" "and the automatic one steps aside (podman refuses duplicates)" assert_contains "already a --mount destination" "and it says why" t_name "--agents-dir chooses the source, and a bad one is an error" mkdir -p "$WORK/other-agents/skills/second" printf 'name: second\n' > "$WORK/other-agents/skills/second/SKILL.md" sb --dry-run --net none --agents-dir "$WORK/other-agents" assert_contains "$WORK/other-agents:/home/sandbox/.agents:ro" "--agents-dir" assert_absent "$FIX:/home/sandbox/.agents" "replacing the default rather than adding to it" sb_env AISB_AGENTS_DIR="$WORK/other-agents" --dry-run --net none assert_contains "$WORK/other-agents:/home/sandbox/.agents:ro" "AISB_AGENTS_DIR" sb --dry-run --net none --agents-dir "$WORK/agents" --agents-dir "$WORK/other-agents" assert_contains "$WORK/other-agents:/home/sandbox/.agents" "the last one wins" assert_count 1 "$WORK/other-agents:/home/sandbox/.agents" "mounted once, not twice" sb --dry-run --net none --agents-dir "$WORK/no-such-dir" assert_rc 1 "an explicit --agents-dir that does not resolve is refused" assert_contains "--agents-dir is not a readable directory" "with a reason" t_name "a leading ~ in a path means \$HOME" # shellcheck disable=SC2088 # quoted on purpose: the script expands ~, not the shell sb --dry-run --net none --agents-dir '~/no-such-dir-41d5' assert_contains "$HOME/no-such-dir-41d5" "expanded in a CLI path (see the error)" assert_rc 1 "and still refused, because it is not a directory" printf 'agents_dir = ~/no-such-dir-0e2b\n' > "$WORK/cfg/config" # AISB_AGENTS_DIR would outrank the config file, so this is the one case that needs it blank sb_env AISB_AGENTS_DIR= --dry-run --net none --agents assert_contains "$HOME/no-such-dir-0e2b" "expanded in a config-file path too" rm -f "$WORK/cfg/config" t_name "a missing default directory is a skip, not a failure" sb_env AISB_AGENTS_DIR="$WORK/nope" --dry-run --net none assert_rc 0 "no directory, no mount, no complaint" assert_absent "/home/sandbox/.agents" "nothing is mounted there" sb_env AISB_AGENTS_DIR="$WORK/nope" --print-policy --net none assert_contains "agents=off" "the policy reports it off" sb_env AISB_AGENTS_DIR="$WORK/nope" --dry-run --net none --agents assert_rc 0 "--agents with nothing to share is a warning, not a refusal" assert_contains "no readable directory" "and it says so" t_name "an unreadable directory is treated as absent" NOREAD="$WORK/mode-0700"; mkdir -p "$NOREAD/skills/x" && chmod 000 "$NOREAD" sb_env AISB_AGENTS_DIR="$NOREAD" --dry-run --net none assert_rc 0 "not readable, so not shared" assert_absent "$NOREAD:/home/sandbox/.agents" "and not mounted" chmod 755 "$NOREAD" t_name "a symlinked ~/.agents is followed" LINK="$WORK/agents-link"; ln -sfn "$FIX" "$LINK" sb_env AISB_AGENTS_DIR="$LINK" --dry-run --net none assert_contains "$LINK:/home/sandbox/.agents:ro" "mounted under the name you configured" t_name "the mount never widens the sandbox in any other way" sb --dry-run --net none LINE="$(inv)" assert_absent "--privileged" "still unprivileged" assert_contains "--cap-drop=ALL" "still capability-free" assert_contains "--security-opt no-new-privileges" "still no new privileges" [[ "$(printf '%s' "$LINE" | grep -o "$FIX" | wc -l | tr -d ' ')" == "1" ]] \ && _ok "the host directory appears exactly once" || _bad "the host directory appears exactly once"