Add selectable CASAN IDE integrations

This commit is contained in:
thanhnv
2026-07-23 23:04:54 +07:00
parent ff4e9d5a53
commit ce708fafe5
28 changed files with 1658 additions and 407 deletions
@@ -12,11 +12,10 @@ member. Codex tool hooks are a guardrail, not a complete boundary: a turn that
uses a hosted/specialized tool outside hook coverage is DOWNGRADED, not
silently certified.
Output contract (kept portable across Codex versions, which is why the exact
field mapping is a Wave-3 experiment): a decision JSON on stdout AND an exit
code — 0 = allow, 2 = block/deny — so a host that reads either signal fails
closed the same way. Field names are accepted defensively (tool_name|tool,
cwd|project, session_id|session) to absorb payload drift.
Output follows Codex's current command-hook contract: turn hooks use
`continue`/`stopReason`/`systemMessage`; PreToolUse allows with exit 0 and
blocks with exit 2 plus `systemMessage`. Field names are accepted defensively
(tool_name|tool, cwd|project, session_id|session) to absorb payload drift.
"""
from __future__ import annotations
@@ -96,14 +95,25 @@ def handle_user_prompt_submit(payload):
"prompt": _first(payload, "prompt", "input", "message") or "",
"integration_mode": integration_mode(),
})
if resp.get("admission_id"):
if resp.get("admission_id") and resp.get("decision") != "block":
store_pointer(session, resp["admission_id"], resp.get("trace_id"))
if resp.get("decision") == "block":
return _emit({"decision": "block", "reason": resp.get("reason"),
"trace_id": resp.get("trace_id")}, 2)
return _emit({"decision": "allow", "context": resp.get("context"),
"certification_strength": resp.get("certification_strength"),
"warnings": resp.get("warnings", [])}, 0)
reason = resp.get("reason") or "policy"
return _emit({
"continue": False,
"stopReason": "CASAN blocked this prompt: %s" % reason,
"systemMessage": "CASAN trace %s denied admission" %
(resp.get("trace_id") or "unknown"),
}, 0)
context = resp.get("context") or "admission open"
warnings = resp.get("warnings", [])
if warnings:
context += " | " + "; ".join(warnings)
return _emit({
"continue": True,
"systemMessage": "[CASAN] %s (strength=%s)" % (
context, resp.get("certification_strength") or "unknown"),
}, 0)
def handle_pre_tool_use(payload):
@@ -117,16 +127,18 @@ def handle_pre_tool_use(payload):
"project": _first(payload, "cwd", "project", "workspace"),
})
if resp.get("decision") == "allow":
return _emit({"decision": "allow", "reason": resp.get("reason")}, 0)
return _emit({"decision": "deny", "reason": resp.get("reason"),
"trace_id": resp.get("trace_id")}, 2)
return _emit({}, 0)
return _emit({
"systemMessage": "CASAN denied this tool call: %s" %
(resp.get("reason") or "policy"),
}, 2)
def handle_post_tool_use(payload):
session = _first(payload, "session_id", "session", "conversation_id")
ptr = load_pointer(session)
if not ptr:
return _emit({"decision": "allow", "reason": "no_admission"}, 0)
return _emit({}, 0)
status = _first(payload, "status") or ("error" if _first(payload, "error", "is_error") else "success")
bridge.op_post_tool({
"op": "post-tool",
@@ -151,16 +163,16 @@ def handle_post_tool_use(payload):
"cost_currency": usage.get("cost_currency"),
"cost_source": usage.get("source"),
})
return _emit({"decision": "allow"}, 0)
return _emit({}, 0)
def handle_stop(payload):
if payload.get("stop_hook_active"):
return _emit({"decision": "allow"}, 0)
return _emit({}, 0)
session = _first(payload, "session_id", "session", "conversation_id")
ptr = load_pointer(session)
if not ptr:
return _emit({"decision": "allow", "reason": "no_admission"}, 0)
return _emit({}, 0)
resp = bridge.op_finalize({
"op": "finalize",
"admission_id": ptr.get("admission_id"),
@@ -168,8 +180,13 @@ def handle_stop(payload):
"assistant_summary": _first(payload, "last_assistant_message", "assistant_summary"),
})
clear_pointer(session)
return _emit({"decision": "allow", "certified": resp.get("decision") == "certified",
"certification_strength": resp.get("certification_strength")}, 0)
return _emit({
"continue": True,
"systemMessage": "CASAN finalized trace %s (certified=%s, strength=%s)" % (
resp.get("trace_id") or "unknown",
str(resp.get("decision") == "certified").lower(),
resp.get("certification_strength") or "unknown"),
}, 0)
HANDLERS = {
@@ -191,13 +208,14 @@ def main(argv=None):
event = args.event or _first(payload, "hook_event_name", "event", "type")
handler = HANDLERS.get(event)
if handler is None:
return _emit({"decision": "allow", "reason": "unknown_event"}, 0)
return _emit({}, 0)
try:
return handler(payload)
except Exception as exc: # noqa: BLE001
if event == "PreToolUse":
return _emit({"decision": "deny", "reason": "adapter_error:%s" % exc}, 2)
return _emit({"decision": "allow", "reason": "adapter_error:%s" % exc}, 0)
return _emit({"systemMessage": "CASAN adapter error: %s" % exc}, 2)
return _emit({"continue": True,
"systemMessage": "CASAN adapter error: %s" % exc}, 0)
if __name__ == "__main__":
@@ -1,18 +1,11 @@
# CASAN Plan-20 Codex config fragment (.codex/config.toml).
# Merge these keys into the target repo's .codex/config.toml. This enables the
# project hooks in hooks.template.json after Codex trust review.
# Optional CASAN Plan-20 Codex config fragment (.codex/config.toml).
# Hooks are enabled by default in current Codex; this explicit feature flag is
# useful only when an organization wants the project intent visible in TOML.
#
# For ENTERPRISE enforcement, the managed policy path pins hooks so a member
# cannot disable them (Spike-20 §4.2, Plan-20 Wave 3.3). In that deployment set
# CASAN_AGENTIC_INTEGRATION_MODE=managed_hook via managed environment/MDM, not
# in this committed file.
[hooks]
enabled = true
# project-local hooks load only after the user accepts the trust prompt.
project_hooks = true
[casan]
# Bridge feature flags — safe defaults (observe first, then enforce per Plan-20 §9).
enforcement_mode = "observe" # observe | enforce
integration_mode = "project_hook"
[features]
hooks = true
@@ -1,18 +1,59 @@
{
"//": "CASAN Plan-20 Codex project hooks. Commit as .codex/hooks.json in the target repo. Codex loads project-local hooks ONLY after a trust review — run `casan doctor --client codex` to confirm the trust/onboarding state (Spike-20 §4.2). The exact key names are pinned during the Wave-3 Codex payload spike; the command contract (stdin JSON -> exit 0 allow / exit 2 block) is stable. No secrets or absolute paths here.",
"version": 1,
"description": "CASAN Plan-20 lifecycle hooks. Review with /hooks; the project bootstrap resolves and verifies the pinned global harness.",
"hooks": {
"UserPromptSubmit": [
{ "command": ["python3", "packages/casan-harness/adapters/codex/codex_hook.py", "--event", "UserPromptSubmit"], "timeout_ms": 15000 }
{
"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",
"timeout": 15,
"statusMessage": "CASAN admission"
}
]
}
],
"PreToolUse": [
{ "matcher": "*", "command": ["python3", "packages/casan-harness/adapters/codex/codex_hook.py", "--event", "PreToolUse"], "timeout_ms": 15000 }
{
"matcher": "*",
"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",
"timeout": 15,
"statusMessage": "CASAN policy gate"
}
]
}
],
"PostToolUse": [
{ "matcher": "*", "command": ["python3", "packages/casan-harness/adapters/codex/codex_hook.py", "--event", "PostToolUse"], "timeout_ms": 15000 }
{
"matcher": "*",
"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",
"timeout": 15,
"statusMessage": "CASAN evidence"
}
]
}
],
"Stop": [
{ "command": ["python3", "packages/casan-harness/adapters/codex/codex_hook.py", "--event", "Stop"], "timeout_ms": 15000 }
{
"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",
"timeout": 15,
"statusMessage": "CASAN finalize"
}
]
}
]
}
}