Skip to content

Commit bfefd2e

Browse files
committed
feat(cron): honor hermes tools config for the cron platform (NousResearch#14798)
Cron now resolves its toolset from the same per-platform config the gateway uses — `_get_platform_tools(cfg, 'cron')` — instead of blindly loading every default toolset. Existing cron jobs without a per-job override automatically lose `moa`, `homeassistant`, and `rl` (the `_DEFAULT_OFF_TOOLSETS` set), which stops the "surprise $4.63 mixture_of_agents run" class of bug (Norbert, Discord). Precedence inside `run_job`: 1. per-job `enabled_toolsets` (PR NousResearch#14767 / NousResearch#6130) — wins if set 2. `_get_platform_tools(cfg, 'cron')` — new, the blanket gate 3. `None` fallback (legacy) — only on resolver exception Changes: - hermes_cli/platforms.py: register 'cron' with default_toolset 'hermes-cron' - toolsets.py: add 'hermes-cron' toolset (mirrors 'hermes-cli'; `_get_platform_tools` then filters via `_DEFAULT_OFF_TOOLSETS`) - cron/scheduler.py: add `_resolve_cron_enabled_toolsets(job, cfg)`, call it at the `AIAgent(...)` kwargs site - tests/cron/test_scheduler.py: replace the 'None when not set' test (outdated contract) with an invariant ('moa not in default cron toolset') + new per-job-wins precedence test - tests/hermes_cli/test_tools_config.py: mark 'cron' as non-messaging in the gateway-toolset-coverage test
1 parent 9774fc4 commit bfefd2e

5 files changed

Lines changed: 88 additions & 5 deletions

File tree

cron/scheduler.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,37 @@
4040

4141
logger = logging.getLogger(__name__)
4242

43+
44+
def _resolve_cron_enabled_toolsets(job: dict, cfg: dict) -> list[str] | None:
45+
"""Resolve the toolset list for a cron job.
46+
47+
Precedence:
48+
1. Per-job ``enabled_toolsets`` (set via ``cronjob`` tool on create/update).
49+
Keeps the agent's job-scoped toolset override intact — #6130.
50+
2. Per-platform ``hermes tools`` config for the ``cron`` platform.
51+
Mirrors gateway behavior (``_get_platform_tools(cfg, platform_key)``)
52+
so users can gate cron toolsets globally without recreating every job.
53+
3. ``None`` on any lookup failure — AIAgent loads the full default set
54+
(legacy behavior before this change, preserved as the safety net).
55+
56+
_DEFAULT_OFF_TOOLSETS ({moa, homeassistant, rl}) are removed by
57+
``_get_platform_tools`` for unconfigured platforms, so fresh installs
58+
get cron WITHOUT ``moa`` by default (issue reported by Norbert —
59+
surprise $4.63 run).
60+
"""
61+
per_job = job.get("enabled_toolsets")
62+
if per_job:
63+
return per_job
64+
try:
65+
from hermes_cli.tools_config import _get_platform_tools # lazy: avoid heavy import at cron module load
66+
return sorted(_get_platform_tools(cfg or {}, "cron"))
67+
except Exception as exc:
68+
logger.warning(
69+
"Cron toolset resolution failed, falling back to full default toolset: %s",
70+
exc,
71+
)
72+
return None
73+
4374
# Valid delivery platforms — used to validate user-supplied platform names
4475
# in cron delivery targets, preventing env var enumeration via crafted names.
4576
_KNOWN_DELIVERY_PLATFORMS = frozenset({
@@ -886,7 +917,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
886917
providers_ignored=pr.get("ignore"),
887918
providers_order=pr.get("order"),
888919
provider_sort=pr.get("sort"),
889-
enabled_toolsets=job.get("enabled_toolsets") or None,
920+
enabled_toolsets=_resolve_cron_enabled_toolsets(job, _cfg),
890921
disabled_toolsets=["cronjob", "messaging", "clarify"],
891922
quiet_mode=True,
892923
skip_context_files=True, # Don't inject SOUL.md/AGENTS.md from scheduler cwd

hermes_cli/platforms.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class PlatformInfo(NamedTuple):
3838
("qqbot", PlatformInfo(label="💬 QQBot", default_toolset="hermes-qqbot")),
3939
("webhook", PlatformInfo(label="🔗 Webhook", default_toolset="hermes-webhook")),
4040
("api_server", PlatformInfo(label="🌐 API Server", default_toolset="hermes-api-server")),
41+
("cron", PlatformInfo(label="⏰ Cron", default_toolset="hermes-cron")),
4142
])
4243

4344

tests/cron/test_scheduler.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,15 @@ def test_run_job_passes_enabled_toolsets_to_agent(self, tmp_path):
710710
kwargs = mock_agent_cls.call_args.kwargs
711711
assert kwargs["enabled_toolsets"] == ["web", "terminal", "file"]
712712

713-
def test_run_job_enabled_toolsets_none_when_not_set(self, tmp_path):
713+
def test_run_job_enabled_toolsets_resolves_from_platform_config_when_not_set(self, tmp_path):
714+
"""When a job has no explicit enabled_toolsets, the scheduler now
715+
resolves them from ``hermes tools`` platform config for ``cron``
716+
(PR #14xxx — blanket fix for Norbert's surprise ``moa`` run).
717+
718+
The legacy "pass None → AIAgent loads full default" path is still
719+
reachable, but only when ``_get_platform_tools`` raises (safety net
720+
for any unexpected config shape).
721+
"""
714722
job = {
715723
"id": "no-toolset-job",
716724
"name": "test",
@@ -725,7 +733,39 @@ def test_run_job_enabled_toolsets_none_when_not_set(self, tmp_path):
725733
run_job(job)
726734

727735
kwargs = mock_agent_cls.call_args.kwargs
728-
assert kwargs["enabled_toolsets"] is None
736+
# Resolution happened — not None, is a list.
737+
assert isinstance(kwargs["enabled_toolsets"], list)
738+
# The cron default is _HERMES_CORE_TOOLS with _DEFAULT_OFF_TOOLSETS
739+
# (``moa``, ``homeassistant``, ``rl``) removed. The most important
740+
# invariant: ``moa`` is NOT in the default cron toolset, so a cron
741+
# run cannot accidentally spin up frontier models.
742+
assert "moa" not in kwargs["enabled_toolsets"]
743+
744+
def test_run_job_per_job_toolsets_win_over_platform_config(self, tmp_path):
745+
"""Per-job enabled_toolsets (via cronjob tool) always take precedence
746+
over the platform-level ``hermes tools`` config."""
747+
job = {
748+
"id": "override-job",
749+
"name": "test",
750+
"prompt": "hello",
751+
"enabled_toolsets": ["terminal"],
752+
}
753+
fake_db, patches = self._make_run_job_patches(tmp_path)
754+
# Even if the user has ``hermes tools`` configured to enable web+file
755+
# for cron, the per-job override wins.
756+
with patches[0], patches[1], patches[2], patches[3], patches[4], \
757+
patch("run_agent.AIAgent") as mock_agent_cls, \
758+
patch(
759+
"hermes_cli.tools_config._get_platform_tools",
760+
return_value={"web", "file"},
761+
):
762+
mock_agent = MagicMock()
763+
mock_agent.run_conversation.return_value = {"final_response": "ok"}
764+
mock_agent_cls.return_value = mock_agent
765+
run_job(job)
766+
767+
kwargs = mock_agent_cls.call_args.kwargs
768+
assert kwargs["enabled_toolsets"] == ["terminal"]
729769

730770
def test_run_job_empty_response_returns_empty_not_placeholder(self, tmp_path):
731771
"""Empty final_response should stay empty for delivery logic (issue #2234).

tests/hermes_cli/test_tools_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ def test_gateway_toolset_includes_all_messaging_platforms(self):
463463

464464
gateway_includes = set(TOOLSETS["hermes-gateway"]["includes"])
465465
# Exclude non-messaging platforms from the check
466-
non_messaging = {"cli", "api_server"}
466+
non_messaging = {"cli", "api_server", "cron"}
467467
for platform, meta in PLATFORMS.items():
468468
if platform in non_messaging:
469469
continue

toolsets.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,18 @@
295295
"tools": _HERMES_CORE_TOOLS,
296296
"includes": []
297297
},
298-
298+
299+
"hermes-cron": {
300+
# Mirrors hermes-cli so cron's "default" toolset is the same set of
301+
# core tools users see interactively — then `hermes tools` filters
302+
# them down per the platform config. _DEFAULT_OFF_TOOLSETS (moa,
303+
# homeassistant, rl) are excluded by _get_platform_tools() unless
304+
# the user explicitly enables them.
305+
"description": "Default cron toolset - same core tools as hermes-cli; gated by `hermes tools`",
306+
"tools": _HERMES_CORE_TOOLS,
307+
"includes": []
308+
},
309+
299310
"hermes-telegram": {
300311
"description": "Telegram bot toolset - full access for personal use (terminal has safety checks)",
301312
"tools": _HERMES_CORE_TOOLS,

0 commit comments

Comments
 (0)