feat: add governed chat agent selection

This commit is contained in:
thanhnv
2026-07-08 22:19:51 +09:00
parent 06d8d31b16
commit 004afa73c9
18 changed files with 677 additions and 39 deletions
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
set -uo pipefail
# Plan-18 MVP-2 Track 4: agent/skill selection is governed by registry + RBAC.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
RESOLVER="$CASAN_HARNESS_ROOT/scripts/bash/chat-agent-resolver.py"
TURN="$CASAN_HARNESS_ROOT/scripts/bash/chat-turn.py"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
export CASAN_STATE_ROOT="$WORK/state"
PASS=0; FAIL=0
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
echo "===== Plan-18 MVP-2 agent/skill selection ====="
python3 "$RESOLVER" list-agents --role viewer > "$WORK/list.json"
python3 - "$WORK/list.json" <<'PY' \
&& pass "agent registry lists versioned agents with role visibility" || fail "agent registry listing invalid"
import json, sys
d = json.load(open(sys.argv[1]))
ids = {a["id"]: a for a in d["agents"]}
assert d["success"] is True
assert {"evidence-reader", "ops-operator", "codegen-draft"}.issubset(ids)
assert ids["evidence-reader"]["allowed_for_role"] is True
assert ids["ops-operator"]["allowed_for_role"] is False
PY
python3 "$RESOLVER" bind --agent evidence-reader --skill evidence-summary --role viewer --actor alice --project default --tenant tenant-a > "$WORK/viewer.json"
python3 - "$WORK/viewer.json" <<'PY' \
&& pass "viewer can bind read-only evidence-reader" || fail "viewer could not bind read-only agent"
import json, sys
d = json.load(open(sys.argv[1]))
assert d["success"] is True
assert d["decision"] == "BOUND"
assert d["agent_selected"] == "evidence-reader"
assert d["delegation_level"] == 0
assert d["tool_allowlist"] == []
PY
set +e
python3 "$RESOLVER" bind --agent ops-operator --role viewer --actor alice --project default --tenant tenant-a > "$WORK/viewer-op.json"
RC=$?
set -e 2>/dev/null || true
python3 - "$WORK/viewer-op.json" "$RC" <<'PY' \
&& pass "viewer cannot select operator agent" || fail "viewer selected operator agent"
import json, sys
d = json.load(open(sys.argv[1]))
assert int(sys.argv[2]) == 2
assert d["success"] is False
assert d["decision"] == "DENIED"
assert d["reason"] == "role_not_allowed_for_agent"
PY
set +e
python3 "$RESOLVER" bind --agent evidence-reader --role viewer --actor alice --project default --tenant tenant-a --target-tenant tenant-b > "$WORK/cross-tenant.json"
RC=$?
set -e 2>/dev/null || true
python3 - "$WORK/cross-tenant.json" "$RC" <<'PY' \
&& pass "cross-tenant agent binding is denied by RBAC" || fail "cross-tenant agent binding was allowed"
import json, sys
d = json.load(open(sys.argv[1]))
assert int(sys.argv[2]) == 2
assert d["success"] is False
assert d["decision"] == "DENIED"
assert "rbac_denied" in d["reason"]
PY
set +e
python3 "$RESOLVER" bind --agent evidence-reader --role viewer --actor alice --project default --tenant tenant-a --delegation-level 1 > "$WORK/delegation.json"
RC=$?
set -e 2>/dev/null || true
python3 - "$WORK/delegation.json" "$RC" <<'PY' \
&& pass "delegation above agent max requires approval" || fail "delegation escalation was not held"
import json, sys
d = json.load(open(sys.argv[1]))
assert int(sys.argv[2]) == 3
assert d["success"] is False
assert d["decision"] == "REQUIRES_APPROVAL"
assert d["requires_approval"] is True
PY
set +e
python3 "$RESOLVER" bind --agent ops-operator --role operator --actor bob --project default --tenant tenant-a --tool shell > "$WORK/tool.json"
RC=$?
set -e 2>/dev/null || true
python3 - "$WORK/tool.json" "$RC" <<'PY' \
&& pass "tool outside agent allowlist is blocked" || fail "tool outside allowlist was accepted"
import json, sys
d = json.load(open(sys.argv[1]))
assert int(sys.argv[2]) == 2
assert d["success"] is False
assert d["decision"] == "DENIED"
assert d["action_gate"]["outcome"] == "BLOCK"
PY
set +e
python3 "$TURN" ask --message "run tests" --agent ops-operator --role viewer --actor alice --chat-id agent-chat --tenant tenant-a > "$WORK/turn-deny.json"
RC=$?
set -e 2>/dev/null || true
python3 - "$WORK/turn-deny.json" "$RC" <<'PY' \
&& pass "chat-turn denies unauthorized selected agent before operator runtime" || fail "chat-turn bypassed agent resolver"
import json, sys
d = json.load(open(sys.argv[1]))
assert int(sys.argv[2]) == 2
assert d["success"] is False
assert d["decision"] == "DENIED"
assert d["agent_selected"] == "ops-operator"
PY
python3 "$RESOLVER" verify-audit > "$WORK/audit.out" \
&& grep -q "CHAT_AGENT_AUDIT ok=true" "$WORK/audit.out" \
&& pass "agent binding audit hash chain verifies" || fail "agent binding audit invalid"
echo ""
echo "===== CHAT AGENT SUMMARY: PASS=$PASS FAIL=$FAIL ====="
[[ "$FAIL" -eq 0 ]] || exit 1
@@ -37,7 +37,7 @@ ids = {a["id"] for a in d["actions"]}
assert {"run-chat-tests", "build-evidence-pack", "verify-evidence-pack"}.issubset(ids)
PY
python3 "$TURN" ask --message "run tests" --actor bob --chat-id op1 > "$WORK/run.json"
python3 "$TURN" ask --message "run tests" --actor bob --role operator --chat-id op1 > "$WORK/run.json"
python3 - "$WORK/run.json" <<'PY' \
&& pass "registered run tests action executes through chat-turn" || fail "registered action did not complete"
import json, os, sys