将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。 同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
147 lines
5.8 KiB
Python
147 lines
5.8 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 与 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",
|
||
)
|
||
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")
|
||
if any(token in text for token in FORBIDDEN_CLAUDE_RUNTIME_TOKENS):
|
||
offenders.append(path.relative_to(ROOT).as_posix())
|
||
continue
|
||
if re.search(
|
||
r"(?:/bin/claude|[\"']claude[\"']\s*,\s*[\"']--version|"
|
||
r"[\"']pi[\"']\s*,|\bpi\s+--model)",
|
||
text,
|
||
):
|
||
offenders.append(path.relative_to(ROOT).as_posix())
|
||
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()
|