harness(rolepolicy): prove the DEPLOYED gate enforces the role policy
deploy / test (push) Successful in 4m59s
deploy / build (push) Successful in 1m0s

The role policy was unit-tested against the script the code generates.
This probes the script the SERVER INSTALLED, inside a live mission
container: the verifier's Write exits 2 with its reason, the lead's
identical Write exits 0, the verifier's Read and another role's Edit exit
0, and the denial the deployed gate wrote names role-verifier-readonly and
agent_type verifier. 5/5 on prod.

Three of the four probes are negative controls. A gate that refused
everything would pass the first and be worthless — the same trade the
module's header refuses. 'Compiled in and CI-green' and 'enforced by the
artifact in production' are different claims; the gap between them is
this module's history.

The record is matched with a shell glob on the raw JSON line, not a
nested python -c: the first version could not survive quoting through
bash, ssh and sh, and reported an empty record while the gate had written
a correct one.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
Omar Sobh
2026-09-22 09:42:07 -05:00
co-authored by Claude Opus 5
parent ad67ce2fde
commit 70dbee662d
2 changed files with 102 additions and 1 deletions
+13
View File
@@ -535,6 +535,19 @@ Rendered into the same guest script as the floor, so the shell and the
Rust predicate cannot disagree; shell tests run the generated script and
check that the lead's identical write is still allowed.
**Proven against the deployed artifact**, not only the unit-tested one
(`rolepolicy` scenario, 5/5): payloads piped through the gate script the
server actually installed, inside a live mission container. The verifier's
`Write` exits 2 with the reason; the lead's identical `Write` exits 0; the
verifier's `Read` and another role's `Edit` exit 0; and the denial the
deployed gate wrote names `rule: role-verifier-readonly` and
`agent_type: verifier`. Three of the four probes are negative controls on
purpose — a gate that refused everything would pass the first one and be
worthless. "Compiled in and CI-green" and "enforced by the artifact in
production" are different claims, and the gap between them is this
module's whole history (a gate installed and inert; an `--agents` list
that silently did nothing until 2.1.243 rejected it).
**The real gap the paper names, and we do not have:** `arg_provenance`.
Our gate sees a command string and cannot tell a URL the operator supplied
from one a fetched web page supplied — so "no outbound action derived from
+89 -1
View File
@@ -1365,6 +1365,90 @@ scenario_door() {
ssh "$HOST" "docker exec clawmates_postgres_1 psql -U postgres -d clawmates -tAc \"delete from outbox where subject in ('harness: judge spend','Onboarding') and recipient in ('[email protected]','[email protected]');\"" >/dev/null
}
# ── Scenario: rolepolicy — the DEPLOYED gate enforces a role's limits ──
#
# The gate's role policy is unit-tested against the script the code
# generates. This checks the script the SERVER INSTALLED, inside a real
# mission container, by piping hook payloads through it — because "compiled
# in and CI-green" and "enforced by the artifact in production" are
# different claims, and this module's history is full of the gap between
# them (a gate installed and inert, an --agents list that silently did
# nothing until 2.1.243 rejected it).
#
# Three of the four probes are negative controls. A gate that refused
# everything would pass the first one and is worthless: the lead's
# identical write, the verifier's read and another role's edit must all
# still go through.
ROLEPROBE_BODY=$(cat <<JSON
{"title":"verify: the deployed gate enforces the role policy",
"template_kind":"research_and_code",
"team_template_id":"$TEAM_TEMPLATE",
"repo_id":"$REPO_ID",
"description":"Probes the installed tool-gate script with role-tagged payloads.",
"phases":[
{"kind":"coding","order_idx":0,"config":{"commit_policy":"always","max_iterations":1,
"task":"Create ROLEPROBE.md at the repository root containing the single word: probe. Create no other files."}}
]}
JSON
)
scenario_rolepolicy() {
local token mission container waited rc rec
token=$(mint_session) || { norun "rolepolicy: could not mint a session"; return 1; }
mission=$(create_mission "$token" "$(echo "$ROLEPROBE_BODY" | tr -d '\n')") \
|| { norun "rolepolicy: mission create failed"; return 1; }
echo " rolepolicy: mission=$mission"
api "$token" PATCH "/api/missions/$mission/status" '{"status":"running"}' >/dev/null
# The container is the artifact under test; it exists only while the
# mission runs, so this waits for the gate file rather than for the
# mission, and gives up loudly rather than reporting a pass it did not earn.
container="cm-runtime-mission-$(printf '%s' "$mission" | tr -d -)"
waited=0
while [ "$waited" -lt 300 ]; do
ssh "$HOST" "docker exec $container test -f /root/toolhooks/tool-gate.sh" 2>/dev/null && break
sleep 5; waited=$((waited + 5))
done
if ! ssh "$HOST" "docker exec $container test -f /root/toolhooks/tool-gate.sh" 2>/dev/null; then
norun "rolepolicy: no gate script in $container after ${waited}s — nothing to probe"
return 1
fi
# `probe <want-exit> <label> <payload>`
probe() {
local want="$1" label="$2" out
out=$(printf '%s' "$3" | ssh "$HOST" "docker exec -i $container sh /root/toolhooks/tool-gate.sh" 2>&1; echo "rc=$?")
rc=${out##*rc=}
if [ "$rc" = "$want" ]; then
pass "rolepolicy: $label (exit $rc)"
else
fail "rolepolicy: $label — expected exit $want, got $rc: $(printf '%s' "$out" | head -c 140)"
fi
}
probe 2 "the verifier's write is refused by the installed gate" \
'{"tool_name":"Write","agent_type":"verifier","tool_input":{"file_path":"/mission/repo/src/lib.rs","content":"x"}}'
probe 0 "the lead's identical write still goes through" \
'{"tool_name":"Write","tool_input":{"file_path":"/mission/repo/src/lib.rs","content":"x"}}'
probe 0 "the verifier can still read" \
'{"tool_name":"Read","agent_type":"verifier","tool_input":{"file_path":"/mission/repo/src/lib.rs"}}'
probe 0 "a role the policy does not name is untouched" \
'{"tool_name":"Edit","agent_type":"explorer","tool_input":{"file_path":"/mission/repo/a.rs","old_string":"a","new_string":"b"}}'
# The record the refusal left, read from the container the gate wrote it
# in. Matched with `case` on the raw line rather than parsed: the line is
# one JSON object and the three facts are literals in it, so a shell glob
# says what a nested python -c inside a double-quoted ssh could not
# survive the quoting to say.
rec=$(ssh "$HOST" "docker exec $container tail -1 /root/toolhooks/denied.jsonl 2>/dev/null" | tr -d '\r')
case "$rec" in
*'"rule":"role-verifier-readonly"'*'"agent_type":"verifier"'*)
pass "rolepolicy: the denial names its rule and the role that made the call" ;;
'') fail "rolepolicy: the gate refused the write but wrote no denial record" ;;
*) fail "rolepolicy: denial record does not name the rule and role: $(printf '%s' "$rec" | head -c 160)" ;;
esac
}
# ── Scenario: multi-role with a real test suite ──────────────────
#
# The workload that failed with `COMMIT_EDITMSG: Permission denied` under the
@@ -2021,6 +2105,9 @@ case "${1:-all}" in
door)
scenario_door
;;
rolepolicy)
scenario_rolepolicy
;;
research-only)
run_scenario research-only "$(echo "$RESEARCH_ONLY_BODY" | tr -d '\n')" assert_research_only no-checkout
;;
@@ -2065,6 +2152,7 @@ case "${1:-all}" in
run_scenario goodhart "$(echo "$GOODHART_BODY" | tr -d '\n')" assert_goodhart
run_scenario gatepolicy "$(echo "$GATEPOLICY_BODY" | tr -d '\n')" assert_gatepolicy
scenario_door
scenario_rolepolicy
run_scenario research-only "$(echo "$RESEARCH_ONLY_BODY" | tr -d '\n')" assert_research_only no-checkout
run_scenario research-vm "$(echo "$RESEARCH_VM_BODY" | tr -d '\n')" assert_research_only no-checkout
run_scenario benchmark "$(echo "$BENCHMARK_BODY" | tr -d '\n')" assert_benchmark
@@ -2077,7 +2165,7 @@ case "${1:-all}" in
scenario_drain_midmission
;;
*)
die "unknown scenario: $1 (selftest|uids|chain|multirole|noop|microvm|canary|glm|kimi|gatecap|goodhart|gatepolicy|door|research-only|research-vm|benchmark|security|refactor|composed|roster|local-ornith|capacity|drain-midmission|all)"
die "unknown scenario: $1 (selftest|uids|chain|multirole|noop|microvm|canary|glm|kimi|gatecap|goodhart|gatepolicy|door|rolepolicy|research-only|research-vm|benchmark|security|refactor|composed|roster|local-ornith|capacity|drain-midmission|all)"
;;
esac