#!/usr/bin/env python3 """ Regression tests for skill packaging security behavior. """ import sys import tempfile import types import zipfile from pathlib import Path from unittest import TestCase, main from unittest.mock import patch SCRIPT_DIR = Path(__file__).resolve().parent if str(SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(SCRIPT_DIR)) fake_quick_validate = types.ModuleType("quick_validate") fake_quick_validate.validate_skill = lambda _path: (True, "Skill is valid!") sys.modules["quick_validate"] = fake_quick_validate import package_skill as package_skill_module from package_skill import package_skill class TestPackageSkillSecurity(TestCase): def setUp(self): self.temp_dir = Path(tempfile.mkdtemp(prefix="test_skill_")) def tearDown(self): import shutil if self.temp_dir.exists(): shutil.rmtree(self.temp_dir) def create_skill(self, name="test-skill"): skill_dir = self.temp_dir / name skill_dir.mkdir(parents=True, exist_ok=True) (skill_dir / "SKILL.md").write_text("---\nname: test-skill\ndescription: test\n---\n") (skill_dir / "script.py").write_text("print('ok')\n") return skill_dir def test_packages_normal_files(self): skill_dir = self.create_skill("normal-skill") out_dir = self.temp_dir / "out" out_dir.mkdir() result = package_skill(str(skill_dir), str(out_dir)) self.assertIsNotNone(result) skill_file = out_dir / "normal-skill.skill" self.assertTrue(skill_file.exists()) with zipfile.ZipFile(skill_file, "r") as archive: names = set(archive.namelist()) self.assertIn("normal-skill/SKILL.md", names) self.assertIn("normal-skill/script.py", names) def test_skips_symlink_to_external_file(self): skill_dir = self.create_skill("symlink-file-skill") outside = self.temp_dir / "outside-secret.txt" outside.write_text("super-secret\n") link = skill_dir / "loot.txt" out_dir = self.temp_dir / "out" out_dir.mkdir() try: link.symlink_to(outside) except (OSError, NotImplementedError): self.skipTest("symlink unsupported on this platform") result = package_skill(str(skill_dir), str(out_dir)) self.assertIsNotNone(result) skill_file = out_dir / "symlink-file-skill.skill" self.assertTrue(skill_file.exists()) with zipfile.ZipFile(skill_file, "r") as archive: names = set(archive.namelist()) self.assertIn("symlink-file-skill/SKILL.md", names) self.assertIn("symlink-file-skill/script.py", names) self.assertNotIn("symlink-file-skill/loot.txt", names) def test_skips_symlink_directory(self): skill_dir = self.create_skill("symlink-dir-skill") outside_dir = self.temp_dir / "outside" outside_dir.mkdir() (outside_dir / "secret.txt").write_text("secret\n") link = skill_dir / "docs" out_dir = self.temp_dir / "out" out_dir.mkdir() try: link.symlink_to(outside_dir, target_is_directory=True) except (OSError, NotImplementedError): self.skipTest("symlink unsupported on this platform") result = package_skill(str(skill_dir), str(out_dir)) self.assertIsNotNone(result) skill_file = out_dir / "symlink-dir-skill.skill" with zipfile.ZipFile(skill_file, "r") as archive: names = set(archive.namelist()) self.assertIn("symlink-dir-skill/SKILL.md", names) self.assertIn("symlink-dir-skill/script.py", names) self.assertNotIn("symlink-dir-skill/docs/secret.txt", names) def test_rejects_resolved_path_outside_skill_root(self): skill_dir = self.create_skill("escape-skill") out_dir = self.temp_dir / "out" out_dir.mkdir() original_within = package_skill_module._is_within def fake_is_within(path_obj: Path, root: Path): if path_obj.name == "script.py": return False return original_within(path_obj, root) with patch.object(package_skill_module, "_is_within", fake_is_within): result = package_skill(str(skill_dir), str(out_dir)) self.assertIsNone(result) def test_allows_nested_regular_files(self): skill_dir = self.create_skill("nested-skill") nested = skill_dir / "lib" / "helpers" nested.mkdir(parents=True, exist_ok=True) (nested / "util.py").write_text("def run():\n return 1\n") out_dir = self.temp_dir / "out" out_dir.mkdir() result = package_skill(str(skill_dir), str(out_dir)) self.assertIsNotNone(result) skill_file = out_dir / "nested-skill.skill" with zipfile.ZipFile(skill_file, "r") as archive: names = set(archive.namelist()) self.assertIn("nested-skill/lib/helpers/util.py", names) def test_skips_output_archive_when_output_dir_is_skill_dir(self): skill_dir = self.create_skill("self-output-skill") result = package_skill(str(skill_dir), str(skill_dir)) self.assertIsNotNone(result) skill_file = skill_dir / "self-output-skill.skill" self.assertTrue(skill_file.exists()) with zipfile.ZipFile(skill_file, "r") as archive: names = set(archive.namelist()) self.assertIn("self-output-skill/SKILL.md", names) self.assertIn("self-output-skill/script.py", names) self.assertNotIn("self-output-skill/self-output-skill.skill", names) if __name__ == "__main__": main()