feat(devkit): enforce governed prompt adoption
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
# CASAN_MANAGED_PROMPT_ENTRYPOINT
|
||||
# Project-owned governed chat. The DevKit installer places the project identity
|
||||
# in .casan/prompt-policy.json; this launcher refuses to run when that contract
|
||||
# or its repository instruction boundary has drifted.
|
||||
|
||||
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$SELF_DIR/.." && pwd)"
|
||||
CHAT="$ROOT/packages/casan-harness/scripts/bash/chat-turn.py"
|
||||
VERIFY="$ROOT/packages/casan-harness/scripts/bash/prompt-enforcement-verify.sh"
|
||||
POLICY="$ROOT/.casan/prompt-policy.json"
|
||||
|
||||
if [[ ! -f "$CHAT" || ! -f "$VERIFY" || ! -f "$POLICY" ]]; then
|
||||
echo "casan-chat: prompt enforcement runtime is incomplete; reinstall CASAN DevKit" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "casan-chat: python3 is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! bash "$VERIFY" --root "$ROOT" --quiet; then
|
||||
echo "casan-chat: prompt enforcement verification failed closed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROJECT="$(python3 - "$POLICY" <<'PY'
|
||||
import json, sys
|
||||
print(json.load(open(sys.argv[1], encoding="utf-8"))["project_id"])
|
||||
PY
|
||||
)"
|
||||
DOMAIN_REL="$(python3 - "$POLICY" <<'PY'
|
||||
import json, sys
|
||||
print(json.load(open(sys.argv[1], encoding="utf-8"))["domain_root"])
|
||||
PY
|
||||
)"
|
||||
|
||||
export CASAN_APP_ROOT="$ROOT"
|
||||
export CASAN_DOMAIN_ROOT="$ROOT/$DOMAIN_REL"
|
||||
export CASAN_PROJECT_ID="$PROJECT"
|
||||
|
||||
ACTOR="${CASAN_CHAT_ACTOR:-${USER:-member}}"
|
||||
ROLE="${CASAN_CHAT_ROLE:-viewer}"
|
||||
AGENT="${CASAN_CHAT_AGENT:-}"
|
||||
SKILL="${CASAN_CHAT_SKILL:-}"
|
||||
MODEL_PROVIDER="${CASAN_CHAT_MODEL_PROVIDER:-}"
|
||||
CHAT_ID="${CASAN_CHAT_ID:-$PROJECT-$(date +%Y%m%d-%H%M%S)-$$}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
bin/casan-chat "normal prompt"
|
||||
bin/casan-chat # interactive mode
|
||||
|
||||
Every accepted prompt is submitted to CASAN chat-turn and produces H1-H7
|
||||
evidence. Direct Claude, ChatGPT, Codex or Copilot UI prompts are outside this
|
||||
enforcement boundary and must not be described as CASAN-certified.
|
||||
|
||||
Optional environment variables:
|
||||
CASAN_CHAT_ACTOR
|
||||
CASAN_CHAT_ROLE
|
||||
CASAN_CHAT_AGENT
|
||||
CASAN_CHAT_SKILL
|
||||
CASAN_CHAT_MODEL_PROVIDER
|
||||
CASAN_CHAT_ID
|
||||
EOF
|
||||
}
|
||||
|
||||
run_turn() {
|
||||
local message="$1"
|
||||
local raw_file rc
|
||||
raw_file="$(mktemp)"
|
||||
|
||||
local command=(python3 "$CHAT" ask
|
||||
--message "$message"
|
||||
--actor "$ACTOR"
|
||||
--role "$ROLE"
|
||||
--project "$PROJECT"
|
||||
--chat-id "$CHAT_ID")
|
||||
[[ -n "$AGENT" ]] && command+=(--agent "$AGENT")
|
||||
[[ -n "$SKILL" ]] && command+=(--skill "$SKILL")
|
||||
[[ -n "$MODEL_PROVIDER" ]] && command+=(--model-provider "$MODEL_PROVIDER")
|
||||
|
||||
set +e
|
||||
"${command[@]}" >"$raw_file"
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
python3 - "$raw_file" "$ROOT" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
raw_path = pathlib.Path(sys.argv[1])
|
||||
root = pathlib.Path(sys.argv[2])
|
||||
lines = [line for line in raw_path.read_text(encoding="utf-8", errors="replace").splitlines() if line.strip()]
|
||||
try:
|
||||
result = json.loads(lines[-1])
|
||||
except (IndexError, json.JSONDecodeError):
|
||||
print(raw_path.read_text(encoding="utf-8", errors="replace"))
|
||||
raise SystemExit(0)
|
||||
|
||||
answer = result.get("answer") or result.get("reason") or result.get("decision") or "No answer returned."
|
||||
print(answer)
|
||||
print()
|
||||
trace_id = result.get("trace_id", "")
|
||||
certified = result.get("certified", False)
|
||||
decision = result.get("decision", "UNKNOWN")
|
||||
mode = result.get("mode", "UNKNOWN")
|
||||
print(f"CASAN decision={decision} mode={mode} certified={str(bool(certified)).lower()} trace_id={trace_id or 'n/a'}")
|
||||
if trace_id:
|
||||
trace = root / ".specify" / "logs" / "trace-events" / f"{trace_id}.jsonl"
|
||||
print(f"CASAN evidence={trace}")
|
||||
PY
|
||||
|
||||
if [[ "$rc" -eq 0 ]]; then
|
||||
local certification trace_id certified
|
||||
certification="$(python3 - "$raw_file" <<'PY'
|
||||
import json, pathlib, sys
|
||||
lines = [line for line in pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").splitlines() if line.strip()]
|
||||
try:
|
||||
result = json.loads(lines[-1])
|
||||
except (IndexError, json.JSONDecodeError):
|
||||
print("|false")
|
||||
else:
|
||||
print(f"{result.get('trace_id', '')}|{str(bool(result.get('certified', False))).lower()}")
|
||||
PY
|
||||
)"
|
||||
trace_id="${certification%%|*}"
|
||||
certified="${certification#*|}"
|
||||
if [[ -z "$trace_id" || "$certified" != "true" ]]; then
|
||||
echo "casan-chat: runtime returned success without a certified trace" >&2
|
||||
rc=2
|
||||
elif ! bash "$VERIFY" --root "$ROOT" --project "$PROJECT" --trace-id "$trace_id"; then
|
||||
echo "casan-chat: per-prompt certification verification failed closed" >&2
|
||||
rc=2
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$raw_file"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$#" -gt 0 ]]; then
|
||||
run_turn "$*"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
echo "CASAN governed chat — project=$PROJECT role=$ROLE actor=$ACTOR"
|
||||
echo "Every accepted turn writes H1-H7 evidence. Type /quit to exit."
|
||||
while true; do
|
||||
printf 'casan> '
|
||||
IFS= read -r message || break
|
||||
case "$message" in
|
||||
/quit|/exit) break ;;
|
||||
'') continue ;;
|
||||
esac
|
||||
run_turn "$message" || true
|
||||
done
|
||||
Reference in New Issue
Block a user