Uh oh!
There was an error while loading. Please reload this page.
fix(cli): honour host-wired marketplace plugins on the cloud-connected serve arm (#8357) - #8646
Conversation
…d serve arm (#8357) `objectstack serve` auto-wires MarketplaceProxyPlugin, MarketplaceInstallLocalPlugin, the same-origin cloud-connection surface and RuntimeConfigPlugin whenever `resolveCloudUrl()` resolves. Those four mounts were unconditional: the block never asked whether the loaded host config had already wired any of them. Each mount is now guarded on its own `Serve.providesCapability` presence check -- the idiom the offline arm has carried since #8343 -- so CLI auto-wiring is a fallback for hosts that wire nothing rather than a second opinion about a surface the host already composed. Two new identity registries (MARKETPLACE_PROXY_IDENTITIES, CLOUD_CONNECTION_IDENTITIES) join the two the offline arm already declared, and `planMarketplaceWiring` reports one flag per surface so a host composing any SUBSET keeps exactly what it wired. Measured correction to the card's premise: the CLI's block runs several hundred lines BEFORE `config.plugins` are registered, and `Kernel.use` overwrites by name, so today the HOST's instance is the survivor -- the CLI's four are constructed, registered and dropped. This is therefore precedence restoration, not a live-bug fix: the host winning stops being an accident of block ordering that nothing states or pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7vaLs7bhBPi9m3JyzkhDj
…8357) Reads the surviving instance off the kernel rather than trusting the planner alone, with every host fixture constructed from an argument the CLI cannot produce so the two instances are distinguishable at all. Also records a measured gap found while writing the drift guard: esbuild rewrites a class that references itself by name into `class _X`, so the BUILT MarketplaceProxyPlugin reports `_MarketplaceProxyPlugin` and the class-name limb of the identity registry never matches it. The registered-name limb carries the guard; filed separately rather than fixed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P7vaLs7bhBPi9m3JyzkhDj
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8357
What changed
objectstack serveauto-wires four surfaces wheneverresolveCloudUrl()resolves and the kernel is not a runtime host kernel:MarketplaceProxyPlugin,MarketplaceInstallLocalPlugin, the same-origin cloud-connection surface, andRuntimeConfigPlugin. All four mounts were unconditional — the block never asked what the loaded host config had already wired.Each is now guarded on its own
Serve.providesCapabilitypresence check, the idiom the offline arm has carried since the install-local change in #8343.planMarketplaceWiringreports one flag per surface (cloudProxy,cloudInstallLocal,cloudConnection,cloudRuntimeConfig), so a host composing any subset keeps exactly what it wired and the CLI supplies only the rest. Two new identity registries —MARKETPLACE_PROXY_IDENTITIESandCLOUD_CONNECTION_IDENTITIES— join the two the offline arm already declared.cloudSurfaceskeeps its name and its existing assertions, with its meaning tightened in the doc comment: it is the arm selector (does this boot take the cloud-connected arm at all), not a mount decision.Per the grading round,
Kernel.use()is not touched. Making it refuse or warn on a duplicate name was the card's alternative option and was explicitly rejected there — engine-core blast radius, and its own card if anyone wants it.The sentinel trap the card documents is avoided: the check keys off each plugin's own identity, never off
ObjectOSEnvironmentPlugin. The EE single-environment branch — the host this card is about — never constructs one; only theOS_MULTI_TENANTbranch does, viacreateObjectOSStack. A rule hung off that sentinel would not fire for the exact host it was written for.Premise re-verification — one part of the card is inverted
#8526 (artifact-pinned boot) landed in
serve.tsafter this card was written, so everything below was re-measured on this branch rather than taken from the card.Confirmed as stated:
ObjectKernel.use()isthis.plugins.set(pluginMeta.name, meta)(packages/core/src/kernel.ts) — a Map keyed byplugin.name. No error, no dedupe, last write wins.PluginLoader.loadPluginhas no duplicate check either.Serve.providesCapabilitycheck is still there to mirror.Not confirmed — the direction of the replacement. The CLI's marketplace block runs at
serve.ts:1957; the loop that registersconfig.pluginsruns atserve.ts:2474, roughly five hundred lines later. Since the latersetwins, today the host's instance is the survivor and the CLI's four are constructed, registered, and dropped. The card's "a host that mounts its own therefore has it replaced by the CLI's instance" is backwards on this tree.That does not weaken the graded scope, it sharpens it. The host winning is an accident of where two blocks sit in one 4000-line file, neither of which mentions the other; nothing states it and nothing pinned it. It inverts silently if either block moves, and on a kernel whose
userejects duplicates (LiteKernelthrows) it is the host's registration that fails instead. This change makes the outcome independent of all of that, and stops the CLI constructing four plugins it is about to discard.Severity — read this before reviewing it as a bug fix
There is no measured user impact today, and the card says so. Both sides currently construct their instances from the same
resolveCloudUrl()value, so the mounts are interchangeable; combined with the ordering above, nothing observable differs. This is precedence and config-authority restoration, not a live-bug fix. It becomes real the moment a host passes an argument the CLI cannot: a private control plane, a custom installstorageDir, a credential path, white-label branding.The test, and why it is shaped the way it is
Because both sides build identical instances today, the naive test — "does the app still boot?" — passes before and after and proves nothing. Every host fixture in
packages/cli/test/serve-marketplace-cloud-host-precedence.test.tsis therefore constructed with something the CLI's auto-wiring cannot produce, and each case asserts the survivor carries the host's value:controlPlaneUrl: https://control-plane.internal.example+cacheMaxEntries: 4096storageDir: /srv/objectos/installed-packagescredentialPath: /srv/objectos/cloud-connection.jsonproductName: Contoso Operations(white-label branding)19 tests over three layers: the planner rule per surface and for the whole EE shape; drift guards that the declared identities still match the real classes; and a kernel layer that registers into a real
ObjectKerneland reads back which instance survived. A precondition assertion pairs with each identity check, so if a construction argument is ever renamed the test goes red rather than vacuously green.The kernel layer runs both registration orders on purpose.
cli-then-hostis the orderserve.tsreally runs in — and it passes with or without this change, so on its own it cannot tell a fixed build from a broken one.host-then-cliis the order in which the missing guard bites. Passing both is the property being claimed: the host's composition wins because it is a rule, not because of block placement.Reverse verification
Ablation: replace the four
!Serve.providesCapability(...)guards in the cloud branch with literaltrue— the pre-change behaviour, with the flag shape kept so the tests still compile. The fix was committed first, so the restore came out of a real commit.Predicted before running (recorded ahead of the run, and deliberately not the template's presumption that everything goes red): 6 red, 13 green — the four per-surface tests, the whole-EE-shape test, and
host-then-cli.cli-then-hostpredicted to stay GREEN, because the host already wins in that order without any guard; that green is the evidence for the inverted direction above.Measured: exactly that split — 6 failed, 13 passed,
cli-then-hostgreen andhost-then-clired:Restored with
git restore --source=HEAD,git status --porcelainempty.Verification
pnpm --filter @objectstack/cli typecheck— cleanpnpm --filter @objectstack/cli exec vitest run— 120 files, 1316 tests, all passingpnpm check:type-check-debt(--self-test && --re-measure, the strict job CI runs) — OK, 33 ledger entries re-measured, none above its recorded number. No ledger entry was raised. The only note is a pre-existing surplus in@objectstack/lint(records 20, measures 19), untouched by this branch.scripts/pm/dispatch-gates.mjs, which surfaced several the dispatch prompt did not name; all run and green:check:changeset-gate-self-tests,check:objectui-changeset,check:cross-package-test-inputs,check:query-options-erasure,check:type-check-coverage,check:nul-bytes, pluscheck-adr-0087-registration.mjs,check-changeset-no-major.mjs,check-empty-changeset.mjs.Changeset:
.changeset/cloud-arm-host-marketplace-precedence.md(patch,@objectstack/cli).Out of scope — filed as its own finding, not changed here
#8645 remains open and is not addressed by this PR. While writing the drift guard, the built
MarketplaceProxyPluginmeasured as_MarketplaceProxyPlugin: esbuild renames a class that references itself by name, so the class-name limb of these identity registries matches nothing against the shipped package for that plugin. The registered-name limb carries every guard, so there is no impact and the change here is correct as landed, but the redundancy is dead and the existing drift test cannot see it (it compares the registry against its own spec string, never againstCtor.name). The new test compares modulo one leading underscore and says why.Release freeze
Opened as a draft and staying that way. No auto-merge armed, not enqueued.
Generated by Claude Code