fix(authz)!: make the group posture an entitlement again — ADR-0105 D12 correction - #3570
Merged
Conversation
… D12 correction The first ADR-0105 wave (#3559) read D12 as "the `group` wall ships open" and made the posture self-activating: it never probed for `@objectstack/organizations`. That turned `group` into a free multi-org path around the `isolated` gate (ADR-0081 D2), and left the WEAKER isolation as the free one — not a boundary anyone would draw deliberately. The distinction missed: open code is not free activation. The wall's implementation has always lived in open packages — equally true of `isolated`, whose Layer 0 wall sits in plugin-security gated on a service the enterprise package registers. Cloud ADR-0016's 铁律 guarantees a deployment RUNNING a multi-org shape is safe; that is satisfied by REFUSING to run one unwalled (ADR-0093 D5), not by giving the posture away. - tenancy-service: `group` probes `org-scoping` exactly like `isolated`; without it the posture resolves to `single` + `degraded`. - serve.ts: the ADR-0093 D5 boot guard keys off the resolved POSTURE, not `OS_MULTI_ORG_ENABLED`. `OS_TENANCY_POSTURE=group` previously skipped both the package load AND the fail-fast, silently degrading to an unwalled deployment — the exact ADR-0049 class that guard exists to close. - New seam: `org-scoping` may declare `supportedPostures` (`OrgScopingEntitlement`); the open side honours it and fails closed on anything unlisted. Whether `group` and `isolated` are one tier or two is packaging policy, which belongs to the commercial runtime rather than hard-coded in open core. Omitting it entitles every walled posture. - `organization_id` STAMPING returns to the enterprise runtime. The previous wave moved it into the open engine, removing the closed package's only load-bearing runtime duty — a five-line forged `org-scoping` registration would then have produced a fully working multi-org deployment. Back where it was, a forged registration yields NULL-org rows the wall hides. Write-side VALIDATION stays open and unchanged, bulk-insert coverage included: rejecting a forged organization_id is a security property, not a packaging one. Only filling an ABSENT value moved back. - Default-org bootstrap returns to `single`-only (ADR-0081 D1). Operator note: `OS_TENANCY_POSTURE=group` without the enterprise runtime now refuses to boot rather than running single-org. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 5 package(s): 116 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…upersedes The env var gates whether a multi-organization deployment boots, and the boot failure message names it, but it had no entry in the environment-variables reference. Records the three postures, the derivation from OS_MULTI_ORG_ENABLED that keeps existing deployments unchanged, the refuse-to-boot behavior on an unrecognized value, and the enterprise-runtime requirement for both walled postures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL
os-zhuang
marked this pull request as ready for review
July 27, 2026 07:03
This was referenced Jul 27, 2026
Merged
os-zhuang
added a commit
that referenced
this pull request
Jul 27, 2026
…led, not open (#3570) (#3603) Founder ruling: OS_TENANCY_POSTURE=group is not an open-edition feature. The code was already corrected in #3570 (group probes org-scoping exactly like isolated; no runtime => single + degraded; an os serve boot configured for group fail-fasts). This brings the ADR text in line: - D12 rewritten to the code-vs-activation split: the wall's implementation ships open, posture activation is entitled for BOTH multi-org postures; iron-rule safety is satisfied by refusing to run an unwalled group boot, not by free activation. ADR-0081 D2's commercial line stands. - Amendment block records the original text, why it was wrong (inverted ADR-0081 D2 + the silent-degradation hole), and the #3559 -> #3570 trail. - Consequences clause updated to match; header Status moves to Accepted with the amendment note; Tracking points at #3541 / #3539 / #3540 / cloud #874. Claude-Session: https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL 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.
Follow-up to #3559. Tracking: #3541.
What was wrong
#3559 read ADR-0105 D12 as "the
groupwall ships open" and made the posture self-activating —tenancy-servicereturnedisolationActive: trueforgroupwithout ever probing for@objectstack/organizations.Two consequences, both bad:
groupbecame a free multi-org path around theisolatedgate (ADR-0081 D2). Anyone wanting multi-org would simply pickgroup. The weaker isolation ended up as the free one — not a boundary anyone would draw deliberately.os servestill gated the enterprise package load onOS_MULTI_ORG_ENABLED, soOS_TENANCY_POSTURE=groupskipped both the package load and the ADR-0093 D5 fail-fast — booting an unwalled multi-org deployment with no warning. That is precisely the ADR-0049 class the D5 guard exists to close.The distinction that was missed
Open code is not free activation. The wall's implementation has always lived in the open packages — equally true of
isolated, whose Layer 0 wall sits inplugin-securityand is gated on a service the enterprise package registers. Cloud ADR-0016's 铁律 (强制免费、治理收费) guarantees that a deployment running a multi-org shape is safe; that is satisfied by refusing to run one unwalled, not by giving the posture away.Changes
tenancy-service—groupprobesorg-scopingexactly likeisolated. Without it the posture resolves tosingleand reportsdegraded.os serve— the D5 boot guard now keys off the resolved posture, notOS_MULTI_ORG_ENABLED, and its message is posture-aware. Agrouprequest without the runtime refuses to boot unlessOS_ALLOW_DEGRADED_TENANCY=1.New seam: the runtime declares what it entitles.
org-scopingmay exposesupportedPostures(OrgScopingEntitlement,@objectstack/spec/security); the open side honours it and fails closed on anything unlisted. Whethergroupandisolatedare one commercial tier or two is packaging policy, which belongs to the commercial runtime rather than hard-coded in open core. Omitting the field entitles every walled posture, so existing runtimes are unaffected.organization_idstamping returns to the enterprise runtime.This one is worth spelling out. feat(authz)!: group tenancy posture + org scope as a first-class dimension — ADR-0105 Phase 0/1 #3559 moved auto-stamping into the open engine, which removed the closed package's only load-bearing runtime duty. The framework checks the gate by service truthiness, so after that change a five-line forged registration —
— would have yielded a fully working multi-org deployment. Before it, the same forgery gave you a wall with no stamper: rows land with
organization_id = NULL, the wall hides them, and you get a broken deployment instead of an unlicensed working one. Stamping is back where it was.Write-side validation stays open and is unchanged, bulk-insert coverage included — rejecting a forged
organization_idis a security property, not a packaging one. Only filling an absent value moved back.Default-organization bootstrap returns to
single-only; every walled posture keeps its existing owner (ADR-0081 D1).Still open (not this PR)
Presence-of-service remains a coarse gate — cloud ADR-0081 already listed licence-gating beyond package presence as a deferred ADR-0022 follow-up. The
supportedPosturesseam added here is the right carrier for a real licence check when that lands. Worth stating plainly: any check living in open code can be patched out, so the realistic goal is raising effort and making circumvention a deliberate licence violation, not technical prevention.Verification
pnpm build— 71/71pnpm test— 131/131 (serial). In the default parallel runpackages/lint/src/lazy-deps.test.tsfails reproducibly: it spawns childnodeprocesses that importpackages/lint/distwhile turbo restores that samedistfrom cache. Pre-existing infrastructure race, not related to this diff — the rule added in feat(authz)!: group tenancy posture + org scope as a first-class dimension — ADR-0105 Phase 0/1 #3559 is a pure, import-free module and the test's structural check passes. Confirmed byTURBO_CONCURRENCY=1 pnpm testgoing green.pnpm lint+ all five repo grep guards — cleanapi-surfaceregenerated,docs,spec-changes,upgrade-guide) — in syncNew coverage in
tenancy-service.test.ts:groupdegrades without the runtime; entitlement honoured / withheld / empty / throwing; entitlement never consulted when the runtime is absent.security-plugin.test.tsandauthz-matrix-gate.test.tsassert the engine does not stamp while still rejecting forged values, bulk included.🤖 Generated with Claude Code
https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL
Generated by Claude Code