From 1e4df599ece6987f93a57598026e9ce8d88568e5 Mon Sep 17 00:00:00 2001 From: Riyasudeen Farook Date: Thu, 25 Jun 2026 01:37:25 +0530 Subject: [PATCH 1/2] fix(delegate): strip cronjob toolset from delegated children (#43466) _strip_blocked_tools used a hardcoded set missing 'cronjob'. Children on gateway platforms could inherit the cronjob toolset, scheduling persistent jobs that outlive the delegation despite DELEGATE_BLOCKED_TOOLS. Fix: derive the strip set from DELEGATE_BLOCKED_TOOLS at runtime so the two lists can never drift. Add 'cronjob' to DELEGATE_BLOCKED_TOOLS for documentation consistency. Two regression tests lock the invariant. Salvaged from #43687 by @riyas22. Adapted test to current main (no 'messaging' toolset exists -- send_message is intentionally not registered as an agent tool). Closes #43466 --- tests/tools/test_delegate.py | 31 +++++++++++++++++++++++++++++++ tools/delegate_tool.py | 20 +++++++++++++++----- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 911025e9993b4..b6ba4c66f7a05 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -156,6 +156,37 @@ class TestStripBlockedTools(unittest.TestCase): result = _strip_blocked_tools([]) self.assertEqual(result, []) + def test_strips_cronjob_toolset(self): + """Regression for issue #43466: child subagents must not inherit + the cronjob toolset from a parent running on a gateway platform. + Without this guard, a delegated child could schedule new cron jobs + under the parent's identity. + """ + result = _strip_blocked_tools( + ["terminal", "file", "cronjob", "web"] + ) + self.assertNotIn("cronjob", result) + self.assertIn("terminal", result) + self.assertIn("file", result) + self.assertIn("web", result) + + def test_strip_set_derived_from_blocklist(self): + """The strip set must be derived from DELEGATE_BLOCKED_TOOLS so a + new blocked tool can't silently leak through as a toolset name + (regression for issue #43466's 'more robust variant' suggestion). + """ + from tools.delegate_tool import TOOLSETS, _strip_blocked_tools + # Every toolset whose tools are ALL in the blocklist should be stripped + for name, defn in TOOLSETS.items(): + tools = defn.get("tools", []) + if tools and all(t in DELEGATE_BLOCKED_TOOLS for t in tools): + self.assertNotIn( + name, + _strip_blocked_tools([name, "terminal"]), + f"Toolset {name!r} (tools={tools}) is fully blocked " + f"but was not stripped", + ) + class TestDelegateTask(unittest.TestCase): def test_no_parent_agent(self): diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 1be02f240e07a..04cf67f4f1a64 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -49,6 +49,7 @@ DELEGATE_BLOCKED_TOOLS = frozenset( "memory", # no writes to shared MEMORY.md "send_message", # no cross-platform side effects "execute_code", # children should reason step-by-step, not write scripts + "cronjob", # no scheduling more work in the parent's name ] ) @@ -766,12 +767,21 @@ def _resolve_workspace_hint(parent_agent) -> Optional[str]: def _strip_blocked_tools(toolsets: List[str]) -> List[str]: - """Remove toolsets that contain only blocked tools.""" + """Remove toolsets that contain only blocked tools. + + The strip set is derived from DELEGATE_BLOCKED_TOOLS plus the explicit + composite/scenario toolsets (delegation, code_execution) that have no + one-to-one tool. This keeps the blocklist and the strip set in lockstep + so new blocked tools can't silently leak through as toolset names. + """ + # Composite toolsets that should never pass through to children, even + # though their individual tools aren't all in DELEGATE_BLOCKED_TOOLS. + _COMPOSITE_BLOCKED_TOOLSETS = frozenset({"delegation", "code_execution"}) blocked_toolset_names = { - "delegation", - "clarify", - "memory", - "code_execution", + name + for name, defn in TOOLSETS.items() + if name in _COMPOSITE_BLOCKED_TOOLSETS + or all(t in DELEGATE_BLOCKED_TOOLS for t in defn.get("tools", [])) } return [t for t in toolsets if t not in blocked_toolset_names] From e25b56fc648728552b3b3145bafb2f4aa665c285 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Thu, 25 Jun 2026 01:39:11 +0530 Subject: [PATCH 2/2] chore: AUTHOR_MAP entry for riyas22 (PR #43687 salvage) --- scripts/release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.py b/scripts/release.py index d0ee8bf0cf941..45a3737ed152b 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1616,6 +1616,7 @@ AUTHOR_MAP = { "claw@openclaw.ai": "wanwan2qq", # PR #10215 (strip brackets/quotes from /resume; gateway session-ID lookup) "simo.kiihamaki@gmail.com": "SimoKiihamaki", # PR #30773 (Windows /reset+/new freeze; stdin fallback for modal) "66773372+Tranquil-Flow@users.noreply.github.com": "Tranquil-Flow", # PR #27518 (bracketed-paste timeout) + "uriyas22@gmail.com": "riyas22", # PR #43687 salvage (strip cronjob toolset from delegated children, #43466) "8bit64k@pm.me": "8bit64k", # PR #14681 (TUI /q alias from quit to queue) "chenglunhu@gmail.com": "hclsys", # PR #31985 (TUI /q alias regression test) "dearmayo@localhost": "ffr31mr", # PR #32103 (SubdirectoryHintTracker workspace boundary)