package-unified-workshop-skill.py 4.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. """Build a strictly enumerated public Skill upgrade; never reads installed credentials."""
  2. from pathlib import Path
  3. import datetime
  4. import hashlib
  5. import json
  6. import zipfile
  7. ROOT = Path(__file__).resolve().parents[1]
  8. OUT = ROOT / 'docs/workshop-skill-release-20261108-r1'
  9. NAME = 'ksf-workshop-unified-20261108-r1'
  10. sha = lambda body: hashlib.sha256(body).hexdigest()
  11. def main():
  12. skill = ROOT / 'skills/ksf-workshop-runtime'
  13. candidate = {}
  14. files = {}
  15. for line in (skill / 'FILES.sha256').read_text(encoding='utf-8').splitlines():
  16. digest, name = line.split(' ', 1)
  17. assert name not in candidate and '..' not in Path(name).parts and not Path(name).is_absolute()
  18. raw = (skill / name).read_bytes()
  19. assert sha(raw) == digest
  20. candidate[name] = digest
  21. files['skill/' + name] = raw
  22. assert len(candidate) == 37
  23. raw = (skill / 'FILES.sha256').read_bytes()
  24. candidate['FILES.sha256'] = sha(raw)
  25. files['skill/FILES.sha256'] = raw
  26. received = ROOT / 'docs/workshop-skill-review-20261108/received-15ffc90f'
  27. export = json.loads((received / 'EXPORT.json').read_text(encoding='utf-8-sig'))
  28. baseline = {}
  29. for entry in export['files']:
  30. raw = (received / 'ksf-workshop-runtime' / entry['path']).read_bytes()
  31. assert sha(raw) == entry['sha256']
  32. baseline[entry['path']] = entry['sha256']
  33. assert len(baseline) == 14
  34. prior_path = ROOT / 'docs/agent-library-read-repair-20261108/receipts/r2/agent-library-read-receipt.json'
  35. prior = json.loads(prior_path.read_text(encoding='utf-8-sig'))
  36. assert prior['ok'] and prior['installedFilesUnchanged']
  37. assert prior['skillFingerprint'] == 'b4266efed78c19c0162aa2200cf0be83b0e7afa2473cef134459eb14c97ce69c'
  38. expected = {'schema': 'ksf.agent-library-read-expected.v1',
  39. 'generation': prior['contract']['generation'],
  40. 'sourceReceiptSha256': sha(prior_path.read_bytes()),
  41. 'skillFingerprint': candidate['SKILL.md'],
  42. 'installedFingerprints': prior['installedFingerprints']}
  43. release = {'schema': 'ksf.unified-workshop-release.v1', 'releaseId': NAME,
  44. 'createdAt': datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))).isoformat(),
  45. 'baseline': baseline, 'candidate': candidate,
  46. 'legacySkillSha256': prior['skillFingerprint'],
  47. 'runtimeReplacementAllowed': False, 'paidCallsAllowed': False,
  48. 'businessWritesDuringUpgrade': False}
  49. files.update({
  50. 'release.json': (json.dumps(release, ensure_ascii=False, indent=2) + '\n').encode(),
  51. 'expected-agent-read.json': (json.dumps(expected, ensure_ascii=False, indent=2) + '\n').encode(),
  52. 'upgrade-skill.mjs': (ROOT / 'scripts/upgrade-unified-workshop-skill.mjs').read_bytes(),
  53. 'probe-agent-library-read.py': (ROOT / 'scripts/probe-agent-library-read.py').read_bytes(),
  54. 'README.md': (OUT / '王刚001-升级与实机验收.md').read_bytes(),
  55. 'local-rehearsal-summary.json': (OUT / 'local-rehearsal-summary.json').read_bytes(),
  56. })
  57. for name in ('scope-request.json', 'keyword-request.json'):
  58. files[name] = (prior_path.parent / name).read_bytes()
  59. for name in files:
  60. assert not any(term in name.lower() for term in ('credential-binding', 'private.pem', 'envelope', 'channel-config'))
  61. files['FILES.sha256'] = ''.join(sha(raw) + ' ' + name + '\n' for name, raw in sorted(files.items())).encode()
  62. dest = OUT / (NAME + '.zip')
  63. assert not dest.exists(), 'Use a new revision; do not overwrite a released archive'
  64. with zipfile.ZipFile(dest, 'x', zipfile.ZIP_DEFLATED) as archive:
  65. for name, raw in sorted(files.items()):
  66. info = zipfile.ZipInfo(name, (2026, 11, 8, 0, 0, 0))
  67. info.external_attr = 0o100600 << 16
  68. archive.writestr(info, raw, compress_type=zipfile.ZIP_DEFLATED)
  69. with zipfile.ZipFile(dest) as archive:
  70. assert archive.testzip() is None
  71. for line in archive.read('FILES.sha256').decode().splitlines():
  72. digest, name = line.split(' ', 1)
  73. assert sha(archive.read(name)) == digest
  74. receipt = {'schema': 'ksf.unified-workshop-package-receipt.v1', 'ok': True,
  75. 'filename': dest.name, 'bytes': dest.stat().st_size, 'sha256': sha(dest.read_bytes()),
  76. 'entries': len(files), 'outerManifestMatched': len(files) - 1,
  77. 'skillManifestMatched': len(candidate) - 1, 'baselineFilesPinned': len(baseline),
  78. 'runtimeCodeReplaced': False, 'credentialsIncluded': False, 'deployed': False,
  79. 'agentLoaded': False, 'businessDatabaseWrites': 0, 'paidCalls': 0}
  80. (OUT / 'package-receipt.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  81. print(json.dumps(receipt, ensure_ascii=False))
  82. if __name__ == '__main__':
  83. main()