Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 67 additions & 14 deletions .chock/bin/claude_code.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by agentseam 0.3.0 -- bundle("claude_code"). Do not hand-edit, except the
# Generated by agentseam 0.3.1 -- bundle("claude_code"). Do not hand-edit, except the
# HANDLER block below (between "agentseam handler >>>" and "<<< agentseam handler"),
# which is exactly what this file leaves for you to fill in.
#
Expand All @@ -19,10 +19,11 @@
import subprocess as _chock_subprocess
from datetime import datetime as _chock_datetime, timezone as _chock_timezone
from pathlib import Path as _chock_Path
import traceback
import warnings as _warnings

# ------------------------------------------------------------------------------
# contract (agentseam 0.3.0)
# contract (agentseam 0.3.1)

"""Canonical event vocabulary, normalized envelope, and decision type."""

Expand Down Expand Up @@ -203,7 +204,9 @@ def tool_input_of(raw):
if isinstance(raw, str) and raw[:1] == "{":
try:
parsed = _json.loads(raw)
except _json.JSONDecodeError:
except (ValueError, RecursionError):
# JSONDecodeError is a ValueError; a string nested past the interpreter's
# limit raises RecursionError instead, and neither is an input to crash on.
return {}
if isinstance(parsed, dict):
return parsed
Expand All @@ -230,9 +233,22 @@ def looks_like_claude_code(raw):

PROBES = {"looks_like_claude_code": looks_like_claude_code}

UNREADABLE_NAME = ""

def wire_name_of(raw, key):
"""The event name under `key`: text, None when absent, UNREADABLE_NAME when not text.

Total over anything `json.loads` can return: a payload that is not an object names no
event, and a name that is not a string is not one any adapter can map.
"""
name = raw.get(key) if isinstance(raw, dict) else None
if name is None or isinstance(name, str):
return name
return UNREADABLE_NAME

def _wire_name(cfg, raw):
for key in cfg["claims"].get("event_key", ()):
name = raw.get(key)
name = wire_name_of(raw, key)
if name is not None:
return name
return None
Expand Down Expand Up @@ -317,6 +333,8 @@ def _field(raw, ti, chain):
_FIELD_META = ("tool_input", "content_only_for_write_tools", "stringify")

def _tool_input_raw(cfg, raw):
if not isinstance(raw, dict):
return None
for key in cfg["fields"].get("tool_input", ("tool_input",)):
value = raw.get(key)
if value is not None:
Expand Down Expand Up @@ -1076,6 +1094,43 @@ def _emit(out, text):
out.flush()


def _report(text):
# Best effort, never raising: a diagnostic must not pre-empt the refusal it accompanies.
# A hook's stderr is whatever the host gave it -- closed (then sys.stderr is None, and
# the traceback module's default print would land on STDOUT, inside the verdict), a
# console code page that cannot hold the payload text quoted in the exception, a pipe
# nobody reads.
try:
sys.stderr.write(text)
sys.stderr.flush()
except Exception:
return


def _decide(raw):
"""(stdout_text, exit_code) for one decoded payload: agentseam.dispatch.handle(), inlined."""
event = parse(raw)
if event.event == UNKNOWN:
# A vendor event this adapter has no mapping for. handle() is not called: it
# reasons about the canonical vocabulary, and handing it something outside that
# vocabulary invites a decision made on a false premise.
return "", 0
try:
decision = _coerce(handle(event))
except Exception:
# A door that cannot decide refuses. Escaping here would exit 1 with a traceback,
# which every host reads as a non-blocking error and allows past; instead the
# failure is printed for the operator and the gate answers deny in 'claude_code''s
# own dialect. Only the failure's class is named to the host: its message may quote
# the very payload content the policy was inspecting.
_report(traceback.format_exc())
decision = Decision.deny(
"policy handler failed (%s); refusing rather than allowing what it could not judge"
% sys.exc_info()[0].__name__
)
return respond(degrade(decision, event), event)


def main(stdin=None, stdout=None, exit=True):
"""Read one payload from stdin, dispatch, emit the 'claude_code' response, exit."""
stream = stdin if stdin is not None else sys.stdin
Expand All @@ -1087,16 +1142,14 @@ def main(stdin=None, stdout=None, exit=True):
if exit:
sys.exit(0)
return 0
event = parse(raw)
if event.event == UNKNOWN:
# A vendor event this adapter has no mapping for. handle() is not called: it
# reasons about the canonical vocabulary, and handing it something outside that
# vocabulary invites a decision made on a false premise.
if exit:
sys.exit(0)
return 0
decision = degrade(_coerce(handle(event)), event)
text, code = respond(decision, event)
try:
text, code = _decide(raw)
except Exception:
# Past the handler, which _decide() already answers for: a fault in this file on a
# payload it did decode. With no Event to answer in dialect, the one refusal left is
# the host's blocking exit code, and nothing on stdout to be misread as a verdict.
_report(traceback.format_exc())
text, code = "", 2
if text:
_emit(out, text)
if exit:
Expand Down