Repository navigation
#4488 审计发现的四个"授权门断连":email_template / job / validation 的元数据条目到不了执行点,action 导航项点不动 #4509
Description
Activity
- added a commit that references this issue
on Aug 2, 2026 - added a commit that references this issue
on Aug 2, 2026 🔒 CLAIM — 「顺带的三个小清理候选」这一节
- session:
session_01E5CYr5SDwe85gH2Jr5KSgu - branch:
claude/auth-gate-disconnect-issues-6wz8ew - worktree:
objectstack-4509
只认领这一节。 正文的四条主线已经完成并合并:#4558(
email_template建桥 +job关闸 +validation退役 kind)、objectui#3180(导航 action 接onAction)。
为什么现在做:这批是限时的
@objectstack/spec现在17.0.0-rc.1,.changeset/pre.json仍是mode: pre / tag: rc,latest停在 16.1.0。破坏性移除现在落地进 17.0.0;一旦changeset pre exit,同样的移除要等 v18。更要紧的是这四个键的共同性质 ——
mapping.errorPolicy、mapping.batchSize、app.contextSelectors[].includeAll、app.contextSelectors[].placement全部带_authorWarnSkipped:它们都有 schema 默认值,默认值在编译期物化,活性 lint 分不清「作者写的」和「schema 填的」,所以永远无法 authorWarn。让作者听到的唯一路线就是移除。错过 v17,整个 17.x 生命周期里作者写这些键都收不到任何信号。最尖锐的证据来自平台自己 ——
packages/platform-objects/src/apps/studio.app.ts:76,79同时写了includeAll: true和placement: 'sidebar_header',而渲染器故意忽略includeAll(ContextSelectors.tsx:242-246):选择器是 mandatory-scope 语义,一个 "All" 行会解除作用域,对 Studio 的 package 过滤器等于泄露系统元数据。同一文件的注释还写着「we deliberately hide the platform's own system/cloud kernel packages」,而它下面三行的includeAll: true如果真被读,做的正好相反。
处置与范围(相对正文有两处调整)
项 键 路线 变更类别 A mapping.extractQuery/errorPolicy/batchSizestrict 删除 major(限时) B app.contextSelectors[].includeAll/placementstrict 删除 major(限时) C book.groups[].include: { tag }接通,不移除 minor(不限时) 调整 1 —— 加收
mapping.extractQuery。 正文只点了errorPolicy/batchSize,但extractQuery在同一个 schema、同一条 conversion、同一份账本里,而且它是authorWarn级(比另外两个更严重:「promises an export path that does not exist」)。分开做要动两次mapping.zod.ts,两条 conversion 的 fixture 还得互相避让。调整 2 ——
book的{ tag }变体走 enforce,不是 remove。 正文写的是「either 给 doc 加tagsor 收掉变体」。查下来这是一条只差最后一厘米的完整链路:- 消费端已实现:
matchesInclude的 tag 分支 ——packages/spec/src/system/book.zod.ts:213 - 传输端已接:
packages/rest/src/rest-server.ts:3218已经在转发tags: d.tags - resolver 接口已声明:
ResolverDoc.tags?: string[]——book.zod.ts:172-173,注释写着(P3d; absent today) - 唯一缺口:
DocSchema是strictObject且没有tags键 —— 所以在 doc 上写tags:是解析错误,d.tags永远 undefined,include: { tag }永远匹配不中
ADR-0049 的原话是功能存在、只差接线时 enforce 胜出。而且移除 union 成员没有干净的墓碑机制(
retiredKey只适用于 object 键),作者只会拿到泛泛的 union 报错。
清单
认领与分支
- 认领评论(本条)
- 分支从最新 main 重开(上一批 feat(spec)!: datasource 死键归零 —— 退役 retryPolicy / healthCheck / external 两键(#4583 批 B/C/D) #4629 已合并)
项 A —
mapping三键退役-
data/mapping.zod.ts:删 shape 三键(:145/:148/:149) - 删指向它们的 alias(:118-120
query/onError·errorHandling·errorMode/batch·chunkSize)—— 留着就是把作者指向已不存在的键 - 重写
guidance.skipErrors(:126 现在说「errorPolicy: 'skip'是默认值且已经这么做了」,移除后指向空气) - 加三条
guidance处方;必须消歧:bulk-action/offline/sync/connector/events-integrations/seed-loader/driver-nosql上都有活的batchSize,处方要明说被删的是mapping.batchSize。不提供改名建议 —— 没有等价物(导入错误处理走请求自己的 options,写路径自己定批大小,导出走普通 query API) -
liveness/mapping.json删这三行(strict 删除 ⇒ 留行会报 ORPHAN)
项 B —
app.contextSelectors[]两键退役-
ui/app.zod.ts:删 shape 两键(:767/:777)、删knownKeys(:782)、删 aliasshowall·location(:783) - 加
guidance两条处方(includeAll的处方要写出安全理由) - 连带:
allValue重新核验 —— 账本 note 明写 "re-verify if includeAll is ever removed"。它仍live(哨兵读法为真),但其声明用途「value emitted when All is selected」永不可能发生:改.describe()为哨兵语义 + 改账本 note + 新verifiedAt -
platform-objects/src/apps/studio.app.ts:76,79停止 author 这两个键,原地留注释 -
liveness/app.json删两行、改allValue行
项 C —
book{ tag }接通-
system/doc.zod.ts加tags: z.array(z.string()).optional() -
book.zod.ts:172-173注释从(P3d; absent today)改为指向DocSchema.tags的实况 - 端到端测试:两个 doc 带 tag、一个不带,group 用
include: { tag },断言只有带 tag 的进组
ADR-0087 D2/D3 注册(A/B 必须,C 不需要)
- 新增 conversion
mapping-inert-keys-removed(toMajor: 17,retiredFromLoadPath: true,expectedNotices: 3)。⚠ 必须mapCollection(stack, 'mappings', …)限定集合,全局 strip 会误删 connector/sync/bulk-action 上活的batchSize - 扩已有的
app-dead-authoring-keys-removed(registry.ts:1595,同toMajor: 17),不新开 —— 同 major 同类型新开会撞 fixture 互斥契约。contextSelectors是数组,需下钻一层并 copy-on-write。expectedNotices4 → 6 -
migrations/registry.ts的step17.conversionIds加mapping-inert-keys-removed+ 扩rationale。⚠ id 打错不会报错、只在 replay 时静默跳过 - fixture 互斥自检(
before最小化,不带别的 conversion 的键;retiredFromLoadPathfixture 还要能零通知走默认加载路径)
生成物与周边
-
gen:schema→authorable-surface.json五行消失(strict 删除的特征,闸门 (a) 的绊线,本 PR 刻意删)+json-schema.manifest.json -
gen:spec-changes/gen:upgrade-guide/gen:api-surface/gen:docs - forms:剪掉这五个键的
{ field: '<key>' }输入(若有),留一行注释 -
pnpm i18n:extract(剪 form 输入会改抽取标签),check:i18n把关 -
lint-liveness-properties的测试(账本驱动会自动停止告警,但测试跑在真实 shipped 账本上 —— 上一轮 datasource 账本判定的 20 条死键至今无人处置:三个块整块无人读,其中 readOnly 让一个 shipped 示例的「只读副本」可写(ADR-0049 enforce-or-remove) #4583 在这里翻过车) - pin 测试:每个被删键一条否定断言(
.toThrow(/…/s)) - 文档:
references/data/mapping.mdx:63-65、references/ui/app.mdx:131,134是生成的,由gen:docs重出;散文/示例另 grep。⛔ 不碰content/docs/releases/ - changesets ×2:
major(A+B,带 FROM → TO 和一行修法)+minor(C)
验证
- 全量
pnpm build→pnpm test(⛔ 不用--filter <pkg>...,部分失败会留陈旧 dist) - 十道 spec 闸门 +
check:i18n -
check:liveness:mapping dead 3 → 0;app open dead 6 → 4 - 回归钉:
bulk-action/sync/connector/offline/seed-loader的batchSize仍正常 parse,未被 conversion 误伤 -
os migrate meta --from 16在带这五个键的 fixture 上重放,第二次applied为空(幂等)
明确不做
app.areas[].visible/app.areas[].requiredPermissions—— 我在核验 B 项时确认了这两个是fail-open 的访问闸门:filterAppForUser只走item.navigation,item.areas一个字都不看(packages/rest/src/rest-server.ts:1823),客户端 area 切换器也照渲染所有 area,所以一个「隐藏的 / 需要权限的」area 对所有人可见。和capabilities.readOnly同形状,同样限时。本轮范围外,需要单独决定 enforce 还是 remove —— 我会另开 issue 记录,不在这里顺手做。- Remove dead object
enable.trash/enable.mrucapability flags (ADR-0049 enforce-or-remove) #3207 / IDataDriver.findStream 没有任何调用方,两个 driver 的实现还正好做了它承诺要避免的事(ADR-0049 enforce-or-remove) #4484(后者已被pm-dispatch round 6认领)/ 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 批 7、8
进度会随着做随时更新本条清单。
Generated by Claude Code
- session:
进度更新 —— 三项全部实现完毕,PR #4664(draft,待 CI)
清单逐条对照:
认领与分支
- 认领评论
- 分支从最新 main(
21676eb)重开
项 A —
mapping三键退役 ✅- 删 shape 三键 + 删指向它们的六个 alias + 重写
guidance.skipErrors -
MAPPING_RETIRED_KEY_GUIDANCE三条处方,batchSize那条点名bulkActionDef/connector/sync/offline/ seed-loader / NoSQL cursor 是 DIFFERENT 键 -
liveness/mapping.json删三行 → mapping 死键 3 → 0
项 B —
contextSelectors两键退役 ✅- 删 shape 两键、
knownKeys、两个 alias,加guidance -
allValue重新核验(账本 note 点名要求的那一步):仍live,但.describe()不再叫它「All 被选中时发出的值」—— 那个事件不可能发生 -
studio.app.ts停止 author 这两个键 -
liveness/app.json删两行、改allValue行
项 C —
book{ tag }接通 ✅-
DocSchema.tags+ResolverDoc注释更正 + 端到端测试(三个 doc 带/不带 tag,断言只有带 tag 的进组)
ADR-0087 ✅
- 新增
mapping-inert-keys-removed,限定mappings集合 - 扩展
app-dead-authoring-keys-removed下钻 contextSelectors 数组,expectedNotices4 → 6 -
step17.conversionIds+rationale扩写
生成物与周边 ✅
-
authorable-surface.json五行删除(闸门 (a) 绊线,刻意)+gen:schema/gen:spec-changes/gen:upgrade-guide/gen:docs - forms 无该五键的输入(查过,无需剪);
i18n:extract+check:i18n绿 -
lint-liveness-properties测试更新 —— 它跑在真实 shipped 账本上,extractQuery的正向断言和四个_authorWarnSkipped键的负向断言都随账本反转了。上一轮 datasource 账本判定的 20 条死键至今无人处置:三个块整块无人读,其中 readOnly 让一个 shipped 示例的「只读副本」可写(ADR-0049 enforce-or-remove) #4583 就是在这里翻的车,这次预先处理 - pin 测试 ×7、changesets ×2(
major+minor) - 文档全是生成的,
gen:docs重出;散文/skills 零命中
验证 ✅
-
pnpm build71/71、typecheck122/122、lint干净、test132/132 - 十三道 spec 闸门 +
check:i18n全绿 - 回归钉:
bulk-action/sync/connector/offline/seed-loader 的batchSize未被误伤
两个分出去的东西
#4651 —— 核验项 B 时确认
filterAppForUser只走顶层navigation,从不读item.areas(rest-server.ts:1823在navigation缺失时提前返回),客户端 area 切换器也照渲染所有 area。所以app.areas[].visible/requiredPermissions是 fail-open 的访问闸门,不只是没人读。已如实写进账本并单开 issue 走决策 —— 没在退役 PR 里就地发明授权机制,理由同 #4584。它同样限时(移除路线是破坏性的)。objectui#3208 —— objectui 的
AppContextSelectorSchema按引用派生自 spec,所以下次抬 spec pin 时report-chart-query-spec-parity.test.ts:76-77的两条断言会如期失败(漂移守卫正常工作)。已记录要改的两处,现在不动 —— objectui 还 pin 在^17.0.0-rc.0,提前改等于对着未发布的形状写代码。
Generated by Claude Code
状态更新 —— PR #4664 已入合并队列
CI 全绿(
Build Core、Test Core×2、Dogfood Regression Gate×2、Temporal Conformance、TypeScript Type Check、ESLint、Build Docs、Spec property liveness、Docs Drift、Duplicate Fix Guard、Check Changeset),已标 ready 并入队,mergeable_state: clean。中途遇到的一次冲突,记一下
推上去之后 PR 挂了一段时间一个 CI check 都没有,只有 Vercel 的 ignored build。我一开始读成"队列慢"——读错了:main 在我建分支后前进了六个合并(到
ce5242c),PR 处于mergeable_state: dirty,GitHub 算不出 merge ref,于是pull_request触发的工作流根本没被触发。PR 上一个 check 都没有,是冲突信号,不是排队信号。
冲突两处:
packages/spec/src/migrations/registry.ts—— IDataDriver.findStream 没有任何调用方,两个 driver 的实现还正好做了它承诺要避免的事(ADR-0049 enforce-or-remove) #4484(findStream退役)和本 PR 都往同一个step17.rationale字符串尾部追加了段落,也都往conversionIds加了 id。两段都保留(按时间序 IDataDriver.findStream 没有任何调用方,两个 driver 的实现还正好做了它承诺要避免的事(ADR-0049 enforce-or-remove) #4484 在前),id 列表 git 自己合干净了。docs/protocol-upgrade-guide.md—— 生成文件,重新生成而不是手工合并。已验证重生成后同时含findStream(4 处)、mapping.batchSize、contextSelectors。
合并后重跑:全量
build71/71、十三道 spec 闸门全绿。两个 bot 评论都不需要动作
Vercel 是 ignored build。Docs Drift Check 明说是 advisory —— 它列的是所有引用
@objectstack/spec的手写文档,任何 spec 改动都会触发全表;不是漂移判定。手写文档里对这五个键的 grep 是零命中,生成的 reference 页已由gen:docs重出。
三项全部完成,合并后本 issue 可关。
app.areas的两个 fail-open 闸门在 #4651,跨仓后续在 objectui#3208。
Generated by Claude Code
- added a commit that references this issue
on Aug 2, 2026 PM 核验:三部分全部落地,关闭本单
队列扫描时复核,本单已无剩余工作:
部分 承载 状态 正文四条主线( email_template建桥 /job关闸 /validation退役 kind)#4558 ✅ 已合并 导航 action 接 onActionobjectui#3180 ✅ 已合并 「顺带的三个小清理候选」(mapping 三键 / contextSelectors 两键 / book {tag}接通)PR #4664 ✅ 已合并,main 上为 5966c2a5966c2a feat(spec)!: retire the five keys the lint could never warn about, and connect doc.tags (#4509) (#4664)—— 破坏性移除赶在changeset pre exit之前进了 17.0.0,这批键的限时窗口已兑现。分出去的两项各自独立跟踪,不阻塞本单关闭:
- app.areas[] 的 visible / requiredPermissions 是 fail-open 的访问闸门 —— 服务端从不走 areas(ADR-0049,v17 限时) #4651 —
app.areas[].visible/requiredPermissions是 fail-open 访问闸门(同样限时,待裁决 enforce / remove) - objectui#3208 — 下次抬 spec pin 时的漂移守卫跟进
同时移除
pm:queue,退出派发队列。
Generated by Claude Code
- app.areas[] 的 visible / requiredPermissions 是 fail-open 的访问闸门 —— 服务端从不走 areas(ADR-0049,v17 限时) #4651 —
- added 4 commits that reference this issue
on Aug 3, 2026 - added 3 commits that reference this issue
on Aug 23, 2026 - added 3 commits that reference this issue
on Oct 7, 2026
#4488 把最后 9 个类型纳入活性账本时,逐类型闭合调用图暴露出四个结构性断连 —— 不是单个死键,而是"整扇授权门通向空地"。每一个都是 webhook #3461 的形状,而 webhook 那扇门已经用 #3489(materializer 桥)修好了,所以修法有现成范本。全部按 ADR-0049 enforce-or-remove 处置:要么建桥,要么关门(撤销注册/收窄 schema),不许维持现状。
1.
email_template—— 整个元数据表面到不了 sendTemplate(最严重,涉及认证邮件)sendTemplate只读sys_email_template行(plugin-email/src/email-service.ts:404-465),行的唯一写入者是内置 auth 模板 + 代码构造的EmailServicePluginOptions.templates(email-plugin.ts:358-371, :431-468);没有任何 bootstrapper 传templates(serve.ts 的 email 参数组装里没有它)。emailTemplates:(被 metadata plugin 摄入为元数据条目)、*.email-template.ts、Studio(studio.app.ts:331 的导航指向 metadata-admin 列表)、PUT /meta —— 全部落进没人读回的元数据存储;包导入路径还显式排除 emailTemplates(runtime/src/domains/packages.ts:569-571)。upsertTemplate的字段映射(email-plugin.ts:432-449)就是未来桥的映射表,和 fix(webhooks): materialize stack-declared webhooks into the dispatcher (#3461) #3489 用 sys_webhook 列映射的方式完全一致。或者:撤销email_template的allowRuntimeCreate并从 stack schema 移除emailTemplates:,让 sys_email_template 数据门成为唯一入口。packages/spec/liveness/email_template.json(全表面 dead,warn 载于name)。2.
job—— 运行时创建的 job 条目永远不会被调度jobs到达调度器(app-plugin.ts:766-802,handler 从 bundle 函数表解析);job注册为allowRuntimeCreate: true(metadata-plugin.zod.ts:640),但没有任何代码把运行时 job 条目送进IJobService.schedule—— 而且结构上不能:运行时条目的handler在 bundle 函数表之外无从解析。allowRuntimeCreate(诚实:job 是代码工件),要么设计 handler 可解析的运行时 job(比如 handler 限定为已注册 flow / 具名函数)再建桥。packages/spec/liveness/job.json类型注记。3.
validation—— 独立 validation 条目绑不到任何对象object.validations(engine.ts:3703/:4017/:4085 → rule-validator);ValidationRuleSchema本身没有对象绑定键,没有任何合并代码,只有引用追踪器假设条目上有object/objectName(metadata-protocol/src/protocol.ts:1306 —— 这个键甚至不在 schema 里,parse 会剥掉它)。objectName+ 建合并桥,要么撤allowRuntimeCreate并把独立*.validation.tsfilePatterns 收掉(规则只在对象里授权)。packages/spec/liveness/validation.json类型注记(规则词汇本身全活 —— 经由内嵌路径)。4.
action导航项渲染但点击无事发生onActionprop(NavigationRenderer.tsx:976),但 objectui 的两条 console 侧边栏路径(AppSidebar / UnifiedSidebar)都不传onAction——actionDef.actionName到不了任何 dispatcher,条目照常渲染、照常被门控,点了没反应。onAction→ ActionEngine(或在接通前把actionvariant 标记 experimental)。跨仓库:objectui 仓库需要对应 issue。packages/spec/liveness/app.json类型注记(actionDef在 union 走查边界之外,故记录在注记而非行)。顺带的三个小清理候选(不阻塞,同 PR 顺手也行)
app.contextSelectors[].includeAll:渲染器有意忽略(ContextSelectors.tsx:242-246,mandatory-scope 语义,注释即处方)→ retiredKey 候选。placement同理(无消费者)。book.groups[].include的{ tag }变体:resolver 实现了 tag 匹配,但DocSchema没有tags属性,永远匹配不中 → either 给 doc 加tagsor 收掉变体。mapping.errorPolicy/batchSize:死键但因 schema 默认值在编译期物化而不可 warn(_authorWarnSkipped)→ 想让作者听到警告只能走移除路线。未指派 —— 认领哪一项请在下面说一声。每项独立可做;1 和 3 是安全形状,优先。