diff --git a/README.md b/README.md index 3b10ef3..d783736 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/VERSION b/VERSION index 3eefcb9..7dea76e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.0.1 diff --git a/docs/casan/CASAN_INSTALL_HYBRID.md b/docs/casan/CASAN_INSTALL_HYBRID.md index b9b698d..404c9d3 100644 --- a/docs/casan/CASAN_INSTALL_HYBRID.md +++ b/docs/casan/CASAN_INSTALL_HYBRID.md @@ -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 diff --git a/packages/casan-devkit/templates/codex/hooks.json b/packages/casan-devkit/templates/codex/hooks.json index 4e86332..88d2067 100644 --- a/packages/casan-devkit/templates/codex/hooks.json +++ b/packages/casan-devkit/templates/codex/hooks.json @@ -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" } diff --git a/packages/casan-devkit/tests/hybrid-install-tests.sh b/packages/casan-devkit/tests/hybrid-install-tests.sh index c7f6212..aeca053 100755 --- a/packages/casan-devkit/tests/hybrid-install-tests.sh +++ b/packages/casan-devkit/tests/hybrid-install-tests.sh @@ -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 =====" diff --git a/packages/casan-harness/adapters/codex/hooks.template.json b/packages/casan-harness/adapters/codex/hooks.template.json index 4e86332..88d2067 100644 --- a/packages/casan-harness/adapters/codex/hooks.template.json +++ b/packages/casan-harness/adapters/codex/hooks.template.json @@ -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" } diff --git a/packages/casan-harness/scripts/python/agentic_bridge.py b/packages/casan-harness/scripts/python/agentic_bridge.py index 062c812..16bd090 100755 --- a/packages/casan-harness/scripts/python/agentic_bridge.py +++ b/packages/casan-harness/scripts/python/agentic_bridge.py @@ -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") diff --git a/packages/casan-harness/tests/phase-agentic-bridge-tests.sh b/packages/casan-harness/tests/phase-agentic-bridge-tests.sh index 64889cf..bfb0bc9 100755 --- a/packages/casan-harness/tests/phase-agentic-bridge-tests.sh +++ b/packages/casan-harness/tests/phase-agentic-bridge-tests.sh @@ -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