[eric] agents: SDK deny list merges effective_disallowed + hard blocks (plain reassignment silently discarded the computed denies; runtime gate was the only wall)

This commit is contained in:
ciregenz
2026-07-16 19:30:40 -07:00
parent 24d197eefd
commit ce3e67d613
3 changed files with 27 additions and 12 deletions
@@ -29,7 +29,7 @@ from backend.apps.agents.manager.prompt.tool_catalog import get_all_tool_names
from backend.apps.agents.manager.prompt.prompt_context import resolve_mode
from backend.apps.agents.manager.run.run_options_helpers import (
pre_send_context_guard, set_framework_overhead, register_web_mcp_server,
append_web_tools_hint, inject_thinking_options,
append_web_tools_hint, inject_thinking_options, merge_hard_blocked_tools,
)
logger = logging.getLogger(__name__)
@@ -227,12 +227,8 @@ class RunOptions(AgentManagerProtocol):
options_kwargs["extra_args"] = p_ea
# The claude_code preset auto-attaches the user's claude.ai- connected partner MCPs (`mcp__claude_ai_*`). Those bypass our MCPActivate gate, don't share OAuth state with the OpenSwarm Gmail/Calendar/Drive connectors the user actually configured here, and confuse the model into picking the partner shim instead of our vetted server. Hard-block them at the SDK layer so the model can't even attempt the call.
options_kwargs["disallowed_tools"] = [
"mcp__claude_ai_*",
# The CLI's built-in sub-agent tool is replaced by our SpawnAgent MCP (prompt + run_in_background only); it is named Task on CLI 2.1.122 (Agent on older builds, kept for safety), and its subagent types resolve to models router setups can't serve.
"Agent",
"Task",
]
# merge EXTENDS effective_disallowed: the old plain assignment silently discarded the computed denies (Cron*/Skill/web-swap/per-tool MCP) and left the runtime gate as the only wall.
options_kwargs["disallowed_tools"] = merge_hard_blocked_tools(effective_disallowed)
if session.cwd:
# Pre-existing sessions may have workspaces that predate the git-init block in launch_agent, leaving them without a valid HEAD. Ensure it here so subagent worktree-add always works.
@@ -12,6 +12,15 @@ from backend.apps.agents.manager.session.history_compaction import estimate_post
logger = logging.getLogger(__name__)
# Always SDK-blocked regardless of permissions: claude.ai partner MCPs bypass our MCPActivate gate, and the CLI's built-in sub-agent tool (Task on 2.1.122, Agent on older builds) is replaced by our SpawnAgent MCP.
HARD_BLOCKED_TOOLS: List[str] = ["mcp__claude_ai_*", "Agent", "Task"]
@typechecked
def merge_hard_blocked_tools(effective_disallowed: List[str]) -> List[str]:
"""The SDK deny list = the computed per-turn denies PLUS the unconditional hard blocks. A plain assignment here once silently discarded effective_disallowed (Cron*/Skill/web-swap/per-tool MCP denies), leaving the runtime gate as the only wall; merge, never overwrite."""
return effective_disallowed + [t for t in HARD_BLOCKED_TOOLS if t not in effective_disallowed]
# `manager` is the AgentManager; it isn't annotated because typing it would import agent_manager back into a module agent_manager already imports (a cycle). Same reason self is never annotated.
@typechecked
+15 -5
View File
@@ -73,13 +73,23 @@ def test_spawn_agent_unknown_parent_raises() -> None:
def test_builtin_subagent_tools_stay_blocked() -> None:
# The CLI's built-in sub-agent tool (Task on 2.1.122, Agent on older builds) must not be offered: out of the catalog AND hard-blocked at the SDK layer.
from backend.apps.agents.manager.prompt.tool_catalog import FULL_TOOLS
from backend.apps.agents.manager.run.run_options_helpers import merge_hard_blocked_tools
assert "Agent" not in FULL_TOOLS
assert "Task" not in FULL_TOOLS
import inspect
from backend.apps.agents.manager.run import RunOptions
src = inspect.getsource(RunOptions)
block = src.split('disallowed_tools"] = [')[1][:400]
assert '"Agent",' in block and '"Task",' in block
blocked = merge_hard_blocked_tools([])
assert "Agent" in blocked and "Task" in blocked and "mcp__claude_ai_*" in blocked
def test_effective_disallowed_reaches_the_sdk_deny_list() -> None:
# Regression: a plain assignment once DISCARDED the computed denies (Cron*/Skill/web-swap/per-tool MCP), leaving the runtime gate as the only wall. The merge must keep them AND append the hard blocks, deduped.
from backend.apps.agents.manager.run.run_options_helpers import merge_hard_blocked_tools
computed = ["CronCreate", "CronList", "CronDelete", "Skill", "mcp__gmail__DeleteEmail", "Task"]
merged = merge_hard_blocked_tools(computed)
for t in computed:
assert t in merged
assert "Agent" in merged and "mcp__claude_ai_*" in merged
assert merged.count("Task") == 1
assert merged[:len(computed)] == computed
def test_spawn_server_schema_is_prompt_plus_background_only() -> None: