Repository navigation
Commit 1920cf3
feat(platform-objects,service-automation,service-realtime)!: seven deployment-level tables lose their injected organization column, and reads need manage_platform_settings (ADR-0131 D7) (#22107)
Part of #15207
Clause-②: no (narrowing: deployment-level objects lose their injected
organization column and the global settings rung moves; whether any
`@objectstack/spec` export widens is measured on the built declaration
closure by the dev, and the measurement decides)
The line above is the claim's, copied verbatim. This PR moves no
settings rung (scope item 3 is not here). The measurement it names:
`check:api-surface` is green on the built spec, so no
`@objectstack/spec` export widens.
## Scope and the two decisions
This PR lands **scope item (1)** of #15207, plus the one `objectql` edit
item (1) needs so that it ships no regression. The seat's claim revision
on the card (comment `6043540291`) narrows this claim's landing to item
(1) and records two decisions on the first round's dev report (comment
`6042515710`):
- **Decision 1 = A.**
`packages/objectql/src/lifecycle/lifecycle-service.ts`,
`tenantWindowsFor` only, joins the file surface. The edit is in this PR
(section below), so the lifecycle regression the first round measured is
closed here, and nothing outside the PR remains before it can be
readied. Refusing such an override at save is not decided, and it is not
built.
- **Decision 2 = A.** The seven objects keep `requiredPermissions:
['manage_platform_settings']`. ADR-0131 D7 says objects without the
column "are governed by object permission, not by the wall", and the
security table below shows what happens without the gate.
Items (2), (3) and (4) are not built under this claim, so this PR says
`Part of` and the card stays open for them.
## What this PR does (scope item 1)
`sys_job`, `sys_job_run`, `sys_job_queue`, `sys_flow_dispatch`,
`sys_migration`, `sys_migration_journal` and `sys_presence` now declare
`systemFields: { tenant: false }`, so the registry injects no
`organization_id` on them (ADR-0131 D7). Each also declares
`requiredPermissions: ['manage_platform_settings']`; the security table
below is why that is part of the same change.
- One ADR-0087 D3 semantic entry per removed column
(`18.sys-*-organization-column-retired.ts`), as the card requires, plus
one step-18 rationale fragment.
- The platform-object tenancy census artefact regenerated: 57 → 50
objects in reach, `systemFields.tenant: false` 1 → 8.
- `sys_job.global-unique.test.ts`: the pin that asserted the injected
column is re-premised on its absence, which is the direction its own
comment asked to re-check from.
- New pins: the injection plan, the declared opt-out and the capability,
per object, each with a control (the same declaration with the opt-out
removed gets the column; `sys_secret` and `sys_automation_run`, which
are tenant-attributed, keep it).
## The lifecycle guard (Decision 1)
A tenant-scope `lifecycle.retention_overrides` entry gives one
organization its own retention window, and the reaper and the archiver
apply it by partitioning the object's rows on `organization_id`: a pass
for that organization's rows, then a global pass whose `$or` covers
everyone else. On a table with no `organization_id` column both passes
name a column the table lacks. The first round measured it on the real
SQL driver (better-sqlite3): both predicates throw `INVALID_FILTER`, so
on a new database such an override on `sys_job_run`, `sys_job_queue` or
`sys_flow_dispatch` (the three of the seven that declare a `lifecycle`)
stopped that table's retention.
`tenantWindowsFor` is the one decision both passes ask. It already
answered no windows for a federated object whose `organization_id` is
the registry's unprovisioned injection. It now also answers no windows
when the registry provenance of `organization_id`
(`resolveInjectedColumnProvenance`, `@objectstack/spec/data`) is
`'absent'`: no injection plan put the column there and the author
declared none. The object then runs its one global pass at the global
window. No row of such a table belongs to an organization, so a tenant
override naming it has nothing to select, and it is not applied. An
object that has the column keeps its per-tenant windows.
- **Measured on the real registry**, at this head: the seven objects,
registered through `ObjectQL`'s registry, carry `systemFields: { tenant:
false }`, have no `organization_id` field, and answer provenance
`'absent'`. The #21918 federated shape answers
`'injected-unprovisioned'`, not `'absent'`, so the federated line stays
and the new line is a second answer to the same question (is there a
provisioned `organization_id`). A federated object that declares
`systemFields: { tenant: false }` answers `'absent'` and is covered by
the new line.
- **Pin and control** (`lifecycle-service.no-tenant-column.test.ts`, on
a real `ObjectQL` engine and registry; the stub driver provisions each
table from the registered object's fields and refuses a filter on a
column the table lacks, as the SQL driver does): a premise case shows
the driver refuses an `organization_id` filter on the column-less table
(`INVALID_FILTER`, 400); the pin sweeps a column-less `sys_job_run` with
one organization's `90d` tenant override, and gets no error and exactly
one read, `created_at` before the `30d` cutoff; the control sweeps the
same declaration without the opt-out and gets the per-tenant read at
`90d` and the global `$or` read at `30d`.
- **Ablation, one-off.** Through `scripts/ablation-replace.mjs` (anchor
hit 1 → 0, blob changed): deleting the new line turns exactly the pin
red (`report.errors` gains the driver's `INVALID_FILTER` refusal for
`sys_job_run`), with the premise and the control green; the federated
reader census turns red too, because its row for the new seam no longer
finds a use. Restored: blob equals HEAD and `git diff HEAD` is empty.
- **The federated reader census**
(`federated-injected-column-readers.test.ts`) scans every non-test
source of the package for each use of a provenance seam, and fails on a
use without a row. The new call is one, so it gains one row, `skips`, on
the site that already skips; the set of skipping sites is unchanged. The
three federated lifecycle pins from #21918 stay green.
- **Other readers of the override and the window**, census at this head:
in `lifecycle-service.ts`, `loadGovernance` reads the global and
per-tenant `retention_overrides` (organization ids from
`sys_organization`, no read of the swept table), `tenantWindowsFor` is
the only reader of the per-tenant map, and `reap` and `archiveObject`
are the only sites that put `organization_id` into a predicate, both
built only from `tenantWindowsFor`'s answer. Governance quotas and
growth count rows with no filter; the rotator falls back to `reap`; the
archive's cold `keep` prune filters on `created_at` only; the retention
floors compare durations and read no rows. Outside `objectql`, the
settings manifest declares the key and `service-queue` registers a
floor; neither reads rows. None names the column.
## Writer census, with a firing control
Read at `e67ba80049`, all non-test sources under `packages/`. Every
write call naming the object (literal or a constant bound to it), its
context, and whether the row or options name an organization; a row that
is not an inline literal was traced to its type.
| object | writers | write sites | verdict |
|---|---|---|---|
| `sys_job` | `DbJobAdapter` (service-job) | 4 | system context; rows
name no organization |
| `sys_job_run` | `DbJobAdapter` | 2 | system context; rows name no
organization |
| `sys_job_queue` | `DbQueueAdapter` (service-queue) | 9 | system
context; rows name no organization |
| `sys_flow_dispatch` | `ObjectStoreFlowDispatchStore`
(service-automation) | 2 | system context; key and outcome only |
| `sys_migration` | migration-flag helpers, the engine's two flag
writes, seed-tenancy, membership-backfill and flow-credential receipts |
11 in 6 files | system context; `DataMigrationFlagSchema` has no
organization field |
| `sys_migration_journal` | core migration runner | 1 | system context,
or the transaction it opened with one; `MigrationJournalEventSchema` has
no organization field |
| `sys_presence` | none through ObjectQL | 0 | `apiMethods: ['get',
'list']`; presence travels the realtime path |
Raw-SQL writes to the seven tables: 0. The same grep shape finds the raw
`INSERT INTO sys_packages` writes elsewhere, so it can match.
**Firing control.** The same procedure, run over the three tables the
card and triage name as tenant-attributed:
- `sys_http_delivery`: 6 write sites flagged (caller-supplied context).
- `sys_secret`: the engine's secret write passes the business write's
driver options, so the SQL driver stamps the caller's organization.
- `sys_email`: the call-site pass is silent (system context), and the
row trace fires: the email service stamps `organization_id` into the row
it hands the persistence insert. That is why rows that are not inline
literals were traced.
`sys_secret` stays off this PR (triage's correction; its fate is C7's).
`sys_http_delivery` and `sys_email` are excluded by the card.
## Security: who reads these tables, before and after
Measured by driving the real `SecurityPlugin` middleware with a `find`
on `sys_job_queue` (a scratch harness, not committed), with the shipped
`organization_admin`, `admin_full_access` and `member_default` sets:
| posture | shape | organization admin | platform admin | member |
|---|---|---|---|---|
| isolated | before (column injected) | admitted, `organization_id =
org-1`: 0 rows, since every row is NULL | same: 0 rows | 403 |
| isolated | column removed, **no gate** | **admitted, no filter: every
organization's rows** | admitted, no filter | 403 |
| isolated | column removed, with the gate (this PR) | 403 (missing
`manage_platform_settings`) | admitted, no filter | 403 |
| single | before | admitted, no filter | admitted, no filter | 403 |
| single | with the gate (this PR) | 403 | admitted, no filter | 403 |
The middle row is the reason for the gate (Decision 2). With no column
there is no tenant wall, and `organization_admin`'s `'*'` grant carries
the superuser bits, so Layer 1 is skipped too. D7 governs these tables
by object permission, and the gate is that permission, on the
`sys_sso_provider` precedent. The `single` rows are a declared
narrowing: an organization administrator who is not a platform
administrator loses generic reads of these seven tables. No shipped app
or nav entry names any of them. Every platform reader and writer uses a
system context, which the gate does not apply to.
## Existing databases
Schema sync only adds. On an existing database each table keeps its
physical `organization_id` column (and its index where one was
provisioned), and the boot drift report names it orphaned: "column
exists in the database but not in metadata (orphaned) — os migrate apply
--allow-destructive to drop it". It is never dropped automatically, and
never in production. By the census the column holds only NULL, so no
data step is needed and the drop loses nothing. The v18 ceremony of
ADR-0131 D10 (C7) is where a guided drop belongs; this PR invents no
migration. The lifecycle guard reads the registered object, not the
physical table, so on such a database the orphaned column is simply
never named.
## Acceptance notes
- **Scope items not in this PR** (not built under this claim, per the
revision `6043540291`):
- (2) `sys_audit_log`: removing the column opens the no-record rows
(`config_change`, `import`, `platform_admin_standing_change`) to every
organization's admin, by the same mechanism as the middle row above. The
card's "RLS readers filter on `tenant_id` explicitly" needs either a
platform RLS policy on `tenant_id` plus an explicit `sys_audit_log`
entry in `organization_admin` without the superuser bits
(`plugin-security`), or a filter in `plugin-audit` that re-derives the
wall's posture ladder. With the guard in this PR, a column-less
`sys_audit_log` would get the global retention window only; whether that
is right for audit is item (2)'s call, not made here.
- (3) the settings global rung: six `service-settings` manifests (ai,
auth, knowledge, mail, sms, storage) and `objectql`'s `lifecycle`
manifest are global and edited at runtime in Setup, so §6 Q3's
measurement points to a tenant-less `sys_platform_setting`, not to
configuration. Existing global rows would need a data step to move; that
is the C7 ceremony's.
- (4) #12699 made total: `applySystemFields` (`objectql`) has to receive
the deployment's declaration, and the stand-down in `plugin-security`
retires.
- **Not decided, not built:** a tenant-scope `retention_overrides` entry
naming a table with no organization column is still accepted at save; it
now has no effect instead of stopping retention.
- **Noted only:** `sys_job_queue.metadata_json`'s description says it
carries `tenant_id`; no producer measured writes it there.
- **Step-18 rationale order.** After the merge of `main`, this PR's
fragment and the builtin node-config fragment both sat at order 85, and
the id tie-break rendered this one between the approval-node fragment
and the sentence that continues it ("Then the builtin arm stops being
presence-only"). No pin refuses a duplicate order, but the rendered text
read wrong, so this fragment moved to 86, the next free order. The
rendered rationale now runs approval-node, builtin values, then this
fragment.
## Merge of `main`
Two merges, both through `scripts/pm/os-regen-merge.sh`, no rebase and
no force-push: `b016a64661` (main at `aa71c4d9d1`) and `1ef09a5127`
(main at `dd39171835`). Neither stopped on a conflict.
`packages/spec/src/migrations/registry.ts` text-merged with the step-18
entries of the flow builtin node-config change, and
`check:migration-registry` reads its generated regions current.
`scripts/platform-object-tenancy-census.json` kept this branch's bytes
(main had not changed it), and `check-platform-object-tenancy-census` is
green at this head. `.changeset/pre.json` does not exist at
`dd39171835`, so the changeset keeps `minor` with its **BREAKING**
banner.
## Tests and gates
Readings at this head, `1ef09a5127`, unless a line says otherwise.
- **`@objectstack/objectql`**: `vitest --project local` 379 files, 7516
passed; typecheck (`tsc --noEmit` and the test-layer check) exit 0. The
four lifecycle and census files alone: 128 passed.
- **`@objectstack/spec`**: build exit 0; `check:generated` 15 of 15 up
to date; typecheck exit 0; `vitest --project local` 622 files, 18567
passed, 1 todo, and the step-18 ledger merge tests
(`step18-rationale-merge`, `conversions-major18-merge`, project `repo`)
2 files, 21 passed, both measured at `dc24a4e051`. The second merge
changed no file under `packages/spec` (`git diff dc24a4e 1ef09a5
-- packages/spec` is empty).
- **`@objectstack/platform-objects`** 64 files, 1028 passed;
**`@objectstack/service-automation`** 175 files, 2120 passed;
**`@objectstack/service-realtime`** 5 files, 35 passed. Typecheck exit 0
for each.
- **Derived gate families**: `dispatch-gates --commands`, derived with
no paths at `1ef09a5127`: 103 commands. On the first pass 100 exited 0;
`check-engine-split-ratio --days 90` exited 2 (this clone was shallow
inside its 90-day window), and `check:dual-build-cjs-loads` and
`check:i18n` exited 3 (prerequisite: built output). After deepening
history to 2026-07-02 and a full workspace build, all three re-ran with
exit 0. Reconciled with `--ran`: "103 derived famil(ies) accounted for —
103 run, 0 NOT-MEASURED (a DERIVED zero — all 103 recorded an exit code
and none of them is 3)". Among them: `check-adr-0087-registration` ("1
declared-breaking changeset(s), each carrying an ADR-0087 disposition",
the seven ids registered), `check-changeset-no-major` ("This diff
introduces no `major` bump"), `check-platform-object-tenancy-census` (83
objects, 50 in reach, 33 outside) and `check:nul-bytes` OK.
`check-governed-merges --pr 22107` after the push: 0 of 25 paths hit the
governed register.
- **Lint, narrowed and proven**: `eslint --no-inline-config --format
json` over the 22 changed `.ts` files: 22 files in the report, 0 errors,
0 warnings. All 22 are inside the config's `packages/**` population, and
`eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`, no typed rules), so this diff cannot move a
verdict on an untouched file. The full `pnpm lint` is CI's.
- **Changed lines**: 1229 (1195 added, 34 deleted) across 25 files
against the merge base `dd39171835`, under the 5,000-line human-merge
threshold. The first round's 1022 grew by the lifecycle guard, its pin
and census row, and the objectql changeset.
## Changesets
- `.changeset/15207-deployment-plumbing-no-organization-column.md`:
`minor` with the **BREAKING** banner and the ADR-0087 marker registering
the seven ids (pre mode is not on at `dd39171835`).
- `.changeset/15207-lifecycle-no-tenant-column-no-partition.md`:
`@objectstack/objectql` `patch`, the lifecycle guard.
This body was revised in patch round 1 by session
`session_01GV6oYwgc1kWiUCb1YaprQ7`, from the dispatch of the
`domain:spec` seat 2 PM.
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f2a45db commit 1920cf3
25 files changed
Lines changed: 1195 additions & 34 deletions
File tree
- .changeset
- packages
- objectql/src
- lifecycle
- platform-objects/src
- audit
- system
- services
- service-automation/src
- service-realtime/src/objects
- spec/src/migrations
- entries/semantic
- scripts
Lines changed: 28 additions & 0 deletions
| 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 | + | |
Lines changed: 9 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
Lines changed: 6 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
151 | 151 | | |
152 | 152 | | |
153 | 153 | | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
154 | 160 | | |
155 | 161 | | |
156 | 162 | | |
| |||
Lines changed: 182 additions & 0 deletions
| 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 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
1559 | 1559 | | |
1560 | 1560 | | |
1561 | 1561 | | |
| 1562 | + | |
| 1563 | + | |
| 1564 | + | |
| 1565 | + | |
| 1566 | + | |
| 1567 | + | |
| 1568 | + | |
1562 | 1569 | | |
1563 | 1570 | | |
1564 | 1571 | | |
1565 | 1572 | | |
1566 | 1573 | | |
1567 | 1574 | | |
| 1575 | + | |
1568 | 1576 | | |
1569 | 1577 | | |
1570 | 1578 | | |
| |||
Lines changed: 9 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
37 | 46 | | |
38 | 47 | | |
39 | 48 | | |
| |||
Lines changed: 10 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
25 | 35 | | |
26 | 36 | | |
27 | 37 | | |
| |||
Lines changed: 16 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
59 | 61 | | |
60 | 62 | | |
61 | 63 | | |
| |||
150 | 152 | | |
151 | 153 | | |
152 | 154 | | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
160 | 166 | | |
161 | 167 | | |
162 | | - | |
163 | | - | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
164 | 171 | | |
165 | 172 | | |
166 | 173 | | |
| |||
0 commit comments