Skip to content

fix(cli): a --no-server boot takes the datasource admin routes' own degrade line, not an ordering fault - #24132

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24022-datasource-admin-no-server
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24022-datasource-admin-no-server

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24022

Clause-②: no

What was wrong

On a boot that composes no HTTP server, the boot diagnostics printed:

WARN [datasource-admin] route registration failed: [Kernel] Service 'http.server' not found (while plugin 'com.objectstack.cli.datasource-admin-routes' was initializing — a composition/ordering fault; registration order is not a contract.)

Nothing was at fault. --no-server composes no server on purpose. The plugin read the server with ctx.getService?.('http.server') ?? ctx.getService?.('http-server'). The kernel's getService throws on a name nothing registered (packages/core/src/kernel.ts, the [Kernel] Service '...' not found throw, with the init-order suffix from describeInitOrderFault in plugin-order.ts). So the ?? never ran. The throw landed in the registration catch, and the plugin's own degrade line was unreachable.

The fix

packages/cli/src/commands/serve.ts only, in two plugins: com.objectstack.cli.datasource-admin-routes, and its sibling com.objectstack.cli.unknown-hostname-guard:

  • The plugin moves out of run() into an exported factory, createDatasourceAdminRoutesPlugin(registerRoutes), next to createUnknownHostnameGuardPlugin. That makes its init reachable from a test against a real kernel. run() now calls kernel.use(createDatasourceAdminRoutesPlugin(registerDatasourceAdminRoutes)). Name, version and optionalDependencies are unchanged.
  • init reads the server through the existing readComposedHttpServer (reused, not changed): http.server only, inside a try. No server: the plugin's own line, [datasource-admin] http.server unavailable; REST routes not installed. A server composed and registration throws: route registration failed with the error, as before.
  • The old fallback to the deprecated http-server alias was never reached: the canonical read threw first. Every in-repo producer of http-server (the Hono server plugin and the http-conformance node plugin) also registers http.server, the same instance. So every boot that mounted these routes before still mounts them.
  • The unknown-hostname guard (createUnknownHostnameGuardPlugin, composed when OS_ROOT_DOMAIN is set) had the same trap, so its init now reads the server through readComposedHttpServer too. It is a one-line hunk. The read stays inside the existing try, and readComposedHttpServer never throws. With no server, the boot now takes [unknown-hostname-guard] http.server unavailable; guard not installed. On a composed server whose install throws, it still warns install failed with the error. The claim's file surface was widened for this in comment 6228552643.
  • scripts/slot-lookup-baseline.json: serve.ts 9 to 6, because both untyped two-lookup chains are gone (check:slot-lookup holds at 101 sites in 25 files).

Pins

  • packages/cli/src/commands/serve-datasource-admin-routes.test.ts (unit tier, new). The plugin runs inside a real ObjectKernel, so the throw it has to survive is the kernel's own:
    1. premise: the kernel throws for an unregistered http.server;
    2. no server: no route registration failed, no ordering fault, exactly one [datasource-admin] line;
    3. only http.server is asked for, never the alias;
    4. CONTROL: a composed server gets the real routes, all under /api/v1/datasources, and no [datasource-admin] line;
    5. CONTROL: a composed server whose registration throws still warns route registration failed, naming the error;
    6. source pin, comments masked: run() mounts through the factory, and no second inline copy exists.
  • packages/cli/src/commands/serve-no-server-banner.integration.test.ts (integration tier), against the built CLI:
    • shapes 1 and 2 (production --no-server --preset minimal, and --dev --no-server) carry exactly one [datasource-admin] line, and it does not report a failed registration or an ordering fault;
    • shape 3 (a default boot) is the CONTROL: /api/v1/datasources is not 404, while an unmounted sibling path is 404, and no [datasource-admin] line prints;
    • shape 4 (--no-server with a config-mounted HonoServerPlugin) has the same control. It backs the changeset's claim that the routes still mount on that shape. This round adds it.
  • packages/cli/src/commands/serve-unknown-hostname-guard.test.ts (unit tier, the guard's existing file):
    1. a real ObjectKernel boot with no server: no install failed, no ordering fault, and exactly one [unknown-hostname-guard] line, the degrade line;
    2. CONTROL: a real kernel boot with a composed server, ordered ahead by optionalDependencies, logs installed, prints no [unknown-hostname-guard] warning, and refuses an unmapped host with 404 ENVIRONMENT_NOT_FOUND;
    3. CONTROL: a usable server whose raw app refuses the middleware still warns install failed, naming the error;
    4. the existing throwing service-resolution case now expects the degrade line instead of install failed. The kernel throws on an unregistered name, so a throwing read IS the no-server boot.

Verification, all at head 6c33626a8

  • Unit tier: pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 reads 309 files, 4591 tests, all passed, exit 0.
  • Integration, the touched file: pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 src/commands/serve-no-server-banner.integration.test.ts reads 4 of 4 passed, exit 0. The other integration-tier files are declared to CI.
  • pnpm --filter @objectstack/cli typecheck exits 0, check:test-typecheck included. tsc --noEmit --listFiles compiles all three touched test files.
  • pnpm exec eslint --no-inline-config on the four changed .ts files exits 0 with no output. The .md and .json files are outside eslint's configured file set.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 75 commands on 6c33626a8, and all 75 exit 0. --ran reads 75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN.
    • The derivation warns that the tree is 3 commits behind origin/main c1c0a9094, where 3 family files changed: check-route-envelope.mjs, engine-double-contract.pinned.json and one scripts/adr-anchors file. Those families ran the branch's copies. CI's merge-ref run is the reading on main's copies.
    • Also run, outside the derived set: check:startup-registry-verdict (exit 0, 44 seams, none recording a verdict the boot can contradict).
  • At the door, against the built CLI, with OS_ROOT_DOMAIN=example.test:
    • os serve objectstack.config.ts --no-server --preset minimal prints WARN [unknown-hostname-guard] http.server unavailable; guard not installed and WARN [datasource-admin] http.server unavailable; REST routes not installed, and no ordering fault;
    • a default boot, and a --no-server boot whose config mounts its own HonoServerPlugin, each log INFO [unknown-hostname-guard] installed, with no guard warning.

Ablation, round 1 (the datasource admin routes, at a12bf2f9b)

The fix was committed first. The mutation, done with scripts/ablation-replace.mjs, puts the pre-fix read back: the throwing getService chain, inside the try. The anchor count went from 1 to 0, and the blob went from a48fc3da93e4 to 95b1a98c5f93.

  • Mutated leg: the CLI was rebuilt, and ablation-dist-preflight @objectstack/cli 'readComposedHttpServer(ctx)' --absent passed.
    • The unit file reads 1 failed, 5 passed. The failure is takes its own degrade line.
    • The integration file reads 2 failed, 2 passed. Shapes 1 and 2 failed, each showing the card's exact route registration failed ... a composition/ordering fault line. The two controls stayed green, as they should.
  • Restore leg: blob back to the HEAD blob, git diff HEAD empty, git status --porcelain empty. The CLI was rebuilt, and the preflight found the marker present again. Unit 6 of 6 and integration 4 of 4 passed.
  • Unit case 3 (only http.server asked) stays green under the mutation, because the old chain also never reached the alias once the canonical read threw. It guards against an alias read coming back. It does not discriminate this card's defect.

Ablation, round 2 (the guard)

The fix was committed first (6c33626a8). scripts/ablation-replace.mjs put the guard's pre-fix line back. The anchor count went from 1 to 0, and the blob went from a080f01a90c5 to a48fc3da93e4, which is byte-identical to the previous head's serve.ts. The test imports its subject from source (./serve.js), so no rebuild sits in the path.

  • Mutated leg: the guard file reads 2 failed, 36 passed. Both failures are the cases that read a throwing service as no server: the real-kernel no-server boot, which received the exact [unknown-hostname-guard] install failed ... a composition/ordering fault line, and the throwing-resolution case. Both controls stayed green.
  • Restore leg: blob back to the HEAD blob, git diff HEAD empty, git status --porcelain empty. 38 of 38 passed.

Handover

This resumes the released claim's branch at 4b0a631575. Every commit was re-read hunk by hunk:

  • kept 7177b4e8c (the fix, and the new unit file, which landed in this commit, not in 2e52e010a), 2e52e010a (the integration cases) and 16be28cdd (the baseline row, re-measured after this round's merge of origin/main 50ccfa669);
  • corrected acd285a2b: the changeset's alias sentence now says the fallback never ran, instead of reading like a narrowing;
  • added the shape 4 control.

All verification above was re-run in this round.

Acceptance notes

  • The new unit file imports serve.ts, and on this shared box that import costs about 18.8 s (tests 14 ms). A sibling that imports the same module (serve-unknown-hostname-guard.test.ts) measured the same in one run, 36.6 s of import across the two files. No budget line is declared, since none of the three budget classes fits. This is a reading, not a gate.
  • origin/main moved to c1c0a9094 after this branch's merge. The 3 commits touch packages/rest, plugin-email, qa/dogfood, runtime tests and spec/liveness, none of them under packages/cli. git merge-tree is clean, so they were not merged.
  • packages/runtime/README.md shows a server plugin registering only the deprecated http-server alias. Docs drift, noted here, not filed.

Generated by Claude Code

…ering fault

The datasource admin routes plugin read the HTTP server with
getService?.('http.server') ?? getService?.('http-server'). The kernel's
getService throws on a name nothing registered, so on a boot with no server
the ?? never ran and the throw landed in the registration catch, which
printed "route registration failed" with the kernel's
"composition/ordering fault" text when nothing was at fault.

The plugin moves to an exported factory, createDatasourceAdminRoutesPlugin,
and reads the server through readComposedHttpServer (http.server only, in a
try). A boot with no server now takes the plugin's own degrade line; a
composed boot whose registration genuinely throws still warns "route
registration failed".

Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp
Co-authored-by: Claude <noreply@anthropic.com>
…boots

The production and --dev --no-server boots of serve-no-server-banner carry
exactly one [datasource-admin] line and none reporting a failed registration
or an ordering fault; the default production boot (the control) still mounts
/api/v1/datasources (not 404, while an unmounted sibling path is) and prints
no [datasource-admin] line.

Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp
Co-authored-by: Claude <noreply@anthropic.com>
The datasource admin routes plugin no longer reads the server through the
untyped getService?.() ?? getService?.() chain.

Claude-Session: https://claude.ai/code/session_01PFB9ZGRWYvjhTXsd8i6KBp
Co-authored-by: Claude <noreply@anthropic.com>
… under --no-server

The changeset says a --no-server boot whose config mounts its own
HonoServerPlugin still mounts /api/v1/datasources. Shape 4 of the banner
integration file now measures it: the sibling path answers 404, the routes
path does not, and no [datasource-admin] line prints. The changeset's alias
sentence now says the old http-server fallback never ran, so no boot that
mounted the routes before stops mounting them.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVm1pAXVXQ3PjC42j1kPFf
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 17, 2026
@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 6 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via /api/v1/datasources (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/data-modeling/external-datasources.mdx (via /api/v1/datasources (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/kernel/cluster.mdx (via /api/v1/datasources (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/plugins/index.mdx (via http.server (literal, a string literal in Serve; a string literal in createUnknownHostnameGuardPlugin))
  • content/docs/protocol/kernel/error-handling.mdx (via /api/v1/datasources (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via /api/v1/datasources (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-6.mdx (via /api/v1/datasources (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-7.mdx (via /api/v1/datasources (route, a path literal in a comment in Serve; a path literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: os serve (command, 33 pages), /api/v1 (route, 96 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 29 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c1c0a90942ff702ee6a5b40d0382808f6b2b154c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from feccac428fa0f192aedc6b65f3c4c4188bfeddbc — the merge of head 6c33626a8df327db1be7cdf94c11ca8508fc5c87 into base c1c0a90942ff702ee6a5b40d0382808f6b2b154c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin feccac428fa0f192aedc6b65f3c4c4188bfeddbc && git checkout feccac428fa0f192aedc6b65f3c4c4188bfeddbc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c1c0a90942ff702ee6a5b40d0382808f6b2b154c 6c33626a8df327db1be7cdf94c11ca8508fc5c87 && git checkout -B drift-repro c1c0a90942ff702ee6a5b40d0382808f6b2b154c && git merge --no-ff 6c33626a8df327db1be7cdf94c11ca8508fc5c87

node scripts/docs-audit/affected-docs.mjs --json c1c0a90942ff702ee6a5b40d0382808f6b2b154c

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c1c0a90942ff702ee6a5b40d0382808f6b2b154c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ard ordering fault

The unknown-hostname guard read the HTTP server with
getService?.('http.server') ?? getService?.('http-server'), the same trap as
the datasource admin routes: the kernel's getService throws on a name nothing
registered, so on a boot with no server (OS_ROOT_DOMAIN set, --no-server) the
throw landed in the install catch and printed "install failed" with the
kernel's "composition/ordering fault" text. init now reads the server through
readComposedHttpServer, so that boot takes the guard's own degrade line, and
an install that throws on a composed server still warns "install failed".

Pins in serve-unknown-hostname-guard.test.ts: a real ObjectKernel boot with no
server (the degrade line once, no "install failed", no "ordering fault"), its
composed control (installed, and refusing an unmapped host), and an install
failure on a usable server still warning "install failed". The throwing
service-resolution case now expects the degrade line. slot-lookup: serve.ts
7 -> 6. The changeset names the guard's line too.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVm1pAXVXQ3PjC42j1kPFf
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · domain:cli seat 1 · os-sales · session session_01BVm1pAXVXQ3PjC42j1kPFf · 2026-10-17T03:08Z

Head 6c33626a8. This is the seat's review of record for #24022 (claim 6228120195, amendment 6228552643), read against GitHub and origin/main, ⛔ not against the reports (6228522842 and the patch-round report).

  • Form: draft → main. The first line is Fixes #24022, the body's only closing keyword (scanned after the seat's body edit). Clause-②: no is line-initial and matches the claim.
  • Premise, measured: readComposedHttpServer's docblock on origin/main (serve.ts, above :546) records that getService throws on an unregistered name; the PR pins it on a real ObjectKernel. Both producers of the http-server alias also register http.server, the same instance (plugin-hono-server/src/hono-plugin.ts :286 / :289; qa/http-conformance/src/node-plugin.ts :40 / :41). So dropping the alias read changes no mount.
  • Surface: 6 files, all inside the claim plus amendment. The serve.ts hunks are the datasource-admin plugin moving into createDatasourceAdminRoutesPlugin (same name, version, optionalDependencies) and the guard's one-line read. readComposedHttpServer is unchanged.
    • Published surface: the @objectstack/cli entry re-exports only default as ServeCommand from serve.js (packages/cli/src/index.ts:19). The new named export is not reachable from any entry in package.json exports, so Clause-②: no stands.
  • Family: on origin/main the seat's grep (getService?.('http[.-]server') ??, non-test packages/**/*.ts) hits 3 lines: readComposedHttpServer itself, and this PR's two plugins. After this PR only the first remains (the dev's reading on the head).
  • Changeset (one @objectstack/cli patch), sentence by sentence against the diff:
    • "no longer reports an ordering fault for the datasource admin routes or for the unknown-hostname guard": both hunks. ✓
    • "Each now prints its own degrade line": both strings match the init bodies. ✓
    • "Both plugins now read only the canonical http.server service": readComposedHttpServer reads http.server only. ✓
    • "Their old fallback … never ran": the throw comes before the ??. ✓
  • Tests: both ablations recorded with both legs.
    • Round 1, the datasource plugin, rebuilt: unit 1 failed, integration 2 failed, each showing the card's exact line.
    • Round 2, the guard, from source: 2 failed.
    • The controls stayed green in both. The refusal control asserts 404 and ENVIRONMENT_NOT_FOUND.
  • Gates, pinned to 6c33626a8: Lint & Repo Gates success; TypeScript Type Check success; the rest success or skipped. At this record, the four checks that read the PR body are re-running after the seat's body edit. Landing waits for every check on the head to be green.
  • Accepted deviations:
    • (a) The branch is 3 commits behind main. merge-tree is clean, and none of them touches packages/cli; the merge ref is CI's reading.
    • (b) The guard ablation ran without a rebuild, because its test imports ./serve.js from source.
    • (c) The seat wrote the patch-round edits into the PR body from the dev's report: a read-modify-write of the live body, 10,455 bytes read back identical.
  • Out of scope: the sibling-plugin finding was fixed in place under the amendment, so none is open. The packages/runtime/README.md alias line is docs drift: noted, not filed. The docs-drift bot's pages: no page under content/docs or apps/docs quotes any of the changed log lines (0 hits; the control api/v1/datasources hits external-datasources.mdx twice).
  • Landing: not governed (check-governed-merges --pr 24132: 0 of 6 paths, 446 lines ≤ 3000). The PR goes ready with auto-merge armed once every check on the head is green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants