Files
CASAN/packages/casan-harness/scripts/bash/prompt-enforcement-verify.sh
T

125 lines
4.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/casan-paths.sh"
ROOT="$CASAN_APP_ROOT"
TRACE_ID=""
EXPECTED_PROJECT=""
QUIET=0
while [[ $# -gt 0 ]]; do
case "$1" in
--root) ROOT="$(cd "$2" && pwd)"; shift 2 ;;
--trace-id) TRACE_ID="$2"; shift 2 ;;
--project) EXPECTED_PROJECT="$2"; shift 2 ;;
--quiet) QUIET=1; shift ;;
-h|--help)
echo "usage: prompt-enforcement-verify.sh [--root dir] [--project id] [--trace-id uuid] [--quiet]"
exit 0 ;;
*) echo "PROMPT_ENFORCEMENT_INVALID_ARGUMENT value=$1" >&2; exit 64 ;;
esac
done
POLICY="$ROOT/.casan/prompt-policy.json"
python3 - "$ROOT" "$POLICY" "$EXPECTED_PROJECT" "$TRACE_ID" "$QUIET" <<'PY'
import json
import pathlib
import re
import sys
root = pathlib.Path(sys.argv[1])
policy_path = pathlib.Path(sys.argv[2])
expected_project = sys.argv[3]
trace_id = sys.argv[4]
quiet = sys.argv[5] == "1"
marker_start = "<!-- CASAN_PROMPT_ENFORCEMENT_START -->"
marker_end = "<!-- CASAN_PROMPT_ENFORCEMENT_END -->"
def fail(reason: str) -> None:
print(f"CASAN_PROMPT_ENFORCEMENT_INVALID reason={reason}", file=sys.stderr)
raise SystemExit(2)
try:
policy = json.loads(policy_path.read_text(encoding="utf-8"))
except FileNotFoundError:
fail("policy_missing")
except (OSError, ValueError):
fail("policy_unreadable")
project = str(policy.get("project_id") or "")
domain_root = str(policy.get("domain_root") or "")
if policy.get("schema_version") != 1 or policy.get("mode") != "enforced":
fail("policy_contract_invalid")
if not re.fullmatch(r"[a-z][a-z0-9-]{1,62}", project):
fail("project_id_invalid")
if expected_project and project != expected_project:
fail("project_id_mismatch")
if pathlib.PurePosixPath(domain_root).is_absolute() or ".." in pathlib.PurePosixPath(domain_root).parts:
fail("domain_root_unsafe")
if not (root / domain_root).is_dir():
fail("domain_root_missing")
managed = {
"bin/casan-chat": "CASAN_MANAGED_PROMPT_ENTRYPOINT",
"bin/casan-chat.ps1": "CASAN_MANAGED_PROMPT_ENTRYPOINT",
"AGENTS.md": marker_start,
"CLAUDE.md": marker_start,
".github/copilot-instructions.md": marker_start,
".gitea/workflows/casan-prompt-enforcement.yml": "CASAN prompt enforcement contract",
}
for relative, marker in managed.items():
path = root / relative
try:
content = path.read_text(encoding="utf-8")
except OSError:
fail(f"managed_file_missing:{relative}")
if marker not in content:
fail(f"managed_marker_missing:{relative}")
if relative.endswith(".md") and marker_start in content and marker_end not in content:
fail(f"managed_marker_unclosed:{relative}")
if not trace_id:
if not quiet:
print(f"CASAN_PROMPT_ENFORCEMENT_VALID project={project} mode=enforced")
raise SystemExit(0)
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{5,127}", trace_id):
fail("trace_id_invalid")
trace_path = root / ".specify" / "logs" / "trace-events" / f"{trace_id}.jsonl"
metrics_path = root / ".specify" / "logs" / "cost" / "metrics.jsonl"
try:
events = [json.loads(line) for line in trace_path.read_text(encoding="utf-8").splitlines() if line.strip()]
except FileNotFoundError:
fail("trace_missing")
except (OSError, ValueError):
fail("trace_unreadable")
latest = {}
for event in events:
if event.get("trace_id") != trace_id:
fail("trace_identity_mismatch")
latest[str(event.get("gate_id") or "")] = event
required = [f"H{i}-{name}" for i, name in enumerate(("context", "tool", "eval", "security", "governance", "agentops", "orchestration"), 1)]
missing = [gate for gate in required if gate not in latest]
if missing:
fail("trace_gates_missing:" + ",".join(missing))
if any(latest[gate].get("status") != "pass" for gate in required):
fail("trace_not_all_pass")
if latest["H7-orchestration"].get("evidence", {}).get("certified") is not True:
fail("trace_not_certified")
try:
metrics = [json.loads(line) for line in metrics_path.read_text(encoding="utf-8").splitlines() if line.strip()]
except (FileNotFoundError, OSError, ValueError):
fail("metrics_unreadable")
matches = [row for row in metrics if str(row.get("trace_id") or row.get("run_id") or "") == trace_id]
if not matches:
fail("trace_metrics_missing")
if not any(str(row.get("project_id") or row.get("project") or "") == project for row in matches):
fail("trace_project_attribution_missing")
print(f"CASAN_PROMPT_TRACE_CERTIFIED project={project} trace_id={trace_id} gates=7")
PY