diff --git a/backend/apps/agents/manager/prompt/tool_catalog.py b/backend/apps/agents/manager/prompt/tool_catalog.py index dfb41235..1f2afced 100644 --- a/backend/apps/agents/manager/prompt/tool_catalog.py +++ b/backend/apps/agents/manager/prompt/tool_catalog.py @@ -28,16 +28,17 @@ FULL_TOOLS = [ def resolve_builtin_tools_option() -> Union[List[str], Dict[str, str]]: """The SDK `tools` option (CLI `--tools`), the base set of built-in tools. - Default: the full claude_code preset, which ships every preset built-in's schema. With - OSW_TOOL_MANIFEST=1, an explicit FULL_TOOLS list instead, ONLY the built-ins OpenSwarm exposes, - which prunes the ~9 preset extras nothing here references (Cron*/Monitor/Task*/PushNotification/ - RemoteTrigger/etc) for ~940 schema tokens/turn, cache-stable. MCP tools ride mcp_servers, so - SpawnAgent/browser/schedule/skill/web/user MCPs are untouched; ToolSearch stays in FULL_TOOLS so - deferred loading survives (live-proven: the model still ToolSearch-loads + calls MCP tools under - the manifest). Flag-gated pending a real-app soak before default-on.""" - if os.environ.get("OSW_TOOL_MANIFEST") == "1": - return list(FULL_TOOLS) - return {"type": "preset", "preset": "claude_code"} + Default: an explicit FULL_TOOLS list, ONLY the built-ins OpenSwarm exposes. The claude_code + preset also ships Monitor/PushNotification/RemoteTrigger/ScheduleWakeup/ExitWorktree, which + nothing in this repo references, plus a bare `Skill` that build_effective_tool_lists hard-denies + anyway. Measured on a live "hi" turn (Anthropic count_tokens on the captured wire request, and + the provider's own usage reporting, agreeing within 1 token): 31,091 -> 26,880 prompt tokens, + 4,211 saved per turn, cache-stable. MCP tools ride mcp_servers, so SpawnAgent/browser/schedule/ + skill/web/user MCPs are untouched; ToolSearch stays in FULL_TOOLS so deferred loading survives. + Kill switch: OSW_TOOL_MANIFEST=0 restores the preset.""" + if os.environ.get("OSW_TOOL_MANIFEST") == "0": + return {"type": "preset", "preset": "claude_code"} + return list(FULL_TOOLS) @typechecked diff --git a/backend/tests/test_tool_manifest.py b/backend/tests/test_tool_manifest.py index 75e08dfc..a2c306f6 100644 --- a/backend/tests/test_tool_manifest.py +++ b/backend/tests/test_tool_manifest.py @@ -1,7 +1,7 @@ -"""OSW_TOOL_MANIFEST gate: default ships the full claude_code preset; flag-on ships an explicit -FULL_TOOLS list that prunes dead preset built-ins WITHOUT dropping anything OpenSwarm uses. The -manifest MUST keep every built-in in effective_allowed's source set + ToolSearch (so deferred MCP -loading survives); MCP tools ride mcp_servers, not this list.""" +"""OSW_TOOL_MANIFEST gate: default ships an explicit FULL_TOOLS list that prunes dead preset +built-ins WITHOUT dropping anything OpenSwarm uses; OSW_TOOL_MANIFEST=0 is the kill switch back to +the full claude_code preset. The manifest MUST keep every built-in in effective_allowed's source set ++ ToolSearch (so deferred MCP loading survives); MCP tools ride mcp_servers, not this list.""" from backend.apps.agents.manager.prompt.tool_catalog import ( @@ -9,14 +9,11 @@ from backend.apps.agents.manager.prompt.tool_catalog import ( resolve_builtin_tools_option, ) +PRESET = {"type": "preset", "preset": "claude_code"} -def test_default_is_the_full_preset(monkeypatch): + +def test_default_is_the_explicit_full_tools_manifest(monkeypatch): monkeypatch.delenv("OSW_TOOL_MANIFEST", raising=False) - assert resolve_builtin_tools_option() == {"type": "preset", "preset": "claude_code"} - - -def test_flag_on_is_the_explicit_full_tools_manifest(monkeypatch): - monkeypatch.setenv("OSW_TOOL_MANIFEST", "1") out = resolve_builtin_tools_option() assert isinstance(out, list) assert out == list(FULL_TOOLS) @@ -24,22 +21,34 @@ def test_flag_on_is_the_explicit_full_tools_manifest(monkeypatch): assert out is not FULL_TOOLS +def test_kill_switch_restores_the_full_preset(monkeypatch): + monkeypatch.setenv("OSW_TOOL_MANIFEST", "0") + assert resolve_builtin_tools_option() == PRESET + + def test_manifest_keeps_toolsearch_so_deferred_mcp_loading_survives(monkeypatch): - monkeypatch.setenv("OSW_TOOL_MANIFEST", "1") - out = resolve_builtin_tools_option() - assert "ToolSearch" in out + monkeypatch.delenv("OSW_TOOL_MANIFEST", raising=False) + assert "ToolSearch" in resolve_builtin_tools_option() def test_manifest_keeps_every_core_builtin_openswarm_exposes(monkeypatch): # These are the built-ins the agent actually uses; none may vanish from the manifest. - monkeypatch.setenv("OSW_TOOL_MANIFEST", "1") + monkeypatch.delenv("OSW_TOOL_MANIFEST", raising=False) out = resolve_builtin_tools_option() for core in ("Read", "Edit", "Write", "Bash", "Glob", "Grep", "AskUserQuestion"): assert core in out -def test_only_the_exact_flag_value_flips_it(monkeypatch): - monkeypatch.setenv("OSW_TOOL_MANIFEST", "true") # not "1" - assert resolve_builtin_tools_option() == {"type": "preset", "preset": "claude_code"} - monkeypatch.setenv("OSW_TOOL_MANIFEST", "0") - assert resolve_builtin_tools_option() == {"type": "preset", "preset": "claude_code"} +def test_manifest_drops_only_builtins_nothing_in_the_repo_references(monkeypatch): + # Live-measured saving comes from exactly these; if one ever gains a caller, it must come back. + monkeypatch.delenv("OSW_TOOL_MANIFEST", raising=False) + out = resolve_builtin_tools_option() + for dead in ("Monitor", "PushNotification", "RemoteTrigger", "ScheduleWakeup", "ExitWorktree", "Skill"): + assert dead not in out + + +def test_only_the_exact_kill_switch_value_flips_it(monkeypatch): + monkeypatch.setenv("OSW_TOOL_MANIFEST", "false") # not "0" + assert resolve_builtin_tools_option() == list(FULL_TOOLS) + monkeypatch.setenv("OSW_TOOL_MANIFEST", "1") + assert resolve_builtin_tools_option() == list(FULL_TOOLS)