Просмотр исходного кода

fix(provider): 隔离用户 MCP(--strict-mcp-config) — 治"我只有 Gmail/Calendar 写不了文件"

实锤(run_f2a1eca9c6ca):工作区助手绑对目录、读到真论文,却 77 步只 WriteFile 1 次,
末尾 claude-code 回"当前工具集不匹配,我仅拥有 Gmail/Google Calendar 认证工具,无法
完成任务"并劝用户去用工作区模式(它自己就是)。

根因:provider 用 `--tools ""` 关掉 claude-code **原生**工具(防它私自跑 WriteFile
编假数据),但用户 ~/.claude 里的 **MCP 服务(Gmail/Calendar)是另一路、没被关**。
claude-code 一自省"我有啥工具"只看到这些 MCP——平台的 ReadFile/WriteFile 只是 system
prompt 里"请输出 JSON"协议、不算它持有的真工具——于是判定没工具、拒绝。fresh
session(本 run 多次熔断换新)冷启动时尤其容易这样犯。

修:first-turn + resume 两处命令都加 `--strict-mcp-config`(不配 --mcp-config =
不加载任何 MCP)→ 工具集彻底清空 → 纯走 react JSON 协议,不再误判。
回归 TestMcpIsolation(2)+ lambdagent 576 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kenny67nju 3 месяцев назад
Родитель
Сommit
be598b2cee

+ 8 - 0
lambdagent/src/lambdagent/providers/claude_code_provider.py

@@ -418,6 +418,13 @@ class ClaudeCodeProvider(LLMProvider):
             # orchestrator fabricated 150 KB of fake `sim_results.json` via
             # native WriteFile while every `call_physsim` invocation STALLed).
             "--tools", "",
+            # 同时隔离用户的 MCP 服务。--tools "" 只关原生工具,但用户 ~/.claude
+            # 配置里的 MCP(如 Gmail/Google Calendar)仍会被加载——claude-code 一
+            # 自省就只看到这些 MCP 工具、看不到平台 react 工具(ReadFile/WriteFile
+            # 只是 prompt 里的"输出 JSON"协议、不算它持有的真工具),于是判定"工具
+            # 不匹配、写不了文件"并拒绝(run_f2a1eca9c6ca 实锤)。--strict-mcp-config
+            # 不配 --mcp-config = 不加载任何 MCP → 工具集清空 → 纯走 react JSON 协议。
+            "--strict-mcp-config",
             "--dangerously-skip-permissions",
         ]
 
@@ -536,6 +543,7 @@ class ClaudeCodeProvider(LLMProvider):
             # See _call_new comment — keep parity on resume so claude can
             # never run native tools, regardless of session phase.
             "--tools", "",
+            "--strict-mcp-config",  # 同 _call_new:隔离用户 MCP,防"我只有 Gmail/Calendar"误判
             "--dangerously-skip-permissions",
         ]
 

+ 46 - 0
lambdagent/tests/test_providers.py

@@ -474,3 +474,49 @@ class TestResumeStallGiveup(unittest.TestCase):
                 p._call_resume([{"role": "user", "content": "x"}])
             self.assertIsNone(p._session_id, "连卡2次后应清会话换新")
             self.assertEqual(p._resume_stall_streak, 0, "熔断后计数归零")
+
+
+class TestMcpIsolation(unittest.TestCase):
+    """claude-code 子进程必须隔离用户 MCP(--strict-mcp-config),否则模型只看到
+    Gmail/Calendar 等 MCP 工具、判定'没有工具写不了文件'而拒绝(run_f2a1eca9c6ca)。"""
+
+    def _capture_cmd(self, session_id):
+        from lambdagent.providers import claude_code_provider as ccp
+        from lambdagent.providers.base import ProviderConfig
+
+        class FakeResult:
+            returncode = 0
+            stdout = '{"result":"ok"}' if session_id is None else "ok"
+            stderr = ""
+
+        captured = {}
+
+        def fake_run(cmd, **kw):
+            captured["cmd"] = cmd
+            return FakeResult()
+
+        p = ccp.ClaudeCodeProvider.__new__(ccp.ClaudeCodeProvider)
+        p.config = ProviderConfig(model="sonnet", timeout=600, extra={})
+        p.claude_bin = "claude"
+        p._session_id = session_id
+        p._resume_count = 0
+        p._resume_stall_streak = 0
+        msgs = [{"role": "user", "content": "hi"}]
+        with patch.object(ccp, "_run_with_idle_timeout", side_effect=fake_run):
+            if session_id is None:
+                p._call_new(msgs)
+            else:
+                p._call_resume(msgs)
+        return captured["cmd"]
+
+    def test_first_turn_isolates_mcp_and_native_tools(self):
+        cmd = self._capture_cmd(session_id=None)
+        self.assertIn("--strict-mcp-config", cmd)
+        self.assertIn("--tools", cmd)
+        # --tools 后面跟空串(禁原生工具)
+        self.assertEqual(cmd[cmd.index("--tools") + 1], "")
+
+    def test_resume_isolates_mcp_and_native_tools(self):
+        cmd = self._capture_cmd(session_id="sess-1")
+        self.assertIn("--strict-mcp-config", cmd)
+        self.assertIn("--tools", cmd)