Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions tests/tools/test_delegate_toolset_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,32 @@ def test_strip_blocked_removes_delegation(self):
assert "memory" not in child
assert "terminal" in child

def test_strip_blocked_removes_kanban(self):
"""The kanban toolset must never pass through to a delegated child.

A child inherits the parent's ``HERMES_KANBAN_TASK`` env, so a child
that also held ``kanban_complete`` / ``kanban_block`` would resolve
the *parent's* card and could falsely flip it to done with the
child's summary (control-surface-bleed / false-done). Children get no
card-terminal authority.
"""
child = _strip_blocked_tools(["terminal", "file", "kanban"])
assert "kanban" not in child
assert "terminal" in child
assert "file" in child

def test_kanban_stripped_even_when_inherited_from_parent(self):
"""No-toolsets-requested inheritance path must also drop kanban.

When ``toolsets`` is None the child inherits the parent's full set
through ``_strip_blocked_tools`` β€” the kanban strip has to hold on
that path too, not just when the LLM explicitly requests it.
"""
parent_toolsets = ["terminal", "file", "kanban", "web"]
child = _strip_blocked_tools(parent_toolsets)
assert "kanban" not in child
assert sorted(child) == ["file", "terminal", "web"]

def test_empty_intersection_yields_empty_toolsets(self):
"""If parent has no overlap with requested, child gets nothing extra."""
parent = SimpleNamespace(enabled_toolsets=["terminal"])
Expand Down
16 changes: 13 additions & 3 deletions tools/delegate_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,13 +769,23 @@ def _strip_blocked_tools(toolsets: List[str]) -> List[str]:
"""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
composite/scenario toolsets (delegation, code_execution, kanban) 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.

``kanban`` is stripped unconditionally β€” even for orchestrator children.
A delegated child inherits the parent's ``HERMES_KANBAN_TASK`` env, so a
child holding the kanban terminal verbs (``kanban_complete`` /
``kanban_block``) would resolve the *parent's* card and could retire it
with the child's summary while the real deliverable was never produced
(false-done, control-surface-bleed). Children get no card-terminal
authority.
"""
# 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"})
_COMPOSITE_BLOCKED_TOOLSETS = frozenset(
{"delegation", "code_execution", "kanban"}
)
blocked_toolset_names = {
name
for name, defn in TOOLSETS.items()
Expand Down
Loading