Fix privilege boundary: subagents cannot exceed parent tools - #101
Merged
Conversation
Reported privately against 695c735 and confirmed from source. _build_subagent_tools equipped a child with any tool family named in a subtask, falling back to enabling it when the parent had no such entry. So the orchestrator's prompt decided the child's privileges, not the parent's configuration — and that prompt can be influenced by anyone talking to the agent. A child with execute_shell_command, running as the server user, was one injected instruction away on any agent with delegation enabled. Three things made it serious rather than untidy. code_executor is not in the forbidden set, which covers only delegation tools to stop fork bombs, and {"code_executor": true} alone is enough to build the shell tool. The widget guard from #72 keys off the agent name, and a subagent is named subagent_<uuid>, which has no widget key — so the refusal that exists precisely to keep anonymous visitors away from a shell did not reach the child. And a subagent is not a row in agents_config, so it carries no RBAC of its own; the parent's tool set is the only boundary there is. The same route granted create_agent, which pulls delete_agent and update_agent with it, and shop, which returns place_order. Requested tools are now intersected with the parent's effective set. The widget refusal is part of that set, applied by calling the factory's own check rather than restating it, so the two cannot drift. A parent that legitimately holds the executor still passes it on. Children inherit the parent's settings for a tool rather than a bare true, so a shop subagent gets the real catalog instead of an empty default. MCP is bound the same way: a parent with no servers grants none. Also stops the tool's own schema advertising code_executor to the orchestrator as an example — it was actively suggesting the escalation. 876 tests, OK. Ten of the eleven new tests fail against the previous implementation; the four that pass on both are the no-regression cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ToCUNy2SqwvfTq4a6xfSk1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a critical security vulnerability where subagents could be equipped with tools their parent agent does not hold, allowing prompt injection to grant unauthorized privileges like
code_executorrunning as the server user.Problem
_build_subagent_toolswas granting any tool named in a subtask request, defaulting to enabled when the parent had no configuration entry. This meant:code_executorwas not in the forbidden set, so{"code_executor": true}alone could buildexecute_shell_commandChanges
subagent_delegation_tool.py:_parent_effective_tools()to compute the parent's actual tool set, accounting for widget exposure (code_executor refusal)_build_subagent_tools()to enforce that subagents can only receive tools the parent already holdsmemory_blocksconfig) rather than flattening toTrueSubtaskSpec.toolsdocstring to clarify the boundarytest_subagent_privilege_boundary.py(new):DYNAMIC_SUBAGENTS.md:CHANGELOG.md:Implementation Details
The fix calls
ToolFactory._agent_has_widget_key()to check the parent's widget exposure rather than restating the logic, ensuring the two checks cannot drift. A subagent's effective tool set is the intersection of what it requests and what its parent actually holds (after widget checks).https://claude.ai/code/session_01ToCUNy2SqwvfTq4a6xfSk1