Extracted from pi-workspaces/memo-inbox/.pi/extensions/memo-guard.ts, which has enforced these patterns in production since 2026-07. Exports: - inside() / safeRealPath() / makePathResolver(): path containment that resolves symlinks before checking, so a link inside an allowed root cannot escape it - registerRestrictedRead(): a path-restricted 'read' that shadows the built-in. Required rather than optional: pi emits the skills block only when a tool named 'read' is active and skill bodies load through it, while the built-in 'read' accepts absolute paths and could reach the service's credential files - installGuard(): the two capability layers, setActiveTools plus a tool_call block, re-asserted on resources_discover as well as session_start - truncate(): byte-aware truncation ahead of pi's 50 KB / 2000 line caps - registerBridgeTools() / fetchBridgeSpecs(): loopback HTTP bridge, with the baseUrl asserted to be loopback. Since registerTool accepts a plain JSON Schema object, the backend can own the schema instead of a drifting copy Verified against a real pi process with zero model tokens (shared/extensions/tests/run-guard-checks.sh, 14 assertions): active tools are exactly the declared set, the read override wins with source=cli, the skills section is present and contains only the scenario's own skill, and reads of an outside file, a ../ traversal and an absolute path to ~/.config/curator/curator.env are all denied. The deny-path fixture is named .txt and renamed to .env only inside the temp work directory, because verify-no-secrets.sh correctly refused to track a file called *.env.sample.
63 lines
2.0 KiB
Bash
Executable File
63 lines
2.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Zero-token verification of pi-guard-base against a real pi process.
|
|
#
|
|
# Drives pi with `get_state` only, so no model call is billed. Checks the two
|
|
# capability layers, the `read` override, skill reachability, path containment
|
|
# (including symlink/traversal/absolute escapes) and truncation.
|
|
#
|
|
# Usage: shared/extensions/tests/run-guard-checks.sh
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO="$(git -C "$HERE" rev-parse --show-toplevel)"
|
|
PI_BIN="${PI_BIN:-/home/claw/.npm-global/bin/pi}"
|
|
|
|
WORK="$(mktemp -d /tmp/pi-guard-checks.XXXXXX)"
|
|
trap 'rm -rf "$WORK"' EXIT
|
|
|
|
cp -r "$HERE/fixtures/." "$WORK/"
|
|
mv "$WORK/secret-lookalike.txt" "$WORK/secret-lookalike.env"
|
|
sed -e "s|__FIXTURES__|$WORK|g" \
|
|
-e "s|__GUARD_BASE__|$REPO/shared/extensions/pi-guard-base.ts|" \
|
|
"$HERE/guard-probe.ts.in" > "$WORK/guard-ext.ts"
|
|
|
|
OUT="$(printf '{"id":"1","type":"get_state"}\n' | timeout 120 "$PI_BIN" \
|
|
--mode rpc --no-session --no-builtin-tools \
|
|
--no-extensions -e "$WORK/guard-ext.ts" \
|
|
--no-skills --skill "$WORK/.pi/skills/guard-skill" \
|
|
--no-prompt-templates --no-themes --approve \
|
|
--provider zenmux --model openai/gpt-5.6-luna 2>&1 >/dev/null | grep '^GUARD_')"
|
|
|
|
echo "$OUT"
|
|
echo
|
|
|
|
fails=0
|
|
expect() {
|
|
if grep -qxF "GUARD_$1" <<<"$OUT"; then
|
|
echo " PASS $1"
|
|
else
|
|
echo " FAIL $1 (actual: $(grep "^GUARD_${1%%=*}=" <<<"$OUT" || echo '<absent>'))"
|
|
fails=$((fails + 1))
|
|
fi
|
|
}
|
|
|
|
echo "== assertions =="
|
|
expect 'ACTIVE=["probe_noop","read"]'
|
|
expect 'READ_SOURCE=["cli"]'
|
|
expect 'SP_HAS_SKILLS=true'
|
|
expect 'SP_SKILLNAMES=["guard-skill"]'
|
|
expect 'INSIDE_same=true'
|
|
expect 'INSIDE_child=true'
|
|
expect 'INSIDE_escape=false'
|
|
expect 'INSIDE_prefix_trap=false'
|
|
expect 'RESOLVE_skill=ALLOW'
|
|
expect 'RESOLVE_outside=DENY'
|
|
expect 'RESOLVE_traversal=DENY'
|
|
expect 'RESOLVE_abs_home=DENY'
|
|
expect 'TRUNC_applied=true len=true'
|
|
expect 'REALPATH_missing_throws=yes'
|
|
|
|
echo
|
|
if [ "$fails" -eq 0 ]; then echo "RESULT: all checks passed"; else echo "RESULT: $fails check(s) failed"; fi
|
|
exit $((fails > 0))
|