1- """The F1 `hook_json` family engine: claims/parse/respond driven by a vendor config entry.
1+ """The F1 `hook_json` / F2 `flat_decision` engine: claims/parse/respond driven by a vendor config entry.
22
33Every function takes the vendor's `data/vendors/<agent>.json` entry as its first argument;
44`_family.bind()` closes them over one entry, and a bundle inlines this module next to a
@@ -98,10 +98,24 @@ def _field(raw, ti, chain):
9898 return value
9999
100100
101+ #: `fields` keys that steer the extraction rather than naming an Event field.
102+ _FIELD_META = ("tool_input" , "content_only_for_write_tools" )
103+
104+
105+ def _tool_input_raw (cfg , raw ):
106+ for key in cfg ["fields" ].get ("tool_input" , ("tool_input" ,)):
107+ value = raw .get (key )
108+ if value is not None :
109+ return value
110+ return None
111+
112+
101113def hj_parse (cfg , raw ):
102114 """Normalise one payload along the entry's ordered field-fallback chains."""
103- ti = tool_input_of (raw .get ("tool_input" ))
104- fields = {name : _field (raw , ti , chain ) for name , chain in cfg ["fields" ].items ()}
115+ ti = tool_input_of (_tool_input_raw (cfg , raw ))
116+ fields = {name : _field (raw , ti , chain ) for name , chain in cfg ["fields" ].items () if name not in _FIELD_META }
117+ if cfg ["fields" ].get ("content_only_for_write_tools" ) and fields .get ("tool" ) not in cfg ["tools" ].get ("write" , ()):
118+ fields ["content" ] = None
105119 if isinstance (fields .get ("output" ), (dict , list )):
106120 fields ["output" ] = _json .dumps (fields ["output" ])
107121 return Event (
@@ -147,17 +161,23 @@ def _note_for(v, decision, at_gate, missing_input):
147161 return None
148162
149163
150- def _default_for (v , decision , at_gate ):
164+ def _default_for (v , decision , at_gate , wire = None ):
165+ gate_defaults = v .get ("gate_reason_defaults" , {})
166+ if wire in gate_defaults :
167+ return gate_defaults [wire ]
151168 defaults = v .get ("reason_defaults" , {})
152169 key = {DENY : "deny" , ESCALATE : "escalate" , TRANSFORM : "transform" }.get (decision .outcome , "deny" )
153170 if at_gate and key + "_gate" in defaults :
154171 return defaults [key + "_gate" ]
155172 return defaults .get (key , "blocked by policy" )
156173
157174
158- def _refusal_text (v , decision , at_gate ):
175+ def _refusal_text (v , decision , at_gate , wire = None ):
159176 note = _note_for (v , decision , at_gate , decision .updated_input is None )
160- default = _default_for (v , decision , at_gate )
177+ default = _default_for (v , decision , at_gate , wire )
178+ if note and "%s" in note :
179+ # A template note fills (the reason or its default, the wire event name) itself.
180+ return note % (decision .reason or default , wire )
161181 reason = decision .reason
162182 if v .get ("note_style" ) == "suffix" :
163183 reason = reason or default
@@ -174,12 +194,30 @@ def _g1(v, gate, decision, wire, name):
174194 value = _context_value (v , decision )
175195 if at_context_event and value :
176196 return _context_body (name , value )
197+ if wire in v .get ("allow_silent_events" , ()):
198+ return "" , 0
177199 if "allow" in words :
178- return _json .dumps ({"decision" : words ["allow" ]}), 0
200+ out = {"decision" : words ["allow" ]}
201+ if v .get ("allow_context_key" ) and decision .outcome == ALLOW and value :
202+ out [v ["allow_context_key" ]] = value
203+ return _json .dumps (out ), 0
179204 return "" , 0
180- if decision .outcome == TRANSFORM and gate ["honours_transform" ] and decision .updated_input is not None :
181- return _json .dumps ({"hookSpecificOutput" : {"hookEventName" : name , "updatedInput" : decision .updated_input }}), 0
182- out = {"decision" : words .get ("block" , "block" ), "reason" : _refusal_text (v , decision , False )}
205+ if decision .outcome == TRANSFORM and gate ["honours_transform" ]:
206+ if v .get ("transform_grammar" ) == "hook_specific_tool_input" :
207+ return _json .dumps ({"hookSpecificOutput" : {"tool_input" : decision .updated_input }}), 0
208+ if decision .updated_input is not None :
209+ if v .get ("transform_grammar" ) == "top_level_updated_input" :
210+ out = {"decision" : words .get ("transform" , "allow" ), "updatedInput" : decision .updated_input }
211+ if decision .reason :
212+ out ["reason" ] = decision .reason
213+ return _json .dumps (out ), 0
214+ return _json .dumps (
215+ {"hookSpecificOutput" : {"hookEventName" : name , "updatedInput" : decision .updated_input }}
216+ ), 0
217+ if decision .outcome == ESCALATE and gate ["honours_escalate" ] and "escalate" in words :
218+ reason = decision .reason or _default_for (v , decision , True , wire )
219+ return _json .dumps ({"decision" : words ["escalate" ], "reason" : reason }), 0
220+ out = {"decision" : words .get ("block" , "block" ), "reason" : _refusal_text (v , decision , False , wire )}
183221 if at_context_event and v .get ("context_source" ) == "context" and decision .context :
184222 out ["hookSpecificOutput" ] = {"hookEventName" : name , "additionalContext" : decision .context }
185223 return _json .dumps (out ), 0
@@ -221,6 +259,8 @@ def hj_respond(cfg, decision, event):
221259 wire = _wire_name (cfg , event .raw or {})
222260 if wire is None :
223261 wire = v .get ("default_wire_event" )
262+ if wire is None and v .get ("missing_wire" ) == "reverse_map" :
263+ wire = hj_reverse (cfg ).get (event .event )
224264 name = wire if v .get ("echo" ) == "payload" else hj_reverse (cfg ).get (event .event , "PreToolUse" )
225265 gate = v ["gates" ].get (wire )
226266 if gate is None :
0 commit comments