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