Files
CASAN/packages/casan-devkit/templates/prompt-enforcement/casan-chat
T

167 lines
4.8 KiB
Bash
Executable File

#!/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