fix: harden Codex hook finalization

This commit is contained in:
thanhnv
2026-07-24 13:06:23 +07:00
parent bfebfa9d39
commit 34a6b013c0
8 changed files with 79 additions and 24 deletions
@@ -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