From 66de0d276b9dd08303ad3132957ac57efb1e79f3 Mon Sep 17 00:00:00 2001 From: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com> Date: Mon, 21 Sep 2026 01:07:29 +0000 Subject: [PATCH] Resync on chock 0.9.1: the vendored runtime follows agentseam 0.3.1 `chock sync --repo .` on the released 0.9.1 wheel, committed as it came out. One file: `.chock/bin/claude_code.py` is agentseam 0.3.1's bundle (parse and detect are total over non-object payloads; a handler that raises is refused in Claude Code's dialect rather than crashing to a non-blocking error). No policies, no config, no hook entry changed. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Jothi Mani Rajendran <250249270+jothimani-rajendran@users.noreply.github.com> --- .chock/bin/claude_code.py | 81 ++++++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/.chock/bin/claude_code.py b/.chock/bin/claude_code.py index 6185c6f..e4c6207 100755 --- a/.chock/bin/claude_code.py +++ b/.chock/bin/claude_code.py @@ -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. # @@ -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.""" @@ -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 @@ -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 @@ -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: @@ -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 @@ -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: