Skip to content

fix(cloud-connection): derive features.marketplace from what is actually mounted (#8356) - #8387

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-8356-marketplace-feature-derived
Aug 13, 2026
Merged

fix(cloud-connection): derive features.marketplace from what is actually mounted (#8356)#8387
os-zhuang merged 1 commit into
mainfrom
claude/issue-8356-marketplace-feature-derived

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#8356

RuntimeConfigPlugin built its /api/v1/runtime/config response with
marketplace: true as a literal, so every runtime that mounted the plugin told the
Console the package catalog was browsable — including runtimes where
MarketplaceProxyPlugin was never mounted because no control plane resolved. The SPA
rendered a browse affordance the runtime could not serve. Same declared-is-not-enforced
shape as #8343, one key over.

The flag is now observed per request, off the route table of the app serving the
response.

The seam — measured, not inherited

The card proposed that the proxy "registers a service or is discoverable on the kernel at
kernel:ready". It does not, and that mattered enough to change the design:

  • MarketplaceProxyPlugin registers no service. Its init says so in as many words
    ("No services registered — pure HTTP wiring during start()"). It announces itself
    only by mounting /api/v1/marketplace/* on the raw app. So there was nothing on the
    kernel to look up — and adding a registration purely to read it back would have been a
    mechanism invented for its own observation rather than a fix.
  • The IHttpServer mount-introspection members cannot see it either.
    getMountedRoutes() and resolveMountedRoute() look like the right contract, but both
    are scoped to routes registered through the adapter's own verb methods — "routes an
    adapter mounts on its framework-native handle behind getRawApp are outside this table
    by construction" (the contract's own words in packages/spec/src/contracts/http-server.ts),
    and resolveMountedRoute filters the live router's verdict back through that same
    ledger. The proxy mounts through getRawApp(), so the adapter ledger reports nothing
    for it.
  • A proxy-specific signal would under-report where it matters most. The ObjectStack
    Cloud control plane serves /api/v1/marketplace/packages*natively, from its own
    route module, with no proxy anywhere. Its own declaration of this flag already reads
    "/api/v1/marketplace/* is reachable (proxy or native)". Keying on the proxy would
    report false on the one deployment that definitely has a catalog.

The raw app's route ledger is the union of everything registered on it — adapter verb
methods and framework-native mounts alike — so it answers exactly the question the flag
claims to answer, for every distribution, with no new registration invented. Measured
against hono@4.12.34: app.routes collects verb methods, all() and use() alike.

Read per request rather than snapshotted at kernel:ready: plugin start() order
across hooks is not guaranteed, and by request time every hook has run. There is a test
for exactly that (a proxy mounted after this plugin still counts).

/api/v1/marketplace/install-local is excluded from the derivation on purpose: it is the
offline install half, mounted precisely on the runtimes that have no catalog, and counting
it as browse would recreate this defect one key over.

No config knob

Per the #8343 ACCEPT ruling of 2026-08-13, and for the reason the card gives: a
marketplace?: boolean option repeats one layer up the every-host-must-remember failure
that propagated the original defect into the self-hosted EE image, where both the host
config and this package's README kept a hand-maintained flag out of step with their own
mounting.

Both directions pinned, and the negative direction ablated

packages/cloud-connection/src/runtime-config-marketplace-derivation.test.ts — the
positive cases mount the realMarketplaceProxyPlugin / MarketplaceInstallLocalPlugin
on the shared app rather than copying their route strings, so a change to the proxy's
prefix fails the suite instead of silently flipping this flag to false in production.

Ablation (predicted before running: negative cases red, positive cases green) — reverting
the derivation to the pre-fix literal marketplace: true:

Tests 7 failed | 5 passed (12)
x reports false on a runtime with no marketplace mount at all
x the cloud-less runtime reports installLocal truthfully WITHOUT claiming browse
x broad middleware and an SPA catch-all are not evidence of a catalog
x reports false - and warns once - when the raw app exposes no route table
x is read per request, so a proxy mounted AFTER this plugin still counts
x always ships the base mechanism flags (runtime-config-plugin.test.ts)
x passes ARBITRARY distribution keys through verbatim (runtime-config-plugin.test.ts)
-> AssertionError: expected true to be false
v reports true when the REAL MarketplaceProxyPlugin is mounted on the same app
v counts a NATIVE browse mount this package never installed
v a host that knows better can still declare it through resolveFeatures

The fix was committed before the ablation and restored from that commit;
git hash-object confirms the restored file is byte-identical to the pre-ablation
state (8b878abef715e1966612083b7f1ed8f6769ea075).

Behaviour change

A runtime that mounts a marketplace browse surface reports exactly what it did before. A
runtime that mounts none now reports marketplace: false instead of true — the
correction — and its Console stops offering catalog browse it cannot serve. A cloud-less
runtime can therefore report installLocal: true truthfully without also claiming
browse, which is the constraint that kept #8343 from mounting this plugin on an air-gapped
runtime at all.

Escape hatch unchanged: the derivation is the base value, not a veto. The open-core
resolveFeatures seam still merges over it, so a host on an adapter whose raw app exposes
no route ledger — where the flag conservatively reports false, with a warning logged once
at mount time — can still declare the capability it knows it serves.

Verification

  • pnpm --filter @objectstack/cloud-connection test — 18 files, 123 tests, all passing.
  • pnpm --filter '@objectstack/cloud-connection^...' build — dependency closure built
    first (30 projects), so the run is not reading stale dist.
  • Consumer sweep, downstream direction: no package in this repo reads features.marketplace
    other than the plugin itself; the SPA consumer lives in objectui and reads it as the
    boolean it already was.
  • eslint clean on the three changed source files; check:nul-bytes,
    check:changeset-gate-self-tests, check:objectui-changeset,
    check-changeset-no-major, check:query-options-erasure, check:type-check-coverage
    all green. check:objectui-pin-fresh is red on a stale .objectui-sha — a repo-wide
    release-time gate on a file this diff does not touch (git diff origin/main -- .objectui-sha
    is empty).

Backlink: #8343. Related: #8355 (the README example, sequenced behind this) — that card
gets simpler as a result: its trap 2 ("an example that mounts RuntimeConfigPlugin
outside the cloud branch advertises browse as well") is retired here, so the corrected
example can mount RuntimeConfigPlugin unconditionally, which is what an air-gapped
runtime needs.


Generated by Claude Code

…ed (#8356)
RuntimeConfigPlugin built its /api/v1/runtime/config response with
`marketplace: true` as a literal, so every runtime mounting the plugin told
the Console the catalog was browsable -- including runtimes where
MarketplaceProxyPlugin was never mounted because no control plane resolved.
Same declared-is-not-enforced shape as #8343, one key over.
The flag is now read per request off the route table of the app serving the
response. Measured rather than assumed: the proxy registers no service (its
init says so), and IHttpServer.getMountedRoutes/resolveMountedRoute exclude
framework-native getRawApp() mounts by construction -- so the raw app's own
route ledger is the only honest seam, and it is also the one that covers the
cloud control plane, which serves the catalog natively with no proxy.
install-local routes are excluded from the derivation: they are the offline
half, mounted exactly on the runtimes with no catalog.
Both directions pinned, with the positive case mounting the REAL proxy on the
shared app rather than copying its route string.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7vaLs7bhBPi9m3JyzkhDj
@vercel

vercelBot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 13, 2026 10:50am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection.

1 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/protocol/kernel/metadata-service.mdx(via @objectstack/cloud-connection)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/cloud-connection)

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.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 13, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 13, 2026 11:28
@os-zhuang
os-zhuang added this pull request to the merge queueAug 13, 2026
Merged via the queue into main with commit 631ddbfAug 13, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8356-marketplace-feature-derived branch August 13, 2026 11:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted

2 participants

@os-zhuang@claude