| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- """Split the verified handoff into packages with explicit recipients; no installation."""
- from pathlib import Path, PurePosixPath
- import hashlib
- import json
- import zipfile
- ROOT = Path(__file__).resolve().parent.parent
- SOURCE = ROOT / 'work/delivery-aibot-identity-20261102/final/ksf-aibot-identity-20261102.zip'
- EXPECTED_SOURCE = '375ba276e88b579887c89420ec6256c90e4afba79ee1393c46e85896c66984e4'
- DEST = ROOT / 'work/delivery-by-recipient-20261102'
- sha = lambda value: hashlib.sha256(value).hexdigest()
- assert sha(SOURCE.read_bytes()) == EXPECTED_SOURCE, 'SOURCE_HASH_MISMATCH'
- def verify_manifest(payloads, filename='FILES.sha256', prefix=''):
- rows = payloads[filename].decode('utf-8').strip().splitlines()
- for row in rows:
- fingerprint, name = row.split(' ', 1)
- assert not PurePosixPath(name).is_absolute() and '..' not in PurePosixPath(name).parts
- assert sha(payloads[prefix + name]) == fingerprint, 'MANIFEST_HASH_MISMATCH'
- return len(rows)
- with zipfile.ZipFile(SOURCE) as z:
- assert z.testzip() is None
- assert len(z.namelist()) == len(set(z.namelist())) == 24
- source = {name: z.read(name) for name in z.namelist()}
- assert verify_manifest(source) == 23
- def pack(name, recipient, payloads, manifest_count):
- assert 'FILES.sha256' not in payloads
- assert len(payloads) == manifest_count
- for filename, data in payloads.items():
- assert not PurePosixPath(filename).is_absolute() and '..' not in PurePosixPath(filename).parts
- assert not filename.endswith(('.pem', '.key', '.env'))
- assert 'credential-binding.json' not in filename
- assert b'-----BEGIN PRIVATE KEY-----' not in data
- payloads['FILES.sha256'] = ''.join(sha(payloads[x]) + ' ' + x + '\n' for x in sorted(payloads)).encode()
- DEST.mkdir(parents=True, exist_ok=True)
- target = DEST / (name + '.zip')
- # Reproducible archive written outside the final destination first.
- from io import BytesIO
- stream = BytesIO()
- with zipfile.ZipFile(stream, 'w', compression=zipfile.ZIP_DEFLATED) as z:
- for filename in sorted(payloads):
- z.writestr(zipfile.ZipInfo(filename, (2026, 11, 2, 0, 0, 0)), payloads[filename], compress_type=zipfile.ZIP_DEFLATED)
- packed = stream.getvalue()
- if target.exists():
- assert target.read_bytes() == packed, 'DELIVERY_ALREADY_EXISTS'
- else:
- with target.open('xb') as f:
- f.write(packed)
- with zipfile.ZipFile(target) as z:
- assert z.testzip() is None
- unpacked = {filename: z.read(filename) for filename in z.namelist()}
- assert unpacked == payloads
- assert verify_manifest(unpacked) == manifest_count
- if recipient == '王刚001':
- assert not any(x.startswith('server/') for x in unpacked)
- assert verify_manifest(unpacked, 'skills/ksf-workshop-runtime/FILES.sha256', 'skills/ksf-workshop-runtime/') == 9
- else:
- assert not any(x.startswith('skills/') for x in unpacked)
- assert not any('install-workshop-skill' in x for x in unpacked)
- result = dict(recipient=recipient, file=str(target), sizeBytes=len(packed), sha256=sha(packed),
- manifestEntries=manifest_count, zipIntegrity=True, manifestsVerified=True,
- derivedFromVerifiedSource=True, containsCredentials=False,
- installed=False, deployed=False, gatewayModified=False)
- target.with_suffix('.json').write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
- target.with_suffix('.txt').write_text(
- f'只发给:{recipient}\n附件:{name}.zip\n字节数:{len(packed)}\nSHA-256:{sha(packed)}\n\n'
- '请先按包内 INSTALL.txt 核对附件与清单、审读代码,返回结果。本轮只核验和准备,不执行安装、生产发布、重启或业务写入。\n', encoding='utf-8')
- return result
- skill_names = [name for name in source if name.startswith('skills/ksf-workshop-runtime/')]
- wang = {name: source[name] for name in skill_names + ['scripts/install-workshop-skill.mjs', 'checks/empty.json', 'templates/agent-self.json', 'templates/employee.json', 'templates/channel-context.example.json']}
- agent_template = json.loads(wang['templates/agent-self.json'])
- agent_template.update(name='王刚001', role='workshop')
- wang['templates/agent-self.json'] = (json.dumps(agent_template, ensure_ascii=False, indent=2) + '\n').encode()
- wang['INSTALL.txt'] = '''收件数字人:王刚001。此包仅用于升级你现有的 ksf-workshop-runtime,不给运维001安装。
- 本轮先核验与审读,不自动执行安装或业务;下面的安装命令供后续执行时使用。
- 1. 按同发执行单的字节数/SHA-256核对ZIP,在/opt/data下新建目录解压,不覆盖旧包。
- 2. 包根运行sha256sum -c FILES.sha256,预期16/16 OK;技能子清单9/9 OK。
- 3. 阅读scripts/install-workshop-skill.mjs、skills/ksf-workshop-runtime/SKILL.md和references/identity.md。
- 4. 返回清单核验结果、已安装Skill是否匹配已知升级基线,以及当前自身botid字段是否可从可信运行配置取得;不贴完整配置、bot secret或其他凭据。
- 后续安装命令:node scripts/install-workshop-skill.mjs。
- 安装器仅升级已知基线,备份旧文件,保留原入口、runtime-lock、加密绑定和员工/Agent档案;未知内容停止,重复安装零改动。安装后重新加载Skill,并在实际安装目录用本包技能子清单检查九文件。
- 自身模板固定名称为王刚001、role=workshop;accountId必须读取你实际订阅配置的botid,未知则null,不能填secret或运维001的botid。没有可信ID不声称完成自动识别。
- 本包没有后台发布工具,也没有Hermes适配器补丁。运维001负责让当前消息的botId/from.userid等元数据通过可信适配器或hook交付,负责后台两函数的发布准备。元数据未接入前不要自己生成文件冒充消息;姓名缺失可在接通后用员工待绑定标签,不要求员工重复填写账号。
- 本轮不创建员工、会话、样例或报告,不改网关、不重启,不重复已有G/H/K任务。后续真实消息验收需接入和后台能力均就绪。
- '''.encode()
- ops = {name: source[name] for name in source if name.startswith('server/')}
- ops['docs/wecom-aibot-identity-handoff.md'] = source['docs/wecom-aibot-identity-handoff.md'].replace(
- b'references/identity.md', b'docs/aibot-context-contract.md')
- ops['docs/aibot-context-contract.md'] = '''# 给运维001的消息身份交付约定
- Skill安装在王刚001,运维001不安装或复制其Skill、加密绑定及私钥。
- 当前接入为aibot WebSocket;按当前消息字段取senderId=body.from.userid、messageId=body.msgid、chatType=实际chattype、chatId=实际chatid(单聊可null)。botId来自处理该消息的机器人订阅配置,不是secret、corpId或应用agentId。
- 交付结构:
- {"version":"ksf.wecom-aibot-context.v1","botId":"本实例实际botid","senderId":"当前消息from.userid","chatType":"single","chatId":null,"messageId":"当前消息msgid"}
- 接入层/hook须保证元数据来自已经鉴别的当前消息,与当前执行bot和消息关联;不能接受用户正文替代,不能用全局变量或共享临时文件导致并发串人。若用文件,放/opt/data下,0600,逐消息隔离并提供失效机制,不含正文或凭据。JSON格式本身不证明来源,来源可信性须在接入层落实。
- 王刚001的Skill按botId+senderId生成稳定员工键;无姓名显示员工待绑定,企业未知为null。群chatid不作为员工ID,跨bot未经核实不自动合并,不能从相同ID前缀认定类型。实名、工号及跨bot关联需另行受控绑定。
- 后台只涉及ksfStartWorkshopSession与ksfGetIngestContract,新增channelIdentitySchema=ksf.wecom-aibot-sender.v1及作用域核对。账号/作用域保持私有,公开看板样式不改。本包没有现场适配器源码和可直接套用的适配器补丁。
- '''.encode()
- ops['INSTALL.txt'] = '''收件数字人:运维001。此包用于企微/Hermes接入准备与后台两函数发布准备,不安装到王刚001的Skill目录。
- 本轮仅只读核验和准备:
- 1. 按同发执行单核对ZIP字节数/SHA-256,在/opt/data新目录解压;包根sha256sum -c FILES.sha256,预期8/8 OK。
- 2. 阅读docs/wecom-aibot-identity-handoff.md及docs/aibot-context-contract.md。在隔离副本准备适配器/hook补丁,返回相关版本/文件指纹、具体差异、并发与过期处理、离线测试和回退方案。此包不含现场适配器补丁,不按报告行号盲改。
- 3. 审读server/scripts/deploy-identity.mjs和server/dist/identity-manifest.json。
- 4. 若现有受控管理连接可用,将KSF_CONNECTION_CONFIG指向既有配置路径,运行node server/scripts/deploy-identity.mjs做只读预检。预期readOnly=true、operation=read-only-plan、两函数、schemaChanges=permissionChanges=businessWrites=0。无配置如实报告,不从聊天接收明文凭据。
- 5. 返回核验结果与预检JSON/退出码。此轮不运行--apply,不改客户容器配置、不重启、不触发客户消息,也不修改静态看板。
- 后续正式部署时才运行node server/scripts/deploy-identity.mjs --apply,涉及两个共享后台Function.code的真实写入,有备份和回退;须等接入方案、安装顺序和生产发布安排明确后执行。Skill包单独交给王刚001,由王刚001核验和安装,运维001不替它登记自身身份或复制凭据。
- '''.encode()
- results = [pack('ksf-wanggang001-skill-20261102', '王刚001', wang, 16),
- pack('ksf-yunwei001-integration-20261102', '运维001', ops, 8)]
- (DEST / 'delivery-map.json').write_text(json.dumps(results, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
- print(json.dumps(results, ensure_ascii=False, indent=2))
|