Skip to content

fix(cli): mount install-local on a runtime with the cloud switched off - #8358

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-8343-install-local-offline-gate
Aug 13, 2026
Merged

fix(cli): mount install-local on a runtime with the cloud switched off#8358
os-zhuang merged 2 commits into
mainfrom
claude/issue-8343-install-local-offline-gate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of #8343.

Part of rather than Fixes: the card's first ask (registration not gated on resolveCloudUrl()) is implemented here; its closing line — "whichever component reports features.installLocal should report what is actually mounted" — was excluded by the dispatch scope and went to separate cards, one of which lands in another repo. See "What this PR does not do".

The premise held, but not for the reason it was checked

The card was dispatched with a falsifiable doubt attached: the wiring block is wrapped in if (!isRuntimeHostKernel), checked before the cloud-URL test, and if the measured objectos-ee image ran a runtime host kernel then the diagnosis was wrong and the fix belonged elsewhere. Measured, it does not:

  • ObjectOSEnvironmentPlugin is constructed in exactly one place, createObjectOSStack, which apps/objectos-ee/objectstack.config.ts calls only on its OS_MULTI_TENANT branch. The measured deployment is OS_MODE=standalone multi-org, which is a different axis, so it takes the single-environment branch and isRuntimeHostKernel is false.
  • Confirmed independently from the field data rather than by reading alone. The measured /api/v1/runtime/config carries autoPublishAiBuilds: true; the open framework plugin's static default for that key is false, and only the cloud subclass injects a resolveFeatures hook that returns true. So the response came from the cloud-wired RuntimeConfigPlugin, which only that single-environment branch mounts — which is also what proves the branch ran.

So the second guard did not fire, the cloud-URL guard is the one that skipped the mount, and the card's diagnosis is correct. premise_still_valid: true.

The fix

The block mounted a control-plane client and the local install surface behind one flag, though only the former needs a URL. handleInstall's inline-manifest branch reads no cloud URL at all — that is what makes os package install ./dist/objectstack.json the documented offline path. The condition is now split by what each surface actually needs:

  • cloud surfaces (browse proxy, cloud-connection, pushed runtime-config) — unchanged, still require a resolved URL;
  • install-local — mounts regardless, pinned to no control plane.

The decision moved into a pure Serve.planMarketplaceWiring() static, the same seam-for-testability idiom as Serve.providesCapability, because the call site sits deep inside run() behind a dynamic import where the only way to observe a mounting rule is to boot a kernel.

Two details that are easy to get wrong and are pinned by tests:

  1. The offline mount passes off, never the empty string already in scope. The plugin re-resolves whatever it is handed through resolveCloudUrl(), which reads '' as unset and substitutes the public DEFAULT_CLOUD_URL — so the obvious spelling would point an air-gapped runtime's catalog branch at cloud.objectos.ai, the exact opposite of what off requested, and invisible until a box with no egress hangs. It is a named constant so a test asserts the property rather than a comment claiming it.
  2. A host config that wires its own install-local keeps it. kernel.use keys plugins by name, so an unguarded mount would replace a host's instance — and a host may have built one with an explicit control-plane URL while OS_CLOUD_URL says off, making the replacement a silent capability downgrade.

Reconciling with the vanilla-dev intent

The gate's comment called itself deliberate — "OS_CLOUD_URL=off produces nothing mounted, preserving the vanilla marketplace-less objectstack dev" — and each plugin self-registers a Setup nav bundle, so the cost of mounting more had to be weighed rather than waved through. Measured, that cost is much narrower than it reads:

  • A plain objectstack dev sets no OS_CLOUD_URL. resolveCloudUrl() then returns DEFAULT_CLOUD_URL, which is truthy — so it takes the cloud arm and has been mounting install-local and its "Installed Apps" entry all along. Vanilla dev is byte-identical after this change.
  • The only runs affected are those that explicitly opted out (off/none/local/disabled) — the same population the card is about.
  • For those, the nav-ownership rule in marketplace-ui.ts ("the entry lives and dies with the capability, so no dead page") is satisfied rather than violated: the entry now appears exactly when a working offline install surface is behind it. Nothing that makes a remote call mounts under off.

off also is not an exotic setting here: that image's compose file ships OS_CLOUD_URL: ${OS_CLOUD_URL:-off}, so it is the default every self-hosted stack gets unless it overrides it.

And the package's own README already stated the intended contract in as many words — "OS_CLOUD_URL=off disables every remote call; air-gapped installs keep working via inline manifests handed to install-local" — so this makes the code agree with its documented contract rather than making a new product call.

Tests

packages/cli/test/serve-marketplace-offline-install.test.ts (9) pins the split in both directions, because a fix that merely mounts more would be indistinguishable from one that stopped honouring off: cloud-off mounts the offline surface and nothing that dials out; a resolved URL still mounts the full set; a runtime host kernel mounts neither arm (for both URL states, since the offline arm is a new path through that branch); a host wiring its own is left alone; plus a drift check that the matched identities are the real plugin's, which otherwise fails open.

packages/cloud-connection/src/marketplace-install-local-offline-degradation.test.ts (3) pins the property the fix rests on and nothing asserted: with no cloud, the catalog branch degrades locally — asserting code MARKETPLACE_UNAVAILABLE and status 503 together, since this handler never throws and a 404 from an unmounted route would satisfy a status-only check just as happily as the fix does.

Reverse-verified from the committed state, with the direction predicted first: restoring the pre-fix rule turns exactly 1 of 9 red (the offline-mount pin) and leaves 8 green; changing the constant to '' turns the trap pin red, reporting expected 'https://cloud.objectos.ai' to be ''.

What this PR does not do

Gates run locally

check:cross-package-test-inputs, check:startup-registry-verdict, check:type-check-coverage, check:type-check-debt (the four named at dispatch), plus check:nul-bytes and a control-character self-scan. Re-derived against the final diff with scripts/pm/dispatch-gates.mjs, which surfaced the .changeset-triggered families the dispatch list predated — check:changeset-gate-self-tests, check:objectui-changeset, check:query-options-erasure — all run and green. check:type-check-debt first refused to re-measure against an incomplete build closure; built the full closure as lint.yml does, after which it reported 33 ledger entries re-measured, none above its recorded number.

Also run: @objectstack/cli full suite (116 files / 1265 tests), @objectstack/cloud-connection full suite (17 files / 115 tests), tsc --noEmit for the CLI.


Generated by Claude Code

claude added 2 commits August 13, 2026 08:08
#8343)

`OS_CLOUD_URL=off` skipped the whole marketplace wiring block, including
`MarketplaceInstallLocalPlugin` — whose inline-manifest branch is the
documented air-gapped install path and reads no cloud URL at all. A
self-hosted EE box measured 404 on both GET and POST
/api/v1/marketplace/install-local, with no other install surface served.

Split the registration condition by what each surface needs: the
control-plane clients (browse proxy, cloud-connection, runtime-config)
still require a resolved URL; install-local mounts regardless, pinned to
no control plane so its catalog branch degrades to 503 locally instead
of dialling out to the public cloud. A host that wires its own keeps it;
the runtime host-kernel skip is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pY4Xb2iDecfDtT3CWoiTW
…d quote (#8343)

The card quoted the EE compose file as documenting `OS_CLOUD_URL=off` for
完全自托管. That phrase appears nowhere in the cloud repo — it is the
reporter's paraphrase, so it had no business sitting in a code comment as
a quotation. What the file actually says is stronger and checkable:
`OS_CLOUD_URL: ${OS_CLOUD_URL:-off}`, i.e. `off` is the SHIPPED DEFAULT,
so every self-hosted stack that does not override it hit this bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pY4Xb2iDecfDtT3CWoiTW
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 13, 2026 8:26am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

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

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)

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

  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v17.mdx (via @objectstack/cli)

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-actions github-actions Bot 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 08:40
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit 08dcd1e Aug 13, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8343-install-local-offline-gate branch August 13, 2026 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants