任务:计划第 12、13 步缺的证据:凭据导入、钥匙丢失演练、PLM 构建存在核对、连接器只读冒烟
依据:
- 终审(
docs/hub-r4-progress.md211–212 行):第 12 步的演练没有记录,circle-hub secrets import没有实现,现在只有secret set(hub/server/cli-admin.ts50 行);第 13 步 PLM 的构建存在核对没有实现,plm:builds声明了(packs/system/plm/system.json127 行)却没有包读它;各连接器的只读冒烟没有记录。 - 简报“怎么验证”(
docs/hub-r4-brief.md72–73 行):在另一个数据目录里演练备份与恢复,包括钥匙串里的钥匙丢失后重新导入;切换失败回到上一个部署;重启后自动恢复;只监听本机;每个连接器一次只读冒烟,日志里只有条数和摘要。 - 所有者批准的计划(不在仓库里,集成者转述):导入只读环境配置文件里点名的键,只显示键名、有没有和长度,解析出错也不回显值;PLM 的“版本”就是构建,没有发布标记,只用来核对构建存在,记找到、没有或不唯一,从不挡评审;发布状态来自构建站。
- 之后的决定:Hub 没有模型密钥(决定 15,进度 258 行);SVN 账号存进 Hub 的密钥库(决定 28,进度 296 行)。
-
分层:平台不导入 records(
hub/platform/README.md176 行)。所以钥匙丢失分两半:钥匙串和备份那一半进rehearse.ts,密钥库和重新导入那一半写成 server 一侧的测试。 - 分支和工作树:
plan-evidence,目录circle-hub-plan-evidence(依赖已装好) - 可以改的路径:
hub/server/cli-admin.ts;hub/server/assemble/下的新文件env-file.ts、vault.ts的提示、cli-admin.test.ts、README(提示改了以后跟着变的断言照改);hub/platform/rehearse.ts、rehearse.test.ts、cli.ts里rehearse的输出、README.md;packs/system/plm/**(gold-trace 第 5 条也改这个包,先后和版本号由集成者定);packs/workflow/case-review/**(packs.test.ts除外;core-fixes 还改了这个包的pack.json、README.md、functions/expected-sources.ts,加了生成的词表,第 5 条等它合并以后在它之上做);新文件scripts/real/gold/plm-build.ts、scripts/real/smoke-connectors.ts和它们的测试;tests/acceptance/里因为多了一个要求项而变的断言。contracts/、hub/runs/engine/、hub/rules/、hub/connectors/、packs/product/famx/不动,缺什么先回报。 - 规则:同前(不推送、不建 PR、不发布、不部署;不碰 7420、钥匙串、环境配置文件和任何真实系统;测试只用合成数据;
git commit -s)。每条一个提交。要用钥匙串的地方只用临时钥匙串文件(建法照hub/platform/keychain.test.ts489–496 行),服务名用一次性的,不用部署的circle-hub;每个security调用都要点名这个文件(照hub/platform/test-support.ts33 行的 guard);不经平台的key命令(它用默认钥匙串)。合成构建名用 10.6 以后的版本线,地址用 192.0.2.0/24。取值只用contracts/generated的类型,包函数里用import type;运行时要整组取值的,照 core-fixes 给包生成functions/generated/vocabulary.ts的办法(它合并以后;登记要改tools/gen-contracts.ts,先回报)。 - 验证:
npx tsc --noEmit;node packs/workflow/case-review/packaging.ts check plm case-review;node --test --test-concurrency=1对hub/server/assemble/*.test.ts、hub/platform/rehearse.test.ts、hub/platform/cli.test.ts、packs/system/plm/**/*.test.ts、packs/workflow/case-review*/**/*.test.ts、scripts/real/**/*.test.ts;npm run test:acceptance。
要做的
secret import(加在hub/server/cli-admin.ts637 行的secret组里;计划写作secrets import,不另开复数命令组,README 写明这个对应):circle-hub secret import --env-file <文件> --key <键名>=<资源>/<凭据项> … [--basic <用户键>:<密码键>=<资源>/<凭据项>] [--dry-run]- 先要管理员令牌和密钥库主密钥(
--dry-run不要主密钥),再照secret set核对每个目标(646–651 行):资源和凭据项都在;同一个凭据项不能写两次;同一个键可以写进几个资源(门户账号同时给敏捷平台和 PLM)。这些都过了才读文件。 - 读文件放新文件
hub/server/assemble/env-file.ts,格式照scripts/real/lib/env-file.ts14–15 行(KEY = value、#注释、节标记):只认没有注释的行,只留点名的键的值,别的行读过就丢。不是普通文件、超过 64 KiB、不是 UTF-8、含 NUL、点名的键出现在两行,都整份拒绝;错误只写键名和行号,不写路径和行内容。 - 每个键一行:
<键名>:找到,长度 <n>或<键名>:没有。有一个没有就一个都不写,退出 1。都找到时按资源分组调hub.connectors.admin.setCredentials(hub/connectors/connectors.ts71 行:先核对全部值,最后清一次登录状态),每个凭据项一行,照secret set的说法写按什么规则存。 --basic存Basic base64(用户:密码):openkm 和 svn 的authorization要整个头值;用户名里有冒号就拒绝。--dry-run只核对,只报有没有和长度,不写。- 值、值的片段和摘要都不显示。
- 测试(
hub/server/assemble/cli-admin.test.ts,合成环境文件,断言抓到的输出;照 43–44、56–60 行把文件里所有的值都加进扫描):- 正例:两个键写进一个合成资源,密码按规则存成摘要,经桩服务读一次成功;一个键写进两个资源;
--basic的头值;--dry-run什么都不写。 - 反例:键不在文件里或只在注释行;资源或凭据项不存在、没有令牌、不是管理员、没有主密钥(都不读文件);同一凭据项两次;文件里别的键的值不出现在输出、密钥库和日志行里;坏文件(非 UTF-8、NUL、同名两行、超大)的错误里没有任何值的片段。
- 正例:两个键写进一个合成资源,密码按规则存成摘要,经桩服务读一次成功;一个键写进两个资源;
- 先要管理员令牌和密钥库主密钥(
- 演练里的钥匙丢失:钥匙串和备份那一半(
hub/platform/rehearse.ts232 行起)。没有/usr/bin/security时这一段在报告和输出里写“跳过,不算通过”,其余照旧。- 演练数据根里建临时钥匙串文件,用一次性服务名写两个项:密钥库主密钥和操作令牌(32 字节随机值),写在核对前的文件摘要之前。所有
security调用经一个只放行这个文件的包装;不读部署设置。 - 备份以后(259 行):备份目录里每个文件都查两个项的值(十六进制大小写两种写法,和原始 32 字节),每处命中记一条不一致,说明里不写值。
- 销毁时(262–265 行)两个项一起删掉,这就是钥匙丢失。恢复后的核对(278–286 行)之后:两个项都不在;照第 3 条的步骤把恢复回来的
vault/移到set-aside/<时间>-key-loss/,新建密钥库主密钥。 - 不论成败、加不加
--keep,finally里都删掉临时钥匙串文件。 - 报告多一段:项数、备份里的命中数、丢失后还在的项数、移开的密钥库文件数,或者跳过的原因;
hub/platform/cli.ts344 行一带多打一行,只有这些数。README 第 10 条(231 行)照改。 - 测试(
rehearse.test.ts,没有 libpq 或security就跳过并写原因):正常一次,命中 0,丢失后 0 个项在,每个security调用都点名根里的临时文件;反例:传进去的执行器把写入的值抄进根的vault/,备份命中至少 1 处,报告不通过,说明里没有值;security指向不存在的路径(选项可以给),这一段记跳过,其余照常通过。
- 演练数据根里建临时钥匙串文件,用一次性服务名写两个项:密钥库主密钥和操作令牌(32 字节随机值),写在核对前的文件摘要之前。所有
- 钥匙丢失以后重新导入:密钥库那一半(
cli-admin.test.ts新的 describe;同一个场景跑两遍:钥匙在文件里,各平台都跑;钥匙在临时钥匙串文件里,只在 macOS 跑,别处跳过并写原因)。- 建管理员,发布合成的敏捷平台包,加资源,
secret import,经桩服务读一次成功(登录 1 次)。 - 丢掉钥匙:删钥匙文件,或删临时钥匙串里的两个项。钥匙串那遍改变状态的命令先因为没有操作令牌被拒,提示
operator-token reissue;reissue 以后,两遍里要用密钥库的命令都被拒,提示照hub/server/assemble/vault.ts11 行,环境文件没被读;resource list照常列出。 - 恢复:把
<数据目录>/vault(hub/records/index.ts63 行)移到<数据目录>/set-aside/,新建钥匙,再secret import;读一次成功(登录 2 次),密钥库里是重新导入的值。钥匙串的 provider 按进程缓存钥匙(hub/platform/keychain.ts201 行),每次运行新建一个。 - 这套步骤写进
hub/platform/README.md292 行一带和hub/server/assemble/README.md的 Secrets 一段(117 行):停服务、移开vault/、key init、operator-token reissue、secret import、起服务。vault.ts11 行的提示加一句:找不回原来的钥匙串项时照这套步骤。
- 建管理员,发布合成的敏捷平台包,加资源,
- PLM 系统包按名字读构建列表(
packs/system/plm/system.json125–131 行)。plm:builds的键多一种写法:这条线的各个名字,各自 URI 编码后用+连接。改成规划函数(functions/plm.ts加buildsPlan、buildsProject,max_requests: 2):数字键照旧只读这个产品的构建列表;名字键先读产品列表,产品名整串先 NFKC(全角/也就成了/),再按/拆开,每段去首尾空白、转小写,含有每个名字的产品正好一个,才读它的构建列表,没有或不止一个就不读。依据:这条线的产品记录名字里列出两族名字(docs/hub-r4-architecture.md156 行;合成桩照这样写,hub/connectors/testing/plm.ts19 行)。- content:
{ product: {id, name} | null, product_resolution, builds: [{ name }] }。product_resolution用词表轴resolution的值(contracts/generated/vocabulary.ts12 行:已解析、有歧义、未解析);构建名取每项的name(合成夹具的形状,tests/fixtures/plm/index.ts43–49 行)。 - 升小版本(gold-trace 第 5 条也升这个包,版本号接在先合并的那件后面),改 README,
node packs/workflow/case-review/packaging.ts sign plm。 - 测试(
plm.test.ts,合成应答写在测试里,不改tests/fixtures/plm现有的值):数字键;名字解析到一个产品;一个也没有;两个产品都列出这些名字;构建列表为空。
- case-review 的 PLM 构建核对(等 core-fixes 合并以后,在它之上做)。
- 期望来源(
functions/expected.ts42 行一带):运行有锁定构建、有产品名时加一条plm:builds:<名字>+<名字>,角色 expected,理由写“核对锁定构建在 PLM 里有没有;只作记录,不挡评审,不作评审的根据”。 - 新要求项
plm_build:target run,fact,static,required: false(contracts/src/pack.ts145 行:只有门禁型运行要求必需项满足),bookkeeping。新检查plm_build:包内函数,reads: { roles: ["expected"], types: ["plm:builds"] }(照packs/workflow/traceability/checks.json9 行),applies run,subject 只取context.build,judges expected(先例packs/workflow/case-review-report/checks.json12 行),tier advisory。 - 判定:构建列表里和锁定构建同名(NFKC、去首尾空白后逐字相等)的条数。1 条支持;0 条反驳
case-review.plm_build_none;多于 1 条反驳case-review.plm_build_not_unique;data 写resolution(同上的轴)和条数,根据是那份来源的src:引用。没有锁定构建、PLM 没绑上或在unread_sources里、产品没解析出来,都记未知(缺输入),说明写是哪一种。不提发布状态。 - 码写进
tables/codes.json;样例:正例找到,反例没有、不唯一(testing/samples.ts write);SKILL.md 加一句:这一项 Hub 自己核对,模型不填,PLM 构建列表不是评审证据;README 写明这条记录要到第一次提交以后才有(评审包不在 start_run 时算事实);升小版本,接在 core-fixes 的 1.0.4 后面;packaging.ts sign case-review。 - 测试:检查函数和期望来源,上面每种情形一例;用
hub/server/testing-real.ts(真实的包和引擎、PLM 桩、临时 PostgreSQL)跑一次评审,提交以后这个要求项的结论和合成答案一致;tests/acceptance里因为多了一个要求项变的断言照实改,不放宽。 packs.test.ts不改,它的发布检查(55 行:每个检查都要有正例和反例)和词表扫描(98 行)要照样通过。引擎要是在评审运行里不给运行级事实要求项结论,停下回报,不改hub/runs/engine。
- 期望来源(
- 独立答案
scripts/real/gold/plm-build.ts(集成者跑)。--products <产品列表原件> --builds <构建列表原件> --names <名字>+<名字> --build <构建名> [--hub <run show 的 JSON>]:自己解析两份原始 JSON(产品列表带信封,构建列表是数组),按第 4、5 条写的规则自己数,得出产品的解析状态和同名条数;给了--hub就和这个要求项的 data 比。- 独立性:不导入
packs/和hub/;取值用contracts/generated/vocabulary.ts的RESOLUTION,那只是封闭的取值集。 - 屏幕上只有解析状态、条数和一致或不一致,不出现名字、构建名和产品。
- 测试:合成原件三种情形各一例,加一个和 Hub 不一致的反例。
resource read(hub/server/cli-admin.ts48 行的resource组):circle-hub resource read <资源> <读取名> <键>。- 读会存原件和读清单,所以要管理员令牌;调
hub.connectors.executeRead(connectors.ts92 行),目的写“管理员冒烟读取”。 - 只打印:系统包和读取名、完成或失败的码、每个产物的名字、sha256 和字节数、读清单的摘要。不打印键、内容、meta 和失败原因(原因在读清单和管理员记录里)。
- 测试(合成桩):成功的一次,输出里没有桩应答的任何文字,也没有键;失败的一次(404、登录失败)只有码;非管理员、没有的资源、没声明的读取名被拒。
- 读会存原件和读清单,所以要管理员令牌;调
- 冒烟脚本
scripts/real/smoke-connectors.ts(集成者跑)。--logs <部署的 logs 目录> --read <资源>:<读取名>:<键>,每个系统一条,可以重复;键是真实数据,只在运行时给。- 每条:记下开始时间和
hub.out.log、hub.err.log的长度;子进程跑node bin/circle-hub resource read …;结束后跑node bin/circle-hub logs export --from <开始> --to <结束>(调法照scripts/real/review-circle.ts149 行),再读出两个服务日志新增的部分;这次的产物用original export导进一个 0700 的临时目录,用完就删。 - 核对:从原件取出应答文字(JSON 的每个字符串值,文本按标签和换行切开的段;至少 8 个字符;去掉 64 位十六进制和请求的键里就有的串),在命令输出、调用日志和服务日志三处查,原样和 JSON 转义两种写法都查。读取完成、命中 0 才算通过。
- 每条只打一行:
<系统包>/<读取名>:读取完成|失败(码),产物 n 个共 m 字节,命令输出 a 行、调用日志 b 行、服务日志 c 行,命中 h:通过|不通过;命中只报条数和在哪一处,不报文字。全部通过才退出 0。 - 测试(跑命令的函数换成假的,不起真实 CLI):JSON 和 HTML 应答取文字;命中的正例和反例(原样、转义;请求的键回显不算);整段输出里没有合成应答的文字和键。
集成者之后跑
合并、发布、部署以后,在这台 Mac 上做,结果记进进度(只记计数和一致与否,不记路径):
- 导入凭据(部署的 Hub),先加
--dry-run跑一遍:circle-hub secret import --env-file <环境配置文件> --key <门户账号键>=<敏捷平台资源>/username --key <门户密码键>=<敏捷平台资源>/password --key <门户账号键>=<PLM 资源>/username --key <门户密码键>=<PLM 资源>/password --basic <SVN 账号键>:<SVN 密码键>=<SVN 资源>/authorization屏幕上只有键名、找到或没有、长度和写入行。不导入模型密钥。
- 第 12 步演练(演练数据根、本机回环):
circle-hub rehearse:行数、对象、权限和钥匙串那一行,核对一致;演练根的路径不记;node --test --test-concurrency=1 hub/server/assemble/cli-admin.test.ts:钥匙丢失两遍都通过,macOS 上跳过 0;node --test --test-concurrency=1 hub/platform/deploy-system.test.ts hub/platform/service.test.ts hub/platform/lifecycle.test.ts hub/config/config.test.ts tests/acceptance/17-hub-restart.test.ts:切换失败回滚(deploy-system.test.ts188 行)、重启恢复(service.test.ts17 行、场景 17)、只监听本机(数据库只有本机套接字,lifecycle.test.ts54 行;Hub 只收回环地址,config.test.ts125 行),记计数。这些测试里 launchctl 是假的:服务标签固定(hub/platform/settings.ts35 行),真跑会顶掉在用的服务,记录里照写;- 部署的 Hub 只读看一眼监听(
lsof -nP -iTCP -sTCP:LISTEN里 Hub 进程那几行):只有 127.0.0.1,只记地址和端口。
- PLM 构建核对:发布 plm 和 case-review 的新版本,
circle-hub resource update <PLM 资源> --system-pack plm;用所有者选的构建经真实客户端开一次评审、提交一次(第 15 步:HUB_REAL_TOKEN=<令牌> node scripts/real/review-circle.ts --endpoint-from circle --prompt <评审提示> --out <目录> --name <标签>,运行号取<标签>.calls.jsonl里start_run那一行),run show <运行> --json存到 Git 之外;resource read <PLM 资源> products all和resource read <PLM 资源> builds <名字>+<名字>,两份原件用original export导出;然后:node scripts/real/gold/plm-build.ts --products <文件> --builds <文件> --names <名字>+<名字> --build <构建名> --hub <run show 的 JSON>屏幕上只有解析状态、条数和一致或不一致。
- 冒烟,每个系统一条(敏捷平台、PLM、构建站、手册库、文档库、SVN);PLM 用
products:all,构建站用listing:index,其余挑一个便宜的读:node scripts/real/smoke-connectors.ts --logs <部署的 logs 目录> --read <资源>:<读取名>:<键> [--read …]每个系统一行通过或不通过。
完成后在终端里回报:提交号、测试数量、macOS 上跳过的测试(应为 0)和 Linux 上会跳过的、第 5 条引擎给没给运行级事实要求项的结论、没做完或拿不准的地方。
分两步做(集成者 2026-10-23)
- 现在做:
secret import、部署演练里钥匙丢失的情形、连接器只读冒烟脚本。 - 后做:PLM 构建存在核对(要改
packs/system/plm/**)。另一个终端(story-trace)正在改这个包,等它合并以后再做这一条;在那之前不动packs/system/plm/**和评审包里这一条的改动。
集成者对待定问题的答复(2026-10-23)
- 用现有的
secret组:secret import,不另开secrets组,也不加别名。进度页记下和计划写法的差别。 - 要
--basic:环境文件里 SVN 的账号和密码是分开存的(决定 28),由它拼成整个 Basic 头值存进密钥库,不显示任何值。 - PLM 里这条线的产品先按名字找;集成者冒烟以后看真实产品名,不对就改成在 PLM 资源设置里写产品编号。
- 构建名的字段由集成者冒烟后确认。
- “没有”和“不唯一”记成提示档的反驳,不挡评审,照页面。
- 核对放在评审包,核的是评审运行锁定的构建,照页面。
- 重新导入那一半写成服务端测试,照页面。另外做:
key init遇到非空的vault/就拒绝,并给出把它移开的选项。 - 真实服务上的切换失败回滚和重启恢复,由集成者放在下一次部署时演练。
- 冒烟改为核对命令输出(连接器事件)和服务日志,照页面。
第 44 项注:hub-variables 第一步合并以后的做法
这一件照现在的写,到时由 hub-variables 第 3 条改:
- 第 1 条的
secret import改成写到名字:--key <键名>=<密钥名>、--basic <用户键>:<密码键>=<密钥名>,加--digest <密钥名>=sha512_base64、--short <密钥名>和必填的--reason;“一个键写进两个资源”改成“两个资源引用同一个密钥”;--basic的核对改成核引用这个名字的各凭据项。 - 第 3 条的恢复步骤里,
secret import照到时的写法;导入之前secret list写“登记了第 n 版,密钥库里没有它的条目”。 - “集成者之后跑”第 1 条:hub-variables 第一步部署以前照现在的命令;部署以后先
resource update <资源> --credential <凭据项>=<名字>迁移原值,以后再导入就写进PORTAL_USERNAME、PORTAL_PASSWORD(--digest PORTAL_PASSWORD=sha512_base64)、SVN_AUTHORIZATION这些名字。