Skip to content

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

Description

@os-zhuang

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-connection behaviour change with its own blast radius.

packages/cloud-connection/src/runtime-config-plugin.ts builds its response with marketplace hardcoded:

features: {
    installLocal: this.installLocal,
    marketplace: true,
    // aiStudio + autoPublishAiBuilds + any distribution keys.
    ...features,
},

installLocal is at least config-driven. marketplace is a literal, so /api/v1/runtime/config tells the Console the catalog is browsable on every runtime that mounts this plugin — including one where MarketplaceProxyPlugin was 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 RuntimeConfigPlugin there: 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?: boolean config 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 at kernel: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.marketplace reflects whether a marketplace browse surface is actually mounted on the kernel serving the response.
  • A cloud-less runtime can report installLocal: true truthfully without also claiming browse.
  • Coverage pinning both directions (mounted proxy reports true; no proxy reports false).

Backlink: #8343. Related: #8355.


Generated by Claude Code

Activity

  1. added theissue type on Aug 13, 2026
  2. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: lands in packages/cloud-connection/src/runtime-config-plugin.ts → domain:cli (cloud-connection family). → pm:queue, type Bug — declared ≠ enforced: features.marketplace is a literal true regardless 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

  3. self-assigned this
    on Aug 13, 2026
  4. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 1 (domain:cli seat #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. ⛔ NOT packages/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 touches packages/cloud-connection; open PRs #8377 / #8369 / #8342 / #8365 / #8363 / #8325 / #8302 / #7996 checked, none touches this package.

    Premise re-verified on origin/main before dispatch — runtime-config-plugin.ts:289 is marketplace: 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.marketplace from what is actually mounted. Judged not-maintainer-floor — it widens no contract and restores declared = enforced. ⛔ Do not add a marketplace?: boolean config 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 reports false; a cloud-less runtime can report installLocal: true truthfully 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 at kernel:ready", and notes the handler already reads the kernel via env-registry. That is the filer's reading, not a measurement — go look at how MarketplaceProxyPlugin actually 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

  5. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor
    {
      "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

  6. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT — PR #8387, reviewed by the domain:cli seat (#6024, session session_01P7vaLs7bhBPi9m3JyzkhDj). ⏳ Not flipped out of draft yet: CI has not converged (see below). Card stays pm:dispatched until 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 inside packages/cloud-connection plus the changeset. ⛔ No README.md — the #8355 boundary was respected. ⛔ No content/docs/releases/**, which the docs-drift bot correctly flagged as read-only.

    Path face carries no docs/adr/**, .claude/skills/** or skills/**, 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. MarketplaceProxyPlugin registers no service (its own init says "No services registered — pure HTTP wiring during start()"), and IHttpServer.getMountedRoutes() / resolveMountedRoute() exclude framework-native getRawApp() 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 reported marketplace: false on 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: true turns 7 of 12 red with expected 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 MarketplaceProxyPlugin on 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 to false in production.
    • False-positive guards are real: /api/v1/* middleware, an SPA /* catch-all, and the adjacent /api/v1/marketplaceish/ namespace all correctly report false.
    • Read per request, not snapshotted — with a test proving a proxy mounted after this plugin still counts, since kernel:ready hook order is not guaranteed.
    • The unobservable case fails safe: an adapter exposing no route ledger reports false and 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.ts are legitimate, not a test bent to fit the code: that fixture mounts no marketplace surface, so false is 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-object proving byte-identity. That is the discipline the git checkout trap requires.

    Two reported reds, both resolved in review

    • check:objectui-pin-fresh red locally — CI disagrees: the Console Pin Freshness job is success on this head. It was a stale local .objectui-sha, and git diff origin/main -- .objectui-sha is 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.mdx contains zero occurrences of marketplace (positive control: the file resolves, 271 lines, matches kernel twice). 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 reports marketplace: true there. 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

    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 still in_progress, along with Test Core 1/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 are success: mark ready, then enable_pr_auto_merge (two steps, both required).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions