feat(plugin-security): A4 — managed_by 三态统一 + listView 露出 (#2920) - #2934
Merged
Conversation
Unify the record-level provenance vocabulary across sys_capability, sys_permission_set and sys_position onto platform / package / admin. - sys_permission_set.managed_by and sys_position.managed_by: text -> select (options platform/package/admin, defaultValue admin, readonly), matching sys_capability. - Writers re-stamped: built-in positions seed managed_by:'platform' (was 'system'); env/Studio-authored permission sets project managed_by:'admin' (was 'user'). - sys_position list views (active/default_positions/custom/all_positions) now surface the managed_by column. - No destructive migration: no runtime path branches on the legacy values (every access decision keys on 'package'/'platform'), so the rename never changes an authorization outcome. Built-ins/declared sets self-heal on their bootstrap upsert; new idempotent kernel:ready reconciler normalizeManagedByVocab rewrites residual legacy rows (system->platform, config->package, user->admin). - i18n: managed_by field + option labels added for all three objects across en / zh-CN / ja-JP / es-ES. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 12 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…A4 vocab) A4 unified record-level managed_by onto platform/package/admin; env-authored permission sets now project as 'admin' (formerly 'user'). Update the two showcase-permission-projection assertions the vocab change made stale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs
…ed-by-tristate # Conflicts: # packages/plugins/plugin-security/src/security-plugin.ts
os-zhuang
marked this pull request as ready for review
July 15, 2026 00:09
This was referenced Jul 15, 2026
os-zhuang
pushed a commit
that referenced
this pull request
Jul 15, 2026
…ows (#2926 ①) The system-row write gate (#2918/#2930) keyed sys_position provenance on the legacy managed_by values (system/config), but the A4 vocab unification (#2934) stamps and boot-normalizes rows to platform/package — so the gate silently stopped firing for positions, letting admins physically delete the everyone/ guest audience anchors once their bindings were removed. Guard both the canonical and legacy vocabularies, update the gate tests to the canonical values, add legacy-row regressions, and correct the stale 'no runtime path branches on legacy values' safety comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe
os-zhuang
added a commit
that referenced
this pull request
Jul 15, 2026
) * fix(plugin-security): protect platform/package-stamped sys_position rows (#2926 ①) The system-row write gate (#2918/#2930) keyed sys_position provenance on the legacy managed_by values (system/config), but the A4 vocab unification (#2934) stamps and boot-normalizes rows to platform/package — so the gate silently stopped firing for positions, letting admins physically delete the everyone/ guest audience anchors once their bindings were removed. Guard both the canonical and legacy vocabularies, update the gate tests to the canonical values, add legacy-row regressions, and correct the stale 'no runtime path branches on legacy values' safety comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe * fix(rest): pass through explicit 4xx status+code in mapDataError (#2926 ⑦) Record-scope authorization denials (plugin-sharing throws status=403, code=FORBIDDEN) degraded to a bare 400 with no code because the generic data routes call mapDataError directly, bypassing sendError's status passthrough. Add a guarded 4xx passthrough after the structured-code branches (409 envelopes keep their rich fields; 5xx still goes through the sanitizing heuristics; oversized messages fall back to generic text), and stop logging expected statuses as unhandled on the list route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe * feat(metadata-protocol): reject unknown $-prefixed query params with 400 (#2926 ⑩) Unsupported $ parameters (e.g. $foo, $inlinecount) used to fall into the implicit-filter bucket and silently match zero rows — and before the $filter alias existed, were dropped entirely, returning the unfiltered first page to callers that believed they had a filtered result set. All supported aliases are consumed before the guard, so anything $-prefixed that remains is a hard 400 UNSUPPORTED_QUERY_PARAM naming the offending keys and the supported list. Bare-key implicit equality filters are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe * feat(plugin-sharing): backfill rule grants at boot (#2926 ③) Sharing-rule grants are materialized by write hooks that deliberately skip isSystem writes, so seed-loader records never got sys_record_share rows — demo data with matching rules was broken on a fresh deploy until each record was touched at runtime. Reconcile every active rule once per boot (idempotent, best-effort per rule) right after the rule hooks bind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe * feat(plugin-sharing): sys_sharing_rule provenance + seed-not-clobber (#2909 P0/T1) sys_sharing_rule is record-authoritative (ADR-0094 addendum): declared rules are a boot seed, the row is the authority — but every boot re-ran a clobbering upsert, so an admin's active:false on an over-sharing rule was silently resurrected on redeploy. Add readonly managed_by (A4 tri-state) + customized provenance columns, put defineRule in seed mode when the bootstrap passes managedBy:'package' (pristine/legacy rows adopted and updated; admin-authored or customized rows untouched), and stamp customized via a beforeUpdate hook on any non-system edit of a seeded rule. No write gate on purpose — sharing rules stay a first-class admin authoring surface; edits are remembered, not blocked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe * test(plugin-security): lock bootstrap-declared-positions seed semantics (#2909 T2) sys_position is record-authoritative — the declared seeder refreshes only label/description and must never touch bindings, active, is_default, delegatable, or managed_by. That behavior predates these tests but was never locked; regressions would silently clobber admin state at every boot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe * fix(plugin-security): stop clobbering admin-edited capability scope (#2909 T3) scope is an admin-editable classification select on sys_capability, but the curated seeder refreshed it every boot — silently reverting admin reclassifications. Make it seed-once (insert only); label/description remain platform-owned and keep refreshing. Lock both sides with tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe * docs(adr): resolve 0094 addendum audit items; record sharing-rule seed-not-clobber tradeoff (#2909 T4) The three 'Audit that…' per-type rows are now shipped decisions: sys_sharing_rule = record-authoritative with provenance + seed-not-clobber; sys_position = seed-only identity/display, locked by test; sys_capability = seed-not-clobber with scope seed-once. Record why sharing rules got no write gate (admin tuning surface), the customized-stamp boundaries, and the scope migration cost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe * feat(app-showcase): bind persona positions on kernel:listening + announcements; document isDefault dual-track (#2926 ②⑤) A fresh deploy booted with ZERO position↔permission-set bindings — every persona silently degraded to the everyone baseline. Bindings are record-authoritative (ADR-0090/0094) and cannot be a declarative seed (the seed loader runs before the security bootstrap creates sys_position / sys_permission_set, so name references can't resolve). They are ensured imperatively by registerShowcasePositionBindings on kernel:listening — the phase that fires only after every kernel:ready handler (incl. the security bootstrap) has settled, so the referenced rows exist. everyone→member_default is bound here too: the framework only auto-binds an app's isDefault set to everyone when it is application-owned; the showcase ships as a package, so its default lands in sys_audience_binding_suggestion (pending admin confirmation) and is not live until confirmed. Also seeds showcase_announcement demo rows and documents the isDefault dual-track (app-level auto-bind vs package-level suggestion) in the spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe * fix(plugin-sharing): defer rule backfill to kernel:listening so seed rows materialize (#2926 ③) The boot backfill ran inside a kernel:ready handler, but SeedLoader also seeds on kernel:ready (raced against a budget, in a different AppPlugin handler). Since kernel:ready handlers fire sequentially in registration order, the backfill could run before the seed records exist and materialize nothing. Move the reconcile to kernel:listening (Phase 4), which the kernel fires only after every kernel:ready handler has settled — so the seeded rows are present. Verified end-to-end: a seeded red project now yields a sys_record_share to the exec recipient at boot, no runtime touch required. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bJWtKmZ2mFpRFAmmmoeqe --------- Co-authored-by: Claude <noreply@anthropic.com>
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.
A4 — 权限来源三态统一 + 露出(framework 侧)
Tracking: #2920. 跨仓配对 PR:objectstack-ai/objectui#2501。
管理员现在能用同一套词表分清权限集/岗位/能力是「平台发的 / 应用发的 / 自己建的」。三个 RBAC 目录对象的
managed_by此前各说各话,本 PR 统一为 platform / package / admin(以sys_capability为准)。改动
sys_permission_set.managed_by/sys_position.managed_by:text→ 约束select,选项platform/package/admin,defaultValue: 'admin',readonly—— 与sys_capability完全一致。bootstrapBuiltinRoles):managed_by: 'system'→'platform'。upsertEnvPermissionSet创建路径):'user'→'admin'。'package')与平台能力('platform')本就是规范值,未动。sys_position四个 listView(active/default_positions/custom/all_positions)补上managed_by列,与 capability/permission-set 视图对齐;并加入highlightFields。managed_by字段 + 选项标签(platform/package/admin),三对象 × 四语言(en / zh-CN / ja-JP / es-ES)补齐;顺带补sys_capability的 scope 选项标签。历史数据兼容策略(无破坏性 migration)
关键取舍:没有一处运行时逻辑对分歧的旧值分支——所有访问决策只读
'package'/'platform'(两者跨三对象本就一致、未改),因此这次是纯展示词表重命名,永不改变任何鉴权结果。bootstrapupsert 时自愈到规范值。'user'、旧岗位的system/config/user)由新增的幂等kernel:ready协调器normalizeManagedByVocab重写(system→platform、config→package、user→admin)。字段为readonly,故以isSystem上下文写入。bootstrapBuiltinRoles/bootstrapDeclaredPermissions等皆如此),与之保持一致。验证
pnpm --filter @objectstack/plugin-security test—— 341 passed(16 files)。pnpm --filter @objectstack/plugin-security build—— ESM/CJS/DTS 均通过。normalize-managed-by.test.ts(重写正确性 + 幂等 + 空 ql 容错);rbac-objects.test.ts新增三对象词表一致性 + 岗位视图列断言;projection/anchor 测试的创建路径断言随写入端更新(user→admin、system→platform)。存疑点
managed_by由text→select。select只在写入时校验选项、读取不校验,故残留旧值行仅在协调器跑到前展示原始字符串,无硬失败。security-plugin的 package 伪造闸(仍只认'package'),亦未改对象级managedBy: 'config'(那是 schema 保护级别,与记录级managed_by字段无关)。🤖 Generated with Claude Code
https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs