Skip to content

fix(delegation): allow explicit code_execution + honor explicit toolsets - #34294

Closed
Kyzcreig wants to merge 2 commits into
NousResearch:mainfrom
ANG-Ventures:fix/delegation-explicit-toolsets
Closed

fix(delegation): allow explicit code_execution + honor explicit toolsets#34294
Kyzcreig wants to merge 2 commits into
NousResearch:mainfrom
ANG-Ventures:fix/delegation-explicit-toolsets

Conversation

@Kyzcreig

Copy link
Copy Markdown
Contributor

Delegated tasks stripped code_execution and did not honor explicitly-requested toolsets. _strip_blocked_tools now keeps code_execution when explicitly requested and explicit toolsets are honored. Includes test_delegate.py coverage.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tools Tool registry, model_tools, toolsets tool/delegate Subagent delegation labels May 29, 2026
@liuhao1024

Copy link
Copy Markdown
Contributor

I found one issue that looks worth fixing before merge.

Title says "allow explicit code_execution" but the diff also unblocks delegation, clarify, and memory for explicit requests.

tools/delegate_tool.py:955 — The PR removes child_toolsets = _strip_blocked_tools(child_toolsets) from the explicit-request branch (if toolsets:). This changes the behavior for ALL four blocked toolsets, not just code_execution:

Toolset Current (explicit request) After PR
code_execution stripped allowed ✓ (intended)
delegation stripped allowed ⚠️
clarify stripped allowed ⚠️
memory stripped allowed ⚠️

The orchestrator re-add mechanism at line 967 (if effective_role == "orchestrator") is the intended way to grant delegation — it's role-based, not request-based. Removing the strip from the explicit-request branch means any caller can request delegation for a non-orchestrator subagent, bypassing the role gate. This could enable infinite recursion if a subagent with delegation spawns another subagent with delegation.

Why it matters: delegation was blocked specifically to prevent recursive subagent spawning. The orchestrator role controls when delegation is safe. Allowing it via explicit request undermines that safety mechanism.

Suggested fix: Keep _strip_blocked_tools in the explicit-request branch but modify it to only strip delegation, clarify, and memory (not code_execution):

# In _strip_blocked_tools, remove "code_execution" from blocked_toolset_names
# (as the PR already does), but KEEP the strip call at line 955:
blocked_toolset_names = {
    "delegation",
    "clarify",
    "memory",
    # "code_execution" removed — subagents already have terminal
}

This achieves the stated goal (allow explicit code_execution) without also unblocking delegation, clarify, and memory. The explicit-request branch would still strip the three safety-critical toolsets while allowing code_execution through.

@Kyzcreig
Kyzcreig force-pushed the fix/delegation-explicit-toolsets branch from fb3d1c1 to 3fe1add Compare July 12, 2026 09:59
Two related fixes to delegate_task toolset scoping:

1. Remove code_execution from the default subagent block list.
   Subagents already inherit `terminal` (a strictly larger capability),
   so blocking only `execute_code` was asymmetric and prevented
   legitimate use cases — e.g. "compute SHA256 + sum of primes" would
   silently fail because the subagent had no execute_code tool despite
   requesting toolsets=['code_execution'].

2. Stop calling _strip_blocked_tools() on explicit caller-requested
   toolsets. When a user/parent agent explicitly passes `toolsets=[...]`
   to delegate_task(), their intent wins over the implicit safety block
   list. Inherited/default toolsets still get the strip applied. This
   prevents silent toolset deletion which was the root cause of the
   debugging session that surfaced this bug.

Repro before fix: parent with toolsets=['hermes-cli'] delegating with
toolsets=['code_execution'] → intersection preserves code_execution →
_strip_blocked_tools deletes it → child gets [] toolsets → falls back
to default tool surface, missing execute_code.

After fix: child receives ['code_execution'] as requested.

Tests updated:
- TestStripBlockedTools.test_removes_blocked_toolsets: assert
  code_execution survives strip.
- TestStripBlockedTools.test_code_execution_no_longer_blocked: new
  regression test pinning the explicit allow.

Local patch on main; upstream PR deferred per Ace's call.
@Kyzcreig
Kyzcreig force-pushed the fix/delegation-explicit-toolsets branch from 3fe1add to 08de1dd Compare July 12, 2026 10:17

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the child-toolset behavior. The current implementation needs re-scoping against a later mainline design change.

Problems

  • The stated explicit-toolsets behavior is no longer present in the model-facing API: commit ba0bc01d1f740c562b55925e404b82a48809c364 deliberately removed it. Current run_agent.py:5718-5725 does not forward toolsets, and tests/tools/test_async_delegation.py:489-510 locks that in.
  • Removing the strip in tools/delegate_tool.py:1136 also admits delegation, clarify, and memory through the internal explicit branch. This conflicts with the role-gated delegation re-add at tools/delegate_tool.py:1139-1144 and the documented blocked-toolset contract in website/docs/user-guide/features/delegation.md:157-161.
  • Removing execute_code from the blocklist changes inherited children too, not only an explicit request, and needs a production-path regression test plus documentation if that is the intended policy.

Suggested changes

  • Rebase the proposal on parent-toolset inheritance, retaining the safety strip for delegation, clarify, and memory.
  • Treat code_execution inheritance as a focused policy change with an end-to-end child construction test and corresponding docs update.

This is an automated hermes-sweeper review.

Comment thread tools/delegate_tool.py Outdated
child_toolsets, parent_toolsets
)
child_toolsets = _strip_blocked_tools(child_toolsets)
# Explicit caller requests bypass the default block list (memory,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this strip admits delegation, clarify, and memory along with code_execution. delegation is currently re-added only for an effective orchestrator role below; please retain the safety strip for these three toolsets rather than letting an explicit internal request bypass that gate.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
Caught by @liuhao1024 on NousResearch#34294. The PR title promises "allow explicit
code_execution", but removing `_strip_blocked_tools()` from the
explicit-request branch also unblocked `delegation`, `clarify`, and
`memory` for any caller-supplied toolsets list.

`delegation` is the serious one: it is granted by ROLE via the
orchestrator re-add in `_build_child_agent`, deliberately not by request.
Letting a caller request it means a non-orchestrator subagent can be
handed delegation and spawn further subagents -- exactly the recursion
the gate exists to prevent.

Reproduced end-to-end against `_build_child_agent` with a mocked
`run_agent.AIAgent`, a depth-0 non-orchestrator parent, and an explicit
toolsets request:

  pre-PR base (dc3f61c):  terminal
  this PR as filed:         clarify, code_execution, delegation,
                            memory, terminal        <- gate bypassed
  after this commit:        code_execution, terminal

That is @liuhao1024's suggested shape: `code_execution` stays out of the
strip set (so an explicit request obtains it -- the PR's actual goal),
and the strip call is restored so the three safety-critical toolsets are
removed on every path, inherited or explicit.

Tests: an end-to-end child-construction assertion that explicit
`code_execution` is honored while `delegation`/`clarify`/`memory` are
not, plus a unit assertion on the strip set. The E2E test goes RED with
this commit's implementation reverted.

Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
@Kyzcreig

Copy link
Copy Markdown
Contributor Author

@liuhao1024 — you were right, and thank you for catching this. Apologies for the slow reply. This is a real safety bug, not a nit, and it's fixed in e226fbe; you're credited as Co-authored-by on the commit.

Your core claim:

Removing the strip from the explicit-request branch means any caller can request delegation for a non-orchestrator subagent, bypassing the role gate.

I reproduced it end-to-end rather than just reading the diff — _build_child_agent with a mocked run_agent.AIAgent, a depth-0 non-orchestrator parent, and an explicit toolsets=["terminal","delegation","clarify","memory","code_execution"]:

tree child's resolved toolsets
pre-PR base (dc3f61cb0) terminal
this PR as filed clarify, code_execution, delegation, memory, terminal
after e226fbe code_execution, terminal

So your table was accurate on all four rows. A non-orchestrator child was being handed delegation — and since that child then becomes a parent, it could spawn further delegating children. Your framing is the correct one:

The orchestrator re-add mechanism at line 967 (if effective_role == "orchestrator") is the intended way to grant delegation — it's role-based, not request-based.

That's exactly the invariant I broke. I'd written a docstring justifying the removal as "silent deletion of explicitly requested toolsets is a footgun; user intent wins" — which is a defensible principle for ergonomic toolsets and simply the wrong principle for a recursion gate. Role-granted and request-granted are not interchangeable, and I collapsed them.

The fix is your suggested shape. code_execution stays out of blocked_toolset_names (which is what actually achieves the PR's stated goal), and the _strip_blocked_tools() call is restored on the explicit-request branch so delegation/clarify/memory are removed on every path — inherited, fallback, and explicit:

child_toolsets = _strip_blocked_tools(child_toolsets)

I also rewrote the _strip_blocked_tools docstring, since the old one stated the bypass as intentional design and would have re-taught the same mistake to the next reader.

Tests: an E2E child-construction assertion that explicit code_execution is honored while the three safety toolsets are not, plus a unit assertion on the strip set. The E2E test goes RED with the fix reverted — I checked, rather than assuming. tests/tools/test_delegate.py 159 passed, tests/tools/test_async_delegation.py 21 passed.

One thing I'd value your view on, since you clearly read this path closely: the sweeper review notes that run_agent.py:5718-5725 no longer forwards toolsets to the model-facing API (removed deliberately in ba0bc01), with tests/tools/test_async_delegation.py:489-510 locking that in. So the explicit branch is now reachable only from internal callers, not from a model-issued delegate_task. That narrows the exposure of the bug you found but not its correctness — an internal caller bypassing a recursion gate is still a bug. It does raise the question of whether the code_execution half of this PR is still worth landing on its own, or whether the whole thing should be rebased onto parent-toolset inheritance instead. If you have an opinion there I'd rather hear it before I rework it.

@Kyzcreig

Copy link
Copy Markdown
Contributor Author

Closing this — but I owe a correction to my own comment above, because the reason I'm closing is not the reason I'd have given yesterday.

@liuhao1024 — your finding is now satisfied on main, and by a commit that also delivers what this PR was actually for.

Upstream landed 295e20358feat(delegation): let subagents use execute_code (#69325, 2026-07-22), an ancestor of current main. It achieves this PR's stated goal the right way: it removes execute_code from DELEGATE_BLOCKED_TOOLS so code_execution survives the strip, and it leaves the _strip_blocked_tools() call on the explicit-request branch intact. That is exactly the shape you argued for — permission granted by changing the blocklist, not by removing the gate.

Verified by executing main's own code rather than reading the diff:

_strip_blocked_tools(['terminal','delegation','clarify','memory','code_execution'])
  ->  ['code_execution', 'terminal']

  code_execution honored : True     ← what this PR wanted
  delegation blocked     : True     ← the invariant you defended
  clarify / memory blocked: True

tools/delegate_tool.py:1153 on main still ends the explicit branch with child_toolsets = _strip_blocked_tools(child_toolsets), and DELEGATE_BLOCKED_TOOLS is now {clarify, cronjob, delegate_task, memory, send_message}execute_code gone, the recursion gate untouched.

So the outcome is: your objection was upheld, and the feature shipped without the bypass. My e226fbe fix on this branch reached the same end state independently, which means this branch now has nothing left to add to main — every line of value in it is already there via #69325, and re-landing it would only re-introduce merge conflicts (~3,400 commits of drift).

I also want to correct something I nearly got wrong. I was initially told this PR was moot because the explicit-toolsets API had been deleted by ba0bc01d1. That's not accurate — ba0bc01d1 removed the model-facing toolsets argument, but the if toolsets: branch is alive and well at tools/delegate_tool.py:1143 and still reachable from internal callers. Had I closed on that reasoning I'd have closed a PR whose safety bug was still live in the codebase. It isn't live — but it's live-ness was worth checking properly, and that check only exists because you made the argument you made.

Thank you for the review. It was specific, it was correct on all four rows of your table, and it caught a real recursion-gate bypass rather than a style nit — the Co-authored-by credit on e226fbe stands, and I'd welcome your eyes on delegation changes any time. The open question I asked you (whether the code_execution half was worth landing standalone) is answered by #69325 shipping it, so please don't feel you owe me a reply on that thread.

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 sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/delegate Subagent delegation type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants