Files
CASAN/packages/casan-harness/tests/phase-chat-tenant-tests.sh

102 lines
4.8 KiB
Bash

#!/usr/bin/env bash
set -uo pipefail
# Plan-18 MVP-3 Track 9: chat state is tenant-partitioned, guarded,
# encrypted-at-rest as an offline snapshot, and scoped for tenant resources.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
S="$CASAN_HARNESS_ROOT/scripts/bash"
TURN="$S/chat-turn.py"
REPLAY="$S/chat-replay.py"
TS="$S/tenant-store.sh"
TC="$S/tenant-crypt.sh"
KS="$S/kill-switch.sh"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
export CASAN_STATE_ROOT="$WORK/state"
export CASAN_TENANT_STATE_ROOT="$WORK/tenants"
export CASAN_KILLSWITCH_DIR="$WORK/ks"
PASS=0; FAIL=0
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
rc_of() { set +e; "$@" >/dev/null 2>&1; echo $?; set -e 2>/dev/null || true; }
echo "===== Plan-18 MVP-3 chat tenant hardening ====="
CASAN_TENANT_ID=alpha python3 "$TURN" ask --message "Summarize Plan 18 status" --actor alice --role viewer --tenant alpha --chat-id tenant-chat > "$WORK/alpha.json"
CASAN_TENANT_ID=beta python3 "$TURN" ask --message "Summarize Plan 18 status" --actor bob --role viewer --tenant beta --chat-id tenant-chat > "$WORK/beta.json"
pA="$(CASAN_TENANT_ID=alpha bash "$TS" resolve chat/chat-turns.jsonl 2>/dev/null)"
pB="$(CASAN_TENANT_ID=beta bash "$TS" resolve chat/chat-turns.jsonl 2>/dev/null)"
{ [[ -f "$pA" && -f "$pB" && "$pA" != "$pB" ]]; } \
&& pass "18.9.1 tenant A/B chat audits are separate files" || fail "chat audit partitioning failed"
CASAN_TENANT_ID=alpha python3 "$REPLAY" verify-chain > "$WORK/alpha-chain.json"
CASAN_TENANT_ID=beta python3 "$REPLAY" verify-chain > "$WORK/beta-chain.json"
python3 - "$WORK/alpha-chain.json" "$WORK/beta-chain.json" <<'PY' \
&& pass "18.9.1 each tenant verifies only its own chat chain" || fail "tenant chat chain verify mismatch"
import json, sys
a = json.load(open(sys.argv[1]))
b = json.load(open(sys.argv[2]))
assert a["decision"] == "OK" and a["records"] == 1
assert b["decision"] == "OK" and b["records"] == 1
PY
[[ "$(rc_of env CASAN_TENANT_ID=alpha CASAN_CHAT_AUDIT_LOG="$pB" python3 "$REPLAY" verify-chain)" -ne 0 ]] \
&& pass "18.9.1 explicit cross-tenant audit path is denied" || fail "cross-tenant audit override was allowed"
[[ -f "$pA.enc" ]] \
&& pass "18.9.2 encrypted chat audit snapshot is written" || fail "encrypted audit snapshot missing"
CASAN_TENANT_ID=alpha bash "$TC" decrypt "$pA.enc" "$WORK/alpha.dec" >/dev/null 2>&1
grep -q "tenant-chat" "$WORK/alpha.dec" \
&& pass "18.9.2 tenant A decrypts its own chat audit snapshot" || fail "tenant A could not decrypt own audit"
CASAN_TENANT_ID=beta bash "$TC" decrypt "$pA.enc" "$WORK/beta.dec" >/dev/null 2>&1 || true
grep -q "tenant-chat" "$WORK/beta.dec" \
&& fail "18.9.2 tenant B read tenant A audit plaintext" \
|| pass "18.9.2 tenant B cannot decrypt tenant A audit snapshot"
bash "$KS" engage tenant alpha "chat incident" >/dev/null 2>&1
set +e
CASAN_TENANT_ID=alpha python3 "$TURN" ask --message "Summarize Plan 18 status" --actor alice --role viewer --tenant alpha --chat-id killed > "$WORK/killed.json"
RC_A=$?
CASAN_TENANT_ID=beta python3 "$TURN" ask --message "Summarize Plan 18 status" --actor bob --role viewer --tenant beta --chat-id alive > "$WORK/alive.json"
RC_B=$?
set -e 2>/dev/null || true
python3 - "$WORK/killed.json" "$WORK/alive.json" "$RC_A" "$RC_B" <<'PY' \
&& pass "18.9.3 tenant kill-switch halts only its own tenant" || fail "tenant kill-switch isolation failed"
import json, sys
k = json.load(open(sys.argv[1]))
a = json.load(open(sys.argv[2]))
assert int(sys.argv[3]) == 3
assert k["decision"] == "HALTED"
assert int(sys.argv[4]) == 0
assert a["success"] is True
PY
gLOG="$(CASAN_TENANT_ID=gamma bash "$TS" resolve telemetry/provider-usage.jsonl 2>/dev/null)"
dLOG="$(CASAN_TENANT_ID=delta bash "$TS" resolve telemetry/provider-usage.jsonl 2>/dev/null)"
printf '{"step":"chat","total_tokens":9000}\n' > "$gLOG"
printf '{"step":"chat","total_tokens":10}\n' > "$dLOG"
set +e
CASAN_TENANT_ID=gamma CASAN_COST_CUMULATIVE_BUDGET_TOKENS=1000 python3 "$TURN" ask --message "Summarize Plan 18 status" --actor g --role viewer --tenant gamma --chat-id quota > "$WORK/quota.json"
RC_G=$?
CASAN_TENANT_ID=delta CASAN_COST_CUMULATIVE_BUDGET_TOKENS=1000 python3 "$TURN" ask --message "Summarize Plan 18 status" --actor d --role viewer --tenant delta --chat-id quota > "$WORK/quota-ok.json"
RC_D=$?
set -e 2>/dev/null || true
python3 - "$WORK/quota.json" "$WORK/quota-ok.json" "$RC_G" "$RC_D" <<'PY' \
&& pass "18.9.3 tenant quota halts only over-budget tenant" || fail "tenant quota isolation failed"
import json, sys
q = json.load(open(sys.argv[1]))
ok = json.load(open(sys.argv[2]))
assert int(sys.argv[3]) == 3
assert q["decision"] == "HALTED"
assert int(sys.argv[4]) == 0
assert ok["success"] is True
PY
echo ""
echo "===== CHAT TENANT SUMMARY: PASS=$PASS FAIL=$FAIL ====="
[[ "$FAIL" -eq 0 ]] || exit 1