feat(devkit): enforce governed prompt adoption
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user