#!/usr/bin/env python3 """Generate and verify Agent OS bootstrap exports.""" from __future__ import annotations import argparse import fnmatch import json import shutil import stat import sys from pathlib import Path BOOTSTRAP_ROOT = Path(__file__).resolve().parent DEFAULT_OUTPUT = BOOTSTRAP_ROOT / "exports" / "generic" def load_json(path: Path) -> dict: with path.open("r", encoding="utf-8") as handle: return json.load(handle) def parse_mode(mode: str) -> int: return int(mode, 8) def ensure_safe_output(output: Path) -> Path: resolved = output.resolve() if resolved.parts[-3:] != ("bootstrap", "exports", "generic"): raise SystemExit( "refusing to write outside a bootstrap/exports/generic directory: " f"{resolved}" ) return resolved def copy_file(source: Path, target: Path, mode: str) -> None: if not source.is_file(): raise FileNotFoundError(f"missing source file: {source}") target.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(source, target) target.chmod(parse_mode(mode)) def clean_output(output: Path) -> None: ensure_safe_output(output) if output.exists(): shutil.rmtree(output) output.mkdir(parents=True, exist_ok=True) def create_directories(output: Path, manifest: dict) -> None: for directory in manifest["directories"]: target = output / directory["path"] target.mkdir(parents=True, exist_ok=True) if directory.get("keep"): keep_file = target / ".gitkeep" keep_file.touch() keep_file.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) def export_bundle(output: Path, manifest: dict, rules: dict) -> None: clean_output(output) create_directories(output, manifest) for item in rules["metadata"]: copy_file( BOOTSTRAP_ROOT / item["source"], output / item["target"], item.get("mode", "0644"), ) template_root = BOOTSTRAP_ROOT / rules["template_root"] for item in manifest["templates"]: copy_file( template_root / item["source"], output / item["target"], item.get("mode", "0644"), ) def verify_required(output: Path, rules: dict) -> list[str]: failures: list[str] = [] for item in rules["verify"]["required"]: target = output / item["path"] expected_type = item["type"] if expected_type == "file" and not target.is_file(): failures.append(f"missing file: {item['path']}") elif expected_type == "directory" and not target.is_dir(): failures.append(f"missing directory: {item['path']}") return failures def verify_forbidden(output: Path, rules: dict) -> list[str]: failures: list[str] = [] for path in output.rglob("*"): rel = path.relative_to(output).as_posix() for pattern in rules["verify"].get("forbidden", []): if fnmatch.fnmatch(rel, pattern): failures.append(f"forbidden path exported: {rel}") return failures def verify_export(output: Path, rules: dict) -> None: failures = verify_required(output, rules) + verify_forbidden(output, rules) if failures: for failure in failures: print(f"FAIL {failure}", file=sys.stderr) raise SystemExit(1) def count_files(output: Path) -> int: return sum(1 for path in output.rglob("*") if path.is_file()) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--output", type=Path, default=DEFAULT_OUTPUT, help="Export directory. Must end in bootstrap/exports/generic.", ) parser.add_argument( "--verify-only", action="store_true", help="Verify an existing export without regenerating it.", ) args = parser.parse_args() output = ensure_safe_output(args.output) manifest = load_json(BOOTSTRAP_ROOT / "bundle-manifest.json") rules = load_json(BOOTSTRAP_ROOT / "export-rules.json") if not args.verify_only: export_bundle(output, manifest, rules) verify_export(output, rules) print(f"OK {manifest['bundle']['id']} -> {output}") print(f"OK verified {len(rules['verify']['required'])} required paths") print(f"OK exported {count_files(output)} files") return 0 if __name__ == "__main__": raise SystemExit(main())