refactor(spec,cli,runtime)!: 退役 crypto.hash 能力 —— 声明四层、构建期还自动推断,沙箱从没实现 (#4391) - #4871
Conversation
…#4391) `crypto.hash` 由四层声明、零层实现:`HookBodyCapability` 枚举、枚举旁文档表、 CLI 提取器的构建期推断、`ScriptContext.crypto.hash` 签名 —— 而 `installCtx` 只 往 VM 的 `ctx.crypto` 装了 `randomUUID`,该 token 唯一授权的调用每次都在 VM 里抛。 构建期推断是危险的放大器:作者写下 `ctx.crypto.hash(...)`,提取器替他把能力加进 `capabilities`,`os build` 因此全绿,直到运行时才炸。维护者裁决 remove:从未实现、 调用即抛、零投诉 —— 对一个每次使用都抛错的能力,这本身就是最强的活性证据;在沙箱里 实现 crypto 会扩大能力面与安全审查面,无业务拉动不做。实现先行、声明随实现走。 同批删除四处声明,并注册 ADR-0087 D2 转换 `hook-body-crypto-hash-removed` (D3 挂 protocol-17):枚举值收窄不是 key 移除,故无 `retiredKey()` 墓碑,处方由 枚举 error map 按 `issue.input` 承载(`object.managedBy: 'system'` 先例)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9
…pto-hash-retire # Conflicts: # docs/protocol-upgrade-guide.md # packages/spec/spec-changes.json # packages/spec/src/migrations/registry.ts
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 package(s): 118 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…pto-hash-retire # Conflicts: # docs/protocol-upgrade-guide.md # packages/spec/spec-changes.json
第二次同步 main 完成(入队前)
冲突与处置:只有两个生成物冲突(
逐条核对(按 PM 清单)① 本单 D2/D3 与在飞条目并存 main 侧新增的 ② #4616 / #4657 的删除未被复活(带引号精确名,避开
对照组(证明 grep 有效、不是空跑): ③ 四张 ratchet 相对 main 零差异(用本单自己的结论做自检) 同时确认改动确实在(零差异不是因为把自己的改动弄丢了): 重跑读数spec 用例数 7364 → 7365,增量来自 main 侧 #4861;本单新增的 9 条 pin(spec 4 / cli 3 / runtime 2)全绿。#4856 的 未 un-draft、未 auto-merge、未碰 Generated by Claude Code |
Fixes #4391
裁决依据(以及一条已作废的 PM 裁定)
本单方向以 维护者 2026-08-02 09:50Z 的裁决为准:remove。
claude/issue-4391-crypto-hash-retire那条撤回自陈的三点误判值得留档,因为它们正是本单最容易走偏的地方:把「签名已写好」误读成需求信号(那只是声明面自己长出来的);算工时时漏了「在沙箱里实现 crypto 是扩大能力面与安全审查面」这项长期成本;以及在「防 AI 犯错」轴上得出了相反结论 —— 实现它只是把炸点从运行时挪走,却永久扩大了能力面。
维护者 remove 的理由(已写进 changeset):从未实现、调用即抛、零投诉 —— 对一个每次使用都抛错的能力,这本身就是最强的活性证据;构建期自动推断是危险的放大器(AI 写 hash 调用 → 能力被自动声明 → tsc 全绿 → 运行时才炸);删除声明与推断后,未知能力在作者时就红。真需要时按能力准入流程重提,实现先行、声明随实现走。
改了什么(四层声明同批删)
spec/data/hook-body.zod.tsHookBodyCapability枚举cli/utils/extract-hook-body.ts@capabilities覆盖白名单(共 4 处,原 issue 只点了 1 处)runtime/sandbox/script-runner.tsScriptContext.crypto.hash保留:
quickjs-runner.ts的cryptoObj.randomUUID(已实现、在用)一字未动。同批清理的下游:
content/docs/automation/hook-bodies.mdx(那句_(not yet wired)_—— 移除后它就成了新的谎言)、skills/objectstack-data/references/data-hooks.md(「exactly six」→ five + 表格行)、skills/objectstack-data/rules/hooks.md(六 token 列表)。后两个 skills 文件不在 issue 的范围清单里,是裸名扫描扫出来的。退役形态:枚举值收窄,不是 key 移除(以门禁实跑为准)
按
spec-property-retirement的路由表实跑判定,结论是三条路都不走:retiredKey()墓碑 —— 墓碑是给 key 的,而capabilities这个 key 依然活着、依然被强制。给它上墓碑会把整个 key 打死。UNKNOWN_KEY_GUIDANCE条目 —— 那是.strict()对象的未知键通道;这里被拒的是数组里的值。object.managedBy的'system'退役先例(data/object.zod.ts)以issue.input为键:只有「曾经合法」的那个拼写会被告知 was removed;写错成
crypto.hsah的作者拿到的仍是 zod 自己那条列出合法 token 的消息 —— 告诉他「你的值被退役了」属于误导(这条有独立 pin 测试)。这是本 PR 最该被 review 的一条实测结论:
authorable-surface.jsondata/ScriptBody:capabilities)json-schema.manifest.jsondata/HookBodyCapability仍在)api-surface.jsondual-source-exports.baseline.jsonpackages/spec/json-schema/本身 gitignore(实测json-schema/data/HookBodyCapability.json的enum已收窄到 5 个值,但那是构建产物,不进版本库)。实跑读数:
结论:没有任何一张基线能自动兜住这次移除 —— 兜住它的只有本 PR 新增的 pin 测试。这也是下面 sabotage 实跑非做不可的原因。
移动的生成物只有三处(
check:generated精确点名,照它的指示重生成):spec-changes.json、docs/protocol-upgrade-guide.md、两页生成参考文档(data/hook-body.mdx、ui/action.mdx—— 枚举选项随之少一项)。liveness 台账:不动,且这是对的
liveness/hook.json的body.{language,source,capabilities,timeoutMs} wired一行保持原样 —— 台账走的是 schema shape 的属性,capabilities键仍在 shape 里、仍然被强制。check:liveness(墓碑方向)与孤儿行检查(strict-removal 方向)双向 PASS,证明这里既不该留墓碑行、也没有留下孤儿行。ADR-0087 conversion 逐条评估:需要,与 #4767 / #4783 / #4616 分属两侧
判断依据是 D2 的那一问:有没有可改写的作者源 /
sys_metadata行?EnvironmentArtifact双源导出DriverCapabilities能力位enable.trash/enable.mrubody.capabilities里的'crypto.hash'capability token 是写在作者源 hook / action body
capabilities: []数组里的值,也会躺在已存的sys_metadata行里 —— 所以本单在 #4734 那一侧。注册:hook-body-crypto-hash-removed,surface 为hook.body.capabilities / action.body.capabilities,同时走hooks[]与actions[](两者共用HookBodySchema);MIGRATIONS_BY_MAJOR[17].conversionIds并扩写该步 rationale;retiredFromLoadPath: true—— 枚举当场拒绝,活作者在 parse 时就被教育,不做静默改写。这条 entry 存在是为了:①已存的 16.x / 17-rc 行重放干净(否则永远被打成metadata_spec_invalid,把链上历史误标成当期违约);②os migrate meta --from 16能改写作者源。这正是object-enable-trash-mru-removed的理由,只是深一层:token 是数组里的值,stripKeys(只管顶层键)够不着,故写了专用 walker。转换刻意不做的事:只剥掉
body.capabilities里的死 token,不碰 body 源码里那行ctx.crypto.hash(...)。那行调用从未返回过值,剥掉授权不会让任何还能跑的东西变坏;但把它一并「修好」会让作者失去唯一一个还在提醒他「这里有段死代码」的信号 —— 处方明确要求作者自己删。fixture 按 disjointness 契约设计:
crypto.uuid在同一个数组里存活(证明是外科手术式剥离,不是整块删),stamp_lead整条不被触碰,expectedNotices: 2(hook 一条 + action 一条)。三仓消费方复核(裸名扫描,非 import 扫描)
扫描词:
crypto.hash、ctx.crypto.hash、带引号的'crypto.hash'、HookBodyCapability。785b8a55df2c69两个兄弟仓的空结果做了对照验证(用
capabilities反查,objectui / cloud 各命中一大把,证明扫描器和路径没问题,不是cd把 cwd 重置成假空结果)。全程用git -C REPO grep形式,没有cd。特别扫了
examples/、skills/、content/docs/:examples/零命中(没有教作者用它的示例),skills/两处、content/docs/手写两处,均已同批清理。未发现任何真实运行时实现或真消费方 —— 对
packages/runtime/src/sandbox/quickjs-runner.ts搜crypto.hash依旧无结果,installCtx上只有randomUUID,与 issue 正文一致。Pin 测试 + sabotage 实跑
每一处移除都配了可失败的断言,并逐条 sabotage 实跑验证「复活即红」:
① 复活枚举值 → spec pin 必须红:
② 复活 extractor 模式 → cli pin 必须红:
③ 往 VM seam 上装回 hash → runtime pin 必须红:
④ 拆掉 D3 接线(把 id 从
conversionIds摘掉)→ 链重放门禁必须红:四次 sabotage 全部按预期变红,随后逐一还原(
git diff已确认零 SABOTAGE 残留)。一个诚实的说明:runtime 的类型 pin 目前是休眠的
@objectstack/runtime没有typecheck脚本(它在scripts/check-type-check-coverage.mjs的 DEBT 表里),所以纯类型级断言在这里永远不会被编译 —— 那种 pin 读起来像保障,实际给不了保障。因此 runtime 侧的主 pin 写成了可执行的:在 VM 里Object.keys(ctx.crypto)枚举 seam 上真正装了什么,并且穷举而非只盯hash(缺陷本质是「成员先于实现被声明」,所以任何新成员都该经过一次会同步更新此列表的 review)。类型级那条保留但注明休眠,待 runtime 接入 typecheck 时自动上膛。全量验证读数
重活全程串行在
flock /tmp/os-heavy-verify.lock下,NODE_OPTIONS=--max-old-space-size,vitest--maxWorkers=2,turbo--concurrency=2。合并 main 的处理
merge 期间 main 前进 14 个提交(含 #4657、#4756 等 spec 单)。按 AGENTS.md §3
git merge origin/main(未 rebase、未 force-push)。migrations/registry.ts(源码)+spec-changes.json、protocol-upgrade-guide.md(生成物)。activationEvents声明四仓零 reader —— declared-but-unenforced,且 studio 侧z.string()零校验 #4657 的 activationEvents 段 + 本单的 crypto.hash 段),已核对二者均在。git checkout origin/main取其版本、再整体重新生成,不做文本合并。核对结果:四张表与origin/main逐字节一致(0 changed lines) —— 与「枚举值收窄对 ratchet 不可见」的结论自洽;ADR-0049:两份activationEvents声明四仓零 reader —— declared-but-unenforced,且 studio 侧z.string()零校验 #4657 的kernel/DynamicLoadRequest:activationEvents [RETIRED]一行完好未被吞。activationEvents声明四仓零 reader —— declared-but-unenforced,且 studio 侧z.string()零校验 #4657 / fix(metadata-protocol)!: batch 逐行结果迁移到BatchOperationResultSchema形状 —— 方案 B 已拍板,硬切 + 诚实迁移说明(Blocked-by #4620) #4793 /sys_comment.visibilityandsys_comment.reply_countare declared but nothing anywhere reads or maintains them (ADR-0049 enforce-or-remove) #4756 /app.homePageId退役的前提写错了 —— objectui 的resolveLandingRoute()一直在读它,现在成了永远走不到的死分支 #4709 / 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 的改动均未被本次 merge 复活。迁移话术(FROM → TO)
capabilities: ['crypto.hash']await ctx.crypto.hash(algo, data)一句话:两个都删。
os migrate meta --from 16自动剥掉 token;那行死调用是作者自己要删的(转换刻意不改 body 源码,理由见上)。定级
@objectstack/spec/@objectstack/cli/@objectstack/runtime均 major,按 #4535 §5 三问在 changeset 里逐条自证(TS2305 / TS2339 的具体触发形状、元数据迁移的有无、形状变更属枚举收窄而非 key 移除)。范围外发现
merge.os-regen.driver指向「上一个装过依赖的 worktree」的绝对路径 —— 该 worktree 一删,全容器的生成物合并驱动就坏了 #4868 ——merge.os-regen.driver存的是上一个装过依赖的 worktree 的绝对路径,该 worktree 一删,全容器的生成物合并驱动就 MODULE_NOT_FOUND(本次 merge 实际撞到)。pnpm check:merge-driver的--self-test照不出来。已按 Prime Directive chore: version packages #10 单独归档、未在本 PR 顺手改。⛔ 全程未碰
content/docs/releases/。