package-final-workshop-skill.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
  1. """Package the confirmed Skill from its explicit public file manifest."""
  2. from pathlib import Path, PurePosixPath
  3. import hashlib
  4. import json
  5. import zipfile
  6. ROOT = Path(__file__).resolve().parents[1]
  7. SKILL = ROOT / 'skills/ksf-workshop-runtime'
  8. OUT = ROOT / 'docs/workshop-final-files-20261111'
  9. ARCHIVE = OUT / 'final-康师傅工作坊.skill'
  10. sha = lambda value: hashlib.sha256(value).hexdigest()
  11. def main():
  12. entry = (SKILL / 'SKILL.md').read_bytes()
  13. if entry != (SKILL / 'final-SKILL.md').read_bytes():
  14. raise ValueError('Run node scripts/build-workshop-skill.mjs before packaging')
  15. manifest = (SKILL / 'FILES.sha256').read_bytes()
  16. files = {}
  17. for line in manifest.decode('utf8').splitlines():
  18. digest, name = line.split(' ', 1)
  19. part = PurePosixPath(name)
  20. if part.is_absolute() or '..' in part.parts or '\\' in name or ':' in name or name in files:
  21. raise ValueError('Invalid manifest path')
  22. if any(term in name.lower() for term in ('credential-binding', 'credential-envelope', 'private.pem', 'channel.json', '.env')):
  23. raise ValueError('Private file is not a public Skill resource')
  24. data = (SKILL / name).read_bytes()
  25. if sha(data) != digest:
  26. raise ValueError('Manifest mismatch: ' + name)
  27. files[name] = data
  28. files['FILES.sha256'] = manifest
  29. OUT.mkdir(parents=True, exist_ok=True)
  30. temporary = ARCHIVE.with_suffix('.skill.tmp')
  31. with zipfile.ZipFile(temporary, 'w', zipfile.ZIP_DEFLATED) as archive:
  32. for name, data in sorted(files.items()):
  33. info = zipfile.ZipInfo('ksf-workshop-runtime/' + name, (2026, 11, 11, 0, 0, 0))
  34. info.external_attr = 0o100600 << 16
  35. archive.writestr(info, data, compress_type=zipfile.ZIP_DEFLATED)
  36. with zipfile.ZipFile(temporary) as archive:
  37. if archive.testzip() is not None or len(set(archive.namelist())) != len(files):
  38. raise ValueError('Archive verification failed')
  39. for name, data in files.items():
  40. if archive.read('ksf-workshop-runtime/' + name) != data:
  41. raise ValueError('Archive content mismatch: ' + name)
  42. temporary.replace(ARCHIVE)
  43. receipt = {
  44. 'ok': True, 'file': ARCHIVE.name, 'bytes': ARCHIVE.stat().st_size,
  45. 'sha256': sha(ARCHIVE.read_bytes()), 'publicFilesVerified': len(files),
  46. 'entrySha256': sha(entry), 'canonicalEntry': 'skills/ksf-workshop-runtime/final-SKILL.md',
  47. 'archiveEntry': 'ksf-workshop-runtime/SKILL.md',
  48. 'format': 'zip-compatible .skill', 'credentialsIncluded': False,
  49. 'deployed': False, 'installed': False,
  50. 'externalRuntimeDependenciesBundled': False,
  51. }
  52. (OUT / 'skill-package-receipt.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf8')
  53. print(json.dumps(receipt, ensure_ascii=False))
  54. if __name__ == '__main__':
  55. main()