Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .changeset/runtime-config-install-local-derived.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
---
"@objectstack/cloud-connection": patch
---

fix(cloud-connection): `features.installLocal` is derived from what is mounted; the constructor option becomes a ceiling (#8388)

`GET /api/v1/runtime/config` reported `installLocal` straight from the
constructor option, next to a `marketplace` key that #8356 had just made an
observation. Two flags in one object, answered by different rules — and the
declared one is the key #8343 actually measured wrong on a real self-hosted
deployment:

```json
{"features":{"installLocal":true,"marketplace":true, …}}
```

```
GET /api/v1/marketplace/install-local -> 404 {"error":"Not found"}
POST /api/v1/marketplace/install-local -> 404 {"error":"Not found"}
```

Nothing checked that `MarketplaceInstallLocalPlugin` was mounted on the kernel
serving the response, so `new RuntimeConfigPlugin({ installLocal: true })` on a
runtime that never mounted it announced a capability whose route 404s, and the
Console rendered an install affordance that could not work.

The flag is now **observed** per request, off the route table of the app serving
the response — the same seam #8356 built, read by a sibling predicate rather than
a shared one, because the browse predicate subtracts exactly the paths this one
requires. The two share the prefix constant, so "what counts as install-local"
has one definition and the flags cannot both claim, or both disown, the same
route.

**The `installLocal` constructor option is kept, as a ceiling.** Hosts pass it
today, so it is not removed:

- omitted or `true` — report what is actually mounted (what every host passing
`installLocal: true` already meant);
- `false` — report `false` even where the plugin is mounted, for an operator who
wants the affordance hidden.

It deliberately cannot raise the answer. A plain override would have left the
measured defect standing: the CLI's own frozen `RUNTIME_CONFIG_OPTIONS` passes
`installLocal: true` unconditionally, so honouring `true` upward would keep
"declared `true`, route 404s" reachable on exactly the product path #8343
reported, leaving the derivation inert where it is most needed.

**What changes for hosts.** A runtime that mounts an install-local surface
reports exactly what it did before. A runtime that mounts none now reports
`installLocal: false` instead of whatever it declared — the correction. An
omitted option no longer means `false`: it defers to the observation, so a host
that mounts the plugin and forgot the flag now gets the truthful `true` it should
always have had.

**Escape hatch, unchanged.** The derivation is the base value, not a veto: the
open-core `resolveFeatures` seam still merges over it (and over the ceiling), so a
host on an adapter whose raw app exposes no route ledger — where both derived
flags conservatively report `false`, with a warning logged once at mount time —
can still declare the capability it knows it serves.
Original file line numberDiff line numberDiff line change
Expand Up@@ -311,13 +311,35 @@ describe('#8389: the wiring plan mounts runtime-config on the offline arm', () =
// One shared gate would have been the smaller change and would have
// excluded exactly this box: it serves install-local (its own), serves no
// runtime-config, and therefore has the #8389 defect in full.
//
// [#8388] "the host wires its OWN install-local" now has to mean the host
// MOUNTS one. `INSTALL_LOCAL` below is only an identity handed to the
// resolver, and since `features.installLocal` became an observation of the
// route table rather than the constructor flag, an identity with no route
// behind it models a box announcing a capability it cannot serve — #8343's
// measured defect, which is not this case's subject. So the host's own
// plugin is really started on the same app, ahead of the arm, exactly as
// the positive control mounts the real proxy. The assertions below are
// unchanged; what changed is that the fixture now earns them.
const dir = tempStorageDir();
try {
const { wiring, app } = await bootOfflineArm({ plugins: [INSTALL_LOCAL], storageDir: dir });
const { MarketplaceInstallLocalPlugin } = await import('@objectstack/cloud-connection');
const { wiring, app } = await bootOfflineArm({
plugins: [INSTALL_LOCAL],
storageDir: dir,
preMounted: [new MarketplaceInstallLocalPlugin({
controlPlaneUrl: Serve.OFFLINE_CONTROL_PLANE,
storageDir: dir,
})],
});

expect(wiring.offlineInstallLocal, "the host's own install-local is left alone").toBe(false);
expect(wiring.offlineRuntimeConfig).toBe(true);

// ...and the host's own mount is the ONLY install-local surface here —
// the arm added none, which is what `offlineInstallLocal: false` means.
expect(app.routes.some((r) => r.path.startsWith('/api/v1/marketplace/install-local'))).toBe(true);

const body = await readConfig(app);
expect(body.features.installLocal).toBe(true);
expect(body.features.marketplace).toBe(false);
Expand Down
11 changes: 8 additions & 3 deletions packages/cloud-connection/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,9 +40,14 @@ const plugins = [
// DEFAULT_CLOUD_URL. 'off' is one of the documented disable sentinels and
// is the value that actually resolves to no cloud.
new MarketplaceInstallLocalPlugin({ controlPlaneUrl: cloudUrl || 'off' }),
// NOT cloud-gated: features.marketplace is derived from what is actually
// mounted, not from this constructor call, so a cloud-less runtime reports
// marketplace: false on its own — there is nothing here to keep in sync.
// NOT cloud-gated: BOTH features.marketplace and features.installLocal are
// derived from what is actually mounted, not from this constructor call, so
// a cloud-less runtime reports marketplace: false and installLocal: true on
// its own — there is nothing here to keep in sync.
// `installLocal: true` is therefore a CEILING, not a declaration: it is the
// default, and it cannot make the flag report a route this runtime never
// mounted. Pass `false` to hide the affordance on a box that could serve it;
// omitting it entirely behaves the same as `true`.
// `''` 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'.
Expand Down
Loading
Loading