test(organizations): prove an open-only install RAISES the wall — the matrix against a resolved posture, no stub (#16137) - #16719
Merged
Conversation
…e through the real open registrar, not a stub (#16137) The two REST matrices (`single-kernel-isolated-api-key-matrix.test.ts`, `single-kernel-isolated-session-org-claim-matrix.test.ts`) drive the whole matrix under a live `isolated` posture, and both MANUFACTURE that posture with `async () => ({ posture: 'isolated' })`. They prove the wall behaves correctly GIVEN a posture and say nothing about whether anything open can PRODUCE one. `open-only-wall-acceptance.test.ts` is the other half. It boots a real `LiteKernel` with the real `AuthPlugin` and the real `OrganizationsPlugin` under `OS_TENANCY_POSTURE=isolated` and `OS_ALLOW_DEGRADED_TENANCY` UNSET, reads the posture back out of the `tenancy` service that boot RESOLVED, and runs the matrix against it through a real `RestServer`. - §0 asserts the deployment shape through the same resolvers `serve.ts` reads. - §1 is the card's ⭐: `org-scoping` resolves and IS the mounted plugin instance; the effective posture is `isolated`, `isolationActive`, NOT degraded. - §2 runs the matrix: ex-member API key refused (401, nothing lands), organization-less caller refused, ex-member session dropped-not-refused (ADR-0123 D2 403 on write) — and, as the control without which the refusals prove nothing, a healthy member still reads and writes over both a key and a session. - §3 measures acceptance clause 3 twice: a recording service registry shows no licence-shaped lookup over boot AND request time, and the composition's transitive workspace closure is Apache-2.0 throughout. Each half carries a positive control. - §4 ablates the mount: `org-scoping` stops resolving, the posture degrades to `single`, and the measured leak returns — so §1 and §2 are measuring the mount rather than the fixture. The acceptance lives in this package because `no-framework-dependents.pin.test.ts` refuses any `packages/**` package declaring `@objectstack/organizations`; the prohibition is asymmetric, so the acceptance comes to the plugin. The stub suites are untouched (acceptance clause 4), which is why the fixture is a deliberate second copy rather than an extraction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
hotlong
marked this pull request as ready for review
September 8, 2026 00:56
hotlong
enabled auto-merge
September 8, 2026 00:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #16137
The open-only wall acceptance for the #16130 migration: an install that mounts
only Apache-2.0 packages raises the organization wall, and the matrix runs
against the posture that composition actually resolves — no stub anywhere.
The gap, and what closes it
packages/rest/src/single-kernel-isolated-api-key-matrix.test.tsandsingle-kernel-isolated-session-org-claim-matrix.test.tsalready drive the wholematrix at REST level under a live
isolatedposture. Both manufacture it:So the tree proved the wall behaves correctly given a posture and proved
nothing about whether anything open can produce one.
packages/plugins/organizations(PR #16215, Apache-2.0) is the registrar that makes the other half measurable for
the first time.
packages/plugins/organizations/src/open-only-wall-acceptance.test.tsboots a realLiteKernelwith the realAuthPluginand the realOrganizationsPluginunderOS_TENANCY_POSTURE=isolatedandOS_ALLOW_DEGRADED_TENANCYunset, reads theposture back out of the
tenancyservice that boot resolved, and drives the matrixthrough a real
RestServerwhose tenancy provider isrest-api-plugin.ts's sync legreading that kernel.
Acceptance, clause by clause
1 — the composition boots with the wall ACTIVE, asserted from the resolved posture.
§0 asserts the deployment shape through the same resolvers
serve.tsreads(
resolveTenancyPosture() === 'isolated',resolveAllowDegradedTenancy() === false) —the degrade flag is never set anywhere in this file, since a wall configured-but-not-enforced
is the state this card exists to make unnecessary. §1 then asserts, off the resolved
tenancyservice:requestedPosture: 'isolated',isolationActive: true,posture: 'isolated',degraded: false. It also assertsgetService('org-scoping')is the mounted plugin instance — identity, not truthiness, so a stand-in that merely
registered the name could not satisfy it.
2 — the matrix, unchanged in substance, including the control. Ex-member API key
refused (401 on both verbs, nothing lands, read back from the store); organization-less
caller refused; ex-member session dropped, not refused (option B, #15409 — a 200
with nothing in it and the ADR-0123 D2 403 on write, and the same person still works in
an organization they really are in); and the control without which the refusals prove
nothing — a healthy member still reads and writes, over both an API key and a session,
with the second organization's rows present in the store and not served. The refusal and
drop log lines are asserted with their reason codes and without the credential.
3 — no licence check is reachable, measured twice, each with a positive control.
(a) At run time: the kernel's service registry is swapped for a recording
Map, so everyservice the composition asks for — over boot and over request time — is on the ledger;
no name is licence-shaped, the ledger is non-empty and contains
org-scoping/tenancy/objectql, and a plantedgetService('license-entitlement')is found by the same detector.(b) Structurally: the composition's transitive workspace closure, read from the manifests,
is Apache-2.0 throughout and declares no licence-shaped dependency; the detector finds a
planted
@objectstack/security-enterprise. §1 adds the third leg — the only entitlementquestion open core asks (
probeEntitledPostures→supportedPostures) terminates in thispackage's own declared constant.
4 — the stub suites stay exactly as they are.
git diff origin/main -- packages/rest/is empty. That is also why the fixture here is a deliberate second copy rather than an
extraction: hoisting theirs into a shared module would have rewritten both, which is the one
thing the clause forbids.
The maintainer's negative control (comment of 2026-09-06) is §4, which is also the
ablation: the identical composition with the single open registrar removed —
org-scopingstops resolving, the posture degrades (requested isolated,effective single,degraded: true), and the measured leak returns.Why the acceptance lives in the organizations package
no-framework-dependents.pin.test.tsrefuses anypackages/**workspace package declaring@objectstack/organizationsin any dependency field — ADR-0132's entitlement boundary — so itcannot live beside the suites it accepts. The prohibition is asymmetric on purpose ("this
package may depend on framework packages freely"), so the acceptance comes to the plugin.
@objectstack/restis added as a devDependency of this package, aliased to source invitest.config.tsand mapped intsconfig.json(check:test-source-alias/check:type-source-resolutionboth green): an acceptance resolved fromdist/would renderits verdict about the last build of the very code that raises the wall.
Verification — all commands run at
7aa6347d2, real outputDeclared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot ship it), so the commands below were run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for these
runs, nor for any sibling agent in this container while they ran.
pnpm --filter @objectstack/organizations exec vitest run --maxWorkers=2 src/open-only-wall-acceptance.test.tspnpm --filter @objectstack/organizations testpnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/single-kernel-isolated-*-matrix.test.tspnpm --filter @objectstack/organizations typechecktsc -p tsconfig.test.json --listFilespackages/rest/src/rest-server.tsare both in the programpnpm lint(eslint . --no-inline-config)node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack→ 58 runnablecheck:dual-build-cjs-loadsandcheck:type-check-debtfirst answeredPREREQUISITE NOT MET(exit 3 — nothing measured, not a failure). Both were re-run afterturbo run build --filter='./packages/*' --filter='./packages/*/*'(72/72 tasks) and aregreen; their own verdict lines are the authority quoted here, not
$?.pnpm check:merge-driveris red, and not from this diff — this PR touches noscripts/or.github/file, and the identical two cases fail on a checkout that does notcarry this branch. Root-caused and filed as #16717: on macOS the runner-missing
classifier misses both its legs (
/bin/shwrites noline N:, andpnpm -s runreportsexit 1 rather than 127), whose field consequence is the false
staleclaim.The ablation — watched, not asserted
The ⭐ assertion has to depend on the mount. Recorded run, mutation confirmed on disk before
the measurement (anchor occurrences 1→0, injected 0→1; blob
947ff8f8…→3b5afbd5…) andrestore confirmed by observation afterwards (
git diff HEADempty, blob back to947ff8f8…).No rebuild leg:
vitest.config.tsaliases every package this suite reaches to source, sonothing in the reading comes from
dist/.With
mountOrgScopingforced tofalse:The 20 that stay green are the narrowness controls: the healthy-member arms still serve, the
anonymous door still refuses, and §4's own ablation assertions still hold. So §1 and §2 are
measuring the mount, not the fixture — and a suite that would have passed on the tree
before PR #16215 is not what landed here.
Release
No package behaviour changes: a new test file, a devDependency, and two test-time configs.
Labelled
skip-changeset.Out of scope, filed
staleclaim #15722 removed #16717 —check:merge-driveris permanently red on macOS; the field direction is thefalse
staleclaim on a genuinely missing runner.@objectstack/organizationsenterprise/closed-source in 65 places — includingtenancy-service.ts, the file that decides the posture #16718 — after ADR-0132 the open tree still calls@objectstack/organizationsenterprise / closed-source in 65 places across 45 files, including
plugin-auth/src/tenancy-service.ts, the file that decides the posture.Neither is touched here. Cloud's half of #16130 (cloud#2027) is in the other repository and
is not measured by this PR.
🤖 Generated with Claude Code
Generated by Claude Code