Repository navigation
Bring single-database multi-organization isolation back to the open core — the wall is already open, only the org-scoping switch is closed (~1350 of 1660 lines move; the licence gate stays) #16130
Description
Chartered by the maintainer, 2026-09-06, live chat, verbatim: 「感觉 单库多组织隔离是开源基本需求,如果迁移回开源项目成本有多大」 then 「直接立专题卡派发处理吧」. ⇒ The cost question is answered below by measurement; this card is the work.
The finding that makes this cheap
The wall is already open. What is closed is the switch.
Layer 0 (plugin-security/src/tenant-layer.ts, computeTenantLayer0Filter), the three postures in packages/spec, the API-key and session membership guards, ADR-0105's group posture — all open source. Three cross-organization defects were found and fixed in the open tree this week (#15256, #15409, cloud#1987's framework half). What an open-source deployment cannot do is turn the wall on, because the org-scoping service has exactly one registrar anywhere: the cloud-private @objectstack/organizations.
⭐ And it went the other way once, deliberately: organizations-plugin.ts's own docblock says it was "migrated verbatim from the open plugin-org-scoping" (ADR-0081 D2). The open core still consumes it unchanged — getService('org-scoping') and requiresService: 'org-scoping' nav gates — and the service name was kept on purpose so those anchors would not break. ⇒ No consumer-side change is needed.
Measured on cloud origin/main, 2026-09-06
8 non-test source files, 1660 lines, 8 test files. The licence headers already draw the boundary:
| file | lines | header |
|---|---|---|
organizations-plugin.ts |
508 | proprietary |
membership-policy-gate.ts |
416 | proprietary |
multi-org-license-gate.ts |
306 | proprietary |
index.ts |
107 | proprietary |
manifest.ts |
28 | proprietary |
claim-orphan-org-rows.ts |
135 | Apache-2.0 |
claim-org-seed-ownership.ts |
116 | Apache-2.0 |
ensure-default-organization.ts |
44 | Apache-2.0 |
Coupling to cloud is exactly two points, both entitlement: multi-org-license-gate.ts imports security-enterprise's license/entitlement.ts; index.ts:41 carries a bare import '@objectstack/security-enterprise' to mount the multi-node authorization gate as a side effect (cloud#1827). Everything else imports framework packages only — 4 of the package's 5 dependencies are link: into this repo.
⇒ ~1350 lines move; 306 stay.
ADR-0081 already split this once: D1's open member-management basics landed here (plugin-auth's 415-line ensureDefaultOrganization), leaving cloud a 44-line multi-org wrapper.
⛔ Hard boundaries — these do NOT move
multi-org-license-gate.tsstays in cloud. It is the entitlement, and it exists because the gate must be answered by the package that implements multi-org, at construction — cloud#1020 records the previous shape (an env rewrite in a host config) failing exactly when ADR-0105 D1 made the posture authoritative. ⛔ Do not move it, do not reimplement it here, and ⛔ do not leave a hook that a host could use to re-add one.- The multi-node gate carrier stays.
MULTI_NODE_GATE_CARRIER_PACKAGES(fix(service-cluster,cli): multi-node gate fails closed when unregistered, and mounts on every boot route #14114) names both packages precisely so a deployment declares whichever it needs. Whatever remains in cloud must keep carrying it; the open package must not acquire it. - ⛔ The open package must not gain a licence check of any kind. If enforcement is wanted, it is cloud's, on cloud's side of the split.
⚠️ This reverses a recorded decision — the record moves with the code
ADR-0081 D2 put the multi-org machinery in enterprise on purpose. Landing this without amending it leaves the tree contradicting a live ADR. The open mirror of that record is #14508 (pm:queue, unstarted). ⇒ The ADR amendment is part of this work, not a follow-up: docs/adr/** is a governed surface — draft PR, human merge.
Phase 1 — classify before moving. ⛔ Do not open a code PR first.
Read all 1660 lines and produce, on this card, a line-level disposition for every one: move as-is / move with changes (say which) / stays in cloud / deleted. Two files decide whether the estimate above survives:
organizations-plugin.ts(508) — what it registers beyond the service, and what of that is cloud-shaped.membership-policy-gate.ts(416) — a boot-time membership-policy assertion, and the one file whose depth this seat did not measure.⚠️ If it encodes enterprise-only assumptions, the estimate is wrong and this card's shape changes.
⛔ Stop and report rather than proceeding if: the classification finds a third coupling point; membership-policy-gate.ts cannot be separated from entitlement; or the open package would need any part of security-enterprise.
Phase 2 — the move, once phase 1 is accepted
New open package (name it after the classification — plugin-org-scoping is the historical one and the anchors expect the service name org-scoping, not a package name). Cloud keeps the gate and imports the open package. Both sides' tests move with their code. ⛔ The service name org-scoping does not change — the open core's nav gates and getService calls are anchored on it.
Acceptance: an open-source-only install with OS_TENANCY_POSTURE=isolated boots with the wall ACTIVE and enforces it — the same matrix the three cross-organization repairs pinned (an ex-member's API key and session are refused; a member reads only its own organization).
Refs: cloud packages/organizations · cloud ADR-0081 D1/D2 · #14508 (the open mirror) · cloud#1020 (why the gate lives where it lives) · #14114 / cloud#1827 (multi-node carrier) · ADR-0105 D1 · #15256 / #15409 (the wall's open-source repairs).
Activity
hotlong commentedon Sep 6, 2026
Claim: PM loop — maintainer direct-dispatch channel (2026-08-10 standing authorization).⚠️ phase 2 also touches ⚠️ its writer and this card's phase 2 must not both write that record; whoever moves second adopts the other's text.
Provenance: maintainer, 2026-09-06, live chat with this session — 「感觉 单库多组织隔离是开源基本需求,如果迁移回开源项目成本有多大」 then 「直接立专题卡派发处理吧」.
Session:
6679d191-11f4-465b-b322-0e0409d76793Branch: none for phase 1 — the deliverable is a line-level classification posted on this card. ⛔ No code PR opens until that classification is accepted.
Worktree: sibling level for the cloud side (
/Users/zhuangjianguo/Documents/GitHub/cloud-issue-16130), ⛔ never under.claude/worktrees— cloud resolves dependencies by relative path.Domain:
domain:servicesprovisional (the code lands as a plugin);docs/adr/**, a governed surface — draft PR, human merge, and it is part of this work rather than a follow-up.File surface, phase 1: read-only over
cloud/packages/organizations/**and the open consumers. ⛔ Nothing is edited in phase 1.Container & model: L,
mode:subagent,model: opus— a two-repo move across a commercial boundary, where the expensive mistake is moving one line too many.Clause-②: yes, expected at phase 2 — a new open package registering a service the open core already consumes widens the published surface. Re-judged from the diff at review.
Serial constraints cleared: no open PR touches
cloud/packages/organizations(REST search 2026-09-06); #14508 (the ADR-0081 open mirror) ispm:queue, unstarted and unassigned —hotlong commentedon Sep 6, 2026
os-dev-report
Phase 1 — line-level disposition of all 1660 lines. No code moved, no branch, no PR.
Read against⚠️ The local cloud checkout at
origin/mainin both repos (clouddaaac081, objectstack932acc3d)./Users/zhuangjianguo/Documents/GitHub/cloudis 75 commits behindorigin/main; every line number and quote below is read throughgit show origin/main:PATH, never the working tree.Verdict: the estimate survives. ~1350 → 1300 move, 342 stay, 18 deleted.
None of the four stop conditions fired. No third coupling point,
membership-policy-gate.tsis cleanly separable from entitlement, the open package needs nothing fromsecurity-enterprise, and the movable size is within 3.7% of the card's figure.Which measurements held
import/fromacross all 8 files finds@objectstack/security-enterpriseat exactlymulti-org-license-gate.ts:51-56andindex.ts:41.link:into the frameworkcore,plugin-auth,spec,typesarelink:; onlysecurity-enterpriseisworkspace:*organizations-plugin.tsmigrated verbatim from openplugin-org-scoping; open core consumes it unchangedgetService('org-scoping')atplugin-auth/src/auth-plugin.ts:589,601,plugin-security/src/security-plugin.ts:1250,deployment-org-scoping-entitlement.ts, andrequiresService: 'org-scoping'atplatform-objects/src/apps/setup-nav.contributions.ts:86,account.app.ts:144,dashboards/system_overview.dashboard.ts:69index.ts's two cloud blocks, which a whole-file count could not seeThe line-level disposition
organizations-plugin.tsmembership-policy-gate.tsmulti-org-license-gate.tsindex.tsmanifest.tsclaim-orphan-org-rows.tsclaim-org-seed-ownership.tsensure-default-organization.tsMoving = 1265 + 35 = 1300.
The three Apache-2.0 files — 295 lines, move as-is, zero edits
claim-orphan-org-rows.ts(135),claim-org-seed-ownership.ts(116),ensure-default-organization.ts(44). Their only non-local imports aretype ServiceObjectfrom@objectstack/spec/dataandensureDefaultOrganization/EnsureDefaultOrganizationResultfrom@objectstack/plugin-auth— both verified present on objectstackorigin/main(packages/spec/src/data/object.zod.ts:2870,packages/plugins/plugin-auth/src/ensure-default-organization.ts:276). The headers already say Apache-2.0. Nothing in these three needs touching.organizations-plugin.ts(508) — 480 as-is, 10 changed, 18 deletedWhat it registers, beyond the service. Answering the card's question directly:
init()registers exactly two things —ctx.registerService('org-scoping', this)(line 183) and onemanifestregistration of an empty object list (organizationsObjects = [], lines 185-190).start()adds two ObjectQL middlewares (insert auto-stamp at 257-291; per-org seed pipeline at 294-406), onekernel:bootstrappedhook (the membership gate, 225-232), onekernel:readyhook plus a third middleware for the default-org bootstrap (409-450).How much of that is cloud-shaped: none of it. The complete set of services this file touches is
org-scoping,manifest,objectql,metadata,seed-datasets,seed-replayer— every one an open framework service. A sweep forsys_environment,kernel-manager,control-plane,environment-registry, Cloudflare/D1/turso,service-cloudandobjectosacross all 8 files returns only comment prose citing cloud files for provenance — zero code references. The only env vars named anywhere in the moving files areOS_TENANCY_POSTURE,OS_AUTH_MEMBERSHIP_POLICYandOS_ALLOW_DEGRADED_TENANCY, all open-framework knobs.OS_LICENSE_KEYappears only inmulti-org-license-gate.ts.Deleted — 18 lines, the entitlement excision:
import { assertMultiOrgEntitled } from './multi-org-license-gate.js';assertMultiOrgEntitled();call, the first statement of the constructor. The constructor itself survives (152, 170-173) to setthis.opts.Move with changes — 10 lines, all prose, two of them boundary-critical:
plugin-org-scoping, which the framework no longer ships)". The parenthetical inverts on this move; it becomes the record of the round trip.supportedPosturesin the closed package, and moving the file contradicts it in place. The declaration itself (line 145,['group', 'isolated']) moves as-is and is correct for an open install — an open deployment is entitled to both walled postures. Only the justification changes, and it should say so explicitly rather than be quietly deleted.groupas a higher tier thanisolated, gating it behind a licence flag, running a trial — becomes a one-line edit HERE." Left verbatim in an open package this reads as an invitation to add exactly the licence check hard boundary 3 forbids. Must be reworded, not merely carried over.Deliberately kept verbatim:
name = 'com.objectstack.organizations'— anchored by cloudservice-cloud/src/control-plane-preset.ts:691(lazyPlugin('com.objectstack.organizations', ...)) and by open fixtures atpackages/types/src/node.test.ts:176,552,packages/verify/src/harness.host-resolution.test.ts:44,packages/qa/dogfood/test/enterprise-organizations.test.ts:98. The plugin id is a second anchor alongside the service name; changing it is a separate decision with its own blast radius.org-scopingON PURPOSE" comment and the registration. Boundary 4, satisfied by doing nothing.membership-policy-gate.ts(416) — the file whose depth was unmeasuredIt separates from entitlement completely. Its only two imports are
isMembershipPolicy, MEMBERSHIP_POLICIESfrom@objectstack/plugin-auth(verified atreconcile-membership.ts:63,68) andresolveTenancyPosturefrom@objectstack/types(verified attypes/src/env.ts:145). Zero references tosecurity-enterprise, tomulti-org-license-gate.ts, or to any entitlement symbol. The header states the independence as a designed non-feature, lines 61-64: no env escape hatch, "and neither does a licence". It adjudicates a declaration, not an authorization — it reads the settings service's ownsourcefield forauth.membership_policyand refusesdefault. Its services areauthandsettings, both open.No enterprise-only assumption survives inspection. What looks enterprise-only is the refuse-rather-than-warn rationale at lines 29-32 — the 2026-08-04 ruling that "EE has NO deployed customers — zero breakage surface". That argument does not transfer to open, but its conclusion still holds for a different and verifiable reason: today no open install can reach a walled posture at all.
serve.ts:3700computesmultiTenant = tenancyPosture !== 'single', so bothgroupandisolatedrequire the cloud package; absent it,serve.ts:3747prints the ADR-0093 D5 fatal andprocess.exit(1), and the only way past isOS_ALLOW_DEGRADED_TENANCY=1, which leavesorgModundefined so this gate never runs either. The breakage surface at the moment of the move is genuinely zero — it just needs to be argued from that fact instead of from EE's customer count.Move with changes — 9 prose lines: line 1 (header), line 8 ("an EE deployment" → "a deployment"), lines 29-32 (re-argue zero-breakage from the boot-refusal fact above), lines 55 and 111 (citations of cloud-private paths
service-cloudandpackages/objectos-runtime/src/artifact-kernel-factory.ts, dangling for an open reader), and line 337.Line 337 deserves its own note. The fatal message says, at 336-337:
Dropping
, licensed,from line 337 is the whole edit. It is test-safe: the pin atmembership-policy-gate.test.ts:368matches/NOT a licensing failure/, which lives on line 336 and is untouched. Keeping the sentence would not breach boundary 3 — prose is not a check — but it would be false on an open-only install.The other 407 lines move as-is, including the whole 100-line message builder whose text is pinned by 18 assertions in the suite.
index.ts(107) — the file that genuinely splits three waysMULTI_NODE_GATE_CARRIER_PACKAGEScomment and the bareimport '@objectstack/security-enterprise';. Boundary 2 — the open package must not acquire this. Verified againstservice-cluster/src/multi-node-gate-mount.ts:70-73, which still names both packages.multi-org-license-gate.jssymbols. Consumed byapps/objectos-ee/objectstack.config.ts:98(assertWalledPostureEntitled,isMultiOrgLicenseError), both EE showcase configs (grantDevelopmentMultiOrgEntitlement), andservice-cloud/src/control-plane-organizations.ts:40(grantFirstPartyMultiOrgEntitlement). Cloud's package must keep exporting these under the same specifier.OS_MULTI_ORG_ENABLED=true" — false for the open package, whichservewill reach differently, see phase 2 below); line 45 (the transitional-alias comment, whose migration direction inverts — theOrgScopingPluginalias itself at 46-47 is worth keeping and moves as-is).manifest.ts(28) — 24 as-is, 4 changedChanged: line 1 (header), line 4 (package name in the docblock), line 23 (
name: 'Organizations (Enterprise)'), line 25 (description opens "Enterprise multi-organization runtime").Two lines that look cloud-shaped and are not, both move as-is:
defaultDatasource: 'cloud'— this is the open framework's own convention forscope: 'system'packages, documented at objectstackpackages/spec/src/kernel/manifest.zod.ts:315("Packages with this scope should also setdefaultDatasource: 'cloud'") and used by openpackages/metadata/src/plugin.ts:392. It names a datasource, not this repo.ORGANIZATIONS_PLUGIN_ID— see the plugin-id anchors above.multi-org-license-gate.ts(306) — stays in cloud, in fullAll 306 lines. Boundary 1, no line-level analysis needed beyond confirming nothing else in the package depends on it except the two seams already named.
"Coupling to cloud is exactly two" is true and I verified it mechanically — but it counts cross-package imports, and the move has a third touch point that lives inside the package:
organizations-plugin.tsimportsassertMultiOrgEntitled(line 8) and calls it as the first statement of its constructor (line 168). The 508-line file that moves is wired to the 306-line file that stays.This is not a stop condition — it is not a dependency on
security-enterprise, and it is exactly where the split is supposed to happen. But it has one consequence the card should carry into phase 2:organizations-plugin.tscannot move as-is, and cloud must re-establish a construction-time refusal without the open package offering a hook for it (boundary 1's "do not leave a hook a host could use to re-add one" rules out any injectable-assertion parameter).The shape that satisfies this without any open-side seam: cloud's package keeps
multi-org-license-gate.tsand exports its ownOrganizationsPluginthat extends the open class and callsassertMultiOrgEntitled()in its own constructor beforesuper()-side work. Cloud-side code calling a cloud-side gate; the open package gains nothing and offers nothing. It also preserves the two existing consumers byte-for-byte:serve.ts:3782'snew orgMod.OrganizationsPlugin()and its two-stage classifier, which deliberately keys on which stage threw rather than the error's shape, andartifact-kernel-factory.ts:862-866'snew organizations.OrganizationsPlugin()plusisMultiOrgLicenseError. Both keep working untouched.Phase 2 on the open side — what an open-only
isolatedboot would take⛔ Not built. Scope only. The card budgets the move; it does not budget this, and this is the larger half.
packages/cli/src/utils/tenancy-posture-hints.ts:117—ORGANIZATIONS_RUNTIME_PKGis one hard-coded spelling, and this file's own header records a maintainer ruling (2026-08-27, The two ⛔ notes forbidding a single-sourcedORGANIZATIONS_RUNTIME_PKGname a constraint that no longer exists — decide whether to consume the resolver hop or restate why not #12579) that the spelling is single-sourced here, may not be read from the spec roster, and may not be imported from theservecommand. Teachingservea second, open candidate is therefore a governed edit at a spot with a standing ruling on it.packages/cli/src/commands/serve.ts:3700-3800— the two-stage import/mount. Stage 1 resolves throughimportFromHostbecause the package is cloud-private; an open registrar lives in the framework workspace and resolves differently. And the ADR-0093 D5 semantics change meaning: once an open registrar exists, "the multi-org runtime is absent" can no longer be the normal state forisolated, soformatOrganizationsAbsentFatal, the install remedy text andOS_ALLOW_DEGRADED_TENANCY's scope all need re-deriving.packages/spec/src/kernel/platform-capabilities.ts:237-243— the roster row for@objectstack/organizationsisedition: 'enterprise'and its note saysserveloads it off the tenancy posture. That note becomes wrong. A new open package likely takes no row (the roster is for out-of-repoplugins[]-wired runtimes), but the drift test inserve-capability-vocabulary.test.tsreads both the row andMULTI_NODE_GATE_CARRIER_PACKAGES, whose header describes both carriers as "enterprise runtimes the spec roster declares".serve-organizations-message-spelling.test.ts,doctor-organizations-message-spelling.test.ts,serve-cluster-host-resolution.test.ts(a named-literal sweep that keeps'@objectstack/organizations'in an expected set),serve-capability-vocabulary.test.ts,serve-organizations-host-resolution.e2e.test.ts,serve-organizations-mount-failure.e2e.test.ts,packages/types/src/node.test.ts,packages/verify/src/harness.host-resolution.test.ts.plugin-auth/src/auth-plugin.ts:589,601,plugin-security/src/security-plugin.ts:1250,plugin-security/src/deployment-org-scoping-entitlement.ts, and the threerequiresService: 'org-scoping'nav gates all probe by service name. ℹ️ One thing to carry:OrgScopingEntitlementhas grown two keys beyondsupportedPostures—platformGlobalObjectsandsuppressUnboundedOrgAdminGrant— both fail-closed when absent, so the moving plugin (which declares neither) is unaffected, but the open package now owns that declaration surface.MULTI_NODE_GATE_CARRIER_PACKAGESneeds no edit — both names stay, boundary 2 satisfied by inaction.apps/ee-tenant-crm-showcase/test/isolated-posture.dogfood.test.tsandapps/ee-group-showcase/test/*.dogfood.test.ts, both of which boot the real runtime after callinggrantDevelopmentMultiOrgEntitlement. On the open side the nearest thing ispackages/qa/dogfood/test/enterprise-organizations.test.ts, which tests only the availability probe using stand-in packages on disk — it never raises a wall. A grep for a walledOS_TENANCY_POSTUREacross openexamples/,apps/andpackages/qa/returns one unrelated dogfood file. An open walled-posture acceptance app has to be built from nothing, and until it exists the card's own acceptance criterion cannot be evaluated in the open repo.sys_member), recorded in this repo with Provenance #14508.Tests (not part of the 1660, stated for phase 2 planning)
8 test files, 2081 lines. Every entitlement reference in the five moving-side suites is a test-setup grant that simply deletes on the open side — a one-line
grantDevelopmentMultiOrgEntitlement(...)plus its import, atorganizations-plugin.test.ts:5,11,walled-default-org-self-registrant.pin.test.ts:40,43,org-creation-no-cross-org-copy.test.ts:35,43,membership-policy-gate.test.ts:24-26,32. There is nothing to grant when there is no gate. The single exception ismembership-policy-gate.test.ts:367,expect(isMultiOrgLicenseError(err)).toBe(false)— an assertion discriminating this refusal from the licensing one, which has no counterpart in an open tree and drops.multi-org-license-gate.test.ts(344) andmulti-node-gate-carrier.test.ts(37) stay in cloud with their subjects.Nothing was written
⛔ No file edited in either repo, no branch, no PR. Both repos read through
origin/mainonly; the cloud working tree was never used and never touched.Generated by Claude Code
hotlong commentedon Sep 6, 2026
PM answers the three open questions — A, A, A, with one correction of scope on the third
Phase 1 is accepted. The estimate survived at 1300 move / 342 stay / 18 delete against the card's ~1350, none of the four stop conditions fired, and the classification was taken through
git show origin/main:PATHon both repos rather than a working tree that is 75 commits behind — the discipline that makes the readings usable.Q1 — the constructor's
assertMultiOrgEntitled()→ A: cloud subclassesCloud's package exports its own
OrganizationsPluginextending the open class and calls the gate in its own constructor. It satisfies hard boundary 1 exactly — cloud code, cloud gate, and the open package neither gains nor offers a hook — and it is the only option that leavesserve.ts's two-stage classifier untouched, since that classifier keys on which stage threw rather than on the error's shape. ⛔ B changes thenew orgMod.OrganizationsPlugin()call shape that two call sites depend on for no gain; ⛔ C is the forbidden hook.Q2 — the ADR-0105 D12 paragraph → A: reword, and the amendment carries D12 too
⭐ And the seat's separate observation is upgraded to a requirement: lines 134-136 — "gating it behind a licence flag … becomes a one-line edit HERE" — ⛔ must be reworded regardless of what happens to 128-130. Left verbatim in an open file, that sentence is a written invitation to add the licence check hard boundary 3 forbids, sitting in the exact file where it would go. Leaving it would be worse than the drift it describes.
Q3 — the acceptance matrix → A: its own card, but it is far smaller than estimated
origin/main: the matrix already has an open home —packages/rest/src/single-kernel-isolated-api-key-matrix.test.tsandsingle-kernel-isolated-session-org-claim-matrix.test.ts, the two suites the cross-organization repairs pinned, both driving a liveisolatedposture at REST level.What they do not do is produce that posture. Both raise it with a hand-written stub:
⇒ The open tree already proves the wall behaves correctly given a posture. What nothing open proves is that anything can produce one — which is precisely what this card moves. So the acceptance card is not "build a walled showcase app"; it is "run the existing open matrices against the real open
org-scopingregistrar instead of the stub". Filed separately as decided, and ⛔ it stays a blocking acceptance: phase 2's PR ⛔ must not claim the card's acceptance criterion is met, and must name that card as where it is discharged.⇒ ⛔ C (porting cloud's showcase suites) is not needed for the acceptance and should not be scoped into it.
Dispatching phase 2 on this basis.
hotlong commentedon Sep 6, 2026
The goal, stated by the maintainer — and measured against what is already open
Maintainer, 2026-09-06, live chat, verbatim:
⇒ The bar is not "the
org-scopingservice can be registered". It is an open-source-only install runs a metadata application in single-database multi-tenant mode. Measured onorigin/mainagainst that bar, and the news is good — everything except the switch is already open:plugin-security/src/tenant-layer.ts, the three postures inpackages/specresolveTenancyPosture()inpackages/typesplatform-objects/src/identity/sys-organization.object.ts,sys-invitation.object.tsplugin-auth/src/auth-manager.ts:2795, andcreateOrganizationresolves in openplugin-auth(ADR-0081 D1: the open member-management basics own it)requiresService: 'org-scoping'gates insetup-nav.contributions.ts,account.app.ts,system_overview.dashboard.ts; they appear the moment the service registersorg-scopingregistrar⇒ The maintainer's stated effect is delivered by this card, and by this card alone. ⛔ No second migration is hiding behind it.
⭐ And the sharpest way to say what changes
Today an open-source install that sets
OS_TENANCY_POSTURE=isolatedcannot enforce it. Its only way forward isOS_ALLOW_DEGRADED_TENANCY=1— the wall configured but not enforced, which is the precise shape this session spent the day closing three cross-organization defects against, and the shape ADR-0131 D9 refuses.⇒ The acceptance on #16137 is therefore stated at its sharpest, and phase 2 should be read against it: an open-only install,
OS_TENANCY_POSTURE=isolated, withOS_ALLOW_DEGRADED_TENANCYUNSET, boots with the wall ACTIVE and enforces the matrix. Today that is unsatisfiable — the degraded flag is the only way an open install reaches a walled posture at all, and it reaches it without a wall.feat(organizations): the multi-organization runtime returns to open c…
docs(adr): ADR-0132 — the multi-organization runtime is open core (#1…
fix(organizations): satisfy the open tree's gates the closed package …
fix(organizations): type-source resolution, the WHERE double, and the…
hotlong commentedon Sep 6, 2026
os-dev-report
{ "issue": 16130, "status": "done", "branch": "claude/issue-16130-open-org-scoping", "pr": "https://github.com/objectstack-ai/objectstack/pull/16215", "premise_still_valid": true, "summary": "Phase 2, objectstack half. New Apache-2.0 package packages/plugins/organizations = @objectstack/organizations, carrying the 1300 lines comment 5556477998 classified as moving; the 306-line licence gate stays in cloud, untouched. Q1/A: the open class has no entitlement check, hook, option or protected seam — cloud subclasses and calls its gate in its own constructor, so `new X.OrganizationsPlugin()` and serve's two-stage classifier are unchanged. Q2/A: the ADR-0105 D12 paragraph is reworded to say the open package entitles both walled postures by construction, and the mandatory 'gating it behind a licence flag ... one-line edit HERE' sentence is replaced by a stop-marker saying the opposite in the same place. ADR-0132 is in this PR (new record, Provenance naming cloud ADR-0081) and ADR-0105 D12 is amended in place; #14508 was found unstarted (pm:queue, unassigned, no branch, no PR, last touched 2026-09-04) so I am the first writer, and ADR-0132 plus a comment on that card tell its writer to cite this number rather than open a competing record. The PM's Claim comment says 'Branch: none for phase 1' — that is this same session's phase-1 claim, not a foreign one; this branch is phase 2's and is named in the PR. Coordinator's mid-task rename to @objectstack/organizations was taken and its hazard measured (see tests field). PR is DRAFT and stays draft: docs/adr/** is governed. Final head 6d7d0e4c9; PR draft, auto_merge null, no labels. PLACEMENT: raised in review as possibly belonging at packages/organizations/, refused with evidence, and CLOSED by the maintainer ruling 'A' (recorded on #16130 comment 5559628606) — the package stays at packages/plugins/organizations. The decisive fact was mechanical: ci.yml's ONLY capability-package enumeration is a literal four-directory glob over packages/{triggers,services,drivers,plugins}/*, in the step 'Verify capability packages ship a runtime entry (no dts-only / half-built)', whose comment says such packages are loaded by a DYNAMIC import of their published entry and that a half-built dist makes the capability SILENTLY fail to load — which is this package exactly and is the very state this card exists to end. Supporting: no retirement of packages/plugins is recorded anywhere; AGENTS.md's Monorepo Layout and Context Routing table both still name it; #14062's 2026-09-01 director ruling names the packages/plugins family as live; and the most recent new package in the repo before this one went INTO packages/plugins (2026-07-16, later than the last top-level one). SECOND MERGE ROUND: main moved again and the PR went 'dirty' (a conflicting PR gets no fresh CI, so the green checks on the old head were stale as evidence). origin/main merged in append-only — no rebase, no force-push, no squash — CLEAN with zero conflicts. Final head 11ca403f6, mergeable true, mergeable_state 'blocked' (draft + pending checks), draft true, auto_merge null. main has NEVER touched packages/plugins/organizations, so the classification's premise did not move under this card and there was nothing to stop-and-report.", "tests": "Re-verified ENTIRELY at the new head 11ca403f6 after the second origin/main merge; no number below is carried forward from an earlier head. Gate set derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (machine-readable mode, not the human-readable output). 131 commands, exit codes captured before any pipe (`cmd > file 2>&1; EXIT=$?`), 125 GREEN. The 6 non-green are all NOT MEASURED, none is a finding: check:dual-build-cjs-loads exit 3 'PREREQUISITE NOT MET'; check:i18n exit 3 (needs the CLI extract-config build closure); check:type-check-debt exit 3; @objectstack/spec check:skill-examples exit 1 (packages/client-react/dist holds no .d.ts — the gate refuses rather than compute a false green); check:merge-driver exit 1 — PRE-EXISTING, proven earlier by running check-regen-pending.mjs --self-test in a clean worktree at origin/main and getting the identical failure ('a gate whose RUNNER is not installed'); check:bash32-floor exit 1 — HOST: its probes need a bash that cannot parse coproc / ';;&' / '[[ -v ]]' and this host's /bin/bash is 3.2; zero shell files in the diff. IMPROVEMENT vs the previous head: check:dts-closure was the 7th failure at 6d7d0e4c9 and is GREEN at 11ca403f6 — it had named @objectstack/embedder-openai, and building the dependency closure supplied its declarations, confirming it was a prerequisite of this checkout rather than a finding. Package level, all green at 11ca403f6: dependency closure built first (`pnpm --filter '@objectstack/organizations^...' build`, 87 packages); `pnpm --filter @objectstack/organizations build` -> 'check-dts-emitted: 1/1 declared declaration file(s) present.'; `pnpm --filter @objectstack/organizations run typecheck` -> tsc --noEmit clean AND 'check:test-typecheck: OK — 0 file(s) / 0 error(s)'; `pnpm --filter @objectstack/organizations test` -> 'Test Files 7 passed (7) / Tests 76 passed (76)'. MERGE HANDLING: the merge was clean (zero conflicts). main has never touched packages/plugins/organizations — checked explicitly before merging, since a conflict there would have meant the classification's premise moved and required a stop-and-report. The three files both sides touched were content/docs/permissions/system-context.mdx, packages/runtime/src/dispatcher-error-vocabulary.ts and scripts/engine-double-contract.pinned.json; my content survived in all three (row 62, the WALLED_MEMBERSHIP_POLICY_UNDECLARED boot-refusal row, and the three pinned fakes). packages/spec came in again (10 files incl. authorable-surface.base.json), so per AGENTS.md section 10 it was rebuilt and check:generated re-run -> 'All 15 generated artifacts are up to date'. The merge=os-regen deferral fired again: the driver recorded content/docs/permissions/system-context.mdx in os-regen-pending (per-WORKTREE git dir, not the common one) and .githooks/pre-push refused the push until it was regenerated from the merged tree. Regenerated with `pnpm gen:system-context-census`, kept as its own commit, and the anchor-only claim PROVEN not asserted: 15 anchors rewritten; diffing the page against a pre-regeneration copy and filtering out every anchor-bearing line leaves NOTHING; row 62 byte-identical; the entire declared-counts table byte-identical (106 sites / 20 packages / 45 files; rows 1-62 behaviour, rows 63-66 carry-onward). DECLARED NARROWING (unchanged): scripts/pm/os-verify-lock.sh finds no usable flock on this macOS host, so every heavy command ran through that entry point in DECLARED UNLOCKED MODE — nothing serialized against sibling agents; official wording in the PR body. STILL-STANDING EVIDENCE from earlier heads, unaffected by the merge because main never touched these files: the three Apache-2.0 files and two as-is test files are BYTE-IDENTICAL to cloud origin/main (git hash-object 08eb3641, 3b8d03f6, da45ee8f, 8ad6cbee, 6ab67bbc); every non-local import re-verified to resolve; and the entitlement-bypass measurement — six cloud manifests declare @objectstack/organizations as workspace:* (apps/cloud, apps/ee-group-showcase, apps/ee-tenant-crm-showcase, apps/objectos-ee, packages/objectos-runtime, packages/service-cloud), pnpm's workspace: protocol cannot fall through to the registry, and the only other occurrences of the name in cloud are the package's own `name`, security-enterprise's objectstack.preflightAlsoReads entry and a paths alias in its tsconfig.test.json — none a dependency resolution. SELF-CAUGHT DEVIATION: the regeneration commit 11ca403f6 is missing the Claude-Session / Co-authored-by trailer pair AGENTS.md requires; I noticed only after pushing, and amending would need a force-push, which is forbidden — so it stands, and the other four commits on this branch carry the trailers.", "mcp_calls": "0 — every GitHub read and write went through gh REST/CLI; no MCP GitHub call was made", "open_questions": [], "out_of_scope_findings": [] }chore(docs): regenerate the isSystem census anchors after merging main (
hotlong commentedon Sep 6, 2026
Placement ruled: A — the package stays at
packages/plugins/organizationsMaintainer, 2026-09-06, live chat, verbatim: 「A」 — chosen from the three options the dev carried into this card and PR #16215's "Where the package sits, and why" section. ⇒ No move. The PR's placement is final and this question is closed.
The dev was right to refuse, and the record should say so plainly
I instructed it to move the package to top-level
packages/organizations/. It declined and did not move, invoking the escape clause the dispatch gave it ("if you find a recorded convention that says otherwise, follow the record and tell me"). Every load-bearing claim in its refusal was re-verified by this seat onorigin/main:.github/workflows/ci.yml:1565enumerates capability packages as a literal four-directory glob —packages/triggers/* packages/services/* packages/drivers/* packages/plugins/*— for the step "Verify capability packages ship a runtime entry (no dts-only / half-built)". Its own comment gives the reason: these packages are loaded by the multi-tenant runtime through a dynamic import of their published entry, so a half-builtdistmakes the capability silently fail to load. ⭐ That is this package exactly (servereaches it viaimportFromHost), and a silent load failure is the wall configured but not enforced state this whole card exists to end. Top-levelpackages/*has no equivalent guard.packages/plugins/is recorded anywhere. AGENTS.md line 678 still listsplugins/ # 🧱 Official plugins & drivers, and line 828 still routespackages/plugins/**→ Plugin Developer.packages/plugins/** family"* into thecheck:test-typecheck` instrument family.Two errors of mine, recorded because they are the reusable part
⇒ Kept as a working rule: an instruction that contradicts the tree needs its record cited in the instruction itself. The escape clause is what saved this one, and it only worked because the dev used it instead of complying.
State
PR #16215 stays draft —
docs/adr/**is a governed surface, so the human merge is its review record. Placement is settled; nothing else on the card changes. The cloud half (bump the pin, strip what moved, subclass, keep the gate) dispatches after this lands.chore(docs): regenerate the isSystem census anchors after merging main (
hotlong commentedon Sep 7, 2026
Status — the open half is written and reviewed, but not landed; the cloud half is not dispatched
PR #16215 was armed for auto-merge at 01:39:13Z and sat in the merge queue until 02:23Z without
landing.
Governed Surface Queue Guardrefused it twice: the PR editsdocs/adr/0105and the newdocs/adr/0132, which puts it on the governed register, and the only review on it is from anaccount outside
GOVERNED_APPROVERS. Full evidence and the two exits are recorded on the PR:#16215 (comment)
The entry was dequeued (it was refused on every rebuild and burned a full CI matrix each time it
re-based behind the queue). The PR is intact and back in draft — nothing about the work changed.
Why the cloud half is not dispatched yet
Its two hard preconditions both read
main, and both are still false:git ls-tree origin/main -- packages/plugins/organizations→ empty. The cloud class is tosubclass the open
OrganizationsPlugin; there is nothing to extend until this lands..objectstack-shabump must point at a commit that contains the package. Today'sorigin/mainhead is not such a commit.Dispatching now would hand a dev a premise that is false at read time — the failure mode this
queue keeps re-learning. It goes out the moment the open half is on
main.What is waiting behind this
.objectstack-sha, strip the ~1300 moved lines, subclass the open classand call
assertMultiOrgEntitled()in cloud's own constructor; the 306-line licence gate andthe multi-node gate carrier stay exactly where they are, and ⛔ no licence check enters the open
package.
OS_TENANCY_POSTURE=isolatedandOS_ALLOW_DEGRADED_TENANCYunset boots with the wall ACTIVE. That is the reading that makesthe whole migration true rather than merely moved.
hotlong commentedon Sep 7, 2026
The cloud half now has a card: objectstack-ai/cloud#2027, filed
pm:blockedbehind this one.It carries the measured facts rather than a plan: all six cloud manifests declaring
@objectstack/organizationsuseworkspace:*(re-verified on cloudorigin/main9b85d761 today), so the shared package name cannot let an EE assembly resolve to the ungated open class; cloud's.objectstack-shais 240 commits behind and its bump is a separate PR, not a rider on the strip; and the strip is 3054 lines across 13 files, withmulti-org-license-gate.ts+ tests andmulti-node-gate-carrier.test.tsexplicitly untouched.It flips to
pm:queueand gets dispatched when PR #16215 is onmainand a pin target containingpackages/plugins/organizationsexists.os-warren commentedon Sep 7, 2026
只报一条残留:ADR-0132 已落地,但它自己的 Status 仍写
Proposeddomain:servicesPM 席 · session03324ae2-0f5b-5ad2-8a2e-cf4aaff5a909· 2026-09-07。⛔ 本席不动别的席位的记录,只把测量报过来。实测
origin/main:git log origin/main --oneline | grep -c '(#16215)'= 1(cwd 对照'(#15365)'= 1),提交c677cda81"feat(organizations): bring the multi-organization runtime back to open core … (ADR-0132) (feat(organizations): bring the multi-organization runtime back to open core — the org-scoping registrar ships open, the licence gate stays in cloud (ADR-0132) #16215)"docs/adr/0132-multi-organization-runtime-is-open-core.md在origin/main上packages/plugins/organizations/package.json在origin/main上 ⇒ 包确实进了开源仓而该文件头部仍写着:
⛔ 本席不代改。归属本卡/#16215 的席位。
顺带:下游已经跟上了
sys_member), recorded in this repo with Provenance #14508,cloud ADR-0081 的开源半边镜像 = ADR-0133)本来把 0132 写成「未合并的 draft PR」、把 D2 写成「stay in cloud」,现已更新并按ADR-0132编号 + 相对链接引用。它自己写的触发条件("When feat(organizations): bring the multi-organization runtime back to open core — the org-scoping registrar ships open, the licence gate stays in cloud (ADR-0132) #16215 merges, its number becomes citable and this file should be updated to use it")已经履行。PM 复核记录:docs(adr): ADR-0133 — organization management, the open basics; mirroring cloud ADR-0081 D1/D3/D4 with Provenance #16267 (comment)sys_member), recorded in this repo with Provenance #14508 的合并时决定参考:ADR-0132 携带它自己的 D1–D6,与 cloud ADR-0081 的 D1/D3/D4 是不同的决策;把镜像折进 0132 会让一份文档里出现两个互不相干的D1—— 而不重编号正是镜像可对照的前提。⇒ 「等 0132 合并再决定要不要合成一份」这个理由已经消失,但这一条没有。docs(adr): ADR-0133 — organization management, the open basics; mirroring cloud ADR-0081 D1/D3/D4 with Provenance #16267 把它陈述出来、⛔ 不裁决,留作 merge-time 的选择。Generated by Claude Code
hotlong commentedon Sep 8, 2026
Both halves landed — closing, with the acceptance explicitly NOT claimed
Framework half: PR #16215 merged as
c677cda81.packages/plugins/organizationsis onmain—@objectstack/organizations@17.3.0, Apache-2.0, public, 20 files, registeringorg-scopingitself.Cloud half: objectstack-ai/cloud#2027, PRs cloud#2035 and cloud#2039, both merged. Verified on cloud
origin/main: the package is down from 16 files to 5,organizations-plugin.ts:70isextends OpenOrganizationsPluginwithassertMultiOrgEntitled()in cloud's own constructor, the 306-line licence gate and the multi-node carrier are untouched, and.objectstack-shaisa5eccf925774.⛔ The framework diff for the cloud half was zero files. No licence check, hook, callback or gate seam entered the open package — the open constructor was already public, already took its options object and already registered the service, so the subclass needed no new seam. That was the one rule that could not bend and it did not.
The entitlement boundary rests on a measurement, re-run on the branch before merge: all six cloud manifests declaring⚠️ That is the thing to re-check if any of those six lines ever becomes a version range.
@objectstack/organizationsuseworkspace:*, no non-workspace declaration of the name exists anywhere in that tree, and each consumer's on-disk symlink resolves to cloud's own1.0.0 UNLICENSEDpackage — never the public Apache-2.0 one.What is deliberately not claimed here
The maintainer's stated goal was that an open-source install can run a metadata application in single-database multi-organization mode. This card moved the code that makes that possible; it did not prove it. #16137 is where that is measured — an open-only composition raising the posture through the real registrar instead of the stub every existing matrix uses. Dispatching it now.
Closing this one.
feat(organizations): bring the multi-organization runtime back to ope…
docs(organizations): stop calling the open multi-org runtime enterpri…