Files
hermes-agent/tests/cron/test_cron_explicit_empty_toolsets.py
Austin Pickett b4969d8e2d fix(tools): explicit empty toolset allowlist must fail closed, not open
An explicitly-set empty allowlist meant 'nothing allowed' everywhere in
prose, but every boundary normalized it away:

- tui_gateway _load_enabled_toolsets converted an empty resolved set to
  None (= no restriction), so platform_toolsets.cli: [] handed the
  session every default toolset (#82010).
- cron create/update normalized enabled_toolsets=[] to null, and the
  dispatch resolver treated a stored [] as absent (falsy), falling back
  to platform_toolsets.cron.

Distinguish unset (None = no restriction, unchanged) from explicit
empty ([] = zero toolsets) end to end: the gateway returns [] bare (no
client-surface fold-in), cron persists and reloads [] verbatim, and
dispatch resolves [] to zero toolsets with no MCP merge. Absent keys
keep today's defaults.
2026-10-06 14:51:26 -04:00

82 lines
2.9 KiB
Python

"""An explicitly-set EMPTY per-job toolset allowlist must mean 'nothing allowed' end to end.
The create/update normalizers turned ``enabled_toolsets=[]`` into null, and the dispatch
resolver treated a stored ``[]`` as absent (falsy), so a locked-down job silently widened
back to the platform default — every toolset (#82010). Absent/null keeps meaning
'no per-job restriction'."""
from __future__ import annotations
import pytest
@pytest.fixture
def hermes_env(tmp_path, monkeypatch):
"""Isolate HERMES_HOME for each test so jobs don't leak between tests."""
home = tmp_path / ".hermes"
home.mkdir()
(home / "scripts").mkdir()
(home / "cron").mkdir()
monkeypatch.setenv("HERMES_HOME", str(home))
import importlib
import hermes_constants
importlib.reload(hermes_constants)
import cron.jobs
importlib.reload(cron.jobs)
import cron.scheduler
importlib.reload(cron.scheduler)
return home
def test_create_persists_explicit_empty_allowlist_and_survives_reload(hermes_env):
import importlib
from cron.jobs import create_job
job = create_job(prompt="daily digest", schedule="every 1h", enabled_toolsets=[])
assert job["enabled_toolsets"] == []
# A fresh process (module reload) must read the restriction back from jobs.json as [],
# not null — null would read as 'no restriction' and widen the job.
import cron.jobs
importlib.reload(cron.jobs)
reloaded = cron.jobs.get_job(job["id"])
assert reloaded is not None and reloaded["enabled_toolsets"] == []
def test_update_path_preserves_explicit_empty_allowlist(hermes_env):
from cron.jobs import create_job, update_job
job = create_job(prompt="daily digest", schedule="every 1h")
assert job["enabled_toolsets"] is None # absent = no per-job restriction
updated = update_job(job["id"], {"enabled_toolsets": []})
assert updated is not None and updated["enabled_toolsets"] == []
def test_scheduler_resolves_explicit_empty_as_zero_toolsets(hermes_env):
from cron.scheduler import _resolve_cron_enabled_toolsets
# [] = zero toolsets, fail closed — not a fall-through to the platform default.
assert _resolve_cron_enabled_toolsets({"enabled_toolsets": []}, {}) == []
# Absent stays 'no per-job restriction': the platform default selection still applies.
assert _resolve_cron_enabled_toolsets({}, {})
def test_cronjob_tool_roundtrip_preserves_explicit_empty(hermes_env):
"""The reporter's exact path: cronjob create with enabled_toolsets=[] from a session."""
import json
from cron.jobs import get_job
from tools.cronjob_tools import cronjob
result = json.loads(cronjob(
action="create", prompt="daily digest", schedule="every 1h", enabled_toolsets=[]))
assert result["success"] is True, result
job = get_job(result["job_id"])
assert job is not None and job["enabled_toolsets"] == []