fix(delegate): composite parents can grant their included toolsets to children
_expand_parent_toolsets built the parent's tool surface from each toolset's declared `tools` only, so a composite parent's `includes` were invisible: a child of a `debugging` parent (terminal/process_manage + includes web/file) asking for `file` or `web` was refused, and `safe` / `hermes-gateway` parents could grant nothing but their own name. Same root cause as the `_strip_blocked_tools` fix in the previous commit (#111700, "Related" section). Both sides of the subset check now use the resolved static surface (`resolve_toolset(name, include_registry=False)`), so a child may request any toolset whose real tools the parent genuinely holds, and still never gains a tool the parent lacks. Candidates that resolve to nothing are not expanded into (they cannot be a meaningful subset). Co-authored-by: DresvyanskiyDenis <dresvyanskiydenis@gmail.com>
This commit is contained in:
@@ -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"]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user