167 lines
4.8 KiB
Bash
Executable File
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
|