176 lines
7.5 KiB
Python
176 lines
7.5 KiB
Python
#!/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`、`framework`、`muse` 和当前运行时入口;测试、humanization/tests/tools/eval 不在范围内。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import pathlib
|
||
import re
|
||
import unittest
|
||
|
||
def _find_project_root(start: pathlib.Path) -> pathlib.Path:
|
||
resolved = start.resolve()
|
||
for directory in (resolved, *resolved.parents):
|
||
if (directory / "AGENTS.md").is_file() and (directory / ".git").exists():
|
||
return directory
|
||
raise RuntimeError(f"无法从 {start} 向上找到项目根")
|
||
|
||
|
||
ROOT = _find_project_root(pathlib.Path(__file__))
|
||
SKILL_SCAN_ROOTS = (pathlib.Path(".agent") / "skills", pathlib.Path("muse"))
|
||
DASHBOARD_ROOTS = (pathlib.Path("muse") / "authority" / "studio" / "read",)
|
||
ACTIVE_RUNTIME_RELATIVE_ROOTS = (
|
||
pathlib.Path(".agent") / "skills",
|
||
pathlib.Path("framework"),
|
||
pathlib.Path("runtime"),
|
||
pathlib.Path("muse"),
|
||
pathlib.Path("muse") / "lifecycle" / "quality" / "harness",
|
||
pathlib.Path("muse") / "platform" / "llm",
|
||
pathlib.Path("muse") / "platform" / "embed",
|
||
pathlib.Path("docs") / "write-chapter",
|
||
)
|
||
# 框架派发接缝:允许直接调用 Agent 框架二进制的适配器文件。
|
||
# 新增框架适配器(codex/opencode…)必须在此登记,并保持独立于 Muse 业务。
|
||
FRAMEWORK_ADAPTER_ALLOWLIST = frozenset(
|
||
{"framework/adapters/pi/runner.py", "framework/adapters/dsh/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)
|
||
offenders = [
|
||
rel for rel in self._skill_files()
|
||
if not (owner and owner in pathlib.Path(rel).parts)
|
||
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 relative_root in DASHBOARD_ROOTS:
|
||
for path in (ROOT / relative_root).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 relative_root in ACTIVE_RUNTIME_RELATIVE_ROOTS:
|
||
root = ROOT / relative_root
|
||
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 path.suffix == ".py" and re.search(
|
||
r"(?:/bin/claude|[\"']claude[\"']\s*,\s*[\"']--version|"
|
||
r"[\"'](?:pi|dsh)[\"']\s*,\s*[\"']--(?:model|profile|version)|"
|
||
r"\b(?:pi|dsh)\s+--(?:model|profile|version))",
|
||
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 relative_root in ACTIVE_RUNTIME_RELATIVE_ROOTS:
|
||
root = ROOT / relative_root
|
||
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]:
|
||
files: set[str] = set()
|
||
for relative_root in SKILL_SCAN_ROOTS:
|
||
scan_root = ROOT / relative_root
|
||
for skill_md in scan_root.rglob("SKILL.md"):
|
||
for path in skill_md.parent.rglob("*.py"):
|
||
files.add(path.relative_to(ROOT).as_posix())
|
||
return sorted(files)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|