Repository navigation
cloud-connection README's wiring example gates install-local on the cloud URL — the recipe that propagated #8343 into the EE image #8355
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Aug 13, 2026 Triage: lands in
packages/cloud-connection/README.md→domain:cli(package README travels with its package;cloud-connectionis adomain:clifamily). →pm:queue, type Bug — the example contradicts the air-gapped contract stated in the prose one paragraph above it, and it is the measured vector that propagated the #8343 P1 into the EE image.Premise verified: the serve.ts pattern the corrected example should mirror is on
origin/main(#8343's PR #8358 merged, confirmed at08dcd1eb2per that card's hold comment) — noBlocked-byneeded. Docs-only,skip-changeset. The corrected example must keep both traps the card names: the''-resolves-to-DEFAULT_CLOUD_URLsentinel trap, and not mountingRuntimeConfigPluginoutside the cloud branch while #8356 is unfixed.Size/model suggestion: S (mechanical, docs-only),
mode:subagent,model: sonnet.
Generated by Claude Code
Held this round behind #8356 — and it must be re-priced, not just re-queued (
domain:cliseat #6024, sessionsession_01P7vaLs7bhBPi9m3JyzkhDj, round 1). Stayspm:queue, unassigned.Why held. Two reasons, and the second is the one that matters:
- Same package as RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356, dispatched this round (
packages/cloud-connection). Different files —README.mdhere,src/runtime-config-plugin.tsthere — but same-package cards go serial unless there is a reason to risk it. ⚠️ This card's correct content depends on RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356's outcome. The triage note requires the corrected example to keep two traps, and one of them is "not mountingRuntimeConfigPluginoutside the cloud branch while RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356 is unfixed" — a condition with an expiry date on it. RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356 is fixing exactly that. Writing the README against today'smainwould produce a recipe that is correct for a few hours and then quietly wrong again — in the very file that propagated the original P1 into the EE image.
The re-pricing question is already in #8356's dispatch brief, to be answered before this card goes out: does that change make #8355 simpler, harder, unnecessary, or unaffected, and what should the corrected example now say about
RuntimeConfigPlugin? ⛔ This card's brief must be written from that answer, not from the triage note as it stands today.What survives either way (a property of config resolution, not of the marketplace flag): trap 1, where
new MarketplaceInstallLocalPlugin({ controlPlaneUrl: '' })does not mean "no cloud" — the constructor re-resolves throughresolveCloudUrl(), which reads''as unset and substitutesDEFAULT_CLOUD_URL, pointing an air-gapped runtime's catalog branch at the public cloud.'off'is the sentinel that actually resolves to no cloud. The corrected example must keep that regardless of how #8356 lands. Trap 2 is the one that may evaporate.No
Blocked-by:line, deliberately — this is a sequencing hold inside one seat's own queue, not an upstream dependency. #8355 is dispatchable on its own the moment #8356 resolves, and if #8356 forks or is abandoned this card goes out unchanged with both traps intact. Size/model stands at S,mode:subagent,model: sonnet, docs-only,skip-changeset.
Generated by Claude Code
- Same package as RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356, dispatched this round (
Held this round behind #8356 — and it must be re-priced, not just re-queued (
domain:cliseat #6024, sessionsession_01P7vaLs7bhBPi9m3JyzkhDj, round 1). Stayspm:queue, unassigned.Why held. Two independent reasons, and the second is the one that matters:
- Same package as RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356, dispatched this round (
packages/cloud-connection). Different files —README.mdhere,src/runtime-config-plugin.tsthere — but same-package cards go serial unless there is a reason to risk it. ⚠️ This card's correct content depends on RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356's outcome. The triage note requires the corrected example to keep two traps, and one of them is "not mountingRuntimeConfigPluginoutside the cloud branch while RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356 is unfixed" — a condition with an expiry date on it. RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356 is fixing exactly that. Writing the README against today'smainwould produce a recipe that is correct for a few hours and then quietly wrong again, in the same file that propagated the original P1 into the EE image.
Re-pricing question already put to #8356's developer, to be answered before this card is dispatched: does your change make #8355 simpler, harder, unnecessary, or unaffected? ⛔ The dispatch brief for this card must be written from that answer, not from the triage note as it stands today.
What survives either way (verified, not inherited): the
''-resolves-to-DEFAULT_CLOUD_URLsentinel trap is a property of the config resolution, not of the marketplace flag, so the corrected example must keep it regardless of how #8356 lands. The other trap is the one that may evaporate.No
Blocked-by:line added on purpose — this is a sequencing hold within one seat's own queue, not an upstream dependency; #8355 is still dispatchable on its own the moment #8356 resolves, and if #8356 forks or is abandoned this card goes out unchanged with the trap note intact. Size/model stands at S,mode:subagent,model: sonnet, docs-only,skip-changeset.
Generated by Claude Code
- Same package as RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356, dispatched this round (
- addedskip-changesetPR has no user-facing published change; bypasses the changeset gatePR has no user-facing published change; bypasses the changeset gateand removed
on Aug 13, 2026 Claim: PM loop round 2 (
domain:cliseat #6024)
Session:session_01P7vaLs7bhBPi9m3JyzkhDj
Branch:claude/issue-8355-cloud-connection-readme-wiring
Worktree:objectstack-issue-8355
Domain:domain:cli
File surface:packages/cloud-connection/README.mdonly. Docs-only,skip-changeset. ⛔ No source, no tests, no other package. (stop on breach; explain in the report)
Container & model:S(mechanical, docs-only),mode:subagent,model: sonnet
Serial constraints cleared: the hold on this card is released — #8356 landed as PR #8387 (631ddbf36), verified onorigin/main(hasMarketplaceBrowseMountatruntime-config-plugin.ts:152; themarketplace: trueliteral is gone). No in-flight claim touchespackages/cloud-connection; #7912 is inpackages/rest/src/rest-server.ts.Re-priced, as promised — one of the two traps is now retired
This card was held behind #8356 specifically because its correct content depended on that outcome. #8356's developer answered the re-pricing question, and the answer changes the deliverable:
- ⛔ Trap 2 is RETIRED. Do not write the caveat. The old requirement was "do not mount
RuntimeConfigPluginoutside the cloud branch, because it advertises browse unconditionally". That is no longer true:features.marketplaceis now derived from the serving app's route table, so mountingRuntimeConfigPluginon a runtime with no proxy reportsmarketplace: falseby itself, with nothing for the example or its readers to remember. ⇒ The corrected example should mountRuntimeConfigPluginunconditionally, which is exactly what an air-gapped runtime needs. - ✅ Trap 1 SURVIVES and must be kept. It is a property of config resolution, untouched by RuntimeConfigPlugin reports features.marketplace: true unconditionally — the Console is told browse is live whether or not the proxy mounted #8356:
new MarketplaceInstallLocalPlugin({ controlPlaneUrl: '' })does not mean "no cloud" — the constructor re-resolves throughresolveCloudUrl(), which reads''as unset and substitutesDEFAULT_CLOUD_URL, pointing an air-gapped runtime's catalog branch at the public cloud.'off'is the sentinel that actually resolves to no cloud.
The defect
The README states the air-gapped contract correctly in prose — "
OS_CLOUD_URL=offdisables every remote call; air-gapped installs keep working via inline manifests handed toinstall-local" — and the code example a few lines above does the opposite:MarketplaceInstallLocalPluginsits inside thecloudUrl ?ternary, soOS_CLOUD_URL=offunmounts the very surface the prose promises.cloud'sapps/objectos-ee/objectstack.config.tsis a faithful copy of this recipe, which is how a P1 customer deployment ended up unable to install a package by any route. This file is the propagation vector, not a cosmetic doc.Shape of the corrected example (a lead to verify, not to inherit)
serve.tsafter #8343 is the reference implementation — mirror what it actually does, do not copy this sketch blindly:- the ternary keeps only the genuinely cloud-gated halves (
MarketplaceProxyPlugin,CloudConnectionPlugin); MarketplaceInstallLocalPluginmoves outside it, constructed with the explicit disable sentinel rather than''—serve.tsspells itServe.OFFLINE_CONTROL_PLANE, so read the real spelling offorigin/mainrather than hardcoding'off'from this comment;RuntimeConfigPluginstays outside, unconditional.
⚠️ Verify each of these againstorigin/mainat the moment you start. Named symbols and file paths are this lane's most frequently stale sentences, and this brief is quoting a fix that landed hours ago.One thing worth stating in the README if it reads honestly: with install-local mounted unconditionally, the example's
installLocal: truebecomes true for the first time — but only because the example mounts it unconditionally, not because anything enforces it. That gap is filed as #8388 and is ⛔ out of scope here; do not try to fix it in prose.Non-negotiables
- Docs-only. If you find yourself wanting to change source, stop and report — that is a different card.
- Run any command you cite as a foreground Bash call and block on it. ⛔ Do not background anything, ⛔ do not poll.
- Draft PR, target
main, first lineFixes #8355. - Return the structured JSON report and post it as an issue comment here with first line
<!-- os-dev-report -->.
Generated by Claude Code
- ⛔ Trap 2 is RETIRED. Do not write the caveat. The old requirement was "do not mount
{ "issue": 8355, "status": "done", "branch": "claude/issue-8355-cloud-connection-readme-wiring", "pr": "https://github.com/objectstack-ai/objectstack/pull/8419", "premise_still_valid": true, "summary": "Moved MarketplaceInstallLocalPlugin outside the OS_CLOUD_URL ternary in packages/cloud-connection/README.md's usage example, constructing it with `cloudUrl || 'off'` (never the bare '' that resolveCloudUrl() misreads as unset and re-resolves to the public DEFAULT_CLOUD_URL -- trap 1, confirmed still live). RuntimeConfigPlugin now mounts unconditionally with no caveat, since #8387 (merged, verified on origin/main at 631ddbf36) derives features.marketplace from what is actually mounted rather than a hardcoded true -- trap 2 is retired exactly as the PM's re-pricing predicted. The ternary now holds only the genuinely cloud-gated MarketplaceProxyPlugin and CloudConnectionPlugin. Docs-only; no source touched.", "tests": "Read-only verification against origin/main: packages/cli/src/commands/serve.ts (Serve.OFFLINE_CONTROL_PLANE='off', Serve.planMarketplaceWiring) confirmed the disable-sentinel spelling and that trap 1 is real; also noticed serve.ts's own offlineInstallLocal arm still does not mount RuntimeConfigPlugin -- a separate, already-filed and already-dispatched gap (#8389, a worktree for it exists alongside mine), so this README example is the documented recipe the Suggested Shape asked for, not a literal mirror of serve.ts's CLI branches. packages/cloud-connection/src/cloud-url.ts confirmed resolveCloudUrl()'s disable sentinels (off/none/local/disabled) and the ''-means-unset behavior. packages/cloud-connection/src/runtime-config-plugin.ts confirmed features.marketplace is derived via hasMarketplaceBrowseMount(rawApp) post-#8387, and that RuntimeConfigPluginConfig.controlPlaneUrl:'' is deliberately handled as same-origin in THIS plugin specifically (different from the two Marketplace plugins' resolveCloudUrl re-resolution), so the existing controlPlaneUrl:'' on RuntimeConfigPlugin needed no change. Build (foreground, under flock): `pnpm --filter '@objectstack/cloud-connection^...' build` then `pnpm --filter @objectstack/cloud-connection build` -- both exit 0. The example is verified runnable, not merely illustrative: copied the exact corrected snippet into packages/cloud-connection/src/ as an untracked scratch file importing from './index.js', ran `npx tsc --noEmit -p tsconfig.json` under the shared lock -- exit 0, zero diagnostics -- then deleted the scratch file before committing (git status confirmed only README.md changed thereafter). `node scripts/check-nul-bytes.mjs` -- OK, 7599 files scanned, no raw control bytes. `node scripts/pm/dispatch-gates.mjs packages/cloud-connection/README.md` -- no convention-scoped gate family names this path; nothing beyond check:nul-bytes applies to a docs-only change touching no tests/fixtures/error-codes/agent-configs. An extra `pnpm --filter @objectstack/cloud-connection test` run was started as additional diligence but never acquired the shared flock within a reasonable window (heavy contention -- at least 4 sibling agents' builds/tests queued on it concurrently); stopped it by its own recorded PID rather than idle-waiting on it, since no source or test file was touched and the load-bearing verification (build + typecheck of the actual corrected example) was already green.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
Patch round (one addition) — everything else ACCEPTed. PR #8419, reviewed by the
domain:cliseat (#6024, sessionsession_01P7vaLs7bhBPi9m3JyzkhDj). Same claim, same branch, original dev continued viaSendMessageso no context is re-derived. Card stayspm:dispatched.Verified against GitHub, not the report: 1 file,
packages/cloud-connection/README.md, +12/−1. No source, nocontent/docs/releases/**, no other package.Check Changesetsuccesswithskip-changeset. The re-pricing held exactly as predicted — trap 2 retired, trap 1 kept ascloudUrl || 'off', ternary reduced to the genuinely cloud-gatedMarketplaceProxyPlugin+CloudConnectionPlugin.⭐ The verification method is worth recording: the corrected snippet was copied into the package as an untracked scratch file importing from
./index.js,tsc --noEmitrun against the real constructors, then deleted before commit (confirmed by the diff being README-only). That is the difference between "the example is verified runnable" and "the example looks right" — and it is the right instinct for a file whose entire defect history is being copied.The one thing being patched
The corrected example now carries two different spellings of "no cloud", two lines apart, with nothing saying why they differ:
new MarketplaceInstallLocalPlugin({ controlPlaneUrl: cloudUrl || 'off' }), ... new RuntimeConfigPlugin({ controlPlaneUrl: '', singleEnvironment: true, installLocal: true }),
The asymmetry is real and deliberate, and the dev established it in its own verification:
RuntimeConfigPlugintreats''as an intentional "stay on this origin", while the two Marketplace plugins re-resolve throughresolveCloudUrl(), where''reads as unset and becomes the publicDEFAULT_CLOUD_URL.⚠️ That finding is in the report and not in the file.Why it blocks the flip on this card specifically: this README is the measured propagation vector for the #8343 P1. A reader who spots the inconsistency and normalises it picks one of two directions, and one of them — changing the install-local line to
''— reproduces the exact defect this card exists to remove. Retiring one copy-trap while leaving an adjacent unexplained one is a poor trade in this file above all others.⇒ Requested: a sentence or two on the
RuntimeConfigPluginline stating why itscontrolPlaneUrlis''and not the'off'sentinel its neighbour needs. ⛔ No restructuring, no source, no changeset, no scope growth.Landing
ESLint and TypeScript Type Check are still
in_progresson the current head, so nothing was flippable yet regardless. On the patch push: re-check both job conclusions, then mark ready andenable_pr_auto_merge.Noted for the record and needing no action:
serve.ts's ownofflineInstallLocalarm does not yet mountRuntimeConfigPlugin, so this README is the documented recipe, not a mirror of the CLI's current branches. That gap is #8389, in flight now. The README does not claim otherwise — checked.
Generated by Claude Code
- added a commit that references this issue
on Aug 13, 2026 Patch round closed — ACCEPT. PR #8419 @
3866536c1,domain:cliseat (#6024). ⏳ Flip held: ESLint and TypeScript Type Check re-queued on the new head.Verified against the diff: still 1 file,
packages/cloud-connection/README.md, +15/−1. No source, no changeset, no scope growth — exactly the one addition requested.The added comment does the job, and slightly better than asked:
// `''` here, unlike its neighbor above, is correct as-is: this plugin does // NOT re-resolve controlPlaneUrl through resolveCloudUrl(), so '' means // "stay on this origin" rather than "unset" — do not "fix" it to 'off'.
It names the asymmetry, gives the mechanism, and closes the trap in both directions with an explicit imperative. The brief asked only for the explanation; the "do not fix it to
'off'" clause is the dev's own addition and it is the half that actually stops the miscopy — a reader who understands why the two differ can still normalise them out of tidiness, and this sentence removes that move.The example now states, inline, every reason a line is or is not inside the ternary. For the file that was the measured propagation vector of the #8343 P1, that is the right ending: the recipe no longer relies on the reader knowing which of two spellings of "no cloud" belongs where.
On CI convergence: mark ready, then
enable_pr_auto_merge.
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
Found while fixing #8343 (registration condition in
packages/cli/src/commands/serve.ts). Filed rather than fixed: that card's file surface was the CLI wiring block plus tests.packages/cloud-connection/README.mdstates the air-gapped contract correctly in prose:The code example a few lines above it does the opposite — it puts
MarketplaceInstallLocalPlugininside thecloudUrl ?ternary, soOS_CLOUD_URL=offunmounts the very surface the prose promises keeps working, whileRuntimeConfigPlugin({ installLocal: true })sits outside the ternary and keeps advertising it:That is exactly the shape #8343 was filed against, including its second symptom (
features.installLocal: truewith a 404 behind it) — andcloud'sapps/objectos-ee/objectstack.config.tsis a faithful copy of this recipe, which is how a P1 customer deployment ended up unable to install a package by any route.Two further traps a corrected example should avoid teaching:
new MarketplaceInstallLocalPlugin({ controlPlaneUrl: cloudUrl })wherecloudUrlis the empty string does not mean "no cloud". The constructor re-resolves throughresolveCloudUrl(), which reads''as unset and substitutesDEFAULT_CLOUD_URL— so an air-gapped runtime wired that way points its catalog branch at the public cloud. One of the disable sentinels ('off') is the value that resolves to no cloud.RuntimeConfigPluginreportsfeatures.marketplace: trueunconditionally (separate finding, filed alongside this one), so an example that mounts it outside the cloud branch advertises browse as well.Suggested fix
Rewrite the example so install-local is outside the ternary and constructed with an explicit disable sentinel when there is no control plane, matching what
serve.tsnow does after #8343. Docs-only.Backlink: #8343.
Generated by Claude Code