Skip to content

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

Closed

Description

@os-zhuang

Found while fixing #8356. Filed rather than fixed: that card's file surface is packages/cloud-connection, and this is a change to the CLI wiring block in packages/cli/src/commands/serve.ts.

Serve's marketplace wiring has two arms. The cloud arm mounts the proxy, install-local, cloud-connection and RuntimeConfigPlugin. The offlineInstallLocal arm (added by #8343, OS_CLOUD_URL=off) mounts install-local alone:

} else if (wiring.offlineInstallLocal) {
  // Cloud explicitly disabled -> mount the OFFLINE half only.
  await kernel.use(new MarketplaceInstallLocalPlugin({ controlPlaneUrl: Serve.OFFLINE_CONTROL_PLANE }));
  trackPlugin('MarketplaceInstallLocal');
}

So an air-gapped runtime now has a working /api/v1/marketplace/install-local route and serves no /api/v1/runtime/config at all — the Console has no way to learn the route is there, and renders no install affordance for a capability that works. #8343 calls this out directly: "a cloud-less framework runtime currently has a working install-local route that the Console cannot discover."

Why it was left that way, and why that reason is now gone

#8343 could not mount RuntimeConfigPlugin in this arm because the plugin hardcoded features.marketplace: true: reporting install-local truthfully would have cost a false browse claim, "trading the reported bug for its mirror image" (#8356's words).

#8356 removes that constraint. features.marketplace is now derived from the route table of the app serving the response, so mounting RuntimeConfigPlugin on a runtime with no proxy reports marketplace: false on its own — no knob, nothing for the wiring to remember. Mounting it in this arm is now safe and is what completes #8343's user-visible outcome.

Suggested change

Mount RuntimeConfigPlugin in the offlineInstallLocal arm with the same shape the cloud arm uses (controlPlaneUrl: '', singleEnvironment: true, installLocal: true), and pin it: an OS_CLOUD_URL=off boot serves /api/v1/runtime/config reporting installLocal: true and marketplace: false.

Worth checking while there: the cloud arm's RuntimeConfigPlugin and this one would be two kernel.use calls keyed by the same plugin name, so the same "a host that wires its own keeps it" presence reasoning the install-local mount already documents in this block should apply here too.

Blocked-by: #8356 (the derivation must land first, or this arm re-introduces the false browse claim on exactly the deployment #8343 was filed against).

Backlink: #8343, #8356.

Activity

  1. added
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 13, 2026
  2. added theissue type on Aug 13, 2026
  3. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: lands in packages/cli/src/commands/serve.ts ⇒ domain:cli, type Task, pm:blocked — the body already carries the machine-readable Blocked-by: #8356 line and #8356 is in flight, unmerged. The unlock scan returns this card when #8356 closes.

    For the cli seat at unlock time (standard re-pricing, stated here so it isn't re-derived):

    Size/model suggestion: S–M, mode:subagent, opus (wiring judgment plus a presence-semantics check the card names).

    Triage seat Routine, scheduled fire 2026-08-13 ~11:11Z.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 13, 2026
  5. self-assigned this
    on Aug 13, 2026
  6. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 2 (domain:cli seat #6024)
    Session: session_01P7vaLs7bhBPi9m3JyzkhDj
    Branch: claude/issue-8389-offline-arm-runtime-config
    Worktree: objectstack-issue-8389
    Domain: domain:cli
    File surface: packages/cli/src/commands/serve.ts (the offlineInstallLocal arm of the marketplace wiring block, ~:1829) + its sibling tests in packages/cli. (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: #8356 MERGED (631ddbf36) — this card's Blocked-by is satisfied. No in-flight claim touches packages/cli: #7912 is packages/rest/src/rest-server.ts, #8355 is packages/cloud-connection/README.md. ⚠️ #8368 is queued behind this card — it is a maintainer-authored Feature landing in the same objectstack start / serve boot path and may well be this same file; it will not be dispatched while this claim is open.

    Unlocked — and the premise was re-verified on the merged ref, not inherited

    The unlock scan's own rule is that the two least trustworthy assertions at unlock time are the card's file surface and the PM's belief that the upstream merge left it standing — the merge that closed the upstream is exactly the one most likely to have quietly fixed this card too. So, measured on origin/main after #8356 landed:

    • serve.ts:1827 mounts new RuntimeConfigPlugin({ controlPlaneUrl: '', singleEnvironment: true, installLocal: true }) — and it sits in the cloud arm, because :1829 is the } else if (wiring.offlineInstallLocal) { that follows it.
    • ⇒ The offline arm still mounts install-local alone. The premise holds; the card is real.
    • Positive control for the search: 33 kernel.use matches in the same file, so a zero result would have meant something.

    Why this is completion, not a new decision

    #8343's ACCEPT ruling adopted route-table derivation, and #8356 delivered it: features.marketplace is now derived from the serving app's route ledger (hasMarketplaceBrowseMount, runtime-config-plugin.ts:152 on main; the marketplace: true literal is gone). The only reason #8343 left this arm without a RuntimeConfigPlugin was that mounting it would have asserted a browse capability that was definitively absent — trading the reported bug for its mirror image. That constraint no longer exists. ⇒ No decision-inbox stop.

    Acceptance — an observable, not a mechanism

    An OS_CLOUD_URL=off boot serves /api/v1/runtime/config reporting installLocal: true and marketplace: false.

    ⚠️ Write the pin against that observable outcome, not against the mechanism that produces it. Sibling card #8388 (derive features.installLocal the same way marketplace now is) is queued and will land after this one. It keeps the installLocal constructor option as an explicit override rather than removing it, so the acceptance above stays true either way — but a pin written against "the constructor flag was passed" would break when it lands, while a pin written against the served payload will not. That is the seat's sequencing call which triage left open: this card goes first, and its pin is required to survive #8388.

    Leads to verify, not inherit

    Line numbers above were read from origin/main minutes ago on a file that changes often — re-measure and anchor on symbols. Also check while you are in that block: the cloud arm and this arm would be two kernel.use calls keyed by the same plugin name, so whatever "a host that wires its own keeps it" presence reasoning the install-local mount already documents there should apply here too. The card says so; confirm it against the code rather than reproducing the sentence.

    Non-negotiables

    • Run every build and test as an ordinary foreground Bash call and block on it (raise timeout up to 600000 ms for slow steps). ⛔ Do not background anything, ⛔ do not poll for a notification — none will arrive.
    • Changeset required (user-visible behaviour change on a boot path).
    • Draft PR, target main, first line Fixes #8389.
    • Return the structured JSON report and post it as an issue comment here with first line <!-- os-dev-report -->.

    Generated by Claude Code

  7. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 8389,
      "status": "done",
      "branch": "claude/issue-8389-offline-arm-runtime-config",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8431",
      "premise_still_valid": true,
      "summary": "Re-measured the premise on the merged ref and it held: serve.ts's cloud arm mounted RuntimeConfigPlugin (line 1827) while the `} else if (wiring.offlineInstallLocal) {` arm (line 1829) mounted install-local alone, so an OS_CLOUD_URL=off boot served no /api/v1/runtime/config at all — all three PM measurements confirmed, including the 33 kernel.use positive control and hasMarketplaceBrowseMount at runtime-config-plugin.ts:152. The offline arm now also mounts RuntimeConfigPlugin, under a new independent `offlineRuntimeConfig` flag on planMarketplaceWiring, with a presence guard mirroring the install-local mount's: Kernel.use does `this.plugins.set(name, meta)`, so an unguarded mount replaces rather than double-mounts, and RuntimeConfigPlugin carries the host's branding plus the open-core resolveFeatures seam. The two offline guards are deliberately independent — a host wiring its own install-local but no runtime-config has this defect in full, and one shared gate would have silently excluded it. Both arms now build the plugin from one shared frozen RUNTIME_CONFIG_OPTIONS so the offline payload cannot drift from the cloud one. packages/cloud-connection untouched; installLocal not derived here.",
      "tests": "New pin packages/cli/test/serve-marketplace-offline-runtime-config.test.ts asserts the SERVED payload (not the constructor flag, so it survives #8388): reads the body out of the really-mounted handler and requires installLocal:true AND marketplace:false. Anti-vacuity, all three present because `marketplace:false` is exactly what a runtime serving nothing would 'report': the read helper THROWS when /api/v1/runtime/config was never registered; the same body must simultaneously report installLocal:true; and a positive control mounts the REAL MarketplaceProxyPlugin on the same app and requires marketplace:true. ABLATION from the committed state (git checkout origin/main -- serve.ts, then restored via git checkout <branch> -- serve.ts; predicted direction was red and that is what happened): `Test Files 1 failed (1) / Tests 11 failed (11)`, with the acceptance case dying on `Error: THE #8389 DEFECT: nothing mounted GET /api/v1/runtime/config` and the plan cases on `expected undefined to be true` — i.e. it fails on 'nothing served', not on a false-looking false. Restored: `Test Files 2 passed (2) / Tests 20 passed (20)` across the new file and #8343's existing pins. Full packages/cli suite: `Test Files 117 passed (117) / Tests 1276 passed (1276)`. `pnpm --filter @objectstack/cli typecheck` clean. Gate families derived from my actual changed paths with scripts/pm/dispatch-gates.mjs (the dispatch prompt named none) and run: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:objectui-changeset, check:startup-registry-verdict ('40 startup/open-registry seam(s) ... none recording a verdict the boot can contradict'), check:query-options-erasure, check:type-check-coverage, check:type-check-debt ('33 ledger entr(ies) re-measured, none above its recorded number' — needed @objectstack/service-knowledge built first, outside the cli closure), check-changeset-no-major.mjs, check:nul-bytes plus a control-char self-scan of the three changed files. All green. ONE red, pre-existing and not from this branch: check:objectui-pin-fresh exits 1 on a stale .objectui-sha, a file not in my diff, so the gate's inputs are byte-identical to origin/main.",
      "open_questions": [],
      "out_of_scope_findings": [
        "not filed — already open as #8357: serve.ts's CLOUD arm mounts RuntimeConfigPlugin (and the proxy/install-local/cloud-connection) with no presence guard, so a host config wiring its own has it replaced. Searched before filing; #8357 names RuntimeConfigPlugin explicitly and states the cloud arm was deliberately left for its own card. This PR's asymmetry (offline arm guarded, cloud arm not) is therefore the intended state, not an oversight."
      ]
    }

    Notes for the PM, outside the JSON:


    Generated by Claude Code


    Generated by Claude Code

  8. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT — PR #8431, reviewed by the domain:cli seat (#6024, session session_01P7vaLs7bhBPi9m3JyzkhDj). ⏳ Flip held: CI started at 12:52 and has not converged. Card stays pm:dispatched.

    Verified against GitHub, not the report. 3 files: .changeset/offline-arm-runtime-config.md (+38), packages/cli/src/commands/serve.ts (+110/−9), packages/cli/test/serve-marketplace-offline-runtime-config.test.ts (+402). ⛔ No packages/cloud-connection — the #8388 boundary held. ⛔ No release-owned docs.

    The acceptance pin does what it was asked to do, and the anti-vacuity work is the best part

    The brief required the pin to assert the served payload rather than the constructor flag, so it survives #8388 making installLocal derived. It does, and the file says why in its own header.

    More importantly, I flagged one specific trap: a pin asserting marketplace: false would read green on a runtime that serves no config payload at all — which is precisely today's defect. Three independent guards close it:

    • readConfig throws when /api/v1/runtime/config was never registered, with the message THE #8389 DEFECT: nothing mounted GET /api/v1/runtime/config. "Serves nothing" can never surface as a passing false.
    • the same body must simultaneously report installLocal: true, which an unserved payload cannot do;
    • a positive control mounts the real MarketplaceProxyPlugin on the same app and requires marketplace: true, so false is an observation rather than a fixture artifact.

    The ablation confirms the shape: 11 red, dying on "nothing mounted" rather than on a false-looking false.

    ⭐ Two findings in the implementation that the card did not contain

    1. kernel.use keys by name (plugins.set(name, meta)), so an unguarded mount REPLACES rather than double-mounts. The card raised the presence question in passing; the dev measured it and found the stakes are higher than for the neighbouring install-local mount — RuntimeConfigPlugin carries the host's branding and the open-core resolveFeatures seam, so a silent replacement answers with framework defaults and no distribution policy at all. There is a drift test pinning that RUNTIME_CONFIG_IDENTITIES still match the real plugin, because a stale identity registry fails open: nothing matches, the guard never fires, the host's instance is replaced, and nothing else notices.
    2. The two offline guards are computed independently, and that is a correctness point, not a style choice. One shared gate would have been the smaller change and would have silently excluded exactly the box that has this defect in full: a host wiring its own install-local, serving no runtime-config. Pinned by its own case.

    Both arms now build from one frozen RUNTIME_CONFIG_OPTIONS, asserted with Object.isFrozen, so the offline payload cannot drift from the cloud one — the difference between the arms comes from what is mounted, never from a second options literal.

    Convergent hardening worth recording

    This PR pins the ''-vs-'off' asymmetry in source — RuntimeConfigPlugin special-cases the empty string as "stay on this origin" and bypasses resolveCloudUrl(), while its neighbour two lines away re-resolves and would substitute the public default cloud — with the comment "harmonising them is the tempting cleanup." That is the same footgun #8355's README patch closed an hour ago, reached independently from the code side. The recipe and the implementation now both refuse the normalisation.

    Out-of-scope: correctly not filed

    The cloud arm mounts RuntimeConfigPlugin with no presence guard, so a host wiring its own has it replaced there. The dev searched before filing, found #8357 already names RuntimeConfigPlugin explicitly and records that the cloud arm was deliberately left for its own card ⇒ this PR's asymmetry (offline guarded, cloud not) is the intended state, and a twin card would have been noise. That is the dedup discipline working.

    Also noted: the dev ran check:type-check-debt proactively and reports "33 ledger entries re-measured, none above its recorded number" — the very gate that turned #8426 red. The one red it reports is check:objectui-pin-fresh on a stale .objectui-sha, a file not in this diff.

    Landing

    On CI convergence — ESLint and TypeScript Type Check both conclusion: success — mark ready, then enable_pr_auto_merge. #8368 (maintainer-authored, artifact-pinned boot) is queued behind this on the packages/cli boot path and goes out once this merges; whoever picks it up should verify whether it truly lands in serve.ts before treating it as same-file.


    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

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions