Add selectable CASAN IDE integrations
This commit is contained in:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user