package-workshop-baseline-upgrade.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. """Seal a strict r6/r7/r8/r9 -> unchanged r10 platform upgrade, without external VOC installation."""
  2. import argparse
  3. import ast
  4. import hashlib
  5. import json
  6. from pathlib import Path
  7. import zipfile
  8. ROOT = Path(__file__).resolve().parents[1]
  9. RELEASE = 'ksf-workshop-r10-baseline-upgrade-20261110-r1'
  10. OUT = ROOT / 'docs' / RELEASE
  11. sha = lambda b: hashlib.sha256(b).hexdigest()
  12. encode = lambda v: (json.dumps(v, ensure_ascii=False, indent=2) + '\n').encode('utf-8')
  13. GUIDE = '''# 原工作坊Agent:已知历史基线升级到r10
  14. 采用“补齐已知完整基线”的方案。本包严格识别已封存的r6/r7/r8/r9,只更新原Skill公开说明,目标仍是手册已引用的r10;不绕过未知版本校验、不做任意覆盖。不安装或改动外部VOC,不改研究运行组件、业务脚本、身份、授权、连接或数据库。
  15. ## 固定资源
  16. 读取本说明同一固定Git提交目录的distribution.json,下载archive.path,核字节数及SHA-256。在独立新目录解包,逐项核FILES.sha256;不要覆盖现有技能目录。
  17. 完整r6是从先前封版r7更新包保留的42文件基线恢复并逐项验证。它与现场回传同样为:相对r10有19项一致、23项更新,缺报告设计文档1项。汇总特征相同不能证明实装相同,仍需下面plan逐文件指纹确认;任何混装或本地改动保持拒绝。具体基线与来源见release.json。
  18. ## 执行
  19. 在原Agent环境,以技能目录实际属主身份执行;结果目录须为新的、技能目录之外的私有目录。同一轮plan/apply/verify共用结果目录:
  20. ```sh
  21. python3 -B upgrade.py plan --target-dir /opt/data/skills/ksf-workshop-runtime --out /opt/data/ksf-r10-baseline-upgrade-20261110-results
  22. python3 -B upgrade.py apply --target-dir /opt/data/skills/ksf-workshop-runtime --out /opt/data/ksf-r10-baseline-upgrade-20261110-results
  23. python3 -B upgrade.py verify --target-dir /opt/data/skills/ksf-workshop-runtime --out /opt/data/ksf-r10-baseline-upgrade-20261110-results
  24. ```
  25. 仅plan成功后继续;命中完整r6时为24项变更(更新23、新增1)。已是r10时只核验,不重复替换。完整r7/r8/r9按自身差异计划执行。
  26. apply前先完整备份整个原Skill到结果目录before-full,校验备份及现场仍与plan一致;所有备份均保留本机,不上传或回传。只替换清单内变动说明及FILES.sha256;7个运行/锁文件必须保持原指纹,credential-binding.json和其他额外文件保留字节与权限。新增文件仅允许已声明且原本不存在的references文档;出现同路径未知文件停止。失败时恢复本次已替换文件,移除本次新增文档;遇并发改动停止覆盖,保留现场。
  27. 若plan仍失败,不用方案2强行覆盖。只读执行下列命令并回传JSON,范围仅43个公开文件的路径/指纹/缺项,不含身份文件或配置正文:
  28. ```sh
  29. python3 -B upgrade.py inventory --target-dir /opt/data/skills/ksf-workshop-runtime --out /opt/data/ksf-r10-baseline-upgrade-20261110-results
  30. ```
  31. ## 回传验收
  32. - plan识别的基线、实际变更/新增数,apply及verify结果,43文件全部一致。
  33. - 7运行文件、身份/授权/连接、额外文件及现有研究组件保持;旧研究目录SKILL.md仍退役。
  34. - 新任务实际发现并读取r10入口、研究闭环、主动澄清、报告设计与研究资料操作;磁盘通过与宿主加载分开反馈。
  35. - 原Agent默认研究contract仍为ssh-channel / parse-postgresql、实际当前代际;workshop-contract成功。
  36. - 零业务写入,不新建会话/报告/项目、不重跑迁移、不采集或发布新代际。
  37. 企微通讯录实名同步已由用户取消,本轮不配置、不验收。企业入场方式另按已确定的康师傅账套适配衔接,不改本次已封版r10的业务运行文件。线上手册已发布,维持原r10内容及安装资源地址,本包仅解决原Agent已知旧基线缺失问题。
  38. '''
  39. def stage():
  40. sealed = ROOT / 'docs/workshop-skill-release-20261109-r7'
  41. old_receipt = json.loads((sealed / 'package-receipt.json').read_bytes())
  42. old_zip = sealed / old_receipt['filename']
  43. assert sha(old_zip.read_bytes()) == old_receipt['sha256']
  44. legacy = json.loads((sealed / 'package/release.json').read_bytes())['baselines']['r6']
  45. assert len(legacy) == 42
  46. baseline_dir = sealed / 'rehearsal-baseline-r6'
  47. assert all(sha((baseline_dir / n).read_bytes()) == h for n, h in legacy.items())
  48. original = ROOT / 'docs/workshop-onboarding-20261109-r5/package'
  49. r10 = json.loads((original / 'release.json').read_bytes())['workshop']['files']
  50. assert len(r10) == 43
  51. baselines = {'r6': legacy}
  52. for version, folder in [('r7', 'r2'), ('r8', 'r3'), ('r9', 'r4')]:
  53. baselines[version] = json.loads((ROOT / ('docs/workshop-onboarding-20261109-' + folder + '/package/release.json')).read_bytes())['workshop']['files']
  54. protected = [n for n in r10 if n.startswith('scripts/') or n == 'runtime-lock.json']
  55. assert len(protected) == 7
  56. assert all(b[n] == r10[n] for b in baselines.values() for n in protected)
  57. package = {'skill/' + n: (original / 'skills/ksf-workshop-runtime' / n).read_bytes() for n in r10}
  58. assert all(sha(package['skill/' + n]) == h for n, h in r10.items())
  59. meta = {'schema': 'ksf.workshop-platform-upgrade.v1', 'releaseId': RELEASE,
  60. 'workshopVersion': 'r10', 'candidate': r10, 'baselines': baselines,
  61. 'protectedFiles': protected, 'knownR6BaselineProvenance': {
  62. 'archive': old_receipt['filename'], 'sha256': old_receipt['sha256'],
  63. 'baselineFilesVerified': 42, 'entrySha256': legacy['SKILL.md']},
  64. 'targetMatchesPublishedOnboardingR5': True, 'vocInstallationIncluded': False,
  65. 'databaseWrites': 0, 'credentialsIncluded': False}
  66. package.update({'release.json': encode(meta), 'README.md': GUIDE.encode('utf-8'),
  67. 'install-core.py': (ROOT / 'scripts/workshop-onboarding/install.py').read_bytes(),
  68. 'upgrade.py': (ROOT / 'scripts/workshop-onboarding/upgrade-platform.py').read_bytes()})
  69. for name in ['install-core.py', 'upgrade.py']:
  70. ast.parse(package[name].decode('utf-8-sig'), feature_version=(3, 9))
  71. package['FILES.sha256'] = ''.join(sha(b) + ' ' + n + '\n' for n, b in sorted(package.items())).encode('utf-8')
  72. for name, b in package.items():
  73. path = OUT / 'package' / name
  74. path.parent.mkdir(parents=True, exist_ok=True)
  75. path.write_bytes(b)
  76. return package, meta
  77. def main():
  78. parser = argparse.ArgumentParser(description=__doc__)
  79. parser.add_argument('--pack', action='store_true')
  80. args = parser.parse_args()
  81. archive = OUT / 'publish' / (RELEASE + '.zip')
  82. assert not archive.exists(), 'NEVER_OVERWRITE_RELEASE'
  83. package, meta = stage()
  84. if not args.pack:
  85. print(json.dumps({'ok': True, 'staged': len(package), 'knownR6Files': 42, 'r10Files': 43}))
  86. return
  87. validation = json.loads((OUT / 'validation.json').read_bytes())
  88. assert validation['ok'] and validation['upgraderSha256'] == sha(package['upgrade.py'])
  89. assert validation['coreSha256'] == sha(package['install-core.py'])
  90. archive.parent.mkdir(parents=True, exist_ok=True)
  91. with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED) as z:
  92. for name, b in sorted(package.items()):
  93. info = zipfile.ZipInfo(name, (2026, 11, 10, 0, 0, 0))
  94. info.external_attr = 0o100600 << 16
  95. z.writestr(info, b, compress_type=zipfile.ZIP_DEFLATED)
  96. with zipfile.ZipFile(archive) as z:
  97. assert z.testzip() is None and all(z.read(n) == b for n, b in package.items())
  98. distribution = {'schema': 'ksf.workshop-platform-upgrade-distribution.v1', 'releaseId': RELEASE,
  99. 'archive': {'path': archive.name, 'bytes': archive.stat().st_size, 'sha256': sha(archive.read_bytes())},
  100. 'workshopVersion': 'r10', 'entrySha256': meta['candidate']['SKILL.md'],
  101. 'supportedBaselines': list(meta['baselines']), 'targetFiles': 43,
  102. 'runtimeFilesModified': 0, 'vocModified': False, 'credentialsIncluded': False,
  103. 'productionUpdated': False}
  104. for name, data in [('distribution.json', encode(distribution)), ('README.md', package['README.md'])]:
  105. (archive.parent / name).write_bytes(data)
  106. (OUT / 'build-receipt.json').write_bytes(encode(distribution))
  107. print(json.dumps(distribution, ensure_ascii=False, indent=2))
  108. if __name__ == '__main__':
  109. main()