事实
packages/runtime/src/action-execution.ts 的 buildActionSession()(第 689-695 行)构造 action body 的 ctx.session:
return {
...( ec . userId != null ? { userId : String ( ec . userId ) } : { } ) ,
...( ec . tenantId != null ? { organizationId : String ( ec . tenantId ) } : { } ) ,
...( Array . isArray ( ec . positions ) && ec . positions . length ? { roles : ec . positions } : { } ) ,
} ;
它的文档注释原话是:"Build the action-body ctx.session from the request ExecutionContext, mirroring the hook ctx.session shape (#3280 ) so an action author reads the caller's active org under the SAME blessed name as a hook author."
调用点两处:packages/runtime/src/domains/actions.ts:296 与 packages/runtime/src/action-execution.ts:920。
三个问题叠在一起
它把 ADR-0090 D3 明令禁止的拼法重新引入了。 值取自 ec.positions —— ExecutionContext 已经在 ADR-0090 D3 把 roles 改名为 positions(packages/spec/src/kernel/execution-context.zod.ts:118,注释原话 "Formerly roles"),这里等于在边界上又把 positions 翻译回 roles 交给作者。ADR-0090 D3 禁的正是这个拼法;plugin-approvals 的 admin 豁免读 session.roles,而 ObjectQL 的 buildSession() 从不填充它 —— 记录锁/delegation 守卫的 admin 覆盖在真实引擎路径上永不生效 #4839 删掉 plugin-approvals 两处 roles.includes('admin') 时,理由也正是「不要第二套权限方言」。
「mirroring hook ctx.session」这句注释现在是错的,而且是危险方向的错。 [spec] 退役 HookContext session.roles —— #4839 双删后零消费方零生产方(ADR-0049) #5050 已按 ADR-0049 把 HookContext.session.roles 退役(墓碑 + 语义迁移)。所以现在两张面对同一个键名给出两种现实:hook 侧 —— 键已退役、写它会 tsc 报错 + parse 拒收;action 侧 —— 键还在,而且真的有值 。同一个平台里同名同位的字段,一个作者在 hook 里读到 undefined(以前)/ 报错(现在),在 action 里读到一串 position 名。
没有任何 schema 声明 action ctx。 actionContext 是裸 any(两处调用点都是 const actionContext: any = {...}),沙箱侧 ScriptContext.session?: unknown(packages/runtime/src/sandbox/script-runner.ts:71)。所以这个键既没有契约、没有文档表、也没有闸门 —— 它只存在于运行时对象里,是 declared-nowhere / produced-anyway,连 liveness 台账都够不着它。
需要裁定(不要直接猜)
三条路,选哪条影响 action body 的公开契约:
A. 改名为 positions,值不变。 与 ExecutionContext / sharing service / ADR-0090 D3 词汇统一,一处方言消失。代价:breaking —— 现存 action body 里 ctx.session.roles 的读会静默变 undefined(裸 any,没有任何东西会报错),所以必须配 ADR-0087 语义迁移 + changeset;而且因为没有 schema,连墓碑都无处可挂,处方只能落在 changeset 和文档上。
B. 直接删。 如果结论是「action body 本就不该拿到任职信息、要判权限就走 security service」,那就删掉这条 spread。同样 breaking、同样静默。
C. 先给 action ctx 立 schema(声明 = 强制),再在其上做 A 或 B。 最贵,但这是唯一能让下一次同类漂移被闸门看见 的路 —— 现状之所以能长这么久,正是因为 action ctx 完全在契约之外。
我倾向 C 的骨架 + A 的语义 (先声明 action 上下文契约,同时把键正名为 positions),理由是两条轴都指向它:长期看,一个每天被客户 action body 读、却没有任何 schema 的运行时上下文,就是下一个 #5050 的温床;而「让 AI 写的元数据/代码难写错」这条更直接 —— 现在一个模型作者在 hook 里被硬拒、在 action 里被放行,它学到的是「roles 有时候能用」,这正是最坏的一课。但这是公开契约 + 安全词汇 的双重决定,按 AGENTS.md Prime Directive #12 / #13 应由维护者裁定,故只登记不实现。
与其他单的关系
按 Prime Directive #10 登记,不在 #5050 的 PR 内修。
Blocked-by: #5779
事实
packages/runtime/src/action-execution.ts的buildActionSession()(第 689-695 行)构造 action body 的ctx.session:它的文档注释原话是:"Build the action-body
ctx.sessionfrom the request ExecutionContext, mirroring the hookctx.sessionshape (#3280) so an action author reads the caller's active org under the SAME blessed name as a hook author."调用点两处:
packages/runtime/src/domains/actions.ts:296与packages/runtime/src/action-execution.ts:920。三个问题叠在一起
它把 ADR-0090 D3 明令禁止的拼法重新引入了。 值取自
ec.positions——ExecutionContext已经在 ADR-0090 D3 把roles改名为positions(packages/spec/src/kernel/execution-context.zod.ts:118,注释原话 "Formerlyroles"),这里等于在边界上又把positions翻译回roles交给作者。ADR-0090 D3 禁的正是这个拼法;plugin-approvals 的 admin 豁免读session.roles,而 ObjectQL 的buildSession()从不填充它 —— 记录锁/delegation 守卫的 admin 覆盖在真实引擎路径上永不生效 #4839 删掉 plugin-approvals 两处roles.includes('admin')时,理由也正是「不要第二套权限方言」。「mirroring hook ctx.session」这句注释现在是错的,而且是危险方向的错。 [spec] 退役 HookContext session.roles —— #4839 双删后零消费方零生产方(ADR-0049) #5050 已按 ADR-0049 把
HookContext.session.roles退役(墓碑 + 语义迁移)。所以现在两张面对同一个键名给出两种现实:hook 侧 —— 键已退役、写它会 tsc 报错 + parse 拒收;action 侧 —— 键还在,而且真的有值。同一个平台里同名同位的字段,一个作者在 hook 里读到undefined(以前)/ 报错(现在),在 action 里读到一串 position 名。没有任何 schema 声明 action ctx。
actionContext是裸any(两处调用点都是const actionContext: any = {...}),沙箱侧ScriptContext.session?: unknown(packages/runtime/src/sandbox/script-runner.ts:71)。所以这个键既没有契约、没有文档表、也没有闸门 —— 它只存在于运行时对象里,是 declared-nowhere / produced-anyway,连 liveness 台账都够不着它。需要裁定(不要直接猜)
三条路,选哪条影响 action body 的公开契约:
positions,值不变。 与ExecutionContext/ sharing service / ADR-0090 D3 词汇统一,一处方言消失。代价:breaking —— 现存 action body 里ctx.session.roles的读会静默变undefined(裸any,没有任何东西会报错),所以必须配 ADR-0087 语义迁移 + changeset;而且因为没有 schema,连墓碑都无处可挂,处方只能落在 changeset 和文档上。我倾向 C 的骨架 + A 的语义(先声明 action 上下文契约,同时把键正名为
positions),理由是两条轴都指向它:长期看,一个每天被客户 action body 读、却没有任何 schema 的运行时上下文,就是下一个 #5050 的温床;而「让 AI 写的元数据/代码难写错」这条更直接 —— 现在一个模型作者在 hook 里被硬拒、在 action 里被放行,它学到的是「roles有时候能用」,这正是最坏的一课。但这是公开契约 + 安全词汇的双重决定,按 AGENTS.md Prime Directive #12 / #13 应由维护者裁定,故只登记不实现。与其他单的关系
HookContext.session.roles)的消费半径扫描。[spec] 退役 HookContext session.roles —— #4839 双删后零消费方零生产方(ADR-0049) #5050 不受影响、也不被本单证伪:action 的ctx.session是另一个对象,永远不会变成 HookContext,没有任何 schema 读它。[spec] 退役 HookContext session.roles —— #4839 双删后零消费方零生产方(ADR-0049) #5050 的墓碑注释、changeset、语义迁移条目里都已显式点名这个邻居,免得后人拿它来「推翻」hook 侧的零生产方结论([移交自 objectui · v17 阻塞]app.homePageId退役墓碑写着「no shell ever read it」,但 objectui 的 AppContent 正在读它 #4865 的教训)。positions/preserveAudit—— 引擎在生产、消费方在读、文档在教,契约里没有(#5050 的镜像方向) #5605(HookContext.session少声明positions/preserveAudit)、[docs-gen] 生成的 reference 把retiredKey()墓碑渲染成any—— 嵌套两层时连[REMOVED]处方都没有,退役键读起来像自由槽 #5606(生成的 reference 把墓碑渲染成any)。按 Prime Directive #10 登记,不在 #5050 的 PR 内修。
Blocked-by: #5779