Repository navigation
Commit ae6bd1a
feat(cli)!: 18.0 guards — os migrate organization-ownership refuses, and os serve refuses a walled posture at boot (#24480)
Part of #15212
Clause-②: yes (narrowing)
The **18.0 guards** stage (plan amendment 14 on #15212, from ruling
records 6259608309 and 6259698357 on #15194). 18.0 ships for fresh
installs under the `single` posture only. This PR makes the two things
18.0 does not support refuse loudly. It deletes no ceremony code, and it
adds no re-enable flag or environment escape hatch.
## What changes
1. **`os migrate organization-ownership` refuses in 18.0, plan and apply
alike.**
- Exit `1` with the standard ADR-0112 code `NOT_IMPLEMENTED`. Under
`--json` it prints `{ error: 'plan_refused' | 'apply_refused', reason:
'upgrade-unavailable', code: 'NOT_IMPLEMENTED', detail }`.
- The message says 18.0 supports fresh installs only and that the v17 →
v18 upgrade path ships in an 18.x release.
- It reads and writes nothing. No posture, supplement, plan file,
occupancy probe or boot is touched before it.
- **Shape:** the ceremony class is kept whole as the named export
`OrganizationOwnershipCeremony`. The registered default command is a
subclass of it whose `run()` refuses: same flags, same help, plus a
description prefix. The 18.x release that carries the upgrade path
deletes the subclass. The ceremony's ~45 existing in-process tests now
import the ceremony class, a one-line import change in each of 16 files,
so the 18.x ceremony keeps its coverage. `organization-ownership-*.ts`
utils are untouched.
2. **`os serve` refuses a walled posture (`group` / `isolated`) at
boot.**
- It refuses at its existing tenancy-posture gate, in the same FATAL
plus `process.exit(1)` shape as an unrecognised posture: before a config
is read, a plugin is mounted or anything is written.
`OS_MULTI_ORG_ENABLED=true` resolves to `isolated` and is refused too.
- The FATAL names `NOT_IMPLEMENTED`, the 18.x release and the fixes:
`OS_TENANCY_POSTURE=single`, or stay on 17.x.
`OS_ALLOW_DEGRADED_TENANCY` does not lift it.
- `os dev` and `os start` spawn `serve`, so this is every operator boot.
Under `single` nothing changes.
## Where the walled refusal sits (measured)
| candidate | operator-facing? | walled fixtures it breaks |
|:--|:--|:--|
| `os serve`'s posture gate (chosen) | every CLI boot (`dev` and `start`
spawn `serve`) | the two CLI e2e files that spawn `serve` under
`isolated` — rewritten, see below |
| `os dev` / `os start` only | misses production `os serve` | none |
| runtime / kernel (`resolveTenancyPosture`, `createStandaloneStack`,
`bootStack`) | also every embedded and test boot | every kernel-level
walled suite: the dogfood `wall-*` files, verify posture tests,
plugin-auth, core and driver tests |
The kernel-level walled boots (`@objectstack/verify`'s `bootStack`, the
dogfood wall suites, the unit tests that set
`OS_TENANCY_POSTURE=isolated`) never pass through `serve`, and they keep
working. The only walled boots this refusal breaks are the two
nightly-tier CLI e2e files that spawn `os serve` under `isolated`. Those
tested the organizations runtime's host resolution and its mount-stage
classification, which no 18.0 walled boot reaches. They keep their
fixtures and now pin, over the real process, that the 18.0 refusal comes
first: whatever the package state (installed in the app, absent,
reachable only through `NODE_PATH`, present-and-refusing) and with or
without `OS_ALLOW_DEGRADED_TENANCY=1`, no organizations stage runs. The
18.x release restores their resolution cases from git history.
## The `os dev` automatic ceremony (A1)
`os dev` runs the ceremony only when the boot guard refuses the
database. That never happens on a fresh database: the guard passes a
database with no retired column and no marker. When it does run, it
spawns this same command, so it now gets the refusal. It prints the
command's own refusal line, says nothing was changed, and keeps the copy
it took. No `os dev` change was needed.
## Docs
`content/docs/deployment/cli.mdx` (the command's section, its table row,
and `os dev`'s in-place upgrade section), `tenancy-modes.mdx`,
`environment-variables.mdx` (the `OS_TENANCY_POSTURE` row) and
`self-hosting.mdx` (walled first boot) each gain the sentence "18.0
supports fresh `single` installs; upgrades and walled postures arrive in
18.x", only where the text would otherwise be false. Nothing under
`content/docs/releases/`.
## Tests
- New
`packages/cli/src/commands/migrate/organization-ownership.release-refusal.test.ts`
(5 cases): plan and apply refuse with `NOT_IMPLEMENTED` and exit 1; the
plain output carries the code; nothing is touched (boot and occupancy
stubs that throw, an unrecognised posture, a missing supplement and plan
file, no db or out file created); the default class is a subclass of the
ceremony with the same flags.
- New `packages/cli/src/commands/serve-walled-posture-refusal.test.ts`
(5 cases): `single` gives `null`; every walled posture in the spec
vocabulary is refused; the `OS_MULTI_ORG_ENABLED` source is named; and
the real `serve`, run in-process under `isolated` and under
`OS_MULTI_ORG_ENABLED=true`, exits 1 with nothing announced (no config
read).
-
`packages/qa/dogfood/test/organization-ownership-boot-refusal-order.dogfood.test.ts`:
new step 2a, in which the real CLI binary refuses with `NOT_IMPLEMENTED`
and the database columns are unchanged. Step 2's upgrade proof now runs
`OrganizationOwnershipCeremony` from the CLI's built module in its own
process (oclif auto-transpile off, as `bin/run.js` does), so the
ceremony → boot-succeeds proof stays alive for 18.x.
Readings on head `cca3540481` (the measurements below were taken on
`9141352b`, before the merge of `main`, which touched only unrelated cli
tests; the ratchet families were re-run on `cca3540481`):
- cli `typecheck`: exit 0.
- cli unit project: 312 files / 4615 tests passed.
- The 16 changed ceremony test files plus the two new pins and
`serve-tenancy-posture-gate`: 18 passed, 1 skipped (`remote-face`, which
needs `sqld`), 199 tests.
- The two nightly e2e files (`OS_TEST_TIERS=nightly`): 7/7.
- The dogfood file: 5/5.
Ablations, each through `scripts/ablation-replace.mjs`, mutation landed
and restored with `git diff HEAD` empty:
- the registered command's `run` replaced by the ceremony's → 4/5
refusal cases red, the ceremony answering `posture-unresolved`;
- serve's `walledPostureRefusal(tenancyPosture)` call replaced by `null`
→ both in-process boot cases red;
- the `single` early return deleted → the `single` case red.
## Acceptance notes
- The boot guard (`organization-ownership-boot-guard.ts`), left as is
per the dispatch, still names `os migrate organization-ownership` as the
remedy for a v17-shaped database. In 18.0 that command refuses, and its
refusal says to stay on 17.x. The guard's prescription is therefore
incomplete for 18.0. Whether to reword it is the seat's call: the
dispatch said to leave the guard alone.
- `os serve`'s unrecognised-posture FATAL still lists `group` and
`isolated` among its fixes (from `TENANCY_POSTURE_FIX_HINTS`). Following
either now reaches the 18.0 refusal on the next boot, which is loud, so
it is not touched here.
- Embedded hosts that boot the kernel directly (adapters,
`createStandaloneStack`) are not refused: they are not the operator's
`os` boot path, and refusing there would break the kernel-level walled
suites.
- `os doctor` still reports a walled posture without saying 18.0 refuses
it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Q4U6vXvHNpcDZr24aXeJo2)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2ec6827 commit ae6bd1a
28 files changed
Lines changed: 565 additions & 234 deletions
File tree
- .changeset
- content/docs/deployment
- packages
- cli
- src
- commands
- migrate
- utils
- test
- qa/dogfood/test
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
203 | 203 | | |
204 | 204 | | |
205 | 205 | | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
206 | 211 | | |
207 | 212 | | |
208 | 213 | | |
| |||
1197 | 1202 | | |
1198 | 1203 | | |
1199 | 1204 | | |
1200 | | - | |
| 1205 | + | |
1201 | 1206 | | |
1202 | 1207 | | |
1203 | 1208 | | |
| |||
1476 | 1481 | | |
1477 | 1482 | | |
1478 | 1483 | | |
| 1484 | + | |
| 1485 | + | |
| 1486 | + | |
| 1487 | + | |
| 1488 | + | |
| 1489 | + | |
| 1490 | + | |
1479 | 1491 | | |
1480 | 1492 | | |
1481 | 1493 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
101 | | - | |
| 101 | + | |
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
466 | 466 | | |
467 | 467 | | |
468 | 468 | | |
469 | | - | |
| 469 | + | |
| 470 | + | |
470 | 471 | | |
471 | 472 | | |
472 | 473 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
19 | 24 | | |
20 | 25 | | |
21 | 26 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
| 60 | + | |
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
161 | | - | |
| 161 | + | |
162 | 162 | | |
163 | 163 | | |
164 | 164 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
47 | | - | |
| 47 | + | |
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
| 53 | + | |
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
| 35 | + | |
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| |||
0 commit comments