#!/usr/bin/env python3 """Build a deterministic HOLD booking-desk-event `.skill` archive from repository source.""" from __future__ import annotations import argparse import hashlib import json import sys import tempfile import zipfile from pathlib import Path ARCHIVE_ROOT = "booking-desk-event" FIXED_TIMESTAMP = (2026, 8, 19, 0, 0, 0) FIXED_TIMESTAMP_UTC = "2026-08-19T00:00:00Z" RELEASE = "booking-agent-skill-v2-rc4" PUBLICATION_STATUS = "HOLD" DISTRIBUTION = "INTERNAL_ONLY" ALLOWED_TOP_LEVEL = {"SKILL.md", "agents", "manifest.json", "schemas", "references", "fixtures"} REQUIRED_FILES = { "SKILL.md", "agents/openai.yaml", "manifest.json", "schemas/booking-business-agent-compact-input-v1.schema.json", "schemas/booking-business-agent-compact-decision-v1.schema.json", "fixtures/scenario-matrix.json", } def fail(message: str) -> None: raise ValueError(message) def sha256_bytes(value: bytes) -> str: return hashlib.sha256(value).hexdigest() def sha256(path: Path) -> str: return sha256_bytes(path.read_bytes()) def source_files(source: Path) -> list[Path]: if not source.is_dir(): fail(f"Skill source directory is missing: {source}") main_prompt = source / "prompts/main-prompt.md" if not main_prompt.is_file(): fail(f"Independent Main Prompt is missing: {main_prompt}") files = sorted( ( path for path in source.rglob("*") if path.is_file() and path.relative_to(source).parts[0] != "prompts" ), key=lambda path: path.relative_to(source).as_posix(), ) relative = {path.relative_to(source).as_posix() for path in files} missing = REQUIRED_FILES - relative if missing: fail(f"Skill source is missing required members: {', '.join(sorted(missing))}") forbidden = [ value for value in relative if value.split("/", 1)[0] not in ALLOWED_TOP_LEVEL ] if forbidden: fail(f"Skill source contains a forbidden top-level member: {forbidden[0]}") if any(path.name in {".DS_Store"} or "__pycache__" in path.parts for path in files): fail("Skill source contains an OS/cache member") return files def archive_member(source: Path, file_path: Path) -> str: return f"{ARCHIVE_ROOT}/{file_path.relative_to(source).as_posix()}" def write_archive(source: Path, output: Path) -> list[str]: files = source_files(source) output.parent.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: for file_path in files: info = zipfile.ZipInfo(archive_member(source, file_path), FIXED_TIMESTAMP) info.compress_type = zipfile.ZIP_DEFLATED info.external_attr = 0o100644 << 16 archive.writestr(info, file_path.read_bytes(), compress_type=zipfile.ZIP_DEFLATED, compresslevel=9) return [archive_member(source, path) for path in files] def validate_archive(source: Path, archive_path: Path) -> list[str]: expected_files = source_files(source) expected_members = [archive_member(source, path) for path in expected_files] if not archive_path.is_file(): fail(f"Archive is missing: {archive_path}") with zipfile.ZipFile(archive_path) as archive: if archive.namelist() != expected_members: fail("Archive member list does not exactly match the allowed source") if any("/prompts/" in member or member.endswith("/main-prompt.md") for member in archive.namelist()): fail("Main Prompt was packaged into the Skill") for source_file, member in zip(expected_files, expected_members): if archive.read(member) != source_file.read_bytes(): fail(f"Archive content differs from source: {member}") return expected_members def build_manifest(source: Path, archive: Path, members: list[str]) -> dict[str, object]: files = source_files(source) member_hashes = { archive_member(source, file_path): sha256_bytes(file_path.read_bytes()) for file_path in files } scenario_matrix = json.loads((source / "fixtures/scenario-matrix.json").read_text(encoding="utf-8")) source_manifest = json.loads((source / "manifest.json").read_text(encoding="utf-8")) if source_manifest.get("publication_status") != PUBLICATION_STATUS: fail("Editable manifest publication_status must be HOLD") if source_manifest.get("distribution") != DISTRIBUTION: fail("Editable manifest distribution must be INTERNAL_ONLY") if source_manifest.get("main_prompt_packaged") is not False: fail("Editable manifest must declare main_prompt_packaged=false") return { "manifest_schema_version": 1, "artifact": archive.name, "release": RELEASE, "skill_release_version": "Booking Agent Skill V2", "business_version": "booking-business-agent-v1.0", "input_contract": "booking-business-agent-compact-input-v1", "output_contract": "booking-business-agent-compact-decision-v1", "instruction_language": "zh-CN", "publication_status": PUBLICATION_STATUS, "distribution": DISTRIBUTION, "publication_hold_reason": ( "Booking Agent Skill V2 still requires publication to the bound SuperAgent Profile and one real EML revalidation." ), "main_prompt_packaged": False, "main_prompt_sha256": sha256(source / "prompts/main-prompt.md"), "scenario_count": scenario_matrix.get("scenario_count"), "sha256": sha256(archive), "member_count": len(members), "members": members, "member_sha256": member_hashes, "archive_timestamp_utc": FIXED_TIMESTAMP_UTC, } def render_manifest(manifest: dict[str, object]) -> bytes: return (json.dumps(manifest, ensure_ascii=False, indent=2) + "\n").encode("utf-8") def write_or_check_manifest(path: Path, manifest: dict[str, object], check: bool) -> None: expected = render_manifest(manifest) if check: if not path.is_file() or path.read_bytes() != expected: fail(f"Package manifest is stale or missing: {path}") return path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(expected) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--source", required=True, type=Path) parser.add_argument("--output", required=True, type=Path) parser.add_argument("--manifest", required=True, type=Path) parser.add_argument("--check", action="store_true") args = parser.parse_args() source = args.source.resolve() output = args.output.resolve() if args.check: with tempfile.TemporaryDirectory(prefix="booking-desk-event-package-") as directory: expected = Path(directory) / output.name write_archive(source, expected) if not output.is_file() or expected.read_bytes() != output.read_bytes(): fail("Archive is not reproducible from the editable source") else: write_archive(source, output) members = validate_archive(source, output) write_or_check_manifest(args.manifest.resolve(), build_manifest(source, output, members), args.check) print(f"Validated Skill archive: {output} ({len(members)} files, sha256={sha256(output)})") return 0 if __name__ == "__main__": try: raise SystemExit(main()) except (OSError, ValueError, json.JSONDecodeError, zipfile.BadZipFile) as error: print(f"Packaging failed: {error}", file=sys.stderr) raise SystemExit(1)