fix: fail-closed on non-object JSON and surface HOL Guard denial in result

- Return False for non-dict verdicts (e.g. [], "allow", null) to avoid
  AttributeError and preserve fail-closed behavior (CodeRabbit)
- Set ToolResponse.result to the denial message since the agent tool
  executor returns result and discards error, so denials reach the model
  instead of a null result (Greptile)
- Add non-object JSON regression test; update block test assertion

Co-authored-by: Mervin Praison <[email protected]>
This commit is contained in:
praisonai-triage-agent[bot]andMervin Praison committed 2026-08-24 01:17:08 +00:00
1 parent 96f6d0d93b
commit 37a80c3133
2 files changed
+34 -3

No files matched your search

+10 -2
View File
@@ -65,6 +65,9 @@ def _inspect_with_hol_guard(command: str) -> bool:
except (json.JSONDecodeError, ValueError):
return False
if not isinstance(verdict, dict):
return False
decision = str(verdict.get("decision", "")).lower()
return decision in ("allow", "benign")
@@ -80,10 +83,15 @@ def hol_guard_tool_policy(request: ToolRequest, call_next):
if _inspect_with_hol_guard(command):
return call_next(request)
blocked_message = "Blocked by HOL Guard: command was not explicitly allowed."
# Surface the denial in `result` too: the agent tool executor returns
# `ToolResponse.result` to the model and discards `error`, so a denial with
# only `error` set would reach the model as a null result. Keeping `error`
# populated preserves the signal for after_tool hooks and logging.
return ToolResponse(
tool_name=request.tool_name,
result=None,
error="Blocked by HOL Guard: command was not explicitly allowed.",
result=blocked_message,
error=blocked_message,
context=request.context,
)
@@ -84,7 +84,9 @@ def test_block_verdict_does_not_execute():
assert executed == []
assert result.error is not None
assert result.result is None
# The denial message is surfaced in `result` too, because the agent tool
# executor returns `ToolResponse.result` to the model and discards `error`.
assert result.result == result.error
def test_missing_cli_fails_closed():
@@ -122,6 +124,27 @@ def test_malformed_json_fails_closed():
assert result.error is not None
def test_non_object_json_fails_closed():
module = _load_example()
executed = []
def call_next(req):
executed.append(req)
return "ran"
for payload in ("[]", '"allow"', "null"):
with patch.object(module.shutil, "which", return_value="/usr/bin/hol-guard"), \
patch.object(
module.subprocess,
"run",
return_value=_FakeCompleted(0, payload),
):
result = module.hol_guard_tool_policy(_make_request(), call_next)
assert executed == []
assert result.error is not None
def test_nonzero_exit_fails_closed():
module = _load_example()
executed = []