Repository navigation
Commit edac218
fix(cli): a production serve with no storage says so before any plugin starts, and refuses when its auth service needs a driver (#23631)
Fixes #23573
Clause-②: no
## What this changes
A production `os serve` of a host config (one whose `plugins` hold
plugin instances) with no recognized `OS_DATABASE_URL`, no `--dev` and
no plugin named like a driver declared no default datasource at the
storage step, and said nothing. The boot then died in
`AuthPlugin.start()` with `No driver available for object
'sys_organization'`, after an objectql ERROR whose fix sends the
operator to "check the datasource behind sys_metadata", which nobody
declared.
This is the seat's option A (claim `6185741820`): a guard at the end of
kernel Phase 1.
- **`packages/cli/src/commands/serve.ts`, storage step (step 2).** Only
records `productionResolvedNoStorage` when `resolveStorageDefinition`
answers `null`. It refuses nothing.
- **`packages/cli/src/commands/serve.ts`, composition tail.** One CLI
plugin, `com.objectstack.cli.no-storage-boot-guard`, registered only
when that flag is set, and registered LAST: after
`com.objectstack.cli.seed-settlement-announcer`, immediately before
`runtime.start()`. Its `init()` reads `ctx.getServices()` once, the same
map ObjectQL adopts `driver.*` services from at its start, plus the
engine's `getDefaultDriverName()` for a driver registered straight on
the engine.
- A driver is present: silent.
- No driver, the `auth` service is registered, and `tenancy.posture` is
`single`: the boot is refused before any `start()`, exit 1, through
serve's existing refusal idiom (the artifact gate's: `restoreOutput()`,
a red block, a thrown `Refusing to boot: …` that the fatal handler
prints). It names three fixes: set `OS_DATABASE_URL`, mount a storage
driver in the stack's `plugins` (for example `new
DriverPlugin(driver)`), or run with `--dev`.
- No driver otherwise: one `⚠ No storage: …` line naming the same fixes,
worded as of the end of plugin init, and the boot goes on.
-
**`packages/cli/test/serve-no-storage-boot-guard.integration.test.ts`.**
Five production boots of the built CLI, one per cause (below).
- **`scripts/check-cli-test-child-env.mjs`.** The pin spawns
`bin/run.js`, so it joins that gate's pinned built-entrypoint
population, by name, the way the gate asks (one roster entry, "seven"
becomes "eight").
- **`.changeset/23573-serve-no-storage-loud.md`.** `@objectstack/cli`
patch, with the before and after.
⛔ Not touched: the `5c` blocks (PR #23469, now on `main` as `5c-ter`),
`packages/objectql`, `plugin-auth`, `packages/core`, the drivers,
`content/docs/**`.
## Measured shapes
Production posture: `node packages/cli/bin/run.js serve
objectstack.config.ts -p PORT` on the built CLI, `NODE_ENV`,
`OS_DATABASE_URL`, `DATABASE_URL` and `OS_DATABASE_DRIVER` unset,
`OS_CLOUD_URL=off`. Fixtures are throwaway `defineStack` host configs
with one object. "Before" is base `9f81260487` unless marked (8d05456
= the measurement report `6184721596`). "After" is this branch,
re-measured on the merged head `650e28a731` for the rows marked *.
| Shape | Before | After |
|---|---|---|
| R1 `examples/app-showcase`, auth secret | exit 1, `sys_organization`
(8d05456) | refused, exit 1; neither downstream line |
| R2* host fixture, auth secret | exit 1, `sys_organization` | refused,
exit 1; fixture plugin `init` ran, `start` never did |
| R3* host fixture, no secret | exit 1 at `package-service` start
(8d05456) | the line, then the same `package-service` failure |
| R4 declared `default` datasource (`autoConnect`), auth secret | exit
1, `sys_organization` | refused |
| R8 stack `AuthPlugin`, a stack plugin registers a driver on the engine
in `start()` | exit 1 (8d05456) | refused |
| R8b a stack plugin registers `driver.*` in `start()`, listed before a
stack `ObjectQLPlugin` and `AuthPlugin` | exit 1, `sys_organization` |
refused |
| R8c the same, registering on the engine | exit 1, "database refused"
(`no such table`) | refused |
| R9* plugin `com.acme.store` (no driver-like name) registers `driver.*`
in `init()`, auth secret | boots, health 200 | boots, silent |
| R9e a plugin registers on the engine in `init()`, auth secret | boots
| boots, silent |
| R10* `--no-server --preset minimal`, no secret | boots DEGRADED
(8d05456) | the line before any `start()`, then boots |
| R11* a module `onEnable` registers a driver (`ctx.drivers.register`),
no secret | boots | the line, then boots |
| R20c* stack `AuthPlugin({ membershipPolicy })` +
`OrganizationsPlugin`, `OS_TENANCY_POSTURE=group`, `--no-server --preset
minimal`, no driver anywhere | boots, DEGRADED: job | the line, then
boots |
| R12* `OS_DATABASE_URL=file:ctl.db`, auth secret | boots (8d05456) |
boots, silent |
| R13* `--dev`, auth secret | boots (8d05456) | boots, silent |
A refused boot wrote nothing to disk before the refusal: no
`.objectstack/` in the fixture or in `examples/app-showcase`, and an
empty `OS_HOME`. Its Phase-1 side effects are two `init()` log lines
(the SDUI-manifest notice, the `StorageServicePlugin` local-driver
warning).
## The ordering the guard rests on, verified
- `resolvePluginOrder` (`packages/core/src/plugin-order.ts`) is a DFS
over insertion order. The guard is inserted last and nothing declares it
as a dependency, so its `init()` is the last `init()` and precedes every
`start()`. Measured: on R2 the fixture plugin's `init` marker is on the
stream and its `start` marker is not; on R9 a driver registered in
another plugin's `init()` is seen.
- No driver registered in a `start()` reaches `AuthPlugin.start()` in
time. The flag needs declared objects, so step 3 composes `AppPlugin`,
whose `optionalDependencies` are `com.objectstack.engine.objectql`,
`com.objectstack.auth` and `com.objectstack.auth.identity-objects`. That
hoists ObjectQL and auth ahead of every stack plugin. `ObjectQLPlugin`
declares no dependencies. Measured: R8, R8b and R8c fail before this
change.
- R11's `onEnable` driver registers during `AppPlugin.start()`, after
the guard's `init()`. No auth is composed there, so it gets the line and
boots.
## Two departures from the dispatch, both measured
- **The refusal predicate is `auth` registered AND `tenancy.posture ===
'single'`, not `auth` registered alone.** R20c registers the `auth`
service, has no driver anywhere, and boots today: `AuthPlugin.start()`
reads `sys_organization` only under the `single` posture
(`auth-plugin.ts`, `start()`). Refusing on `auth` alone would refuse a
configuration that boots, which the claim forbids. The tenancy service's
`posture` is the posture in force (a degraded walled request reads
`single`), which is exactly the condition `AuthPlugin.start()` branches
on.
- **The datasource fix is spelled "mount a storage driver in the stack's
plugins (e.g. `new DriverPlugin(driver)`)", not "declare a
datasource".** R4 declares a `default` datasource with `autoConnect` and
still fails: a config-declared datasource connects in
`AppPlugin.start()`, after `AuthPlugin.start()`. Telling an R4 operator
to declare a datasource would send them to the thing they already did.
## Tests
All on the final head `650e28a731` (this branch merged with
`origin/main` `7c6b7f4a35`), after a rebuild of the CLI's dependency
closure (`turbo run build --filter='@objectstack/cli...'`, 62 tasks,
exit 0).
- **The pin and its eight neighbours** (the diff adds an
integration-tier file, so that tier is run here for these files; the
rest of it is CI's): `pnpm --filter @objectstack/cli exec vitest run
--maxWorkers=2` over
`test/serve-no-storage-boot-guard.integration.test.ts` plus
`serve-config-boot-flat-docs`, `serve-config-boot-residual`,
`serve-host-config-metadata-door`, `serve-migrate-exit-schema-parity`,
`serve-view-container-read` (`.integration`),
`serve-package-declared-capabilities`, `serve-runtime-state-project-key`
and `serve-host-config`: `Test Files 9 passed (9)`, `Tests 61 passed
(61)`. The pin alone, three runs: 23.58 s, 24.37 s, 24.68 s.
- **The unit layer:** `pnpm --filter @objectstack/cli exec vitest run
--project unit --maxWorkers=2`: `Test Files 290 passed (290)`, `Tests
4323 passed (4323)`.
- **Typecheck:** `pnpm --filter @objectstack/cli typecheck`, exit 0
(`tsc --noEmit`, then `check:test-typecheck: OK`; the pin is in the
`tsconfig.test.json` program, counted with `--listFilesOnly`).
- **Ablation of the refusal** (committed fix first; `node
scripts/ablation-replace.mjs` with its EXIT/INT/TERM restore). Mutation:
the refusal's condition prefixed with `false &&`, anchor x1 to x0, blob
`c016f7b009c1` to `139994049e2d`. `pnpm --filter @objectstack/cli
build`, then `node scripts/ablation-dist-preflight.mjs @objectstack/cli`
found the marker in 1 built file. The pin: `Tests 1 failed | 4 passed`;
the refusal leg failed on `no refusal was printed`, and the boot it read
printed `[ObjectQL] No driver available for object 'sys_organization'`
and `ERROR sys_metadata could NOT be read at boot`. Restore: blob back
to `c016f7b009c1` = HEAD, `git diff HEAD` empty; rebuild, preflight
`--absent` (absent from all 632 built files, tree clean against HEAD);
the pin `Tests 5 passed (5)`. Measured before the merge with `main`,
which touches no line of this diff.
## Gates
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `650e28a731`: 86 commands (the edit to
`scripts/check-cli-test-child-env.mjs` brings 19 of them). All 86 run,
each exit code recorded before any pipe: all 86 exit 0. `--ran`: `86
derived famil(ies) accounted for — 86 run, 0 NOT-MEASURED (a DERIVED
zero …)`.
- The edited gate's own suite: `node
scripts/check-cli-test-child-env.mjs --self-test` (152 cases pass) and
its audit (`all 179 spawn call(s) declare their child's env`), both exit
0; `node scripts/check-ratchet-remedy-authority.mjs` and its
`--self-test`, which name that script, exit 0.
- Read for this change though not derived: `pnpm
check:startup-registry-verdict` exit 0 (`none recording a verdict the
boot can contradict`), `pnpm check:init-service-contract` exit 0.
- **Lint, narrowed:** `eslint --no-inline-config --format json` over the
three changed code files, exit 0, 0 messages. The narrowing holds
because (1) the root `lint` is `eslint . --no-inline-config`, the same
flags; (2) `eslint.config.mjs` enables no type-aware linting for any
file (its own note: no `parserOptions.project`, no typed rules), so a
file's verdict depends on that file alone; (3) `--print-config` resolves
active rules for each of the three files (5, 5 and 2), so none of them
is ignored. The repo-wide `pnpm lint` is CI's.
- CI check-runs: not yet read; they start when this PR opens.
## Acceptance notes
- The `⚠ No storage:` line is worded as of the end of plugin init ("… so
objects have no store unless a plugin registers one when it starts"),
not as a verdict, because a `start()` can still register a driver (R11).
AGENTS.md "Startup registry reads" is the rule; `pnpm
check:startup-registry-verdict` is green.
- The pin is named `.integration.test.ts`, not `.e2e.test.ts`: the
`.e2e` name moves a file to the nightly run on `main` (`OS_TEST_TIERS`),
and this pin guards a boot order any `serve.ts` edit can break. Its
declared budget is 40 s (`// test-budget:` header, class "spawns the
real producer once per single-cause fixture"): the slower of two local
runs, 24.4 s on a shared container, x 1.25, rounded up. No CI reading
exists yet; the first refresh that reads one re-measures it.
- R3 (no auth secret, default preset) still dies at `package-service`
start. It now prints the line with the fixes first. Refusing it was not
in the ruling.
- Out-of-scope findings are in the `os-dev-report` comment on #23573 for
the seat to file or route; none is fixed here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b513343 commit edac218
4 files changed
Lines changed: 403 additions & 1 deletion
File tree
- .changeset
- packages/cli
- src/commands
- test
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3089 | 3089 | | |
3090 | 3090 | | |
3091 | 3091 | | |
| 3092 | + | |
| 3093 | + | |
| 3094 | + | |
| 3095 | + | |
| 3096 | + | |
| 3097 | + | |
| 3098 | + | |
3092 | 3099 | | |
3093 | 3100 | | |
3094 | 3101 | | |
| |||
3136 | 3143 | | |
3137 | 3144 | | |
3138 | 3145 | | |
| 3146 | + | |
| 3147 | + | |
3139 | 3148 | | |
3140 | 3149 | | |
3141 | 3150 | | |
| |||
5108 | 5117 | | |
5109 | 5118 | | |
5110 | 5119 | | |
| 5120 | + | |
| 5121 | + | |
| 5122 | + | |
| 5123 | + | |
| 5124 | + | |
| 5125 | + | |
| 5126 | + | |
| 5127 | + | |
| 5128 | + | |
| 5129 | + | |
| 5130 | + | |
| 5131 | + | |
| 5132 | + | |
| 5133 | + | |
| 5134 | + | |
| 5135 | + | |
| 5136 | + | |
| 5137 | + | |
| 5138 | + | |
| 5139 | + | |
| 5140 | + | |
| 5141 | + | |
| 5142 | + | |
| 5143 | + | |
| 5144 | + | |
| 5145 | + | |
| 5146 | + | |
| 5147 | + | |
| 5148 | + | |
| 5149 | + | |
| 5150 | + | |
| 5151 | + | |
| 5152 | + | |
| 5153 | + | |
| 5154 | + | |
| 5155 | + | |
| 5156 | + | |
| 5157 | + | |
| 5158 | + | |
| 5159 | + | |
| 5160 | + | |
| 5161 | + | |
| 5162 | + | |
| 5163 | + | |
| 5164 | + | |
| 5165 | + | |
| 5166 | + | |
| 5167 | + | |
| 5168 | + | |
| 5169 | + | |
| 5170 | + | |
| 5171 | + | |
| 5172 | + | |
| 5173 | + | |
| 5174 | + | |
| 5175 | + | |
| 5176 | + | |
| 5177 | + | |
| 5178 | + | |
| 5179 | + | |
| 5180 | + | |
| 5181 | + | |
5111 | 5182 | | |
5112 | 5183 | | |
5113 | 5184 | | |
| |||
0 commit comments