Files
CASAN/packages/casan-harness/tests/phase-control-plane-hitl-tests.sh
T
2026-07-08 19:07:35 +09:00

85 lines
3.7 KiB
Bash

#!/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