Repository navigation
RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356
Description
Activity
Triage: lands in
packages/cloud-connection/src/runtime-config-plugin.ts→domain:cli(cloud-connectionfamily). →pm:queue, type Bug — declared ≠ enforced:features.marketplaceis a literaltrueregardless of whether a browse surface is mounted.Not escalated as a decision card: the direction is already settled on the record — the #8343 ACCEPT ruling (2026-08-13 08:30Z) adopted option A "fix the flag at its source under #8356, deriving features.marketplace from what is actually mounted", explicitly judged not-maintainer-floor (widens no contract, restores declared = enforced). The card's own acceptance list is the spec: mounted proxy reports true, no proxy reports false, install-local reportable without claiming browse — with pins in both directions.
Note for the lane seat: prefer presence-derivation over a config knob, per the card's argument and the adopted ruling; a knob repeats the every-host-must-remember failure that produced the P1.
Size/model suggestion: M,
mode:subagent,model: opus.
Generated by Claude Code
Claim: PM loop round 1 (
domain:cliseat #6024)
Session:session_01P7vaLs7bhBPi9m3JyzkhDj
Branch:claude/issue-8356-marketplace-feature-derived
Worktree:objectstack-issue-8356
Domain:domain:cli
File surface:packages/cloud-connection/src/runtime-config-plugin.ts+ tests in the same package. ⛔ NOTpackages/cloud-connection/README.md— that is #8355, held behind this card and re-priced on your result. (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus
Serial constraints cleared: same-package #8355 (README) is held, not in flight — deliberately sequenced behind this card because its corrected example's content depends on what you land here. No other in-flight claim touchespackages/cloud-connection; open PRs #8377 / #8369 / #8342 / #8365 / #8363 / #8325 / #8302 / #7996 checked, none touches this package.Premise re-verified on
origin/mainbefore dispatch —runtime-config-plugin.ts:289ismarketplace: true, a literal, exactly as the card states.Direction is settled, not open. The #8343 ACCEPT ruling (2026-08-13 08:30Z) adopted option A: fix the flag at its source here, deriving
features.marketplacefrom what is actually mounted. Judged not-maintainer-floor — it widens no contract and restores declared = enforced. ⛔ Do not add amarketplace?: booleanconfig knob: that repeats one layer up the every-host-must-remember failure that propagated the original P1 into the EE image.Acceptance is the card's own list: mounted proxy reports
true; no proxy reportsfalse; a cloud-less runtime can reportinstallLocal: truetruthfully without also claiming browse; both directions pinned.⚠️ Verify the seam, do not inherit it. The card proposes the proxy "registers a service or is discoverable on the kernel atkernel:ready", and notes the handler already reads the kernel viaenv-registry. That is the filer's reading, not a measurement — go look at howMarketplaceProxyPluginactually announces itself and derive from whatever is really observable at response time. If no honest presence signal exists, say so and report the fork rather than manufacturing one; inventing a registration just to read it back is a mechanism, not a fix.
Generated by Claude Code
{ "issue": 8356, "status": "done", "branch": "claude/issue-8356-marketplace-feature-derived", "pr": "https://github.com/objectstack-ai/objectstack/pull/8387", "premise_still_valid": true, "summary": "The defect premise held exactly as filed: runtime-config-plugin.ts:289 was a literal `marketplace: true`, and it is now derived per request from the route table of the app serving the response — true when a marketplace browse surface is mounted on it, false when none is, with /api/v1/marketplace/install-local deliberately excluded so the offline half is never mistaken for a catalog. The card's proposed MECHANISM is falsified, and that changed the design: MarketplaceProxyPlugin registers NO service (its init says so in as many words — 'No services registered — pure HTTP wiring during start()'), and the IHttpServer mount-introspection members (getMountedRoutes / resolveMountedRoute) exclude framework-native getRawApp() mounts by construction, per the contract's own wording — so there was no kernel registration to read and no adapter ledger that could see the proxy. The raw app's own route ledger was chosen because it is the only honest observable AND the only one that also covers the ObjectStack Cloud control plane, which serves the catalog natively with no proxy at all (cloud's own declaration of this flag already reads '/api/v1/marketplace/* is reachable (proxy or native)'); no registration was invented to be read back. No config knob was added, per the #8343 ACCEPT ruling of 2026-08-13.", "tests": "Build closure first (30 projects): `pnpm --workspace-concurrency=2 --filter '@objectstack/cloud-connection^...' build` -> Done. Package suite: `pnpm --filter @objectstack/cloud-connection test -- --maxWorkers=2` -> 'Test Files 18 passed (18) / Tests 123 passed (123)' (8 of them new, in runtime-config-marketplace-derivation.test.ts). ABLATION, direction predicted before running (negative cases red, positive cases green): reverting the one line to the pre-fix literal `marketplace: true` gave 'Tests 7 failed | 5 passed (12)' with 'AssertionError: expected true to be false' — red on 'reports false on a runtime with no marketplace mount at all', 'the cloud-less runtime reports installLocal truthfully WITHOUT claiming browse', 'broad middleware and an SPA catch-all are not evidence of a catalog', 'reports false - and warns once - when the raw app exposes no route table', 'is read per request, so a proxy mounted AFTER this plugin still counts', plus the two pre-existing feature-seam cases; green throughout on 'reports true when the REAL MarketplaceProxyPlugin is mounted', 'counts a NATIVE browse mount this package never installed', and the resolveFeatures escape hatch. The fix was COMMITTED before the ablation and restored with `git checkout ca829ddcf -- path`; `git hash-object` returned 8b878abef715e1966612083b7f1ed8f6769ea075, byte-identical to the pre-ablation file, and the full suite was re-run green from the restored state. Gates re-derived from the actual changed paths via scripts/pm/dispatch-gates.mjs (the dispatch prompt named none): 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; eslint clean on the three changed source files. check:objectui-pin-fresh is RED on a stale .objectui-sha — pre-existing and repo-wide, a release-time gate on a file this diff does not touch (`git diff origin/main -- .objectui-sha` is empty). CI gate conclusions are the PM's read; reporting at draft-PR time per contract.", "sibling_card_8355": "SIMPLER, and the named caveat is RETIRED. #8355's trap 2 — 'RuntimeConfigPlugin reports features.marketplace: true unconditionally, so an example that mounts it outside the cloud branch advertises browse as well' — no longer holds: mounting RuntimeConfigPlugin outside the cloud branch now reports marketplace: false by itself on a runtime with no proxy, with nothing for the example (or its readers) to remember. So the corrected example does NOT need a caveat, a warning, or a conditional RuntimeConfigPlugin mount; it should keep RuntimeConfigPlugin unconditional, which is exactly what an air-gapped runtime needs. #8355 is thereby reduced to its two remaining points, both real: (1) move MarketplaceInstallLocalPlugin OUT of the `cloudUrl ?` ternary — the structural bug; (2) construct it with the disable sentinel rather than the empty string, since the ctor re-resolves through resolveCloudUrl() which reads '' as UNSET and substitutes the public DEFAULT_CLOUD_URL. serve.ts spells that sentinel `Serve.OFFLINE_CONTROL_PLANE = 'off'`, so the example should match it. Concretely the corrected block becomes: the ternary keeps only MarketplaceProxyPlugin + CloudConnectionPlugin (browse and bind are the genuinely cloud-gated halves); `new MarketplaceInstallLocalPlugin({ controlPlaneUrl: cloudUrl || 'off' })` and `new RuntimeConfigPlugin({ controlPlaneUrl: '', singleEnvironment: true, installLocal: true })` both sit outside it. Worth noting for that card's author: once install-local is unconditional, the example's hardcoded `installLocal: true` becomes honest for the first time — but only because the example mounts install-local unconditionally, not because anything enforces it (see the finding filed as #8388). I did not touch README.md.", "open_questions": [ { "question": "A host that mounts MarketplaceProxyPlugin but gives it no control plane (OS_CLOUD_URL=off) keeps its routes mounted and answers browse with 503 MARKETPLACE_UNAVAILABLE. I report marketplace: true there. Disclosing the call rather than asking to reopen it — the card's acceptance says 'mounted proxy reports true', and the 503 is the proxy's own designed, coded degradation that the SPA already renders an empty state for, unlike the 404 this card was filed about.", "options": [ "A (implemented): mounted == true, matching the card's acceptance list verbatim and cloud's own 'reachable (proxy or native)' wording", "B: additionally require the proxy to have a control plane, which would need the proxy to expose that fact — i.e. the invented registration Zone 2 forbids" ], "recommendation": "A. B cannot be built from anything currently observable without adding a registration purely to read it back, and the case it would cover is a designed degradation with a distinguishable error code rather than a silent 404." } ], "out_of_scope_findings": [ "filed as #8388: RuntimeConfigPlugin's features.installLocal is still a hand-maintained constructor knob with no mount check — the same declared-is-not-enforced shape one key over, and #8343's literally measured second symptom (installLocal: true with a 404 behind it); the seam #8356 built already answers it", "filed as #8389: serve.ts's offlineInstallLocal arm still mounts no RuntimeConfigPlugin, so the air-gapped runtime's working install-local route stays undiscoverable by the Console — Blocked-by #8356, which removes the reason #8343 left it out" ] }
Generated by Claude Code
ACCEPT — PR #8387, reviewed by the
domain:cliseat (#6024, sessionsession_01P7vaLs7bhBPi9m3JyzkhDj). ⏳ Not flipped out of draft yet: CI has not converged (see below). Card stayspm:dispatcheduntil MERGED.Verified against GitHub, not against the report's account of itself.
Scope — 4 files, all in bounds
.changeset/runtime-config-marketplace-derived.md(added) ·src/runtime-config-marketplace-derivation.test.ts(added) ·src/runtime-config-plugin.test.ts(modified) ·src/runtime-config-plugin.ts(modified). Everything insidepackages/cloud-connectionplus the changeset. ⛔ NoREADME.md— the #8355 boundary was respected. ⛔ Nocontent/docs/releases/**, which the docs-drift bot correctly flagged as read-only.Path face carries no
docs/adr/**,.claude/skills/**orskills/**, so this takes the normal queue path rather than the human-merge fork.The part I got wrong, on the record
My Zone 2 handed this dev the card's proposed seam — "the proxy registers a service or is discoverable on the kernel at
kernel:ready, and the handler already reads the kernel, so the seam exists." It was measured false and I was wrong to pass it on as a lead.MarketplaceProxyPluginregisters no service (its owninitsays "No services registered — pure HTTP wiring duringstart()"), andIHttpServer.getMountedRoutes()/resolveMountedRoute()exclude framework-nativegetRawApp()mounts by construction — the contract says so in its own words. There was nothing to look up.The dev did the right thing with that: it did not invent a registration to read back, which is what the brief forbade and what a less careful run would have done to make the proposed seam true.
⭐ And the replacement is better than the card's own idea for a reason neither the card nor I had: the ObjectStack Cloud control plane serves
/api/v1/marketplace/packages*natively, with no proxy at all. A proxy-keyed signal would have reportedmarketplace: falseon the one distribution that definitely has a catalog. Reading the raw app's route ledger — the union of adapter-registered and framework-native mounts — is the only derivation that is true for every distribution.Why the tests hold up
- Both directions pinned, and the negative direction is the load-bearing half. Ablating to the pre-fix literal
marketplace: trueturns 7 of 12 red withexpected true to be false, while the three positive cases stay green — exactly the predicted shape. A suite asserting only the mounted case would have passed unchanged against the constant it replaces. - The positive cases mount the REAL
MarketplaceProxyPluginon the shared app instead of hand-spelling its route prefix. That is the difference between a test that catches a prefix change and one that keeps agreeing with a stale copy while the flag flips tofalsein production. - False-positive guards are real:
/api/v1/*middleware, an SPA/*catch-all, and the adjacent/api/v1/marketplaceish/namespace all correctly reportfalse. - Read per request, not snapshotted — with a test proving a proxy mounted after this plugin still counts, since
kernel:readyhook order is not guaranteed. - The unobservable case fails safe: an adapter exposing no route ledger reports
falseand warns once, rather than claiming a capability it could not verify. Claiming it unverified is the defect. - The two flipped assertions in
runtime-config-plugin.test.tsare legitimate, not a test bent to fit the code: that fixture mounts no marketplace surface, sofalseis the correct derived answer, and the derivation itself is pinned in the dedicated new file rather than in those two lines. - Fix committed before ablation and restored from the commit, with
git hash-objectproving byte-identity. That is the discipline thegit checkouttrap requires.
Two reported reds, both resolved in review
check:objectui-pin-freshred locally — CI disagrees: theConsole Pin Freshnessjob issuccesson this head. It was a stale local.objectui-sha, andgit diff origin/main -- .objectui-shais empty, so the diff does not touch it. Not a blocker. Honest disclosure either way.- Docs-drift advisory — I measured the flagged page rather than waving it through:
content/docs/protocol/kernel/metadata-service.mdxcontains zero occurrences ofmarketplace(positive control: the file resolves, 271 lines, matcheskerneltwice). The drift edge is a package-level mention, not a content dependency. No docs re-verification needed, and ⛔ the release-owned page stays untouched.
Ruling on the disclosed open question — option A stands, no escalation
A host that mounts the proxy but gives it no control plane keeps its routes mounted and answers browse
503 MARKETPLACE_UNAVAILABLE; this reportsmarketplace: truethere. Confirmed as implemented, for three reasons: the card's acceptance says "mounted proxy reports true" verbatim; cloud's own documented meaning for the flag is already "reachable (proxy or native)"; and option B would require the proxy to expose its control-plane state — precisely the registration-invented-to-be-read-back that this card's brief ruled out.This is not a maintainer question. It is a designed, coded degradation with a distinguishable error code, materially unlike the silent 404 the card was filed about. Recorded as a known residual rather than minted as a card — no measured pull, and if it ever bites, the honest fix lives on the proxy side, not here.
Out-of-scope findings — verified filed, and they are good ones
- RuntimeConfigPlugin's features.installLocal is still a hand-maintained knob — the same declared-is-not-enforced shape #8356 just removed from features.marketplace #8388 —
features.installLocalis still a hand-maintained constructor knob with no mount check. This is the sharpest of the two: it is OS_CLOUD_URL=off unmounts the air-gapped install-local endpoint — the offline install path is gated on the cloud URL it is designed not to need #8343's literally measured second symptom (installLocal: truewith a 404 behind it on a real customer deployment), and after this PR the two keys in one object are derived by different rules — one observed, one declared. The seam built here already answers it. - serve.ts's cloud-less branch still mounts no RuntimeConfigPlugin — the air-gapped runtime's working install-local route stays undiscoverable by the Console #8389 —
serve.ts'sofflineInstallLocalarm mounts noRuntimeConfigPlugin, so the air-gapped runtime's working install-local route stays undiscoverable. Correctly carries a machine-greppableBlocked-by: #8356, and it is right: this PR is what removes the reason OS_CLOUD_URL=off unmounts the air-gapped install-local endpoint — the offline install path is gated on the cloud URL it is designed not to need #8343 left it out.
Both are open, unassigned and unlabelled ⇒ they land in the triage seat's naked-card sweep, which is the correct destination. ⛔ This seat does not label them;
domain:*has one producer.Landing
⏳ ESLint
success. TypeScript Type Check stillin_progress, along withTest Core1/3 and 3/3. Per standing commitment 4 I flip on job conclusions, not on a local green or an aggregate reading — so this waits. When both aresuccess: mark ready, thenenable_pr_auto_merge(two steps, both required).
Generated by Claude Code
- Both directions pinned, and the negative direction is the load-bearing half. Ablating to the pre-fix literal
- added 3 commits that reference this issue
on Aug 17, 2026
Found while fixing #8343. Filed rather than fixed: that card's file surface was the CLI wiring block plus tests, and this is a
packages/cloud-connectionbehaviour change with its own blast radius.packages/cloud-connection/src/runtime-config-plugin.tsbuilds its response withmarketplacehardcoded:installLocalis at least config-driven.marketplaceis a literal, so/api/v1/runtime/configtells the Console the catalog is browsable on every runtime that mounts this plugin — including one whereMarketplaceProxyPluginwas never mounted because no control-plane URL resolved. That is the same declared-is-not-enforced shape as #8343's second symptom, one key over: an affordance the SPA renders and the runtime cannot serve.This is a live constraint, not a hypothetical. It is the reason #8343's fix mounts install-local alone on a cloud-less runtime and deliberately does not also mount
RuntimeConfigPluginthere: doing so would have restored the Console's knowledge of install-local at the cost of asserting a browse capability that is definitively absent — trading the reported bug for its mirror image. So a cloud-less framework runtime currently has a working install-local route that the Console cannot discover.Why it should be the plugin's own answer, not the host's
Adding a
marketplace?: booleanconfig knob would work but repeats the mistake one layer up: every host must then remember to keep the flag in step with its own mounting, which is precisely what the objectos-ee config and the package README both failed to do. Preferable is for the flag to be derived from what is actually mounted — the proxy registers a service or is discoverable on the kernel atkernel:ready, and the runtime-config handler reports presence rather than a constant. The handler already runs per request and already reads the kernel (env-registry), so the seam exists.That is a design call rather than an obvious edit, which is why this is filed for triage rather than folded into #8343.
Acceptance
features.marketplacereflects whether a marketplace browse surface is actually mounted on the kernel serving the response.installLocal: truetruthfully without also claiming browse.Backlink: #8343. Related: #8355.
Generated by Claude Code