"""SkillLoader v7 provenance + 危险能力告警单元测试""" import os import tempfile from unittest.mock import patch import yaml from agentkit.skills.base import Skill, SkillConfig from agentkit.skills.loader import SkillLoader from agentkit.skills.registry import SkillRegistry def _write_yaml(directory: str, filename: str, data: dict) -> str: path = os.path.join(directory, filename) with open(path, "w", encoding="utf-8") as f: yaml.dump(data, f, allow_unicode=True) return path class _FakeEntryPoint: """模拟 importlib.metadata.EntryPoint""" def __init__(self, name: str, skill: Skill): self.name = name self._skill = skill def load(self): return self._skill def _make_skill(name: str = "ep_skill", capabilities=None, tools=None) -> Skill: config = SkillConfig( name=name, agent_type="test", task_mode="llm_generate", prompt={"identity": "test"}, capabilities=capabilities, tools=tools, ) return Skill(config) class TestSkillLoaderProvenance: def test_load_from_file_sets_yaml_provenance(self): registry = SkillRegistry() loader = SkillLoader(skill_registry=registry) with tempfile.TemporaryDirectory() as tmpdir: path = _write_yaml( tmpdir, "s.yaml", { "name": "s", "agent_type": "t", "task_mode": "llm_generate", "prompt": {"identity": "x"}, }, ) skill = loader.load_from_file(path) assert skill.config.provenance == f"yaml:{path}" def test_load_from_skill_md_sets_provenance(self): registry = SkillRegistry() loader = SkillLoader(skill_registry=registry) skill_md = """\ --- name: md-skill description: "test" agent_type: test execution_mode: react --- # Trigger - test # Steps 1. step # Pitfalls - none # Verification - ok """ with tempfile.TemporaryDirectory() as tmpdir: path = os.path.join(tmpdir, "SKILL.md") with open(path, "w", encoding="utf-8") as f: f.write(skill_md) skill = loader.load_from_skill_md(path) assert skill.config.provenance == f"skill_md:{path}" def test_load_from_entry_points_sets_provenance(self): registry = SkillRegistry() loader = SkillLoader(skill_registry=registry) fake_ep = _FakeEntryPoint("my_ep", _make_skill("ep_skill")) with patch("agentkit.skills.loader.sys.version_info", (3, 12, 0)): with patch("importlib.metadata.entry_points", return_value=[fake_ep]): skills = loader.load_from_entry_points() assert len(skills) == 1 assert skills[0].config.provenance == "entry_point:my_ep" def test_entry_points_dangerous_capability_warning(self, caplog): """entry_points 加载声明 shell 能力的 Skill 时触发 warning""" import logging registry = SkillRegistry() loader = SkillLoader(skill_registry=registry) dangerous_skill = _make_skill( "dangerous_skill", capabilities=[{"tag": "shell"}, {"tag": "code_execution"}] ) fake_ep = _FakeEntryPoint("dangerous_ep", dangerous_skill) with patch("agentkit.skills.loader.sys.version_info", (3, 12, 0)): with patch("importlib.metadata.entry_points", return_value=[fake_ep]): with caplog.at_level(logging.WARNING): skills = loader.load_from_entry_points() assert len(skills) == 1 assert skills[0].config.provenance == "entry_point:dangerous_ep" # warning 包含 skill 名与危险能力 warnings = [r for r in caplog.records if r.levelno == logging.WARNING] assert any( "dangerous_skill" in r.getMessage() and "shell" in r.getMessage() for r in warnings ) def test_entry_points_dangerous_tools_warning(self, caplog): """entry_points 加载绑定 shell 工具但未声明 capabilities 的 Skill 时触发 warning""" import logging registry = SkillRegistry() loader = SkillLoader(skill_registry=registry) # 有危险 tools 但无 capabilities 声明——旧逻辑会漏检 dangerous_skill = _make_skill("stealthy_skill", capabilities=None, tools=["shell"]) fake_ep = _FakeEntryPoint("stealthy_ep", dangerous_skill) with patch("agentkit.skills.loader.sys.version_info", (3, 12, 0)): with patch("importlib.metadata.entry_points", return_value=[fake_ep]): with caplog.at_level(logging.WARNING): skills = loader.load_from_entry_points() assert len(skills) == 1 warnings = [r for r in caplog.records if r.levelno == logging.WARNING] assert any( "stealthy_skill" in r.getMessage() and "shell" in r.getMessage() for r in warnings ) def test_entry_points_no_capabilities_no_warning(self, caplog): import logging registry = SkillRegistry() loader = SkillLoader(skill_registry=registry) safe_skill = _make_skill("safe_skill", capabilities=None) fake_ep = _FakeEntryPoint("safe_ep", safe_skill) with patch("agentkit.skills.loader.sys.version_info", (3, 12, 0)): with patch("importlib.metadata.entry_points", return_value=[fake_ep]): with caplog.at_level(logging.WARNING): skills = loader.load_from_entry_points() assert len(skills) == 1 # 不应有危险能力 warning(只可能有其他 warning) dangerous_warnings = [ r for r in caplog.records if r.levelno == logging.WARNING and "dangerous capabilities" in r.getMessage() ] assert dangerous_warnings == [] def test_yaml_provenance_overridden_by_loader(self): """YAML 中已有 provenance 字段时,加载路径覆盖它(加载路径是权威来源)""" registry = SkillRegistry() loader = SkillLoader(skill_registry=registry) with tempfile.TemporaryDirectory() as tmpdir: path = _write_yaml( tmpdir, "s.yaml", { "name": "s", "agent_type": "t", "task_mode": "llm_generate", "prompt": {"identity": "x"}, "provenance": "user_supplied:should_be_overridden", }, ) skill = loader.load_from_file(path) assert skill.config.provenance == f"yaml:{path}" assert "user_supplied" not in skill.config.provenance