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
20 changes: 20 additions & 0 deletions .changeset/manifest-bridge-arm-on-project-kernels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@objectstack/objectql": patch
---

fix(objectql): arm the late-manifest metadata bridge on project kernels too

The per-manifest bridge added for marketplace installs (#3428) armed itself
inside the same `environmentId === undefined` gate as the one-shot startup
bridge — but `os dev` boots the kernel project-scoped (environmentId
'env_local'), which is marketplace install-local's primary home, so the fix
was inert exactly where it matters. Caught by browser-dogfooding the install
flow.

The gate is correct for the one-shot bridge (it copies the entire
process-wide SchemaRegistry, which would leak sibling-project objects on
multi-environment servers) but does not apply to the per-manifest bridge: it
only copies the objects of the one package this kernel just registered.
Arming now happens unconditionally at the end of `start()`; boot-time
behavior on every kernel shape is unchanged (the flag still flips only after
the startup path has run), and the one-shot bridge keeps its gate.
20 changes: 20 additions & 0 deletions packages/objectql/src/plugin.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,26 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
expect(kept.label).toBe('Authored Contact');
});

it('bridges late installs on project kernels too (`os dev` passes environmentId)', async () => {
// `os dev` boots the kernel with environmentId 'env_local', so the
// one-shot startup bridge is (correctly) skipped there — but the
// per-manifest bridge must still arm, or marketplace install-local
// (whose primary home IS `os dev`) never reaches the metadata
// service. Unlike the one-shot bridge, bridging one just-registered
// package cannot leak sibling-project objects, so the environmentId
// gate does not apply to it.
await kernel.use(new ObjectQLPlugin({ environmentId: 'env_local' }));
await kernel.bootstrap();

const manifest = kernel.getService('manifest') as ManifestService;
await manifest.register(crmManifest());

const metadata = kernel.getService('metadata') as any;
const bridged = await metadata.getObject('crm_contact');
expect(bridged).toBeDefined();
expect(bridged.label).toBe('Contact');
});

it('boot-time registrations still flow through the one-shot startup bridge', async () => {
await kernel.use(new ObjectQLPlugin());
await kernel.use({
Expand Down
44 changes: 28 additions & 16 deletions packages/objectql/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,17 @@ export class ObjectQLPlugin implements Plugin {
private reloadSchemaSync: Promise<void> = Promise.resolve();
private hydrateMetadataFromDb = false;
/**
* Armed once `start()` has run the one-shot
* {@link bridgeObjectsToMetadataService}. From that point on, every
* manifest registered through the `manifest` service bridges its own
* objects into the metadata service incrementally — the one-shot bridge
* never runs again, so late registrations (marketplace install /
* rehydrate on `kernel:ready`) would otherwise stay invisible to every
* IMetadataService consumer. Stays false on project kernels
* (`environmentId` set), matching the one-shot bridge's gate.
* Armed at the end of `start()` (AFTER the one-shot
* {@link bridgeObjectsToMetadataService}, where that runs). From that
* point on, every manifest registered through the `manifest` service
* bridges its own objects into the metadata service incrementally — the
* one-shot bridge never runs again, so late registrations (marketplace
* install / rehydrate on `kernel:ready`) would otherwise stay invisible
* to every IMetadataService consumer. Armed on ALL kernels, including
* project-scoped ones (`environmentId` set — `os dev` boots as
* 'env_local'): unlike the one-shot registry-wide bridge, a per-manifest
* bridge cannot leak sibling-project objects, and gating it would turn
* the fix off in marketplace install-local's primary home.
*/
private bridgeLateManifests = false;
/** Unsubscribe handles for metadata-event subscriptions (ADR-0008 PR-7). */
Expand Down Expand Up @@ -541,15 +544,24 @@ export class ObjectQLPlugin implements Plugin {
// skip it in that case.
if (this.environmentId === undefined) {
await this.bridgeObjectsToMetadataService(ctx);
// The one-shot bridge above covered everything registered so far.
// Arm the incremental per-manifest bridge for everything after —
// marketplace install / rehydrate register through the `manifest`
// service on `kernel:ready`, long after this line, and without the
// incremental bridge their objects never reach the metadata service
// (AI describe_object, Studio object lists, metadata.listObjects all
// miss them; only the seed loader has an engine fallback, #3422).
this.bridgeLateManifests = true;
}
// Arm the incremental per-manifest bridge for everything after start() —
// marketplace install / rehydrate register through the `manifest`
// service on `kernel:ready`, long after this line, and without the
// incremental bridge their objects never reach the metadata service
// (AI describe_object, Studio object lists, metadata.listObjects all
// miss them; only the seed loader has an engine fallback, #3422).
//
// Deliberately NOT inside the `environmentId === undefined` gate above:
// that gate exists because the one-shot bridge copies the ENTIRE
// process-wide SchemaRegistry (cross-project leakage on multi-env
// servers), whereas the per-manifest bridge only copies the objects of
// the one package THIS kernel just registered — nothing to leak. And
// `os dev` boots project-scoped (environmentId 'env_local'), which is
// marketplace install-local's primary home: gating on environmentId
// would switch the fix off exactly where it matters (caught by
// browser-dogfooding the install flow).
this.bridgeLateManifests = true;

// Register built-in audit hooks
this.registerAuditHooks(ctx);
Expand Down