@@ -193,31 +193,69 @@ def _format_approval_rejected_message(reason: str | None) -> str:
193193 return f"[APPROVAL REJECTED] Action was reviewed and denied: { reason_text } "
194194
195195
196+ _UNKNOWN_DECISION_REASON = "Unrecognized governance decision; denied under enforce."
197+
198+
199+ def _interceptor_enforces (callback_handler : Any ) -> bool :
200+ """Return whether the wired interceptor is in fail-closed ``enforce`` posture.
201+
202+ The governance interceptor (``RuntimeQueryInterceptor`` /
203+ ``_FailClosedInterceptor``) carries ``_enforce`` set from
204+ ``enforcement_mode == "enforce"`` (AAASM-3106). A bare ``GatewayClient`` — used
205+ when no native runtime authority is engaged — has no such attribute and
206+ defaults to fail-open. AAASM-3107 reuses this flag so an unknown / malformed
207+ verdict denies under enforce instead of silently allowing.
208+
209+ Compared strictly against ``True`` so a stub interceptor whose ``__getattr__``
210+ synthesizes truthy values for missing attributes is not mistaken for the
211+ enforce posture; the real flag is always a ``bool``.
212+ """
213+ target = getattr (callback_handler , "_interceptor" , callback_handler )
214+ return getattr (target , "_enforce" , False ) is True
215+
216+
217+ def _unknown_decision (enforce : bool ) -> tuple [Literal ["allow" , "deny" , "pending" ], str | None ]:
218+ """Map an unrecognized / malformed verdict, failing closed under ``enforce``.
219+
220+ Under ``enforce`` the SDK is a security control: an unknown, ``None``, or
221+ malformed verdict must not be silently allowed (AAASM-3107), so it denies.
222+ Under ``observe`` / ``disabled`` it proceeds (fail open), preserving the
223+ dry-run / hermetic posture.
224+ """
225+ if enforce :
226+ return "deny" , _UNKNOWN_DECISION_REASON
227+ return "allow" , None
228+
229+
196230def _normalize_decision (
197231 decision : object ,
232+ * ,
233+ enforce : bool = False ,
198234) -> tuple [Literal ["allow" , "deny" , "pending" ], str | None ]:
199235 if isinstance (decision , str ):
200236 normalized = decision .strip ().lower ()
237+ if normalized == "allow" :
238+ return "allow" , None
201239 if normalized == "deny" :
202240 return "deny" , None
203241 if normalized == "pending" :
204242 return "pending" , None
205- return "allow" , None
243+ return _unknown_decision ( enforce )
206244
207245 if isinstance (decision , Mapping ):
208- raw_status = str (decision .get ("status" , "allow" )).strip ().lower ()
209- if raw_status == "deny" :
210- status : Literal ["allow" , "deny" , "pending" ] = "deny"
211- elif raw_status == "pending" :
212- status = "pending"
213- else :
214- status = "allow"
215-
246+ raw_status = str (decision .get ("status" , "" )).strip ().lower ()
216247 reason_value = decision .get ("reason" )
217248 reason = str (reason_value ) if reason_value is not None else None
218- return status , reason
249+ if raw_status == "allow" :
250+ return "allow" , reason
251+ if raw_status == "deny" :
252+ return "deny" , reason
253+ if raw_status == "pending" :
254+ return "pending" , reason
255+ unknown_status , unknown_reason = _unknown_decision (enforce )
256+ return unknown_status , reason if reason is not None else unknown_reason
219257
220- return "allow" , None
258+ return _unknown_decision ( enforce )
221259
222260
223261def _invoke_sync_tool_check (
@@ -306,6 +344,7 @@ def _apply_basetool_run_patch(base_tool_cls: type[Any], callback_handler: Any) -
306344 return None
307345
308346 original_run = base_tool_cls .run
347+ enforce = _interceptor_enforces (callback_handler )
309348
310349 @wraps (original_run )
311350 def patched_run (self : Any , * args : Any , ** kwargs : Any ) -> Any :
@@ -318,7 +357,7 @@ def patched_run(self: Any, *args: Any, **kwargs: Any) -> Any:
318357 tool_args = tool_args ,
319358 agent_id = agent_id ,
320359 )
321- status , reason = _normalize_decision (decision )
360+ status , reason = _normalize_decision (decision , enforce = enforce )
322361 is_pending_flow = False
323362 if status == "pending" :
324363 is_pending_flow = True
@@ -330,7 +369,7 @@ def patched_run(self: Any, *args: Any, **kwargs: Any) -> Any:
330369 tool_args = tool_args ,
331370 agent_id = agent_id ,
332371 )
333- status , reason = _normalize_decision (final_decision )
372+ status , reason = _normalize_decision (final_decision , enforce = enforce )
334373
335374 if status == "deny" :
336375 if is_pending_flow :
0 commit comments