#!/usr/bin/env python3 """Install a verified BJJ skill package without changing agent settings. Python 3.10+ standard library only. Inspect this file before running it. Checksums detect changed files; trust in the manifest comes from its HTTPS host. """ import argparse from datetime import datetime, timezone import hashlib import ipaddress import json from pathlib import Path, PurePosixPath import re import shutil import sys import tempfile from urllib.parse import unquote, urljoin, urlsplit from urllib.request import HTTPRedirectHandler, Request, build_opener NAME = "bjj-tournament-brackets" VERSION_PATTERN = r"[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?" MAX_FILE_SIZE = 32 * 1024 * 1024 MAX_TOTAL_SIZE = 128 * 1024 * 1024 def check_url(url): """Permit HTTPS, plus HTTP on explicit loopback hosts for local previews.""" parsed = urlsplit(url) if not parsed.hostname or parsed.username or parsed.password: raise ValueError("Download URL must have a host and no embedded credentials") loopback = parsed.hostname.lower() == "localhost" try: loopback = loopback or ipaddress.ip_address(parsed.hostname).is_loopback except ValueError: pass if parsed.scheme != "https" and not (parsed.scheme == "http" and loopback): raise ValueError("Public downloads require HTTPS; HTTP is allowed only on loopback") class SafeRedirects(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): check_url(newurl) return super().redirect_request(req, fp, code, msg, headers, newurl) def fetch(location, limit): parsed = urlsplit(str(location)) if parsed.scheme: check_url(str(location)) request = Request(str(location), headers={"User-Agent": "bjj-skill-installer/1.0"}) with build_opener(SafeRedirects()).open(request, timeout=30) as response: check_url(response.geturl()) data = response.read(limit + 1) else: with Path(location).open("rb") as stream: data = stream.read(limit + 1) if len(data) > limit: raise ValueError("Download exceeds allowed size") return data def safe_path(value): if not isinstance(value, str) or not value or "\\" in value or ":" in value or "\x00" in value: raise ValueError("Invalid package path") path = PurePosixPath(value) if path.is_absolute() or any(part in ("", ".", "..") for part in value.split("/")): raise ValueError(f"Unsafe package path: {value}") return value def resource_location(manifest_location, resource): if not isinstance(resource, str) or not resource: raise ValueError("File URL must be a nonempty string") if urlsplit(str(manifest_location)).scheme: location = urljoin(str(manifest_location), resource) check_url(location) return location parsed = urlsplit(resource) if parsed.scheme: check_url(resource) return resource if parsed.netloc or parsed.query or parsed.fragment: raise ValueError("Local package URLs must be relative paths") relative = safe_path(unquote(parsed.path)) base = Path(manifest_location).absolute().parent.resolve() result = (base / relative).resolve() if not result.is_relative_to(base): raise ValueError("Local package URL escapes the manifest directory") return result def validate_skill(data): try: text = data.decode("utf-8") except UnicodeDecodeError as exc: raise ValueError("SKILL.md must use UTF-8") from exc lines = text.splitlines() if not lines or lines[0] != "---" or "---" not in lines[1:]: raise ValueError("SKILL.md must begin with YAML frontmatter") header = "\n".join(lines[1:lines.index("---", 1)]) name = re.search(r"^name:\s*([^\n]+)$", header, re.MULTILINE) description = re.search(r"^description:\s*(\S.*)$", header, re.MULTILINE) if not name or name.group(1).strip().strip("\"'") != NAME or not description: raise ValueError("SKILL.md frontmatter must declare the expected name and description") def verified_files(manifest_location): """Fetch and verify the entire package before creating any installation path.""" manifest = json.loads(fetch(manifest_location, 2 * 1024 * 1024)) if not isinstance(manifest, dict) or manifest.get("schema_version") != "1.0" or manifest.get("name") != NAME: raise ValueError("Unsupported manifest schema or skill name") version = manifest.get("version") if not isinstance(version, str) or not re.fullmatch(VERSION_PATTERN, version): raise ValueError("Invalid manifest version") entries = manifest.get("files") if not isinstance(entries, list) or not entries or len(entries) > 5000: raise ValueError("Manifest must list package files including SKILL.md") validated = [] paths = set() total_size = 0 for entry in entries: if not isinstance(entry, dict): raise ValueError("Invalid manifest file entry") path = safe_path(entry.get("path")) size = entry.get("size") checksum = entry.get("sha256") if path in paths or any(path.startswith(other + "/") or other.startswith(path + "/") for other in paths): raise ValueError("Duplicate or overlapping package paths") # Case insensitive systems must not alias two distinct files. if path.casefold() in {other.casefold() for other in paths}: raise ValueError("Case-colliding package paths") if type(size) is not int or not 0 <= size <= MAX_FILE_SIZE: raise ValueError("Invalid manifest file size") if not isinstance(checksum, str) or not re.fullmatch(r"[0-9a-f]{64}", checksum): raise ValueError("Invalid SHA-256 checksum") total_size += size if total_size > MAX_TOTAL_SIZE: raise ValueError("Package exceeds allowed total size") location = resource_location(manifest_location, entry.get("url")) validated.append((path, size, checksum, location)) paths.add(path) if "SKILL.md" not in paths: raise ValueError("Package must contain SKILL.md") files = {} for path, size, checksum, location in validated: data = fetch(location, MAX_FILE_SIZE) if len(data) != size or hashlib.sha256(data).hexdigest() != checksum: raise ValueError(f"Size or checksum mismatch: {path}") files[path] = data validate_skill(files["SKILL.md"]) return version, files def reject_symlinks(path): for ancestor in [path, *path.parents]: if ancestor.is_symlink(): raise ValueError(f"Symlink installation path is not allowed: {ancestor}") if ancestor.exists() and ancestor != path and not ancestor.is_dir(): raise ValueError(f"Installation ancestor is not a directory: {ancestor}") def identical(target, files): if not target.is_dir(): return False found = {} for path in target.rglob("*"): if path.is_symlink(): return False relative = path.relative_to(target) if "__pycache__" in relative.parts or path.suffix in {".pyc", ".pyo"}: continue if path.is_file(): found[relative.as_posix()] = path.read_bytes() return found == files def install(manifest, agent, scope, root=None, replace=False, dry_run=False): version, files = verified_files(manifest) root = Path(root).expanduser().absolute() if root is not None else (Path.home() if scope == "user" else Path.cwd()) agents = ("codex", "claude") if agent == "both" else (agent,) plans = [] for selected in agents: directory = ".agents" if selected == "codex" else ".claude" target = root / directory / "skills" / NAME reject_symlinks(target) unchanged = target.exists() and identical(target, files) if target.exists() and not unchanged and not replace: raise ValueError(f"Existing installation differs: {target}. Use --replace to retain a backup and replace it.") if target.exists() and not unchanged: reject_symlinks(target.parent.parent / "skill-backups") plans.append((target, unchanged)) if dry_run: for target, unchanged in plans: print(f"Verified {NAME} {version}: {'unchanged' if unchanged else 'would install'} {target}") return staged = [] try: for target, unchanged in plans: if unchanged: print(f"Already installed: {target}") continue reject_symlinks(target) target.parent.mkdir(parents=True, exist_ok=True) stage = Path(tempfile.mkdtemp(prefix=f".{NAME}.stage-", dir=target.parent)) staged.append((target, stage)) for relative, data in files.items(): destination = stage / relative destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes(data) for target, stage in staged: reject_symlinks(target) backup = None if target.exists(): stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ") backup = target.parent.parent / "skill-backups" / f"{NAME}-{stamp}" reject_symlinks(backup) backup.parent.mkdir(parents=True, exist_ok=True) target.rename(backup) try: stage.rename(target) except OSError: if backup is not None: backup.rename(target) raise print(f"Installed {NAME} {version}: {target}") if backup is not None: print(f"Previous installation retained: {backup}") finally: for _, stage in staged: if stage.exists(): shutil.rmtree(stage) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--manifest", required=True, help="HTTPS manifest URL, loopback HTTP URL, or local manifest path") parser.add_argument("--agent", choices=("codex", "claude", "both"), required=True) parser.add_argument("--scope", choices=("user", "project"), required=True) parser.add_argument("--root", help="Home directory for user scope, or repository directory for project scope") parser.add_argument("--replace", action="store_true", help="Keep a backup before replacing a different installation") parser.add_argument("--dry-run", action="store_true", help="Verify all downloads and destinations without installing") args = parser.parse_args() try: install(args.manifest, args.agent, args.scope, args.root, args.replace, args.dry_run) except (OSError, ValueError, TypeError, KeyError) as exc: print(f"Installation failed: {exc}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": sys.exit(main())