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.
This commit is contained in:
+15
-1
@@ -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:
|
||||
|
||||
@@ -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;勿提交)
|
||||
|
||||
@@ -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 启动;与路径修复独立,另记。
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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)
|
||||
|
||||
+58
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user