feat(devkit): enforce governed prompt adoption
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user