feat(devkit): enforce governed prompt adoption

This commit is contained in:
thanhnv
2026-07-21 23:40:26 +07:00
parent 4d5dca9400
commit 7b2ba60a3a
16 changed files with 622 additions and 12 deletions
@@ -0,0 +1,11 @@
<!-- CASAN_PROMPT_ENFORCEMENT_START -->
## CASAN prompt enforcement — mandatory
- Do not process a direct user prompt in this repository as a governed or certified task.
- The supported prompt boundary is `bin/casan-chat` (or `bin/casan-chat.ps1` on Windows through WSL2) and the CASAN Control Panel.
- If this agent was opened directly through Codex, Claude Code, Copilot, ChatGPT, or another assistant UI, stop before using tools or changing files and instruct the member to resubmit the request through `bin/casan-chat`.
- Every accepted governed prompt must return a CASAN `trace_id`; verify it with `bin/casan prompt trace <trace-id>`.
- Commands with side effects must use `bin/casan run` or a registered action with the required approval.
- Never claim that a direct external-UI response passed H1-H7. Absence of CASAN evidence means not certified.
- Do not bypass this boundary because a request was denied, blocked, degraded, or requires approval.
<!-- CASAN_PROMPT_ENFORCEMENT_END -->
@@ -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
@@ -0,0 +1,22 @@
# CASAN_MANAGED_PROMPT_ENTRYPOINT
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]] $Prompt,
[string] $Distro = 'Ubuntu'
)
$ErrorActionPreference = 'Stop'
$projectRootWindows = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$projectRootWsl = (& wsl.exe -d $Distro -- wslpath -a $projectRootWindows).Trim()
if (-not $projectRootWsl) {
throw 'CASAN could not resolve the project path in WSL2.'
}
$arguments = @('-d', $Distro, '--cd', $projectRootWsl, './bin/casan-chat')
if ($Prompt) {
$arguments += $Prompt
}
& wsl.exe @arguments
exit $LASTEXITCODE