Skip to content

Commit 5741ff1

Browse files
claude[bot]claude
andauthored
docs(organizations): state ADR-0132's entitlement boundary as the reason the cross-tenant proofs stand in (#17910)
Fixes #17369 Clause-②: no > **Re-written by the `domain:cli` seat (#6024) at 2026-09-13T01:17Z after a body rewrite dropped it.** The declaration is the seat's, ⛔ not the implementer's, and it is **re-measured on this head** `7990da9233af387b15acd8c5e8e748855087d450` (unchanged since 00:38Z) against merge base `a9c64779046facc1b0b2e74f71a9441b0f5b7fb8`: added exported declarations in `packages/verify/src/harness.ts` — the diff's only non-test source file — **0**, control lit (`export` occurs **5** times there); **non-comment added lines in that file: 0**, so 「comment-only」 is measured rather than asserted; whole-diff added `export const|function|class|interface|type|default`: **0**; `package.json` files touched: **0**, so no dependency moved and ADR-0132's entitlement-boundary pin keeps its premise. ⇒ the axis stands down and `@objectstack/verify: patch` stands. > ⚠️ **Please leave these four lines in place.** `scripts/check-changeset-no-major.mjs` reads this declaration out of the PR body; removing it re-reds `Check Changeset` with no push involved, which is what happened at 01:15:13Z. The seat's process fault, owned on the card at the record below: the gate was cleared at 00:40Z and the author was never told, so a later body rewrite clobbered it. Option 3, as ruled (director batch #114 item 3, carried by the maintainer's 「其他同意」 at `5629544916`): **the prose moves, the behaviour does not.** Zero dependency changes, zero behaviour changes, `no-framework-dependents.pin.test.ts` untouched and green. Acceptance is triage's rewritten one (`5649534177` §③), not the ruling's execution line: **class (i) reaches zero**, never "a phrase reaches zero". > 4. ⛔ **永远不要求 `git grep -nF "not installable"` 归零** That grep still reads 5 hits / 5 files, and it is supposed to. ## The enumeration — method, then membership Subject + cause, **with context**, exactly as triage prescribed after breaking the single-line form on this very card: ``` git grep -nE -C2 "cloud-private|not installable|closed-source" ``` On the merge base `a9c64779046facc1b0b2e74f71a9441b0f5b7fb8` (non-shallow): **60 hits / 36 files**, minus published `CHANGELOG.md` and `.changeset/**` ⇒ **47 hits / 30 files**. Every hit was then read in context and classified by hand — ⛔ no single-line `| grep -i organizations` filter, which is what hides a site whose subject and cause sit on different lines (`harness.posture.test.ts` is one: the subject is on line 9, the cause on line 10). **Membership proof against the claim's 30-file set** (`5649601883`) — ⛔ not a count agreeing with a count: | check | reading | |:--|--:| | files in my enumeration | 30 | | files in the seat's table | 30 | | in mine, not in the seat's | **0** | | in the seat's, not in mine | **0** | | symmetric difference | **0** | | intersection (the instrument firing) | **30** | The two sets are equal by membership, and the intersection is non-empty, so the comparison instrument is live rather than silently matching nothing. ## Per-site classification — all 47 in-scope hits **(i) still states the stale reason as currently true = MUST-FIX · (ii) already past tense / already recorded as "no longer cloud-private" = not touched · (iii) the subject is another package = not touched** Counts: **(i) 21 hits / 13 files — all fixed · (ii) 19 hits / 13 files · (iii) 7 hits / 5 files**. ### Class (i) — fixed (21 hits, 13 files) | site | what it said | why (i) | |:--|:--|:--| | `packages/verify/src/harness.ts:45` | "Stand-in for the **cloud-private** `@objectstack/organizations` runtime" | the epithet is the justification for the stand-in, present tense | | `packages/verify/src/harness.ts:159` | "instead of requiring the **cloud-private** enterprise package" | same, in the `BootOptions.multiTenant` TSDoc | | `packages/verify/src/harness.ts:513` | "is **cloud-private** and only ever lives in the host app's `node_modules`, **so** the import could never succeed" | explicit causal `so` | | `packages/verify/src/harness.ts:534` | "is **cloud-private** and resolves from nowhere in the framework workspace" | present-tense cause | | `packages/verify/src/harness.posture.test.ts:10` | "The enterprise package is **not installable in this workspace**, **so** a fake stands in for it" | explicit causal `so` — the card's headline site | | `packages/verify/src/harness.posture-only.test.ts:4` | "without the **cloud-private** enterprise runtime" | epithet justifying the stand-in | | `packages/verify/src/harness.posture-only.test.ts:12` | "is **not installable in this workspace**, **so** the fixture needed an honest way…" | explicit causal `so` | | `packages/verify/src/harness.host-resolution.test.ts:9` | "is **cloud-private** and only ever lives in the verified app's `node_modules`, **so** the import could never succeed" | explicit causal `so` | | `packages/verify/src/harness.host-resolution.test.ts:24` | "(it is **not installable in this workspace** — that is the whole point)" | states the stale cause as the point of the fixture | | `packages/cli/test/serve-organizations-host-resolution.e2e.test.ts:9` | "is **cloud-private** and only ever lives in the served app's `node_modules`, **so** the import could never succeed" | explicit causal `so` | | `packages/cli/test/serve-organizations-host-resolution.e2e.test.ts:24` | "(it is **not installable in this workspace** — that is the whole point)" | as above | | `packages/types/src/node.test.ts:10` | "while the package **is cloud-private** and only ever exists in the host app's `node_modules`. It could **therefore** never resolve" | present-tense cause inside a past-defect narrative | | `packages/qa/dogfood/test/automation-toggle-tenant-scope.dogfood.test.ts:41` | "runtime **is cloud-private and genuinely absent from this workspace**" | both halves false — the package IS a workspace member now | | `packages/qa/dogfood/test/showcase-external-autoconnect.dogfood.test.ts:78` | "the single honest walled harness is `multiTenant: true` with the **cloud-private** `@objectstack/organizations`" | the epithet is why feasibility is in doubt | | `packages/plugins/plugin-dev/src/dev-plugin-tenancy-failfast.test.ts:22` | "is a **cloud-private** enterprise package **genuinely absent from this workspace**, **so** the dynamic import genuinely fails" | explicit causal `so`, and "absent from this workspace" is false | | `packages/plugins/plugin-dev/src/dev-plugin-tenancy-mount-refusal.test.ts:23` | "off the **genuinely-absent cloud-private** package" / "the thing the open-source workspace cannot have" | same premise, restated | | `packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts:23` | "is a **cloud-private** enterprise package that is **genuinely absent from this workspace**, **so** the dynamic import genuinely fails" | explicit causal `so` | | `docs/qa/platform-checklist/areas/attachments-storage.json:189` | MARKER: "`@objectstack/organizations` is **cloud-private**, **so** the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(...)`" | explicit causal `so` | | `docs/qa/platform-checklist/areas/attachments-storage.json:277` | same MARKER string, second carried copy | as above | | `docs/qa/platform-checklist/areas/attachments-storage.json:358` | same MARKER string, third carried copy | as above | | `content/docs/plugins/packages.mdx:331` | "Ships as a **separate, closed-source enterprise package** — it is **not part of the open framework repo**" | ⭐ named by neither the card, the objection nor triage; the most customer-visible site in the set, and flatly false | ⭐ **The `attachments-storage.json` cluster is the one the order singled out, and the effect survives verbatim.** The three copies carry the same MARKER string; only the cause clause changed. The sentences that carry the *effect* — "skips BY DESIGN here. That is `blocked(dependency)`, never a pass and never a defect. A run that is supposed to ship the package sets `OS_TEST_MULTI_ORG_ENABLED=1`, which turns the skip into a hard failure instead of a silent one." — are byte-identical to before, and all three copies stay identical to each other (verified: 3 occurrences before, 3 after, and the file still parses as JSON). ### Class (ii) — already past tense or already self-correcting (19 hits, not touched) | site | why (ii) | |:--|:--| | `packages/cli/src/commands/serve.ts:1107` | already past-tensed: "the **then**-cloud-private package". ⚠️ triage cited this as `:820`; on `a9c64779` it sits at **`:1107`** — the only §1/§3 figure that moved | | `packages/rest/src/rest-api-plugin.ts:300` | record of a **cloud-side** measurement ("cloud#1982 reproduced it … mounted") | | `packages/rest/src/single-kernel-isolated-api-key-matrix.test.ts:21` | same: "cloud#1982 reproduced it on `apps/objectos-ee`" | | `packages/rest/src/single-kernel-isolated-session-org-claim-matrix.test.ts:11` | same: "On a real `objectstack serve` of **cloud's** `apps/objectos-ee`" | | `packages/core/src/security/resolve-authz-context.ts:483` | same incident, past tense ("**measured on** a live `isolated` boot") | | `packages/core/src/security/resolve-authz-context.test.ts:1604` | under the heading "## What **was** measured, before the guard existed" | | `packages/qa/dogfood/test/enterprise-organizations.ts:26` | "while the package **was** cloud-private and **lived** in the host app's `node_modules`" | | `packages/qa/dogfood/test/enterprise-organizations.ts:59` | quotes prose it says the file "**used to**" carry | | `packages/qa/dogfood/test/enterprise-organizations.ts:101` | "⚠️ #16539: **no longer cloud-private** — ADR-0132 / #16215 brought it back to open core" | | `packages/qa/dogfood/test/enterprise-organizations.ts:152` | "the clause that **used to** close this comment … **died with** #16215" | | `packages/qa/dogfood/test/enterprise-organizations.test.ts:7` | "The **old** probe answered 'unavailable' … because it resolved a cloud-private package" | | `packages/qa/dogfood/test/enterprise-organizations.test.ts:20` | "`@objectstack/organizations` **was** cloud-private" | | `packages/qa/dogfood/test/enterprise-organizations.test.ts:53` | "(… **was** cloud-private **when these cases were written**)" | | `packages/types/src/node.test.ts:50` | "a real cloud-private package **at the time**", then records #16215 explicitly | | `packages/plugins/organizations/src/open-only-wall-acceptance.test.ts:20` | "**Until** ADR-0132 moved this package into the open core that **was** unavoidable — the only registrar **was** cloud-private" | | `packages/services/service-cluster/src/multi-node-gate-mount.ts:61` | the same doc block corrects itself eight lines later: "⚠️ `@objectstack/organizations` is ONE NAME over TWO packages **since ADR-0132**" | | `docs/adr/0132-…:35` | history: records what cloud ADR-0081 D2 did. ⛔ ADRs are a maintainer floor and out of scope | | `docs/adr/0132-…:218` | ADR-0132 stating its own consequence. ⛔ out of scope | | `docs/adr/0105-…:357` | already carries an editorial note saying those sentences 「read as history」. ⛔ out of scope | ⚠️ **The five cloud-measurement rows are the one judgement call in this table a reviewer might reasonably take the other way**, so the reasoning is stated rather than implied. Each records a measurement performed in the **commercial** repository, on cloud's `apps/objectos-ee`, with the copy that app declares. ADR-0132 split one package name across two packages, and this repository says so in its own words at `multi-node-gate-mount.ts`: "the framework publishes an Apache-2.0 package of that name and **the commercial repo keeps a private licence-gated subclass of it**". The copy those five sentences name is therefore still cloud-private, and the sentences are still true. `serve.ts:1107` — which triage excluded by name — is the same shape with an explicit `then-` marker; these five are the same shape without one. ### Class (iii) — the subject is another package (7 hits, not touched) | site | subject | |:--|:--| | `content/docs/kernel/services-checklist.mdx:74` | **`@objectstack/service-ai`** — and true as written. This is the hit that makes the ruling's grep-to-zero a wrong edit | | `packages/types/src/node.ts:34` | the epithet attaches to **`@objectstack/service-ai-studio`**, not to organizations. The claim it does make about organizations — host-supplied, invisible to a bare import — is still true (measured below) | | `packages/types/src/node.test.ts:74` | "none of them can see **a cloud-private package**" — the class of such packages generically, and true | | `packages/spec/src/kernel/platform-capabilities.ts:204` | closed-source `@objectstack/` packages **in general** | | `packages/spec/src/kernel/platform-capabilities.ts:252` | the roster row for **`@objectstack/security-enterprise`** (ADR-0057) | | `packages/spec/src/api/error-code-ledger.zod.ts:43` | a closed-source **product's** error states | | `content/docs/references/api/error-code-ledger.mdx:47` | generated from the row above — and `content/docs/references/` is ⛔ never hand-edited | ⇒ The order's `packages/spec` reading is confirmed independently: **neither spec hit names `@objectstack/organizations`**, so nothing here routes to the `domain:spec` seat and no file under `packages/spec/**` is touched. ## The two controls, printed | control | before | after | expected | |:--|--:|--:|:--| | **must FIRE** — `entitlement boundary` | 2 files | **15 files** | fires; it grew because every corrected site now cites the real cause | | **must say NO** — `not installable in this galaxy` | 0 | **0** | still says no, so a zero here is a reading and not a dead instrument | | **must NOT be driven to zero** — `git grep -nF "not installable"` | 5 hits / 5 files | 5 hits / 5 files | unchanged on purpose (`services-checklist.mdx:74` is true) | Class (i) after the change: **0**. Remaining in-scope hits: 32 — every one of them class (ii) or (iii), including five new ones created by the corrections themselves, which say in so many words that being closed-source is *not* the reason. ## The pin, and the boundary it defends ⛔ **Zero dependency changes.** `git grep -l '"@objectstack/organizations"' -- '*package.json'` returns **exactly one** file before and after — `packages/plugins/organizations/package.json`, the package's own manifest. `no-framework-dependents.pin.test.ts` is not in this diff and is green. ## Measured while classifying — the `plugin-dev` premise holds, and is not a function of build state Three `plugin-dev` sites said the dynamic import fails because the package is "genuinely absent from this workspace". Since ADR-0132 it is a workspace member, so before rewriting the cause I had to know whether the **effect** is still true — the order's STOP rule covers exactly this ("if a class-(i) site cannot be made to tell the truth without a behaviour change — STOP and report"). It is true, and for the entitlement-boundary reason: - A bare ESM `import('@objectstack/organizations')` from `packages/plugins/plugin-dev`, run on this file's own vitest runner, answers `ERR_MODULE_NOT_FOUND` **"Cannot find package"** — the package directory is never found. - **Ablation, with `packages/plugins/organizations/dist` stubbed present**: same answer, `ERR_MODULE_NOT_FOUND` "Cannot find package". So the signal is **not** a function of build state. - **Discriminating control, same moment, same tree**: a `require`-shaped resolve with the pnpm bin shim's `NODE_PATH` *did* resolve, to `packages/plugins/organizations/dist/index.js` — so the hoisted store copy is genuinely reachable that way, and the instrument can say "found". Node's ESM resolver does not consult `NODE_PATH`, which is why the two differ. - The stub was removed and the removal proven by `git status --porcelain` on that path being empty; the throwaway probe test was deleted before the first commit. ⇒ No behaviour change is needed anywhere in class (i), so the STOP rule did not fire. That measurement is recorded in `dev-plugin-tenancy-failfast.test.ts` itself, because it is the reason that file may keep reading a real failure rather than a stubbed one — and it is the same build-state trap #16539 had to move a `require`-shaped probe off a workspace name to escape. ## Changeset The gate's own words, from `pr-automation.yml`'s "Require a changeset (or the `skip-changeset` label)" step: > 2. It releases nothing (`.github/`, `.claude/`, `skills/`, `docs/`, `content/`, `examples/`, tests-only, and the like) > -> apply the 'skip-changeset' label. (the workflow marks this route PREFERRED) This PR is **not** tests-only, so route 2 does not apply and the label would be wrong. Measured rather than assumed: `packages/verify/src/harness.ts` is source, its TSDoc is carried into `dist/index.d.ts`, and `dist` is in that package's `files[]`. After a real build of `@objectstack/verify` and its closure, the corrected sentence is present in `packages/verify/dist/index.d.ts`, with a **positive control** (untouched TSDoc prose from the same block: HIT) and a **stale-cache probe** (`cloud-private` in that `.d.ts`: absent, so the artifact is fresh rather than a turbo cache hit). ⚠️ The first attempt at that measurement read **zero for the subject and zero for the control** — a single-line `grep` for a phrase the formatter had wrapped across two lines. A zero against a dead instrument is not a reading; the figures above come from the whitespace-normalised re-measurement. ⇒ Route 1, **`patch`, `@objectstack/verify` only**. The other three touched published packages (`cli`, `plugin-dev`, `types`) changed only `*.test.ts` files, which the shared tsup entry (`entry: ['src/index.ts']`) never reaches and which CI's "No compiled test files in any dist" step forbids in `dist` anyway; `packages/qa/dogfood` is `private: true`. ⛔ No package is named whose published bytes do not move, and no level is raised: no behaviour and no public surface moves, so `patch`. ## Verification Everything below was run on this branch. Exit codes captured **before any pipe**; every heavy run went through `scripts/pm/os-verify-lock.sh` (slot `issue-17369-cli-seat`). | what | command | reading | |:--|:--|:--| | derived gate set | `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` | **92** families (85 before the changeset existed; the changeset added 7) | | all 92 | each run individually | **92 green** | | reconciliation | `node scripts/pm/dispatch-gates.mjs --ran … --repo …` | `92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN` — a **derived** zero: every family carries a recorded exit code and none is 3 | | repo lint | `pnpm lint` | **exit 0**, whole repo, unnarrowed (`eslint . --no-inline-config`) | | ⭐ the pin | `pnpm --filter @objectstack/organizations test` | **exit 0** — `no-framework-dependents.pin.test.ts` green, and not in this diff | | the pin's package typecheck | `pnpm --filter @objectstack/organizations typecheck` | **exit 0** | | plugin-dev | `test` · `typecheck` | **exit 0** · **exit 0** | | types | `test` · `typecheck` | **exit 0** · **exit 0** | | verify typecheck | `pnpm --filter @objectstack/verify typecheck` | **exit 0** | | the touched cli test | `OS_TEST_TIERS=nightly … vitest run test/serve-organizations-host-resolution.e2e.test.ts` | **3 passed (3)** | | the touched dogfood tests | `… vitest run test/automation-toggle-tenant-scope.dogfood.test.ts test/showcase-external-autoconnect.dogfood.test.ts` | **2 files, 11 passed (11)** | | full package build | `turbo run build --filter='./packages/*' --filter='./packages/*/*'` | **72 successful, 72 total** | **Three gates first read non-zero and are recorded twice on purpose**, because a prerequisite failure is neither a pass nor a finding: | gate | first reading | after the prerequisite existed | |:--|:--|:--| | `pnpm --filter @objectstack/spec run check:skill-examples` | exit 1 — its own text: "`packages/client-react/dist` holds no `.d.ts` declarations — the package is not built" | **exit 0**, 258 prose examples type-check | | `pnpm check:dual-build-cjs-loads` | **exit 3** — "PREREQUISITE NOT MET … Run `pnpm build` first. ⛔ This is NOT a pass: nothing was measured." | **exit 0** | | `pnpm check:type-check-debt` | **exit 3** — PREREQUISITE NOT MET (unbuilt closure), then exit 3 again on a tsc OOM under my own `--max-old-space-size` cap, which the gate names as the binding constraint | **exit 0** — "5 ledger entr(ies) re-measured in 72.1s, 55 raw tsc error(s) total, none above its recorded number" | ⚠️ **`pnpm --filter @objectstack/cli exec vitest run --project integration …` returned exit 1 reading "No test files found" — recorded as NOT MEASURED, ⛔ not as a red.** I had assumed "spawns the CLI ⇒ integration tier" and that was wrong twice over: `packages/cli/vitest-tiers.ts` classifies `*.e2e.test.*` as a **nightly** tier, so with `OS_TEST_TIERS` unset the file is outside **both** projects' population by design. The passing reading in the table above is the one taken under the switch the file actually runs in. `test/vitest-tiers-partition.test.ts` is green (22 passed), so the partition itself is intact. ### ⛔ One pre-existing failure in `packages/verify`, proven not to be this diff — filed as #17911 `pnpm --filter @objectstack/verify test` reads **`Tests 1 failed | 102 passed (103)`**. The failure is `harness.host-resolution.test.ts` → "CONTROL — the `declared-unresolvable` remedy is unchanged: declared, not installed (#4719)", and it is **#16539's defect in a file that card's fix did not reach**: - **My delta to that file is comment-only** — the failing assertion is at line 299, untouched; the diff is a doc-block header. - **Ablation, same built tree**: the file was restored from its `origin/main` blob `c89a1c1b1` and re-run — **same single failure, same case**. Restore proven by `git diff HEAD` empty, `git status --porcelain` empty, and the blob back to HEAD's `e4ea9ed7e`. - **Cause, two-legged on one tree**: `packages/plugins/organizations/dist` present → `1 failed | 6 passed (7)`; the same directory moved aside → **`7 passed (7)`**. So the control's verdict is a function of whether a sibling package has been built. ⇒ Not fixed here — it is a behaviour change to a test fixture, which this prose-only card may not make. Filed unassigned and bare as **#17911**. ## Acceptance notes Measured in passing, in scope for nothing here: - **`packages/plugins/plugin-dev/src/dev-plugin-tenancy-failfast.test.ts:35-37`** said `@objectstack/organizations` "is really absent" — the same falsified premise as the class-(i) hit eleven lines above it, but carrying none of the three enumerated phrases, so the ruled instrument cannot see it. Corrected in place to "it really does fail to resolve from here", because leaving it would have made the file contradict its own corrected paragraph. ⛔ Same file, same sentence, same defect — no new behaviour and no new verification surface. - **`content/docs/plugins/packages.mdx:329`** still heads the section `### @objectstack/organizations (enterprise)`. "enterprise" reads as a product tier rather than a licensing claim, and the heading is a doc anchor other pages may link to, so it is left alone. noted, not filed: renaming it is a docs decision, not a defect. - **`packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts:29`** says a successfully mounted plugin is "unobservable in open-source CI". That is still true for `plugin-dev` (it may not declare the package), so it is not a class-(i) site — but the phrase "open-source CI" now carries the old framing. noted, not filed: no false statement, and no PR or person is queued to touch this file. - ⛔ **No `packages/spec/**` and no `docs/adr/**` file is touched**, and both readings that would have forced one were checked independently rather than inherited: the two spec hits name other subjects, and all three ADR hits are history. - **A pre-existing, build-state-dependent failure in `packages/verify/src/harness.host-resolution.test.ts`** — filed as **#17911** after a targeted duplicate search (REST `/search` is 403 for this seat by design, so one targeted MCP `search_issues`, declared here as the channel switch; it returned 2 results, so the instrument fired rather than being silently empty). #16539 is the same defect in `packages/qa/dogfood` and is **closed**; #16552 applied the same remedy to `packages/types`. `packages/verify` was never covered. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7ce3154 commit 5741ff1

14 files changed

Lines changed: 96 additions & 44 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/verify": patch
3+
---
4+
5+
Comment-only correction: the reason `bootStack`'s cross-tenant proofs stand in for `@objectstack/organizations` is now stated as the true one.
6+
7+
Those doc comments said the enterprise multi-organization runtime was **cloud-private / not installable in this workspace**. ADR-0132 falsified that: the runtime is open core, Apache-2.0, and published on npm. The effect they describe has not changed, so the text now gives the reason that is actually load-bearing — **ADR-0132's entitlement boundary forbids any framework package DECLARING `@objectstack/organizations`** (`packages/plugins/organizations/src/no-framework-dependents.pin.test.ts`, its mechanical half: "Apps declare it; packages do not"), because the commercial repository ships a licence-gated subclass under the same package name. So `packages/verify` cannot depend on the runtime and cannot resolve it, the `'posture-only'` stand-in stays exactly what it was, and the proof that the real plugin walls tenants still lives in cloud's `security-enterprise` multi-organization integration test.
8+
9+
⛔ **No behaviour, no dependency and no public surface moves.** `BootOptions.multiTenant` accepts and does the same things it did; the only shipped bytes that change are the doc comments carried into `dist/index.d.ts`. Apps that mount the runtime keep declaring it in their own `package.json`, which is and remains the supported wiring.

‎content/docs/plugins/packages.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern
328328

329329
### @objectstack/organizations (enterprise)
330330

331-
**Organization Scoping** — Multi-org (a.k.a. "soft" multi-tenant) row-level scoping. Ships as a **separate, closed-source enterprise package** — it is not part of the open framework repo. It composes with the Layer 0 tenant wall in `plugin-security` (`tenant-layer.ts`).
331+
**Organization Scoping** — Multi-org (a.k.a. "soft" multi-tenant) row-level scoping. Ships from this repo as the **open-core, Apache-2.0 package** `packages/plugins/organizations`, published on npm — ADR-0132 brought it back to open core, so it is no longer closed-source or absent from this repository. ⛔ Only a **host app** may declare it: ADR-0132's entitlement boundary forbids any framework package taking `@objectstack/organizations` as a dependency, because the commercial repository ships a licence-gated subclass under the same package name and a framework-level dependency would put the ungated class inside the tree a commercial app links against (`packages/plugins/organizations/src/no-framework-dependents.pin.test.ts` is the mechanical half). It composes with the Layer 0 tenant wall in `plugin-security` (`tenant-layer.ts`).
332332

333333
- **Features**: `organization_id` auto-stamp on insert; every query is AND-composed against the tenant wall, so no row outside the caller's organization scope is ever returned
334334
- **When to use**: Multi-organization SaaS where every row is scoped to an organization. Enable by setting `OS_TENANCY_POSTURE` to a walled posture — `group` (union read across every organization the caller belongs to) or `isolated` (the hard per-organization wall) — and installing `@objectstack/organizations`; if a walled posture is requested but the package is missing, the platform refuses to boot (override with `OS_ALLOW_DEGRADED_TENANCY=1`). The legacy `OS_MULTI_ORG_ENABLED` boolean is still honoured, but only as a fallback **input** when `OS_TENANCY_POSTURE` is unset, and it can only ever select `isolated` — never gate application code on it (ADR-0105 D1). See [Tenancy Postures & Membership](/docs/deployment/tenancy-modes) for how the posture resolves

‎docs/qa/platform-checklist/areas/attachments-storage.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
"why": "qa_vault (private + files) is the invisible parent both 403 clauses need, qa_shared is the entitled contrast, and the two personas are the non-admin principals the deny side must be checked as. Without the recipe both deny clauses are blocked(fixture) on stock seeds."
187187
},
188188
"knownGaps": [
189-
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is cloud-private, so the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
189+
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is open core since ADR-0132 (Apache-2.0, published on npm) — ⛔ it being closed-source is NOT the reason and has not been since #16215. The reason is ADR-0132's entitlement boundary: no framework package may declare `@objectstack/organizations` (pinned by `packages/plugins/organizations/src/no-framework-dependents.pin.test.ts` — apps declare it, packages do not), so `packages/qa/dogfood` cannot depend on it and the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
190190
"CLOSED by the qa-scratch-authz recipe (#7670): stock showcase's only files-enabled object (showcase_project) is public_read_write, so no stock parent is invisible to any member. The 403 deny side is otherwise reachable only through the pinned dogfood fixture (att_secret, private owner-scoped: fixtures/attachments-fixture.ts). Run the recipe; fall back to the dogfood pin only if it cannot be provisioned, and record which one the verdict rests on.",
191191
"[API half CLOSED by #9483] no stock seed mints an acl='public_read' attachments file (the anonymous-embed opt-out) — author one via a system write on a qa_vault-attached file (the recipe gives you the parent; the acl flip is still a system write) or accept the unit-test coverage in storage-routes.test.ts for that variant. What changed: attachments-public-read-acl.dogfood.test.ts performs that system flip at runtime on its own private-parent fixture and asserts both sides plus the flip back, so clause 3 no longer waits on a stock seed. Landing one in the showcase seeds would additionally unblock the BROWSER half (clause 4), which a runtime-only file cannot reach."
192192
]
@@ -274,7 +274,7 @@
274274
"why": "the whole item turns on a parent one persona cannot read; qa_vault (private + files) is that parent, and persona B is the restricted member. On stock seeds there is no such pair."
275275
},
276276
"knownGaps": [
277-
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is cloud-private, so the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
277+
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is open core since ADR-0132 (Apache-2.0, published on npm) — ⛔ it being closed-source is NOT the reason and has not been since #16215. The reason is ADR-0132's entitlement boundary: no framework package may declare `@objectstack/organizations` (pinned by `packages/plugins/organizations/src/no-framework-dependents.pin.test.ts` — apps declare it, packages do not), so `packages/qa/dogfood` cannot depend on it and the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
278278
"CLOSED by the qa-scratch-authz recipe (#7670): stock showcase's files-enabled parent (showcase_project) is public_read_write — every member sees every project, so the invisible-parent case is not demonstrable on stock seeds, and the pinned dogfood matrix proves it only on its own private fixture (att_secret). The recipe provisions the private files-enabled parent at runtime; landing one in the showcase seeds proper would retire this gap entirely, and remains the better long-term fix.",
279279
"the READ_SCAN_LIMIT fail-closed branch (clause 4) needs a broad read past the 2000-candidate cap — run #7635 reached it by bulking to 2100 join rows on the scratch parent. Budget for that write volume, or the clause degrades to 'the warning never appeared', which is indistinguishable from a silent leak."
280280
]
@@ -355,7 +355,7 @@
355355
"why": "persona A is provisioned read-but-NOT-edit precisely for the attach gate (the whole point of clause 0: read inherits parent READ, attach requires parent EDIT — two different gates), and qa_nofiles is the purpose-built FILES_DISABLED probe target for the last clause."
356356
},
357357
"knownGaps": [
358-
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is cloud-private, so the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
358+
"MARKER (QA run #9401 finding 5, carried here so runs stop re-deriving it): the CROSS-TENANT clauses of this area are structurally unprovable in the OSS workspace. `@objectstack/organizations` is open core since ADR-0132 (Apache-2.0, published on npm) — ⛔ it being closed-source is NOT the reason and has not been since #16215. The reason is ADR-0132's entitlement boundary: no framework package may declare `@objectstack/organizations` (pinned by `packages/plugins/organizations/src/no-framework-dependents.pin.test.ts` — apps declare it, packages do not), so `packages/qa/dogfood` cannot depend on it and the dogfood matrix's cross-tenant block is gated behind `describe.skipIf(!organizationsAvailable)` and skips BY DESIGN here. That is blocked(dependency), never a pass and never a defect. A run that is supposed to ship the package sets OS_TEST_MULTI_ORG_ENABLED=1, which turns the skip into a hard failure instead of a silent one.",
359359
"clause 3 ('unscoped multi-delete refused outright', #4757) IS PINNED as of 2026-08-23. The product gap this entry used to record (#9719) was fixed by PR #9797 and extended to the update verb by #9974: the engine now offers an opt-in whole-operation dispatch (`dispatchUnscopedMultiWrite`) that fires ONCE, before any row is resolved and zero-match included, and attachment-access-hooks.ts declares it on BOTH sys_attachment write registrations — so the refusal answers with ATTACHMENT_DELETE_DENIED / 403 / the 'Refusing an unscoped multi-delete of attachments' message. ⭐ The durable lesson, which outlives the fix: a fixture whose rows SPLIT entitled/not cannot tell 'refused outright' from 'the per-row gate refused one of the rows' — both answer 403 ATTACHMENT_DELETE_DENIED, so the clause reads green either way. Measured on this suite while re-verifying: with the declaration removed from both registrations and service-storage rebuilt, the original split-fixture block stayed 5/5 GREEN. Score clause 3 ONLY from a caller entitled to EVERY matched row (or from the message text) — never from a split fixture. The REST lane still cannot express the shape at all (DeleteManyDataRequestSchema strips options.where and requires ids, #3897), so the reachable callers are server-side engine holders — flows, actions, scripts, MCP tools — which is why this clause's oracle is `test` rather than `api`.",
360360
"the FILES_DISABLED create-side clause (acceptance clause 4) still probes showcase_account, which is files-disabled only incidentally — if the showcase ever enables files on it, that clause goes green for the wrong reason. The recipe's qa_nofiles exists to be probed instead; prefer it there too, and treat showcase_account as the fallback. (The new UPDATE-side clause 5 below already uses qa_nofiles, since it needed a real parent_id to re-point onto.)"
361361
]

‎packages/cli/test/serve-organizations-host-resolution.e2e.test.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
*
77
* The defect: the organizations load used a bare `import()`, which Node ESM
88
* resolves against the importer's own realpath — the CLI's, inside the
9-
* framework workspace. `@objectstack/organizations` is cloud-private and only
10-
* ever lives in the served app's `node_modules`, so the import could never
9+
* framework workspace. `@objectstack/organizations` is host-supplied — ADR-0132's
10+
* entitlement boundary forbids any framework package declaring it — so it only
11+
* ever lives in the served app's `node_modules` and the import could never
1112
* succeed: EVERY self-hosted deployment with `OS_TENANCY_POSTURE=group` or
1213
* `isolated` hit the ADR-0093 D5 fail-fast and exited 1, and the only way past
1314
* it was `OS_ALLOW_DEGRADED_TENANCY=1` — i.e. the unwalled state D5 exists to
@@ -21,9 +22,10 @@
2122
* against a real app directory, with a real package in a real `node_modules`
2223
* and nothing mocked, exercises it.
2324
*
24-
* The fixture stands in for the enterprise package (it is not installable in
25-
* this workspace — that is the whole point), registering the same `org-scoping`
26-
* service and posture entitlement the real one does. What is under test here is
25+
* The fixture stands in for the enterprise package (`packages/cli` may not
26+
* declare it — ADR-0132's entitlement boundary — so it does not resolve here;
27+
* that is the whole point), registering the same `org-scoping` service and
28+
* posture entitlement the real one does. What is under test here is
2729
* RESOLUTION, not the enterprise semantics: proof that the real plugin walls
2830
* tenants lives in cloud's security-enterprise multi-org integration test.
2931
*/

‎packages/plugins/plugin-dev/src/dev-plugin-tenancy-failfast.test.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,18 @@
1919
// fail-open path — which is what this file now forbids.
2020
//
2121
// ── What is observed, and why it is honest ──────────────────────────────────
22-
// `@objectstack/organizations` is a cloud-private enterprise package genuinely
23-
// absent from this workspace, so the dynamic import genuinely fails and the
24-
// real stage-1 catch runs — no stubbing of the thing under test. That makes
22+
// `@objectstack/organizations` is open core since ADR-0132 and IS a member of
23+
// this workspace — ⛔ "absent from this workspace" is no longer the reason. The
24+
// reason is ADR-0132's entitlement boundary: no framework package may declare it
25+
// (`no-framework-dependents.pin.test.ts`), so it is genuinely unresolvable from
26+
// `plugin-dev` and the dynamic import genuinely fails and the real stage-1 catch
27+
// runs — no stubbing of the thing under test. ⚠️ Measured on this file's own
28+
// runner: a bare ESM `import()` from here answers ERR_MODULE_NOT_FOUND "Cannot
29+
// find package" with `packages/plugins/organizations/dist` BUILT and unbuilt
30+
// alike, because Node's ESM resolver does not consult `NODE_PATH` and so never
31+
// reaches pnpm's hoisted store — i.e. this signal is NOT a function of build
32+
// state, unlike the `require`-shaped probe #16539 had to move off a workspace
33+
// name. That makes
2534
// this file the faithful witness for the ABSENT-package half of #4818's split.
2635
// The PRESENT-but-refusing half needs the package to resolve, so it lives in
2736
// `dev-plugin-tenancy-mount-refusal.test.ts`, which mocks it.
@@ -33,8 +42,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3342
// timeout under a parallel `pnpm test`. Each factory throws the shape an absent
3443
// package produces, so the graceful-degradation branches run for real with zero
3544
// module resolution on the hot path. `@objectstack/organizations` is
36-
// deliberately NOT listed: it is really absent, and its real failure is the
37-
// signal this file reads.
45+
// deliberately NOT listed: it really does fail to resolve from here, and that
46+
// real failure is the signal this file reads.
3847
vi.mock('@objectstack/objectql', () => { throw Object.assign(new Error("Cannot find package '@objectstack/objectql'"), { code: 'ERR_MODULE_NOT_FOUND' }); });
3948
vi.mock('@objectstack/runtime', () => { throw Object.assign(new Error("Cannot find package '@objectstack/runtime'"), { code: 'ERR_MODULE_NOT_FOUND' }); });
4049
vi.mock('@objectstack/driver-memory', () => { throw Object.assign(new Error("Cannot find package '@objectstack/driver-memory'"), { code: 'ERR_MODULE_NOT_FOUND' }); });

‎packages/plugins/plugin-dev/src/dev-plugin-tenancy-mount-refusal.test.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@
1919
// nothing about the plugin's internals.
2020
//
2121
// ── Why this file mocks what its sibling refuses to mock ────────────────────
22-
// `dev-plugin-tenancy-failfast.test.ts` reads the ABSENT-package path off the
23-
// genuinely-absent cloud-private package, stubbing nothing. That is impossible
22+
// `dev-plugin-tenancy-failfast.test.ts` reads the ABSENT-package path off a
23+
// package that genuinely does not resolve from a framework package — ADR-0132's
24+
// entitlement boundary forbids any of them declaring `@objectstack/organizations`
25+
// (`no-framework-dependents.pin.test.ts`), and the package being open core since
26+
// ADR-0132 changes nothing about that — stubbing nothing. That is impossible
2427
// here by construction: "the package is present and refused" cannot be observed
25-
// without a present package. The mock therefore supplies only the thing the
26-
// open-source workspace cannot have — a resolvable `@objectstack/organizations`
28+
// without a present package. The mock therefore supplies only the thing no
29+
// framework package may declare — a resolvable `@objectstack/organizations`
2730
// — and the refusal semantics under test stay entirely in dev-plugin.ts.
2831

2932
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

‎packages/plugins/plugin-dev/src/dev-plugin-tenancy-posture.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
// service "is the wall up?" would be circular.
2121
//
2222
// ── What is observed, and why it is honest ──────────────────────────────────
23-
// `@objectstack/organizations` is a cloud-private enterprise package that is
24-
// genuinely absent from this workspace, so the dynamic import genuinely fails
23+
// `@objectstack/organizations` is open core since ADR-0132 and IS a member of
24+
// this workspace, but ADR-0132's entitlement boundary forbids any framework
25+
// package declaring it (`no-framework-dependents.pin.test.ts`), so it is
26+
// genuinely unresolvable from `plugin-dev`: the dynamic import genuinely fails
2527
// and the real catch branch runs. That makes the emitted warning a faithful
2628
// witness for "the multi-org branch was ENTERED": under the bug there is no
2729
// warning at all, because the `if` was never taken. The assertions therefore

‎packages/qa/dogfood/test/automation-toggle-tenant-scope.dogfood.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
*
3939
* `multiTenant: 'posture-only'` activates the tenancy POSTURE and no row wall
4040
* (see `BootOptions.multiTenant`) — the enterprise `@objectstack/organizations`
41-
* runtime is cloud-private and genuinely absent from this workspace. An
41+
* runtime is open core since ADR-0132, but ADR-0132's entitlement boundary
42+
* forbids any framework package declaring it, so it is genuinely absent from
43+
* this package's dependency graph and unresolvable from here. An
4244
* organization wall scopes ROWS, and the enabled bit is not a row:
4345
* `toggleFlow(name, enabled)` writes the automation engine's in-process
4446
* `flowEnabled` map, keyed by flow name and nothing else, `getFlowRuntimeStates()`

0 commit comments

Comments
 (0)