harness(rolepolicy): prove the DEPLOYED gate enforces the role policy
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:
co-authored by
Claude Opus 5
parent
ad67ce2fde
commit
70dbee662d
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user