Skip to content

fix(delegate): lazy MCP discovery + profile toolset bypass for no_mcp platforms#32727

Closed
davidgut1982 wants to merge 4 commits into
NousResearch:mainfrom
davidgut1982:fix/profile-mcp-toolset-bypass
Closed

fix(delegate): lazy MCP discovery + profile toolset bypass for no_mcp platforms#32727
davidgut1982 wants to merge 4 commits into
NousResearch:mainfrom
davidgut1982:fix/profile-mcp-toolset-bypass

Conversation

@davidgut1982

@davidgut1982 davidgut1982 commented May 26, 2026

Copy link
Copy Markdown
Contributor

fix(delegate): lazy MCP discovery + profile toolset bypass for no_mcp platforms

What this is

Two related fixes that together solve silent tool-loss when using named agent_profiles on api_server / no_mcp platforms. Fixes #32668. Extended and security-hardened in #35526; supports the progressive-disclosure work in #35457.


Problem

The orchestrator pattern — a no_mcp parent routing work to MCP-capable profile sub-agents — fails silently in two distinct ways before this PR:

Phase 1 — silent tool-loss via parent intersection. _build_child_agent() resolves child toolsets by intersecting the requested list against what the parent has loaded. This is correct as a security boundary for ad-hoc delegation. But for named agent_profiles, it breaks: if the orchestrator's MCP context is restricted (via platform_toolsets + no_mcp), any child requesting domain MCP tools (e.g. mcp-fastmail) gets an empty tool list — no error, no warning, just a child that cannot do its job.

Phase 2 — eager MCP startup cost on no_mcp platforms. discover_mcp_tools() ran at startup unconditionally. For api_server / no_mcp platforms, all MCP connections were established and schemas loaded (12,000–14,000 tokens of domain tool definitions) for work the orchestrator would never perform directly.


Approach

Phase 1 — profile toolset bypass. When spawning from a named profile, MCP toolsets bypass the parent intersection and resolve from global config. Non-MCP toolsets still require parent membership, preserving the security boundary for ad-hoc delegation.

Phase 2 — lazy MCP discovery. Skip eager startup when no_mcp is in platform toolsets. Trigger a one-shot thread-safe discovery on the first child build that actually requests MCP toolsets. Failure during lazy discovery is tolerant — partial results are used and logged rather than crashing the child build.

Together these enable the orchestrator-stub pattern: the orchestrator sees only routing stubs at startup; workers load domain tools on first activation. This is the same pattern used by LangGraph, CrewAI, AutoGen, and similar multi-agent frameworks.


PROOF — profile-toolset authority and MCP isolation

Validated against a live deployed build (OpenRouter + deepseek). Adversarial 9-scenario suite (A–I), critic-reviewed over two cycles.

Observed invariants (all verified):

Scenario Expected Result
Profile toolset is authoritative Profile-declared toolset cannot be widened by caller or model Confirmed — batch-mode per-task toolset injection rejected; profile wins
No parent MCP bleed Child spawned via named profile with inherit_mcp_toolsets: false Child sees only its own profile toolset; parent MCP context does not leak
Invalid profile — no crash, no fallback widening Profile name not found in config Logged, child build aborted cleanly; no fallback to a broader toolset
Multi-tool delegation Multiple children with distinct profiles delegated in sequence Each child received its isolated toolset; no cross-contamination

Reproduce:

git clone https://github.com/NousResearch/hermes-agent
pytest tests/tools/test_delegate_toolset_scope.py \
       tests/tools/test_mcp_lazy_discovery.py -v
# Expect: 15 tests pass (3 Phase 1 + 12 Phase 2)

E2E adversarial suite (0/10 regressions):

# From scripts/out/_summary.json (real LLM calls — OpenRouter/deepseek):
# scenarios A–I: underlying_tools_called == expected, error: false, on all 9 scenarios
# See davidgut1982/hermes-agent#1 for the eval harness that produced this output.

PROOF — lazy discovery behavior

Reproduce lazy discovery:

# 1. Configure platform_toolsets to include no_mcp
# 2. Start hermes — MCP connections are NOT established at startup (observe logs)
# 3. Delegate to a profile that uses MCP toolsets
# 4. On first delegation: MCP discovery fires once (thread-safe one-shot)
# 5. Child receives its MCP tools; subsequent delegations reuse the cached discovery
grep "discover_mcp\|lazy.*discovery\|mcp_discovered" /path/to/logs/agent.log

Results

Test Count Result
Phase 1: profile bypass, non-MCP security boundary, no-profile pass-through 3 pass
Phase 2: skip-on-no_mcp, one-shot trigger, thread safety, failure tolerance 12 pass
Full test suite 5578 passed, 67 skipped, 3 pre-existing failures unrelated to these changes
E2E adversarial 9 scenarios 0 regressions

Caveats

Non-MCP toolsets are still parent-gated. Only MCP toolsets bypass the intersection when a profile is specified. The security boundary for ad-hoc delegation is preserved.

Lazy discovery failure is tolerant. Partial MCP results are used and logged if discovery fails mid-session; the child build is not crashed. This trades strict completeness for resilience; review if your deployment requires all-or-nothing MCP initialization.


How to test

# 1. Unit and integration tests
pytest tests/tools/test_delegate_toolset_scope.py tests/tools/test_mcp_lazy_discovery.py -v

# 2. Full test suite
pytest tests/tools/ -q
# Expect: 5578 passed, 67 skipped, 3 pre-existing failures unrelated to these changes

# 3. End-to-end
# Configure api_server platform with no_mcp in platform_toolsets
# Define an agent_profiles entry with MCP toolsets
# Delegate via profile_name — verify child receives MCP tools despite orchestrator having none loaded

Files changed

  • tools/delegate_tool.pyprofile_name param, bypass logic for MCP toolsets, ensure_mcp_discovered() trigger on first child build
  • tools/mcp_tool.pymark_eager_discovery_skipped() + ensure_mcp_discovered() (thread-safe one-shot, failure-tolerant)
  • gateway/run.py_active_platform_uses_no_mcp() helper; gate eager discovery on no_mcp check
  • hermes_cli/main.py — same gate for CLI startup path
  • agent/AGENTS.md — documents MCP bypass behaviour and lazy discovery semantics
  • tests/tools/test_delegate_toolset_scope.py — 3 new tests: profile bypass, non-MCP security boundary, no-profile pass-through
  • tests/tools/test_mcp_lazy_discovery.py — 12 new tests: skip-on-no_mcp, one-shot trigger, thread safety, failure tolerance

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes
  • I've tested on my platform: Ubuntu 24.04

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Related PRs

  • #35526 — extends and security-hardens this work (batch injection block, aliasing fix, inherit guard)
  • #35457 — embedding reranker + progressive tool disclosure
  • davidgut1982/hermes-agent#1 — eval harness that ran the adversarial E2E suite

When delegate_task is called with a named agent_profile, MCP toolsets
declared in the profile now resolve from global mcp_servers config
rather than being filtered against the parent agent's loaded tools.

Previously, if the orchestrator restricted its own MCP context (via
no_mcp or simply not loading domain servers), child agents spawned with
profile toolsets like ["mcp-fastmail"] received empty tool lists
silently. The intersection logic in _build_child_agent() treated
parent-loaded tools as the upper bound for all children.

This fix adds a profile_name parameter to _build_child_agent(). When
set, _is_mcp_toolset_name() gates MCP toolsets through unconditionally;
non-MCP toolsets still require parent membership (security boundary
preserved). delegate_task() passes the resolved per-task profile name
through to _build_child_agent() at every call site.

Fixes the child-tool-loss failure mode described in issue NousResearch#32668.
Three regression tests added to test_delegate_toolset_scope.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working tool/delegate Subagent delegation comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists labels May 26, 2026
davidgut1982 and others added 3 commits May 26, 2026 19:17
…string

Add AGENTS.md note to the delegation section explaining that named
agent_profile toolsets bypass the parent intersection for MCP servers
(fix introduced in NousResearch#32668). Expand the _build_child_agent() docstring
to describe the profile_name parameter's semantics and the rationale
for the security-boundary split.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the active platform includes the no_mcp sentinel in its toolsets,
skip eager MCP server discovery at gateway/CLI startup. Discovery is
deferred until the first delegate_task() call that targets an MCP toolset,
using a thread-safe one-shot Event/Lock pattern.

This eliminates unnecessary MCP connection overhead for api_server platform
(orchestrator) while preserving full MCP access for child agents via the
Phase 1 profile_name bypass. cli/cron/telegram platforms are unaffected:
their toolsets lack no_mcp, so the gate evaluates False and eager discovery
runs exactly as before.

Changes:
- tools/mcp_tool.py: add mark_eager_discovery_skipped() + ensure_mcp_discovered()
  (one-shot, thread-safe, failure-tolerant lazy discovery trigger)
- gateway/run.py: add _active_platform_uses_no_mcp() helper; gate the eager
  discover_mcp_tools() call in start_gateway() on the platform no_mcp check
- hermes_cli/main.py: gate the inline CLI-startup discover_mcp_tools() call in
  _prepare_agent_startup() with the same no_mcp check (covers `gateway run`)
- tools/delegate_tool.py: call ensure_mcp_discovered() before building any
  child agent that requests MCP toolsets
- tests/tools/test_mcp_lazy_discovery.py: 12 tests covering the skip flag,
  no-op/once/idempotent/thread-safe/failure paths, and platform resolution

Part of fix/profile-mcp-toolset-bypass branch (stacked on Phase 1).
Resolves: NousResearch#32668

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pre-declare _cfg as Dict[str, Any] = {} so it is always bound and
Pyright knows the type is dict throughout the config bridge block.

Use an intermediate _expanded variable after _expand_env_vars() and
guard with isinstance(_, dict) so the re-assignment stays within the
declared dict type — _expand_env_vars has no return annotation and
Pyright infers a broad str | list | dict union.

Simplify the IPv4 network_cfg line: now that _cfg is always bound and
typed as dict, the old ('_cfg' in dir() else {}) guard is unnecessary.

Fixes Pyright errors at lines 863, 901, 917, 926, 930, 936, 978 that
were introduced by the Phase 2 lazy MCP discovery work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@davidgut1982 davidgut1982 changed the title fix(delegate): profile MCP toolsets bypass parent toolset intersection fix(delegate): lazy MCP discovery + profile toolset bypass for no_mcp platforms May 26, 2026
davidgut1982 added a commit to davidgut1982/hermes-agent that referenced this pull request May 30, 2026
… toolsets bypass no_mcp parent intersection (NousResearch#32668/NousResearch#32727)

Under a no_mcp orchestrator (platform_toolsets: [no_mcp, delegation,
knowledge]), expanded_parent has zero MCP entries. The previous call site
hardcoded profile_name=None into _build_child_agent, so the MCP-bypass
branch (lines 972-976 of delegate_tool.py) never activated and fat
sub-agents received an empty toolset.

Fix:
- Add _load_agent_profiles() helper to read agent_profiles from the
  top-level config (not the delegation sub-key that _load_config() returns).
- Add profile: Optional[str] param to delegate_task(); when set, resolve
  the named profile's toolsets from agent_profiles and store as the
  effective toolsets for the child.
- Change the _build_child_agent call site from profile_name=None to
  profile_name=resolved_profile_name so the MCP-bypass branch activates
  for named profiles.
- Add "profile" to DELEGATE_TASK_SCHEMA so the model reliably emits the
  field; without a formal schema entry the LLM strips it at the provider
  API boundary.
- Update registry lambda and _dispatch_delegate_task in run_agent.py to
  forward profile= through both invocation paths.

Security: the bypass is scoped to MCP toolsets only, and only when a
named profile explicitly declares them in config. Non-MCP toolsets still
go through the parent intersection. Unknown profile names fall back
gracefully (warning + no bypass).

Tests added (test_delegate_toolset_scope.py):
- TestDelegateTaskProfileWiring: verifies delegate_task() calls
  _build_child_agent with profile_name='documents' and the profile's
  resolved toolsets. Key regression guard: FAILS against pre-fix code
  (profile_name=None hardcoded) and PASSES after the fix.
- TestDelegateTaskSchemaProfile: asserts 'profile' is a declared string
  property in DELEGATE_TASK_SCHEMA and is not in required[].
- TestProfileMcpBypassEndToEnd: direct _build_child_agent tests covering
  the post-fix bypass (profile_name set → mcp-nextcloud-files retained)
  and the security baseline (profile_name=None → MCP stripped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
davidgut1982 added a commit to davidgut1982/hermes-agent that referenced this pull request May 31, 2026
… toolsets bypass no_mcp parent intersection (NousResearch#32668/NousResearch#32727)

Under a no_mcp orchestrator (platform_toolsets: [no_mcp, delegation,
knowledge]), expanded_parent has zero MCP entries. The previous call site
hardcoded profile_name=None into _build_child_agent, so the MCP-bypass
branch (lines 972-976 of delegate_tool.py) never activated and fat
sub-agents received an empty toolset.

Fix:
- Add _load_agent_profiles() helper to read agent_profiles from the
  top-level config (not the delegation sub-key that _load_config() returns).
- Add profile: Optional[str] param to delegate_task(); when set, resolve
  the named profile's toolsets from agent_profiles and store as the
  effective toolsets for the child.
- Change the _build_child_agent call site from profile_name=None to
  profile_name=resolved_profile_name so the MCP-bypass branch activates
  for named profiles.
- Add "profile" to DELEGATE_TASK_SCHEMA so the model reliably emits the
  field; without a formal schema entry the LLM strips it at the provider
  API boundary.
- Update registry lambda and _dispatch_delegate_task in run_agent.py to
  forward profile= through both invocation paths.

Security: the bypass is scoped to MCP toolsets only, and only when a
named profile explicitly declares them in config. Non-MCP toolsets still
go through the parent intersection. Unknown profile names fall back
gracefully (warning + no bypass).

Tests added (test_delegate_toolset_scope.py):
- TestDelegateTaskProfileWiring: verifies delegate_task() calls
  _build_child_agent with profile_name='documents' and the profile's
  resolved toolsets. Key regression guard: FAILS against pre-fix code
  (profile_name=None hardcoded) and PASSES after the fix.
- TestDelegateTaskSchemaProfile: asserts 'profile' is a declared string
  property in DELEGATE_TASK_SCHEMA and is not in required[].
- TestProfileMcpBypassEndToEnd: direct _build_child_agent tests covering
  the post-fix bypass (profile_name set → mcp-nextcloud-files retained)
  and the security baseline (profile_name=None → MCP stripped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
davidgut1982 added a commit to davidgut1982/hermes-agent that referenced this pull request May 31, 2026
…ed import

Two CI fixes for PR #1 (feat/offline-eval-suite):

1. check-attribution: add david.gutowsky@gmail.com → davidgut1982 to
   AUTHOR_MAP in scripts/release.py so the contributor check passes.

2. test (2) / test (5): guard the `from tools.mcp_tool import
   ensure_mcp_discovered` call in _build_child_agent with a try/except
   ImportError. The symbol lives in the not-yet-merged NousResearch#32727 branch;
   without the guard every test that calls _build_child_agent with an
   MCP toolset raises ImportError. Degrades gracefully (no-op) when
   the symbol is absent; is a no-op anyway when eager discovery already
   ran at startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@davidgut1982

Copy link
Copy Markdown
Contributor Author

Closing in favor of #35526, which now contains this PR's lazy MCP discovery (folded in and verified — 9 lazy-discovery + 24 toolset-scope tests passing) on top of the profile-toolset isolation and security hardening. #35526 is the single complete implementation that closes #32668. Thanks @alt-glitch for flagging the overlap.

davidgut1982 added a commit to davidgut1982/hermes-agent that referenced this pull request Jun 4, 2026
… toolsets bypass no_mcp parent intersection (NousResearch#32668/NousResearch#32727)

Under a no_mcp orchestrator (platform_toolsets: [no_mcp, delegation,
knowledge]), expanded_parent has zero MCP entries. The previous call site
hardcoded profile_name=None into _build_child_agent, so the MCP-bypass
branch (lines 972-976 of delegate_tool.py) never activated and fat
sub-agents received an empty toolset.

Fix:
- Add _load_agent_profiles() helper to read agent_profiles from the
  top-level config (not the delegation sub-key that _load_config() returns).
- Add profile: Optional[str] param to delegate_task(); when set, resolve
  the named profile's toolsets from agent_profiles and store as the
  effective toolsets for the child.
- Change the _build_child_agent call site from profile_name=None to
  profile_name=resolved_profile_name so the MCP-bypass branch activates
  for named profiles.
- Add "profile" to DELEGATE_TASK_SCHEMA so the model reliably emits the
  field; without a formal schema entry the LLM strips it at the provider
  API boundary.
- Update registry lambda and _dispatch_delegate_task in run_agent.py to
  forward profile= through both invocation paths.

Security: the bypass is scoped to MCP toolsets only, and only when a
named profile explicitly declares them in config. Non-MCP toolsets still
go through the parent intersection. Unknown profile names fall back
gracefully (warning + no bypass).

Tests added (test_delegate_toolset_scope.py):
- TestDelegateTaskProfileWiring: verifies delegate_task() calls
  _build_child_agent with profile_name='documents' and the profile's
  resolved toolsets. Key regression guard: FAILS against pre-fix code
  (profile_name=None hardcoded) and PASSES after the fix.
- TestDelegateTaskSchemaProfile: asserts 'profile' is a declared string
  property in DELEGATE_TASK_SCHEMA and is not in required[].
- TestProfileMcpBypassEndToEnd: direct _build_child_agent tests covering
  the post-fix bypass (profile_name set → mcp-nextcloud-files retained)
  and the security baseline (profile_name=None → MCP stripped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
davidgut1982 added a commit to davidgut1982/hermes-agent that referenced this pull request Jun 5, 2026
… toolsets bypass no_mcp parent intersection (NousResearch#32668/NousResearch#32727)

Under a no_mcp orchestrator (platform_toolsets: [no_mcp, delegation,
knowledge]), expanded_parent has zero MCP entries. The previous call site
hardcoded profile_name=None into _build_child_agent, so the MCP-bypass
branch (lines 972-976 of delegate_tool.py) never activated and fat
sub-agents received an empty toolset.

Fix:
- Add _load_agent_profiles() helper to read agent_profiles from the
  top-level config (not the delegation sub-key that _load_config() returns).
- Add profile: Optional[str] param to delegate_task(); when set, resolve
  the named profile's toolsets from agent_profiles and store as the
  effective toolsets for the child.
- Change the _build_child_agent call site from profile_name=None to
  profile_name=resolved_profile_name so the MCP-bypass branch activates
  for named profiles.
- Add "profile" to DELEGATE_TASK_SCHEMA so the model reliably emits the
  field; without a formal schema entry the LLM strips it at the provider
  API boundary.
- Update registry lambda and _dispatch_delegate_task in run_agent.py to
  forward profile= through both invocation paths.

Security: the bypass is scoped to MCP toolsets only, and only when a
named profile explicitly declares them in config. Non-MCP toolsets still
go through the parent intersection. Unknown profile names fall back
gracefully (warning + no bypass).

Tests added (test_delegate_toolset_scope.py):
- TestDelegateTaskProfileWiring: verifies delegate_task() calls
  _build_child_agent with profile_name='documents' and the profile's
  resolved toolsets. Key regression guard: FAILS against pre-fix code
  (profile_name=None hardcoded) and PASSES after the fix.
- TestDelegateTaskSchemaProfile: asserts 'profile' is a declared string
  property in DELEGATE_TASK_SCHEMA and is not in required[].
- TestProfileMcpBypassEndToEnd: direct _build_child_agent tests covering
  the post-fix bypass (profile_name set → mcp-nextcloud-files retained)
  and the security baseline (profile_name=None → MCP stripped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
davidgut1982 added a commit to davidgut1982/hermes-agent that referenced this pull request Jun 5, 2026
… toolsets bypass no_mcp parent intersection (NousResearch#32668/NousResearch#32727)

Under a no_mcp orchestrator (platform_toolsets: [no_mcp, delegation,
knowledge]), expanded_parent has zero MCP entries. The previous call site
hardcoded profile_name=None into _build_child_agent, so the MCP-bypass
branch (lines 972-976 of delegate_tool.py) never activated and fat
sub-agents received an empty toolset.

Fix:
- Add _load_agent_profiles() helper to read agent_profiles from the
  top-level config (not the delegation sub-key that _load_config() returns).
- Add profile: Optional[str] param to delegate_task(); when set, resolve
  the named profile's toolsets from agent_profiles and store as the
  effective toolsets for the child.
- Change the _build_child_agent call site from profile_name=None to
  profile_name=resolved_profile_name so the MCP-bypass branch activates
  for named profiles.
- Add "profile" to DELEGATE_TASK_SCHEMA so the model reliably emits the
  field; without a formal schema entry the LLM strips it at the provider
  API boundary.
- Update registry lambda and _dispatch_delegate_task in run_agent.py to
  forward profile= through both invocation paths.

Security: the bypass is scoped to MCP toolsets only, and only when a
named profile explicitly declares them in config. Non-MCP toolsets still
go through the parent intersection. Unknown profile names fall back
gracefully (warning + no bypass).

Tests added (test_delegate_toolset_scope.py):
- TestDelegateTaskProfileWiring: verifies delegate_task() calls
  _build_child_agent with profile_name='documents' and the profile's
  resolved toolsets. Key regression guard: FAILS against pre-fix code
  (profile_name=None hardcoded) and PASSES after the fix.
- TestDelegateTaskSchemaProfile: asserts 'profile' is a declared string
  property in DELEGATE_TASK_SCHEMA and is not in required[].
- TestProfileMcpBypassEndToEnd: direct _build_child_agent tests covering
  the post-fix bypass (profile_name set → mcp-nextcloud-files retained)
  and the security baseline (profile_name=None → MCP stripped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
davidgut1982 added a commit to davidgut1982/hermes-agent that referenced this pull request Jun 6, 2026
… toolsets bypass no_mcp parent intersection (NousResearch#32668/NousResearch#32727)

Under a no_mcp orchestrator (platform_toolsets: [no_mcp, delegation,
knowledge]), expanded_parent has zero MCP entries. The previous call site
hardcoded profile_name=None into _build_child_agent, so the MCP-bypass
branch (lines 972-976 of delegate_tool.py) never activated and fat
sub-agents received an empty toolset.

Fix:
- Add _load_agent_profiles() helper to read agent_profiles from the
  top-level config (not the delegation sub-key that _load_config() returns).
- Add profile: Optional[str] param to delegate_task(); when set, resolve
  the named profile's toolsets from agent_profiles and store as the
  effective toolsets for the child.
- Change the _build_child_agent call site from profile_name=None to
  profile_name=resolved_profile_name so the MCP-bypass branch activates
  for named profiles.
- Add "profile" to DELEGATE_TASK_SCHEMA so the model reliably emits the
  field; without a formal schema entry the LLM strips it at the provider
  API boundary.
- Update registry lambda and _dispatch_delegate_task in run_agent.py to
  forward profile= through both invocation paths.

Security: the bypass is scoped to MCP toolsets only, and only when a
named profile explicitly declares them in config. Non-MCP toolsets still
go through the parent intersection. Unknown profile names fall back
gracefully (warning + no bypass).

Tests added (test_delegate_toolset_scope.py):
- TestDelegateTaskProfileWiring: verifies delegate_task() calls
  _build_child_agent with profile_name='documents' and the profile's
  resolved toolsets. Key regression guard: FAILS against pre-fix code
  (profile_name=None hardcoded) and PASSES after the fix.
- TestDelegateTaskSchemaProfile: asserts 'profile' is a declared string
  property in DELEGATE_TASK_SCHEMA and is not in required[].
- TestProfileMcpBypassEndToEnd: direct _build_child_agent tests covering
  the post-fix bypass (profile_name set → mcp-nextcloud-files retained)
  and the security baseline (profile_name=None → MCP stripped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
davidgut1982 added a commit to davidgut1982/hermes-agent that referenced this pull request Jun 6, 2026
… toolsets bypass no_mcp parent intersection (NousResearch#32668/NousResearch#32727)

Under a no_mcp orchestrator (platform_toolsets: [no_mcp, delegation,
knowledge]), expanded_parent has zero MCP entries. The previous call site
hardcoded profile_name=None into _build_child_agent, so the MCP-bypass
branch (lines 972-976 of delegate_tool.py) never activated and fat
sub-agents received an empty toolset.

Fix:
- Add _load_agent_profiles() helper to read agent_profiles from the
  top-level config (not the delegation sub-key that _load_config() returns).
- Add profile: Optional[str] param to delegate_task(); when set, resolve
  the named profile's toolsets from agent_profiles and store as the
  effective toolsets for the child.
- Change the _build_child_agent call site from profile_name=None to
  profile_name=resolved_profile_name so the MCP-bypass branch activates
  for named profiles.
- Add "profile" to DELEGATE_TASK_SCHEMA so the model reliably emits the
  field; without a formal schema entry the LLM strips it at the provider
  API boundary.
- Update registry lambda and _dispatch_delegate_task in run_agent.py to
  forward profile= through both invocation paths.

Security: the bypass is scoped to MCP toolsets only, and only when a
named profile explicitly declares them in config. Non-MCP toolsets still
go through the parent intersection. Unknown profile names fall back
gracefully (warning + no bypass).

Tests added (test_delegate_toolset_scope.py):
- TestDelegateTaskProfileWiring: verifies delegate_task() calls
  _build_child_agent with profile_name='documents' and the profile's
  resolved toolsets. Key regression guard: FAILS against pre-fix code
  (profile_name=None hardcoded) and PASSES after the fix.
- TestDelegateTaskSchemaProfile: asserts 'profile' is a declared string
  property in DELEGATE_TASK_SCHEMA and is not in required[].
- TestProfileMcpBypassEndToEnd: direct _build_child_agent tests covering
  the post-fix bypass (profile_name set → mcp-nextcloud-files retained)
  and the security baseline (profile_name=None → MCP stripped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
davidgut1982 added a commit to davidgut1982/hermes-agent that referenced this pull request Jun 6, 2026
… toolsets bypass no_mcp parent intersection (NousResearch#32668/NousResearch#32727)

Under a no_mcp orchestrator (platform_toolsets: [no_mcp, delegation,
knowledge]), expanded_parent has zero MCP entries. The previous call site
hardcoded profile_name=None into _build_child_agent, so the MCP-bypass
branch (lines 972-976 of delegate_tool.py) never activated and fat
sub-agents received an empty toolset.

Fix:
- Add _load_agent_profiles() helper to read agent_profiles from the
  top-level config (not the delegation sub-key that _load_config() returns).
- Add profile: Optional[str] param to delegate_task(); when set, resolve
  the named profile's toolsets from agent_profiles and store as the
  effective toolsets for the child.
- Change the _build_child_agent call site from profile_name=None to
  profile_name=resolved_profile_name so the MCP-bypass branch activates
  for named profiles.
- Add "profile" to DELEGATE_TASK_SCHEMA so the model reliably emits the
  field; without a formal schema entry the LLM strips it at the provider
  API boundary.
- Update registry lambda and _dispatch_delegate_task in run_agent.py to
  forward profile= through both invocation paths.

Security: the bypass is scoped to MCP toolsets only, and only when a
named profile explicitly declares them in config. Non-MCP toolsets still
go through the parent intersection. Unknown profile names fall back
gracefully (warning + no bypass).

Tests added (test_delegate_toolset_scope.py):
- TestDelegateTaskProfileWiring: verifies delegate_task() calls
  _build_child_agent with profile_name='documents' and the profile's
  resolved toolsets. Key regression guard: FAILS against pre-fix code
  (profile_name=None hardcoded) and PASSES after the fix.
- TestDelegateTaskSchemaProfile: asserts 'profile' is a declared string
  property in DELEGATE_TASK_SCHEMA and is not in required[].
- TestProfileMcpBypassEndToEnd: direct _build_child_agent tests covering
  the post-fix bypass (profile_name set → mcp-nextcloud-files retained)
  and the security baseline (profile_name=None → MCP stripped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
davidgut1982 added a commit to davidgut1982/hermes-agent that referenced this pull request Jun 6, 2026
… toolsets bypass no_mcp parent intersection (NousResearch#32668/NousResearch#32727)

Under a no_mcp orchestrator (platform_toolsets: [no_mcp, delegation,
knowledge]), expanded_parent has zero MCP entries. The previous call site
hardcoded profile_name=None into _build_child_agent, so the MCP-bypass
branch (lines 972-976 of delegate_tool.py) never activated and fat
sub-agents received an empty toolset.

Fix:
- Add _load_agent_profiles() helper to read agent_profiles from the
  top-level config (not the delegation sub-key that _load_config() returns).
- Add profile: Optional[str] param to delegate_task(); when set, resolve
  the named profile's toolsets from agent_profiles and store as the
  effective toolsets for the child.
- Change the _build_child_agent call site from profile_name=None to
  profile_name=resolved_profile_name so the MCP-bypass branch activates
  for named profiles.
- Add "profile" to DELEGATE_TASK_SCHEMA so the model reliably emits the
  field; without a formal schema entry the LLM strips it at the provider
  API boundary.
- Update registry lambda and _dispatch_delegate_task in run_agent.py to
  forward profile= through both invocation paths.

Security: the bypass is scoped to MCP toolsets only, and only when a
named profile explicitly declares them in config. Non-MCP toolsets still
go through the parent intersection. Unknown profile names fall back
gracefully (warning + no bypass).

Tests added (test_delegate_toolset_scope.py):
- TestDelegateTaskProfileWiring: verifies delegate_task() calls
  _build_child_agent with profile_name='documents' and the profile's
  resolved toolsets. Key regression guard: FAILS against pre-fix code
  (profile_name=None hardcoded) and PASSES after the fix.
- TestDelegateTaskSchemaProfile: asserts 'profile' is a declared string
  property in DELEGATE_TASK_SCHEMA and is not in required[].
- TestProfileMcpBypassEndToEnd: direct _build_child_agent tests covering
  the post-fix bypass (profile_name set → mcp-nextcloud-files retained)
  and the security baseline (profile_name=None → MCP stripped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists tool/delegate Subagent delegation type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: per-agent toolset restriction for orchestrator pattern (parent-scoped no_mcp)

2 participants