102 lines
4.8 KiB
Bash
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
|