#!/usr/bin/env bash set -uo pipefail # CASAN Plan-13 — Control Plane HITL approval inbox + delegation. # Deterministic; no app/model required. Proves delegation resolution, pending # inbox, approve/reject oversight hash-chain, SoD, and governed setting apply. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh" INBOX="$CASAN_HARNESS_ROOT/scripts/bash/approval-inbox.py" CP="$CASAN_HARNESS_ROOT/scripts/bash/control-plane-settings.py" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT export CASAN_APPROVAL_INBOX_FILE="$WORK/approval-inbox.json" export CASAN_CP_STORE_FILE="$WORK/settings.json" export CASAN_CP_KEY_DIR="$WORK/keys" export CASAN_CP_PUB="$WORK/cp.pub" PASS=0; FAIL=0 pass() { echo "PASS: $1"; PASS=$((PASS + 1)); } fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); } echo "===== Plan-13 HITL approval inbox + delegation =====" set +e python3 "$INBOX" resolve --project default --action settings.write.sensitive --risk high --sensitive > "$WORK/resolve-sensitive.json" RC=$? set -e 2>/dev/null || true [[ "$RC" -eq 2 ]] && grep -q '"requires_approval": true' "$WORK/resolve-sensitive.json" \ && pass "delegation resolver requires approval for high-risk sensitive setting" \ || fail "sensitive delegation did not require approval (rc=$RC)" python3 "$INBOX" submit \ --project default \ --action settings.write \ --target security.strict \ --risk high \ --sensitive \ --proposer alice \ --reason "tighten strict mode" \ --payload '{"key":"security.strict","value":true}' > "$WORK/proposal.json" 2>"$WORK/submit.err" PID="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["id"])' "$WORK/proposal.json")" [[ -n "$PID" ]] && pass "submit creates a pending approval proposal ($PID)" || fail "proposal id missing" python3 "$INBOX" list --status pending > "$WORK/pending.json" grep -q "$PID" "$WORK/pending.json" && pass "pending inbox lists submitted proposal" || fail "pending inbox missing proposal" set +e python3 "$INBOX" decide --id "$PID" --decision approve --approver alice --reason self >/dev/null 2>"$WORK/self.err" RC=$? set -e 2>/dev/null || true [[ "$RC" -eq 3 ]] && grep -q "sod_self_approval" "$WORK/self.err" \ && pass "SoD blocks self-approval" || fail "self-approval was not blocked (rc=$RC)" python3 "$INBOX" decide --id "$PID" --decision approve --approver bob --reason reviewed > "$WORK/approved.json" 2>"$WORK/approve.err" grep -q '"status": "approved"' "$WORK/approved.json" \ && pass "distinct approver approves proposal" || fail "approval decision failed" python3 "$INBOX" verify-audit >/dev/null 2>&1 \ && pass "oversight hash-chain verifies intact" || fail "oversight hash-chain failed" # Approved settings proposals are applied by the Control Panel API. The harness # primitive still provides the approval token shape used by that API; prove the # governed store accepts and audits it. python3 "$CP" set security.strict true --actor alice --reason "approved:$PID:reviewed" --approval "inbox:$PID:bob" >/dev/null 2>&1 \ && pass "approved proposal can apply via governed settings store" || fail "governed set failed after approval" python3 "$CP" verify-audit >/dev/null 2>&1 \ && pass "settings audit verifies after approved apply" || fail "settings audit failed after approved apply" python3 - "$CASAN_APPROVAL_INBOX_FILE" <<'PY' import json, sys d = json.load(open(sys.argv[1])) d["oversight"][0]["status"] = "tampered" json.dump(d, open(sys.argv[1], "w")) PY set +e python3 "$INBOX" verify-audit >/dev/null 2>"$WORK/tamper.err" RC=$? set -e 2>/dev/null || true [[ "$RC" -eq 1 ]] && pass "oversight hash-chain detects tampering" || fail "oversight tamper not detected (rc=$RC)" echo "" echo "===== CONTROL-PLANE HITL SUMMARY: PASS=$PASS FAIL=$FAIL =====" [[ "$FAIL" -eq 0 ]] || exit 1