From aaee264ca3387bf1d0361f3bfe4012a468a7985c Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:58:14 +0800 Subject: [PATCH 1/3] docs(organizations): stop calling the open multi-org runtime enterprise/closed-source (#16718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0132 moved the `org-scoping` registrar into open core: `packages/plugins/organizations` is Apache-2.0, carries no licence check of any kind, and declares `['group','isolated']` as its own constant. #16137's acceptance proved an open-only composition raises the `isolated` wall with no licence check reachable. The prose did not follow — 65 occurrences across 45 files still described that package as enterprise / closed-source / cloud-private. Prose only. Every non-comment hunk is a string literal (an operator message, a flag description, a conformance-matrix note, a test expectation); no control flow, no identifier and no behaviour changes anywhere. Corrected (population 1), led by the two sites the card names: - `plugin-auth/src/tenancy-service.ts` — open core's own authority for resolving the posture. Its header said both walled postures require the *enterprise* package and that multi-org operation is *a commercial capability*; `isolationActive` argued "ENABLING a multi-organization posture is an entitlement". Rewritten around ADR-0132 D2/D3/D4: the mechanism (both postures probe the same registrar) is unchanged, the answer to "can an open install run isolated?" is now yes, and the `probeEntitledPostures` seam is explained as existing for the commercial subclass rather than as a tier. - `cli/src/commands/serve.ts` — the mount block no longer says the runtime "lives in the closed-source package", and the operator-facing install remedy no longer ends "this runtime is closed-source and is NOT on the public npm registry ... Without one this bullet is not followable". That sentence told an open-source operator the remedy they had just been given was unusable. - `cli/src/utils/tenancy-posture-hints.ts` — the `isolated` hint both `os serve` and `os doctor` render. Not in the card's count: its regex cannot see `the enterprise ${ORGANIZATIONS_RUNTIME_PKG}`. - `plugin-dev/src/dev-plugin.ts` — two more operator-facing strings (the degraded-tenancy warning and the stage-2 mount refusal). Because one name now denotes two packages (ADR-0132 D3), every rewritten site names which one it means; where a site was load-bearing on ADR-0105 D12 the citation is repointed at D12-as-amended rather than deleted. Pin tests updated deliberately, with the reason recorded in each file: `serve-organizations-message-spelling.test.ts` (5 hits) and `doctor-organizations-message-spelling.test.ts`. The pins were not relaxed — they still compare whole rendered lines built from `Serve.ORGANIZATIONS_RUNTIME_PKG`, and both CONTROL blocks still differ from their expectation only in spacing. The doctor roster assertion still pins `edition: 'enterprise'`, which stays true: the roster records the ENTITLED variant. Left alone on purpose, and why, in the PR body: 2 hits that describe the genuinely commercial same-named package, and 15 that state the package is absent/not installable in this workspace as the reason a fixture fakes or skips a proof — the live capability question the card dispatches as report-only. Refs ADR-0132, ADR-0105 D12 (as amended), #16130, #16137, PR #16215. Co-Authored-By: Claude Opus 5 --- ...tor-organizations-message-spelling.test.ts | 20 +++-- .../serve-cluster-host-resolution.test.ts | 2 +- ...rve-organizations-message-spelling.test.ts | 34 ++++++-- packages/cli/src/commands/serve.ts | 32 ++++--- packages/cli/src/commands/verify.ts | 2 +- .../cli/src/utils/tenancy-posture-hints.ts | 2 +- ...ve-organizations-mount-failure.e2e.test.ts | 13 +-- packages/core/src/security/api-key.test.ts | 4 +- packages/core/src/security/api-key.ts | 2 +- ...dio-tenancy-posture-api-key-matrix.test.ts | 2 +- .../src/apps/setup-nav.contributions.ts | 2 +- .../plugins/plugin-auth/src/auth-manager.ts | 2 +- .../plugin-auth/src/auth-plugin.test.ts | 2 +- .../plugins/plugin-auth/src/auth-plugin.ts | 4 +- .../src/ensure-default-organization.test.ts | 4 +- .../src/ensure-default-organization.ts | 12 +-- .../src/org-create-posture-gate.test.ts | 2 +- .../plugin-auth/src/tenancy-service.ts | 86 ++++++++++++------- .../src/dev-plugin-tenancy-posture.test.ts | 2 +- packages/plugins/plugin-dev/src/dev-plugin.ts | 14 +-- packages/plugins/plugin-security/README.md | 2 +- .../src/authz-matrix-gate.test.ts | 2 +- .../src/bootstrap-system-capabilities.test.ts | 2 +- ...-active-organization-write-refusal.test.ts | 2 +- .../src/security-plugin.test.ts | 2 +- .../plugin-sharing/src/sharing-plugin.ts | 2 +- ...achments-permission-matrix.dogfood.test.ts | 2 +- .../dogfood/test/authz-conformance.matrix.ts | 4 +- .../test/enterprise-organizations.test.ts | 2 +- .../dogfood/test/enterprise-organizations.ts | 13 ++- ...organization-write-refusal.dogfood.test.ts | 2 +- .../src/app-plugin.tenancy-posture.test.ts | 2 +- packages/runtime/src/app-plugin.ts | 4 +- packages/runtime/src/seed-datasets.ts | 2 +- .../service-datasource/src/admin-routes.ts | 2 +- .../src/settings-service-plugin.ts | 2 +- .../src/storage-service-plugin.ts | 2 +- packages/types/src/env.ts | 2 +- packages/types/src/node.ts | 10 +-- 39 files changed, 188 insertions(+), 116 deletions(-) diff --git a/packages/cli/src/commands/doctor-organizations-message-spelling.test.ts b/packages/cli/src/commands/doctor-organizations-message-spelling.test.ts index 30ceed751f..d149d2ba25 100644 --- a/packages/cli/src/commands/doctor-organizations-message-spelling.test.ts +++ b/packages/cli/src/commands/doctor-organizations-message-spelling.test.ts @@ -167,7 +167,7 @@ describe('doctor — the posture description an operator reads names the declara // what is pinned. it('leg (i) — the `isolated` fix-list bullet names it, with the spacing intact', () => { expect(renderIsolatedBullet()).toBe( - ` • OS_TENANCY_POSTURE=isolated — organization wall + the enterprise ${PKG} runtime ` + ` • OS_TENANCY_POSTURE=isolated — organization wall + the ${PKG} runtime ` + "(the legacy spelling 'multi' is accepted and normalizes to this)", ); }); @@ -183,8 +183,16 @@ describe('doctor — the posture description an operator reads names the declara + 'where it ships from (#10921); a command that prints a package name at operators must name a row in it.', ).toContain(PKG); - // Provenance, read through doctor's own spelling rather than a literal: the - // row this advice describes is the enterprise one. + // Provenance, read through doctor's own spelling rather than a literal. + // + // ⚠️ The row stays `edition: 'enterprise'` after ADR-0132 and that is NOT a + // contradiction of the reworded hint above. One name now denotes two + // packages (ADR-0132 D3): the framework publishes an Apache-2.0 build of it + // and a commercial deployment resolves the same name to a private, + // licence-gated subclass. The roster row records the ENTITLED variant — its + // own note says so — while the operator hint names the PACKAGE, which is + // followable on any install. So this assertion still measures roster drift, + // and it is deliberately not relaxed to match the prose reword. const row = PLATFORM_PLUGIN_WIRED_RUNTIMES[PKG]; expect(row.edition, `edition drift for the runtime doctor names ('${PKG}')`).toBe('enterprise'); }); @@ -233,14 +241,14 @@ describe('#12464 CONTROL — these pins can say no', () => { // If this instrument could not tell the two apart, leg (i) would be // decorative. Anchored on the exact regression interpolation invites. const expected = - ` • OS_TENANCY_POSTURE=isolated — organization wall + the enterprise ${PKG} runtime ` + ` • OS_TENANCY_POSTURE=isolated — organization wall + the ${PKG} runtime ` + "(the legacy spelling 'multi' is accepted and normalizes to this)"; expect( - ` • OS_TENANCY_POSTURE=isolated — organization wall + the enterprise ${PKG} runtime` + ` • OS_TENANCY_POSTURE=isolated — organization wall + the ${PKG} runtime` + "(the legacy spelling 'multi' is accepted and normalizes to this)", ).not.toBe(expected); expect( - ` • OS_TENANCY_POSTURE=isolated — organization wall + the enterprise ${PKG} runtime ` + ` • OS_TENANCY_POSTURE=isolated — organization wall + the ${PKG} runtime ` + "(the legacy spelling 'multi' is accepted and normalizes to this)", ).not.toBe(expected); // …and says yes to the real thing, so the two `not.toBe`s above are a diff --git a/packages/cli/src/commands/serve-cluster-host-resolution.test.ts b/packages/cli/src/commands/serve-cluster-host-resolution.test.ts index 2e0149c891..a9d43f47ea 100644 --- a/packages/cli/src/commands/serve-cluster-host-resolution.test.ts +++ b/packages/cli/src/commands/serve-cluster-host-resolution.test.ts @@ -46,7 +46,7 @@ * binding. A load placed above it is not a compile error — the author writes * a bare `import()`, which resolves from the CLI and is green in any dev * checkout where everything is hoisted into one `node_modules`. The first - * time it cost the enterprise organizations load (cloud#1013); the second + * time it cost the organizations load (cloud#1013); the second * time it cost EE multi-node boot outright (#10645). * * #10769 closed the class rather than hoisting a third time: the helper is diff --git a/packages/cli/src/commands/serve-organizations-message-spelling.test.ts b/packages/cli/src/commands/serve-organizations-message-spelling.test.ts index 72bf140bff..f374c71acf 100644 --- a/packages/cli/src/commands/serve-organizations-message-spelling.test.ts +++ b/packages/cli/src/commands/serve-organizations-message-spelling.test.ts @@ -66,6 +66,28 @@ * via `test/serve-capability-vocabulary.test.ts` and the shared const's via * doctor's leg (ii). * + * ── The 2026-09-08 reword, and why it was made HERE rather than absorbed ── + * + * These pins previously expected the word "enterprise" in two operator-facing + * sentences: the install remedy ("add … (the enterprise multi-org runtime)") + * and the `isolated` posture hint. ADR-0132 moved the multi-org runtime into + * open core — `packages/plugins/organizations` is Apache-2.0 and carries no + * licence check — so both sentences told an open-source operator that a remedy + * they can in fact follow required a subscription. + * + * ⛔ The pins were NOT relaxed or deleted to accommodate the reword: they still + * compare the whole rendered line, whitespace included, against text built from + * `Serve.ORGANIZATIONS_RUNTIME_PKG`, and the #12151 CONTROL block below still + * proves they can say no. Only the expected PROSE moved, in the same diff as + * the prose itself, which is the shape this file is for — a wording change that + * does not redden a pin here would mean the pin had stopped reading. + * + * ⚠️ ONE NAME, TWO PACKAGES (ADR-0132 D3) is why the new wording names no + * edition at all: a commercial deployment resolves the same package name to a + * private licence-gated subclass, so any adjective this message picks is wrong + * for one of the two installs reading it. The roster keeps the edition fact, + * and `doctor-organizations-message-spelling.test.ts` leg (ii) still pins it. + * * ⚠️ This paragraph used to say the literal HAD to stay in `serve.ts`, because * `serve-cluster-host-resolution.test.ts` resolved the organizations `import()` * through that static and needed the literal in that file or the load dropped @@ -152,7 +174,7 @@ describe('serve — the multi-org runtime name an operator READS comes from the it('site 2 — the "add it to THIS APP" remedy names it, with the spacing intact', () => { // The other instruction an operator acts on: the app never declared it. expect(lines(remedyUndeclared())[0]).toBe( - ` • add ${PKG} (the enterprise multi-org runtime) to THIS APP`, + ` • add ${PKG} (the multi-org runtime) to THIS APP`, ); }); @@ -164,7 +186,7 @@ describe('serve — the multi-org runtime name an operator READS comes from the // package's own `exports` names no runtime entry Node can load — and it // fell into the else leg, rendering the UNDECLARED remedy verbatim: // - // • add @objectstack/organizations (the enterprise multi-org runtime) to THIS APP + // • add @objectstack/organizations (the multi-org runtime) to THIS APP // — declare it in the app's package.json and install; the CLI resolves it from the // // i.e. "declare it and install it" to an operator who has already done @@ -264,7 +286,7 @@ describe('serve — the posture description an operator reads names the declarat expect(verdict.ok, 'the gate accepted a value that is not a posture').toBe(false); if (verdict.ok) return; expect(lines(verdict.fatal)).toContain( - ` • set OS_TENANCY_POSTURE=isolated — organization wall + the enterprise ${PKG} runtime ` + ` • set OS_TENANCY_POSTURE=isolated — organization wall + the ${PKG} runtime ` + "(the legacy spelling 'multi' is accepted and normalizes to this)", ); }); @@ -301,9 +323,9 @@ describe('#12151 CONTROL — these pins can say no', () => { // The exact regression the card names: interpolating into a template is // where a stray space or a lost backtick hides. If this instrument could // not tell the two apart, every assertion above would be decorative. - const expected = ` • add ${PKG} (the enterprise multi-org runtime) to THIS APP`; - expect(` • add ${PKG}(the enterprise multi-org runtime) to THIS APP`).not.toBe(expected); - expect(` • add ${PKG} (the enterprise multi-org runtime) to THIS APP`).not.toBe(expected); + const expected = ` • add ${PKG} (the multi-org runtime) to THIS APP`; + expect(` • add ${PKG}(the multi-org runtime) to THIS APP`).not.toBe(expected); + expect(` • add ${PKG} (the multi-org runtime) to THIS APP`).not.toBe(expected); expect(lines(remedyUndeclared())[0]).toBe(expected); }); diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index cd94d6480a..79c6ba46cf 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -3677,16 +3677,23 @@ export default class Serve extends Command { } } - // Pair: OrganizationsPlugin (multi-org, ENTERPRISE) — must register - // BEFORE SecurityPlugin. The multi-org runtime (`organization_id` + // Pair: OrganizationsPlugin (multi-org) — must register BEFORE + // SecurityPlugin. The multi-org runtime (`organization_id` // auto-stamp, per-org seed replay, multi-org default-org bootstrap) - // lives in the closed-source `@objectstack/organizations` package - // (ADR-0105 D12; it registers the historical `org-scoping` service - // SecurityPlugin probes at start() to keep vs strip the wildcard - // `tenant_isolation` RLS — so registration order matters). Without - // it, deployments are single-org: the open member-management + // lives in the `@objectstack/organizations` package (ADR-0105 D12 + // as amended by ADR-0132; it registers the historical `org-scoping` + // service SecurityPlugin probes at start() to keep vs strip the + // wildcard `tenant_isolation` RLS — so registration order matters). + // Without it, deployments are single-org: the open member-management // basics (plugin-auth's default-org bootstrap + better-auth // invitations) still work. + // ⚠️ ONE NAME, TWO PACKAGES (ADR-0132 D3): the framework publishes + // an Apache-2.0 package of that name (`packages/plugins/organizations`), + // and a commercial deployment resolves the same name to a private, + // licence-gated subclass of it through its own `workspace:*` + // declaration. `serve` does not choose between them and must not + // try — the host app's own manifest decides, which is exactly what + // the host-anchored importer below reads (#4719). // [ADR-0105 D1] Key off the resolved POSTURE, not the legacy boolean. // Both walled postures (`group` and `isolated`) need this package: // gating on `OS_MULTI_ORG_ENABLED` alone would let @@ -4076,7 +4083,7 @@ export default class Serve extends Command { // // #4719 — this used to be a local re-implementation of that read. It was // right, and it was the ONLY place in the boot path that asked the question - // the right way: the enterprise organizations load two blocks up asked + // the right way: the organizations load two blocks up asked // "does it resolve", which a hoisted store answered yes to regardless. Both // now go through the one owner in `@objectstack/types/node`, so "declared" // cannot mean two different things in one file (Prime Directive #12). @@ -5441,14 +5448,15 @@ export function formatOrganizationsInstallRemedy( ' the remedy is in the package, and the cause below is the authority on what it\n' + ' has to publish — or\n'; } - return ` • add ${pkg} (the enterprise multi-org runtime) to THIS APP\n` + + return ` • add ${pkg} (the multi-org runtime) to THIS APP\n` + " — declare it in the app's package.json and install; the CLI resolves it from the\n" + ' app, not from the framework it is linked out of. Being merely reachable\n' + ' through NODE_PATH / a hoisted workspace store is deliberately not enough\n' + ' (#4719) — that made this wall depend on how the process was launched.\n' + - ' NOTE: this runtime is closed-source and is NOT on the public npm registry —\n' + - ' it is distributed with an enterprise / cloud subscription. Without one this\n' + - ' bullet is not followable, and one of the two below is your path — or\n'; + ' NOTE: this runtime is Apache-2.0 and published on the public npm registry\n' + + ' (ADR-0132), so this bullet is followable on any install — no subscription.\n' + + ' A commercial deployment resolves the same package name to its own private,\n' + + ' licence-gated build; the manifest that declares it decides which — or\n'; } /** diff --git a/packages/cli/src/commands/verify.ts b/packages/cli/src/commands/verify.ts index de2b7a09ef..5687622a5d 100644 --- a/packages/cli/src/commands/verify.ts +++ b/packages/cli/src/commands/verify.ts @@ -87,7 +87,7 @@ export default class Verify extends Command { default: false, }), 'multi-tenant': Flags.boolean({ - description: 'Boot org-scoped (register the enterprise @objectstack/organizations plugin) so tenant-isolation RLS policies apply (also honors a walled $OS_TENANCY_POSTURE, and the legacy $OS_MULTI_ORG_ENABLED it falls back to)', + description: 'Boot org-scoped (register the @objectstack/organizations plugin) so tenant-isolation RLS policies apply (also honors a walled $OS_TENANCY_POSTURE, and the legacy $OS_MULTI_ORG_ENABLED it falls back to)', default: false, }), json: Flags.boolean({ description: 'Emit the structured report as JSON', default: false }), diff --git a/packages/cli/src/utils/tenancy-posture-hints.ts b/packages/cli/src/utils/tenancy-posture-hints.ts index e3aeaef77c..e53a486b57 100644 --- a/packages/cli/src/utils/tenancy-posture-hints.ts +++ b/packages/cli/src/utils/tenancy-posture-hints.ts @@ -136,6 +136,6 @@ export const TENANCY_POSTURE_FIX_HINTS: Readonly> = { single: 'one organization, no organization wall — the default', group: 'organization wall enforced by the open engine, one shared database', isolated: - `organization wall + the enterprise ${ORGANIZATIONS_RUNTIME_PKG} runtime ` + `organization wall + the ${ORGANIZATIONS_RUNTIME_PKG} runtime ` + "(the legacy spelling 'multi' is accepted and normalizes to this)", }; diff --git a/packages/cli/test/serve-organizations-mount-failure.e2e.test.ts b/packages/cli/test/serve-organizations-mount-failure.e2e.test.ts index f2ddc33331..d4d91d3d9e 100644 --- a/packages/cli/test/serve-organizations-mount-failure.e2e.test.ts +++ b/packages/cli/test/serve-organizations-mount-failure.e2e.test.ts @@ -2,7 +2,7 @@ /** * #4818 — `os serve` must tell an operator WHICH of two different things went - * wrong with the enterprise multi-org runtime, over the REAL CLI process. + * wrong with the multi-org runtime, over the REAL CLI process. * * The defect: `importFromHost('@objectstack/organizations')` and * `kernel.use(new mod.OrganizationsPlugin())` shared a single `try`, so an @@ -27,10 +27,13 @@ * `serve-organizations-host-resolution.e2e.test.ts`): every other test of the * walled postures hands the plugin in as `extraPlugins` or mocks the module, * which bypasses the CLI's own load/mount sequence — the only thing under test - * here. The fixtures stand in for the closed-source enterprise package: one app - * simply does not ship it, another ships a version whose plugin throws on - * construction (the shape cloud#1020 gave its license gate). What is asserted - * is the CLI's CLASSIFICATION and its message, not any enterprise semantics. + * here. The fixtures stand in for a mounted multi-org runtime: one app simply + * does not ship it, another ships a version whose plugin throws on construction + * — which is specifically the COMMERCIAL package's shape, since ADR-0132 D2 + * leaves construction-time licence refusal to the private subclass and the open + * `packages/plugins/organizations` carries no licence check of any kind (the + * shape cloud#1020 gave its license gate). What is asserted is the CLI's + * CLASSIFICATION and its message, not any runtime's semantics. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; diff --git a/packages/core/src/security/api-key.test.ts b/packages/core/src/security/api-key.test.ts index aa1f1246c5..8ccfa0b489 100644 --- a/packages/core/src/security/api-key.test.ts +++ b/packages/core/src/security/api-key.test.ts @@ -195,8 +195,8 @@ describe('resolveApiKeyAdmission — organization (#8287)', () => { /** * The posture must be the ENFORCED one, not the requested one. ADR-0093 D4/D5: - * a deployment that asks for `isolated` without the enterprise organizations - * runtime runs with NO wall, and `tenancy.isolationActive` is how the service + * a deployment that asks for `isolated` without any organizations runtime + * runs with NO wall, and `tenancy.isolationActive` is how the service * says so. Reading `OS_TENANCY_POSTURE` instead would refuse org-less keys on * a deployment that has no wall at all. */ diff --git a/packages/core/src/security/api-key.ts b/packages/core/src/security/api-key.ts index 086a521aea..f4c5c88bca 100644 --- a/packages/core/src/security/api-key.ts +++ b/packages/core/src/security/api-key.ts @@ -217,7 +217,7 @@ export interface TenancyPostureSource { * ⚠️ Deliberately NOT `resolveTenancyPosture()` from `@objectstack/types`, which * reads `OS_TENANCY_POSTURE` directly. That answers what the operator ASKED * for, not what is ENFORCED: under ADR-0093 D4/D5 a deployment that requests - * `isolated` without the enterprise `@objectstack/organizations` runtime + * `isolated` without the `@objectstack/organizations` runtime * resolves to `single` and runs with NO organization wall. Reading the env * there would refuse org-less API keys on a deployment whose wall is not even * active — breaking working automation to enforce a boundary that does not diff --git a/packages/mcp/src/stdio-tenancy-posture-api-key-matrix.test.ts b/packages/mcp/src/stdio-tenancy-posture-api-key-matrix.test.ts index 1977d21031..f3067d59eb 100644 --- a/packages/mcp/src/stdio-tenancy-posture-api-key-matrix.test.ts +++ b/packages/mcp/src/stdio-tenancy-posture-api-key-matrix.test.ts @@ -543,7 +543,7 @@ describe('[#15348] §5 — the posture and the membership are both re-read per c const { bridge } = await h.start(); expect((await readAll(bridge)).total).toBe(2); - // The enterprise multi-org runtime registers and the wall goes live. + // The multi-org runtime registers and the wall goes live. service.posture = 'isolated'; await expect(bridge.query(OBJECT, {})).rejects.toThrow(/no longer valid/); diff --git a/packages/platform-objects/src/apps/setup-nav.contributions.ts b/packages/platform-objects/src/apps/setup-nav.contributions.ts index e732d1fbef..1428168fd4 100644 --- a/packages/platform-objects/src/apps/setup-nav.contributions.ts +++ b/packages/platform-objects/src/apps/setup-nav.contributions.ts @@ -81,7 +81,7 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [ // plugin-auth's single-org default-org bootstrap guarantees an org to // invite into — these are the OPEN member-management basics. Only the // org LIST below keeps the gate: browsing organizations is meaningful - // only when more than one can exist (enterprise multi-org). + // only when more than one can exist (a walled multi-org posture). { id: 'nav_teams', type: 'object', label: 'Teams', objectName: 'sys_team', icon: 'users-round' }, { id: 'nav_organizations', type: 'object', label: 'Organizations', objectName: 'sys_organization', icon: 'building-2', requiresService: 'org-scoping' }, { id: 'nav_invitations', type: 'object', label: 'Invitations', objectName: 'sys_invitation', icon: 'mail' }, diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index d791d62b39..0733362acb 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -6224,7 +6224,7 @@ export class AuthManager { * contraction over what #5233 shipped. The gate used to judge * `postureEnforcesWall(resolveTenancyPosture())`, the operator's request, which * came apart from `/auth/config` on exactly one deployment shape: ADR-0093 D5 - * degradation (a wall was asked for, the enterprise `@objectstack/organizations` + * degradation (a wall was asked for, the `@objectstack/organizations` * runtime is absent, so nothing isolates anything). There the console hid the * "Create organization" action while the API happily minted organizations whose * boundary NO engine enforces — a declared-but-unenforced security property, diff --git a/packages/plugins/plugin-auth/src/auth-plugin.test.ts b/packages/plugins/plugin-auth/src/auth-plugin.test.ts index 5c6921e194..4327cf8c19 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.test.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.test.ts @@ -1371,7 +1371,7 @@ describe('AuthPlugin', () => { expect(ql.tables.sys_member).toHaveLength(memberCount); }); - it('multi-org: bootstrap is NOT wired (enterprise organizations package owns it)', async () => { + it('multi-org: bootstrap is NOT wired (the organizations package owns it)', async () => { process.env.OS_MULTI_ORG_ENABLED = 'true'; // [#11184] A walled posture now declares its platform owner or refuses // to boot; this fixture's subject is the default-org wiring, so declare diff --git a/packages/plugins/plugin-auth/src/auth-plugin.ts b/packages/plugins/plugin-auth/src/auth-plugin.ts index b18d185127..51ee0b1831 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.ts @@ -1169,8 +1169,8 @@ export class AuthPlugin implements Plugin { // set — how a CONFIG-anchored admin comes into standing), and the // legacy `sys_user_permission_set` insert (how `single`-posture // first-user promotion lands standing, Choice 4A — retired with the - // legacy-grant removal leg). The enterprise organizations package's - // walled wiring should consume the same predicate. + // legacy-grant removal leg). The organizations package's walled + // wiring should consume the same predicate. try { const ql = ctx.getService('objectql'); if (ql && typeof ql.registerMiddleware === 'function') { diff --git a/packages/plugins/plugin-auth/src/ensure-default-organization.test.ts b/packages/plugins/plugin-auth/src/ensure-default-organization.test.ts index f85d4b970f..f2adf10bdd 100644 --- a/packages/plugins/plugin-auth/src/ensure-default-organization.test.ts +++ b/packages/plugins/plugin-auth/src/ensure-default-organization.test.ts @@ -312,8 +312,8 @@ describe('ensureDefaultOrganization (plugin-auth home)', () => { }); // [#11973 / #11663 L3, design H4] The trigger predicate — one definition for -// every wiring (plugin-auth's middleware here; the enterprise organizations -// package's walled wiring is asked to consume the same export). +// every wiring (plugin-auth's middleware here; the organizations package's +// walled wiring is asked to consume the same export). describe('isDefaultOrganizationBootstrapTrigger', () => { it.each([ [{ object: 'sys_user', operation: 'insert' }, true], diff --git a/packages/plugins/plugin-auth/src/ensure-default-organization.ts b/packages/plugins/plugin-auth/src/ensure-default-organization.ts index c7e96c9457..3e559b32fa 100644 --- a/packages/plugins/plugin-auth/src/ensure-default-organization.ts +++ b/packages/plugins/plugin-auth/src/ensure-default-organization.ts @@ -12,9 +12,9 @@ * cloud ADR-0081 D1 closes. * * This helper HOME is plugin-auth (the open member-management basics). The - * enterprise organizations package reuses it for the multi-org bootstrap and - * injects its seed-ownership step via `claimSeedOwnership` (that machinery is - * part of the per-org seed pipeline, not of the basics). + * `@objectstack/organizations` package reuses it for the multi-org bootstrap + * and injects its seed-ownership step via `claimSeedOwnership` (that machinery + * is part of the per-org seed pipeline, not of the basics). * * ## Who "the platform admin" is (#11973 / #11663 L3, design §2 step 5) * @@ -126,8 +126,8 @@ export interface EnsureDefaultOrganizationOptions { logger?: BootstrapLogger; /** * Optional seed-ownership handoff, run after the owner bind (best-effort). - * The enterprise organizations package injects `claimOrgSeedOwnership` - * here; the open single-org path has no per-org seed pipeline and omits it. + * The `@objectstack/organizations` package injects `claimOrgSeedOwnership` + * here; the single-org path has no per-org seed pipeline and omits it. */ claimSeedOwnership?: ( ql: any, @@ -172,7 +172,7 @@ function oldestFirst(a: any, b: any): number { * [#11973 / #11663 L3, design H4] Which writes can change this helper's * answer — the trigger predicate for the default-org bootstrap re-run * middleware. Exported so every wiring (plugin-auth's single-posture - * middleware, and the enterprise organizations package's walled bootstrap + * middleware, and the organizations package's walled bootstrap * wiring) consumes the SAME predicate instead of re-deriving it — the * `shouldReplayBootstrapFor` pattern next door in `plugin-security`. * diff --git a/packages/plugins/plugin-auth/src/org-create-posture-gate.test.ts b/packages/plugins/plugin-auth/src/org-create-posture-gate.test.ts index e1bef818cd..f711d21fdf 100644 --- a/packages/plugins/plugin-auth/src/org-create-posture-gate.test.ts +++ b/packages/plugins/plugin-auth/src/org-create-posture-gate.test.ts @@ -33,7 +33,7 @@ // #5233 corrected the KNOB and left the gate judging the operator's REQUEST, // which is what the demoted boolean also meant. That left exactly one shape // where the gate and the flag answered from different facts: ADR-0093 D5 -// degradation — a wall was requested, the enterprise `@objectstack/organizations` +// degradation — a wall was requested, the `@objectstack/organizations` // runtime is absent, so the `tenancy` service resolves an effective posture of // `single` + `degraded`. The console hid the "Create organization" action while // the route happily minted organizations whose boundary NO engine enforces: diff --git a/packages/plugins/plugin-auth/src/tenancy-service.ts b/packages/plugins/plugin-auth/src/tenancy-service.ts index 39a4eeec3f..d666f607b1 100644 --- a/packages/plugins/plugin-auth/src/tenancy-service.ts +++ b/packages/plugins/plugin-auth/src/tenancy-service.ts @@ -9,7 +9,7 @@ * signals that could disagree: the `OS_MULTI_ORG_ENABLED` env flag, the * `org-scoping` service probe, `sys_organization` row counting, and the * frontend feature flags. The worst disagreement was silent — requesting - * multi-org without the enterprise `@objectstack/organizations` package degrades + * multi-org without the `@objectstack/organizations` package degrades * to zero tenant isolation with only a console warning (an ADR-0049-class * unenforced security property). This service makes the two facts that matter — * what was *requested* and what is *actually active* — first-class and @@ -26,18 +26,32 @@ * - `isolated` — wall = `organization_id = activeOrganizationId`. The hard * legal-entity wall, formerly spelled `multi`. * - * ## Open code, entitled activation + * ## What ACTIVATES a wall — and which package that is * - * Both walled postures require the enterprise `@objectstack/organizations` - * package (the `org-scoping` service) to ACTIVATE, and both can resolve - * DEGRADED without it. The wall's implementation is open — the Layer 0 - * compiler, `accessible_org_ids` resolution and the D5 write - * stamping/validation all ship in open packages — but multi-organization - * operation is a commercial capability (ADR-0105 D12). + * Both walled postures require the `@objectstack/organizations` package (the + * `org-scoping` registrar) to ACTIVATE, and both can resolve DEGRADED without + * it. That mechanism is unchanged. What ADR-0132 changed is the ANSWER to "can + * an open install have it?", and this file is where a reader comes to ask. * - * That is not in tension with cloud ADR-0016's 铁律 (强制免费、治理收费): the rule - * guarantees a deployment RUNNING a multi-org shape is safe, which is satisfied - * by refusing to run one unwalled (ADR-0093 D5), not by giving the posture away. + * ⚠️ ONE NAME, TWO PACKAGES (ADR-0132 D3). This framework publishes an + * Apache-2.0 package of that name (`packages/plugins/organizations`); a + * commercial deployment resolves the same name to a private, licence-gated + * SUBCLASS of it through its own `workspace:*` declaration. Which one is + * mounted is decided by the manifest that DECLARES the name — never by this + * service, which cannot tell them apart and must not try. + * + * So multi-organization operation is **not** a commercial capability. An open + * install that has the open package has both walled postures by construction: + * that runtime declares `['group', 'isolated']` as its own constant rather than + * as a tier (ADR-0132 D4) and carries no licence check of any kind (D2). + * ADR-0105 D12's code-vs-activation split still stands, but the entitlement it + * speaks of now lives entirely in the commercial subclass's own constructor + * gate — cloud code, cloud gate. Nothing on this side of the line asks. + * + * None of that is in tension with cloud ADR-0016's 铁律 (强制免费、治理收费): the + * rule guarantees a deployment RUNNING a multi-org shape is safe, which is + * satisfied by refusing to run one unwalled (ADR-0093 D5). It never required + * withholding the wall, and since ADR-0132 the open tree does not. * * Registered by plugin-auth (the open-core home, alongside the default-org * bootstrap). @@ -71,12 +85,14 @@ export interface TenancyService { * ADR-0093 D5); when it boots anyway, this flag brands the deployment * everywhere an operator looks (`/auth/config`, Setup dashboard). * - * Reachable for BOTH walled postures. `group` probes the enterprise - * `org-scoping` runtime exactly like `isolated` does (ADR-0105 D12) — the - * wall's CODE is open, but ACTIVATING a multi-organization posture is an - * entitlement. So either posture degrades when that runtime is absent, and - * also when it is installed but declares (`supportedPostures`) that it does - * not entitle the posture requested. + * Reachable for BOTH walled postures. `group` probes the `org-scoping` + * runtime exactly like `isolated` does (ADR-0105 D12); since ADR-0132 the + * wall's code AND the registrar that activates it are both open, so an open + * install reaches a non-degraded walled posture on its own. Either posture + * still degrades when that runtime is ABSENT, and also when it is installed + * but declares (`supportedPostures`) that it does not entitle the posture + * requested — which the open package never does (it declares both) and a + * commercial subclass of it may. */ readonly degraded: boolean; /** @@ -93,8 +109,8 @@ export interface TenancyService { * belongs to the only org I can see". Guessing there is the failure ADR-0093 * D6 already refuses for the backfill — "a wrong org in a tenant-isolated * deployment is a data-exposure bug, not a convenience" — and it reached - * production once (cloud#957): a control plane running `isolated` without the - * enterprise package bound every fresh self-serve signup into whichever + * production once (cloud#957): a control plane running `isolated` without any + * `org-scoping` runtime bound every fresh self-serve signup into whichever * organization happened to be the only one, handing them its environments. * Degrading the WALL is survivable; degrading into cross-tenant writes is not. */ @@ -108,7 +124,7 @@ export interface TenancyServiceDeps { */ requested: TenancyPosture | boolean; /** - * Whether the enterprise org-scoping machinery is wired. Called lazily (never + * Whether the org-scoping machinery is wired. Called lazily (never * at construction — the org-scoping provider registers after plugin-auth) and * cheap (a service-registry lookup); consumers that read it hot should cache * the result themselves, as SecurityPlugin does at `start()`. @@ -121,12 +137,14 @@ export interface TenancyServiceDeps { * ENTITLES, as declared by that runtime itself (`org-scoping`'s optional * `supportedPostures`). * - * Presence-of-package is a coarse entitlement: it answers "may this - * deployment run multi-org at all", not "which shapes of it". Deciding that - * `group` and `isolated` are the same commercial tier — or different ones — - * is a PACKAGING policy, and packaging policy belongs to the commercial - * runtime, not to this open-core service. So the open side asks instead of - * assuming, and fails closed on anything not entitled. + * Presence-of-package answers "is a multi-org runtime mounted at all", not + * "which shapes of it". The OPEN package leaves nothing to decide: it + * entitles both walled postures by construction (ADR-0132 D4), and ⛔ that is + * not a place a tier may later be drawn. This seam exists for the OTHER + * package sharing the name — a commercial subclass may narrow what IT + * entitles, and that narrowing is a packaging policy belonging to it rather + * than to this service. So this side asks instead of assuming, and fails + * closed on anything not entitled. * * `undefined` (no declaration) means "every walled posture", preserving the * behavior of every runtime that predates this seam. @@ -182,12 +200,16 @@ export function createTenancyService(deps: TenancyServiceDeps): TenancyService { * - `single` — nothing to enforce. * - `group` / `isolated` — only with the org-scoping machinery registered. * - * BOTH walled postures probe. The wall's CODE is open (the Layer 0 compiler, - * `accessible_org_ids` resolution and D5 stamping all live in open packages), - * but ENABLING a multi-organization posture is an entitlement — exactly the - * shape `isolated` has always had (ADR-0105 D12). Open code, entitled activation: - * the two are separate questions, and conflating them is what briefly made - * `group` a free multi-org back door around the `isolated` gate. + * BOTH walled postures probe, and they probe the SAME registrar — exactly the + * shape `isolated` has always had (ADR-0105 D12). That symmetry is the point: + * treating the two as different questions is what briefly made `group` a free + * multi-org back door around the `isolated` gate (#3570). + * + * Since ADR-0132 the registrar is open as well as the wall, so an open + * install passes this probe on its own — `packages/plugins/organizations` is + * Apache-2.0 and carries no licence check. What is left on the commercial + * side is the private subclass's own constructor gate, which fails the MOUNT + * before this code ever runs, and `supportedPostures`, read just below. * * This does not weaken cloud ADR-0016's iron rule (强制免费、治理收费). The rule * is that a deployment RUNNING the group shape must be safe, not that anyone diff --git a/packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts b/packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts index ce6cff0d37..2ad36ceb8a 100644 --- a/packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts +++ b/packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts @@ -1,6 +1,6 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. // -// #5262 — DevPlugin decides whether to load the enterprise multi-org runtime +// #5262 — DevPlugin decides whether to load the multi-org runtime // from the AUTHORITATIVE tenancy posture, never the demoted // `OS_MULTI_ORG_ENABLED` boolean. // diff --git a/packages/plugins/plugin-dev/src/dev-plugin.ts b/packages/plugins/plugin-dev/src/dev-plugin.ts index dce6439a0c..82b5984c9e 100644 --- a/packages/plugins/plugin-dev/src/dev-plugin.ts +++ b/packages/plugins/plugin-dev/src/dev-plugin.ts @@ -677,7 +677,7 @@ export class DevPlugin implements Plugin { } } - // [#5301] The enterprise organizations plugin, once constructed — held so + // [#5301] The organizations plugin, once constructed — held so // the child-`init()` loop below can tell ITS refusal apart from every other // child plugin's. That loop is best-effort by design (a dev stack survives // an absent service), but "the organization wall failed to come up" is the @@ -688,14 +688,14 @@ export class DevPlugin implements Plugin { // OrganizationsPlugin (when multi-org; ENTERPRISE `@objectstack/organizations`, // ADR-0105 D12) MUST register BEFORE SecurityPlugin because // SecurityPlugin.start() probes the `org-scoping` service (the historical - // name the enterprise plugin keeps registering) and caches the result for + // name the organizations plugin keeps registering) and caches the result for // the lifetime of the plugin. if (enabled('security')) { // [ADR-0105 D1 / #5262] Key off the resolved POSTURE, exactly as // `serve.ts` does — ⛔ never `resolveMultiOrgEnabled()`. That boolean was // DEMOTED to a back-compat input of `resolveTenancyPosture()`, so a dev // stack configured the documented way (`OS_TENANCY_POSTURE=isolated|group`, - // legacy boolean unset) read `false` here and never loaded the enterprise + // legacy boolean unset) read `false` here and never loaded the multi-org // runtime at all — SecurityPlugin then probed an absent `org-scoping`, // stripped the wildcard `tenant_isolation` RLS, and the stack served // traffic in the ADR-0093 D5 degraded state while the `tenancy` service @@ -745,7 +745,7 @@ export class DevPlugin implements Plugin { if (!resolveAllowDegradedTenancy()) { throw new Error( `tenancy posture '${tenancyPosture}' was requested but @objectstack/organizations ` - + '(the enterprise multi-org runtime) could not be loaded, so the organization wall is ' + + '(the multi-org runtime) could not be loaded, so the organization wall is ' + 'INACTIVE. Refusing to initialize — a stack that requested multi-organization ' + 'isolation must not serve traffic without it (ADR-0093 D5). Fix one of: ' + 'install @objectstack/organizations; or set OS_TENANCY_POSTURE=single (and unset ' @@ -757,7 +757,7 @@ export class DevPlugin implements Plugin { // Names the posture that was actually requested, not one knob's // spelling of it: the old text asserted `OS_MULTI_ORG_ENABLED=true` // at an operator who may well have set only `OS_TENANCY_POSTURE`. - ctx.logger.warn(` ✘ DEGRADED TENANCY (OS_ALLOW_DEGRADED_TENANCY=1): tenancy posture '${tenancyPosture}' requested but @objectstack/organizations (enterprise) not installed — running single-org, organization wall INACTIVE (ADR-0093 D5)`); + ctx.logger.warn(` ✘ DEGRADED TENANCY (OS_ALLOW_DEGRADED_TENANCY=1): tenancy posture '${tenancyPosture}' requested but @objectstack/organizations not installed — running single-org, organization wall INACTIVE (ADR-0093 D5)`); // Degraded boot: `orgMod` stays undefined, so stage 2 is skipped. // Nothing was loaded, so nothing can be constructed. } @@ -893,7 +893,7 @@ export class DevPlugin implements Plugin { try { await plugin.init(ctx); } catch (err: any) { - // [#5301] One child's init failure is NOT best-effort: the enterprise + // [#5301] One child's init failure is NOT best-effort: the // organizations plugin declining here means the organization wall a // walled posture asked for is INACTIVE, and ADR-0093 D5 forbids serving // traffic in that state. This is stage 2's other half — under @@ -906,7 +906,7 @@ export class DevPlugin implements Plugin { // absent and I accept that", so it is deliberately not consulted here. if (organizationsPlugin !== undefined && plugin === organizationsPlugin) { throw new Error( - 'the enterprise @objectstack/organizations runtime was loaded but its OrganizationsPlugin ' + 'the @objectstack/organizations runtime was loaded but its OrganizationsPlugin ' + `failed to initialize, so the organization wall requested by tenancy posture ` + `'${resolveTenancyPosture()}' is INACTIVE. Refusing to initialize — a stack that ` + 'requested multi-organization isolation must not serve traffic without it (ADR-0093 D5). ' diff --git a/packages/plugins/plugin-security/README.md b/packages/plugins/plugin-security/README.md index 21c888ffd5..04e20c0fb5 100644 --- a/packages/plugins/plugin-security/README.md +++ b/packages/plugins/plugin-security/README.md @@ -103,7 +103,7 @@ Compilation output is a filter AST merged into every query's `where` clause, so ## When to use - ✅ Any multi-user deployment. -- ✅ Enforcing tenant isolation — the wall itself comes from the enterprise organizations runtime described above, not from this package. +- ✅ Enforcing tenant isolation — the wall itself comes from the `@objectstack/organizations` runtime described above, not from this package. ## When not to use diff --git a/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts b/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts index b7c51b0fd3..a31805e7c3 100644 --- a/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts +++ b/packages/plugins/plugin-security/src/authz-matrix-gate.test.ts @@ -430,7 +430,7 @@ describe('authz Layer-0 matrix gate — ADR-0095 D1 (post-extraction)', () => { }); it('member inserting with NO organization_id passes the wall (auto-stamp territory)', async () => { // plugin-security validates SUPPLIED values; it never fills an absent one. - // Stamping belongs to the enterprise organizations runtime — which is also + // Stamping belongs to the organizations runtime — which is also // what activates a walled posture, so the two are never out of step // (ADR-0105 D5/D12). The Layer 0 check must NOT deny an absent value // (ordering-independent w.r.t. that middleware). diff --git a/packages/plugins/plugin-security/src/bootstrap-system-capabilities.test.ts b/packages/plugins/plugin-security/src/bootstrap-system-capabilities.test.ts index dd988c4f6c..4dbb44254c 100644 --- a/packages/plugins/plugin-security/src/bootstrap-system-capabilities.test.ts +++ b/packages/plugins/plugin-security/src/bootstrap-system-capabilities.test.ts @@ -410,7 +410,7 @@ describe('[#8470] the curated half owns its row, not whichever row shares the na // // Reachability of the other three, measured rather than assumed: // - `admin` — REACHABLE and ordinary. `organization_id` auto-stamping lives - // in the enterprise `@objectstack/organizations` runtime, which is also + // in the `@objectstack/organizations` runtime, which is also // what ACTIVATES every walled posture. A deployment without it is `single` // posture with no stamper, so EVERY Setup-authored capability row lands in // the NULL-organization bucket. This is the default community shape, not diff --git a/packages/plugins/plugin-security/src/no-active-organization-write-refusal.test.ts b/packages/plugins/plugin-security/src/no-active-organization-write-refusal.test.ts index eab245aa02..b58e6f4911 100644 --- a/packages/plugins/plugin-security/src/no-active-organization-write-refusal.test.ts +++ b/packages/plugins/plugin-security/src/no-active-organization-write-refusal.test.ts @@ -11,7 +11,7 @@ * the right guard for a payload naming ANOTHER tenant (#2937 / ADR-0105 D5) and * it leaves the opposite case wide open: a payload naming NO tenant, written by * a caller who HAS no tenant. Nothing downstream fills it either (auto-stamping - * lives in the enterprise organizations runtime and has nothing to stamp), so + * lives in the organizations runtime and has nothing to stamp), so * the row landed with `organization_id` NULL and the read wall then hid it from * every reader — including the author who had just created it (#8208). * diff --git a/packages/plugins/plugin-security/src/security-plugin.test.ts b/packages/plugins/plugin-security/src/security-plugin.test.ts index 7daa709a5f..eb688e2ad3 100644 --- a/packages/plugins/plugin-security/src/security-plugin.test.ts +++ b/packages/plugins/plugin-security/src/security-plugin.test.ts @@ -625,7 +625,7 @@ describe('SecurityPlugin', () => { }; await harness.run(opCtx); // must not throw — no cross-tenant check on an absent value // [ADR-0105 D5/D12] SecurityPlugin never stamps organization_id. That stays - // with the enterprise organizations runtime, which is also what activates a + // with the organizations runtime, which is also what activates a // walled posture — so a forged `org-scoping` registration yields NULL-org // rows the wall hides (a broken deployment), not a working unlicensed one. expect(opCtx.data.organization_id).toBeUndefined(); diff --git a/packages/plugins/plugin-sharing/src/sharing-plugin.ts b/packages/plugins/plugin-sharing/src/sharing-plugin.ts index c8cc0016a9..59c1fb8c80 100644 --- a/packages/plugins/plugin-sharing/src/sharing-plugin.ts +++ b/packages/plugins/plugin-sharing/src/sharing-plugin.ts @@ -8,7 +8,7 @@ import { // classification owns #13906 decision 1 option A (branded "never registered" // stays quiet, every other rejection is the ADR-0112 outage) and reads the // posture IN FORCE off the `tenancy` service (ADR-0093 D4/D5: a deployment - // that REQUESTS `isolated` without the enterprise organizations runtime is + // that REQUESTS `isolated` without the `@objectstack/organizations` runtime is // `single` in force). What stays HERE is the wiring fact -- see // `resolveAdmissionTenancyPosture` for why this seam's quiet answer is its // own argument and not the helper's. diff --git a/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts b/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts index 5013414067..bd796580b5 100644 --- a/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts +++ b/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts @@ -529,7 +529,7 @@ describe('attachments permission matrix (#2755)', () => { }); }); -// ── (g) tenant isolation — enterprise multi-org boot ───────────────────── +// ── (g) tenant isolation — multi-org boot ──────────────────────────────── // #4700: this probe was a bare `import()` resolved against this file's realpath // inside the framework workspace, so it was constant-false and block (g) had // never executed. Shared host-app resolution + a declarative switch now decide diff --git a/packages/qa/dogfood/test/authz-conformance.matrix.ts b/packages/qa/dogfood/test/authz-conformance.matrix.ts index de9e47f8c3..5559e3b633 100644 --- a/packages/qa/dogfood/test/authz-conformance.matrix.ts +++ b/packages/qa/dogfood/test/authz-conformance.matrix.ts @@ -154,13 +154,13 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // unit-proven at the plugin-security layer, like the #2937 row above. ── { id: 'tenancy-posture-spectrum', summary: 'three tenancy postures — single | group | isolated (ADR-0105 D1/D2)', state: 'enforced', enforcement: 'plugin-auth/tenancy-service.ts resolves the posture in force; plugin-security/tenant-layer.ts computeTenantLayer0Filter switches the Layer 0 predicate on it — inert (single), `organization_id IN accessible_org_ids` (group, MOAC union), `organization_id = activeOrganizationId` (isolated); empty/absent scope → RLS_DENY_FILTER', - note: 'Only the PREDICATE widens; composition is untouched — Layer 0 is still computed independently of the RLS compiler, AND-composed outermost, and crossable only by a true PLATFORM_ADMIN on a posture-permitting object, so ADR-0095 W1/W2 hold in every posture. BOTH walled postures require the enterprise @objectstack/organizations runtime to ACTIVATE and can resolve degraded without it (ADR-0105 D12): the wall\'s IMPLEMENTATION is open, but enabling multi-organization operation is an entitlement, and the runtime may narrow it further via `supportedPostures`. Cloud ADR-0016\'s 铁律 is satisfied by REFUSING to run an unwalled multi-org deployment (ADR-0093 D5), not by giving the posture away. Unit-proven in plugin-security/tenant-layer.test.ts and plugin-auth/tenancy-service.test.ts.' }, + note: 'Only the PREDICATE widens; composition is untouched — Layer 0 is still computed independently of the RLS compiler, AND-composed outermost, and crossable only by a true PLATFORM_ADMIN on a posture-permitting object, so ADR-0095 W1/W2 hold in every posture. BOTH walled postures require the @objectstack/organizations runtime to ACTIVATE and can resolve degraded without it (ADR-0105 D12 as amended by ADR-0132): the wall\'s IMPLEMENTATION is open AND, since ADR-0132, so is the registrar that activates it — the Apache-2.0 package entitles both walled postures by construction (ADR-0132 D4) and carries no licence check (D2). ONE NAME, TWO PACKAGES (D3): a commercial subclass resolved from the same name may narrow what IT entitles via `supportedPostures`. Cloud ADR-0016\'s 铁律 is satisfied by REFUSING to run an unwalled multi-org deployment (ADR-0093 D5), not by giving the posture away. Unit-proven in plugin-security/tenant-layer.test.ts and plugin-auth/tenancy-service.test.ts.' }, { id: 'accessible-org-ids', summary: 'accessible_org_ids — core-resolved org access set (ADR-0105 D2)', state: 'enforced', enforcement: 'core/security/resolve-authz-context.ts resolveUserAuthzGrants reads every sys_member row for the user under ADR-0091 validity windows; carried on ExecutionContext by every transport (rest-server, runtime resolve-execution-context, mcp, hono) and read directly by the Layer 0 group wall', note: 'ONE read serves both the active-org position projection and the full membership set, so the two facts cannot disagree. A transport that fails to carry the set denies under `group` rather than falling back to the active org — the wall must not depend on which surface the request arrived through. Delegated (on-behalf-of) reads resolve the DELEGATOR\'s own set (explain-engine buildContextForUser), never inherit the live principal\'s.' }, { id: 'org-write-validation', summary: 'bulk-aware organization_id write validation (ADR-0105 D5)', state: 'enforced', enforcement: 'plugin-security/security-plugin.ts step 3.7 — every SUPPLIED organization_id (single row AND bulk array) must satisfy the Layer 0 filter or the whole write is denied', - note: 'The bulk path was a genuine hole: the pre-D5 check required a non-array payload, so an INSERT of an ARRAY could carry a forged organization_id per row — the #2937 defect one call site down. STAMPING an absent value is deliberately NOT done here: it belongs to the enterprise @objectstack/organizations runtime (Middleware A), which is also what ACTIVATES every walled posture (ADR-0105 D12), so the stamper is always present wherever a wall is. Keeping the stamp there means a forged `org-scoping` registration produces NULL-org rows the wall hides — a broken deployment, not a working unlicensed one — while validation stays open because it is a security property, not a packaging one. Unit-proven in plugin-security/security-plugin.test.ts + authz-matrix-gate.test.ts.' }, + note: 'The bulk path was a genuine hole: the pre-D5 check required a non-array payload, so an INSERT of an ARRAY could carry a forged organization_id per row — the #2937 defect one call site down. STAMPING an absent value is deliberately NOT done here: it belongs to the @objectstack/organizations runtime (Middleware A), which is also what ACTIVATES every walled posture (ADR-0105 D12), so the stamper is always present wherever a wall is. Keeping the stamp there means a forged `org-scoping` registration produces NULL-org rows the wall hides — a broken deployment, not a working unlicensed one — while validation stays in plugin-security because it must hold under every posture, walled or not. Since ADR-0132 the stamper ships open as well, so this split is about WHERE each half runs and no longer about which edition ships it. Unit-proven in plugin-security/security-plugin.test.ts + authz-matrix-gate.test.ts.' }, { id: 'authored-rls-policy-survival', summary: 'app-authored org-scoped RLS policies are never silently stripped (ADR-0105 D3 / F1)', state: 'enforced', enforcement: 'plugin-security/platform-tenant-policies.ts — collectRLSPolicies strips by PROVENANCE (identity against the shipped declaration), not by substring-matching `current_user.organization_id`; an authored policy is retained, warned about once, and fails closed at compile time', note: 'The substring match dropped ANY policy using the token, including app-authored ones — a declared security policy silently unenforced, the ADR-0049 class. getReadFilter shared the defect, so analytics/raw-SQL consumers got an UNSCOPED read. Unit-proven in plugin-security/platform-tenant-policies.test.ts + security-plugin.test.ts.' }, diff --git a/packages/qa/dogfood/test/enterprise-organizations.test.ts b/packages/qa/dogfood/test/enterprise-organizations.test.ts index 8b19788fdb..98a801ee82 100644 --- a/packages/qa/dogfood/test/enterprise-organizations.test.ts +++ b/packages/qa/dogfood/test/enterprise-organizations.test.ts @@ -160,7 +160,7 @@ afterAll(() => { } }); -describe('enterprise multi-org probe (#4700)', () => { +describe('multi-org runtime availability probe (#4700)', () => { it('reports AVAILABLE when the package is installed in the host app', async () => { // The verdict the old probe could never reach, no matter what any app or CI // had installed. This is what makes `describe.skipIf(!organizationsAvailable)` diff --git a/packages/qa/dogfood/test/enterprise-organizations.ts b/packages/qa/dogfood/test/enterprise-organizations.ts index 7d7a7a5d0b..c0788ee48b 100644 --- a/packages/qa/dogfood/test/enterprise-organizations.ts +++ b/packages/qa/dogfood/test/enterprise-organizations.ts @@ -1,8 +1,17 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * #4700 — availability of the enterprise `@objectstack/organizations` package - * (ADR-0105 D12), for the dogfood gates that can only run multi-org. + * #4700 — availability of the `@objectstack/organizations` package (ADR-0105 + * D12 as amended by ADR-0132), for the dogfood gates that can only run + * multi-org. + * + * ⚠️ The FILENAME still says "enterprise" and the package no longer is: since + * ADR-0132 the framework publishes an Apache-2.0 build of that name and a + * commercial deployment resolves the same name to a private licence-gated + * subclass. What this module probes is neither of those in particular — it asks + * only whether the HOST APP has a package of that name it can load, which is + * the one question the `describe.skipIf` gates below actually need. Renaming + * the file is a separate, mechanical change and deliberately not made here. * * ── The defect this replaces ───────────────────────────────────────────────── * diff --git a/packages/qa/dogfood/test/no-active-organization-write-refusal.dogfood.test.ts b/packages/qa/dogfood/test/no-active-organization-write-refusal.dogfood.test.ts index 35475d0a65..2e87fe3bf4 100644 --- a/packages/qa/dogfood/test/no-active-organization-write-refusal.dogfood.test.ts +++ b/packages/qa/dogfood/test/no-active-organization-write-refusal.dogfood.test.ts @@ -73,7 +73,7 @@ describe('[ADR-0123 D2 / #8208] a tenant-scoped write with no active organizatio beforeAll(async () => { // `posture-only` is the mode #8208 measured on: it requests the `isolated` - // posture (the wall is ACTIVE) without the enterprise organizations runtime. + // posture (the wall is ACTIVE) without the organizations runtime. stack = await bootStack(showcaseStack, { multiTenant: 'posture-only' }); ql = stack.kernel.getService('objectql'); adminToken = await stack.signIn(); diff --git a/packages/runtime/src/app-plugin.tenancy-posture.test.ts b/packages/runtime/src/app-plugin.tenancy-posture.test.ts index 089f086895..e94ae0b317 100644 --- a/packages/runtime/src/app-plugin.tenancy-posture.test.ts +++ b/packages/runtime/src/app-plugin.tenancy-posture.test.ts @@ -15,7 +15,7 @@ // The judge here is the EFFECTIVE posture (the `tenancy` service), not the // requested one, and the degraded scenario below is why. What these decisions // actually turn on is "will the per-org replay run INSTEAD of me?" — and that -// replay is enterprise `@objectstack/organizations` middleware. On a degraded +// replay is `@objectstack/organizations` middleware. On a degraded // boot it does not exist, so keying on the REQUEST would defer to a replay that // can never happen and leave the stack with no seed data at all. That case is // pinned explicitly, because it is the one a requested-posture fix would get diff --git a/packages/runtime/src/app-plugin.ts b/packages/runtime/src/app-plugin.ts index e80e8d5d82..228be08e53 100644 --- a/packages/runtime/src/app-plugin.ts +++ b/packages/runtime/src/app-plugin.ts @@ -1613,8 +1613,8 @@ export class AppPlugin implements Plugin { * the analogous moment, and `claim-seed-ownership.ts` names the missing * tenancy half in its own header ("the ownership twin of org-scoping's * `claimOrphanOrgRows`, which back-fills `organization_id`") — that back-fill - * ships in the enterprise organizations runtime, which a single-tenant install - * does not have. This is the open-core half of the same handoff. + * ships in the organizations runtime, which a single-tenant install + * does not have. This is the always-present half of the same handoff. * * Cheap by construction: the backfill's first act is one indexed probe of * `_objectstack_sequences`, and on any install with no untenanted counter it diff --git a/packages/runtime/src/seed-datasets.ts b/packages/runtime/src/seed-datasets.ts index e20a9ea154..389cd5a1a1 100644 --- a/packages/runtime/src/seed-datasets.ts +++ b/packages/runtime/src/seed-datasets.ts @@ -3,7 +3,7 @@ /** * Shared `seed-datasets` registry — the multi-tenant seed-replay contract (#3453). * - * In multi-tenant deployments (enterprise `@objectstack/organizations`) a brand-new + * In multi-tenant deployments (`@objectstack/organizations`) a brand-new * org gets its own private copy of every artifact's demo data by REPLAYING the * kernel's `seed-datasets` list on the `sys_organization` insert (Salesforce-sandbox * style). That list must therefore hold the UNION of every seed source: every diff --git a/packages/services/service-datasource/src/admin-routes.ts b/packages/services/service-datasource/src/admin-routes.ts index 06b97640a5..c06f05ff1e 100644 --- a/packages/services/service-datasource/src/admin-routes.ts +++ b/packages/services/service-datasource/src/admin-routes.ts @@ -16,7 +16,7 @@ import { // classification owns #13906 decision 1 option A (branded "never registered" // stays quiet, every other rejection is the ADR-0112 outage) and reads the // posture IN FORCE off the `tenancy` service (ADR-0093 D4/D5: a deployment - // REQUESTING `isolated` without the enterprise organizations runtime is + // REQUESTING `isolated` without the `@objectstack/organizations` runtime is // `single` in force). The WIRING fact stays here -- see // `resolveAdmissionTenancyPosture` below. classifyAdmissionTenancyPosture, diff --git a/packages/services/service-settings/src/settings-service-plugin.ts b/packages/services/service-settings/src/settings-service-plugin.ts index 3fed0dbdba..75e79fdac4 100644 --- a/packages/services/service-settings/src/settings-service-plugin.ts +++ b/packages/services/service-settings/src/settings-service-plugin.ts @@ -334,7 +334,7 @@ export class SettingsServicePlugin implements Plugin { * * `resolveTenancyPosture()` reads `OS_TENANCY_POSTURE` — what the deployment * ASKED for. Under ADR-0093 D4/D5 a deployment that requests `isolated` - * without the enterprise organizations runtime is `single` in force, so the + * without the organizations runtime is `single` in force, so the * requested value would refuse legitimate automation over a wall that is not * there. The posture the guards must see is the one the `tenancy` service * reports, which is what {@link effectiveTenancyPosture} reads. diff --git a/packages/services/service-storage/src/storage-service-plugin.ts b/packages/services/service-storage/src/storage-service-plugin.ts index 61eb9f1115..d0259bc283 100644 --- a/packages/services/service-storage/src/storage-service-plugin.ts +++ b/packages/services/service-storage/src/storage-service-plugin.ts @@ -6,7 +6,7 @@ import { isAuthzStoreUnavailableError, // [#16013] The download door's tenancy-posture read, in ONE call: the // posture IN FORCE (ADR-0093 D4/D5 - a deployment REQUESTING `isolated` - // without the enterprise organizations runtime is `single` in force), taken + // without the `@objectstack/organizations` runtime is `single` in force), taken // through the one shared classification #13906 decision 1 option A requires // - the registry's "never registered" brand stays quiet, and the loud outage // is what every other rejection becomes. diff --git a/packages/types/src/env.ts b/packages/types/src/env.ts index 1e3f3a5c5e..2b5d97ce4c 100644 --- a/packages/types/src/env.ts +++ b/packages/types/src/env.ts @@ -215,7 +215,7 @@ export function resolvePlatformOwnerEmail(): string | undefined { /** * Escape hatch for the degraded-tenancy boot guard (ADR-0093 D5). * - * When `OS_MULTI_ORG_ENABLED=true` but the enterprise `@objectstack/organizations` + * When `OS_MULTI_ORG_ENABLED=true` but the `@objectstack/organizations` * package cannot provide tenant isolation, the platform refuses to boot — a * deployment that asked for tenant isolation must not serve traffic pretending * to have it (ADR-0049 at the deployment layer). Setting this to a truthy value diff --git a/packages/types/src/node.ts b/packages/types/src/node.ts index f72b9ca85d..e9c6c4cde9 100644 --- a/packages/types/src/node.ts +++ b/packages/types/src/node.ts @@ -30,11 +30,11 @@ * their realpath is inside the *framework* workspace — a bare import from any of * them can only ever see packages installed in the framework's own * `node_modules`. Every package that lives OUTSIDE that workspace and is supplied - * by the app being served, verified or tested — a cloud-private package such as - * `@objectstack/organizations` or `@objectstack/service-ai-studio`, or anything a - * customer installs into their own project — is therefore invisible to a bare - * import, no matter what the host app declares in its `package.json` - * (cloud#1013: `objectstack serve` could never load the enterprise multi-org + * by the app being served, verified or tested — a HOST-SUPPLIED package such as + * `@objectstack/organizations` or the cloud-private `@objectstack/service-ai-studio`, + * or anything a customer installs into their own project — is therefore invisible + * to a bare import, no matter what the host app declares in its `package.json` + * (cloud#1013: `objectstack serve` could never load the multi-org * runtime, so every self-hosted walled-posture deployment hit the ADR-0093 D5 * fail-fast and exited 1; framework#4700: `bootStack({ multiTenant: true })` told * apps to install a package they had already installed, and the dogfood From c221e16f710652a2ff2d85036603bd7ffd376968 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:14:50 +0800 Subject: [PATCH 2/3] docs(organizations): correct the enterprise/closed-source spellings the card's pattern could not see (#16718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The card's measurement is a case-sensitive regex over `enterprise (@objectstack/organizations|multi-org|organizations)` plus two `closed-source`/`cloud-private` forms. Re-running it case-INSENSITIVELY, and widening to the neighbouring `cloud-private` / `enterprise package` spellings inside the two files the card names as its worst sites, turns up nine more population-1 statements. All nine are in files the previous commit already corrects; every one is a comment. - `cli/src/commands/serve.ts` (×5) · the host-importer docblock used `@objectstack/organizations` as THE example of "a cloud-private package the framework cannot see" — the same reasoning #16539 already applied to the dogfood probe: it is no longer a name this repo can use as an example of something a host root does not have. The example is now framed on what actually decides resolution (the host app's declaration), with ADR-0132's two-packages fact named. · the cloud#1013 narrative said the load "never found the cloud-private package" — true of 2026-07, asserted in the present tense. Now "then-cloud-private": the history is kept, the present claim is dropped. · the live Stage-1 mount comment said "This package is cloud-private" in the boot path itself. Now APP-DECLARED, citing ADR-0132 D3 — which build of the name a deployment gets is the host manifest's call, not `serve`'s. · the ADR-0093 D5 fail-fast comment said "the enterprise package can't provide tenant isolation". - `plugin-dev/src/dev-plugin.ts` — `ENTERPRISE @objectstack/organizations`, in capitals. The card's pattern is case-sensitive, so this hit was invisible to the 65-occurrence count while sitting two lines above one that was counted. - `plugin-auth/src/auth-plugin.ts` — "so the enterprise package needs no change to light it up", on the `org-scoping` presence probe. - `qa/dogfood/test/enterprise-organizations.ts` — the doc heading on `ORGANIZATIONS_PKG` still opened "The enterprise multi-org package", directly contradicted by the ⚠️ #16539 note two lines below it. - `qa/dogfood/test/rls-multitenant.dogfood.test.ts` — "moved to the ENTERPRISE package ... not part of this open workspace". Both halves false since #16215. The skip is unchanged and its real reason is now stated: the gate needs the runtime resolvable FROM THE APP UNDER TEST, which is what `organizationsAvailable` probes. ⛔ No behaviour change: every hunk is a comment. Populations 2 and 3 are untouched — see the PR body for the per-site accounting. Co-Authored-By: Claude Opus 5 --- packages/cli/src/commands/serve.ts | 32 +++++++++++-------- .../plugins/plugin-auth/src/auth-plugin.ts | 4 +-- packages/plugins/plugin-dev/src/dev-plugin.ts | 4 +-- .../dogfood/test/enterprise-organizations.ts | 2 +- .../test/rls-multitenant.dogfood.test.ts | 10 ++++-- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index 79c6ba46cf..b3aaf19515 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -790,10 +790,12 @@ function anchorServedApp(configArg: string): { configPath: string; configExists: * Node ESM resolves a bare `import(pkg)` against the IMPORTER's own realpath. * The CLI is reached through a workspace/`link:` dependency, so that realpath is * inside the FRAMEWORK workspace: a bare import can only see what the framework - * itself installed. A package supplied by the app being served — a cloud-private - * one such as `@objectstack/organizations`, a distribution one such as - * `@objectstack/service-cluster`, or anything a customer installs into their own - * project — is invisible to it no matter what the host app declares. + * itself installed. A package supplied by the app being served — an app-declared + * one such as `@objectstack/organizations` (open core since ADR-0132; a + * commercial deployment resolves that same name to its own private build), a + * distribution one such as `@objectstack/service-cluster`, or anything a customer + * installs into their own project — is invisible to it no matter what the host + * app declares. * * #4719: "resolve from the host root" means "resolve what the host root * DECLARES". The host lookup was a CJS require, CJS honours NODE_PATH, and the @@ -815,7 +817,7 @@ function anchorServedApp(configArg: string): { configPath: string; configExists: * * • cloud#1013 — the binding sat below the AUTH block, so the enterprise * organizations load resolved in the framework workspace, never found the - * cloud-private package, and every walled-posture deployment hit the + * then-cloud-private package, and every walled-posture deployment hit the * ADR-0093 D5 fail-fast and exited 1. * • #10645 — the binding sat below the CLUSTER block, so `serve` could not load * an app-declared `@objectstack/service-cluster*` at all: on the published EE @@ -3731,18 +3733,20 @@ export default class Serve extends Command { // ── Stage 1: import. Failure here = the package is ABSENT. ── try { // Resolve from the HOST APP (cloud#1013). This package is - // cloud-private: it is installed in the served app's - // node_modules, never in the framework workspace the CLI's own - // realpath points at, so a bare import here could never find it - // — `objectstack serve` failed the fail-fast below on EVERY - // self-hosted walled-posture deployment, and the only way past - // it was OS_ALLOW_DEGRADED_TENANCY=1, i.e. exactly the unwalled - // state D5 exists to prevent. The host app declares the package; - // this resolves it from there. + // APP-DECLARED (ADR-0132 D3 — which build of the name a + // deployment gets is the host manifest's call, not `serve`'s): + // it is installed in the served app's node_modules, never in + // the framework workspace the CLI's own realpath points at, so a + // bare import here could never find it — `objectstack serve` + // failed the fail-fast below on EVERY self-hosted walled-posture + // deployment, and the only way past it was + // OS_ALLOW_DEGRADED_TENANCY=1, i.e. exactly the unwalled state D5 + // exists to prevent. The host app declares the package; this + // resolves it from there. orgMod = await importFromHost(organizationsPkg); } catch (orgErr) { // ADR-0093 D5 — degraded tenancy fails fast. Multi-org was - // requested but the enterprise package can't provide tenant + // requested but the organizations package can't provide tenant // isolation: `tenant_isolation` RLS would be stripped and every // org boundary inert. A deployment that asked for isolation must // NOT serve traffic pretending to have it (ADR-0049 at the diff --git a/packages/plugins/plugin-auth/src/auth-plugin.ts b/packages/plugins/plugin-auth/src/auth-plugin.ts index 51ee0b1831..6279de6b16 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.ts @@ -552,8 +552,8 @@ export class AuthPlugin implements Plugin { // the plugin's first service registration (consumers and tests rely on that // ordering). The `isolated` posture derives `isolationActive` from the // presence of the `org-scoping` service (registered by - // @objectstack/organizations when installed), so the enterprise package - // needs no change to light it up; `group` is enforced by the open engine and + // @objectstack/organizations when installed), so that package needs no + // change to light it up; `group` is enforced by the open engine and // never probes. `getService` is a cheap registry lookup and org-scoping // registers AFTER plugin-auth, so the probe is deferred to first read // (start()/request time). diff --git a/packages/plugins/plugin-dev/src/dev-plugin.ts b/packages/plugins/plugin-dev/src/dev-plugin.ts index 82b5984c9e..8f242f9ec2 100644 --- a/packages/plugins/plugin-dev/src/dev-plugin.ts +++ b/packages/plugins/plugin-dev/src/dev-plugin.ts @@ -685,8 +685,8 @@ export class DevPlugin implements Plugin { let organizationsPlugin: Plugin | undefined; // 5. Security Plugin (RBAC, RLS, field-level masking) - // OrganizationsPlugin (when multi-org; ENTERPRISE `@objectstack/organizations`, - // ADR-0105 D12) MUST register BEFORE SecurityPlugin because + // OrganizationsPlugin (when multi-org; `@objectstack/organizations`, ADR-0105 + // D12 as amended by ADR-0132) MUST register BEFORE SecurityPlugin because // SecurityPlugin.start() probes the `org-scoping` service (the historical // name the organizations plugin keeps registering) and caches the result for // the lifetime of the plugin. diff --git a/packages/qa/dogfood/test/enterprise-organizations.ts b/packages/qa/dogfood/test/enterprise-organizations.ts index c0788ee48b..5c528f7cb0 100644 --- a/packages/qa/dogfood/test/enterprise-organizations.ts +++ b/packages/qa/dogfood/test/enterprise-organizations.ts @@ -96,7 +96,7 @@ import { createHostImporter, hostImportFailureKind } from '@objectstack/types/node'; /** - * The enterprise multi-org package (ADR-0105 D12) these gates need. + * The multi-org package (ADR-0105 D12 as amended by ADR-0132) these gates need. * * ⚠️ #16539: no longer cloud-private — ADR-0132 / #16215 brought it back to open core as * `packages/plugins/organizations`. It stays the SUBJECT of this probe (the app diff --git a/packages/qa/dogfood/test/rls-multitenant.dogfood.test.ts b/packages/qa/dogfood/test/rls-multitenant.dogfood.test.ts index 8b0a004a29..8681897560 100644 --- a/packages/qa/dogfood/test/rls-multitenant.dogfood.test.ts +++ b/packages/qa/dogfood/test/rls-multitenant.dogfood.test.ts @@ -53,9 +53,13 @@ import crmStack from '@objectstack/example-crm'; import { bootStack, type VerifyStack } from '@objectstack/verify'; import { runRlsProofs, formatRlsReport, type RlsReport } from '@objectstack/verify'; -// The multi-org runtime moved to the ENTERPRISE `@objectstack/organizations` -// package (ADR-0105 D12) — not part of this open workspace. Skip (loudly) when -// it isn't linked in; enterprise/cloud CI, which ships the package, runs this. +// The multi-org runtime lives in `@objectstack/organizations` — ADR-0105 D12 as +// amended by ADR-0132: open core since #16215 (Apache-2.0, no licence check of +// any kind), while a commercial deployment resolves that same name to its own +// private, licence-gated build. What this gate still needs is that runtime +// RESOLVABLE FROM THE APP UNDER TEST, which is what `organizationsAvailable` +// probes — skip (loudly) when it is not; cloud CI, which ships the package, +// runs this. // // #4700: the probe used to be a bare `import()`, which Node ESM resolves against // this file's own realpath in the framework workspace — so it answered "not From 076f1d54ebc4a7d51fb920ebc9157b394c68a751 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:38:25 +0800 Subject: [PATCH 3/3] chore(changeset): operator-facing organizations text is no longer enterprise-only (#16718) Co-Authored-By: Claude Opus 5 --- .changeset/organizations-open-core-prose.md | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .changeset/organizations-open-core-prose.md diff --git a/.changeset/organizations-open-core-prose.md b/.changeset/organizations-open-core-prose.md new file mode 100644 index 0000000000..55844b53e8 --- /dev/null +++ b/.changeset/organizations-open-core-prose.md @@ -0,0 +1,23 @@ +--- +'@objectstack/cli': patch +'@objectstack/plugin-dev': patch +--- + +Operator-facing text no longer tells an open-source install that multi-organization +operation requires a subscription. + +ADR-0132 moved the `org-scoping` registrar into open core — `@objectstack/organizations` +is Apache-2.0, carries no licence check, and declares both walled postures (`group` and +`isolated`) as its own constant. The messages an operator actually reads had not followed: + +- `os serve`'s install remedy for a walled posture ended "this runtime is closed-source and + is NOT on the public npm registry ... Without one this bullet is not followable" — it now + says the runtime is Apache-2.0 and on the public registry, and notes that a commercial + deployment resolves the same package name to its own private, licence-gated build. +- The `isolated` posture hint rendered by `os serve` and `os doctor` no longer calls the + runtime "enterprise". +- `os verify`'s `--org-scoped` flag description drops the same word. +- The dev stack's degraded-tenancy warning and its stage-2 mount refusal no longer describe + the package as the enterprise runtime. + +Text only — no control flow, no identifiers, no behaviour change.