diff --git a/tests/tools/test_delegate_composite_toolsets.py b/tests/tools/test_delegate_composite_toolsets.py index fbb984ea4b..246d3aae6d 100644 --- a/tests/tools/test_delegate_composite_toolsets.py +++ b/tests/tools/test_delegate_composite_toolsets.py @@ -26,6 +26,14 @@ class TestExpandParentToolsets(unittest.TestCase): child_toolsets = [t for t in toolsets if t in expanded] self.assertEqual(child_toolsets, ["web"]) + def test_included_toolsets_of_composite_parent_are_grantable(self): + """A composite parent holds its ``includes`` too (#111700): ``debugging`` = terminal/process_manage + + includes web/file, so a child may request ``web``/``file`` — but never a toolset the parent lacks.""" + expanded = _expand_parent_toolsets({"debugging"}) + self.assertTrue({"debugging", "terminal", "web", "file"} <= expanded) + self.assertNotIn("browser", expanded) + self.assertNotIn("hermes-cli", expanded) + def test_composites_with_allowed_included_tools_are_not_stripped(self): toolsets = ["safe", "hermes-gateway", "hermes-cli", "delegation", "kanban"] diff --git a/tools/delegate_tool_toolsets.py b/tools/delegate_tool_toolsets.py index 0252dd9b65..ead1979a39 100644 --- a/tools/delegate_tool_toolsets.py +++ b/tools/delegate_tool_toolsets.py @@ -37,13 +37,19 @@ def _is_mcp_toolset_name(name: str) -> bool: def _expand_parent_toolsets(parent_toolsets: set) -> set: """Add every toolset whose tools are a subset of the parent's tools: a parent on a composite like ``hermes-cli`` - must still let a child request ``web``/``terminal``; bare name intersection would reject them.""" - parent_tool_names = {t for ts_name in parent_toolsets for t in (TOOLSETS.get(ts_name) or {}).get("tools", [])} + must still let a child request ``web``/``terminal``; bare name intersection would reject them. Both sides use + the RESOLVED static surface: a composite's ``includes`` (``debugging`` -> ``web``/``file``, ``safe``) are tools + the parent genuinely holds, and the child never gains a tool the parent lacks.""" + parent_tool_names = { + t for ts_name in parent_toolsets if ts_name in TOOLSETS for t in resolve_toolset(ts_name, include_registry=False) + } expanded = set(parent_toolsets) if parent_tool_names: expanded.update( - ts_name for ts_name, ts_def in TOOLSETS.items() - if ts_name not in expanded and ts_def.get("tools") and set(ts_def["tools"]).issubset(parent_tool_names) + ts_name for ts_name in TOOLSETS + if ts_name not in expanded + and (resolved := resolve_toolset(ts_name, include_registry=False)) + and set(resolved).issubset(parent_tool_names) ) return expanded