Skip to content

[docs] runtime-services 的 hook 示例在教 ctx.services —— hook 上下文从来没有这个键,照抄的 hook 会拒掉每一次写入 #5720

Closed
1 of 1 issue completed

Description

@os-zhuang

背景

在 #5605(给 HookContext.session 补声明 positions / preserveAudit)核对「文档教的代码能否按 (ctx: HookContext) 正常标类型」时发现:补完 positions 之后这两页示例仍然编译不过,原因不在 session,而在同一段示例的另一个键 —— ctx.services。它根本不在任何 hook 上下文里。

这不是 #5605 的另一半:#5605 是「引擎在产、契约没声明」(补声明即可),本单是「文档在教、任何一端都没有生产者」,两者修法不同,故单独立单。

证据(均对 origin/main 核实)

文档在教(标了 {/* os:check */},即声称「这段应当能编译」):

  • content/docs/kernel/runtime-services/examples.mdx:31-42 —— 第 2 节 "Hook: check sharing permission before mutation":

    export async function beforeUpdate(ctx: any) {
      const ok = await ctx.services?.sharing?.canEdit('contract', ctx.input.id, { ... });
      if (!ok) {
        throw new Error('PERMISSION_DENIED');
      }
    }
    

引擎不产(代码注册的 handler 路径)—— packages/objectql/src/engine.ts:4650 起,每个 hookContext 都是逐字段构造的:

const hookContext: HookContext = {
    object, event, input,
    session: this.buildSession(opCtx.context),
    provenance: ..., user: ..., api: this.buildHookApi(...),
    transaction: ..., ql: this
};

九个键:object / event / input / session / provenance / user / api / transaction / ql。没有 services,其余 triggerHooks 调用点同构。

沙箱也不产(metadata hook body 路径)—— packages/runtime/src/sandbox/body-runner.ts:310-324 的 buildHookSandboxContext 返回 input / previous / user / session / event / object / result / api / log / crypto。同样没有 services。注意对照组:紧挨着的 buildActionSandboxContext(同文件 :326)注释写明 action ctx 约定里有 services —— 即 services 是 action 面的词汇,示例把它搬到了 hook 面。

契约也没有 —— HookContextSchema 声明的键与引擎一致,无 services(见生成的 content/docs/references/data/hook.mdx 属性表)。

为什么是缺陷而不是无害笔误

  1. 照抄的 hook 会拒掉每一次写入。 ctx.services?.sharing?.canEdit(...) 因可选链短路成 undefined,ok 恒为 undefined,于是 if (!ok) throw new Error('PERMISSION_DENIED') 无条件抛出。作者按文档写一个 beforeUpdate 授权检查,得到的是「该对象上所有更新一律 403」,而且失败方向是「看起来在正常拒绝」,极难归因到文档。这正是 Prime Directive chore: version packages #10 推论里那句「绝不宣传运行时并不交付的能力」的实例。
  2. ctx: any 让 {/* os:check */} 变成一道空门。 check:skill-examples 会真的编译这个被标记的块(packages/spec/scripts/check-skill-examples.ts),但块内 ctx 标成了 any,于是块里每一次属性访问都不被检查 —— 门是绿的,覆盖的却是零。同一个 any 同时掩盖了 [spec] HookContext.session 少声明了 positions / preserveAudit —— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605 的 ctx.session?.positions(当时是 TS2339)和本单的 ctx.services。一个标了 os:check 又把入参标 any 的示例,等于声明「这段编译得过」却什么也没证明。
  3. 正确写法尚待确定,所以更该由维护者定。 hook 里访问 sharing service 的真实受支持通道是什么,需要先答:是 ctx.api(buildHookApi)已经覆盖、还是应当像 action ctx 那样给 hook ctx 也注入 services、抑或 hook 就不该直接调 service。示例应改成哪一种,取决于这个答案。

建议修法(不预设结论)

  1. 先定「hook 访问 kernel service 的受支持通道」;
  2. 按该结论改这两处示例(examples.mdx 第 2 节;sharing-service.mdx:60-70 的 Example 块用的是裸 services.sharing,不带 ctx.,是否同病需一并核);
  3. 把示例的 ctx: any 换成 (ctx: HookContext) —— [spec] HookContext.session 少声明了 positions / preserveAudit —— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605 落地后 session.positions 已可正常标类型,any 已无必要,而去掉它才能让 {/* os:check */} 真正生效;
  4. 可选:给 check:skill-examples 加一条「os:check 块内不得把入参标 any」的约束,否则这道门还会被下一个示例同样架空。

与其他单的关系

发现于 #5605 的文档侧核查(Prime Directive #10)。未加标签,交 PM 分诊定级。

Activity

  1. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    分诊:决策箱(needs-user-decision),预挂 domain:devx。

    • 过时前提检查(origin/main):content/docs/kernel/runtime-services/examples.mdx:13-14/:32 仍在教 ctx.services.data / ctx.services?.sharing(第 1 节的 ctx.services.data 也是同病,不止正文点名的第 2 节);engine.ts 各 hookContext 构造点与 sandbox buildHookSandboxContext 均不产 services,hook 面只有 api: buildHookApi(...)(ScopedContext)。[spec] HookContext.session 少声明了 positions / preserveAudit —— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605 已随 PR feat(spec): HookContext.session 补声明 positions / preserveAudit(#5605) #5722 合入 main,session.positions/preserveAudit 半边已解;本单余量与其不重叠,前提成立。
    • 为什么进决策箱:修向取决于「hook 访问 kernel service 的受支持通道」——改教 ctx.api、给 hook ctx 注入 services(公开契约新增,涉 spec + engine),或裁定 hook 不得直调 service。这是公共契约拍板,不是文档措辞细节。裁「注入」则分诊届时按 contract-first 拆分(spec/engine 先行、docs 收尾);裁「ctx.api / 禁止」则为 docs-only,归 devx 车道直派。
    • 域锚定:最小落点 content/docs/kernel/runtime-services/examples.mdx + sharing-service.mdx(devx 域表 content/docs/**),故预挂 devx;正文第 4 点(check:skill-examples 加「os:check 块禁 ctx: any」)落 packages/spec/scripts/,属 spec 面,建议随裁决一并定去留、届时单独转 spec 座位。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. claude commented on Aug 6, 2026

    @claude
    Contributor

    处置(2026-08-06):退回派发队列(docs-only)。

    复核结论:全仓无任何执行上下文生产 ctx.services(engine 四处 hookContext 构造、sandbox、service-automation flow node 面均查无),照抄示例的 hook 会 403 所有写入——docs 纠错 + 恢复既有契约,不触升级门槛。

    修法:runtime-services 两页 + sharing-service.mdx 示例改写为受支持能力(sharing 强制本由中间件自动执行,hook 手查 canEdit 是冗余教学;业务校验示例用 ctx.api.object());hook.zod.ts:416 JSDoc 同修。附带拆一个 spec scripts 子单:check:skill-examples 禁 os:check 块内 ctx: any(把同类空门一次焊死)。

    「hook ctx 注入 services」若日后有真实需求,另立提案走 contract-first,不由本 docs 缺陷单捎带。量级 S。

    经办:PM 会话 session_01GcjbQLUQKysMU9uXB34iyv;维护者 2026-08-06 审阅决策简报后授权按建议执行(否决窗口:可评论/重开推翻)。


    Generated by Claude Code

  3. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    【裁决落地】维护者 2026-08-06 批复全舰队决策箱评估报告(批复「同意」),本单裁定:

    裁「改教 ctx.api」——docs-only:runtime-services 与 examples.mdx 第 1 节的 hook 示例全部改为受支持通道(ctx.api),⛔ 不新增「hook ctx 注入 services」公共契约(不动 spec/engine)。附带批准:os:check 块禁 ctx: any 的门禁约束立项(落 spec scripts,可同 PR 或由承接方拆单)。

    流转:摘 needs-user-decision → pm:queue,归 devx 车道直派(docs-only)。

    评估与落地会话:session_01N3uGFF8teXbpgtbEJ1aYXu


    Generated by Claude Code

  4. self-assigned this
    on Aug 6, 2026
  5. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    Claim(PM devx 车道):本单由本 PM 会话派发实现。


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions