132 lines
4.6 KiB
Bash
Executable File
132 lines
4.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
|
|
# Plan-18 MVP-1 Operator mode: registered actions only, action-gate enforced.
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
|
|
OP="$CASAN_HARNESS_ROOT/scripts/bash/chat-operator.py"
|
|
TURN="$CASAN_HARNESS_ROOT/scripts/bash/chat-turn.py"
|
|
ROUTER="$CASAN_HARNESS_ROOT/scripts/bash/prompt-mode-router.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-1 operator mode ====="
|
|
|
|
python3 "$ROUTER" classify --message "run tests" > "$WORK/router.json"
|
|
python3 - "$WORK/router.json" <<'PY' \
|
|
&& pass "prompt router maps registered action to OPERATOR" || fail "router did not emit OPERATOR"
|
|
import json, sys
|
|
d = json.load(open(sys.argv[1]))
|
|
assert d["mode"] == "OPERATOR"
|
|
assert d["side_effect_allowed"] is True
|
|
assert "ACTION_GATE" in d["gates"]
|
|
PY
|
|
|
|
python3 "$OP" list-actions > "$WORK/actions.json"
|
|
python3 - "$WORK/actions.json" <<'PY' \
|
|
&& pass "operator action registry lists registered actions" || fail "operator registry missing actions"
|
|
import json, sys
|
|
d = json.load(open(sys.argv[1]))
|
|
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 --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
|
|
d = json.load(open(sys.argv[1]))
|
|
assert d["success"] is True
|
|
assert d["mode"] == "OPERATOR"
|
|
assert d["decision"] == "ACTION_COMPLETED"
|
|
assert d["action"]["id"] == "run-chat-tests"
|
|
assert d["action_gate"]["outcome"] == "ALLOW"
|
|
assert d["sources"] and d["sources"][0]["envelope"]["verified"] is True
|
|
PY
|
|
|
|
set +e
|
|
python3 "$TURN" ask --message "execute arbitrary shell command pwd" --actor bob --chat-id op1 > "$WORK/free.json"
|
|
RC=$?
|
|
set -e 2>/dev/null || true
|
|
python3 - "$WORK/free.json" "$RC" <<'PY' \
|
|
&& pass "free command is not supported" || fail "free command was accepted"
|
|
import json, sys
|
|
d = json.load(open(sys.argv[1]))
|
|
assert int(sys.argv[2]) != 0
|
|
assert d["success"] is False
|
|
assert d["mode"] == "NOT_SUPPORTED"
|
|
assert d["decision"] in {"NOT_SUPPORTED", "DENIED"}
|
|
PY
|
|
|
|
cat > "$WORK/danger-actions.json" <<'JSON'
|
|
{
|
|
"version": 1,
|
|
"actions": [
|
|
{
|
|
"id": "danger",
|
|
"label": "Danger",
|
|
"description": "Should be blocked by action-gate before execution.",
|
|
"triggers": ["danger"],
|
|
"command": ["bash", "-lc", "touch ${CASAN_STATE_ROOT}/marker && rm -rf /"],
|
|
"writes": [],
|
|
"timeout_s": 5
|
|
},
|
|
{
|
|
"id": "net",
|
|
"label": "Network",
|
|
"description": "Should require approval before execution.",
|
|
"triggers": ["network"],
|
|
"command": ["curl", "https://example.com"],
|
|
"writes": [],
|
|
"timeout_s": 5
|
|
}
|
|
]
|
|
}
|
|
JSON
|
|
|
|
set +e
|
|
CASAN_OPERATOR_ACTIONS_FILE="$WORK/danger-actions.json" python3 "$OP" run --action danger --message danger --actor bob --chat-id op2 > "$WORK/danger.json"
|
|
RC=$?
|
|
set -e 2>/dev/null || true
|
|
python3 - "$WORK/danger.json" "$RC" "$CASAN_STATE_ROOT/marker" <<'PY' \
|
|
&& pass "action-gate BLOCK prevents registered dangerous command execution" || fail "dangerous registered command was not blocked"
|
|
import json, os, 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"
|
|
assert not os.path.exists(sys.argv[3])
|
|
PY
|
|
|
|
set +e
|
|
CASAN_OPERATOR_ACTIONS_FILE="$WORK/danger-actions.json" python3 "$OP" run --action net --message network --actor bob --chat-id op2 > "$WORK/net.json"
|
|
RC=$?
|
|
set -e 2>/dev/null || true
|
|
python3 - "$WORK/net.json" "$RC" <<'PY' \
|
|
&& pass "action-gate REQUIRE_APPROVAL holds network action" || fail "network action did not require approval"
|
|
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["action_gate"]["outcome"] == "REQUIRE_APPROVAL"
|
|
PY
|
|
|
|
python3 "$CASAN_HARNESS_ROOT/scripts/bash/chat-readonly.py" verify-audit > "$WORK/audit.out" \
|
|
&& grep -q "CHAT_AUDIT ok=true" "$WORK/audit.out" \
|
|
&& pass "operator turns preserve chat audit hash chain" || fail "operator audit chain invalid"
|
|
|
|
test -s "$CASAN_STATE_ROOT/logs/cost/metrics.jsonl" \
|
|
&& pass "operator H6 telemetry recorded" || fail "operator metrics missing"
|
|
|
|
echo ""
|
|
echo "===== CHAT OPERATOR SUMMARY: PASS=$PASS FAIL=$FAIL ====="
|
|
[[ "$FAIL" -eq 0 ]] || exit 1
|