Skip to content

Commit 3c1b2ad

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-8370-filter-refusal-adr0087-entry
2 parents 58d8de5 + 189a732 commit 3c1b2ad

6 files changed

Lines changed: 717 additions & 12 deletions
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
fix(rest): anchor `looksLikeMissingRelation` on the driver's quoted template (#8264)
6+
7+
`mapDataError`'s Postgres limb read `relation` and `does not exist` anywhere in
8+
the message, not necessarily the same sentence — so ordinary business prose
9+
using both words (`This relation does not exist in the diagram`) matched.
10+
`does not exist` is ordinary business English; #8132 already anchored the
11+
shared `@objectstack/types` leak predicate on the driver's own quoted
12+
template for exactly this reason, and pinned the identical string as a
13+
negative case. This file's copy of the same question was not covered by that
14+
change (different package, different call site) and kept the loose reading.
15+
16+
Anchored the same way here — a quoted identifier required between `relation`
17+
and `does not exist` — as a locally-owned pattern rather than a call into the
18+
shared leak predicate: that
19+
predicate answers a different question ("may this be withheld from the
20+
client"), and its other limbs (`sqlite_`, `unique constraint`, `foreign key`,
21+
a bare SQL statement) have nothing to do with this file's question (is this
22+
specifically an unknown-relation condition, for the 404-vs-500 split
23+
`looksLikeMissingRelation` feeds). `relation-sub-object.ts` documents "two
24+
widths, on purpose" for a neighbouring pair of consumers that ask genuinely
25+
different questions; that does not extend to the two USES inside this file,
26+
which both ask the same question and share one predicate correctly.
27+
28+
**Both of the predicate's two call sites are covered, not just the reported
29+
one:** the `DATA_STORE_FAULT` (500) gate the issue named, and the
30+
`looksLikeUnknownObject` (404) limb the issue's own text did not measure. A
31+
business message no longer gets mislabelled a `DATABASE_ERROR`, and a
32+
crafted unquoted-but-attributable message no longer gets silently answered
33+
`OBJECT_NOT_FOUND` — both now fall through to the generic, still-sanitised
34+
terminal fault, which is the direction the branch's own #5462 comment already
35+
argues for ("the safe way to be wrong is loud").
36+
37+
No reachable production path producing the unanchored shape was found at this
38+
call site — this is consistency/invariant restoration between two spellings
39+
of one question, not a fix for a demonstrated live misclassification.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
`serve`: the cloud-connected marketplace arm now leaves a host config's own marketplace and cloud plugins alone.
6+
7+
`objectstack serve` auto-wires `MarketplaceProxyPlugin`, `MarketplaceInstallLocalPlugin`, the same-origin cloud-connection surface and `RuntimeConfigPlugin` whenever a cloud URL resolves. Each of those four mounts is now guarded on whether the loaded host config already wired that surface — the same presence check the offline arm has carried since the install-local fix — so CLI auto-wiring is a fallback for hosts that wire nothing rather than a second opinion about a surface the host already composed.
8+
9+
No behaviour changes for any current deployment: `Kernel.use()` keys plugins by `plugin.name` and the host's registration runs after the CLI's, so the host's instance already won by ordering. What changes is that it now wins by rule instead of by the relative position of two blocks that never referenced each other, and the CLI stops constructing four plugins it was about to discard. It becomes visible the moment a host passes an argument the CLI cannot — a private control plane, a custom install `storageDir`, a credential path, white-label branding.

packages/cli/src/commands/serve.ts

Lines changed: 125 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,35 @@ export default class Serve extends Command {
412412
'RuntimeConfigPlugin',
413413
];
414414

415+
/**
416+
* Identities of the marketplace BROWSE surface (`MarketplaceProxyPlugin`),
417+
* matched EXACTLY by {@link Serve.providesCapability}.
418+
*
419+
* Cloud-arm counterpart of {@link Serve.INSTALL_LOCAL_IDENTITIES} (#8357).
420+
* The host's instance carries its own `controlPlaneUrl`, its public-snapshot
421+
* base URL and its LRU cache tuning — none of which the CLI's auto-wiring
422+
* can know, because it constructs from `resolveCloudUrl()` alone.
423+
*/
424+
static readonly MARKETPLACE_PROXY_IDENTITIES: readonly string[] = [
425+
'com.objectstack.runtime.marketplace-proxy',
426+
'MarketplaceProxyPlugin',
427+
];
428+
429+
/**
430+
* Identities of the same-origin cloud-connection surface
431+
* (`CloudConnectionPlugin`, built by `createCloudConnectionPlugin`), matched
432+
* EXACTLY by {@link Serve.providesCapability}.
433+
*
434+
* Both spellings matter here for a reason the other three do not have: the
435+
* host reaches this plugin through a FACTORY, so the only class name in
436+
* play is the one the factory returns. `createCloudConnectionPlugin` is not
437+
* an identity — a factory function is not what lands in the kernel.
438+
*/
439+
static readonly CLOUD_CONNECTION_IDENTITIES: readonly string[] = [
440+
'com.objectstack.cloud.connection',
441+
'CloudConnectionPlugin',
442+
];
443+
415444
/**
416445
* Constructor options for the `RuntimeConfigPlugin` the marketplace wiring
417446
* mounts — ONE object, shared by both arms on purpose (#8389).
@@ -466,8 +495,11 @@ export default class Serve extends Command {
466495
* The two arms are deliberately asymmetric, because the two surfaces need
467496
* different things:
468497
*
469-
* - `cloudSurfaces` — proxy + cloud-connection + runtime-config. These
470-
* *are* the control plane's client, so a resolved URL is their precondition.
498+
* - `cloudSurfaces` — the ARM SELECTOR, not a mount decision: true when this
499+
* boot takes the cloud-connected arm at all (proxy + install-local +
500+
* cloud-connection + runtime-config). Those surfaces *are* the control
501+
* plane's client, so a resolved URL is their precondition. WHICH of them
502+
* the CLI actually mounts is carried by the four `cloud*` flags below.
471503
* - `offlineInstallLocal` — the air-gapped install surface. Its inline
472504
* branch reads no URL at all, so a control plane is precisely what it
473505
* does NOT need; gating it on one is what left a self-hosted EE box with
@@ -493,20 +525,74 @@ export default class Serve extends Command {
493525
* skip the dynamic import) so this function is the whole rule in one place:
494526
* the cloud distribution wires its own marketplace on the host kernel, so
495527
* NO arm mounts there.
528+
*
529+
* ## The cloud arm honours the host too (#8357)
530+
*
531+
* Every mount on BOTH arms is now per-surface, under the same
532+
* `providesCapability` rule: the CLI's auto-wiring is a FALLBACK for hosts
533+
* that wire nothing, never a second opinion about a surface the host already
534+
* composed. `objectos-ee`'s single-environment config is exactly such a host
535+
* — it wires proxy, install-local, cloud-connection and runtime-config
536+
* itself — and it is NOT covered by the `isRuntimeHostKernel` guard above,
537+
* which detects `ObjectOSEnvironmentPlugin`: only the `OS_MULTI_TENANT`
538+
* branch constructs one, via `createObjectOSStack`. Hanging this rule off
539+
* that sentinel would leave the shipped single-environment shape unguarded.
540+
*
541+
* What this fixes is PRECEDENCE, not a live downgrade — say it plainly,
542+
* because the two read alike and only one is true. Measured on this tree:
543+
* the CLI's wiring block runs several hundred lines BEFORE `config.plugins`
544+
* are registered, and `Kernel.use` -> `this.plugins.set(name, meta)`
545+
* overwrites by name, so today the host's instance is the one that survives
546+
* — the CLI's is constructed, registered and then dropped. The host winning
547+
* is therefore an ACCIDENT OF ORDERING between two blocks that never mention
548+
* each other, not a rule anything states or pins; it inverts silently if
549+
* either block moves, and on a kernel whose `use` rejects duplicates
550+
* (`LiteKernel` throws) it is the HOST's registration that fails instead.
551+
* Checking presence makes the outcome independent of all of that, and stops
552+
* the CLI constructing four plugins it is about to discard.
496553
*/
497554
static planMarketplaceWiring(input: {
498555
isRuntimeHostKernel: boolean;
499556
marketplaceUrl: string;
500557
plugins: readonly unknown[];
501-
}): { cloudSurfaces: boolean; offlineInstallLocal: boolean; offlineRuntimeConfig: boolean } {
558+
}): {
559+
cloudSurfaces: boolean;
560+
cloudProxy: boolean;
561+
cloudInstallLocal: boolean;
562+
cloudConnection: boolean;
563+
cloudRuntimeConfig: boolean;
564+
offlineInstallLocal: boolean;
565+
offlineRuntimeConfig: boolean;
566+
} {
567+
const NO_CLOUD_ARM = {
568+
cloudSurfaces: false,
569+
cloudProxy: false,
570+
cloudInstallLocal: false,
571+
cloudConnection: false,
572+
cloudRuntimeConfig: false,
573+
} as const;
574+
502575
if (input.isRuntimeHostKernel) {
503-
return { cloudSurfaces: false, offlineInstallLocal: false, offlineRuntimeConfig: false };
576+
return { ...NO_CLOUD_ARM, offlineInstallLocal: false, offlineRuntimeConfig: false };
504577
}
505578
if (input.marketplaceUrl) {
506-
return { cloudSurfaces: true, offlineInstallLocal: false, offlineRuntimeConfig: false };
579+
return {
580+
cloudSurfaces: true,
581+
// Each surface is guarded on its OWN presence, never on a shared gate:
582+
// a host may compose any subset of the four (objectos-ee wires
583+
// runtime-config unconditionally but the other three only when it has
584+
// a resolved cloud URL), and one gate would either overwrite what the
585+
// host did wire or withhold what it did not.
586+
cloudProxy: !Serve.providesCapability(input.plugins, Serve.MARKETPLACE_PROXY_IDENTITIES),
587+
cloudInstallLocal: !Serve.providesCapability(input.plugins, Serve.INSTALL_LOCAL_IDENTITIES),
588+
cloudConnection: !Serve.providesCapability(input.plugins, Serve.CLOUD_CONNECTION_IDENTITIES),
589+
cloudRuntimeConfig: !Serve.providesCapability(input.plugins, Serve.RUNTIME_CONFIG_IDENTITIES),
590+
offlineInstallLocal: false,
591+
offlineRuntimeConfig: false,
592+
};
507593
}
508594
return {
509-
cloudSurfaces: false,
595+
...NO_CLOUD_ARM,
510596
// A host config that wires its own install-local keeps it — see the
511597
// call site for why replacing it would be a silent downgrade.
512598
offlineInstallLocal: !Serve.providesCapability(input.plugins, Serve.INSTALL_LOCAL_IDENTITIES),
@@ -1967,15 +2053,43 @@ export default class Serve extends Command {
19672053
const marketplaceUrl = resolveCloudUrl();
19682054
const wiring = Serve.planMarketplaceWiring({ isRuntimeHostKernel, marketplaceUrl, plugins });
19692055
if (wiring.cloudSurfaces) {
1970-
await kernel.use(new MarketplaceProxyPlugin({ controlPlaneUrl: marketplaceUrl }));
1971-
await kernel.use(new MarketplaceInstallLocalPlugin({ controlPlaneUrl: marketplaceUrl }));
2056+
// Every mount here is guarded on what the HOST already wired
2057+
// (#8357), the same rule and the same idiom the offline arm below
2058+
// uses. `kernel.use` keys by name, so an unguarded mount is not a
2059+
// harmless double-mount: one of the two instances is discarded,
2060+
// and WHICH one depends on registration order rather than on any
2061+
// stated rule. The host composed its instance deliberately, with
2062+
// arguments the CLI cannot reconstruct from `resolveCloudUrl()`
2063+
// alone — a distinct control plane, a custom install storageDir, a
2064+
// credential path, cache tuning — so the host's is the one that
2065+
// must stand. See `planMarketplaceWiring` for the measurement.
2066+
let mountedAny = false;
2067+
if (wiring.cloudProxy) {
2068+
await kernel.use(new MarketplaceProxyPlugin({ controlPlaneUrl: marketplaceUrl }));
2069+
mountedAny = true;
2070+
}
2071+
if (wiring.cloudInstallLocal) {
2072+
await kernel.use(new MarketplaceInstallLocalPlugin({ controlPlaneUrl: marketplaceUrl }));
2073+
mountedAny = true;
2074+
}
19722075
// Same-origin /cloud-connection/* surface (status + device-code
19732076
// bind + control-plane catalog views) in single-environment mode.
1974-
await kernel.use(createCloudConnectionPlugin({ singleEnvironment: true, controlPlaneUrl: marketplaceUrl }));
2077+
if (wiring.cloudConnection) {
2078+
await kernel.use(createCloudConnectionPlugin({ singleEnvironment: true, controlPlaneUrl: marketplaceUrl }));
2079+
mountedAny = true;
2080+
}
19752081
// Server-pushed runtime config so the Console knows marketplace +
19762082
// install-local are live (same-origin; install into THIS kernel).
1977-
await kernel.use(new RuntimeConfigPlugin({ ...Serve.RUNTIME_CONFIG_OPTIONS }));
1978-
trackPlugin('Marketplace');
2083+
if (wiring.cloudRuntimeConfig) {
2084+
await kernel.use(new RuntimeConfigPlugin({ ...Serve.RUNTIME_CONFIG_OPTIONS }));
2085+
mountedAny = true;
2086+
}
2087+
// Report the banner line only when this block actually mounted
2088+
// something. A host that wires the whole set gets no entry from
2089+
// here — it will report its own plugins through the config-plugin
2090+
// loader — and an unconditional `trackPlugin` would otherwise
2091+
// credit the CLI with a mount it did not make.
2092+
if (mountedAny) trackPlugin('Marketplace');
19792093
} else if (wiring.offlineInstallLocal || wiring.offlineRuntimeConfig) {
19802094
// Cloud explicitly disabled -> mount the OFFLINE surfaces only:
19812095
// the install route, and the runtime config that makes it

0 commit comments

Comments
 (0)