Repository navigation
[docs] runtime-services 的 hook 示例在教 ctx.services —— hook 上下文从来没有这个键,照抄的 hook 会拒掉每一次写入 #5720
Description
背景
在 #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 属性表)。
为什么是缺陷而不是无害笔误
- 照抄的 hook 会拒掉每一次写入。
ctx.services?.sharing?.canEdit(...)因可选链短路成undefined,ok恒为undefined,于是if (!ok) throw new Error('PERMISSION_DENIED')无条件抛出。作者按文档写一个beforeUpdate授权检查,得到的是「该对象上所有更新一律 403」,而且失败方向是「看起来在正常拒绝」,极难归因到文档。这正是 Prime Directive chore: version packages #10 推论里那句「绝不宣传运行时并不交付的能力」的实例。 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的示例,等于声明「这段编译得过」却什么也没证明。- 正确写法尚待确定,所以更该由维护者定。 hook 里访问 sharing service 的真实受支持通道是什么,需要先答:是
ctx.api(buildHookApi)已经覆盖、还是应当像 action ctx 那样给 hook ctx 也注入services、抑或 hook 就不该直接调 service。示例应改成哪一种,取决于这个答案。
建议修法(不预设结论)
- 先定「hook 访问 kernel service 的受支持通道」;
- 按该结论改这两处示例(
examples.mdx第 2 节;sharing-service.mdx:60-70的 Example 块用的是裸services.sharing,不带ctx.,是否同病需一并核); - 把示例的
ctx: any换成(ctx: HookContext)—— [spec] HookContext.session 少声明了positions/preserveAudit—— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605 落地后session.positions已可正常标类型,any已无必要,而去掉它才能让{/* os:check */}真正生效; - 可选:给
check:skill-examples加一条「os:check 块内不得把入参标any」的约束,否则这道门还会被下一个示例同样架空。
与其他单的关系
- [spec] HookContext.session 少声明了
positions/preserveAudit—— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605(本单的发现来源,已在 PR):补session.positions/preserveAudit的声明。修完后这两页的session读法可正常标类型,但ctx.services仍编译不过 —— 两者不重叠,[spec] HookContext.session 少声明了positions/preserveAudit—— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605 不解决本单。 - hooks 技能与 API 文档仍在教「批量写的行级谓词在
ctx.input.ast」——与 #5273 同一句假话,只是另外三个面 #5670(hooks 技能与 API 文档在教「批量写的行级谓词在ctx.input.ast」)同属「文档教了一个不存在的 hook 面」这一类,但键不同、页不同,修 hooks 技能与 API 文档仍在教「批量写的行级谓词在ctx.input.ast」——与 #5273 同一句假话,只是另外三个面 #5670 不会碰到ctx.services,故不作为其子单。 - 非阻塞:本单不依赖任何在飞的单。
Activity
os-zhuang commentedon Aug 6, 2026
分诊:决策箱(
needs-user-decision),预挂domain:devx。content/docs/kernel/runtime-services/examples.mdx:13-14/:32仍在教ctx.services.data/ctx.services?.sharing(第 1 节的ctx.services.data也是同病,不止正文点名的第 2 节);engine.ts 各hookContext构造点与 sandboxbuildHookSandboxContext均不产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半边已解;本单余量与其不重叠,前提成立。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
claude commentedon Aug 6, 2026
处置(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:416JSDoc 同修。附带拆一个 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
os-zhuang commentedon Aug 6, 2026
【裁决落地】维护者 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_01N3uGFF8teXbpgtbEJ1aYXuGenerated by Claude Code
os-zhuang commentedon Aug 6, 2026
Claim(PM devx 车道):本单由本 PM 会话派发实现。
session_01GX3sL71LFq8m2usg6VqTSEclaude/issue-5720-hook-ctx-api-docsctx.api」docs-only)执行:runtime-services 两页(examples.mdx 第 1、2 节均改)+ sharing-service.mdx 示例改为受支持通道,示例入参改(ctx: HookContext)去any;⛔ 不动 spec/engine 契约。附带批准的 os:check 禁ctx: any门禁(spec scripts 面)由本单拆子单转 spec 座位,不同 PR;hook.zod.ts:416JSDoc 同病在HookEvent的枚举注释仍在说批量写把行级谓词「carried ininput」—— 与 #5273 同一句假话,在同文件 40 行之外 #5899 留证据评论归拢。Generated by Claude Code
check:skill-examples应拒绝 os:check 块内把入参标any—— 标记了却零覆盖的空门 #5943