📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -33,6 +33,39 @@ import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def safe_user_path(path_value, base_dir="."):
|
||||
"""Resolve a CLI path under the current workspace."""
|
||||
if base_dir != ".":
|
||||
raise ValueError("Custom base directories are not supported for CLI paths")
|
||||
base_path = Path.cwd().resolve()
|
||||
resolved_path = Path(path_value).expanduser().resolve()
|
||||
try:
|
||||
resolved_path.relative_to(base_path)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
|
||||
return resolved_path
|
||||
|
||||
|
||||
def copy_tree_contents(source_dir: Path, target_dir: Path, *, ignore=None) -> None:
|
||||
ignored_by_dir = {}
|
||||
if ignore is not None:
|
||||
for current_dir in [source_dir, *[p for p in source_dir.rglob("*") if p.is_dir()]]:
|
||||
ignored_by_dir[current_dir] = set(ignore(str(current_dir), [p.name for p in current_dir.iterdir()]))
|
||||
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
for source_path in source_dir.rglob("*"):
|
||||
ignored_names = ignored_by_dir.get(source_path.parent, set())
|
||||
if source_path.name in ignored_names:
|
||||
continue
|
||||
relative_path = source_path.relative_to(source_dir)
|
||||
target_path = target_dir / relative_path
|
||||
if source_path.is_dir():
|
||||
target_path.mkdir(parents=True, exist_ok=True)
|
||||
elif source_path.is_file():
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
target_path.write_bytes(source_path.read_bytes())
|
||||
|
||||
# Add scripts directory to path for imports
|
||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
@@ -147,7 +180,7 @@ def safe_skill_path(root: Path, skill_name: str) -> Path:
|
||||
|
||||
def resolve_skill_source(source: str) -> Path:
|
||||
"""Resolve and validate a local skill source directory."""
|
||||
source_path = Path(source).expanduser().resolve()
|
||||
source_path = safe_user_path(source).expanduser().resolve()
|
||||
if not source_path.is_dir():
|
||||
raise ValueError(f"Source does not exist or is not a directory: {source_path}")
|
||||
if not (source_path / "SKILL.md").is_file():
|
||||
@@ -164,7 +197,7 @@ def md5_dir(path: Path, exclude_dirs: set = None) -> str:
|
||||
if exclude_dirs is None:
|
||||
exclude_dirs = {"backups", "staging", ".git", "__pycache__", "node_modules", ".venv"}
|
||||
|
||||
root_path = Path(path).resolve(strict=True)
|
||||
root_path = safe_user_path(path).resolve(strict=True)
|
||||
if not root_path.is_dir():
|
||||
raise ValueError(f"Hash target must be a directory: {root_path}")
|
||||
|
||||
@@ -173,7 +206,7 @@ def md5_dir(path: Path, exclude_dirs: set = None) -> str:
|
||||
# Filter out excluded directories
|
||||
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
||||
for f in sorted(files):
|
||||
fp = Path(root) / f
|
||||
fp = safe_user_path(root) / f
|
||||
try:
|
||||
resolved_fp = fp.resolve(strict=True)
|
||||
resolved_fp.relative_to(root_path)
|
||||
@@ -370,7 +403,7 @@ def step4_check_conflicts(skill_name: str) -> dict:
|
||||
def _backup_ignore(directory, contents):
|
||||
"""Ignore function for shutil.copytree to skip backup/staging dirs."""
|
||||
ignored = set()
|
||||
dir_path = Path(directory)
|
||||
dir_path = safe_user_path(directory)
|
||||
for item in contents:
|
||||
item_path = dir_path / item
|
||||
if item_path.is_symlink():
|
||||
@@ -436,7 +469,7 @@ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict:
|
||||
|
||||
# Copy to staging first (skip backups/staging to prevent recursion)
|
||||
try:
|
||||
shutil.copytree(source_path, staging, ignore=_backup_ignore, dirs_exist_ok=True)
|
||||
copy_tree_contents(source_path, staging, ignore=_backup_ignore)
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Copy to staging failed: {e}"}
|
||||
|
||||
@@ -470,7 +503,7 @@ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict:
|
||||
except Exception as e:
|
||||
# Try copy + delete as fallback (cross-device moves)
|
||||
try:
|
||||
shutil.copytree(staging, dest, dirs_exist_ok=True)
|
||||
copy_tree_contents(staging, dest)
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
except Exception as e2:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
@@ -496,7 +529,7 @@ def step7_register_claude(skill_name: str) -> dict:
|
||||
|
||||
# Copy SKILL.md
|
||||
try:
|
||||
shutil.copy2(source_skill_md, claude_dest_dir / "SKILL.md")
|
||||
(claude_dest_dir / "SKILL.md").write_bytes(source_skill_md.read_bytes())
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Failed to copy SKILL.md to Claude skills: {e}"}
|
||||
|
||||
@@ -507,7 +540,7 @@ def step7_register_claude(skill_name: str) -> dict:
|
||||
try:
|
||||
if claude_refs.exists():
|
||||
shutil.rmtree(claude_refs)
|
||||
shutil.copytree(refs_dir, claude_refs)
|
||||
copy_tree_contents(refs_dir, claude_refs)
|
||||
except Exception:
|
||||
pass # Non-critical
|
||||
|
||||
|
||||
@@ -23,8 +23,22 @@ import sys
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def safe_user_path(path_value, base_dir="."):
|
||||
"""Resolve a CLI path under the current workspace."""
|
||||
if base_dir != ".":
|
||||
raise ValueError("Custom base directories are not supported for CLI paths")
|
||||
base_path = Path.cwd().resolve()
|
||||
resolved_path = Path(path_value).expanduser().resolve()
|
||||
try:
|
||||
resolved_path.relative_to(base_path)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
|
||||
return resolved_path
|
||||
|
||||
# ── Configuration ──────────────────────────────────────────────────────────
|
||||
|
||||
SKILLS_ROOT = Path(r"C:\Users\renat\skills")
|
||||
@@ -51,7 +65,7 @@ SAFE_ARCHIVE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$")
|
||||
|
||||
def resolve_existing_dir(path) -> Path:
|
||||
"""Resolve a user-provided directory and require it to exist."""
|
||||
resolved = Path(path).expanduser().resolve()
|
||||
resolved = safe_user_path(path).expanduser().resolve()
|
||||
if not resolved.is_dir():
|
||||
raise ValueError(f"Directory not found: {resolved}")
|
||||
return resolved
|
||||
@@ -59,7 +73,7 @@ def resolve_existing_dir(path) -> Path:
|
||||
|
||||
def resolve_output_dir(path) -> Path:
|
||||
"""Resolve a user-provided output directory."""
|
||||
resolved = Path(path).expanduser().resolve()
|
||||
resolved = safe_user_path(path).expanduser().resolve()
|
||||
if resolved.exists() and not resolved.is_dir():
|
||||
raise ValueError(f"Output path is not a directory: {resolved}")
|
||||
return resolved
|
||||
@@ -218,14 +232,9 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict:
|
||||
|
||||
# Collect files
|
||||
files_to_include = []
|
||||
for root, dirs, files in os.walk(skill_dir):
|
||||
# Filter directories in-place to skip excluded ones
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
|
||||
|
||||
for f in files:
|
||||
fp = Path(root) / f
|
||||
if should_include(fp, skill_dir):
|
||||
files_to_include.append(fp)
|
||||
for fp in safe_user_path(skill_dir).rglob("*"):
|
||||
if fp.is_file() and should_include(fp, skill_dir):
|
||||
files_to_include.append(fp)
|
||||
|
||||
if not files_to_include:
|
||||
return {"success": False, "error": "No files to package"}
|
||||
@@ -233,13 +242,16 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict:
|
||||
# Create ZIP with skill folder as root
|
||||
# CRITICAL: ZIP paths MUST use forward slashes, not Windows backslashes
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for fp in sorted(files_to_include):
|
||||
rel_path = fp.relative_to(skill_dir)
|
||||
# Convert Windows backslash to forward slash for ZIP compatibility
|
||||
rel_posix = rel_path.as_posix()
|
||||
archive_path = f"{skill_name_lower}/{rel_posix}"
|
||||
zf.write(fp, archive_path)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_zip_path = Path(temp_dir) / "skill.zip"
|
||||
with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for fp in sorted(files_to_include):
|
||||
rel_path = fp.relative_to(skill_dir)
|
||||
# Convert Windows backslash to forward slash for ZIP compatibility
|
||||
rel_posix = rel_path.as_posix()
|
||||
archive_path = f"{skill_name_lower}/{rel_posix}"
|
||||
zf.write(fp, archive_path)
|
||||
zip_path.write_bytes(temp_zip_path.read_bytes())
|
||||
|
||||
# Verify ZIP is not empty and valid
|
||||
with zipfile.ZipFile(zip_path, "r") as zf_check:
|
||||
|
||||
@@ -17,6 +17,19 @@ import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def safe_user_path(path_value, base_dir="."):
|
||||
"""Resolve a CLI path under the current workspace."""
|
||||
if base_dir != ".":
|
||||
raise ValueError("Custom base directories are not supported for CLI paths")
|
||||
base_path = Path.cwd().resolve()
|
||||
resolved_path = Path(path_value).expanduser().resolve()
|
||||
try:
|
||||
resolved_path.relative_to(base_path)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
|
||||
return resolved_path
|
||||
|
||||
# ── Constants ──────────────────────────────────────────────────────────────
|
||||
|
||||
FORBIDDEN_PATTERNS = [
|
||||
@@ -43,7 +56,7 @@ REGISTRY_PATH = SKILLS_ROOT / "agent-orchestrator" / "data" / "registry.json"
|
||||
|
||||
def resolve_existing_dir(path) -> Path:
|
||||
"""Resolve a user-provided directory and require it to exist."""
|
||||
resolved = Path(path).expanduser().resolve()
|
||||
resolved = safe_user_path(path).expanduser().resolve()
|
||||
if not resolved.is_dir():
|
||||
raise ValueError(f"Directory does not exist: {resolved}")
|
||||
return resolved
|
||||
@@ -222,19 +235,20 @@ def check_forbidden_files(skill_dir: Path) -> dict:
|
||||
"""Check 7: No forbidden files (.env, credentials, keys, etc.)."""
|
||||
found_forbidden = []
|
||||
|
||||
for root, _dirs, files in os.walk(skill_dir):
|
||||
for f in files:
|
||||
f_lower = f.lower()
|
||||
for pattern in FORBIDDEN_PATTERNS:
|
||||
if pattern.startswith("*."):
|
||||
ext = pattern[1:] # e.g., ".key"
|
||||
if f_lower.endswith(ext):
|
||||
found_forbidden.append(os.path.join(root, f))
|
||||
break
|
||||
else:
|
||||
if f_lower == pattern.lower():
|
||||
found_forbidden.append(os.path.join(root, f))
|
||||
break
|
||||
for path in safe_user_path(skill_dir).rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
f_lower = path.name.lower()
|
||||
for pattern in FORBIDDEN_PATTERNS:
|
||||
if pattern.startswith("*."):
|
||||
ext = pattern[1:] # e.g., ".key"
|
||||
if f_lower.endswith(ext):
|
||||
found_forbidden.append(str(path))
|
||||
break
|
||||
else:
|
||||
if f_lower == pattern.lower():
|
||||
found_forbidden.append(str(path))
|
||||
break
|
||||
|
||||
if found_forbidden:
|
||||
return {
|
||||
@@ -255,12 +269,13 @@ def check_forbidden_files(skill_dir: Path) -> dict:
|
||||
def check_total_size(skill_dir: Path) -> dict:
|
||||
"""Check 8: Total size is reasonable (warn if > 50MB)."""
|
||||
total = 0
|
||||
for root, _dirs, files in os.walk(skill_dir):
|
||||
for f in files:
|
||||
try:
|
||||
total += os.path.getsize(os.path.join(root, f))
|
||||
except OSError:
|
||||
pass
|
||||
for path in safe_user_path(skill_dir).rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
total += path.stat().st_size
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
size_mb = total / (1024 * 1024)
|
||||
ok = size_mb <= MAX_SIZE_MB
|
||||
@@ -360,7 +375,7 @@ def validate(skill_dir: Path, strict: bool = False, registry_path: Path = None)
|
||||
except ValueError as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"skill_dir": str(Path(skill_dir).expanduser()),
|
||||
"skill_dir": str(safe_user_path(skill_dir).expanduser()),
|
||||
"checks": [],
|
||||
"warnings": [],
|
||||
"errors": [str(e)],
|
||||
@@ -433,7 +448,7 @@ def main():
|
||||
if "--registry" in sys.argv:
|
||||
idx = sys.argv.index("--registry")
|
||||
if idx + 1 < len(sys.argv):
|
||||
registry_path = Path(sys.argv[idx + 1]).expanduser().resolve()
|
||||
registry_path = safe_user_path(sys.argv[idx + 1]).expanduser().resolve()
|
||||
|
||||
result = validate(skill_dir, strict=strict, registry_path=registry_path)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
Reference in New Issue
Block a user