Skip to content

Hook body writes are not statically checkable — accept the gap or give HookSchema a structured writes declaration #3700

Description

@os-zhuang

从 #3583 的引用完整性工作中分出来(方案文档 §5 D4)。

现状

HotCRM 审计里有一类问题:hook 往对象上写它没有的字段(contact_email / contact_phone)。这一类目前静态查不出来,而且短期内也查不出来。

原因是写入面没有结构:

  • HookSchema.body(packages/spec/src/data/hook-body.zod.ts)是 ExpressionBodySchema | ScriptBodySchema 的联合,两者的 source 都是不透明 z.string()。
  • HookSchema.handler(已废弃)是函数名或函数本身。
  • 任何地方都没有「这个 hook 写哪些字段」的声明。

对比之下 flow 侧能查,是因为节点结构化地声明了写入:validate-readonly-flow-writes(FLOW_UPDATE_READONLY_FIELD)和 validate-flow-template-paths(FLOW_TEMPLATE_UNKNOWN_FIELD)读的是 update_record 节点的 fields 配置,不是 JS 源码。

读取侧已经覆盖了:validate-expressions 会按对象字段检查 hook.condition,#3657 起也支持数组形态的 hook.object。缺的只有写入侧。

三个选项

  1. 接受缺口并写进文档。 这是 lint: no reference-integrity or option-key validation for app metadata #3583 九类问题里唯一没有静态答案的一类;明说比让作者以为有覆盖要好。
  2. 给 HookSchema 加结构化 writes: string[] 声明,并由运行时强制。 契约优先(Prime Directive Add comprehensive test suite for Zod schema validation #12):作者声明这个 hook 会写哪些字段,lint 校验字段存在,运行时拒绝声明之外的写入。这样既可静态检查,又能给出真正的安全属性。代价是新的 spec 面 + 运行时改动 + 存量迁移。
  3. 注册期开发态诊断(ADR-0078 §4)。与授权面无关,但成本最低。ADR-0078 已把它列为 deferred / evidence-gated。

建议

短期取 1(记录已接受的缺口),把 2 作为独立提案讨论 —— 它是真正的能力增强,不该被塞进 lint 任务里顺手做掉。

关联

Activity

  1. self-assigned this
    on Jul 27, 2026
  2. os-zhuang commented on Jul 28, 2026

    @os-zhuang
    ContributorAuthor

    状态更新:选项 1 已落地(ADR-0107,PR #3716 已合并);本 issue 从此只跟踪选项 2。

    三个选项的当前处置:

    • 选项 1 — 已完成。 ADR-0107 接受该缺口,并在作者会读到的每个界面标注(hook-bodies.mdx 新增 "Not statically checked: the write set" 小节、ScriptBodySchema TSDoc、评估文档 §5 D4 标记为已决策)。ADR-0107 D2 同时把「不对 body 源码做正则/AST 启发式分析」定为永久立场——部分覆盖会制造"写入已被检查"的假信心,这条从此不再是待选项。
    • 选项 3 — 维持原判。 注册期开发态诊断仍按 ADR-0078 §4 处理(deferred / evidence-gated),ADR-0107 明确不重新决策。
    • 选项 2 — 本 issue 的剩余范围。 结构化 writes 声明,定位为能力提案(declare → lint 校验 → runtime 强制,与 body.capabilities 同形),deferred + evidence-gated。

    证据门槛(何时解锁选项 2)

    ADR-0107 D3 的措辞是 "deferred until the bug class recurs outside the HotCRM audit"。把 "recurs" 写具体——满足以下任一即视为门槛达到:

    1. HotCRM 审计之外(后续审计、dogfooding、真实应用)出现 ≥2 起独立的「hook 写目标对象不存在字段」bug;
    2. 任一真实用户或生产环境报告 schemaless driver 下的静默持久化造成了实际数据问题(ADR-0107 记录的最坏失败模式:与成功不可区分,作者拿到 success envelope 就报告"完成");
    3. 其他能力对 per-hook 写入集元数据产生反向依赖(例如未来写入面的 FLS/审计需求需要结构化写入声明作为基础)。

    达到门槛后的第一步是设计 spike,不是直接实现,需要回答:writes 挂在 body 级还是 hook 级;multi-object / "*" 目标与跨对象写入(ctx.api)的语义;运行时强制点(ctx.input 写守卫 proxy + ctx.api 按对象拦截——没有强制的声明本身就违反 ADR-0049);存量迁移策略(writes 先可选、lint 仅告警的过渡期)。

    在此之前,本 issue 保持 open 作为提案跟踪,无需其他行动。


    Generated by Claude Code

  3. os-zhuang commented on Jul 28, 2026

    @os-zhuang
    ContributorAuthor

    最终决策(owner,2026-07-28):放弃选项 2,关闭本 issue(not planned)。 本评论取代上面那条"证据门槛"评论——门槛作废,不再跟踪。

    理由:结构化 writes 声明把写入集在第二个地方重复一遍,作者(尤其 AI 作者)要负责两处同步,这是新增的书写摩擦和出错面;而把 hook 放到 SQL driver 上真跑一次(内存 SQLite 今天就可用)就能暴露 schemaless memory:// 绿灯掩盖的失败模式,收益不成比例。

    处置落点:

    • ADR-0107 经 docs: withdraw ADR-0107, drop the structured writes proposal (#3700) #3735 撤销删除——文档标注本身就是全部处置,不需要决策记录装置;
    • 作者侧文档保留并修订(hook-bodies.mdx "Not statically checked: the write set"、ScriptBodySchema TSDoc),不再暗示有声明在路上;
    • 评估文档 §5 D4 已追记本次修订;(c) 注册期诊断维持 ADR-0078 §4 原判,不受影响。

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions