fix: hide memory tool from cron agents

Cron agents are constructed with skip_memory=True, so the memory
backend is not initialised — exposing the memory tool only gives the
model an unbacked tool that fails at runtime with 'Memory is not
available.'  Add 'memory' to _resolve_cron_disabled_toolsets() so the
tool is stripped from the schema before the model can call it.

Fixes #38129.

Co-authored-by: Paolo Shamoon <Paolo@Dylans-Mac-Studio.local>
This commit is contained in:
Paolo Shamoon 2026-08-06 05:00:01 +05:30 committed by kshitij
parent ca120413fc
commit 03dc4aad52
2 changed files with 47 additions and 2 deletions

View File

@ -165,17 +165,19 @@ class CronPromptInjectionBlocked(Exception):
def _resolve_cron_disabled_toolsets(cfg: dict) -> list[str]:
"""Toolsets a cron-spawned agent must never receive.
Three protected toolsets are always disabled in cron context:
Four protected toolsets are always disabled in cron context:
- ``cronjob`` would let a cron-spawned agent schedule more cron jobs
- ``messaging`` interactive, needs a live gateway session
- ``clarify`` interactive, blocks waiting for user input
- ``memory`` cron agents are constructed with ``skip_memory=True``, so
exposing this tool only gives the model an unbacked tool that fails
User-level ``agent.disabled_toolsets`` from config.yaml is layered on top
so per-job ``enabled_toolsets`` cannot bypass policy that applies to
ordinary agent runs (#25752 — LLM-supplied enabled_toolsets was widening
past config.yaml's denylist).
"""
disabled = ["cronjob", "messaging", "clarify"]
disabled = ["cronjob", "messaging", "clarify", "memory"]
agent_cfg = (cfg or {}).get("agent") or {}
user_disabled = agent_cfg.get("disabled_toolsets") or []
for name in user_disabled:

View File

@ -533,6 +533,49 @@ class TestRunJobSessionPersistence:
yield fake_db, mock_agent_cls
def test_run_job_memory_toolset_disabled_in_cron(self, tmp_path):
"""memory toolset must be disabled in cron sessions — issue #38129.
Cron agents are constructed with skip_memory=True, so the memory
backend is not initialised. Exposing the memory tool only gives the
model an unbacked tool that fails at runtime with
"Memory is not available." Hiding it from the schema prevents that.
"""
job = {
"id": "memory-hide-job",
"name": "test",
"prompt": "hello",
}
with self._run_job_patches(tmp_path) as (fake_db, mock_agent_cls):
run_job(job)
kwargs = mock_agent_cls.call_args.kwargs
assert "memory" in (kwargs["disabled_toolsets"] or []), (
"memory toolset should be disabled in cron to match skip_memory=True"
)
def test_run_job_disables_memory_even_when_per_job_enables_it(self, tmp_path):
"""Cron runs pass skip_memory=True, so memory must not be exposed.
A cron job can request the memory tool through enabled_toolsets, but
there is no MemoryStore injected for cron agents. Keep memory in the
disabled set so AIAgent filters the unbacked tool out before the model
can call it and receive "Memory is not available" failures.
"""
job = {
"id": "memory-toolset-job",
"name": "test",
"prompt": "remember what you learn",
"enabled_toolsets": ["memory", "file"],
}
with self._run_job_patches(tmp_path) as (fake_db, mock_agent_cls):
run_job(job)
kwargs = mock_agent_cls.call_args.kwargs
assert kwargs["skip_memory"] is True
assert kwargs["enabled_toolsets"] == ["memory", "file"]
assert "memory" in kwargs["disabled_toolsets"]
def test_tick_skips_due_jobs_while_dispatch_is_paused(self, tmp_path):
"""The drain gate runs before advancing a due job's schedule."""
from cron.scheduler import tick