OpenHands / OpenHands/software-agent-sdk
[Agent Profile] Sub-agent delegation bypasses a profile's tool and MCP restrictions
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 542
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 137
Description
An agent profile can restrict which tools and MCP servers its agent gets (tools, mcp_server_refs). Sub-agent delegation ignores both: a spawned sub-agent's capabilities come entirely from its own definition, so a profile built to be read-only can delegate its way to a shell and a file editor.
This surfaced while implementing per-profile tool and MCP scoping for OSS-10492 (OpenHands/OpenHands#17234), whose goal is "I can create a conversation with my Code Exploration Agent so that I can ask questions about the codebase without worrying about any possible edits." That guarantee does not currently hold when sub-agents are enabled.
The escape
subagent/registry.py builds a sub-agent's tools from its definition alone. The only check is that the tool is registered on this server — never that the parent has it:
# openhands-sdk/openhands/sdk/subagent/registry.py:242-251
# Resolve tools
tools: list[Tool] = []
registered_tools: set[str] = set(list_registered_tools())
for tool_name in agent_def.tools:
if tool_name not in registered_tools:
raise ValueError(...)
tools.append(Tool(name=tool_name))
MCP is the same — its own, not the parent's:
return Agent(llm=llm, tools=tools, agent_context=agent_context,
mcp_config=agent_def.mcp_config or {}, condenser=condenser)
So with a profile of tools: ["glob", "grep"] and enable_sub_agents: true:
| Sub-agent (shipped) | Declares | Parent restriction |
|---|---|---|
default |
terminal, file_editor, task_tracker |
bypassed — can edit files |
bash_runner |
terminal |
bypassed — arbitrary shell |
code_explorer |
terminal |
bypassed |
web_researcher |
browser_tool_set + its own fetch / tavily MCP servers |
bypasses mcp_server_refs too |
Worth noting code_explorer is described as read-only ("You never create, modify, or delete files") but holds terminal — its restriction is prompt text, not capability. That is fine for a helper a trusted agent calls; it is not fine as the boundary a scoped profile relies on.
Why the asymmetry looks unintentional
AgentDefinition already has an inherit concept — just not for capabilities:
model: str = "inherit"— "'inherit'uses parent model" (subagent/schema.py:208)permission_mode— "None inherits the parent policy" (schema.py:127,:238)
Model and permission inherit; tools and MCP do not. Nothing in the code comments suggests that was a deliberate security decision, which is why this is filed as a question rather than a patch.
Not affected: secrets
TaskConversationManager builds the sub-conversation without passing secrets=:
# openhands-tools/openhands/tools/task/manager.py:216
conversation = LocalConversation(
agent=worker_agent,
workspace=self.parent_conversation.state.workspace.working_dir,
...
)
so it starts with an empty SecretRegistry and inherits none of the parent's secrets. A profile's secret_refs (software-agent-sdk#4931) is therefore not escaped this way. Scope here is tools and MCP only.
Options
- Clamp — intersect each sub-agent's tools/MCP with the parent's. Strongest guarantee; matches Factory AI, where sub-agents inherit the parent's autonomy level and are clamped to an org maximum. Cost: a clamped
bash_runnerwith noterminalis useless, and the failure would be confusing unless it is reported well. - Refuse — error at launch when a profile with an explicit
toolslist also enables sub-agents. Honest and loud, but blunt: it forbids the legitimate "restricted parent, trusted helper" pattern. - Inherit-by-default with opt-out — extend the existing
inheritconvention to tools: a definition without an explicittoolskey inherits the parent's; one with an explicit list keeps today's behavior. Backwards-compatible in spirit, but every shipped definition setstoolsexplicitly, so it would not close the hole for the presets that actually leak. - Warn only — surface it in the profile editor. Cheapest, guarantees nothing.
Leaning toward (1), with the clamp applied at factory time and the dropped names reported in the delegation error so a neutered sub-agent fails legibly rather than mysteriously. But this is a design call with a real cost, which is why it wants a decision before code.
Acceptance Criteria
- A decision is recorded on which model to adopt (clamp / refuse / inherit / warn).
- With a profile whose
toolsis an explicit list, a delegated sub-agent cannot obtain a tool outside that list. - The same holds for
mcp_server_refs, including a sub-agent that declares its ownmcp_servers(e.g.web_researcher). - A profile with
tools: null(the default) keeps today's delegation behavior byte-identical. - Whatever is dropped or refused is reported to the caller — no silent narrowing.
- Tests cover a restricted parent delegating to each shipped sub-agent.
Relevant code
openhands-sdk/openhands/sdk/subagent/registry.py— tool/MCP resolution (:242-251,:277-282)openhands-sdk/openhands/sdk/subagent/schema.py—model/permission_modeinherit precedentopenhands-tools/openhands/tools/preset/subagents/*.md— the shipped definitionsopenhands-tools/openhands/tools/task/manager.py— sub-conversation construction- Consumer:
OpenHands/OpenHands#17234/ PR #17235 (profile tool + MCP scoping)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with tool and MCP resolution in openhands-sdk/openhands/sdk/subagent/registry.py, then read the inherit precedents in subagent/schema.py and the shipped definitions under openhands-tools/openhands/tools/preset/subagents/. Review the delegation construction in openhands-tools/openhands/tools/task/manager.py and the linked profile-scoping work before choosing a model. Done means the decision is recorded, restricted parents cannot gain tools or MCP servers, unrestricted profiles remain unchanged, refusals or drops are reported, and tests cover each shipped sub-agent.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- authorization, backend, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100