Skip to content

refactor(plugin-security): make sys_permission_set a pure projection of the metadata layer (ADR-0094)#2901

Merged
os-zhuang merged 2 commits into
mainfrom
claude/sys-permission-set-projection-2c7w0h
Jul 14, 2026
Merged

refactor(plugin-security): make sys_permission_set a pure projection of the metadata layer (ADR-0094)#2901
os-zhuang merged 2 commits into
mainfrom
claude/sys-permission-set-projection-2c7w0h

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #2875(根因修复;#2867 的投影补丁是最终一致的胶水,本 PR 用结构性手段替代它)。

方案(ADR-0094,随 PR 一同提交)

权限集定义(label / description / 六组 facet / adminScope / active)从此只有一个权威存储:元数据层(包声明 + sys_metadata overlay,按 layered read 的 overlay-wins 合并)。sys_permission_set 数据记录降级为纯投影(只读派生物),并且是结构性保证,不再依赖"新写入路径可能忘记触发"的订阅器:

  1. 数据门写穿(write-through) — 所有非 system 的 sys_permission_set 数据面写入(Setup 通用 CRUD、批量导入、任何走 ObjectQL 的未来 API)在引擎中间件咽喉点被重定向为元数据写入(saveMetaItem / deleteMetaItem),驱动层写入不再发生。中间件注册在安全中间件之内,two-doors 门、delegated-admin 门、CRUD/FLS 检查全部先行生效。
  2. awaited 投影metadata-protocol 新增 registerMutationProjector(type, fn):在 saveMetaItem / publishMetaItem / deleteMetaItem 持久化之后、返回之前 await 执行,结果以 projectionApplied 呈现在响应上。Studio 保存返回时记录已一致——无投影竞态(fix(plugin-security): project env-scope permission-set edits onto sys_permission_set (#2857) #2867 的订阅器是 fire-and-forget)。投影器是记录的唯一写入者
  3. 启动收敛 + 一次性回填 — env overlay 全量投影到记录(Studio 创建的权限集终于出现在 Setup);仅存在于数据面的历史记录一次性回填进元数据存储(强制执行语义不变:evaluator 的 db fallback 本来就解析这一 body);与既有定义漂移的记录按元数据重投影(元数据胜——该漂移从未被强制执行过)。

顺带修复的一致性缺陷(审计中发现)

  • Setup 编辑声明集从未生效:evaluator 解析是 metadata-first,数据门编辑只落在记录上,显示变了、强制执行没变。现在数据门编辑生成 env overlay,真正生效。
  • manager list() registry-first vs layered read overlay-wins:声明集的 env overlay 显示生效但 evaluator 仍用声明体。投影器现在同步 manager 内存注册表条目(带 _envProjection 标记,防止冒充 artifact),显示与强制执行不再分叉。
  • 删除 runtime-only 定义后,deleteMetaItem 的 registry 自愈会把 manager 视图重新注册回引擎注册表,产生"僵尸声明"导致定义删不掉——声明基线改从引擎 SchemaRegistry 读取,并跳过 runtime shadow(_packageId: 'sys_metadata')与投影回声。

行为变化(changeset 已注明)

  • 数据门"删除" artifact-backed 权限集 = 重置为声明体(定义随应用发布,环境删不掉;此前删除产生幽灵:行没了、强制执行照旧)。
  • 数据门改名被拒(400)——name 是元数据身份,请 clone + 删除。
  • 首次启动时,被元数据定义遮蔽的历史记录漂移会被丢弃(响亮告警;该漂移从未被强制执行)。

验证

  • 单测:plugin-security 326 通过(新增投影/写穿/收敛套件),metadata-protocol 23 通过(新增 projector 契约),全仓 pnpm test 通过。
  • e2e(showcase --preset=full 实跑):创建→记录由投影器生成(managed_by:'user' + overlay 落盘);PATCH→overlay 更新、记录同步、未提及 facet 保留;删除 runtime-only→定义与记录同退;编辑/删除 member_default(声明集)→生成 overlay / 重置为声明体;package-owned 集仍被 two-doors 门拒绝。

遗留

🤖 Generated with Claude Code

https://claude.ai/code/session_01SXjD7g2JkEsdqhZFZgAo1Q


Generated by Claude Code

…of the metadata layer (ADR-0094)

Retire the two-store split-brain behind the #2857 display-freshness class
(#2875): the metadata layer (packaged declarations + sys_metadata overlay,
merged overlay-wins) is now the ONE authoritative store for permission-set
definitions, and the queryable sys_permission_set record is a derived
read-model — enforced structurally, not by a subscriber a new write path
might forget.

- metadata-protocol: new registerMutationProjector(type, fn) — an awaited,
  best-effort per-type hook run after persistence inside saveMetaItem /
  publishMetaItem / deleteMetaItem, surfaced as `projectionApplied` on the
  response. A derived read-model is consistent before the write returns
  (the #2867 onMetadataMutation subscriber was fire-and-forget).
- plugin-security: new permission-set-projection module:
  * data-door write-through — every non-system CRUD write on
    sys_permission_set is redirected into the metadata store at the engine
    middleware choke point (after the two-doors + delegated-admin + CRUD/FLS
    gates); the record is written only by the projector;
  * the projector re-reads the layered effective body, upserts/creates the
    env record (Studio-authored sets finally appear in Setup), retires it
    when the definition is gone, resets it to the declared body when an
    overlay tombstone reveals the artifact baseline, and syncs the metadata
    manager's in-memory `permission` entry so evaluator resolution
    (registry-first list) can no longer disagree with the display;
  * boot reconciliation — env overlays project onto records, legacy
    data-door-only records are backfilled into metadata once, and records
    that drifted from an existing definition are re-projected (metadata
    wins; the drift was never enforced);
  * declared (artifact) baselines are read from the engine SchemaRegistry,
    skipping runtime shadows and projection echoes, so a deleted
    runtime-only definition cannot zombie back as "declared".

Behavior changes: deleting an artifact-backed set through the data door now
RESETS it to its declared body instead of removing the row; renames through
the data door are rejected (400); pre-existing record drift shadowed by a
metadata definition is discarded loudly at first boot.

Follow-up: rejecting env-scope overlays of package-owned sets at authoring
time is tracked in #2898.

Closes #2875

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXjD7g2JkEsdqhZFZgAo1Q
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Canceled Canceled Jul 14, 2026 6:53am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/xl labels Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/plugin-security.

12 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…authorization.mdx

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXjD7g2JkEsdqhZFZgAo1Q
@os-zhuang os-zhuang marked this pull request as ready for review July 14, 2026 07:36
@os-zhuang os-zhuang merged commit 1dede32 into main Jul 14, 2026
16 checks passed
@os-zhuang os-zhuang deleted the claude/sys-permission-set-projection-2c7w0h branch July 14, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(plugin-security): make sys_permission_set data record a pure projection (retire the two-store split-brain)

2 participants