From f617d55f4494c0c0f779ba549a9011e2e7362708 Mon Sep 17 00:00:00 2001 From: Bryant Date: Sun, 5 Jul 2026 21:06:56 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=92=20(core):=20Fail=20closed=20on?= =?UTF-8?q?=20non-dict=20runtime=20policy=20return?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A non-dict query_policy() result (e.g. None from a malformed/partial runtime response) previously reached result.get() outside the try/except and raised AttributeError instead of the clean fail-closed deny. Guard with isinstance before extraction so it degrades through _on_query_failure. refs AAASM-4167 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01R7vqjjo5nrebYNt8WnCNbz --- agent_assembly/core/runtime_interceptor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/agent_assembly/core/runtime_interceptor.py b/agent_assembly/core/runtime_interceptor.py index 4eb7ded..f3d3259 100644 --- a/agent_assembly/core/runtime_interceptor.py +++ b/agent_assembly/core/runtime_interceptor.py @@ -279,6 +279,13 @@ def check_tool_start( # proceed (fail open). return self._on_query_failure("runtime query failed") + # A non-dict result (e.g. ``None`` from a malformed/partial runtime + # response) is not an authoritative verdict; route it through the + # fail-closed path rather than raising ``AttributeError`` on ``.get`` + # (AAASM-4167). + if not isinstance(result, dict): + return self._on_query_failure("runtime returned non-dict result") + # No ``"allow"`` default: a missing / empty ``decision`` is not an # authoritative allow and must route through the fail-closed path below # under ``enforce`` (AAASM-4014). From c3df7847ad4e46f59bdddd2228e2e800dcdc0159 Mon Sep 17 00:00:00 2001 From: Bryant Date: Sun, 5 Jul 2026 21:13:02 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=85=20(core):=20Regression=20test=20f?= =?UTF-8?q?or=20non-dict=20runtime=20policy=20return?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Injects a runtime whose query_policy returns a non-dict (None/str/int/list) and asserts a clean deny under enforce and a clean allow under observe, guarding against the AttributeError that result.get() raised before the isinstance guard. refs AAASM-4167 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01R7vqjjo5nrebYNt8WnCNbz --- test/unit/core/test_runtime_interceptor.py | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/unit/core/test_runtime_interceptor.py b/test/unit/core/test_runtime_interceptor.py index d2c7603..9a1cb83 100644 --- a/test/unit/core/test_runtime_interceptor.py +++ b/test/unit/core/test_runtime_interceptor.py @@ -322,6 +322,40 @@ def test_observe_error_decision_still_fails_open(decision: str) -> None: assert result == {"status": "allow"} +class _NonDictRuntimeClient: + """A runtime whose query_policy returns a non-dict (e.g. ``None``).""" + + def __init__(self, result: Any) -> None: + self._result = result + + def query_policy(self, *_args: Any, **_kwargs: Any) -> Any: + return self._result + + +@pytest.mark.parametrize("bad_result", [None, "deny", 42, ["deny"]]) +def test_enforce_non_dict_return_fails_closed(bad_result: Any) -> None: + """AAASM-4167: a non-dict query_policy return denies under enforce rather + than raising AttributeError on ``result.get``.""" + interceptor = RuntimeQueryInterceptor( + _FakeGatewayClient(), _NonDictRuntimeClient(bad_result), "agent-001", enforce=True + ) + + result = interceptor.check_tool_start(serialized={"name": "t"}, input_str="i") + + assert result["status"] == "deny" + + +@pytest.mark.parametrize("bad_result", [None, "deny", 42, ["deny"]]) +def test_observe_non_dict_return_fails_open(bad_result: Any) -> None: + """AAASM-4167: without enforce a non-dict return degrades to a clean allow, + not an AttributeError.""" + interceptor = RuntimeQueryInterceptor(_FakeGatewayClient(), _NonDictRuntimeClient(bad_result), "agent-001") + + result = interceptor.check_tool_start(serialized={"name": "t"}, input_str="i") + + assert result == {"status": "allow"} + + def test_enforce_unreachable_runtime_denies_all(monkeypatch: pytest.MonkeyPatch) -> None: """AAASM-3106: native present but runtime unreachable yields a deny-all interceptor under enforce, not the fail-open bare client."""