Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
199
skills/skill-creator/scripts/test_package_skill.py
Normal file
199
skills/skill-creator/scripts/test_package_skill.py
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/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!")
|
||||
original_quick_validate = sys.modules.get("quick_validate")
|
||||
sys.modules["quick_validate"] = fake_quick_validate
|
||||
|
||||
import package_skill as package_skill_module
|
||||
|
||||
package_skill = package_skill_module.package_skill
|
||||
|
||||
if original_quick_validate is not None:
|
||||
sys.modules["quick_validate"] = original_quick_validate
|
||||
else:
|
||||
sys.modules.pop("quick_validate", None)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def test_archive_entry_order_is_deterministic(self):
|
||||
skill_dir = self.create_skill("order-skill")
|
||||
# Files across multiple levels, created in non-sorted order, so the
|
||||
# filesystem/rglob enumeration order differs from a lexicographic sort.
|
||||
(skill_dir / "zeta.md").write_text("z\n")
|
||||
(skill_dir / "yankee.txt").write_text("y\n")
|
||||
alpha = skill_dir / "alpha"
|
||||
alpha.mkdir()
|
||||
(alpha / "delta.txt").write_text("d\n")
|
||||
(alpha / "bravo.txt").write_text("b\n")
|
||||
nested = skill_dir / "zlib"
|
||||
nested.mkdir()
|
||||
(nested / "november.txt").write_text("n\n")
|
||||
# "alpha-x.txt" discriminates entry-name ordering from Path-object
|
||||
# ordering: "-" (0x2d) sorts before "/" (0x2f) in the archive entry
|
||||
# name, but Path part-tuple ordering places it after the "alpha/" dir.
|
||||
(skill_dir / "alpha-x.txt").write_text("x\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 / "order-skill.skill"
|
||||
with zipfile.ZipFile(skill_file, "r") as archive:
|
||||
names = [name for name in archive.namelist() if not name.endswith("/")]
|
||||
# Entries must be ordered by their archive entry name, regardless of
|
||||
# filesystem enumeration or OS path-flavour, so archives are reproducible.
|
||||
self.assertEqual(names, sorted(names))
|
||||
# Lock the entry-name contract: "alpha-x.txt" precedes "alpha/bravo.txt"
|
||||
# (Path-object sorting would invert these).
|
||||
self.assertLess(
|
||||
names.index("order-skill/alpha-x.txt"),
|
||||
names.index("order-skill/alpha/bravo.txt"),
|
||||
)
|
||||
# Ensure the fixture actually spans multiple directories/files.
|
||||
self.assertIn("order-skill/zlib/november.txt", names)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user