Layer advisory Cursor hooks and skills on the PI executor, shrink the test-report rule to the validation contract, and fix MCP relative paths.
114 lines
4.3 KiB
Python
114 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
||
"""Advisory beforeMCPExecution gate for pi-subagents MCP tools."""
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
import _lib as lib # noqa: E402
|
||
|
||
HOOK = "guard-pi-dispatch"
|
||
|
||
|
||
def main() -> None:
|
||
raw = lib.read_stdin()
|
||
lib.maybe_probe(raw, HOOK)
|
||
data = lib.parse_json_loose(raw)
|
||
if data is None:
|
||
lib.audit(HOOK, "UNRECOGNIZED_INPUT", reason="non_json_or_empty")
|
||
lib.allow()
|
||
|
||
name = lib.tool_name(data)
|
||
server = lib.mcp_server(data)
|
||
if not name:
|
||
lib.audit(HOOK, "UNRECOGNIZED_INPUT", reason="missing_tool_name", keys=sorted(data.keys())[:40])
|
||
lib.allow()
|
||
|
||
if server and "pi-subagents" not in server and not name.startswith("pi_subagent_"):
|
||
if not name.startswith("pi_subagent_"):
|
||
lib.allow()
|
||
|
||
if name in ("pi_subagent_roles", "pi_subagent_result"):
|
||
lib.allow()
|
||
|
||
if name != "pi_subagent_run":
|
||
if name.startswith("pi_subagent_"):
|
||
lib.audit(HOOK, "UNRECOGNIZED_INPUT", reason="unknown_pi_tool", tool=name)
|
||
lib.allow()
|
||
|
||
args = lib.tool_input(data) or {}
|
||
req = args.get("request") if isinstance(args.get("request"), dict) else args
|
||
if not isinstance(req, dict):
|
||
lib.audit(HOOK, "UNRECOGNIZED_INPUT", reason="missing_request")
|
||
lib.ask(
|
||
"pi_subagent_run 缺少可识别的 request 对象。请核对 MCP 参数后再继续。",
|
||
"Hook: unrecognized request shape for pi_subagent_run.",
|
||
)
|
||
|
||
role = req.get("role_id")
|
||
request_id = req.get("request_id")
|
||
goal = req.get("goal")
|
||
auth = req.get("authorization_ref")
|
||
source_sha = req.get("source_sha")
|
||
input_files = req.get("input_files")
|
||
timeout = req.get("timeout_seconds")
|
||
|
||
problems: list[str] = []
|
||
|
||
if not role or not isinstance(role, str):
|
||
problems.append("role_id 缺失")
|
||
elif role not in lib.KNOWN_ROLES:
|
||
problems.append(f"role_id={role!r} 不在已知角色表")
|
||
|
||
if not request_id or not isinstance(request_id, str) or not lib.REQUEST_ID.match(request_id):
|
||
problems.append("request_id 非法(需字母数字开头,长度≤128)")
|
||
|
||
if not goal or not isinstance(goal, str) or not goal.strip():
|
||
problems.append("goal 为空")
|
||
|
||
if not auth or not isinstance(auth, str) or not auth.strip():
|
||
problems.append("authorization_ref 为空")
|
||
|
||
if not isinstance(source_sha, str) or not lib.FULL_SHA.match(source_sha):
|
||
problems.append("source_sha 必须是完整 40-hex 提交(禁止 HEAD/分支名/短 hash)")
|
||
|
||
if not isinstance(input_files, list):
|
||
problems.append("input_files 必须是数组")
|
||
|
||
if not isinstance(timeout, int) or isinstance(timeout, bool) or timeout <= 0:
|
||
if isinstance(timeout, float) and timeout.is_integer() and timeout > 0:
|
||
pass
|
||
else:
|
||
problems.append("timeout_seconds 必须是正整数")
|
||
|
||
if isinstance(role, str) and role in lib.TEST_ROLES:
|
||
cmds = req.get("approved_commands")
|
||
digest = req.get("plan_digest")
|
||
if not isinstance(cmds, list) or not cmds:
|
||
problems.append("test 角色需要 approved_commands(argv 数组的数组)")
|
||
elif any(not isinstance(c, list) or not all(isinstance(x, str) for x in c) for c in cmds):
|
||
problems.append("approved_commands 形状错误(应为字符串数组的数组)")
|
||
if not isinstance(digest, str) or len(digest) != 64 or any(
|
||
c not in "0123456789abcdefABCDEF" for c in digest
|
||
):
|
||
problems.append("plan_digest 必须是 64-hex SHA-256")
|
||
|
||
if "model_override" in req and req.get("model_override") is not None:
|
||
problems.append("出现 model_override(禁止静默换模型;请用任务 override)")
|
||
|
||
if problems:
|
||
detail = ";".join(problems)
|
||
lib.audit(HOOK, "SHAPE_ASK", tool=name, problems=problems, role=role, request_id=request_id)
|
||
lib.ask(
|
||
f"pi_subagent_run 形状检查未通过:{detail}。执行器仍会硬校验;确认后继续或修正参数。",
|
||
f"Hook advisory: {detail}",
|
||
)
|
||
|
||
lib.audit(HOOK, "ALLOW", tool=name, role=role, request_id=request_id)
|
||
lib.allow()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|