[eric] agents: every builtin tool server rides ONE process per chat, and the gate enumerates its grants instead of wildcarding

This commit is contained in:
ciregenz
2026-08-08 12:59:50 -07:00
parent c0d33c0cfb
commit 4faba74d55
17 changed files with 206 additions and 243 deletions
+47 -18
View File
@@ -1,17 +1,16 @@
#!/usr/bin/env python3
"""One stdio MCP process hosting the always-on, ungated meta tools that used to be three separate
python interpreters per agent CLI (MCPList/Search/Activate, SettingsRead/Write, CreateApp).
"""One stdio MCP process per agent session hosting ALL the builtin tool servers that used to be
up to ten separate python interpreters (ENG-208: 5 parked chats measured 56 python processes and
756MB before any real work).
Why merge only these three: their tool NAMES are globally unique, none of them is referenced by
the non-bypassable permission gate (grep for `mcp__openswarm-mcp-meta__` etc. finds nothing in
build_effective_tool_lists / path_gate), and all three are registered unconditionally with the
same env. So collapsing them into one process is pure fan-out reduction with zero behavior change:
5 parked CLIs drop 15 idle interpreters to 5 (ENG-208). The gate-coupled servers (schedule, web,
browser-agent) and the conditional ones (skill, show-ui, spawn, invoke) stay separate on purpose.
Which sub-servers load is decided by the SAME permission logic that used to decide which processes
to spawn, passed in as OSW_MCP_MODULES by register_builtin_mcp_servers, so a denied capability's
tools are absent from tools/list exactly like its dead process used to be. Tool NAMES are globally
unique across sub-servers (asserted below); full ids all live under the one server name
"openswarm-core", and every gate reference was renamed with them in the same commit.
We reuse each sub-server's own TOOLS + handle_tool_call; we own the stdio loop so their main() and
send_response never run. Sibling import (not backend.*) matches how these scripts are launched by
path in both dev and the packaged bundle."""
Each sub-server keeps reading its per-session context from env exactly as before, because this is
still one process per session with the union env. We own the stdio loop; their main() never runs."""
import json
import os
@@ -19,18 +18,48 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import apps_mcp_server as p_apps # noqa: E402
import mcp_meta_server as p_meta # noqa: E402
import settings_meta_server as p_settings # noqa: E402
# Import cost is paid only for modules this session actually gets; all are stdlib-only thin proxies.
P_MODULE_FILES = {
"meta": "mcp_meta_server",
"settings": "settings_meta_server",
"apps": "apps_mcp_server",
"spawn": "spawn_agent_mcp_server",
"invoke": "invoke_agent_mcp_server",
"skill": "skill_mcp_server",
"ui": "show_ui_mcp_server",
"schedule": "schedule_mcp_server",
"web": "web_mcp_server",
"browser": "browser_agent_mcp_server",
}
P_SUBSERVERS = [p_meta, p_settings, p_apps]
P_ENABLED = [m.strip() for m in os.environ.get("OSW_MCP_MODULES", "meta,settings,apps").split(",") if m.strip()]
TOOLS = []
P_ROUTE = {}
for p_mod in P_SUBSERVERS:
for p_key in P_ENABLED:
p_file = P_MODULE_FILES.get(p_key)
if p_file is None:
sys.stderr.write(f"[openswarm-core] unknown module key: {p_key}\n")
continue
p_mod = __import__(p_file)
for p_tool in p_mod.TOOLS:
p_name = p_tool["name"]
if p_name in P_ROUTE:
sys.stderr.write(f"[openswarm-core] duplicate tool {p_name}; keeping first\n")
continue
TOOLS.append(p_tool)
P_ROUTE[p_tool["name"]] = p_mod
P_ROUTE[p_name] = p_mod
def p_call(mod, tool_name: str, arguments: dict) -> dict:
handler = getattr(mod, "handle_tool_call", None)
if handler is not None:
return handler(tool_name, arguments)
# schedule_mcp_server dispatches through a HANDLERS dict instead of one entry function.
fn = mod.HANDLERS.get(tool_name)
if fn is None:
return {"content": [{"type": "text", "text": f"Unknown tool: {tool_name}"}], "isError": True}
return fn(arguments)
def send_response(id_, result=None, error=None):
@@ -73,7 +102,7 @@ def main():
send_response(id_, {"content": [{"type": "text", "text": f"Unknown tool: {tool_name}"}], "isError": True})
continue
try:
send_response(id_, mod.handle_tool_call(tool_name, arguments))
send_response(id_, p_call(mod, tool_name, arguments))
except Exception as e:
send_response(id_, error={"code": -32000, "message": str(e)})
elif method in ("resources/list",):
@@ -49,57 +49,58 @@ def build_effective_tool_lists(
if mcp_servers:
all_tools_list = load_all_tools()
for name in mcp_servers:
if name == "openswarm-browser-agent":
for bt in browser_delegation_tools:
policy = builtin_perms.get(bt, "always_allow")
if name == "openswarm-core":
# Every builtin tool rides ONE combined process now (ENG-208), so the old per-server
# wildcard would blanket gated tools; enumerate per module instead, same policies as
# when each module was its own process. Module list = what registration wired.
p_modules = [m for m in mcp_servers[name].get("env", {}).get("OSW_MCP_MODULES", "").split(",") if m]
from backend.apps.agents import apps_mcp_server, mcp_meta_server, schedule_mcp_server, settings_meta_server
for p_t in (x["name"] for x in mcp_meta_server.TOOLS + settings_meta_server.TOOLS + apps_mcp_server.TOOLS):
effective_allowed.append(f"mcp__openswarm-core__{p_t}")
if "schedule" in p_modules:
for p_t in (x["name"] for x in schedule_mcp_server.TOOLS):
effective_allowed.append(f"mcp__openswarm-core__{p_t}")
if "browser" in p_modules:
for bt in browser_delegation_tools:
policy = builtin_perms.get(bt, "always_allow")
if policy == "always_allow":
effective_allowed.append(f"mcp__openswarm-core__{bt}")
elif policy == "deny":
effective_disallowed.append(f"mcp__openswarm-core__{bt}")
if "invoke" in p_modules:
for it in invoke_agent_tools:
policy = builtin_perms.get(it, "always_allow")
if policy == "always_allow":
effective_allowed.append(f"mcp__openswarm-core__{it}")
elif policy == "deny":
effective_disallowed.append(f"mcp__openswarm-core__{it}")
if "spawn" in p_modules:
policy = builtin_perms.get("Agent", "always_allow")
if policy == "always_allow":
effective_allowed.append(f"mcp__openswarm-browser-agent__{bt}")
effective_allowed.append("mcp__openswarm-core__SpawnAgent")
elif policy == "deny":
effective_disallowed.append(f"mcp__openswarm-browser-agent__{bt}")
continue
if name == "openswarm-invoke-agent":
for it in invoke_agent_tools:
policy = builtin_perms.get(it, "always_allow")
effective_disallowed.append("mcp__openswarm-core__SpawnAgent")
if "skill" in p_modules:
policy = builtin_perms.get("Skill", "always_allow")
if policy == "always_allow":
effective_allowed.append(f"mcp__openswarm-invoke-agent__{it}")
elif policy == "deny":
effective_disallowed.append(f"mcp__openswarm-invoke-agent__{it}")
continue
if name == "openswarm-spawn-agent":
policy = builtin_perms.get("Agent", "always_allow")
if policy == "always_allow":
effective_allowed.append("mcp__openswarm-spawn-agent__SpawnAgent")
elif policy == "deny":
effective_disallowed.append("mcp__openswarm-spawn-agent__SpawnAgent")
continue
if name == "openswarm-skill":
policy = builtin_perms.get("Skill", "always_allow")
if policy == "always_allow":
effective_allowed.append("mcp__openswarm-skill__Skill")
else:
effective_disallowed.append("mcp__openswarm-skill__Skill")
continue
if name == "openswarm-ui":
policy = builtin_perms.get("ShowUI", "always_allow")
for ui_tool in ("ShowUI", "AskUI"):
if policy == "always_allow":
effective_allowed.append(f"mcp__openswarm-ui__{ui_tool}")
effective_allowed.append("mcp__openswarm-core__Skill")
else:
effective_disallowed.append(f"mcp__openswarm-ui__{ui_tool}")
continue
if name == "openswarm-web":
# Expose our DDG-backed web tools under an MCP prefix. Honor existing WebSearch/WebFetch permission policy, if the user disabled them in Settings, don't offer the MCP variants either.
for wt in ("WebSearch", "WebFetch"):
policy = builtin_perms.get(wt, "always_allow")
if policy == "always_allow":
effective_allowed.append(f"mcp__openswarm-web__{wt}")
elif policy == "deny":
effective_disallowed.append(f"mcp__openswarm-web__{wt}")
effective_disallowed.append("mcp__openswarm-core__Skill")
if "ui" in p_modules:
policy = builtin_perms.get("ShowUI", "always_allow")
for ui_tool in ("ShowUI", "AskUI"):
if policy == "always_allow":
effective_allowed.append(f"mcp__openswarm-core__{ui_tool}")
else:
effective_disallowed.append(f"mcp__openswarm-core__{ui_tool}")
if "web" in p_modules:
# Honor existing WebSearch/WebFetch permission policy, if the user disabled them in Settings, don't offer the MCP variants either.
for wt in ("WebSearch", "WebFetch"):
policy = builtin_perms.get(wt, "always_allow")
if policy == "always_allow":
effective_allowed.append(f"mcp__openswarm-core__{wt}")
elif policy == "deny":
effective_disallowed.append(f"mcp__openswarm-core__{wt}")
continue
tool_def = next(
@@ -141,7 +142,7 @@ def build_effective_tool_lists(
for bt in path_gate.UNDELIVERABLE_BACKGROUND_TOOLS:
if bt not in effective_disallowed:
effective_disallowed.append(bt)
# The claude_code preset ships its own bare `Skill` tool that reads ~/.claude/skills directly; always withhold it so skills only ever load through our provider-agnostic mcp__openswarm-skill__Skill (or not at all).
# The claude_code preset ships its own bare `Skill` tool that reads ~/.claude/skills directly; always withhold it so skills only ever load through our provider-agnostic mcp__openswarm-core__Skill (or not at all).
if "Skill" not in effective_disallowed:
effective_disallowed.append("Skill")
# Read-only session (onboarding's unattended audit over the user's real files): the mutation/exec
@@ -47,7 +47,7 @@ async def can_use_tool(
if is_claude_schedule_skill(tool_name, input_data):
note_tool_used(ctx.session_id, tool_name, False)
return PermissionResultDeny(
message="Use the openswarm-schedule MCP tools instead of Claude's internal schedule skill."
message="Use the ScheduleWorkflow MCP tools instead of Claude's internal schedule skill."
)
sensitive_pattern: Optional[str] = None
if tool_name != "AskUserQuestion":
@@ -153,7 +153,7 @@ async def pre_tool_hook(ctx: HookContext, input_data: dict, tool_use_id: Optiona
"hookSpecificOutput": {
"hookEventName": hook_event,
"permissionDecision": "deny",
"permissionDecisionReason": "Use the openswarm-schedule MCP tools instead of Claude's internal schedule skill.",
"permissionDecisionReason": "Use the ScheduleWorkflow MCP tools instead of Claude's internal schedule skill.",
}
}
policy, sensitive_pattern = path_gate.maybe_override_policy(
@@ -151,10 +151,10 @@ def extract_target_path(tool_name: str, tool_input: object) -> str:
# Native-scheduler MCP tools that commit or mutate a recurring schedule. Always-on MCP servers fall through to the always_allow default, so these would otherwise fire silently; force them through ApprovalBar. The Cron* tools are Claude's own internal scheduler, denied outright in favour of the visible/auditable native one.
p_SCHEDULE_GATED = {
"mcp__openswarm-schedule__ScheduleWorkflow",
"mcp__openswarm-schedule__UpdateScheduledWorkflow",
"mcp__openswarm-schedule__DeleteScheduledWorkflow",
"mcp__openswarm-schedule__PauseAllWorkflows",
"mcp__openswarm-core__ScheduleWorkflow",
"mcp__openswarm-core__UpdateScheduledWorkflow",
"mcp__openswarm-core__DeleteScheduledWorkflow",
"mcp__openswarm-core__PauseAllWorkflows",
}
CLAUDE_INTERNAL_SCHEDULER_TOOLS = ("CronCreate", "CronList", "CronDelete")
# Preset built-ins that deliver their payload BETWEEN turns. We drive the CLI one turn at a time and stop reading at the ResultMessage, so nothing ever consumes the event: the agent arms a Monitor, promises "I'll report back", ends the turn, and the user waits forever for a message that cannot arrive. Withheld here rather than only in the tool manifest, because the manifest has a kill switch (OSW_TOOL_MANIFEST=0) that would otherwise hand the model a promise we cannot keep.
@@ -100,7 +100,7 @@ def compose_turn_system_prompt(
rich_ui_note = (
"<rich_ui>\n"
"Strongly prefer rendering rich UI over prose, every time the content fits. The tools "
"are mcp__openswarm-ui__ShowUI and mcp__openswarm-ui__AskUI; they are always available "
"are mcp__openswarm-core__ShowUI and mcp__openswarm-core__AskUI; they are always available "
"this session, so call them DIRECTLY by that name, no ToolSearch step needed.\n"
"- ShowUI for any structured result. Use the EXACT component name: a table = data-table, "
"stats = stats-display, links = link-preview, a plan = plan, steps = progress-tracker, "
@@ -13,7 +13,7 @@ from backend.apps.tools_lib.tools_lib import (
logger = logging.getLogger(__name__)
# Cron* live only in the force-deny list (path_gate): our Schedule MCP replaces the CLI scheduler, so allowing them here just churned the allow/deny lists. InvokeAgent's real tool is the mcp__openswarm-invoke-agent__ ref; the bare name was a no-op.
# Cron* live only in the force-deny list (path_gate): our Schedule MCP replaces the CLI scheduler, so allowing them here just churned the allow/deny lists. InvokeAgent's real tool is the mcp__openswarm-core__ ref; the bare name was a no-op.
FULL_TOOLS = [
"Read", "Edit", "Write", "Bash", "Glob", "Grep", "AskUserQuestion",
"WebSearch", "WebFetch", "NotebookEdit", "TodoWrite",
@@ -1,8 +1,10 @@
"""Register the always-on + delegation MCP servers (browser-agent, invoke-agent, meta,
settings-meta) into the per-turn mcp_servers map. The server scripts live in the agents
package, so we resolve their directory off that package here, NOT off a dir a caller passes
in: a caller in a moved file would compute the wrong dir (this bit us once). Returns the
browser/invoke delegation tool-name lists the allowlist gate needs."""
"""Register the builtin tool servers into the per-turn mcp_servers map as ONE combined stdio
process ("openswarm-core"), instead of one python interpreter per server (ENG-208). The SAME
permission conditions that used to skip a server's process now skip its module inside the combined
process (OSW_MCP_MODULES), so a denied capability's tools stay exactly as absent as before. The
server script lives in the agents package, so we resolve its directory off that package here, NOT
off a dir a caller passes in: a caller in a moved file would compute the wrong dir (this bit us
once). Returns the browser/invoke delegation tool-name lists the allowlist gate needs."""
import os
import sys
@@ -25,145 +27,64 @@ def register_builtin_mcp_servers(
) -> Tuple[List[str], List[str]]:
import backend.apps.agents as p_agents_pkg
agents_dir = os.path.dirname(p_agents_pkg.__file__)
# With no renderer for a webview and no human for a prompt, we shadow the map once here and let the existing deny short-circuits skip those servers; nothing below may read the un-shadowed one.
# With no renderer for a webview and no human for a prompt, we shadow the map once here and let the existing deny short-circuits skip those modules; nothing below may read the un-shadowed one.
builtin_perms = apply_unreachable_denies(builtin_perms)
browser_delegation_tools = ["CreateBrowserAgent", "BrowserAgent", "BrowserAgents", "AppAgent"]
invoke_agent_tools = ["InvokeAgent"]
# The always-on trio: MCP discovery (the activation gate's one doorway), agent-editable
# Settings, and CreateApp.
modules = ["meta", "settings", "apps"]
browser_all_denied = all(
builtin_perms.get(t, "always_allow") == "deny"
for t in browser_delegation_tools
)
if not browser_all_denied:
browser_agent_server_path = os.path.join(
agents_dir, "browser_agent_mcp_server.py"
)
backend_port = os.environ.get("OPENSWARM_PORT", "8324")
# Only the card the user actually picked in select-mode gets claimed for the task, so the sub drives that one instead of opening its own duplicate. Passing EVERY dashboard card here (the old behavior) made the sub force-grab a random, usually-parked card and never navigate it, which broke the bulk of browser tasks.
pre_selected_bids = [b for b in (selected_browser_ids or []) if b]
# Apps the user selected this turn; the AppAgent tool may only target these (anti-hallucination gate in the MCP server, which reads this at startup).
selected_app_ids = [a for a in (selected_app_output_ids or []) if a]
auth_tok = get_auth_token()
mcp_servers["openswarm-browser-agent"] = {
"command": sys.executable,
"args": [browser_agent_server_path],
"env": {
"OPENSWARM_PORT": backend_port,
"OPENSWARM_AUTH_TOKEN": auth_tok,
"OPENSWARM_AGENT_MODEL": session.model,
"OPENSWARM_DASHBOARD_ID": session.dashboard_id or "",
"OPENSWARM_PRE_SELECTED_BROWSER_IDS": ",".join(pre_selected_bids),
"OPENSWARM_SELECTED_APP_IDS": ",".join(selected_app_ids),
"OPENSWARM_PARENT_SESSION_ID": session.id,
},
"type": "stdio",
}
modules.append("browser")
invoke_agent_tools = ["InvokeAgent"]
invoke_all_denied = all(
builtin_perms.get(t, "always_allow") == "deny"
for t in invoke_agent_tools
)
if not invoke_all_denied:
invoke_agent_server_path = os.path.join(
agents_dir, "invoke_agent_mcp_server.py"
)
backend_port = os.environ.get("OPENSWARM_PORT", "8324")
mcp_servers["openswarm-invoke-agent"] = {
"command": sys.executable,
"args": [invoke_agent_server_path],
"env": {
"OPENSWARM_PORT": backend_port,
"OPENSWARM_AUTH_TOKEN": get_auth_token(),
"OPENSWARM_PARENT_SESSION_ID": session.id,
"OPENSWARM_DASHBOARD_ID": session.dashboard_id or "",
},
"type": "stdio",
}
if not all(builtin_perms.get(t, "always_allow") == "deny" for t in invoke_agent_tools):
modules.append("invoke")
# SpawnAgent replaces the CLI's built-in Agent tool (blocked in RunOptions); gated by the same "Agent" permission so the Tools-page toggle keeps working.
if builtin_perms.get("Agent", "always_allow") != "deny":
spawn_agent_server_path = os.path.join(
agents_dir, "spawn_agent_mcp_server.py"
)
mcp_servers["openswarm-spawn-agent"] = {
"command": sys.executable,
"args": [spawn_agent_server_path],
"env": {
"OPENSWARM_PORT": os.environ.get("OPENSWARM_PORT", "8324"),
"OPENSWARM_AUTH_TOKEN": get_auth_token(),
"OPENSWARM_PARENT_SESSION_ID": session.id,
"OPENSWARM_DASHBOARD_ID": session.dashboard_id or "",
},
"type": "stdio",
}
modules.append("spawn")
# Always-on core meta-MCP server: ONE process hosting the three ungated always-on servers that
# used to be three interpreters (MCP discovery MCPList/Search/Activate, SettingsRead/Write,
# CreateApp). None is referenced by the permission gate, so this is pure fan-out reduction
# (ENG-208). The activation gate still governs external MCPs exactly as before, via MCPActivate.
combined_meta_path = os.path.join(agents_dir, "combined_meta_mcp_server.py")
mcp_servers["openswarm-core"] = {
"command": sys.executable,
"args": [combined_meta_path],
"env": {
"OPENSWARM_PORT": os.environ.get("OPENSWARM_PORT", "8324"),
"OPENSWARM_AUTH_TOKEN": get_auth_token(),
"OPENSWARM_PARENT_SESSION_ID": session.id,
},
"type": "stdio",
}
# Skill server: exposes the Skill tool so the agent can load an installed skill on its own (the <skills> catalog in the prompt lists what's available). Gated on at least one non-built-in skill existing AND Skill not being denied, so we never offer a tool with an empty catalog. Kept in sync with build_installed_skills_catalog, which omits the catalog under the same conditions.
skill_denied = builtin_perms.get("Skill", "always_allow") == "deny"
if not skill_denied:
# Skill module: gated on at least one non-built-in skill existing AND Skill not being denied, so we never offer a tool with an empty catalog. Kept in sync with build_installed_skills_catalog, which omits the catalog under the same conditions.
if builtin_perms.get("Skill", "always_allow") != "deny":
try:
from backend.apps.skills.skills import sync_skills
has_loadable_skill = any(not s.built_in and s.enabled for s in sync_skills())
except Exception:
has_loadable_skill = False
if has_loadable_skill:
skill_server_path = os.path.join(agents_dir, "skill_mcp_server.py")
mcp_servers["openswarm-skill"] = {
"command": sys.executable,
"args": [skill_server_path],
"env": {
"OPENSWARM_PORT": os.environ.get("OPENSWARM_PORT", "8324"),
"OPENSWARM_AUTH_TOKEN": get_auth_token(),
"OPENSWARM_PARENT_SESSION_ID": session.id,
},
"type": "stdio",
}
modules.append("skill")
# ShowUI renders rich inline components from the tool_call input (display only, server just
# validates); AskUI renders an interactive component and BLOCKS on /api/ui-requests/wait until
# the user answers in the transcript. Gated on the ShowUI builtin perm.
show_ui_denied = builtin_perms.get("ShowUI", "always_allow") == "deny"
if not show_ui_denied:
show_ui_server_path = os.path.join(agents_dir, "show_ui_mcp_server.py")
mcp_servers["openswarm-ui"] = {
"command": sys.executable,
"args": [show_ui_server_path],
"env": {
"OPENSWARM_PORT": os.environ.get("OPENSWARM_PORT", "8324"),
"OPENSWARM_AUTH_TOKEN": get_auth_token(),
"OPENSWARM_PARENT_SESSION_ID": session.id,
},
"type": "stdio",
}
# ShowUI renders rich inline components from the tool_call input (display only, server just validates); AskUI renders an interactive component and BLOCKS on /api/ui-requests/wait until the user answers in the transcript. Gated on the ShowUI builtin perm.
if builtin_perms.get("ShowUI", "always_allow") != "deny":
modules.append("ui")
# Always-on schedule server: ScheduleWorkflow + CRUD + AddWorkflowStep/EditWorkflowStep so the agent (and the workflow Edit Agent) can build and schedule recurring work via the native scheduler instead of cron/launchctl. The 4 scheduling tools are force-gated in path_gate; Cron* is denied in build_effective_tool_lists.
schedule_server_path = os.path.join(
agents_dir, "schedule_mcp_server.py"
)
mcp_servers["openswarm-schedule"] = {
# Schedule module: ScheduleWorkflow + CRUD + step editing so the agent (and the workflow Edit Agent) can build and schedule recurring work via the native scheduler. The 4 scheduling tools are force-gated in path_gate; Cron* is denied in build_effective_tool_lists.
modules.append("schedule")
# Only the card the user actually picked in select-mode gets claimed for the task, so the sub drives that one instead of opening its own duplicate. Passing EVERY dashboard card here (the old behavior) made the sub force-grab a random, usually-parked card and never navigate it, which broke the bulk of browser tasks.
pre_selected_bids = [b for b in (selected_browser_ids or []) if b]
# Apps the user selected this turn; the AppAgent tool may only target these (anti-hallucination gate in the MCP server, which reads this at startup).
selected_app_ids = [a for a in (selected_app_output_ids or []) if a]
combined_path = os.path.join(agents_dir, "combined_meta_mcp_server.py")
mcp_servers["openswarm-core"] = {
"command": sys.executable,
"args": [schedule_server_path],
"args": [combined_path],
"env": {
"OSW_MCP_MODULES": ",".join(modules),
"OPENSWARM_PORT": os.environ.get("OPENSWARM_PORT", "8324"),
"OPENSWARM_AUTH_TOKEN": get_auth_token(),
"OPENSWARM_PARENT_SESSION_ID": session.id,
"OPENSWARM_DASHBOARD_ID": session.dashboard_id or "",
"OPENSWARM_AGENT_MODEL": session.model,
"OPENSWARM_PRE_SELECTED_BROWSER_IDS": ",".join(pre_selected_bids),
"OPENSWARM_SELECTED_APP_IDS": ",".join(selected_app_ids),
},
"type": "stdio",
}
@@ -145,7 +145,7 @@ class RunOptions(AgentManagerProtocol):
register_web_mcp_server(
mcp_servers, p_m,
browser_ok=bool(browser_delegation_tools),
rich_ui_ok="openswarm-ui" in mcp_servers,
rich_ui_ok="ui" in mcp_servers.get("openswarm-core", {}).get("env", {}).get("OSW_MCP_MODULES", "").split(","),
)
effective_allowed, effective_disallowed = build_effective_tool_lists(
@@ -104,13 +104,9 @@ def set_framework_overhead(session: AgentSession, composed_prompt: Optional[str]
@typechecked
def register_web_mcp_server(mcp_servers: Dict, p_m: str, browser_ok: bool = False, rich_ui_ok: bool = False) -> None:
"""Register the DDG-backed openswarm-web stdio MCP into the server set when the primary has no
reliable native web path. The server script lives in the agents package (not here), so resolve
it off that package dir, not __file__."""
import os
import sys
import backend.apps.agents as p_agents_pkg
web_mcp_server_path = os.path.join(os.path.dirname(p_agents_pkg.__file__), "web_mcp_server.py")
"""Add the DDG-backed web tools when the primary has no reliable native web path. They ride the
combined openswarm-core process now (ENG-208): flip the module flag and env on the entry that
register_builtin_mcp_servers already made, instead of spawning an eleventh interpreter."""
# Tell the MCP which primary the session is using so it can route to that provider's native search tool.
if p_m.startswith(("gc/", "gemini/", "ag/")):
p_primary_hint = "gemini"
@@ -118,11 +114,28 @@ def register_web_mcp_server(mcp_servers: Dict, p_m: str, browser_ok: bool = Fals
p_primary_hint = "openai"
else:
p_primary_hint = ""
core = mcp_servers.get("openswarm-core")
if core is not None:
env = core["env"]
modules = [m for m in env.get("OSW_MCP_MODULES", "").split(",") if m]
if "web" not in modules:
modules.append("web")
env["OSW_MCP_MODULES"] = ",".join(modules)
env["OPENSWARM_PRIMARY_API"] = p_primary_hint
env["OPENSWARM_BROWSER_OK"] = "1" if browser_ok else "0"
env["OPENSWARM_RICH_UI_OK"] = "1" if rich_ui_ok else "0"
return
# Belt for a caller that skipped register_builtin (none today): web tools still arrive, alone.
import os
import sys
import backend.apps.agents as p_agents_pkg
combined_path = os.path.join(os.path.dirname(p_agents_pkg.__file__), "combined_meta_mcp_server.py")
from backend.auth import get_auth_token as p_get_auth_token3
mcp_servers["openswarm-web"] = {
mcp_servers["openswarm-core"] = {
"command": sys.executable,
"args": [web_mcp_server_path],
"args": [combined_path],
"env": {
"OSW_MCP_MODULES": "web",
"OPENSWARM_PORT": os.environ.get("OPENSWARM_PORT", "8324"),
"OPENSWARM_AUTH_TOKEN": p_get_auth_token3(),
"OPENSWARM_PRIMARY_API": p_primary_hint,
@@ -142,8 +155,8 @@ def append_web_tools_hint(composed_prompt: Optional[str], need_web_mcp: bool, ef
"""Append a <web_tools> block naming the MCP-backed WebSearch/WebFetch when the deferred bare
WebSearch tool isn't usable on this session, so smaller models don't thrash on ToolSearch."""
p_web_tools_available = need_web_mcp and (
"mcp__openswarm-web__WebSearch" in effective_allowed
or "mcp__openswarm-web__WebFetch" in effective_allowed
"mcp__openswarm-core__WebSearch" in effective_allowed
or "mcp__openswarm-core__WebFetch" in effective_allowed
)
if not p_web_tools_available:
return composed_prompt
@@ -155,14 +168,14 @@ def append_web_tools_hint(composed_prompt: Optional[str], need_web_mcp: bool, ef
"equivalents instead, call them DIRECTLY, no ToolSearch "
"step needed:"
)
if "mcp__openswarm-web__WebSearch" in effective_allowed:
if "mcp__openswarm-core__WebSearch" in effective_allowed:
p_hint_lines.append(
"- `mcp__openswarm-web__WebSearch(query: str, "
"- `mcp__openswarm-core__WebSearch(query: str, "
"num_results?: int)`, DuckDuckGo search."
)
if "mcp__openswarm-web__WebFetch" in effective_allowed:
if "mcp__openswarm-core__WebFetch" in effective_allowed:
p_hint_lines.append(
"- `mcp__openswarm-web__WebFetch(url: str, prompt?: "
"- `mcp__openswarm-core__WebFetch(url: str, prompt?: "
"str)`, fetch a URL and return readable text."
)
p_hint_lines.append(
@@ -38,7 +38,7 @@ async def run_browser_fast_path(
logger.info(f"[browser-fast-path] direct dispatch for session {session_id} ({verdict})")
text = ""
# The fast-path skips the orchestrator, so the UI never gets the BrowserAgent tool-call that draws the "Browser Agent" bubble. Emit a synthetic tool_call/ tool_result pair (same shape + mcp__ name the orchestrator uses) so the bubble shows here too. None until we actually dispatch a browser (a pure READ answer has no browser, so no bubble).
p_browser_tool = "mcp__openswarm-browser-agent__CreateBrowserAgent"
p_browser_tool = "mcp__openswarm-core__CreateBrowserAgent"
p_bubble_tid: Optional[str] = None
p_action_logs: List[List[Dict[str, object]]] = []
p_last_result: Dict[str, object] = {}
+2 -2
View File
@@ -208,10 +208,10 @@ def resolve_policy_slot(tool_name: str, tools: list[ToolDefinition]) -> PolicySl
differently. That divergence was the bug behind 'Always approve' acting like a
one-time accept: writes landed under the raw mcp__server__action name while the
gate read the parsed inner action, so the next call never saw the policy."""
bm = re.match(r"mcp__openswarm-browser-agent__(.+)", tool_name)
bm = re.match(r"mcp__openswarm-core__(.+)", tool_name)
if bm:
return PolicySlot("builtin", bm.group(1), None)
im = re.match(r"mcp__openswarm-invoke-agent__(.+)", tool_name)
im = re.match(r"mcp__openswarm-core__(.+)", tool_name)
if im:
return PolicySlot("builtin", im.group(1), None)
m = re.match(r"mcp__([^_]+(?:-[^_]+)*)__(.+)", tool_name)
+1 -1
View File
@@ -33,7 +33,7 @@ def test_always_allow_applies_to_next_call_same_run(p_isolated_persistence):
def test_always_allow_namespaced_builtin_uses_inner_slot(p_isolated_persistence):
# Our browser/invoke delegation tools live in builtin_permissions under the INNER name; a write through the namespaced name must land where the next read looks.
live_perms: dict = {}
name = "mcp__openswarm-browser-agent__BrowserAgent"
name = "mcp__openswarm-core__BrowserAgent"
decision.set_tool_policy(name, "always_allow", live_perms)
assert live_perms == {"BrowserAgent": "always_allow"}
assert decision.effective_policy(name, live_perms, {}) == "always_allow"
+1 -1
View File
@@ -45,7 +45,7 @@ def test_empty_assistant_text_is_an_empty_finish():
def test_ui_answer_tools_are_a_legit_finish():
s = p_session(("user", "show me"),
("tool_call", {"tool": "mcp__openswarm-ui__ShowUI", "input": {}}),
("tool_call", {"tool": "mcp__openswarm-core__ShowUI", "input": {}}),
("tool_result", {"text": "rendered"}))
assert turn_finished_empty(s) is False
+13 -13
View File
@@ -78,40 +78,40 @@ def test_a_desktop_launch_denies_nothing_even_before_its_window_loads(monkeypatc
def test_headless_with_a_renderer_offers_the_browser_server_again(monkeypatch, renderer_attached):
monkeypatch.setenv("OPENSWARM_HEADLESS", "1")
mcp_servers, allowed, disallowed = p_run_the_real_pipeline()
assert "openswarm-browser-agent" in mcp_servers
assert "browser" in mcp_servers["openswarm-core"]["env"]["OSW_MCP_MODULES"].split(",")
for tool in BROWSER_DELEGATION:
assert f"mcp__openswarm-browser-agent__{tool}" in allowed
assert f"mcp__openswarm-core__{tool}" in allowed
# Still nobody to answer, so the human-bound pair stays gone.
assert "openswarm-ui" not in mcp_servers
assert "ui" not in mcp_servers["openswarm-core"]["env"]["OSW_MCP_MODULES"].split(",")
assert "AskUserQuestion" in disallowed
def test_headless_drops_the_renderer_bound_servers_and_tools(monkeypatch, no_renderer):
monkeypatch.setenv("OPENSWARM_HEADLESS", "1")
mcp_servers, allowed, disallowed = p_run_the_real_pipeline()
assert "openswarm-browser-agent" not in mcp_servers
assert "openswarm-ui" not in mcp_servers
assert "browser" not in mcp_servers["openswarm-core"]["env"]["OSW_MCP_MODULES"].split(",")
assert "ui" not in mcp_servers["openswarm-core"]["env"]["OSW_MCP_MODULES"].split(",")
for tool in BROWSER_DELEGATION:
assert f"mcp__openswarm-browser-agent__{tool}" not in allowed
assert f"mcp__openswarm-core__{tool}" not in allowed
for ui_tool in ("ShowUI", "AskUI"):
assert f"mcp__openswarm-ui__{ui_tool}" not in allowed
assert f"mcp__openswarm-core__{ui_tool}" not in allowed
assert "AskUserQuestion" not in allowed
assert "AskUserQuestion" in disallowed
# The rest of the surface is untouched; headless prunes the renderer, it doesn't lobotomise the agent.
assert "Read" in allowed and "Bash" in allowed
assert "openswarm-invoke-agent" in mcp_servers
assert "invoke" in mcp_servers["openswarm-core"]["env"]["OSW_MCP_MODULES"].split(",")
assert "openswarm-core" in mcp_servers
def test_without_headless_every_one_of_them_is_offered(monkeypatch, no_renderer):
monkeypatch.delenv("OPENSWARM_HEADLESS", raising=False)
mcp_servers, allowed, _ = p_run_the_real_pipeline()
assert "openswarm-browser-agent" in mcp_servers
assert "openswarm-ui" in mcp_servers
assert "browser" in mcp_servers["openswarm-core"]["env"]["OSW_MCP_MODULES"].split(",")
assert "ui" in mcp_servers["openswarm-core"]["env"]["OSW_MCP_MODULES"].split(",")
for tool in BROWSER_DELEGATION:
assert f"mcp__openswarm-browser-agent__{tool}" in allowed
assert f"mcp__openswarm-core__{tool}" in allowed
for ui_tool in ("ShowUI", "AskUI"):
assert f"mcp__openswarm-ui__{ui_tool}" in allowed
assert f"mcp__openswarm-core__{ui_tool}" in allowed
def test_askuserquestion_survives_when_the_ui_server_is_absent(monkeypatch):
@@ -127,7 +127,7 @@ def test_askuserquestion_survives_when_the_ui_server_is_absent(monkeypatch):
def test_only_the_exact_flag_value_turns_headless_on(monkeypatch, no_renderer):
monkeypatch.setenv("OPENSWARM_HEADLESS", "0")
_, allowed, _ = p_run_the_real_pipeline()
assert "mcp__openswarm-ui__ShowUI" in allowed
assert "mcp__openswarm-core__ShowUI" in allowed
@pytest.mark.asyncio
@@ -21,21 +21,20 @@ def test_registers_always_on_and_delegation_servers():
assert "openswarm-mcp-meta" not in mcp_servers
assert "openswarm-settings-meta" not in mcp_servers
assert "openswarm-apps" not in mcp_servers
# delegation (not denied)
assert "openswarm-browser-agent" in mcp_servers
assert "openswarm-invoke-agent" in mcp_servers
# delegation (not denied) rides the combined process as module flags
mods = mcp_servers["openswarm-core"]["env"]["OSW_MCP_MODULES"].split(",")
assert "browser" in mods and "invoke" in mods
assert browser_tools == ["CreateBrowserAgent", "BrowserAgent", "BrowserAgents", "AppAgent"]
assert invoke_tools == ["InvokeAgent"]
# Every registered server's script path must resolve to a file that ACTUALLY EXISTS. This is the assertion that catches a moved-caller resolving the wrong agents dir.
for name in ("openswarm-core", "openswarm-browser-agent", "openswarm-invoke-agent"):
script = mcp_servers[name]["args"][0]
assert os.path.isfile(script), f"{name} script does not exist on disk: {script}"
script = mcp_servers["openswarm-core"]["args"][0]
assert os.path.isfile(script), f"combined server script does not exist on disk: {script}"
def test_fully_denied_delegation_servers_are_not_registered():
mcp_servers = {}
perms = {t: "deny" for t in ("CreateBrowserAgent", "BrowserAgent", "BrowserAgents", "AppAgent", "InvokeAgent")}
register_builtin_mcp_servers(mcp_servers, p_session(), perms, None, None)
assert "openswarm-browser-agent" not in mcp_servers # all browser tools denied -> skip
assert "openswarm-invoke-agent" not in mcp_servers
assert "openswarm-core" in mcp_servers # always-on regardless
mods = mcp_servers["openswarm-core"]["env"]["OSW_MCP_MODULES"].split(",")
assert "browser" not in mods and "invoke" not in mods # all denied -> module skipped
assert "meta" in mods and "apps" in mods # always-on regardless
+4 -4
View File
@@ -10,10 +10,10 @@ from backend.apps.agents.manager.permissions.workflow_approval import is_claude_
def test_schedule_commit_tools_force_ask_even_when_always_allow():
for tool in (
"mcp__openswarm-schedule__ScheduleWorkflow",
"mcp__openswarm-schedule__UpdateScheduledWorkflow",
"mcp__openswarm-schedule__DeleteScheduledWorkflow",
"mcp__openswarm-schedule__PauseAllWorkflows",
"mcp__openswarm-core__ScheduleWorkflow",
"mcp__openswarm-core__UpdateScheduledWorkflow",
"mcp__openswarm-core__DeleteScheduledWorkflow",
"mcp__openswarm-core__PauseAllWorkflows",
):
policy, _ = path_gate.maybe_override_policy("always_allow", tool, {})
assert policy == "ask", f"{tool} must force an approval, not silently always_allow"
+4 -4
View File
@@ -26,9 +26,9 @@ def test_slot_for_builtin_tool():
def test_slot_for_our_browser_and_invoke_agents_uses_inner_name():
# These live in builtin_permissions under the INNER name, not the namespaced one.
assert resolve_policy_slot("mcp__openswarm-browser-agent__BrowserAgent", []) == \
assert resolve_policy_slot("mcp__openswarm-core__BrowserAgent", []) == \
PolicySlot("builtin", "BrowserAgent", None)
assert resolve_policy_slot("mcp__openswarm-invoke-agent__InvokeAgent", []) == \
assert resolve_policy_slot("mcp__openswarm-core__InvokeAgent", []) == \
PolicySlot("builtin", "InvokeAgent", None)
@@ -79,8 +79,8 @@ def test_always_approve_round_trips_for_every_tool_shape():
shapes = [
"Bash",
"Read",
"mcp__openswarm-browser-agent__BrowserAgent",
"mcp__openswarm-invoke-agent__InvokeAgent",
"mcp__openswarm-core__BrowserAgent",
"mcp__openswarm-core__InvokeAgent",
f"mcp__{slug}__notion-fetch",
]
for tool_name in shapes: