package-workshop-research-update.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. """Public r4 payload; supports the two explicitly delivered full baseline states."""
  2. from pathlib import Path
  3. import argparse
  4. import hashlib
  5. import json
  6. import re
  7. import zipfile
  8. ROOT = Path(__file__).resolve().parents[1]
  9. OUT = ROOT / 'docs/workshop-skill-release-20261109-r4'
  10. NAME = 'ksf-workshop-skill-update-20261109-r4'
  11. sha = lambda b: hashlib.sha256(b).hexdigest()
  12. encode = lambda x: (json.dumps(x, ensure_ascii=False, indent=2) + '\n').encode()
  13. def stage():
  14. old_path = ROOT / 'docs/workshop-skill-release-20261108-r2/ksf-workshop-unified-20261108-r2.zip'
  15. third_path = ROOT / 'docs/workshop-skill-release-20261109-r3/ksf-workshop-skill-update-20261109-r3.zip'
  16. assert sha(old_path.read_bytes()) == '65660a366589d1b7a6c93b333e9ab1017a487e4b3f5c7399f225c38aedb9e844'
  17. assert sha(third_path.read_bytes()) == '83c44c8a2f16c682faa5f91f41f513c7d638839cedf43516f96a6cb9bec14828'
  18. with zipfile.ZipFile(old_path) as z:
  19. old = json.loads(z.read('release.json'))
  20. r2 = old['candidate']
  21. r2bytes = {n: z.read('skill/' + n) for n in r2}
  22. expected = json.loads(z.read('expected-agent-read.json'))
  23. with zipfile.ZipFile(third_path) as z:
  24. third = json.loads(z.read('release.json'))
  25. r3 = third['candidate']
  26. r3bytes = {n: z.read('payload/' + n) if r2.get(n) != h else r2bytes[n] for n, h in r3.items()}
  27. for version, mapping, bodies in [('r2', r2, r2bytes), ('r3', r3, r3bytes)]:
  28. for n, h in mapping.items():
  29. assert sha(bodies[n]) == h
  30. p = OUT / ('rehearsal-baseline-' + version) / n
  31. p.parent.mkdir(parents=True, exist_ok=True)
  32. p.write_bytes(bodies[n])
  33. skill = ROOT / 'skills/ksf-workshop-runtime'
  34. candidate = {}
  35. for line in (skill / 'FILES.sha256').read_text(encoding='utf-8').splitlines():
  36. h, n = line.split(' ', 1)
  37. assert '..' not in Path(n).parts and not Path(n).is_absolute() and n not in candidate
  38. assert sha((skill / n).read_bytes()) == h
  39. candidate[n] = h
  40. assert len(candidate) == 39
  41. candidate['FILES.sha256'] = sha((skill / 'FILES.sha256').read_bytes())
  42. assert candidate['SKILL.md'] == '85e44939bdbb3b55d512a4f5eb6daf9eac216634d1165ac3b576e9cc470176e8'
  43. deltas = {version: {n: h for n, h in candidate.items() if base.get(n) != h} for version, base in [('r2', r2), ('r3', r3)]}
  44. assert len(deltas['r2']) == 14 and len(deltas['r3']) == 12
  45. assert len(set(candidate) - set(r2)) == 2 and len(set(candidate) - set(r3)) == 1
  46. for base in (r2, r3):
  47. assert not set(base) - set(candidate)
  48. assert all(candidate[n] == h for n, h in base.items() if n.startswith('scripts/') or n == 'runtime-lock.json')
  49. expected['skillFingerprint'] = candidate['SKILL.md']
  50. expected['generationPolicy'] = 'Pin current contract generation; compare every read and board to it.'
  51. release = {'schema': 'ksf.workshop-research-update.v1', 'releaseId': NAME,
  52. 'baselines': {'r2': r2, 'r3': r3}, 'candidate': candidate,
  53. 'baselineArchiveSha256': {'r2': sha(old_path.read_bytes()), 'r3': sha(third_path.read_bytes())},
  54. 'researchEntryNames': [n for n in old['legacyManifestPolicy']['entryNames'] if n != 'SKILL.md'],
  55. 'researchRuntimePins': old['legacyManifestPolicy']['verifiedRuntimeEntries'],
  56. 'plans': {v: [{'path': n, 'action': 'update' if n in base else 'create', 'beforeSha256': base.get(n), 'afterSha256': h} for n, h in deltas[v].items()] for v, base in [('r2', r2), ('r3', r3)]},
  57. 'runtimeReplacementAllowed': False, 'businessWritesDuringUpgrade': False,
  58. 'serviceRestartAllowed': False, 'credentialsIncluded': False}
  59. # Link QA is offline and limited to relative file targets, including each anchor's file.
  60. links = 0
  61. for p in [skill / n for n in candidate if n.endswith('.md')]:
  62. for target in re.findall(r'\]\(([^)]+)\)', p.read_text(encoding='utf-8')):
  63. if '://' in target or target.startswith('#'):
  64. continue
  65. resolved = (p.parent / target.split('#', 1)[0]).resolve()
  66. assert resolved.is_relative_to(skill.resolve()) and resolved.is_file(), (p, target)
  67. links += 1
  68. refs = [n for n in candidate if n.startswith('references/')]
  69. integrity = {'ok': True, 'skillSha256': candidate['SKILL.md'], 'manifestSha256': candidate['FILES.sha256'],
  70. 'manifestEntries': 39, 'publicFiles': 40, 'relativeLinks': links,
  71. 'references': {'all': len(refs), 'md': sum(n.endswith('.md') for n in refs), 'json': sum(n.endswith('.json') for n in refs)},
  72. 'runtimeFilesUnchanged': 7, 'generationPolicy': 'same current research generation; external originals listed separately'}
  73. (OUT / 'skill-integrity.json').write_bytes(encode(integrity))
  74. files = {'payload/' + n: (skill / n).read_bytes() for n in deltas['r2']}
  75. files.update({'release.json': encode(release), 'expected-agent-read.json': encode(expected),
  76. 'update-skill.mjs': (ROOT / 'scripts/update-workshop-research.mjs').read_bytes(),
  77. 'run-update.py': (ROOT / 'scripts/run-workshop-research-update.py').read_bytes(),
  78. 'probe-agent-library-read.py': (ROOT / 'scripts/probe-agent-library-read.py').read_bytes(),
  79. 'README.md': (OUT / 'README.md').read_bytes(), 'CHANGES.md': (OUT / 'CHANGES.md').read_bytes(),
  80. 'skill-integrity.json': encode(integrity)})
  81. for n in ('local-acceptance.json', 'installer-validation.json'):
  82. p = OUT / n
  83. if p.exists():
  84. v = json.loads(p.read_bytes())
  85. assert v['ok']
  86. if n == 'local-acceptance.json':
  87. assert v['skillSha256'] == candidate['SKILL.md'] and v['manifestSha256'] == candidate['FILES.sha256']
  88. else:
  89. assert v['updaterSha256'] == sha(files['update-skill.mjs']) and v['runnerSha256'] == sha(files['run-update.py'])
  90. files[n] = p.read_bytes()
  91. files['FILES.sha256'] = ''.join(sha(raw) + ' ' + n + '\n' for n, raw in sorted(files.items())).encode()
  92. for n, raw in files.items():
  93. assert not any(t in n.lower() for t in ('credential-binding', 'private.pem', 'envelope', 'channel-config'))
  94. p = OUT / 'package' / n
  95. p.parent.mkdir(parents=True, exist_ok=True)
  96. p.write_bytes(raw)
  97. return files, release
  98. def main():
  99. parser = argparse.ArgumentParser()
  100. parser.add_argument('--pack', action='store_true')
  101. args = parser.parse_args()
  102. files, release = stage()
  103. if not args.pack:
  104. print(json.dumps({'ok': True, 'staged': len(files), 'plans': {v: len(p) for v, p in release['plans'].items()}}))
  105. return
  106. assert {'installer-validation.json', 'local-acceptance.json'} <= set(files)
  107. dest = OUT / (NAME + '.zip')
  108. assert not dest.exists(), 'Choose a new revision; never overwrite a released ZIP'
  109. with zipfile.ZipFile(dest, 'x', zipfile.ZIP_DEFLATED) as z:
  110. for n, raw in sorted(files.items()):
  111. info = zipfile.ZipInfo(n, (2026, 11, 9, 0, 0, 0))
  112. info.external_attr = 0o100600 << 16
  113. z.writestr(info, raw, compress_type=zipfile.ZIP_DEFLATED)
  114. with zipfile.ZipFile(dest) as z:
  115. assert z.testzip() is None and set(z.namelist()) == set(files)
  116. for n, raw in files.items():
  117. assert z.read(n) == raw
  118. receipt = {'ok': True, 'releaseId': NAME, 'filename': dest.name, 'bytes': dest.stat().st_size,
  119. 'sha256': sha(dest.read_bytes()), 'packageEntries': len(files), 'outerManifestEntries': len(files) - 1,
  120. 'payloadFiles': 14, 'targetPublicFiles': 40, 'plans': {'r2': {'updates': 12, 'creates': 2}, 'r3': {'updates': 11, 'creates': 1}},
  121. 'candidateSkillSha256': release['candidate']['SKILL.md'], 'candidateManifestSha256': release['candidate']['FILES.sha256'],
  122. 'runtimeFilesChanged': 0, 'credentialsIncluded': False, 'remoteInstalled': False, 'externalCollectionPerformed': False, 'paidCalls': 0}
  123. (OUT / 'package-receipt.json').write_bytes(encode(receipt))
  124. print(json.dumps(receipt, ensure_ascii=False, indent=2))
  125. if __name__ == '__main__':
  126. main()