Repository navigation
Commit 41d0d40
Fixes #15207
Clause-②: yes (narrowing: on a deployment that declares an object
platform-global, that object carries no organization column; the dev
measures the built declaration closure)
Measured on the built declaration closure. The value `yes` holds:
`@objectstack/core` gains exports, because the one fail-closed reader of
the `org-scoping` keys moved there. `resolveInjectedSystemColumns` gains
an optional second parameter but no new spec export, and
`check:api-surface` on the rebuilt spec reports the surface unchanged.
The arm stays `(narrowing)`, because the behaviour narrows: a declared
object loses its column on the declaring deployment. The changeset
carries `Clause-②: yes (narrowing)`. This is the last open item of the
card (items (1), (2) and (3) landed as #22107, #22266 and #22166), so
line 1 closes it.
## Scope: item (4), the #12699 declaration made total
ADR-0131 D7: "an object a deployment declares platform-global gets **no
organization column on that deployment** (the injected-columns plan
reads the declaration), so Layer 0 and the driver agree by having
nothing to scope." ADR-0131's retirement list names "#12699's stand-down
semantics (replaced by D7's no-column)". Claim `6061910188`. No stored
row moves and no step runs at boot (ADR-0131 D14).
## What changes
- **The plan** (`spec/src/data/injected-system-columns.ts`):
`resolveInjectedSystemColumns(def, deployment?)`. The second argument
carries the deployment's validated `platformGlobalObjects`. A declared
object is planned with no `organization_id`, and the rest of its plan is
unchanged. With the argument absent or empty, every plan is
byte-identical to the one-argument call (pinned over eight object
shapes). Author-time callers pass nothing. The module doc says where
that leaves them: see P5.
- **The reader** (`objectql/src/registry.ts`, `objectql/src/plugin.ts`):
- `ObjectQLPlugin.start()` reads `org-scoping` FIRST, before
`loadMetadataFromService` and before the first
`installRegisteredSchemas`.
- It installs the validated set with the new
`SchemaRegistry.setDeploymentPlatformGlobalObjects()`.
- The registry passes the set to `applySystemFields` as the plan's
input, and to the tenant-index predicate (`carriesTenantScopeColumn`).
- `materializeBaseLayer` gains a first stamp, `applyDeploymentTenancy`.
It records the plan's answer on the base layer as `systemFields: {
tenant: false }`, which is the vocabulary every registered-object reader
already answers "no organization column" from. Its write-side inverse is
the last strip in `stripMaterializedStampsFrom`, so a Studio GET → PUT
stores the body the author wrote (#4326).
- Objects registered before the install (inside other plugins' `init()`)
are re-planned at the install, on every contributor layer.
- The registry reads the declaration in ONE place,
`deploymentWithholdsTenant`, which asks the spec plan with and without
the deployment input.
- The plugin logs the declared list once. It warns once, by name, for an
object that DECLARES its own `organization_id`: that column is the
author's, so it stays and is walled. It warns once for a refused
(malformed) key.
- **The stand-down retires** (`plugin-security/src/security-plugin.ts`):
- The third `tenancyDisabled` clause in `getObjectSecurityMeta` is gone,
the one that read `platformGlobalObjects`. A declared object reaches the
wall as its own `systemFields.tenant: false`.
- The `[security] deployment declares N platform-global object(s)` boot
line is gone; the engine logs the list.
- The plugin now warns only for the refused key it reads,
`suppressUnboundedOrgAdminGrant`. That key and its behaviour are
unchanged.
- **The reader moves** (`deployment-org-scoping-entitlement.ts`:
plugin-security → `core/src/security/`). Its consumers are now in two
packages that cannot import each other: objectql reads
`platformGlobalObjects`, plugin-security reads
`suppressUnboundedOrgAdminGrant`. It is exported from
`@objectstack/core` with its rules unchanged: absent ⇒ nothing declared;
junk ⇒ the whole key refused, never coerced, each key independently.
- **The provider declaration**
(`plugins/organizations/src/organizations-plugin.ts`): `providesServices
= ['org-scoping']` (ADR-0116 D2). See P2.
- **Narratives**: `tenancy-posture.ts` (the `platformGlobalObjects` and
`suppressUnboundedOrgAdminGrant` docs), `tenant-layer0-verdict.ts` (the
carve-out row), `engine.ts` (two docblocks), and
`auto-org-admin-grant.ts` (one docblock).
- **ADR-0087**: the D3 entry
`18.platform-global-object-organization-column-retired.ts`, one step-18
rationale fragment (order 89), and the regenerated `registry.ts`.
- **Changeset**
`.changeset/15207-platform-global-no-organization-column.md`: `major`
for `@objectstack/objectql` and `@objectstack/plugin-security`
(Changesets pre mode is in, `.changeset/pre.json`), `minor` for spec and
core, `patch` for organizations, with its BREAKING banner, its FROM → TO
and the marker.
## Premise readings (each measured before code)
- **P1, HOLDS.** The harness: a booted `ObjectKernel` with
`ObjectQLPlugin` over SQLite, the real `SecurityPlugin`, and a fixture
provider composed after the objects plugin declaring
`platformGlobalObjects: ['qa_widget_registry']`. It was measured at
`799eb000c7`, before any edit:
- the declared object was registered with `organization_id`, and its
table was created with the column (`columnInfo`);
- `security.getReadFilter(declared, member)` answered no wall (the fold
in `getObjectSecurityMeta`), and the sibling answered `{
organization_id: 'org_acme' }`;
- a system read of the declared table carrying `tenantId: 'org_acme'`
returned only the `org_acme` row of two (the driver's tenant arm).
So Layer 0 and the driver disagreed about one object. Control: with no
declaration, both objects were walled.
- **P2, HOLDS through the Phase 1/2 split, NOT through a per-plugin
edge.**
- **Where the plan is computed:** `SchemaRegistry.registerObject` →
`applySystemFields` → `resolveInjectedSystemColumns`. That runs inside
whichever plugin's `init()` calls `manifest.register`, and again for
later registrations (`loadMetadataFromService` and
`restoreMetadataFromDb` at `start()`, installs after it). The columns
are fixed at `ObjectQLPlugin.start()` → `installRegisteredSchemas`.
- **When `org-scoping` is registered:** in `OrganizationsPlugin.init()`,
which hard-depends on the engine (its `init()` calls
`manifest.register`). `serve` composes it after Auth and the app
plugins, so it initializes after many object registrants, and the
fixture measured that too: the declared object was already registered
when the provider initialized.
- **What orders them:** ADR-0116's Phase 1/2 split. Every `init()`
completes before any `start()`, so the provider has registered by the
engine's `start()`, and no table exists yet. The provider now declares
it in `providesServices`, so "absent at start" is a declared fact
(ADR-0116 D2; the AGENTS.md startup-registry cure 2).
- **Measured:** neither ADR-0116 edge can order the provider ahead of
the registration-time plan itself. An engine-side `optionalDependencies`
on the provider is a cycle, and `resolvePluginOrder` throws on an
optional edge too. An edge from every object registrant is an open-ended
set. That is why the registry re-plans at the install.
- **Validation:** a provider that registers outside `init()` with a
different declaration fails the boot at `kernel:ready`, naming both
lists and `providesServices`.
- **No boot move:** no plugin moved, and no data step runs at boot.
- **P3, HOLDS.** With the key absent, every object's registered shape is
byte-identical: pinned as JSON equality between a registry with no
install and one with an empty install, plus the spec pin over eight
shapes, plus the kernel pin. With junk (`platformGlobalObjects:
'qa_widget_registry'`), the engine warns `'platformGlobalObjects'
REFUSED` once, and every object keeps its column and its wall.
- **P4, HOLDS.** At `799eb000c7`, `git grep platformGlobalObjects` finds
no declarer outside tests: the spec schema and docs, the reader,
plugin-security's consumer and log, and tests only. Every pin uses a
fixture provider.
- **P5, measured.** Author-time surfaces compute the plan with no
deployment, so on the declaring deployment they still name
`organization_id` for a declared object:
- the linter's addressable-name set (`lint/src/system-fields.ts`);
- the import mapper (`spec/src/data/import-mapping-target.ts`);
- the tenancy census (`scripts/platform-object-tenancy-census.mjs`);
- the CLI's authoring filter judge.
Runtime surfaces on that deployment agree with it: the registry, the
DDL, the `/meta` read exits (pinned through
`ObjectStackProtocolImplementation`), the metadata bridge that
`describe` reads, the lifecycle provenance (`absent`), and the field
doors (`INVALID_FIELD` / `INVALID_FILTER`). The plan's module doc and
the changeset say so. One one-shot surface depends on composition: `os
migrate plan` / `apply` composes the host config's plugins, not
`serve`'s posture-driven `OrganizationsPlugin`. A declaring deployment
whose provider arrives only through `serve` would get a migrate plan
that adds the column back. That is under Acceptance notes, for C10.
## Pins (refused and still-accepted case each)
- **The plan** (`spec/src/data/injected-system-columns.test.ts`, 5
cases):
- a declared object has no `organization_id`, and only that moves;
- CONTROL: a sibling keeps it;
- the array form works;
- absent, empty set and empty list are byte-identical over eight shapes;
- a nameless record is never declared.
- **The registry**
(`objectql/src/registry-deployment-platform-global.test.ts`, 7 cases):
- registered after the install: no column, no tenant index, the record
present;
- registered before the install: re-planned on every contributor layer,
`extend` included;
- absent or empty: JSON-identical;
- an authored `organization_id` is kept and reported;
- an object that opted out itself is untouched;
- the `/meta` read exit serves the registry's answer;
- a stored body converges at the read seam, and the write seam takes the
record off. CONTROL: an author's other `systemFields` member survives,
and a non-declared object is never touched.
- **The kernel**
(`plugin-security/src/platform-global-no-organization-column.test.ts`, 8
cases, booted kernel with a fixture provider):
- the plan and the DDL, with the sibling control;
- absent key;
- junk key (warned once; every column kept);
- Layer 0 composes no wall and the driver reaches every row, while the
sibling is walled at both;
- a write naming `organization_id` is refused `INVALID_FIELD` / 400, and
the sibling accepts it;
- **no stand-down path remains**: the plugin over an engine whose plan
never received the declaration walls the object;
- a provider registered after the objects reaches the plan;
- a provider that registers in `start()` refuses the boot by name.
- **Layer 0 and the driver at once**
(`tenant-layer0-verdict-end-to-end.test.ts`): a member's predicate
update on the declared object now matches both rows (it matched one
before, the driver's tenant arm), and the bulk event names no
organization. CONTROL: the sibling sweep matches one row and names
`org_acme`.
- **plugin-security reads no declaration**
(`deployment-platform-global-exemption.test.ts`, rewritten):
- a declared object that still carries its column is walled under
`isolated` and `group`, and on the ADR-0123 D2 write path;
- the registered shape is not walled, and the sibling is;
- under `single`, Layer 0 is inert on both;
- a junk `platformGlobalObjects` draws no warning from this plugin, and
a junk suppress key is warned once;
- the arming log carries no platform-global line.
- **The reader**
(`core/src/security/deployment-org-scoping-entitlement.test.ts`, 11
cases): absent, well-formed, four junk shapes (whole-key refusal),
per-key independence, and memo per instance.
## Reverse verification (one-off, `scripts/ablation-replace.mjs` in hold
mode with a trap restore)
The plan's read of the declaration in `injected-system-columns.ts` was
neutralised: the anchor `!(name !== '' &&
deploymentDeclaresPlatformGlobal` was replaced so it never matches
(anchor 1 → 0, blob `6e571966dd` → `88efcbbb14`). The spec was rebuilt,
and `ablation-dist-preflight.mjs` found the marker in 6 built files.
Results:
- the spec plan: exactly the two declared-object pins red, 20 green;
- the registry: 5 of 7 red, with the absent-declaration and opted-out
controls green;
- the kernel: 4 of 8 red. The plan, Layer 0 / driver, write refusal and
ordering pins went red. The absent-key, junk-key, no-stand-down and
late-provider controls stayed green;
- the end-to-end verdict: the declared-object sweep red, 2 green.
Restore leg:
- blob == HEAD (`6e571966dd`), `git diff HEAD` empty, the whole tree
clean;
- the spec rebuilt, and `ablation-dist-preflight.mjs --absent` finds the
marker in none of the 234 built files;
- the same files re-run green: spec 22, registry 7, kernel and
end-to-end 11.
## Fate for C7 (#15211) and C10
On a declaring deployment, each declared object's existing
`organization_id` column is ADR-0131 D10 fate 1 (column dropped). Schema
sync is additive, so the physical column stays, and the boot drift
report names it orphaned. The declarer (cloud's control plane, C10) owns
the data step: confirm nothing reads it, then `os migrate apply
--allow-destructive`. Its backfill decides any value that must survive.
C7's inventory records the declared set per deployment with this entry
id. No boot step reads or writes the column (D14).
## Files outside the claim's file surface
- `packages/plugins/organizations/src/organizations-plugin.ts`: one
declaration, `providesServices`. It is the "provider declaration" the
claim's ordering bullet names, in the provider's own file. Its lane is
re-declared by the seat.
- `packages/core/src/security/deployment-org-scoping-entitlement.ts` and
`.test.ts`, and `core/src/security/index.ts`: the reader's new home, so
that both consumers can import it (the claim allows "if its reader
moves"; `core` is on the claim's declared lanes).
- `packages/objectql/src/federated-injected-column-readers.test.ts`: two
census rows for the two new `organization_id` seams. That census fails
on any undisposed seam.
- Within the claimed packages: `objectql/src/plugin.ts`,
`plugin-security/src/auto-org-admin-grant.ts` (one docblock), and four
plugin-security test files.
## Acceptance notes
- `os migrate plan` / `apply` compose the host config's plugins, not
`serve`'s posture-driven organizations runtime. On a declaring
deployment whose provider is composed only by `serve`, a migrate plan
reads no declaration and would add the column back to a declared
object's table (additive sync). Carrier: C10 (cloud's control plane
composition), noted, not filed: there is no in-repo declarer to reach it
with.
- Author-time tools name `organization_id` on a declared object; the
declaring deployment refuses it as an unknown field. This is inherent to
a deployment input, and stated in the plan's docs and the changeset.
- `OrganizationsPlugin.providesServices` was absent before, so
ADR-0116's stage-1 check could not name it for an `init()`-time requirer
of `org-scoping`. None exists in-repo (`check:init-service-contract`
green).
## Verification (head `5322c2b755`, which merged `origin/main` at
`dc4a5c6308` through `os-regen-merge.sh`; the regeneration wrote
nothing)
- **Suites, each through the verify lock:**
- spec `--project local`: 626 files, 18728 passed, 1 todo;
- objectql `--project local`: 385 files, 7562 passed;
- core: 83 files, 2258 passed;
- organizations: 11 files, 151 passed;
- plugin-security: 183 files, 3835 passed, 45 skipped. It was run at
`86db7e86f4`; since then plugin-security changed one test file's type
annotation, and objectql (aliased to source there) lost one unused
accessor. The four plugin-security files this PR touches were re-run
green afterwards.
- spec `--project repo`: `step18-rationale-merge` and
`conversions-major18-merge`, 21 passed.
- **Typecheck** exit 0 for core, objectql, plugin-security,
organizations and spec. Each package's script includes
`check:test-typecheck`, and every ledger held unchanged.
- **Spec artifacts:** `check:generated` reports 15 of 15 up to date.
`check:migration-registry`: 400 semantic entries. `check:api-surface`
unchanged.
- **Gates:** `dispatch-gates --commands --repo
objectstack-ai/objectstack` (no paths) at `5322c2b755` derives 109
families. All 109 ran, each exit code captured before any pipe, all 0.
The `--ran` reconciliation: 109 derived, 109 run, 0 NOT-MEASURED, 0
UNRUN.
- Fixed on the way: `check:engine-double-contract` asked for the new
pin's three doubles in the ledger (`--write`), and `check:slot-lookup`
refused one untyped service lookup in a test.
- `check:dts-closure`, `check:dual-build-cjs-loads` and `check:i18n`
first stopped on unbuilt packages (a prerequisite). They are green after
a full build (72 tasks, 71 cached).
- Also green, run by hand: `check:init-service-contract` (36 declared)
and `check:startup-registry-verdict` (none recording a verdict the boot
can contradict).
- **Lint, a proven narrowing:** `eslint --no-inline-config --format
json` over the 21 changed `.ts` files gives 21 results, 0 errors, 0
warnings. The population is `eslint.config.mjs`'s `packages/**` and
`**/*` TS globs. The config states it enables no type-aware linting, so
an untouched file's verdict cannot move. The full `pnpm lint` is CI's.
- **Measurements, one-off and not committed:**
- P1, on a scratch copy of the kernel harness at `799eb000c7`;
- P2's cycle, `resolvePluginOrder` over the two declarations: it throws
`Circular dependency detected: com.objectstack.engine.objectql`.
CONTROL: without the soft edge, the order is engine then organizations;
- the reverse verification above, restored and proven (preflight
`--absent`, tree clean, the same files green).
- `origin/main` has moved 8 commits since `dc4a5c6308`, three of them
through this PR's files (`registry.ts`, `engine.ts`,
`security-plugin.ts`) and the double ledger. A no-commit merge probe
auto-merges them with no conflict. The next hop merges them through
`os-regen-merge.sh`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5ff7cbe commit 41d0d40
23 files changed
Lines changed: 1479 additions & 263 deletions
File tree
- .changeset
- packages
- core/src/security
- objectql/src
- plugins
- organizations/src
- plugin-security/src
- spec/src
- data
- migrations
- entries/semantic
- security
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
8 | 19 | | |
9 | 20 | | |
10 | 21 | | |
| |||
16 | 27 | | |
17 | 28 | | |
18 | 29 | | |
19 | | - | |
20 | | - | |
| 30 | + | |
| 31 | + | |
21 | 32 | | |
22 | 33 | | |
23 | 34 | | |
| |||
28 | 39 | | |
29 | 40 | | |
30 | 41 | | |
31 | | - | |
32 | | - | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
33 | 45 | | |
34 | 46 | | |
35 | 47 | | |
| |||
40 | 52 | | |
41 | 53 | | |
42 | 54 | | |
43 | | - | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
44 | 58 | | |
45 | 59 | | |
46 | 60 | | |
| |||
60 | 74 | | |
61 | 75 | | |
62 | 76 | | |
63 | | - | |
64 | | - | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
65 | 80 | | |
66 | 81 | | |
67 | 82 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
57 | 67 | | |
58 | 68 | | |
59 | 69 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2883 | 2883 | | |
2884 | 2884 | | |
2885 | 2885 | | |
2886 | | - | |
2887 | | - | |
2888 | | - | |
| 2886 | + | |
| 2887 | + | |
| 2888 | + | |
| 2889 | + | |
| 2890 | + | |
2889 | 2891 | | |
2890 | 2892 | | |
2891 | 2893 | | |
| |||
3545 | 3547 | | |
3546 | 3548 | | |
3547 | 3549 | | |
3548 | | - | |
3549 | | - | |
| 3550 | + | |
| 3551 | + | |
3550 | 3552 | | |
3551 | | - | |
| 3553 | + | |
| 3554 | + | |
| 3555 | + | |
| 3556 | + | |
| 3557 | + | |
3552 | 3558 | | |
3553 | 3559 | | |
3554 | 3560 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
316 | 316 | | |
317 | 317 | | |
318 | 318 | | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
319 | 331 | | |
320 | 332 | | |
321 | 333 | | |
| |||
0 commit comments