Skip to content

define_subagent permission flags (enable_mcp_tools, enable_write_tools) are not enforced #65

@jiridanek

Description

@jiridanek

define_subagent exposes permission flags (enable_mcp_tools, enable_write_tools, enable_subagent_tools) but they are not enforced at runtime. Subagents always inherit the parent's full MCP toolset regardless of flags, and write tools are always denied even when the flag is true.

Steps to reproduce

  1. Configure an agent with a custom MCP stdio server exposing a get_secret_word tool (returns "vodka")
  2. Define two subagent types via define_subagent:
    • mcp_yes: enable_mcp_tools=true
    • mcp_no: enable_mcp_tools=false
  3. Invoke each with prompt: "Call get_secret_word and report the result"
  4. Both subagents successfully call the MCP tool and return "vodka"

Results

Tested with gemini-3.1-pro-preview-customtools, reproduced 3 times consistently.

Subagent enable_mcp_tools enable_write_tools MCP call Write call
parent n/a n/a "vodka" n/a
mcp_yes true false "vodka" n/a
mcp_no false false "vodka" n/a
writer false true n/a ❌ denied
  • enable_mcp_tools=false is not enforcedmcp_no called mcp_secret_get_secret_word identically to mcp_yes
  • enable_write_tools=true is not enforcedwriter subagent's edit_file was denied. The parent's policies=[policy.allow_all()] does not propagate to subagents.

Impact

Agents cannot create least-privilege subagents. Any subagent gets the parent's full MCP toolset, bypassing the intended isolation. A subagent meant for read-only research gets access to IDE write tools, external APIs, etc. This is a security boundary violation.

Investigation: how the flags were discovered

The flags are present in the Go harness binary (found via strings localharness):

EnableMCPTools  json:"enable_mcp_tools,omitempty"
  jsonschema_description:"Set true to enable the subagent to call MCP tools."

enable_write_tools  json:"enable_write_tools,omitempty"
  jsonschema_description:"Set true to equip the subagent with tools to create and edit files, and run commands."

enable_subagent_tools  json:"enable_subagent_tools,omitempty"
  jsonschema_description:"Set true to equip the subagent with tools to define and invoke its own subagents"

The define_subagent tool accepts these flags without error — they're part of the JSON schema. The harness just doesn't enforce them.

The invoke_subagent tool takes a Subagents array, each with TypeName, Role, Prompt, and optional Workspace (inherit/branch/share). The Workspace flag controls filesystem isolation but has no bearing on tool access.

Built-in subagent types (research, self, Coder, CodeReviewer, PlanReviewer, InvestigationReviewer, Planner, owl) are also present in the binary. The SDK example (examples/getting_started/subagents.py) enables subagents but doesn't demonstrate or document the permission flags.

Reproduction code

MCP server (secret_mcp_server.py):

from fastmcp import FastMCP

mcp = FastMCP("secret")

@mcp.tool()
def get_secret_word() -> str:
    \"\"\"Returns the secret word. Call this to prove you have MCP access.\"\"\"
    return "vodka"

if __name__ == "__main__":
    mcp.run(transport="stdio")

Test script (key parts of subagent_isolation.py):

config = LocalAgentConfig(
    model="gemini-3.1-pro-preview-customtools",
    capabilities=types.CapabilitiesConfig(enable_subagents=True),
    policies=[policy.allow_all()],
    mcp_servers=[
        types.McpStdioServer(name="secret", command=python, args=[mcp_server]),
    ],
)

async with Agent(config) as agent:
    # Turn 0: verify MCP works from parent
    r = await agent.chat("Call get_secret_word MCP tool and tell me the result.")
    # Returns "vodka" ✓

    # Turn 1: define subagents with different flags
    r = await agent.chat(
        "Define 3 subagent types using define_subagent:\\n"
        "1. name='mcp_yes', enable_mcp_tools=true, enable_write_tools=false\\n"
        "2. name='mcp_no', enable_mcp_tools=false, enable_write_tools=false\\n"
        "3. name='writer', enable_mcp_tools=false, enable_write_tools=true"
    )

    # Turns 2-4: invoke each subagent separately
    # mcp_yes: gets "vodka" ✓ (expected)
    # mcp_no:  gets "vodka" ✓ (BUG — should be denied)
    # writer:  edit_file denied  (BUG — should be allowed)

Full scripts: spike/subagent_isolation.py, spike/secret_mcp_server.py

Environment

  • SDK: google-antigravity (latest from PyPI as of 2026-06-15)
  • Model: gemini-3.1-pro-preview-customtools
  • Platform: macOS 26, Python 3.14
  • fastmcp 3.4.2 (for test MCP server)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions