Skip to content

Tracking: ADR-0105 — group tenancy posture; organization scope as a first-class authorization dimension #3541

Closed
2 of 2 issues completed

Description

@os-zhuang

Tracking issue for ADR-0105 (proposed via #3499).

Status: Phase 0, Phase 1, and Phase 2 D8+D9 COMPLETE (re-evaluated 2026-07-30 against ADR-0108/0111/0112/0113 — see the re-evaluation comment). Engine landed in #3559 (activation corrected in #3570, ADR amended in #3603; driver union scope fixed in #3631); console affordances in objectstack-ai/objectui#2858; acceptance met by the three-plants-one-group dogfood (objectstack-ai/cloud#882 + objectstack-ai/cloud#883 — 9/9 assertions passing in BOTH entitlement states). D8 complete end-to-end (#3645 → #3663 → #3674 → #3695 → #3722 → #3767; the membership-role channel its placement replaces is closed by ADR-0108/#3802); D9 landed (#3824 + #3873). Remaining: D10 (blocked on its follow-up ADR), D13, "all my organizations" affordances, Phase 3. ADR status line updated in #4028.

Summary

Group-shaped customers (multi-plant manufacturing, multi-branch retail, holding structures) need one shared database, organization as the membership boundary, group-wide visibility, and cross-org workflows — a middle posture between today's single and multi. The ADR adds a group tenancy posture with MOAC-style union semantics (organization_id IN accessible_org_ids), engine-enforced, preserving ADR-0095 W1/W2 invariants.

Plan evaluation (2026-07-27)

Findings verified against main: F1 (security-plugin.ts substring strip — #3539), F2 (organization_admin wildcard VAMA + auto-org-admin-grant.ts — #3540), F3 (PermissionSet.contextVariables, ExecutionContext.rlsMembership, territory.zod.ts — all consumer-less), F4 (ADR-0092 whitelist has one entry). The technical premises held.

Design assessment: sound, and it survived implementation. D2 widened the existing Layer 0 predicate from equality to set membership; computeTenantLayer0Filter was already a pure function with the PLATFORM_ADMIN-rung exemption and a fail-closed empty-org path, so the delta stayed contained.

Two corrections to the ADR's own line references, found during implementation:

  • PermissionSet.contextVariables was at permission.zod.ts:308, not :282.
  • The approval org-scoping seam is approval-node.ts:147 (and substantively approval-service.ts:748-768 / :967), not :118.

One ADR claim not confirmed: the "existing 60s cache staleness envelope" the Consequences section attributes to the resolver does not exist there — resolveAuthzContext re-runs per request with no TTL. The 60s window belongs to the better-auth session snapshot cache (auth-manager.ts). D2's resolver therefore adds a per-request read, not a cached one; if that cost matters at scale it needs its own decision.

One implementation finding beyond the ADR: the #2937 write post-image check required a non-array payload, so a BULK insert could carry a forged organization_id per row — the same defect one call site down. Closed as part of D5.

Dogfood findings (the acceptance run paid for itself): #3623 (driver-level native tenant scoping collapsed group union reads to active-org equality — fixed in #3631), #3624 (org creation over HTTP 500s with teams enabled: better-auth 1.7 memberCount vs sys_team — CLOSED by #3647, see the 07-27 comment for the full sweep it triggered), cloud#881 (per-org seed replay primary path never engaged — fixed in cloud#884).

Repo split (per cloud ADR-0016 iron rule 强制免费、治理收费, cloud ADR-0081)

This repo (open, framework): all enforcement primitives (code) — shipped in #3559 + #3631; posture activation is entitled (#3570). Scoped-invitation and delegated-admin console UX live open in objectui's app-shell (correction recorded in the 07-27 D8 comment — cloud sells the runtime that entitles the posture, not the console screens).

cloud repo (commercial, @objectstack/organizations): org lifecycle management, grouping/registry UI, cross-org approval templates, master-data distribution management, per-org seed/config replay, org analytics, D13 promotion tooling. Cloud-side tracking issue: objectstack-ai/cloud#874; acceptance/regression harness: objectstack-ai/cloud#880 (apps/ee-group-showcase, DONE).

D12 line — corrected (#3570; ADR amended in #3603): the open-activation reading shipped in #3559 was reviewed by the founder and ruled wrong — it made the stronger multi-org posture free while isolated stayed entitled, inverting ADR-0081 D2, and it opened a silent-degradation hole (OS_TENANCY_POSTURE=group skipped both the enterprise package load and the ADR-0093 D5 fail-fast). #3570 made group an entitlement again: it probes org-scoping exactly like isolated; without the enterprise runtime the posture resolves to single + degraded, and an os serve boot configured for group refuses to start instead of silently running single-org. Iron-rule safety is satisfied by refusing to run an unwalled group boot, not by free activation — open code is not free activation. Both multi-org postures are @objectstack/organizations capability, and the runtime declares WHICH shapes it entitles (supportedPostures, cloud#875) with the open core failing closed on anything undeclared.

Phases

Phase 0 — correctness (open, immediate, independent of D1) — ✅ landed in #3559

Phase 1 — group posture MVP — ✅ COMPLETE

Phase 2 — group product depth

Phase 3 — governance (commercial, cloud repo)

  • SoD constraints, certification campaigns (ADR-0091 data), deny/muting layer (ADR-0066 ⑦⑧), generalized dimension security (territory's successor — ADR-0111 has since reserved the per-verb capability-mask direction for it)

Refs: ADR-0093, ADR-0095, ADR-0090, ADR-0091, ADR-0092, ADR-0049, ADR-0108, ADR-0111; cloud ADR-0016, cloud ADR-0081.

Activity

  1. os-zhuang commented on Jul 27, 2026

    @os-zhuang
    ContributorAuthor

    Dogfood findings 更新:#3624 已关闭

    正文 "Dogfood findings" 一节里 #3624 仍标着 OPEN,现在已修复并合并,顺带把它牵出的一串问题一并处理了。更新如下:

    #3624(org creation over HTTP 500s with teams enabled:better-auth 1.7 memberCount vs sys_team)—— CLOSED(#3647)

    实际修掉的比报告的多

    原报告只提到 team.memberCount。同一次 better-auth 1.7 升级实际漏了三个 model 的四个列:

    model 列 影响
    team memberCount organization/create 500(org 行已提交,留下半成品组织)
    teamMember membershipKey add-team-member 500
    twoFactor failedVerificationCount / lockedUntil 2FA 注册和输错验证码路径双双 500

    two-factor 那一对不是报告发现的,是新建的 parity gate 第一次运行时抓出来的 —— 那条路径当时零测试覆盖,所以列能凭空消失而没有任何东西变红。

    相关落地

    PR
    #3647 四个列 + better-auth parity gate + 端到端测试
    #3659 / #3667 2FA 锁定路径完整状态机测试(计数、清零、落锁、锁定期拒绝正确码、过期自动清理)
    #3670 / #3683 翻译 bundle 漂移清理 + os i18n extract --check 新鲜度 gate
    #3688 sso/scim parity 缺口 + sys_scim_provider.provider_key
    cloud#885 本 tracking issue 的 Phase 1 侧:apps/ee-group-showcase 现在用真实 organization/create 路由跑 group posture 多组织建组织

    cloud#885 顺带订正了 group-posture.dogfood.test.ts 里 seedOrg 的注释 —— 它此前把这个 500 记作活跃缺陷,现在改回"设计取舍"(Phase 1 断言的是 membership 对墙的含义,不是建组织 UX)。

    由此新开、尚未决策的

    Phase 1 的 checklist 本身无需改动 —— 上述都不影响 "Phase 1 COMPLETE" 的结论。


    Generated by Claude Code

  2. os-zhuang commented on Jul 27, 2026

    @os-zhuang
    ContributorAuthor

    D8 status — engine + console complete; scope-bounded issuance blocked on #3697

    The Phase 2 D8 line can be updated. Everything except one thing is done, and that thing turned out to be a design gap rather than remaining work.

    Landed:

    A correction to this issue's repo split. The body assigns "scoped-invitation UX" to cloud. That was wrong and I moved it: cloud has no invite UI at all — OrganizationsPage / MembersPage / InvitationsPage / InviteMemberDialog all already live open in objectui's app-shell. Cloud sells the runtime that entitles the posture, not the console screens.

    Blocked, and the reason is worth recording: #3697. The scope-bounded issuance path has no caller. better-auth grants invitation: ["create"] to owner/admin only, and under group posture the framework auto-elevates exactly those roles to organization_admin — whose wildcard modifyAllRecords is isTenantAdmin(), for whom DelegatedAdminGate.assert short-circuits. The two sets are disjoint, so placement is bounded by the Layer 0 org wall, never by adminScope.

    Not a security hole (the wall still bounds every issuer to their own org), but it hollows out D8's motivating story — "a plant admin invites into their own subtree" only bites when the plant admin is not an org admin, and that principal cannot issue at all. #3697 proposes registering a delegated_admin org role carrying that one statement; the gate, the read surface, and the picker are all already built.

    Both findings came from the cloud dogfood, not from review. #3695's own unit tests passed because they asserted the context was forwarded, never that it carried authority — the same class of miss as #3623 earlier in this issue. That is twice now the showcase has paid for itself.


    Generated by Claude Code

  3. self-assigned this
    on Jul 28, 2026
  4. os-zhuang commented on Jul 30, 2026

    @os-zhuang
    ContributorAuthor

    重新评估(2026-07-30)— 对照新增 ADR-0108 / 0111 / 0112 / 0113

    上次评估(7-27/28)以来 main 上新增了四篇 ADR。逐一对照本 tracking 的计划,结论:计划方向不变;Phase 2 的 D8/D9 已于 7-28 全部落地,正文 checklist 已同步。真正因新 ADR 而变化的是两点:D8 的收口方式(升级成了一篇独立 ADR),和 Phase 3 一项拿到了方向锚点。ADR-0105 自己的 status 行还停在 "Phase 0/1 implemented",随 #4028 补记。

    ADR-0108 —— D8 线的正式收口(本次重评的核心)

    D8 落地后暴露的 #3697(scope-bounded issuance 无调用者)沿 #3722(delegated_admin 角色 + invitation role cap,二者必须同船——裸角色版本会打开四步提权链)→ #3767(sys_member 进 GOVERNED_OBJECTS,"membership 不是可委派能力")修完,最终上升为 ADR-0108(Accepted 2026-07-28,#3802 实现):sys_member.role 词表封闭为 owner / admin / delegated_admin / member,additionalOrgRoles 整体移除(逆转未发布的 #3747/#3779),并确立 D8 的 invitation placement 是"带能力入场"的唯一通道——对被废通道是严格超集:delegated admin 也能发起、写真实 sys_user_position 行、带 granted_by + ADR-0091 validity window、受 subtree/allowlist/strict containment 约束。

    对本 tracking 的意义:D8 从"engine seam 已落地、其余待做"变为全链路完成且被第二篇 ADR 结构性锁死——不是修好了,是这一类问题不可再现。#3697 的两个 non-goal 中 sys_member 纳管已被 #3767 关闭;attributed cancel 仍开放但已记录。剩余唯一跟进在 objectui:词表镜像从 @objectstack/spec 派生(ADR-0108 consequences;standing instruction 已在 objectui#2907 修正)。

    D9 —— 已落地,ADR 文本随 PR 修订

    #3824 落地时对原文补了四个实现裁决:per-approver 而非 per-node(否则 per_group 并行会签无法表达);$root/$parent 符号优先(流程元数据可移植,org id 是部署期数据,AI 作者在 authoring 时不可能知道);合法性 = 同 parent_organization_id 根、只看组织树不看 submitter;非 group posture 运行时拒绝而非静默忽略(posture 是环境配置,lint 看不见)。#3873 随后修复 directory-less 类型(user/field/manager)early-return 绕过 D9 检查造成的 silently-inert——又是 cloud dogfood 抓到的,不是 review。showcase 第三次收回成本(前两次:#3623、#3695)。

    ADR-0111 —— 相邻授权面,三个交界点

    1. 组合性构造上成立,但从未被验证。ADR-0111 的管理权限探针(canManageShares、D1 DEPTH 的 resolveWriteScope)走 BU 树;D6 把层级留在 org 内,Layer 0 墙仍是外边界,所以 DEPTH 管理权限不会跨 org。但 group dogfood(cloud#880/chore: version packages #882/[Bug] Auth 服务 /api/v1/auth/* 路由无法访问(插件注册顺序导致未注册) #883)全部写于 ADR-0111 之前,sharing 管理权限在 group posture 的 union 读语义下零断言。建议:给 ee-group-showcase 加一组 sharing-authority × group posture 行——plant A 的 share-manager 不能管到 plant B 记录上的 share;group 侧成员经 union 读到 plant 记录后,canManageShares/DEPTH 不因可见而放行。这延续 showcase 三次抓真缺陷的记录,属于低成本高回报。
    2. Phase 3 的 "generalized dimension security(territory 继任者)" 拿到方向锚点:ADR-0111 明确保留 per-verb capability mask(Dataverse access-mask 式)方向——"若要 per-record delete grant,是 mask,不是第四个 access_level"。正文 Phase 3 行已加注。
    3. ADR-0111 D1 DEPTH 的 enterprise hierarchy-scope-resolver(framework 无 resolver 时 fail closed)复用了 D12 的 entitlement 模式——这是第二个按该模式落地的 enterprise seam,模式经受住了复用。

    ADR-0112 / 0113 —— 无影响

    本线错误码(RLS_DENY_FILTER 等)本就 SCREAMING_SNAKE,两轮 sweep(#3967/#4003/#4021)未触及;ADR-0113(required write contract)与 D5 的 organization_id stamping 不相交。

    剩余项

    项 状态
    D10 layered master data 仍阻塞——ADR-0106…0113 没有一篇是它的 follow-up ADR;这是 Phase 2 唯一剩余的框架侧设计工作
    D13 promotion tool 未开工(框架内无代码;repo split 归 cloud,cloud#874)
    "All my organizations" affordances 读视图已随 objectui#2858 在 Phase 1 落地;剩余为 cloud 侧 grouping/registry UI 及更深 console affordances,范围待定
    Phase 3 不变,cloud 侧;dimension security 方向见上

    建议的下一步排序: ① sharing-authority × group posture 的 showcase 断言(上面第 1 点,验证债);② D10 follow-up ADR(设计债,解锁 Phase 2 收尾);③ D13 归 cloud 排期。


    Generated by Claude Code

  5. os-zhuang commented on Jul 30, 2026

    @os-zhuang
    ContributorAuthor

    收官(2026-07-30)

    本 issue 的验收范围——ADR-0105 框架侧执行面(Phase 0、Phase 1、Phase 2 D8/D9)——已全部落地并经 dogfood 验证(9/9 断言、双 entitlement 状态;D8/D9 各自的 e2e 见正文)。按 7-30 重新评估的结论收官:剩余项没有一项是框架侧可动工的代码,且彼此没有共同交付弧线,不再保留 umbrella,拆分如下:

    剩余项 去处
    D10 layered master data #4030 —— 等待它自己的 follow-up ADR(写出时按惯例带 proposal PR + 独立 tracking,如本 ADR 之于 #3499→#3541)
    sharing-authority × group posture showcase 断言(重评建议 ①) objectstack-ai/cloud#919 —— 随 ADR-0111 cloud 侧 wiring 的同一次 SHA bump
    D13 promotion tool、Phase 3 governance objectstack-ai/cloud#874(原有归属,repo split 未变)
    "All my organizations" affordances 读视图已在 Phase 1(objectui#2858);其余范围未定义,拾起时先 scoping,cloud 侧部分已在 cloud#874

    ADR-0105 status 行的补记在 #4028(独立 docs PR,不受本次关闭影响)。后续凡涉及组织轴的新决策,以 ADR-0105/0108/0111 三篇为基线各自开 tracking。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions