feat(plugin-security): 内置行写护栏 — sys_position / sys_capability 平台/应用托管行不可删改#2930
Merged
Merged
Conversation
…/ sys_capability managed rows Platform/application-published system rows (sys_position `managed_by` system/config, sys_capability `managed_by` platform/package) could be deleted or rewritten directly by a customer admin — the write went straight to the driver with no guardrail, silently breaking an app's authorization baseline. sys_permission_set already had the two-doors `assertPackageManagedWriteGate`; sys_position / sys_capability had no equivalent (ADR-0049: a provenance attribute that exists but is never enforced is exactly the gap to close). Add `assertSystemRowWriteGate`, wired into the data-write hook next to the package gate, as an unconditional data-layer boundary over both asset objects: - Refuse forging platform/package `managed_by` through the admin door (insert or update, single object or array) — closes update-to-forge. - Refuse delete / update / transfer / restore / purge on rows whose existing `managed_by` is platform/package-owned. Unlike sys_permission_set these objects have no ADR-0094 overlay write-through, so the refusal holds here rather than deferring downstream. - Admin-authored rows (`user`/∅ on sys_position, `admin` on sys_capability) are untouched — the admin fully owns those, incl. a delegate's rows in their own subtree. The gate fails closed and never depends on the caller's grants, so a superuser with modifyAllRecords cannot delete a platform position. The two objects' `managed_by` vocabularies differ, so the gate keys off a per-object provenance spec. Deny text is business-message only. No conflict with the delegated-admin gate — GOVERNED_OBJECTS excludes both objects, which govern the RBAC link tables, not the definitions. Tests: non-admin/admin × managed/admin-authored × delete/update matrix, forge + update-to-forge + bulk-array cases, filter-write probe, and the isSystem seeder bypass. Closes #2918 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5TEu5LzF1aZdE9rRaF6tV
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 14, 2026 16:15
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>
xuyushun441-sys
pushed a commit
that referenced
this pull request
Jul 17, 2026
…ole coverage The previous revision under-covered the line in three ways, now fixed: 1. 15.0.0 was missing everything outside the ADR-0095 theme from the 14.8.0..15.0.0 range: the strict view/page schema BREAKING change (ADR-0089 D3a, #2943) with its migration, the per-row import automation chain + skipAutomations + runAutomations-default-ON behavior change (#2922), the sys_position/sys_capability system-row write guardrail (#2930), the bidirectional visibility lint (#2931), and the explain posture-label alignment (#2949). 2. 15.0.0 had NO Console section at all, despite bundling the objectui 14.0 major (13.2.0→14.0.0, ~73 commits — first release-page disclosure since much of it rode unversioned in the 14.8.0 pin): the ADR-0057 ChatDock consolidation (including its breaking cleanup #2475), the Gantt batch, lists/forms/auth/Studio/i18n enhancements, two security fixes (#2485/#2410), and the six early-14.1 commits the 15.0.0 pin picked up. 3. The 15.1.0 sections were over-compressed; every domain is now expanded to its full changeset detail, and the Console 14.1 section covers all eight areas of the 94-commit range instead of seven bullets. 465 → ~1000 lines; structure mirrors v14.mdx (per-minor annotations inside one major page, per docs/releases-maintenance.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
os-zhuang
added a commit
that referenced
this pull request
Jul 17, 2026
…ole coverage (#3082) * docs(releases): expand v15 page to full 14.8→15.0→15.1 backend + Console coverage The previous revision under-covered the line in three ways, now fixed: 1. 15.0.0 was missing everything outside the ADR-0095 theme from the 14.8.0..15.0.0 range: the strict view/page schema BREAKING change (ADR-0089 D3a, #2943) with its migration, the per-row import automation chain + skipAutomations + runAutomations-default-ON behavior change (#2922), the sys_position/sys_capability system-row write guardrail (#2930), the bidirectional visibility lint (#2931), and the explain posture-label alignment (#2949). 2. 15.0.0 had NO Console section at all, despite bundling the objectui 14.0 major (13.2.0→14.0.0, ~73 commits — first release-page disclosure since much of it rode unversioned in the 14.8.0 pin): the ADR-0057 ChatDock consolidation (including its breaking cleanup #2475), the Gantt batch, lists/forms/auth/Studio/i18n enhancements, two security fixes (#2485/#2410), and the six early-14.1 commits the 15.0.0 pin picked up. 3. The 15.1.0 sections were over-compressed; every domain is now expanded to its full changeset detail, and the Console 14.1 section covers all eight areas of the 94-commit range instead of seven bullets. 465 → ~1000 lines; structure mirrors v14.mdx (per-minor annotations inside one major page, per docs/releases-maintenance.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(releases): note post-14.1 fixes carried by the final Console pin (#2615/#2617/#2619/#2620/#2621/#2623) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(releases): add v15 to the releases index; mark v14 line final at 14.8.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <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.
Closes #2918
问题
平台/应用发布的系统行(
sys_position的managed_by=system/config、sys_capability的managed_by=platform/package)可被客户管理员直接 delete / update 直达驱动——运行时没有内置写护栏。sys_permission_set早有两道门护栏assertPackageManagedWriteGate,这两个对象却缺失,管理员误删/改写一个平台岗位或应用发的岗位/能力会静默破坏该应用的授权基线(ADR-0049:provenance 字段存在却无强制 = 正是要补的 enforcement gap)。改动
新增
assertSystemRowWriteGate(packages/plugins/plugin-security/src/security-plugin.ts),接线在 data-write hook 中紧邻 package 门,对sys_position/sys_capability两个资产对象施加一道无条件数据层边界:managed_by盖成平台/应用值;只有携带isSystem的平台 seeder / 包发布路径可写。同时封堵 update-to-forge(把自建行改 badge 成托管行)。managed_by已是平台/应用值的行,delete/update/transfer/restore/purge一律拒绝。与sys_permission_set不同,这两个对象没有 ADR-0094 overlay write-through,故写护栏必须在此层直接拒绝,而非下放给下游翻译。managed_by为user/∅(sys_position)或admin(sys_capability)的行完全归管理员所有(含委派管理员在自己 subtree 内的自建行)。护栏 fail-closed 且不依赖调用方授权——持
modifyAllRecords的超管也无法删除平台岗位。两对象的managed_by词表不同,网关按对象分别判定(module-levelSYSTEM_ROW_PROVENANCE规格表)。错误信息仅含业务文案("此岗位/能力由 平台|应用包 提供,不可删除/修改")。与委派管理边界的关系
delegated-admin-gate.ts的GOVERNED_OBJECTS本就不含sys_position/sys_capability——委派管理治理的是 RBAC 链接表(assignments / bindings),而非定义对象。新护栏只拦截平台/应用托管行,管理员/委派管理员的自建行照常放行,二者不冲突。测试
security-plugin.test.ts新增system-row write gatedescribe 块(15 用例):user/ ∅ /admin)不受影响断言;isSystemseeder bypass 放行。全量
pnpm vitest run(15 文件 / 349 用例)通过,tsc --noEmit干净。关联
🤖 Generated with Claude Code
Generated by Claude Code