Repository navigation
fix(cli): a production serve with no storage says so before any plugin starts, and refuses when its auth service needs a driver - #23631
Conversation
… starts A production host-config `serve` with no recognized OS_DATABASE_URL and no driver plugin declared nothing at the storage step and said nothing; the boot then died in AuthPlugin.start with a driver error, after an objectql ERROR naming a datasource nobody declared. The storage step now records that it resolved nothing, and one CLI plugin registered last judges at the end of Phase 1: a driver present is silent; none with the auth service under the 'single' posture refuses before any start(), naming the remedies; none otherwise prints one line and boots on. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…udget Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…slower reading Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…y name check:cli-test-child-env pins the set of files that spawn bin/run.js, so the pin joins it in the same change, and its controls keep each child's env a literal at the spawn call the gate reads NODE_ENV from. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…rve-no-storage-loud
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
席位核对✅ ACCEPT: PR #23631 at
Checklist, read against GitHub and
Accepted deviations:
Filed from this report:
Not taken (the seat's ruling): option E, converging host-config Landing: Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 38704714962 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes #23573
Clause-②: no
What this changes
A production
os serveof a host config (one whosepluginshold plugin instances) with no recognizedOS_DATABASE_URL, no--devand no plugin named like a driver declared no default datasource at the storage step, and said nothing. The boot then died inAuthPlugin.start()withNo 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 recordsproductionResolvedNoStoragewhenresolveStorageDefinitionanswersnull. 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: aftercom.objectstack.cli.seed-settlement-announcer, immediately beforeruntime.start(). Itsinit()readsctx.getServices()once, the same map ObjectQL adoptsdriver.*services from at its start, plus the engine'sgetDefaultDriverName()for a driver registered straight on the engine.authservice is registered, andtenancy.postureissingle: the boot is refused before anystart(), exit 1, through serve's existing refusal idiom (the artifact gate's:restoreOutput(), a red block, a thrownRefusing to boot: …that the fatal handler prints). It names three fixes: setOS_DATABASE_URL, mount a storage driver in the stack'splugins(for examplenew DriverPlugin(driver)), or run with--dev.⚠ 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 spawnsbin/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/clipatch, with the before and after.⛔ Not touched: the
5cblocks (PR #23469, now onmainas5c-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 PORTon the built CLI,NODE_ENV,OS_DATABASE_URL,DATABASE_URLandOS_DATABASE_DRIVERunset,OS_CLOUD_URL=off. Fixtures are throwawaydefineStackhost configs with one object. "Before" is base9f81260487unless marked (8d05456 = the measurement report6184721596). "After" is this branch, re-measured on the merged head650e28a731for the rows marked *.examples/app-showcase, auth secretsys_organization(8d05456)sys_organizationinitran,startnever didpackage-servicestart (8d05456)package-servicefailuredefaultdatasource (autoConnect), auth secretsys_organizationAuthPlugin, a stack plugin registers a driver on the engine instart()driver.*instart(), listed before a stackObjectQLPluginandAuthPluginsys_organizationno such table)com.acme.store(no driver-like name) registersdriver.*ininit(), auth secretinit(), auth secret--no-server --preset minimal, no secretstart(), then bootsonEnableregisters a driver (ctx.drivers.register), no secretAuthPlugin({ membershipPolicy })+OrganizationsPlugin,OS_TENANCY_POSTURE=group,--no-server --preset minimal, no driver anywhereOS_DATABASE_URL=file:ctl.db, auth secret--dev, auth secretA refused boot wrote nothing to disk before the refusal: no
.objectstack/in the fixture or inexamples/app-showcase, and an emptyOS_HOME. Its Phase-1 side effects are twoinit()log lines (the SDUI-manifest notice, theStorageServicePluginlocal-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 itsinit()is the lastinit()and precedes everystart(). Measured: on R2 the fixture plugin'sinitmarker is on the stream and itsstartmarker is not; on R9 a driver registered in another plugin'sinit()is seen.start()reachesAuthPlugin.start()in time. The flag needs declared objects, so step 3 composesAppPlugin, whoseoptionalDependenciesarecom.objectstack.engine.objectql,com.objectstack.authandcom.objectstack.auth.identity-objects. That hoists ObjectQL and auth ahead of every stack plugin.ObjectQLPlugindeclares no dependencies. Measured: R8, R8b and R8c fail before this change.onEnabledriver registers duringAppPlugin.start(), after the guard'sinit(). No auth is composed there, so it gets the line and boots.Two departures from the dispatch, both measured
authregistered ANDtenancy.posture === 'single', notauthregistered alone. R20c registers theauthservice, has no driver anywhere, and boots today:AuthPlugin.start()readssys_organizationonly under thesingleposture (auth-plugin.ts,start()). Refusing onauthalone would refuse a configuration that boots, which the claim forbids. The tenancy service'spostureis the posture in force (a degraded walled request readssingle), which is exactly the conditionAuthPlugin.start()branches on.new DriverPlugin(driver))", not "declare a datasource". R4 declares adefaultdatasource withautoConnectand still fails: a config-declared datasource connects inAppPlugin.start(), afterAuthPlugin.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 withorigin/main7c6b7f4a35), after a rebuild of the CLI's dependency closure (turbo run build --filter='@objectstack/cli...', 62 tasks, exit 0).pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2overtest/serve-no-storage-boot-guard.integration.test.tsplusserve-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-keyandserve-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.pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2:Test Files 290 passed (290),Tests 4323 passed (4323).pnpm --filter @objectstack/cli typecheck, exit 0 (tsc --noEmit, thencheck:test-typecheck: OK; the pin is in thetsconfig.test.jsonprogram, counted with--listFilesOnly).node scripts/ablation-replace.mjswith its EXIT/INT/TERM restore). Mutation: the refusal's condition prefixed withfalse &&, anchor x1 to x0, blobc016f7b009c1to139994049e2d.pnpm --filter @objectstack/cli build, thennode scripts/ablation-dist-preflight.mjs @objectstack/clifound the marker in 1 built file. The pin:Tests 1 failed | 4 passed; the refusal leg failed onno refusal was printed, and the boot it read printed[ObjectQL] No driver available for object 'sys_organization'andERROR sys_metadata could NOT be read at boot. Restore: blob back toc016f7b009c1= HEAD,git diff HEADempty; rebuild, preflight--absent(absent from all 632 built files, tree clean against HEAD); the pinTests 5 passed (5). Measured before the merge withmain, which touches no line of this diff.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon650e28a731: 86 commands (the edit toscripts/check-cli-test-child-env.mjsbrings 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 …).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.mjsand its--self-test, which name that script, exit 0.pnpm check:startup-registry-verdictexit 0 (none recording a verdict the boot can contradict),pnpm check:init-service-contractexit 0.eslint --no-inline-config --format jsonover the three changed code files, exit 0, 0 messages. The narrowing holds because (1) the rootlintiseslint . --no-inline-config, the same flags; (2)eslint.config.mjsenables no type-aware linting for any file (its own note: noparserOptions.project, no typed rules), so a file's verdict depends on that file alone; (3)--print-configresolves active rules for each of the three files (5, 5 and 2), so none of them is ignored. The repo-widepnpm lintis CI's.Acceptance notes
⚠ 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 astart()can still register a driver (R11). AGENTS.md "Startup registry reads" is the rule;pnpm check:startup-registry-verdictis green..integration.test.ts, not.e2e.test.ts: the.e2ename moves a file to the nightly run onmain(OS_TEST_TIERS), and this pin guards a boot order anyserve.tsedit 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.package-servicestart. It now prints the line with the fixes first. Refusing it was not in the ruling.os-dev-reportcomment on cli/serve: a productionobjectstack servewith no OS_DATABASE_URL registers no driver in silence, then fails at AuthPlugin.start with "No driver available for object 'sys_organization'" and a remedy naming a datasource nobody declared #23573 for the seat to file or route; none is fixed here.Generated by Claude Code