OpenHands / OpenHands/software-agent-sdk

[Agent Profile] Sub-agent delegation bypasses a profile's tool and MCP restrictions

Open
#4,953 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

architecture security-related
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

  1. 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_runner with no terminal is useless, and the failure would be confusing unless it is reported well.
  2. Refuse — error at launch when a profile with an explicit tools list also enables sub-agents. Honest and loud, but blunt: it forbids the legitimate "restricted parent, trusted helper" pattern.
  3. Inherit-by-default with opt-out — extend the existing inherit convention to tools: a definition without an explicit tools key inherits the parent's; one with an explicit list keeps today's behavior. Backwards-compatible in spirit, but every shipped definition sets tools explicitly, so it would not close the hole for the presets that actually leak.
  4. 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 tools is 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 own mcp_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.pymodel / permission_mode inherit precedent
  • openhands-tools/openhands/tools/preset/subagents/*.md — the shipped definitions
  • openhands-tools/openhands/tools/task/manager.py — sub-conversation construction
  • Consumer: OpenHands/OpenHands#17234 / PR #17235 (profile tool + MCP scoping)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.