From 476e97c99312880dbaea15c086b318cc38e5dab5 Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 24 Sep 2026 10:35:51 +0000 Subject: [PATCH] fix(tooling): strip MCP: prefix in Cursor hook tool_name matching IDE postToolUse reports tool_name as MCP:pi_subagent_*; without stripping, guard-pi-dispatch skipped shape checks. Confirmed via live pi_subagent_roles. --- .cursor/hooks/_lib.py | 16 ++++- .cursor/rules/cursor-hooks.mdc | 2 + .../2026-09-24-cursor-hook-event-contract.md | 8 +++ .doc/audit/ISSUES.md | 2 +- ...-24-cursor-governance-hooks-test-report.md | 12 ++++ .../ide-mcp-probe-2026-09-24.json | 58 +++++++++++++++++++ .../probe-status.json | 25 ++------ scripts/tests/test_cursor_hooks.py | 38 ++++++++++++ 8 files changed, 139 insertions(+), 22 deletions(-) create mode 100644 .doc/test-reports/assets/2026-09-24-cursor-governance-hooks/ide-mcp-probe-2026-09-24.json diff --git a/.cursor/hooks/_lib.py b/.cursor/hooks/_lib.py index b08429e6..f1b2df16 100644 --- a/.cursor/hooks/_lib.py +++ b/.cursor/hooks/_lib.py @@ -89,9 +89,23 @@ def first_key(data: dict[str, Any], *names: str) -> Any: return None +def normalize_tool_name(name: str | None) -> str | None: + """Strip Cursor MCP prefixes such as 'MCP:' / 'MCP: pi-subagents/'.""" + if name is None: + return None + n = str(name).strip() + # Observed IDE postToolUse: tool_name == "MCP:pi_subagent_roles" + if n.upper().startswith("MCP:"): + n = n[4:].lstrip() + # Rare: "MCP: pi-subagents/pi_subagent_run" + if "/" in n and n.rsplit("/", 1)[-1].startswith("pi_subagent_"): + n = n.rsplit("/", 1)[-1] + return n or None + + def tool_name(data: dict[str, Any]) -> str | None: v = first_key(data, "tool_name", "mcp_tool_name", "name", "toolName") - return str(v) if v is not None else None + return normalize_tool_name(str(v) if v is not None else None) def mcp_server(data: dict[str, Any]) -> str | None: diff --git a/.cursor/rules/cursor-hooks.mdc b/.cursor/rules/cursor-hooks.mdc index bcf393c0..d929b208 100644 --- a/.cursor/rules/cursor-hooks.mdc +++ b/.cursor/rules/cursor-hooks.mdc @@ -24,6 +24,8 @@ alwaysApply: true - `postToolUse`:`pi-receipt-context.sh`(注入收据核对提醒) - `sessionStart` / `stop`:`audit-session.sh`(审计 + 可选只读盘点;**不注入上下文**) +IDE 实测(2026-09-24):`postToolUse` 的 `tool_name` 形如 `MCP:pi_subagent_roles`;hooks 须剥 `MCP:` 前缀后再匹配角色工具名。 + ## 审计与 probe - 审计:`.cursor/hook-audit.jsonl`(gitignore;勿提交) diff --git a/.doc/audit/2026-09-24-cursor-hook-event-contract.md b/.doc/audit/2026-09-24-cursor-hook-event-contract.md index a4861735..12799dda 100644 --- a/.doc/audit/2026-09-24-cursor-hook-event-contract.md +++ b/.doc/audit/2026-09-24-cursor-hook-event-contract.md @@ -39,3 +39,11 @@ lesson_ids: [LESSON-20260924-cursor-hook-advisory-gates] ## 经验与复发 见 [LESSON-20260924-cursor-hook-advisory-gates](../ops/lessons/2026-09-24-cursor-hook-advisory-gates.md)。 + +## 2026-09-24 IDE 采样更新 + +- `postToolUse` 审计:`tool_name` / 审计字段为 `MCP:pi_subagent_roles`(带 `MCP:` 前缀)。 +- 修复:`_lib.normalize_tool_name` 剥前缀;回归 `test_ask_mcp_prefixed_tool_name_bad_sha`。 +- `pi_subagent_roles` IDE MCP 调用 PASS;`postToolUse` 收据 checklist 已注入。 +- `beforeMCPExecution` 完整 stdin 仍缺(Cursor 进程未设 `CURSOR_HOOKS_PROBE=1`);issue 保持 verifying。 +- `agentctl`/`jev` 未出现在 IDE MCP 缓存目录;进程可手动 stdio 启动;与路径修复独立,另记。 diff --git a/.doc/audit/ISSUES.md b/.doc/audit/ISSUES.md index 4746c28f..c9d0db5d 100644 --- a/.doc/audit/ISSUES.md +++ b/.doc/audit/ISSUES.md @@ -16,4 +16,4 @@ | ISSUE-20260924-cursor-test-rule-drift | Cursor / test-reports / 固定模型 / 规则副本 | Cursor 测试规则与验证契约存在固定模型及范围冲突 | medium | verifying | Cursor | 2026-09-24;规则文本已缩为验证契约指针;客户端实际读取行为待实机 | [主记录](2026-09-24-subagent-workflow-research.md#4-issue-20260924-cursor-test-rule-drift) | [LESSON-20260924-cursor-hook-advisory-gates](../ops/lessons/2026-09-24-cursor-hook-advisory-gates.md) | | ISSUE-20260924-pi-executor-boundaries | PI executor / 路径规范化 / MCP / 子进程 / 幂等 | 未发布初版只读边界与执行可靠性缺陷 | high | resolved | Cursor/Codex | 2026-09-24;173 工程回归与真实 Cursor CLI/MCP/PI | [主记录](2026-09-24-pi-executor-boundaries.md) | [工具路径与输入隔离](../ops/lessons/2026-09-24-pi-tool-boundaries.md) | | ISSUE-20260924-cursor-cli-mcp-path | Cursor CLI / MCP / workspaceFolder | 既有 agentctl/jev 项目 MCP 入口在本机 CLI 连接失败 | medium | verifying | Cursor | 2026-09-24;mcp.json 已改相对路径;IDE/CLI list 证据待补 | [主记录](2026-09-24-pi-executor-boundaries.md#cursor-cli-mcp-path) | [LESSON-20260924-cursor-hook-advisory-gates](../ops/lessons/2026-09-24-cursor-hook-advisory-gates.md) | -| ISSUE-20260924-cursor-hook-event-contract | Cursor / hooks / beforeMCPExecution / probe | hook 事件 stdin 字段名与 matcher 官方未文档化 | medium | open | Cursor | 2026-09-24;单元容错已落地;实机 probe 待采样 | [主记录](2026-09-24-cursor-hook-event-contract.md) | [LESSON-20260924-cursor-hook-advisory-gates](../ops/lessons/2026-09-24-cursor-hook-advisory-gates.md) | +| ISSUE-20260924-cursor-hook-event-contract | Cursor / hooks / beforeMCPExecution / probe | hook 事件 stdin 字段名与 matcher 官方未文档化 | medium | verifying | Cursor | 2026-09-24;IDE postToolUse tool_name=`MCP:pi_subagent_roles` 已采样;beforeMCP 全量 stdin 仍缺 | [主记录](2026-09-24-cursor-hook-event-contract.md) | [LESSON-20260924-cursor-hook-advisory-gates](../ops/lessons/2026-09-24-cursor-hook-advisory-gates.md) | diff --git a/.doc/test-reports/2026-09-24-cursor-governance-hooks-test-report.md b/.doc/test-reports/2026-09-24-cursor-governance-hooks-test-report.md index f119178d..a8b9b676 100644 --- a/.doc/test-reports/2026-09-24-cursor-governance-hooks-test-report.md +++ b/.doc/test-reports/2026-09-24-cursor-governance-hooks-test-report.md @@ -77,3 +77,15 @@ PI subagent 执行器已合入 local `dev`(`cfc80b34`)。本任务补齐 Cur 达到授权交付终点(工程治理层合入 local_dev)。后续:IDE probe 采样后关闭/推进 hook 契约 issue;CLI 批准 MCP 后复核 agentctl/jev;lesson 由 candidate → validated。 相关:任务 [2026-09-24-cursor-governance-hooks](../ops/short-term/2026-09-24-cursor-governance-hooks.md);ISSUE hook 契约 / 规则漂移 / MCP 路径。 + +## 八、IDE 实机复测(重启并批准 MCP 后) + +| 项 | 结果 | +|---|---| +| `pi_subagent_roles` | PASS(11 roles,`config_revision=subagent-models-2026-09-24-r1`) | +| `postToolUse` 注入 | PASS(收据 checklist;tool=`MCP:pi_subagent_roles`) | +| `codegraph_explore` | PASS(IDE ready) | +| `agentctl` / `jev` IDE | 未进入 IDE MCP 缓存(手动 stdio initialize 可通;另跟) | +| MCP: 前缀门禁漏拦 | 已发现并修复(见后续 commit) | + +证据:[ide-mcp-probe-2026-09-24.json](assets/2026-09-24-cursor-governance-hooks/ide-mcp-probe-2026-09-24.json) diff --git a/.doc/test-reports/assets/2026-09-24-cursor-governance-hooks/ide-mcp-probe-2026-09-24.json b/.doc/test-reports/assets/2026-09-24-cursor-governance-hooks/ide-mcp-probe-2026-09-24.json new file mode 100644 index 00000000..f7f200e7 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-24-cursor-governance-hooks/ide-mcp-probe-2026-09-24.json @@ -0,0 +1,58 @@ +{ + "ts": "2026-09-24T10:35:42.705540+00:00", + "ide_mcp": { + "pi-subagents": "ready", + "codegraph": "ready", + "agentctl": "not_in_ide_cache", + "jev": "not_in_ide_cache", + "note": "agentctl/jev stdio servers start manually (Content-Length framing) but absent from IDE mcps/ after approve of others" + }, + "pi_subagent_roles": { + "ok": true, + "config_revision": "subagent-models-2026-09-24-r1", + "roles_count": 11, + "postToolUse_injected": true + }, + "observed_hook_fields": { + "postToolUse.tool_name": "MCP:pi_subagent_roles", + "fix": "normalize_tool_name strips MCP: prefix in _lib.py" + }, + "audit_excerpt": [ + { + "hook": "audit-session", + "code": "SESSION_AUDIT", + "event": "stop", + "ts": "2026-09-24T09:34:42.285040+00:00" + }, + { + "hook": "audit-session", + "code": "SESSION_AUDIT", + "event": "stop", + "ts": "2026-09-24T10:28:18.049934+00:00" + }, + { + "hook": "audit-session", + "code": "SESSION_AUDIT", + "event": "stop", + "ts": "2026-09-24T10:28:22.657265+00:00" + }, + { + "hook": "audit-session", + "code": "SESSION_AUDIT", + "event": "stop", + "ts": "2026-09-24T10:30:53.486486+00:00" + }, + { + "hook": "pi-receipt-context", + "code": "CONTEXT_INJECT", + "tool": "MCP:pi_subagent_roles", + "ts": "2026-09-24T10:33:59.179726+00:00" + }, + { + "hook": "pi-receipt-context", + "code": "CONTEXT_INJECT", + "tool": "MCP:pi_subagent_roles", + "ts": "2026-09-24T10:34:23.849934+00:00" + } + ] +} diff --git a/.doc/test-reports/assets/2026-09-24-cursor-governance-hooks/probe-status.json b/.doc/test-reports/assets/2026-09-24-cursor-governance-hooks/probe-status.json index 59719abc..93193090 100644 --- a/.doc/test-reports/assets/2026-09-24-cursor-governance-hooks/probe-status.json +++ b/.doc/test-reports/assets/2026-09-24-cursor-governance-hooks/probe-status.json @@ -1,22 +1,7 @@ { - "status": "pending_ide_sample", - "how_to": "CURSOR_HOOKS_PROBE=1 then trigger pi_subagent_roles and any shell; collect .cursor/hook-probe.jsonl", - "unit_samples": [ - { - "event": "beforeShellExecution", - "synthetic_payload_keys": [ - "command" - ], - "note": "unit-driven; IDE probe pending" - }, - { - "event": "beforeMCPExecution", - "synthetic_payload_keys": [ - "mcp_server_name", - "tool_name", - "tool_input" - ], - "note": "unit-driven; IDE probe pending" - } - ] + "status": "partial_ide_sample", + "postToolUse_tool_name": "MCP:pi_subagent_roles", + "beforeMCP_ide_probe": "pending (CURSOR_HOOKS_PROBE not in Cursor process env)", + "shell_probe": "command field confirmed via manual CURSOR_HOOKS_PROBE=1", + "fix_applied": "normalize_tool_name for MCP: prefix" } diff --git a/scripts/tests/test_cursor_hooks.py b/scripts/tests/test_cursor_hooks.py index 6e01de5b..105017e8 100644 --- a/scripts/tests/test_cursor_hooks.py +++ b/scripts/tests/test_cursor_hooks.py @@ -224,6 +224,34 @@ class GuardPiDispatchTest(unittest.TestCase): } self.assertEqual(self._run(payload).get("permission"), "allow") + def test_ask_mcp_prefixed_tool_name_bad_sha(self): + # IDE postToolUse/beforeMCP observed tool_name="MCP:pi_subagent_*" + payload = { + "mcp_server_name": "pi-subagents", + "tool_name": "MCP:pi_subagent_run", + "tool_input": { + "worktree": "/tmp/wt", + "task_id": "t1", + "request": { + "role_id": "research", + "request_id": "research-001", + "goal": "g", + "authorization_ref": "a", + "source_sha": "HEAD", + "input_files": [], + "timeout_seconds": 30, + }, + }, + } + self.assertEqual(self._run(payload).get("permission"), "ask") + + def test_allow_mcp_prefixed_roles(self): + payload = { + "tool_name": "MCP:pi_subagent_roles", + "tool_input": {"worktree": "/tmp/wt", "task_id": "t1"}, + } + self.assertEqual(self._run(payload).get("permission"), "allow") + class GuardPiShellTest(unittest.TestCase): SCRIPT = HOOKS / "guard-pi-shell.sh" @@ -288,6 +316,16 @@ class PiReceiptContextTest(unittest.TestCase): self.assertIn("additional_context", parsed) self.assertIn("run_status", parsed["additional_context"]) + def test_injects_for_mcp_prefixed_tool_name(self): + code, parsed, err = _run_hook( + self.SCRIPT, + {"tool_name": "MCP:pi_subagent_roles", "mcp_server_name": "pi-subagents"}, + env=self.env, + cwd=self.root, + ) + self.assertEqual(code, 0, err) + self.assertIn("additional_context", parsed) + def test_noop_for_unrelated(self): code, parsed, err = _run_hook( self.SCRIPT,