share the host ~/.agents read-only, so skills come along
A sandboxed harness found none of your skills, prompt templates or standing AGENTS.md, because those live in ~/.agents and only the project was mounted. It is now bind-mounted at /home/sandbox/.agents by default -- bound, never copied -- with --no-agents to opt out and --agents-dir for a curated set, reported by --print-policy and by doctor. Read-only is the load-bearing part and has no escape hatch: a skill is an instruction the model follows later, on the host, so a sandbox that could write ~/.agents had a persistence path out of itself, planted as a helpful-looking SKILL.md. It holds even under --insecure-keep-root, the mount lands after /home/sandbox itself so it cannot be shadowed by the home volume or tmpfs, and an explicit --mount on the same destination wins because podman refuses duplicates. A missing or unreadable directory is a silent skip like the pi bridge's; an explicit --agents-dir that resolves to nothing is a hard error. The exposure is stated in docs/threat-model.md: the tree, its .git history and anything pasted into a skill are readable by whatever runs in there. tests/unit/09_agents.sh covers resolution, precedence and the bind-never-copy canary; tests/e2e/12_sandbox.sh mounts it in a real container, checks the host tree survives a run byte for byte and that even root inside the sandbox cannot write it. LLM-Generated: true
This commit is contained in:
@@ -102,6 +102,53 @@ assert_contains "cached" "the home cache volume persists across runs"
|
||||
sb $DARK --ephemeral-home -- bash -c 'test -f "$HOME/cached-marker" && echo LEAK || echo isolated'
|
||||
assert_contains "isolated" "an ephemeral home does not see it"
|
||||
|
||||
t_name "the host ~/.agents arrives as \$HOME/.agents, read-only"
|
||||
AG="$WORK/host-agents"
|
||||
mkdir -p "$AG/skills/demo-skill" "$AG/prompts"
|
||||
printf 'name: demo-skill\ninstructions: always use the demo skill\n' > "$AG/skills/demo-skill/SKILL.md"
|
||||
printf 'a prompt template\n' > "$AG/prompts/demo-prompt.md"
|
||||
printf '# standing instructions\n' > "$AG/AGENTS.md"
|
||||
export AISB_AGENTS_DIR="$AG"
|
||||
|
||||
sb $DARK -- bash -c 'ls -A "$HOME/.agents" | tr "\n" " "; echo; cat "$HOME/.agents/skills/demo-skill/SKILL.md"; cat "$HOME/.agents/AGENTS.md"'
|
||||
assert_contains "skills" "the skills directory is where a harness looks for user-level skills"
|
||||
assert_contains "always use the demo skill" "a SKILL.md is readable at the real path"
|
||||
assert_contains "# standing instructions" "and so is the global AGENTS.md"
|
||||
|
||||
sb $DARK -- bash -c '
|
||||
touch "$HOME/.agents/skills/demo-skill/injected" 2>/dev/null && echo "BAD: skills writable" || echo skills_locked=ok
|
||||
mkdir "$HOME/.agents/evil-skill" 2>/dev/null && echo "BAD: could add a skill" || echo add_locked=ok
|
||||
touch "$HOME/scratch-still-works" && echo home_still_writable
|
||||
'
|
||||
assert_contains "skills_locked=ok" "a sandboxed agent cannot rewrite the skills it is reading"
|
||||
assert_contains "add_locked=ok" "nor plant a new one"
|
||||
assert_contains "home_still_writable" "while the rest of home keeps working"
|
||||
sb $DARK --insecure-keep-root -- bash -c 'touch "$HOME/.agents/skills/demo-skill/injected" 2>/dev/null && echo "BAD: writable as root" || echo root_still_locked'
|
||||
assert_contains "root_still_locked" "not even --insecure-keep-root gets to write your skills"
|
||||
BEFORE="$(find "$AG" | sort)"
|
||||
sb $DARK -- bash -c 'echo pwned >> "$HOME/.agents/AGENTS.md" 2>/dev/null; rm -f "$HOME/.agents/prompts/demo-prompt.md" 2>/dev/null; true'
|
||||
AFTER="$(find "$AG" | sort)"
|
||||
assert_eq "$BEFORE" "$AFTER" "the host directory is byte-for-byte untouched by the run"
|
||||
grep -q pwned "$AG/AGENTS.md" && _bad "appends from the container do not land on the host" || _ok "appends from the container do not land on the host"
|
||||
|
||||
sb $DARK --ephemeral-home -- bash -c 'cat "$HOME/.agents/skills/demo-skill/SKILL.md"'
|
||||
assert_contains "always use the demo skill" "a tmpfs home still gets the mount nested inside it"
|
||||
sb $DARK --read-only -- bash -c 'cat "$HOME/.agents/AGENTS.md"'
|
||||
assert_contains "# standing instructions" "and so does a read-only root filesystem"
|
||||
# podman creates the mount point inside the home volume, so the directory can outlive a
|
||||
# run that shared it; what --no-agents guarantees is that none of the host is in it.
|
||||
sb $DARK --no-agents -- bash -c 'ls -A "$HOME/.agents" 2>/dev/null | tr "\n" " "; test -e "$HOME/.agents/skills/demo-skill" && echo PRESENT || echo ABSENT'
|
||||
assert_contains "ABSENT" "--no-agents leaves no host skill visible in the container"
|
||||
assert_absent "demo-skill" "not even as a directory name"
|
||||
|
||||
# a project's own .agents needs no bridge: it is inside the project mount. Both are
|
||||
# visible at once, which is what a harness that reads both (pi) expects.
|
||||
mkdir -p "$WORK_DIR/.agents/skills/project-skill"
|
||||
printf 'name: project-skill\ninstructions: project only\n' > "$WORK_DIR/.agents/skills/project-skill/SKILL.md"
|
||||
sb $DARK -- bash -c 'cat /project/.agents/skills/project-skill/SKILL.md; ls "$HOME/.agents/skills" | tr "\n" " "'
|
||||
assert_contains "project only" "the project's own .agents comes in with the project mount"
|
||||
assert_contains "demo-skill" "and the user-level one is still alongside it"
|
||||
|
||||
t_name "read-only root filesystem"
|
||||
sb $DARK --read-only -- bash -c 'touch /usr/bin/nope 2>/dev/null && echo "BAD: wrote to rootfs" || echo rootfs_locked=ok; touch /project/rw-still-works && echo project_still_writable'
|
||||
assert_contains "rootfs_locked=ok" "--read-only blocks writes outside mounts"
|
||||
|
||||
Reference in New Issue
Block a user