Skip to content

test(organizations): prove an open-only install RAISES the wall — the matrix against a resolved posture, no stub (#16137) - #16719

Merged
hotlong merged 1 commit into
mainfrom
claude/open-only-wall-acceptance-16137
Sep 8, 2026
Merged

test(organizations): prove an open-only install RAISES the wall — the matrix against a resolved posture, no stub (#16137)#16719
hotlong merged 1 commit into
mainfrom
claude/open-only-wall-acceptance-16137

Conversation

@hotlong

@hotlong hotlong commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

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.ts and
single-kernel-isolated-session-org-claim-matrix.test.ts already drive the whole
matrix at REST level under a live isolated posture. Both manufacture it:

const tenancyServiceProvider = async () => ({ posture: 'isolated' });

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.ts 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 drives the matrix
through a real RestServer whose tenancy provider is rest-api-plugin.ts's sync leg
reading 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.ts reads
(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
tenancy service: requestedPosture: 'isolated', isolationActive: true,
posture: 'isolated', degraded: false. It also asserts getService('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 every
service 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 planted getService('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 entitlement
question open core asks (probeEntitledPosturessupportedPostures) terminates in this
package'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-scoping stops 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.ts refuses any packages/** workspace package declaring
@objectstack/organizations in any dependency field — ADR-0132's entitlement boundary — so it
cannot 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/rest is added as a devDependency of this package, aliased to source in
vitest.config.ts and mapped in tsconfig.json (check:test-source-alias /
check:type-source-resolution both green): an acceptance resolved from dist/ would render
its verdict about the last build of the very code that raises the wall.

Verification — all commands run at 7aa6347d2, real output

Declared narrowing — verification ran UNLOCKED. scripts/pm/os-verify-lock.sh
could not take the shared verify lock on this host: no usable flock. The shared
verify lock is declared Linux-only (flock is util-linux, and a stock macOS does
not 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.

what command result
the new suite pnpm --filter @objectstack/organizations exec vitest run --maxWorkers=2 src/open-only-wall-acceptance.test.ts 32 passed
the whole package pnpm --filter @objectstack/organizations test 8 files, 108 passed
clause 4, behaviourally pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/single-kernel-isolated-*-matrix.test.ts 2 files, 28 passed, untouched
types pnpm --filter @objectstack/organizations typecheck exit 0
types, coverage of the new file tsc -p tsconfig.test.json --listFiles the new test and packages/rest/src/rest-server.ts are both in the program
repo-wide style pnpm lint (eslint . --no-inline-config) exit 0 — the full run, not a narrowing
gate family node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack → 58 runnable 57 green, 1 pre-existing (below)

check:dual-build-cjs-loads and check:type-check-debt first answered
PREREQUISITE NOT MET (exit 3 — nothing measured, not a failure). Both were re-run after
turbo run build --filter='./packages/*' --filter='./packages/*/*' (72/72 tasks) and are
green; their own verdict lines are the authority quoted here, not $?.

pnpm check:merge-driver is red, and not from this diff — this PR touches no
scripts/ or .github/ file, and the identical two cases fail on a checkout that does not
carry this branch. Root-caused and filed as #16717: on macOS the runner-missing
classifier misses both its legs (/bin/sh writes no line N:, and pnpm -s run reports
exit 1 rather than 127), whose field consequence is the false stale claim.

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…) and
restore confirmed by observation afterwards (git diff HEAD empty, blob back to 947ff8f8…).
No rebuild leg: vitest.config.ts aliases every package this suite reaches to source, so
nothing in the reading comes from dist/.

With mountOrgScoping forced to false:

Tests  12 failed | 20 passed (32)
  × ⭐ `org-scoping` resolves, and it IS the open plugin instance that was mounted
  × ⭐ the EFFECTIVE posture reads `isolated` and is NOT degraded — read back off the resolved service
  × the ONLY entitlement question open core asks is answered by an open constant
  × GET is 401, and the wire says nothing else
  × POST is 401 and NOTHING LANDS — read back from the store
  × GET is 401 — not a 200 carrying a silent empty set
  × GET reads NOTHING from the organization it left, and those rows still exist
  × POST does NOT land in that organization — 403 PERMISSION_DENIED, store read back
  …

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

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

…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>
@hotlong hotlong added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 8, 2026
@github-actions github-actions Bot added the size/l label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 2 changed file(s) yielded no anchor (packages/plugins/organizations/tsconfig.json, packages/plugins/organizations/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/plugins/organizations/tsconfig.json, packages/plugins/organizations/vitest.config.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a0856e3bf93b0aa9fe78e6235dcbc043e5b753b3packageMentionDocs.

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file tests tooling labels Sep 8, 2026
@hotlong
hotlong marked this pull request as ready for review September 8, 2026 00:56
@hotlong
hotlong enabled auto-merge September 8, 2026 00:56
@hotlong
hotlong added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 5e53d73 Sep 8, 2026
40 checks passed
@hotlong
hotlong deleted the claude/open-only-wall-acceptance-16137 branch September 8, 2026 02:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prove an open-only install can RAISE the wall — the matrices exist but every one of them stubs the posture

1 participant