#!/usr/bin/env python3 """门禁:Skill 不得靠 sys.path 去别人的 scripts/ 或 humanization/src 拿实现;看板不得 import Skill。 被多个 Skill 或看板消费的实现装成顶层可安装包(muse-db / muse-llm / muse-embed / muse-deai),调用方 import 已安装的包。角色执行模块 muse_role 随 muse-llm 安装。 拥有该实现的 Skill 自己的 scripts/ 不受限(CLI 与库同属一个能力域)。 扫描范围只含 .agent/skills 与 dashboard;测试、humanization/tests/tools/eval 不在范围内。 """ from __future__ import annotations import pathlib import re import unittest ROOT = pathlib.Path(__file__).resolve().parents[2] SKILLS = ROOT / ".agent" / "skills" DASHBOARD = ROOT / "dashboard" ACTIVE_RUNTIME_ROOTS = ( ROOT / ".agent" / "skills", ROOT / "dashboard", ROOT / "harness", ROOT / "muse-llm", ROOT / "muse-embed", ROOT / "docs" / "write-chapter", ) # 框架派发接缝:唯一允许直接调用 Agent 框架二进制的适配器文件(dispatch-agent-task)。 # 新增框架适配器(codex/opencode…)必须挂在本 Skill scripts/ 下并在此登记。 FRAMEWORK_ADAPTER_ALLOWLIST = frozenset( {".agent/skills/dispatch-agent-task/scripts/pi_runner.py"} ) FORBIDDEN_CLAUDE_RUNTIME_TOKENS = ( "claude_runtime", "muse-claude-runtime", "execute-claude-task", "claudeExecutablePath", "claudeExecutableSha256", "claudeCliVersion", "CLAUDE_CODE_TMPDIR", "CLAUDE_CONFIG_DIR", "CLAUDE_CODE_OAUTH_TOKEN", ) # (禁止注入的路径, 提供替代实现的包, 豁免的 Skill 目录名) FORBIDDEN_PATHS = ( ("humanization/src", "muse-deai(import deai)", None), ("access-database/scripts", "muse-db(from muse_db import connect)", "access-database"), ("call-content-model/scripts", "muse-llm(import muse_llm)", "call-content-model"), ("embed-knowledge/scripts", "muse-embed(import muse_embed)", "embed-knowledge"), ("establish-voice-baseline/scripts", "muse-deai(deai.baseline / deai.load_db)", "establish-voice-baseline"), ) def _path_pattern(path: str) -> re.Pattern[str]: """同时匹配裸路径与 pathlib 拼接形态:``a/b`` 与 ``"a" / "b"``。""" head, tail = path.split("/") return re.compile(rf"""{re.escape(head)}(?:/|["']\s*/\s*["']){re.escape(tail)}""") class ImportBoundaryTest(unittest.TestCase): def test_skills_do_not_syspath_into_shared_implementations(self): for path, replacement, owner in FORBIDDEN_PATHS: with self.subTest(path=path): pattern = _path_pattern(path) prefix = f".agent/skills/{owner}/" if owner else None offenders = [ rel for rel in self._skill_files() if not (prefix and rel.startswith(prefix)) and pattern.search((ROOT / rel).read_text(encoding="utf-8")) ] self.assertEqual( offenders, [], f"Skill 必须消费 {replacement},不得 sys.path 指向 {path}:\n" + "\n".join(offenders), ) def test_dashboard_does_not_import_skills(self): offenders: list[str] = [] for path in DASHBOARD.rglob("*.py"): if path.name.startswith("test_"): continue text = path.read_text(encoding="utf-8") rel = str(path.relative_to(ROOT)) if re.search(r"sys\.path", text) and ".agent/skills" in text: offenders.append(rel) if re.search(r"^from db import|^import db\b", text, re.M): offenders.append(rel) if re.search(r"\b(muse_llm|muse_role|deai)\b", text): offenders.append(rel) self.assertEqual( offenders, [], "看板只读共享运行时包,不得 sys.path 注入 Skill 或 import db.py:\n" + "\n".join(offenders), ) def test_active_runtime_has_no_claude_cli_dependency(self): offenders: list[str] = [] suffixes = {".py", ".json", ".yaml", ".yml", ".toml", ".md"} for root in ACTIVE_RUNTIME_ROOTS: for path in root.rglob("*"): if ( not path.is_file() or path.suffix not in suffixes or "__pycache__" in path.parts or "artifacts" in path.parts ): continue text = path.read_text(encoding="utf-8") rel = path.relative_to(ROOT).as_posix() if any(token in text for token in FORBIDDEN_CLAUDE_RUNTIME_TOKENS): offenders.append(rel) continue # 框架适配器是被批准的唯一直接调用 Agent 框架二进制的位置(07 领域框架派发接缝); # 其余任何位置 shell 调模型/框架 CLI 仍被阻断。 if rel in FRAMEWORK_ADAPTER_ALLOWLIST: continue if re.search( r"(?:/bin/claude|[\"']claude[\"']\s*,\s*[\"']--version|" r"[\"']pi[\"']\s*,|\bpi\s+--model)", text, ): offenders.append(rel) self.assertEqual( sorted(set(offenders)), [], "活跃运行链不得 shell 调模型 CLI 或依赖旧 profile 字段:\n" + "\n".join(sorted(set(offenders))), ) def test_active_runtime_has_no_embedded_api_token(self): offenders: list[str] = [] token_pattern = re.compile(r"sk-[A-Za-z0-9]{20,}") for root in ACTIVE_RUNTIME_ROOTS: for path in root.rglob("*"): if not path.is_file() or "__pycache__" in path.parts or "artifacts" in path.parts: continue try: text = path.read_text(encoding="utf-8") except UnicodeDecodeError: continue if token_pattern.search(text): offenders.append(path.relative_to(ROOT).as_posix()) self.assertEqual( offenders, [], "活跃运行链不得内嵌 API token:\n" + "\n".join(offenders), ) def _skill_files(self) -> list[str]: return sorted(p.relative_to(ROOT).as_posix() for p in SKILLS.rglob("*.py")) if __name__ == "__main__": unittest.main()