Skip to content

[spec] HookContext.session 少声明了 positions / preserveAudit —— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605

Closed

Description

@os-zhuang

背景

#5050 退役的是「声明了、没人生产」的 session.roles。在核查它的生产方时,发现同一个 session 块存在方向相反的漂移:有两个键是引擎真的在生产、消费方真的在读、文档真的在教,但 packages/spec/src/data/hook.zod.ts 的 HookContextSchema.session 里根本没有声明。

这不是同一个 bug 的另一半措辞 —— roles 是 declared-never-produced,这两个是 produced-never-declared,修法完全不同(前者删,后者补声明),所以单独立单。

证据(均对 origin/main 核实)

生产方 —— packages/objectql/src/engine.ts 的 buildSession():

消费方:

  • packages/objectql/src/plugin.ts:782 —— const preserveAudit = session?.preserveAudit === true;(审计戳 hook 的核心分支;该处 session 形参标注为 any,所以类型层看不见这条读)

文档在教(两页,都把 positions 当作喂给 sharing service 的正规写法):

  • content/docs/kernel/runtime-services/examples.mdx:37 —— positions: ctx.session?.positions,
  • content/docs/kernel/runtime-services/sharing-service.mdx:67 —— 同上

契约里没有 —— HookContextSchema.session 声明的键是 userId / actor / organizationId / accessToken / isSystem / skipTriggers / skipAutomations(#5050 后再加一个 roles 墓碑)。没有 positions,没有 preserveAudit。

为什么这是缺陷而不是无害省略

  1. 文档教的代码在类型层编译不过。 一个按 content/docs/automation/index.mdx 的写法把 handler 标注成 (ctx: HookContext) 的作者,写 ctx.session?.positions 会吃 TS2339;上面两页示例之所以看不出来,是因为它们把 ctx 标成了 any。照文档抄 + 照文档标类型 = 编译失败,这是作者第一次写 hook 就会撞上的。
  2. positions 恰恰是 ADR-0090 D3 之后的权限词汇本体。 ExecutionContext 已经把 roles 改名成 positions(packages/spec/src/kernel/execution-context.zod.ts:118,注释原话 "Formerly roles")。[spec] 退役 HookContext session.roles —— #4839 双删后零消费方零生产方(ADR-0049) #5050 把 hook 侧最后一处 roles 拼法退役之后,hook 能看到的唯一任职词汇就是这个未声明的 positions —— 契约上等于说「hook 拿不到任职信息」,而运行时明明给了。
  3. Zod 非 strict 掩盖了它。 HookContextSchema 刻意不 strict(见文件头注释),所以 positions 在 parse 时被静默 strip。也就是说:谁真的去 HookContextSchema.parse(ctx) 一把,谁就把 positions 弄丢了 —— 而生成的 reference 页(content/docs/references/data/hook.mdx)恰恰以 HookContextSchema.parse(data) 作为示例。

需要裁定的点(不要直接猜)

补声明的语义要维护者定,两条路差别很大:

  • A. 两个键都补进 session 声明(positions: z.array(z.string()).optional()、preserveAudit: z.boolean().optional()),配 .describe() 写清「仅供 hook 读取,授权由 security service 裁决,这里不是授权输入」。契约与运行时对齐,文档示例可以正常标类型。风险:positions 出现在 hook 上下文里,可能被下一个作者读成「可以拿它做授权判断」—— 需要用 roles 退役同款的措辞把边界钉死。
  • B. 认为 hook 本就不该看到任职信息,那么该删的是 buildSession() 的 positions 写入 + 两页文档的教法,而不是补声明(即对 positions 走 enforce-or-remove 的 remove 一侧)。但注意 preserveAudit 有真实消费方(plugin.ts:782),它只能走补声明。

我的倾向是 A,理由:preserveAudit 无论如何都得补(有活的消费方,删不掉),而 positions 有两页文档 + sharing service 的真实用法在背书,删它是砍掉一个在用的能力;A 同时消灭「文档教的代码编译不过」这个当下就会撞到的问题。但 positions 是否应当出现在 hook 契约上是安全语义判断,按 ADR-0095 D3 的口径应由维护者拍板,故不擅自实现。

与其他单的关系

发现于 #5050 的生产方核查过程(Prime Directive #10)。

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    分诊:needs-user-decision + domain:spec(预挂)。

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


    Generated by Claude Code

  2. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    维护者裁定(2026-08-06,经 spec 车道 PM session_018fxLGQdatPbBUvCgiVxg6D 誊写):A —— 两键补声明。positions: z.array(z.string()).optional() + preserveAudit: z.boolean().optional() 进 HookContextSchema.session,.describe() 按 issue 预案钉死边界:「仅供 hook 读取,授权由 security service 裁决,此处不是授权输入」——与 roles 退役墓碑同款措辞纪律,防止下一个作者(尤其 AI)把它读成授权判断依据。文档教的代码从此可正常标类型。摘 needs-user-decision,即刻派发。


    Generated by Claude Code

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

    @os-zhuang
    ContributorAuthor

    PM 认领(spec 车道 PM,session_018fxLGQdatPbBUvCgiVxg6D,2026-08-06)


    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