Files
hermes-agent/tests/tui_gateway/test_profile_shell_hooks.py
Teknium 3094cde513 fix(desktop): honor configured shell hooks for each chat profile
Wire the existing consent-aware, profile-keyed hook registrar at agent
construction, where the correct session home is already bound. This covers
serve and TUI agent construction without a startup-only registration or a
new helper that swallows registration failures.

Live isolated serve/WebSocket probes reproduce the missing registration on
base for both write_file and terminal, then verify each consented profile
blocks its configured tool while the unapproved profile still runs normally.
Repeated alpha construction does not duplicate hook callbacks.

Slim implementation of the agent-build placement proposed in #57020;
thanks also to the profile-scoped analysis in #102691.

Co-authored-by: grimmjoww578 <willies578@gmail.com>
2026-09-07 04:55:48 -07:00

54 lines
2.8 KiB
Python

"""Profile-bound agent construction must arm real shell policy."""
import os
import shlex
import subprocess
import sys
from pathlib import Path
import yaml
def test_agent_build_arms_only_consented_profile_policy(tmp_path, monkeypatch):
from agent import shell_hooks
from hermes_cli import plugins
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
from tui_gateway import server
monkeypatch.setenv('HERMES_HOME', str(tmp_path))
monkeypatch.setenv('HOME', str(tmp_path / 'os-home'))
monkeypatch.setattr(Path, 'home', lambda: tmp_path / 'os-home')
monkeypatch.setenv('HERMES_IGNORE_RULES', '1')
monkeypatch.delenv('HERMES_ACCEPT_HOOKS', raising=False)
monkeypatch.setattr(server, '_hermes_home', tmp_path)
monkeypatch.setattr(server, '_get_db', lambda: None)
# Only provider resolution is a fixture; registration, agent, hook process,
# consent, manager selection and public dispatch are the real implementation.
monkeypatch.setattr(server, '_resolve_agent_model_runtime', lambda *_a: (
'fixture-model', {'provider': 'openai-compat', 'base_url': 'http://127.0.0.1:18019/v1', 'api_key': 'fixture-key'}))
plugins._reset_plugin_managers_for_tests()
shell_hooks.reset_for_tests()
try:
for label, consent in [('alpha', True), ('beta', True), ('unapproved', False)]:
home = tmp_path / label
home.mkdir()
script = home / 'guard.py'
script.write_text('import json\nprint(json.dumps({"decision":"block","reason":' + repr(label) + '}))\n', encoding='utf-8')
quote_command = subprocess.list2cmdline if os.name == 'nt' else shlex.join
cfg = {'hooks_auto_accept': consent, 'hooks': {'pre_tool_call': [{
'command': quote_command([sys.executable, str(script)]), 'matcher': 'write_file', 'fail_closed': True}]},
'toolsets': ['file'], 'memory': {'memory_enabled': False, 'user_profile_enabled': False}}
(home / 'config.yaml').write_text(yaml.safe_dump(cfg), encoding='utf-8')
for label in ['alpha', 'beta', 'alpha', 'unapproved']:
token = set_hermes_home_override(tmp_path / label)
try:
agent = server._make_agent(label, label, context_cwd_is_launch_artifact=False)
assert agent is not None
block = plugins.get_pre_tool_call_block_message('write_file', {'path': str(tmp_path / 'protected'), 'content': 'x'})
assert block == (None if label == 'unapproved' else label)
assert plugins.get_pre_tool_call_block_message('read_file', {'path': str(tmp_path / 'protected')}) is None
finally:
reset_hermes_home_override(token)
finally:
plugins._reset_plugin_managers_for_tests()
shell_hooks.reset_for_tests()