Repository navigation
organization/create 的闸门判「请求的 posture」还是「实际生效的 posture」?降级部署(D5)下两者分叉,闸门放行而 /auth/config 隐藏 #5261
Description
Activity
⛔ 认领:
session_015W6nhsDrz6zWQc8je12a1t(objectstack:identity 车道 PM),分支claude/issue-5261-effective-posture-gate,os-dev 单点派发。维护者 2026-08-04 裁定:选 B(闸门改判「实际生效的 posture」),并入 v17。 这是一次能力收缩、破坏性变更,只能搭主版本车——错过 v17 就要等 v18,所以现在做。
裁定同时接受本单「倾向 A」里点名的两笔代价,并按 issue 自己的建议处置第二笔:
- 没装企业包的 OSS 自托管部署将无法创建组织 —— 有意为之(降级态不再产出声明了但没强制的组织边界,ADR-0049 最讨厌的那一类)。
packages/qa/dogfood/test/org-create-default-team.dogfood.test.ts(org creation over HTTP 500s when teams are enabled: better-auth 1.7.0-rc.1 default team carries memberCount, sys_team lacks the column/mapping #3624 回归)靠「boot 后翻 env、闸门 live 读」开路,选 B 后任何 env 组合都过不了 —— 不允许直接删掉。按本单建议给 dogfood 另一条开路方式(如 verify harness 提供可注入的 tenancy stub),保住 org creation over HTTP 500s when teams are enabled: better-auth 1.7.0-rc.1 default team carries memberCount, sys_team lacks the column/mapping #3624 的端到端覆盖。
⚠️ packages/plugins/plugin-auth/src/org-create-posture-gate.test.ts里带pinned as CURRENT behaviour (#5261)的用例是故意钉住旧行为的,本次要有意识地翻转它并改掉用例名/注释,不要留下自相矛盾的钉子。文件面:
packages/plugins/plugin-auth/src/auth-manager.ts+org-create-posture-gate.test.ts+ dogfood 开路所需改动 + changeset。⛔ 不碰reconcile-membership.ts(#5205 排在本条之后)、packages/objectql/plugin-dev/runtime/driver-sql/cli(#5262 在改)、content/docs/releases/。
Generated by Claude Code
PM 验收(第 19 轮,identity 车道):ACCEPT — PR #5294,CI 绿后合入
维护者裁定选 B、搭 v17 主版本车,实现到位。逐条复核:
dev 找到一条我没料到的独立佐证,我已核实为真。
packages/spec/src/kernel/public-auth-features.ts:153-155原文:'Reflects ACTUAL multi-tenancy capability (the tenancy posture enforces ' … 'not just the requested posture.'也就是说 spec 早就声明
multiOrgEnabled反映的是实际能力而非请求的姿态,并由它把守sys_organization.actions.create_organization。出线的一直是路由,不是 flag —— 这次改动不只是维护者的一个取舍,它让路由回到了自己的声明契约上。这条把方案 B 从「二选一」提升成「修正违约」,是本单最有价值的发现。两站点从「两处写法碰巧一致」变成「一次求值」:判据抽成
effectiveTenancyPosture(),getPublicConfig()调同一个方法。仍是 per-request live 读不缓存,理由正确 ——org-scoping在 plugin-auth 之后注册,墙确实是 boot 中途立起来的,冻结任一层都会把部署钉死在立墙前的状态(AGENTS.md startup-registry 规则)。代价二没有靠删测试解决,这是我在派单里划的红线,dev 守住了:
@objectstack/verify新增multiTenant: 'posture-only',注册内置org-scoping替身,让 tenancy 服务解析出真实、非降级的isolated。它比原来的 env-flip 更诚实 —— 原来是单租户栈只对一个闸门撒谎,现在模拟的是真实的有墙部署。dogfood 另加一条「守卫的守卫」,直接断言posture==='isolated'、degraded===false、multiOrgEnabled===true,替身失效时报的是「前置条件塌了」而不是把人引去查一个不存在的 team-insert bug。替身的边界也写死了:不做任何隔离,并点名「用它断言跨租户隔离 = #4700 那个 constant-false 能力探针反过来戴一次面具」;跨租户 gate 在本仓继续 skip 而不是假装通过。harness.posture-only.test.ts刻意不 mock@objectstack/organizations,因为待证命题正是「它不需要企业包」—— 旁边摆个 mock 会让命题不可证伪。这个反向推理是对的。那颗自相矛盾的钉子清干净了:断言/用例名/注释/文件头一起翻,全仓
pinned as CURRENT behaviour只剩叙述历史的散文。不变量表取消了「降级除外」的例外 —— 例外的消失本身就是这次改动的证明,新增三行降级用例。对抗性验证真实执行:回退闸门判据 →
7 failed | 18 passed,expected 200 to be 403,红的正是两种降级、双 env knob、late-probe 与三行不变量表;把 dogfood 换回旧 env-flip →expected 'single' to be 'isolated'+ 403,证明代价二真实存在且新开路方式是承重的,不是摆设。我替 dev 扫了它点名的那条合入前风险(cloud 侧是否有依赖 D5 降级态还能建组织的 e2e):没有,cloud 不受影响。
apps/ee-group-showcase/test/org-create-route.dogfood.test.ts文件头那句「open workspace has no @objectstack/organizations, so it opens the route by flipping OS_MULTI_ORG_ENABLED」描述的是 framework 自己那条单租户测试(正是本 PR 换掉的那个绕法);cloud 这条测试自身import { OrganizationsPlugin }并以真实企业运行时在group姿态启动 —— 非降级态,判决不变。cloud 仓内就有packages/organizations,EE 各栈同理。关于
BootOptions是已发布包公共面这一点(dev 提的第一条 note):我认可现在的做法。它是测试基础设施的公共面,plugin-auth 生产路径零新增 API,替身走的是企业包本来就在用的org-scoping注册接缝;替代方案(每个 fixture 各造一份替身)会失去那一处集中写死「它不隔离任何东西」的文档——而那句文档正是防止它被误用的东西。证据边界如实:本仓装不了 cloud-private 的
@objectstack/organizations,所以「有企业包时墙真的立着并隔离数据」不在本 PR 证据范围内;本 PR 只证明 posture 生效则放行、不生效则拒绝。changeset 为 plugin-auth major + verify minor,与破坏性收缩相符。CI 绿即合入。
Generated by Claude Code
- added a commit that references this issue
on Aug 5, 2026 - added a commit that references this issue
on Sep 9, 2026
从 #5233 的修复中分出来的一个需要维护者定夺的语义问题,#5233 本身没有改动它(那是纠正 knob,不是改能力边界)。
事实
修好 #5233 之后,
packages/plugins/plugin-auth/src/auth-manager.ts的两个站点读的是两个不同的事实,在一种部署形状下会分叉:organizationHooks.beforeCreateOrganizationpostureEnforcesWall(resolveTenancyPosture())/auth/config的features.multiOrgEnabledtenancy?.posture ?? resolveTenancyPosture()只有一种形状让两者不同 —— ADR-0093 D5 的降级态:请求了
isolated/group,但企业包@objectstack/organizations不在(org-scoping服务缺席),于是tenancy.posture解析为single且degraded=true。此时:isolated);/auth/config:multiOrgEnabled=false,console 把「创建组织」入口藏起来;这不是 #5233 引入的
降级前闸门读
resolveMultiOrgEnabled(),那同样是一个纯 env 读,在这里同样返回true、同样放行。#5233 只是把 knob 从被降级的布尔换成权威的 posture,分叉是原样保留的。已在packages/plugins/plugin-auth/src/org-create-posture-gate.test.ts里作为当前行为钉住(用例名带 "pinned as CURRENT behaviour"),所以无论怎么定夺,都必须是有意识地改这条断言。两个选项
A. 保持现状 —— 闸门判「请求的 posture」。
serve.ts的 D5 boot guard 同源(它也是resolveTenancyPosture() !== 'single'),两处口径一致。serve在降级时默认拒绝启动,要OS_ALLOW_DEGRADED_TENANCY=1才走;既然操作者已经明说「我接受没有墙」,那么再单独禁掉建组织意义不大。/auth/config藏按钮而 API 放行,这个分叉长期存在;而且降级部署里建出来的组织边界是「声明了但没强制」——ADR-0049 最讨厌的那一类。B. 闸门改判「实际生效的 posture」(
tenancy?.posture ?? resolveTenancyPosture())。packages/qa/dogfood/test/org-create-default-team.dogfood.test.ts(org creation over HTTP 500s when teams are enabled: better-auth 1.7.0-rc.1 default team carries memberCount, sys_team lacks the column/mapping #3624 的回归)靠的正是「boot 之后翻 env、闸门 live 读」这个技巧来打开路由;它跑在没有企业包的 workspace 里,选 B 之后任何 env 组合都过不了这道闸门,该 dogfood 只能改写或删掉,org creation over HTTP 500s when teams are enabled: better-auth 1.7.0-rc.1 default team carries memberCount, sys_team lacks the column/mapping #3624 的端到端覆盖随之丢失。倾向
倾向 A(即现状),因为降级已经是
OS_ALLOW_DEGRADED_TENANCY的显式选择,B 的收益主要落在一个已经被 boot guard 拦住的形状上,却要付「OSS 部署失去建组织能力 + 丢掉 #3624 的 e2e 覆盖」这两笔。若要选 B,建议同时给 dogfood 一条别的开路方式(例如 verify harness 提供可注入的 tenancy stub),别让 #3624 的覆盖为此消失。不指派 —— 按 Prime Directive #10 归档,等维护者定夺。
出处:#5233 的开发(会话
session_015W6nhsDrz6zWQc8je12a1t)。