mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat(plugins): let pre_tool_call hooks block tool execution
Plugins can now return {"action": "block", "message": "reason"} from
their pre_tool_call hook to prevent a tool from executing. The error
message is returned to the model as a tool result so it can adjust.
Covers both execution paths: handle_function_call (model_tools.py) and
agent-level tools (run_agent.py _invoke_tool + sequential/concurrent).
Blocked tools skip all side effects (counter resets, checkpoints,
callbacks, read-loop tracker).
Adds skip_pre_tool_call_hook flag to avoid double-firing the hook when
run_agent.py already checked and then calls handle_function_call.
Salvaged from PR #5385 (gianfrancopiana) and PR #4610 (oredsecurity).
This commit is contained in:
parent
ea74f61d98
commit
eabc0a2f66
6 changed files with 335 additions and 40 deletions
|
|
@ -18,6 +18,7 @@ from hermes_cli.plugins import (
|
|||
PluginManager,
|
||||
PluginManifest,
|
||||
get_plugin_manager,
|
||||
get_pre_tool_call_block_message,
|
||||
discover_plugins,
|
||||
invoke_hook,
|
||||
)
|
||||
|
|
@ -310,6 +311,50 @@ class TestPluginHooks:
|
|||
assert any("on_banana" in record.message for record in caplog.records)
|
||||
|
||||
|
||||
class TestPreToolCallBlocking:
|
||||
"""Tests for the pre_tool_call block directive helper."""
|
||||
|
||||
def test_block_message_returned_for_valid_directive(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.plugins.invoke_hook",
|
||||
lambda hook_name, **kwargs: [{"action": "block", "message": "blocked by plugin"}],
|
||||
)
|
||||
assert get_pre_tool_call_block_message("todo", {}, task_id="t1") == "blocked by plugin"
|
||||
|
||||
def test_invalid_returns_are_ignored(self, monkeypatch):
|
||||
"""Various malformed hook returns should not trigger a block."""
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.plugins.invoke_hook",
|
||||
lambda hook_name, **kwargs: [
|
||||
"block", # not a dict
|
||||
123, # not a dict
|
||||
{"action": "block"}, # missing message
|
||||
{"action": "deny", "message": "nope"}, # wrong action
|
||||
{"message": "missing action"}, # no action key
|
||||
{"action": "block", "message": 123}, # message not str
|
||||
],
|
||||
)
|
||||
assert get_pre_tool_call_block_message("todo", {}, task_id="t1") is None
|
||||
|
||||
def test_none_when_no_hooks(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.plugins.invoke_hook",
|
||||
lambda hook_name, **kwargs: [],
|
||||
)
|
||||
assert get_pre_tool_call_block_message("web_search", {"q": "test"}) is None
|
||||
|
||||
def test_first_valid_block_wins(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.plugins.invoke_hook",
|
||||
lambda hook_name, **kwargs: [
|
||||
{"action": "allow"},
|
||||
{"action": "block", "message": "first blocker"},
|
||||
{"action": "block", "message": "second blocker"},
|
||||
],
|
||||
)
|
||||
assert get_pre_tool_call_block_message("terminal", {}) == "first blocker"
|
||||
|
||||
|
||||
# ── TestPluginContext ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue