feat(plugin-security): C2-β — explain 引擎 record 粒度行级归因 (#2920)#2939
Merged
Conversation
带 recordId 时,explain 在对象级流水线上叠加行级归因,全部复用 enforcement 同一批函数(explained-by-construction):
- tenant_isolation Layer 0 prepend + 每层 kernelTier(layer_0_tenant vs layer_1_business)
- 每层 record 归因(tenant/owd_baseline/sharing/rls):outcome、有效 rowFilter、
matchesRecord(用 @objectstack/formula 的 matchesFilterCondition 求值)、命中 rules[]
- 顶层 record 判定(visible + decidedBy);读走复合行过滤匹配,写走 sharing service canEdit
- principal.posture(ADR-0095 D2 B2 stand-in 派生,留 TODO)
- computeRlsFilter 拆为 computeLayeredRlsFilter({layer0,layer1})单一路径,不与执行漂移
- REST security.explain 接受可选 recordId
无 recordId 的对象级请求输出 byte-identical(向后兼容)。
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): 15 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 15, 2026 00:41
…e derivePosture Merge B2/B4 into C2-β and replace the explain-engine posture stand-in's PLATFORM_ADMIN/TENANT_ADMIN/MEMBER derivation with the single core derivePosture (ADR-0095 D3 — capability-derived, closes the TODO(B2)). The guest→EXTERNAL mapping stays in the explanation surface (the enforcement resolver's floor is MEMBER). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs
This was referenced Jul 15, 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.
C2-β — explain 引擎 record 粒度行级归因
引擎侧实现,基于已合并的 C2-α 契约(
explain.zod.ts的recordId/ExplainMatchedRule/ExplainRecordAttribution/ExplainDecision.record/ExplainLayer.kernelTier/AuthzPosture/tenant_isolation层)。tracking issue #2920。UI 侧对端 PR:objectstack-ai/objectui#2502(同分支名)。
实现结构
explain(principal, object, operation, recordId?):透传recordId;缺省时对象级行为完全不变。带recordId时叠加行级归因,全部复用 enforcement 同一批函数(explained-by-construction):tenant_isolationLayer 0:作为永远最先的层 prepend;每层打kernelTier(layer_0_tenantvslayer_1_business),区分「租户墙挡」还是「业务 RLS 挡」。record归因(tenant / owd_baseline / sharing / rls):outcome(admitted/excluded/not_evaluated)、有效rowFilter、matchesRecord(用@objectstack/formula的matchesFilterCondition对同一条 FilterCondition 求值——第三个 canonical backend,与查询/SQL 一致)、命中rules[](kind ∈ tenant_filter/owd_baseline/ownership/record_share/sharing_rule/team/rls_policy,含 grants/via/effect)。record判定:visible+decidedBy决定性层。读走复合行过滤匹配,写走 sharing service 的canEdit(均为 enforcement 原语)。principal.posture:ADR-0095 D2 档位的 B2 stand-in 派生(复用resolveAuthzContext已投影的platform_admin/ org-admin 角色证据)。B2 未合并,标注TODO(B2)待换成正式 posture resolver;当前四档能填满(PLATFORM_ADMIN/TENANT_ADMIN/MEMBER/EXTERNAL)。explained-by-construction / 无漂移
computeRlsFilter重构为computeLayeredRlsFilter(暴露{ layer0, layer1 }拆分)+ 薄andComposeLayers包装 —— 单一代码路径,行级归因取自与有效过滤同源的 layer0/layer1。matchesFilterCondition;写用 sharing servicecanEdit;shares 来自listShares。不另写第二套判定。not_evaluated,record 路径仍工作。REST
security.explain(GET/POST)接受可选recordId(经ExplainRequestSchema校验)。向后兼容
无
recordId的对象级请求输出 byte-identical:9 层不变、无tenant_isolation、无kernelTier、无posture、无record。已有对象级测试无改动全绿。验证
explain-engine.test.ts:14 原测(对象级向后兼容)+ 10 新测(带/不带 recordId、Layer 0 vs Layer 1 归因、共享准入/排除、记录不存在、写走 canEdit、无 deps 降级)= 24 全绿。pnpm --filter @objectstack/plugin-security test= 376 全绿。plugin-security/rest包 build(含 DTS tsc)通过。存疑点
rules[]中标neutral(诚实,不臆断);顶层visible仍由 sharing 复合过滤/canEdit 决定,不受影响。Closes part of #2920.
🤖 Generated with Claude Code
https://claude.ai/code/session_019QRUvVfpvSycAHMMF2xTxs