#!/usr/bin/env python3 """Install the checked-out root Skill, or verify it, without another Skill.""" import argparse import copy from datetime import datetime, timezone import hashlib import json import os from pathlib import Path import re import shutil import subprocess import sys import tempfile import uuid if sys.version_info < (3, 12): raise SystemExit("Use Python 3.12+ to install the complete native workflow.") import tomllib def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest() def roster(root): files = {} for path in sorted(root.rglob("*")): relative = path.relative_to(root) if any(part.startswith(".") or part == "__pycache__" for part in relative.parts): continue if path.is_symlink(): raise ValueError(f"Source symlink is unsupported: {relative}") if path.is_file() and path.suffix != ".pyc": files[relative.as_posix()] = sha(path) return files def safe_target(path): path = Path(os.path.abspath(path.expanduser())) if any(item.is_symlink() for item in (path, *path.parents)): raise ValueError(f"Destination symlink is unsupported: {path}") return path def compiler_integrity(source): provenance = json.loads((source / "runtime/provenance.json").read_text()) root = source / "runtime/ppt-master" for item in provenance["files"]: path = root / item["path"] if not path.resolve().is_relative_to(root) or sha(path) != item["sha256"]: raise ValueError(f"Compiler integrity failed: {item['path']}") return len(provenance["files"]) def runtime_check(python, source): code = """ import importlib.metadata, json, pathlib, re, sys if sys.version_info < (3, 12): raise RuntimeError('Native runtime needs Python 3.12+') source = pathlib.Path(sys.argv[1]) versions = {} for line in (source / 'runtime/requirements.txt').read_text().splitlines(): if not line or line.startswith('#'): continue name, expected = line.split('==') actual = importlib.metadata.version(name) if actual != expected: raise RuntimeError(f'{name}: expected {expected}, got {actual}') versions[name] = actual sys.path.insert(0, str(source / 'scripts')) from native_export import load_compiler load_compiler() print(json.dumps({'python': sys.version.split()[0], 'packages': versions, 'compiler_loaded': True})) """ result = subprocess.run([str(python), "-c", code, str(source)], text=True, capture_output=True, check=True) return json.loads(result.stdout) def config_plan(codex, target): path = safe_target(codex / "config.toml") before = path.read_text(encoding="utf-8") if path.is_file() else "" data = tomllib.loads(before) entries = data.get("skills", {}).get("config", []) root_entry = str(target / "SKILL.md") nested = str(target / "runtime/ppt-master/SKILL.md") if any(item.get("path") == root_entry and item.get("enabled") is False for item in entries): raise ValueError("Root Skill is disabled in Codex; preserve that decision and resolve it explicitly.") matches = [item for item in entries if item.get("path") == nested] if matches: if len(matches) != 1 or matches[0].get("enabled") is not False: raise ValueError("Conflicting nested attribution entry; resolve this exact Codex entry explicitly.") return path, before, before addition = f"\n[[skills.config]]\npath = {json.dumps(nested, ensure_ascii=False)}\nenabled = false\n" after = before + addition parsed = tomllib.loads(after) expected = copy.deepcopy(data) expected.setdefault("skills", {}).setdefault("config", []).append({"path": nested, "enabled": False}) if parsed != expected: raise ValueError("Codex configuration change affected unrelated fields.") return path, before, after def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--dest", type=Path, required=True, help="Exact installed Skill directory") parser.add_argument("--codex-home", type=Path, help="Exclude only the nested attribution Skill") parser.add_argument("--venv", type=Path, help="Dedicated Python 3.12+ virtual environment") parser.add_argument("--prepare-runtime", action="store_true", help="Create venv and install pinned packages") parser.add_argument("--replace", action="store_true", help="Back up and replace an existing Skill") parser.add_argument("--backup-root", type=Path, help="Backup root outside Skill discovery directories") parser.add_argument("--check", action="store_true", help="Read-only payload, configuration and runtime check") args = parser.parse_args() source = Path(__file__).resolve().parents[1] target = safe_target(args.dest) if target == source or target.is_relative_to(source) or source.is_relative_to(target): raise ValueError("Source and installed destination must be separate directories.") if target.name != "ppt-template-workflow": raise ValueError("Destination must end in ppt-template-workflow.") version_match = re.search(r'^ version: "([0-9.]+)"$', (source / "SKILL.md").read_text(), re.M) if not version_match: raise ValueError("Missing root Skill version.") files = roster(source) compiler_files = compiler_integrity(source) if args.prepare_runtime and (args.venv is None or args.check): raise ValueError("--prepare-runtime requires --venv and cannot be combined with --check.") codex = safe_target(args.codex_home) if args.codex_home else None config = config_plan(codex, target) if codex else None if args.check: if not target.is_dir() or roster(target) != files: raise ValueError("Installed payload differs from the selected source.") compiler_integrity(target) if config and config[1] != config[2]: raise ValueError("Nested compiler attribution entry has not been disabled.") elif target.exists() and not args.replace: raise FileExistsError("Destination exists; an authorized upgrade needs --replace (with backup).") elif target.exists() and not target.is_dir(): raise ValueError("Existing destination is not a Skill directory.") runtime = None if args.venv: venv = safe_target(args.venv) if venv == target or venv.is_relative_to(target) or venv == source or venv.is_relative_to(source): raise ValueError("Runtime must be outside source and installed Skill.") python = safe_target(venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python")) if os.name == "nt" else venv / "bin/python" # POSIX venv interpreters are normally symlinks to their base Python. if args.prepare_runtime: if not python.is_file(): subprocess.run([sys.executable, "-m", "venv", str(venv)], check=True) subprocess.run([str(python), "-m", "pip", "install", "--disable-pip-version-check", "-r", str(source / "runtime/requirements.txt")], check=True) runtime = {"executable": str(python), **runtime_check(python, source)} commit = subprocess.run(["git", "-C", str(source), "rev-parse", "HEAD"], capture_output=True, text=True) dirty = subprocess.run(["git", "-C", str(source), "status", "--porcelain"], capture_output=True, text=True) if commit.returncode == 0 else None receipt = {"schema_version": 1, "skill": "ppt-template-workflow", "version": version_match[1], "source_commit": commit.stdout.strip() if commit.returncode == 0 else None, "source_dirty": bool(dirty.stdout) if dirty and dirty.returncode == 0 else None, "installed_path": str(target), "payload": files, "compiler_files": compiler_files, "runtime": runtime, "status": "native-ready" if runtime else "files-only", "codex_attribution_disabled": bool(config), "checked": args.check} if not args.check: stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "-" + uuid.uuid4().hex[:8] backup_root = safe_target(args.backup_root or ((codex / "backups/ppt-template-workflow") if codex else target.parent.parent / "skill-backups")) if (backup_root == target or backup_root.is_relative_to(target) or backup_root.is_relative_to(target.parent) or backup_root == source or backup_root.is_relative_to(source)): raise ValueError("Backup must be outside source and Skill discovery directory.") target.parent.mkdir(parents=True, exist_ok=True) stage = Path(tempfile.mkdtemp(prefix=".ppt-install-", dir=target.parent)) backup = backup_root / stamp / "ppt-template-workflow" moved = False config_written = False config_backup = None try: for relative in files: destination = stage / relative destination.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source / relative, destination) if roster(stage) != files: raise ValueError("Staged installation hash mismatch.") if target.exists() or (config and config[1] != config[2]): backup.parent.mkdir(parents=True, exist_ok=False, mode=0o700) if target.exists(): target.rename(backup) moved = True if config and config[1] != config[2]: path, before, after = config if (path.read_text(encoding="utf-8") if path.is_file() else "") != before: raise ValueError("Codex configuration changed during installation.") config_backup = backup.parent / "config.toml.before" config_backup.write_text(before, encoding="utf-8") config_backup.chmod(0o600) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(after, encoding="utf-8") if not before: path.chmod(0o600) config_written = True receipt.update(backup=str(backup) if moved else None, config_backup=str(config_backup) if config_backup else None, installed_at=datetime.now(timezone.utc).isoformat()) (stage / ".installation.json").write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") stage.rename(target) except Exception: if config_written: config[0].write_text(config[1], encoding="utf-8") if moved and not target.exists(): backup.rename(target) raise finally: if stage.exists(): shutil.rmtree(stage) summary = {key: value for key, value in receipt.items() if key != "payload"} summary["payload_files"] = len(files) print(json.dumps(summary, ensure_ascii=False, indent=2)) if __name__ == "__main__": if sys.version_info < (3, 12): raise SystemExit("Use Python 3.12+ to install the complete native workflow.") try: main() except (OSError, ValueError, subprocess.CalledProcessError) as error: raise SystemExit(f"Installation failed: {error}")