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