#!/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 = "" marker_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