#!/usr/bin/env python3 """Install an AI Skills version from its manifest URL, without running its code.""" import argparse import hashlib import io import json import re import stat import sys import tempfile import urllib.parse import urllib.request import zipfile from pathlib import Path, PurePosixPath LIMIT = 24 * 1024 * 1024 def fetch(url, limit=LIMIT): parts = urllib.parse.urlsplit(url) if parts.scheme not in {"https", "http"} or not parts.netloc or parts.username or parts.password: raise ValueError("Use uma URL HTTP(S) sem credenciais") if parts.scheme == "http" and parts.hostname not in {"localhost", "127.0.0.1", "::1"}: raise ValueError("HTTP só é permitido para testes locais; use HTTPS") with urllib.request.urlopen(url, timeout=30) as response: final = urllib.parse.urlsplit(response.url) if final.scheme != parts.scheme or final.netloc != parts.netloc: raise ValueError("Redirecionamento para outra origem não é permitido") data = response.read(limit + 1) if len(data) > limit: raise ValueError("Resposta excede o limite de tamanho") return data def unpack(data, manifest, destination): skill_id = manifest["id"] if not isinstance(skill_id, str) or not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", skill_id) or len(skill_id) > 64: raise ValueError("Identificador inválido") if hashlib.sha256(data).hexdigest() != manifest["sha256"] or len(data) != manifest["size"]: raise ValueError("Checksum SHA-256 ou tamanho inválido") destination = Path(destination).expanduser() target = destination / skill_id if target.exists() or target.is_symlink(): raise ValueError(f"Destino já existe: {target}. Escolha outro diretório ou remova a versão anterior manualmente.") with zipfile.ZipFile(io.BytesIO(data)) as archive: entries = archive.infolist() if len(entries) > 2000 or sum(e.file_size for e in entries) > LIMIT: raise ValueError("Pacote excede os limites de extração") seen = set() for entry in entries: path = PurePosixPath(entry.filename) mode = entry.external_attr >> 16 if (not path.parts or path.is_absolute() or ".." in path.parts or "\\" in entry.filename or path.parts[0] != skill_id or len(path.parts) < 2 or any(p.startswith(".") or ":" in p for p in path.parts) or stat.S_IFMT(mode) not in (0, stat.S_IFREG, stat.S_IFDIR) or entry.filename.casefold() in seen): raise ValueError(f"Entrada insegura no ZIP: {entry.filename}") seen.add(entry.filename.casefold()) required = {f"{skill_id}/{name}" for name in ["SKILL.md", "skill.json", "LICENSE"]} if not required.issubset({e.filename for e in entries if not e.is_dir()}): raise ValueError("Pacote não contém os arquivos obrigatórios") metadata = json.loads(archive.read(f"{skill_id}/skill.json")) if metadata.get("id") != skill_id or metadata.get("version") != manifest["version"]: raise ValueError("Identidade do pacote diverge do manifesto") destination.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix=".aiskills-", dir=destination) as temporary: staging = Path(temporary) for entry in entries: path = staging.joinpath(*PurePosixPath(entry.filename).parts) if entry.is_dir(): path.mkdir(parents=True, exist_ok=True) else: path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(archive.read(entry)) (staging / skill_id / ".aiskills-install.json").write_text( json.dumps({"id": skill_id, "version": manifest["version"], "sha256": manifest["sha256"]}, indent=2) + "\n") # Reserve the destination atomically; never replace another installation. target.mkdir() try: for child in (staging / skill_id).iterdir(): child.rename(target / child.name) except Exception: # Keep partial files visible for diagnosis rather than remove user data. raise ValueError(f"Instalação interrompida; verifique {target}") return target def install(url, destination, expected_sha256=None): manifest = json.loads(fetch(url, 128 * 1024)) if expected_sha256 and manifest["sha256"] != expected_sha256.lower(): raise ValueError("Checksum do manifesto diverge do checksum informado") if manifest.get("archive") != "download.zip": raise ValueError("Nome de pacote inválido") # Status is mutable and lives outside the immutable version manifest. index_url = urllib.parse.urljoin(url, "../../index.json") index = json.loads(fetch(index_url, 2 * 1024 * 1024)) version = next((v for v in index["versions"] if v["version"] == manifest["version"]), None) if index.get("id") != manifest["id"] or not version or version.get("status") != "active": raise ValueError("Versão inexistente ou retirada do catálogo") if version["sha256"] != manifest["sha256"]: raise ValueError("Manifesto diverge do índice") return unpack(fetch(urllib.parse.urljoin(url, "download.zip")), manifest, destination) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("manifest_url", help="URL de versions/X.Y.Z/manifest.json") parser.add_argument("--destination", required=True, help="Diretório de skills configurado no seu agente") parser.add_argument("--sha256", help="Checksum obtido por um canal confiável (opcional)") args = parser.parse_args() try: target = install(args.manifest_url, args.destination, args.sha256) print(f"Skill instalada em {target}. Nenhum script foi executado.") except (ValueError, OSError, KeyError, StopIteration, zipfile.BadZipFile) as error: print(f"Erro: {error}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": sys.exit(main())