fix: harden Codex hook finalization
This commit is contained in:
@@ -130,6 +130,11 @@ casan level show
|
||||
Với Codex, mở `/hooks`, review và trust đúng project hook sau lần init hoặc khi
|
||||
bootstrap hash thay đổi.
|
||||
|
||||
Codex hooks gọi bootstrap tương đối từ project root và không phụ thuộc vào
|
||||
`git rev-parse`, nên ownership hoặc cấu hình Git không thể làm hỏng lifecycle
|
||||
hook. Git vẫn được khuyến nghị mạnh cho source provenance, review diff và
|
||||
rollback trước khi cho agent thực hiện side effect.
|
||||
|
||||
### 4. Dùng workflow hiện có
|
||||
|
||||
Tiếp tục dùng chat, agents, skills và slash commands của project như bình
|
||||
|
||||
@@ -155,7 +155,9 @@ hiện tại.
|
||||
Bootstrap `.casan/casan-hook.py` tự đọc `config.json`; developer không còn phải
|
||||
source `.casan/agentic.env` trước khi mở IDE. Mỗi invocation resolve Core theo
|
||||
runtime mode rồi kiểm tra live hash so với `version.lock` trước khi chạy
|
||||
adapter.
|
||||
adapter. Codex gọi bootstrap bằng đường dẫn tương đối từ project root, không
|
||||
phụ thuộc `git rev-parse`; Git vẫn được khuyến nghị để có provenance, diff và
|
||||
rollback đáng tin cậy.
|
||||
|
||||
### Project đã có `.claude`, `.github`, agents, skills hoặc CASAN vendored
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"description": "CASAN Plan-20 lifecycle hooks. Review with /hooks; the project bootstrap resolves and verifies the pinned global harness.",
|
||||
"description": "CASAN lifecycle hooks. Review with /hooks; the project bootstrap resolves and verifies the pinned Core runtime.",
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event UserPromptSubmit",
|
||||
"commandWindows": "py -3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event UserPromptSubmit",
|
||||
"command": "python3 \".casan/casan-hook.py\" --client codex --event UserPromptSubmit",
|
||||
"commandWindows": "py -3 \".casan\\casan-hook.py\" --client codex --event UserPromptSubmit",
|
||||
"timeout": 15,
|
||||
"statusMessage": "CASAN admission"
|
||||
}
|
||||
@@ -20,8 +20,8 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event PreToolUse",
|
||||
"commandWindows": "py -3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event PreToolUse",
|
||||
"command": "python3 \".casan/casan-hook.py\" --client codex --event PreToolUse",
|
||||
"commandWindows": "py -3 \".casan\\casan-hook.py\" --client codex --event PreToolUse",
|
||||
"timeout": 15,
|
||||
"statusMessage": "CASAN policy gate"
|
||||
}
|
||||
@@ -34,8 +34,8 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event PostToolUse",
|
||||
"commandWindows": "py -3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event PostToolUse",
|
||||
"command": "python3 \".casan/casan-hook.py\" --client codex --event PostToolUse",
|
||||
"commandWindows": "py -3 \".casan\\casan-hook.py\" --client codex --event PostToolUse",
|
||||
"timeout": 15,
|
||||
"statusMessage": "CASAN evidence"
|
||||
}
|
||||
@@ -47,8 +47,8 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event Stop",
|
||||
"commandWindows": "py -3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event Stop",
|
||||
"command": "python3 \".casan/casan-hook.py\" --client codex --event Stop",
|
||||
"commandWindows": "py -3 \".casan\\casan-hook.py\" --client codex --event Stop",
|
||||
"timeout": 15,
|
||||
"statusMessage": "CASAN finalize"
|
||||
}
|
||||
|
||||
@@ -164,13 +164,15 @@ echo "$CLAUDE_PRE" | grep -q '"permissionDecision": "allow"' && pass "Claude gen
|
||||
CODEX_BEGIN=$(printf '%s' '{"session_id":"hybrid-codex","turn_id":"t2","prompt":"review this project","cwd":"'"$PROJ"'"}' |
|
||||
CASAN_APP_ROOT="$PROJ" python3 "$PROJ/.casan/casan-hook.py" --client codex --event UserPromptSubmit)
|
||||
echo "$CODEX_BEGIN" | grep -q '"continue": true' && pass "Codex generated hook opens admission" || fail "Codex generated hook failed ($CODEX_BEGIN)"
|
||||
python3 - "$PROJ/.codex/hooks.json" <<'PY' && pass "Codex hook JSON matches the current nested command schema" || fail "Codex hook JSON schema is stale"
|
||||
python3 - "$PROJ/.codex/hooks.json" <<'PY' && pass "Codex hooks use the current schema and project-relative bootstrap" || fail "Codex hook contract is stale"
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
handlers=[h for groups in d["hooks"].values() for group in groups for h in group["hooks"]]
|
||||
assert handlers and all(h.get("type") == "command" for h in handlers)
|
||||
assert all(isinstance(h.get("command"), str) for h in handlers)
|
||||
assert all(isinstance(h.get("timeout"), int) and "timeout_ms" not in h for h in handlers)
|
||||
assert all('python3 ".casan/casan-hook.py"' in h["command"] for h in handlers)
|
||||
assert all("git rev-parse" not in h["command"] for h in handlers)
|
||||
PY
|
||||
|
||||
echo "===== ③ version.lock pins the installed harness ====="
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"description": "CASAN Plan-20 lifecycle hooks. Review with /hooks; the project bootstrap resolves and verifies the pinned global harness.",
|
||||
"description": "CASAN lifecycle hooks. Review with /hooks; the project bootstrap resolves and verifies the pinned Core runtime.",
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event UserPromptSubmit",
|
||||
"commandWindows": "py -3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event UserPromptSubmit",
|
||||
"command": "python3 \".casan/casan-hook.py\" --client codex --event UserPromptSubmit",
|
||||
"commandWindows": "py -3 \".casan\\casan-hook.py\" --client codex --event UserPromptSubmit",
|
||||
"timeout": 15,
|
||||
"statusMessage": "CASAN admission"
|
||||
}
|
||||
@@ -20,8 +20,8 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event PreToolUse",
|
||||
"commandWindows": "py -3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event PreToolUse",
|
||||
"command": "python3 \".casan/casan-hook.py\" --client codex --event PreToolUse",
|
||||
"commandWindows": "py -3 \".casan\\casan-hook.py\" --client codex --event PreToolUse",
|
||||
"timeout": 15,
|
||||
"statusMessage": "CASAN policy gate"
|
||||
}
|
||||
@@ -34,8 +34,8 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event PostToolUse",
|
||||
"commandWindows": "py -3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event PostToolUse",
|
||||
"command": "python3 \".casan/casan-hook.py\" --client codex --event PostToolUse",
|
||||
"commandWindows": "py -3 \".casan\\casan-hook.py\" --client codex --event PostToolUse",
|
||||
"timeout": 15,
|
||||
"statusMessage": "CASAN evidence"
|
||||
}
|
||||
@@ -47,8 +47,8 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event Stop",
|
||||
"commandWindows": "py -3 \"$(git rev-parse --show-toplevel)/.casan/casan-hook.py\" --client codex --event Stop",
|
||||
"command": "python3 \".casan/casan-hook.py\" --client codex --event Stop",
|
||||
"commandWindows": "py -3 \".casan\\casan-hook.py\" --client codex --event Stop",
|
||||
"timeout": 15,
|
||||
"statusMessage": "CASAN finalize"
|
||||
}
|
||||
|
||||
@@ -857,14 +857,39 @@ def op_finalize(req):
|
||||
# summary as the closing verification control.
|
||||
reasons = []
|
||||
if req.get("assistant_summary"):
|
||||
ok, why = h4_scan(req.get("assistant_summary"), "output")
|
||||
add_evidence(rec, "H3/H7", "finalize-verify", "allow" if ok else "flag", why)
|
||||
if not ok:
|
||||
h4 = h4_scan(req.get("assistant_summary"), "output")
|
||||
if h4 == "ok":
|
||||
add_evidence(
|
||||
rec, "H3/H7", "finalize-verify", "allow", "h4_ok")
|
||||
elif h4 == "unavailable":
|
||||
# An output that could not be scanned must never be certified.
|
||||
# Match begin/pre-tool graceful degradation: keep the developer
|
||||
# workflow available, but make the missing control explicit.
|
||||
rec["integration_mode"] = "observed_only"
|
||||
rec["certification_strength"] = "observed_only"
|
||||
add_evidence(
|
||||
rec, "H3/H7", "finalize-verify", "degraded",
|
||||
"gate_unavailable_no_bash")
|
||||
reasons.append("output_gate_unavailable")
|
||||
else:
|
||||
# A policy block or timeout is fail-closed for certification.
|
||||
# Treat unknown statuses the same way so a future contract change
|
||||
# cannot silently certify an unverified output.
|
||||
why = {
|
||||
"blocked": "h4_blocked",
|
||||
"timeout": "h4_internal_timeout",
|
||||
}.get(h4, "h4_unknown_status")
|
||||
add_evidence(
|
||||
rec, "H3/H7", "finalize-verify", "flag", why)
|
||||
reasons.append("output_flagged:%s" % why)
|
||||
|
||||
# Certification decision.
|
||||
strength = rec.get("certification_strength", "observed_only")
|
||||
certified = True
|
||||
if reasons:
|
||||
# Any closing output-verification failure above is certification-fatal,
|
||||
# even when every earlier admission/tool control succeeded.
|
||||
certified = False
|
||||
if enforcement_mode() != "enforce":
|
||||
certified = False
|
||||
reasons.append("observe_mode")
|
||||
|
||||
@@ -116,6 +116,27 @@ METRICS=$(grep -c '"harness":"H6-agentic"' "$CASAN_STATE_ROOT/logs/cost/metrics.
|
||||
[[ "$(printf '%s' "$F2" | field reason)" == "already_finalized" && "$METRICS" == "1" ]] \
|
||||
&& pass "second finalize is idempotent (one metric only)" || fail "finalize not idempotent (reason=$(printf '%s' "$F2" | field reason) metrics=$METRICS)"
|
||||
|
||||
# ── C8b: output scan status contract is handled without finalize crashes ────
|
||||
echo "===== C8b: finalize handles blocked and unavailable output scans ====="
|
||||
newstate
|
||||
B=$(bridge '{"op":"begin","client":"codex","project":"'"$PROJ"'","session":"c8b-block","prompt":"summarize the work","integration_mode":"project_hook"}')
|
||||
AID=$(printf '%s' "$B" | field admission_id)
|
||||
F=$(bridge '{"op":"finalize","admission_id":"'"$AID"'","stop_reason":"completed","assistant_summary":"API_KEY=supersecret"}')
|
||||
[[ "$(printf '%s' "$F" | field decision)" == "non_certified" ]] \
|
||||
&& printf '%s' "$F" | field reason | grep -q "output_flagged:h4_blocked" \
|
||||
&& pass "blocked assistant output finalizes as non-certified without crashing" \
|
||||
|| fail "blocked assistant output finalize contract failed ($F)"
|
||||
|
||||
newstate
|
||||
B=$(bridge '{"op":"begin","client":"codex","project":"'"$PROJ"'","session":"c8b-unavailable","prompt":"summarize the work","integration_mode":"project_hook"}')
|
||||
AID=$(printf '%s' "$B" | field admission_id)
|
||||
F=$(CASAN_AGENTIC_BASH=/nonexistent/bash-xyz bridge '{"op":"finalize","admission_id":"'"$AID"'","stop_reason":"completed","assistant_summary":"completed safely"}')
|
||||
[[ "$(printf '%s' "$F" | field decision)" == "non_certified" ]] \
|
||||
&& [[ "$(printf '%s' "$F" | field certification_strength)" == "observed_only" ]] \
|
||||
&& printf '%s' "$F" | field reason | grep -q "output_gate_unavailable" \
|
||||
&& pass "unavailable output gate degrades finalize without crashing" \
|
||||
|| fail "unavailable output gate finalize contract failed ($F)"
|
||||
|
||||
# ── C10: token/cost unavailable -> null + warning, never 0 ───────────────────
|
||||
echo "===== C10: missing token/cost = null + partial warning, not zero ====="
|
||||
newstate
|
||||
|
||||
Reference in New Issue
Block a user