fix(plugin-security): 被拒收的 capability 声明不再压掉派生占位,拒收诊断点名授权者 (#4967 Part 1/3) - #5875
Merged
Merged
Conversation
… refused declaration falls back to the derived placeholder (#4967 Part 1/3) bootstrapDeclaredCapabilities filled its returned name list BEFORE the upsert decided anything, so all three refusal paths reported a name they never wrote a row for. The caller uses that list to tell bootstrapSystemCapabilities to skip deriving the back-compat placeholder, so a declaration refused for want of an owning package suppressed the placeholder too and the capability then existed in no sys_capability row at all -- writing the declaration was strictly worse than omitting it. The list (renamed materializedNames) now reports only names this pass CONFIRMED have a row: written here (seeded/updated/claimed), or an existing row that must not be clobbered (admin-authored, another package's, or a curated platform name the curated pass owns). The unowned path reports its name only when a row already exists, and otherwise falls through to the derivation. Adds the skippedUnowned counter that path never had, so every named declaration lands in exactly one counter and the list reconciles with them. Part 3: the unowned-refusal diagnostic stays a warn (#4632 -- functional degradation, not durability) and now names the permission set(s) that GRANT the capability plus the actual consequence, threaded in as an argument from the bootstrap permission sets rather than any new global state. Part 2 of #4967 (stack.capabilities -> registry _packageId) is out of scope here and tracked as #5870. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 6, 2026
baozhoutao
marked this pull request as ready for review
August 6, 2026 10:45
baozhoutao
enabled auto-merge
August 6, 2026 10:45
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes 部分:#4967 Part 1/3(Part 2 → #5870)
本 PR 只做 #4967 的 Part 1(拒收的声明不再压掉派生)与 Part 3(拒收诊断点名授权者)。
Part 2(
stack.capabilities到达 registry、_packageId打戳)已拆到 #5870 的 engine-core车道,本 PR 不碰
packages/objectql/packages/runtime/packages/lint。两半都合并后才应关闭 #4967,所以正文用 "Part of" 语义而非裸
Fixes #4967。前提复核(origin/main @ e2bfa6c)
issue 描述的机制仍然成立,行号未漂:
upsertPackageCapability的三条拒收路径(curated 平台名 / 无归属包 / 他包所有)都在这行之后才决定,一行都不写;而
security-plugin.ts把这份名单交给bootstrapSystemCapabilities当"别派生这些"的跳过表。于是「无归属包」这条既没写行、又占住了名字 —— capability 在任何一行里都不存在。写下这条声明比不写还糟。
Part 1:上报「确认有行的名字」,不是「读到的名字」
判据统一成一句(PM 方向的「派生后是否会 clobber 既有 authored 行」,逐名核实):
一个名字压掉派生,当且仅当本遍结束时
sys_capability里确实有它的行。 保护 authored行不被 humanize 占位刷掉,本来就是这份名单的全部用途;而一条没有行的拒收,不存在任何
需要保护的 authored 行。
三条拒收路径按此各自处置,理由都写进了代码:
byName里),所以这一票对门禁是 no-op,但答案是真的顺带修了两处对账缺口:
skippedUnowned计数器 —— 这条路径此前一个计数器都没有,这正是名单与计数器对不上的原因。现在每条具名声明恰好落进一个计数器。
tryFind之后。不这么做的话,一个无归属声明撞上 admin自建行时会被放去派生,而派生会覆盖该行的 label/description —— 修一个洞、开一个新洞。
这条有独立的反向测试钉住。
declaredNames同时更名为materializedNames(消费侧declaredCapabilityNames→materializedCapabilityNames)。两个符号都不在包的index.ts导出面上,改名不影响外部契约;而这次的 bug 恰恰是「名字承诺的比值兑现的多」造成的,名字说实话是最便宜的防复发。
Part 3:级别不动,消息点名授权者
按 #4632 的规则,这是功能性降级而非持久性失败,级别维持
warn。改的是内容:seeder 现在从
bootstrapPermissionSets(与派生所读的同一个数组,因此「谁授权」与「会不会被派生」出自一个来源)建
capability → permission set索引,以参数逐次传入,⛔ 不引入任何模块级状态。三种真实后果各有措辞:
meta 里另带
grantedBy: ['showcase_ops', …]供结构化消费。一处必须说清的实测偏差(issue 措辞略强)
issue 说「every
systemPermissionsgrant naming it is inert」。实测运行时判定并不查sys_capability:permission-evaluator.ts:267直接对ps.systemPermissions里的字符串取并集。所以修复恢复的是注册表一侧的 declared = enforced —— 能力有定义记录、在 Setup
可见可管理、带 provenance、能被 ADR-0066 ⑨ lint 的第 4 类来源解析 —— 而不是「把一个原本
不生效的授权变成生效」。issue 描述的核心缺陷(拒收 ⇒ 压掉派生 ⇒ 任何一行里都不存在 ⇒
比不声明还糟)完全属实,只是后果的射程比原文窄一档。changeset 按实测写,没有跟着原文放大。
测试(先红后绿,方向先声明后运行)
新增 12 条,复用文件内既有的
makeQl替身(未新建 fake engine —— 该文件在engine-double-contract.baseline.json里是unguarded: 1的 shrink-only DEBT 条目,新增一个带
update的替身会把它顶成 2 而门禁变红)。反向验证 —— 事先预测的方向是「红」:把删掉的肢体装回去(恢复无条件 push),预期
3 条依赖修复的 Part 1 钉子转红,其余保持绿。实测完全吻合:
两条
REVERSE:开头的测试按设计保持绿:它们本来就硬编码旧名单/空名单,钉的是「旧行为会掉进洞」和「不检查既有行就会覆盖 admin 行」这两个反面事实,不随实现摆动。
绿:
文件面
packages/plugins/plugin-security/**(实现 + 测试)+ 一个 changeset。未触packages/objectql/packages/runtime/packages/lint—— lint 对systemPermissions的豁免依赖派生后门的存在,而本 PR 正是把这个后门修通,所以不需要动它。
Generated by Claude Code