diff --git a/src/plugins/memory-authorization-path-inventory.test.ts b/src/plugins/memory-authorization-path-inventory.test.ts index 6a17409d9353..ce2e911938b1 100644 --- a/src/plugins/memory-authorization-path-inventory.test.ts +++ b/src/plugins/memory-authorization-path-inventory.test.ts @@ -15,7 +15,6 @@ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".. const REQUIRED_PHASE_0_PATH_IDS = [ "selected-runtime-manager-acquisition", "selected-runtime-backend-resolution", - "selected-runtime-startup-warmup-and-sync", "bootstrap-memory-and-user-files", "startup-recent-memory-context", "memory-search-tool", diff --git a/src/plugins/memory-authorization-path-inventory.ts b/src/plugins/memory-authorization-path-inventory.ts index a65cb11c0b35..e3530a936646 100644 --- a/src/plugins/memory-authorization-path-inventory.ts +++ b/src/plugins/memory-authorization-path-inventory.ts @@ -63,14 +63,6 @@ export const MEMORY_AUTHORIZATION_PATH_INVENTORY = Object.freeze([ "legacy-only", "src/plugins/memory-runtime.ts", ), - entry( - "selected-runtime-startup-warmup-and-sync", - "control", - "selected-memory-plugin", - "blocked-in-enforced-mode", - "extensions/memory-core/index.ts", - "src/gateway/server-startup-memory.ts", - ), entry( "bootstrap-memory-and-user-files", "egress", diff --git a/src/plugins/memory-authorization-runtime.test.ts b/src/plugins/memory-authorization-runtime.test.ts index 4ad3d5fb3a75..9796d90babca 100644 --- a/src/plugins/memory-authorization-runtime.test.ts +++ b/src/plugins/memory-authorization-runtime.test.ts @@ -4,8 +4,9 @@ import { LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES, MEMORY_AUTHORIZATION_CAPABILITY_NAMES, } from "../memory-host-sdk/host/authorization.js"; -import { inspectMemoryAuthorizationRuntime } from "./memory-authorization-runtime.js"; +import { inspectMemoryAuthorizationCapability } from "./memory-authorization-runtime.js"; import { observeMemoryAuthorizationShadowSurface } from "./memory-authorization-shadow.js"; +import { createEmptyPluginRegistry } from "./registry-empty.js"; const AUTHORIZED_METHOD_NAMES = [ "authorize", @@ -18,12 +19,11 @@ const AUTHORIZED_METHOD_NAMES = [ "statusAuthorized", ] as const; -function createRuntime(capabilities = COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES) { +function createRuntime() { const notCalled = vi.fn(() => { throw new Error("runtime methods must not execute in shadow mode"); }); return { - authorization: capabilities, authorize: notCalled, searchAuthorized: notCalled, readAuthorized: notCalled, @@ -37,8 +37,6 @@ function createRuntime(capabilities = COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES } class PrototypeRuntime { - readonly authorization = COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES; - authorize() { throw new Error("runtime methods must not execute in shadow mode"); } @@ -72,10 +70,18 @@ class PrototypeRuntime { } } -describe("memory authorization runtime inspection", () => { +class PrototypeCapability { + readonly authorization = COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES; + readonly runtime = new PrototypeRuntime(); +} + +describe("memory authorization capability inspection", () => { it("reports a complete declared surface without calling an authorized or legacy method", () => { const runtime = createRuntime(); - const inspection = inspectMemoryAuthorizationRuntime(runtime); + const inspection = inspectMemoryAuthorizationCapability({ + authorization: COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES, + runtime, + }); expect(inspection).toMatchObject({ version: 1, @@ -94,12 +100,13 @@ describe("memory authorization runtime inspection", () => { }); it("reports all-false and incomplete declarations as nonconforming", () => { - const legacy = inspectMemoryAuthorizationRuntime( - createRuntime(LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES), - ); - const incomplete = inspectMemoryAuthorizationRuntime({ + const legacy = inspectMemoryAuthorizationCapability({ + authorization: LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES, + runtime: createRuntime(), + }); + const incomplete = inspectMemoryAuthorizationCapability({ authorization: { ...COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES, scopedSync: false }, - ...Object.fromEntries(AUTHORIZED_METHOD_NAMES.map((name) => [name, () => undefined])), + runtime: Object.fromEntries(AUTHORIZED_METHOD_NAMES.map((name) => [name, () => undefined])), }); expect(legacy).toMatchObject({ @@ -119,16 +126,16 @@ describe("memory authorization runtime inspection", () => { }); it("uses the SDK's exact capability-declaration rules for shadow reporting", () => { - const unexpectedCapability = inspectMemoryAuthorizationRuntime({ - ...createRuntime(), + const unexpectedCapability = inspectMemoryAuthorizationCapability({ authorization: { ...COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES, unexpected: true }, + runtime: createRuntime(), }); - const symbolicCapability = inspectMemoryAuthorizationRuntime({ - ...createRuntime(), + const symbolicCapability = inspectMemoryAuthorizationCapability({ authorization: { ...COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES, [Symbol("plugin-framework-metadata")]: true, }, + runtime: createRuntime(), }); expect(unexpectedCapability).toMatchObject({ @@ -144,12 +151,12 @@ describe("memory authorization runtime inspection", () => { }); it("accepts data descriptors from class prototypes and ignores unrelated symbols", () => { - const runtime = new PrototypeRuntime(); - Object.defineProperty(runtime, Symbol("plugin-framework-metadata"), { + const capability = new PrototypeCapability(); + Object.defineProperty(capability, Symbol("plugin-framework-metadata"), { value: "not an authorization capability", }); - expect(inspectMemoryAuthorizationRuntime(runtime)).toMatchObject({ + expect(inspectMemoryAuthorizationCapability(capability)).toMatchObject({ capabilityDeclaration: "complete", implementedMethodCount: AUTHORIZED_METHOD_NAMES.length, surfaceComplete: true, @@ -160,13 +167,6 @@ describe("memory authorization runtime inspection", () => { it("fails closed on accessor and proxy surfaces without evaluating their getters or methods", () => { let getterCalls = 0; const accessorRuntime = Object.create(null) as Record; - Object.defineProperty(accessorRuntime, "authorization", { - enumerable: true, - get() { - getterCalls += 1; - throw new Error("must not read authorization getter"); - }, - }); for (const name of AUTHORIZED_METHOD_NAMES) { Object.defineProperty(accessorRuntime, name, { enumerable: true, @@ -176,7 +176,31 @@ describe("memory authorization runtime inspection", () => { }, }); } - const proxyRuntime = new Proxy( + const accessorCapability = Object.create(null) as Record; + Object.defineProperty(accessorCapability, "authorization", { + enumerable: true, + value: COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES, + }); + Object.defineProperty(accessorCapability, "runtime", { + enumerable: true, + value: accessorRuntime, + }); + const capabilityGetter = Object.create(null) as Record; + Object.defineProperty(capabilityGetter, "authorization", { + enumerable: true, + get() { + getterCalls += 1; + throw new Error("must not read authorization getter"); + }, + }); + Object.defineProperty(capabilityGetter, "runtime", { + enumerable: true, + get() { + getterCalls += 1; + throw new Error("must not read runtime getter"); + }, + }); + const proxyCapability = new Proxy( {}, { getOwnPropertyDescriptor() { @@ -184,7 +208,7 @@ describe("memory authorization runtime inspection", () => { }, }, ); - const declarationProxyRuntime = { + const declarationProxyCapability = { authorization: new Proxy( {}, { @@ -193,15 +217,24 @@ describe("memory authorization runtime inspection", () => { }, }, ), + runtime: createRuntime(), }; - const accessor = inspectMemoryAuthorizationRuntime(accessorRuntime); - const undefinedDeclaration = inspectMemoryAuthorizationRuntime({ authorization: undefined }); - const proxy = inspectMemoryAuthorizationRuntime(proxyRuntime); - const declarationProxy = inspectMemoryAuthorizationRuntime(declarationProxyRuntime); + const accessor = inspectMemoryAuthorizationCapability(accessorCapability); + const getter = inspectMemoryAuthorizationCapability(capabilityGetter); + const undefinedDeclaration = inspectMemoryAuthorizationCapability({ + authorization: undefined, + runtime: createRuntime(), + }); + const proxy = inspectMemoryAuthorizationCapability(proxyCapability); + const declarationProxy = inspectMemoryAuthorizationCapability(declarationProxyCapability); expect(getterCalls).toBe(0); expect(accessor).toMatchObject({ + capabilityDeclaration: "complete", + reasonCode: "backend-nonconforming", + }); + expect(getter).toMatchObject({ capabilityDeclaration: "malformed", reasonCode: "backend-nonconforming", }); @@ -221,15 +254,17 @@ describe("memory authorization runtime inspection", () => { }); describe("memory authorization shadow inspection", () => { - it("returns one bounded, content-free observation per selected runtime", () => { - const runtime = Object.assign(createRuntime(LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES), { + it("returns one bounded, content-free observation per selected registry", () => { + const runtime = Object.assign(createRuntime(), { content: "private content sentinel", prompt: "private prompt sentinel", query: "private query sentinel", principalId: "private principal sentinel", }); - const first = observeMemoryAuthorizationShadowSurface(runtime); - const second = observeMemoryAuthorizationShadowSurface(runtime); + const capability = { authorization: LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES, runtime }; + const registry = createEmptyPluginRegistry(); + const first = observeMemoryAuthorizationShadowSurface({ capability, registry }); + const second = observeMemoryAuthorizationShadowSurface({ capability, registry }); expect(first).toMatchObject({ mode: "shadow", @@ -240,8 +275,39 @@ describe("memory authorization shadow inspection", () => { expect(JSON.stringify(first)).not.toMatch(/private|content|prompt|query|principal/u); }); - it("does not let a hostile proxy change a selected runtime path", () => { - const runtime = new Proxy( + it("observes runtime-less selected capabilities without invoking or creating a runtime", () => { + const observation = observeMemoryAuthorizationShadowSurface({ + capability: { authorization: COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES }, + registry: createEmptyPluginRegistry(), + }); + + expect(observation).toEqual( + expect.objectContaining({ + capabilityDeclaration: "complete", + implementedMethodCount: 0, + surfaceComplete: false, + reasonCode: "backend-nonconforming", + }), + ); + }); + + it("deduplicates by selected registry rather than a shared runtime object", () => { + const runtime = createRuntime(); + const first = observeMemoryAuthorizationShadowSurface({ + capability: { authorization: COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES, runtime }, + registry: createEmptyPluginRegistry(), + }); + const second = observeMemoryAuthorizationShadowSurface({ + capability: { authorization: COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES, runtime }, + registry: createEmptyPluginRegistry(), + }); + + expect(first).toEqual(expect.objectContaining({ surfaceComplete: true })); + expect(second).toEqual(expect.objectContaining({ surfaceComplete: true })); + }); + + it("does not let a hostile proxy change a selected capability path", () => { + const capability = new Proxy( {}, { getOwnPropertyDescriptor() { @@ -249,7 +315,10 @@ describe("memory authorization shadow inspection", () => { }, }, ); - const observation = observeMemoryAuthorizationShadowSurface(runtime); + const observation = observeMemoryAuthorizationShadowSurface({ + capability, + registry: createEmptyPluginRegistry(), + }); expect(observation).toEqual( expect.objectContaining({ reasonCode: "backend-nonconforming", surfaceComplete: false }), diff --git a/src/plugins/memory-authorization-runtime.ts b/src/plugins/memory-authorization-runtime.ts index 9ef8abca82c7..9d9419ec37a6 100644 --- a/src/plugins/memory-authorization-runtime.ts +++ b/src/plugins/memory-authorization-runtime.ts @@ -19,7 +19,7 @@ const AUTHORIZED_MEMORY_RUNTIME_METHODS = [ type AuthorizedMemoryRuntimeMethodName = (typeof AUTHORIZED_MEMORY_RUNTIME_METHODS)[number]; -type MemoryAuthorizationRuntimeInspection = Readonly<{ +type MemoryAuthorizationCapabilityInspection = Readonly<{ version: 1; capabilityDeclaration: "missing" | "malformed" | "partial" | "complete"; declaredCapabilityCount: number; @@ -32,9 +32,9 @@ type MemoryAuthorizationRuntimeInspection = Readonly<{ reasonCode: "surface-complete" | "backend-nonconforming"; }>; -// Runtime interfaces are shallow; the bound prevents hostile prototype chains from extending a -// shadow-only inspection beyond its fixed metadata budget. -const MAXIMUM_RUNTIME_PROTOTYPE_DEPTH = 8; +// Capability and runtime interfaces are shallow; the bound prevents hostile prototype chains +// from extending a shadow-only inspection beyond its fixed metadata budget. +const MAXIMUM_INSPECTION_PROTOTYPE_DEPTH = 8; function isObjectReference(value: unknown): value is object { return (typeof value === "object" && value !== null) || typeof value === "function"; @@ -45,10 +45,9 @@ type DataPropertyLookup = | { kind: "missing" | "accessor" | "unavailable" }; /** - * Reads a data descriptor without evaluating the corresponding property. Runtime interfaces may - * use class methods, so the bounded prototype walk accepts data descriptors there too. A getter - * or hostile reflection failure remains nonconforming without touching an authorized or legacy - * runtime method. + * Reads a data descriptor without evaluating the corresponding property. Capability and runtime + * interfaces may use class methods, so the bounded prototype walk accepts data descriptors there + * too. A getter or hostile reflection failure remains nonconforming without touching a method. */ function readDataProperty(value: unknown, key: string): DataPropertyLookup { if (!isObjectReference(value)) { @@ -56,7 +55,7 @@ function readDataProperty(value: unknown, key: string): DataPropertyLookup { } try { let current: object | null = value; - for (let depth = 0; current && depth < MAXIMUM_RUNTIME_PROTOTYPE_DEPTH; depth += 1) { + for (let depth = 0; current && depth < MAXIMUM_INSPECTION_PROTOTYPE_DEPTH; depth += 1) { const descriptor = Object.getOwnPropertyDescriptor(current, key); if (descriptor) { return "value" in descriptor @@ -73,8 +72,8 @@ function readDataProperty(value: unknown, key: string): DataPropertyLookup { } /** - * The SDK validator deliberately enforces exact descriptor shape. A plugin runtime can still be - * a hostile Proxy, so shadow inspection turns any reflection failure into a nonconforming result. + * The SDK validator deliberately enforces exact descriptor shape. A plugin capability can still + * be a hostile Proxy, so shadow inspection turns any reflection failure into a nonconforming result. */ function inspectCapabilityDeclaration(value: unknown): { hasWellFormedDeclaration: boolean; @@ -99,7 +98,7 @@ function inspectCapabilityDeclaration(value: unknown): { } } -function freezeInspection(params: Omit) { +function freezeInspection(params: Omit) { return Object.freeze({ version: 1 as const, ...params, @@ -110,12 +109,12 @@ function freezeInspection(params: Omit ({ name, - property: readDataProperty(runtime, name), + property: readDataProperty(runtime.kind === "data" ? runtime.value : undefined, name), })); const implementedMethodCount = methods.filter( ({ property }) => property.kind === "data" && typeof property.value === "function", diff --git a/src/plugins/memory-authorization-shadow.ts b/src/plugins/memory-authorization-shadow.ts index 0f286799d6cb..0f4b435bc107 100644 --- a/src/plugins/memory-authorization-shadow.ts +++ b/src/plugins/memory-authorization-shadow.ts @@ -1,7 +1,8 @@ import { MEMORY_AUTHORIZATION_CONTRACT_VERSION } from "../memory-host-sdk/host/authorization.js"; -import { inspectMemoryAuthorizationRuntime } from "./memory-authorization-runtime.js"; +import { inspectMemoryAuthorizationCapability } from "./memory-authorization-runtime.js"; +import type { PluginRegistry } from "./registry-types.js"; -const inspectedRuntimes = new WeakSet(); +const inspectedRegistries = new WeakSet(); type MemoryAuthorizationShadowMetadata = Readonly<{ event: "memory-authorization-backend-surface"; @@ -16,26 +17,19 @@ type MemoryAuthorizationShadowMetadata = Readonly<{ reasonCode: "surface-complete" | "backend-nonconforming"; }>; -function isObjectReference(value: unknown): value is object { - return (typeof value === "object" && value !== null) || typeof value === "function"; -} - /** - * Shadow mode returns bounded surface counts once per selected runtime. Reflection failures are + * Shadow mode returns bounded surface counts once per selected registry. Reflection failures are * nonconforming observations; logging remains with the runtime owner and cannot change selection. */ export function observeMemoryAuthorizationShadowSurface( - runtime: unknown, + params: Readonly<{ capability: unknown; registry: PluginRegistry }>, ): MemoryAuthorizationShadowMetadata | undefined { - if (!isObjectReference(runtime)) { - return undefined; - } try { - if (inspectedRuntimes.has(runtime)) { + if (inspectedRegistries.has(params.registry)) { return undefined; } - inspectedRuntimes.add(runtime); - const inspection = inspectMemoryAuthorizationRuntime(runtime); + inspectedRegistries.add(params.registry); + const inspection = inspectMemoryAuthorizationCapability(params.capability); const metadata = Object.freeze({ event: "memory-authorization-backend-surface" as const, mode: "shadow" as const, diff --git a/src/plugins/memory-runtime.test.ts b/src/plugins/memory-runtime.test.ts index b1d7b85e51f0..1c5a25314633 100644 --- a/src/plugins/memory-runtime.test.ts +++ b/src/plugins/memory-runtime.test.ts @@ -1,17 +1,21 @@ /** Covers non-activating memory registry handles and requesting-agent workspace ownership. */ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES, + LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES, +} from "../memory-host-sdk/host/authorization.js"; import type { MemorySearchResult } from "../memory-host-sdk/host/types.js"; -import type { MemoryPluginRuntime } from "./registry-contribution-types.js"; +import type { MemoryPluginCapability, MemoryPluginRuntime } from "./registry-contribution-types.js"; import { createEmptyPluginRegistry } from "./registry-empty.js"; import { getPluginRuntimeGatewayRequestScope } from "./runtime/gateway-request-scope.js"; type AuthorizeSearchHits = NonNullable; const mocks = vi.hoisted(() => ({ - getMemoryRuntime: vi.fn(), loadPluginRegistryHandle: vi.fn(), logDebug: vi.fn(), observeMemoryAuthorizationShadowSurface: vi.fn(), + requireActivePluginRegistry: vi.fn(), resolvePluginRegistryLoadCacheKey: vi.fn((options: unknown) => JSON.stringify(options)), resolveAgentWorkspaceDir: vi.fn(), })); @@ -33,9 +37,9 @@ vi.mock("./memory-authorization-shadow.js", () => ({ observeMemoryAuthorizationShadowSurface: mocks.observeMemoryAuthorizationShadowSurface, })); -vi.mock("./memory-state.js", async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual, getMemoryRuntime: mocks.getMemoryRuntime }; +vi.mock("./runtime.js", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, requireActivePluginRegistry: mocks.requireActivePluginRegistry }; }); import { @@ -62,6 +66,7 @@ function createRuntime() { type TestRegistry = { registry: ReturnType; runtime: T; + capability: MemoryPluginCapability; }; function createRegistry(): TestRegistry>; @@ -70,8 +75,13 @@ function createRegistry( runtime: MemoryPluginRuntime = createRuntime(), ): TestRegistry { const registry = createEmptyPluginRegistry(); - registry.memoryCapabilities.push({ pluginId: "memory-core", capability: { runtime } }); - return { registry, runtime }; + const capability = { + authorization: LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES, + runtime, + } satisfies MemoryPluginCapability; + registry.plugins.push({ id: "memory-core", memorySlotSelected: true } as never); + registry.memoryCapabilities.push({ pluginId: "memory-core", capability }); + return { registry, runtime, capability }; } const memoryConfig = { @@ -81,10 +91,10 @@ const memoryConfig = { describe("memory runtime handles", () => { beforeEach(() => { resetStandaloneMemoryRegistrySlot(); - mocks.getMemoryRuntime.mockReset().mockReturnValue(undefined); mocks.loadPluginRegistryHandle.mockReset(); mocks.logDebug.mockReset(); mocks.observeMemoryAuthorizationShadowSurface.mockReset(); + mocks.requireActivePluginRegistry.mockReset().mockReturnValue(createEmptyPluginRegistry()); mocks.resolvePluginRegistryLoadCacheKey.mockClear(); mocks.resolveAgentWorkspaceDir .mockReset() @@ -230,9 +240,9 @@ describe("memory runtime handles", () => { expect(mocks.loadPluginRegistryHandle).not.toHaveBeenCalled(); }); - it("prefers an already-registered runtime", () => { - const runtime = createRuntime(); - mocks.getMemoryRuntime.mockReturnValue(runtime); + it("prefers an already-registered selected capability runtime", () => { + const { registry } = createRegistry(); + mocks.requireActivePluginRegistry.mockReturnValue(registry); expect(resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "main" })).toEqual({ backend: "builtin", @@ -240,13 +250,19 @@ describe("memory runtime handles", () => { expect(mocks.loadPluginRegistryHandle).not.toHaveBeenCalled(); }); - it("inspects direct selected-runtime acquisition through the canonical seam", () => { - const runtime = createRuntime(); - mocks.getMemoryRuntime.mockReturnValue(runtime); + it("inspects direct selected capability resolution through the canonical seam", () => { + const { registry, runtime } = createRegistry(); + mocks.requireActivePluginRegistry.mockReturnValue(registry); expect(getSelectedMemoryRuntime()).toBe(runtime); expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledOnce(); - expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledWith(runtime); + expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledWith({ + capability: expect.objectContaining({ + authorization: LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES, + runtime, + }), + registry, + }); }); it("wires each legacy resolution through shadow inspection without changing legacy resolution", () => { @@ -261,10 +277,66 @@ describe("memory runtime handles", () => { }); expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledTimes(2); - expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledWith(runtime); + expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledWith({ + capability: expect.objectContaining({ + authorization: LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES, + runtime, + }), + registry, + }); expect(runtime.resolveMemoryBackendConfig).toHaveBeenCalledTimes(2); }); + it("observes a selected runtime-less capability without inventing a runtime", () => { + const registry = createEmptyPluginRegistry(); + registry.plugins.push({ id: "memory-lancedb", memorySlotSelected: true } as never); + registry.memoryCapabilities.push({ + pluginId: "memory-lancedb", + capability: { authorization: LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES }, + }); + mocks.requireActivePluginRegistry.mockReturnValue(registry); + + expect(getSelectedMemoryRuntime()).toBeUndefined(); + expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledWith({ + capability: expect.objectContaining({ + authorization: LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES, + }), + registry, + }); + }); + + it("observes selected authorization when it inherits a sidecar runtime", () => { + const registry = createEmptyPluginRegistry(); + const runtime = createRuntime(); + registry.plugins.push({ id: "memory-lancedb", memorySlotSelected: true } as never); + registry.memoryCapabilities.push( + { + pluginId: "memory-core", + capability: { + authorization: COMPLETE_MEMORY_AUTHORIZATION_CAPABILITIES, + runtime, + }, + }, + { + pluginId: "memory-lancedb", + capability: { + authorization: LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES, + publicArtifacts: { listArtifacts: async () => [] }, + }, + }, + ); + mocks.requireActivePluginRegistry.mockReturnValue(registry); + + expect(getSelectedMemoryRuntime()).toBe(runtime); + expect(mocks.observeMemoryAuthorizationShadowSurface).toHaveBeenCalledWith({ + capability: expect.objectContaining({ + authorization: LEGACY_MEMORY_AUTHORIZATION_CAPABILITIES, + runtime, + }), + registry, + }); + }); + it("keeps legacy resolution when shadow logging fails", () => { const { registry, runtime } = createRegistry(); mocks.loadPluginRegistryHandle.mockReturnValue(registry); diff --git a/src/plugins/memory-runtime.ts b/src/plugins/memory-runtime.ts index 59e1f80e6834..25087ad14ad3 100644 --- a/src/plugins/memory-runtime.ts +++ b/src/plugins/memory-runtime.ts @@ -7,12 +7,12 @@ import { normalizePluginsConfig } from "./config-state.js"; import { loadPluginRegistryHandle, resolvePluginRegistryLoadCacheKey } from "./loader.js"; import { observeMemoryAuthorizationShadowSurface } from "./memory-authorization-shadow.js"; import { - getMemoryRuntime, resolveSelectedMemoryCapabilityRegistration, setStandaloneMemoryManagerActive, } from "./memory-state.js"; -import type { MemoryPluginRuntime } from "./registry-contribution-types.js"; +import type { MemoryPluginCapability, MemoryPluginRuntime } from "./registry-contribution-types.js"; import type { PluginRegistry } from "./registry-types.js"; +import { requireActivePluginRegistry } from "./runtime.js"; import { withPluginRuntimeRegistryScope } from "./runtime/gateway-request-scope.js"; type MemoryRuntime = NonNullable< @@ -52,8 +52,11 @@ function resolveMemoryRuntimeWorkspaceDir( return resolveUserPath(dir); } -function resolveMemoryRuntimeFromRegistry(registry: PluginRegistry) { - return resolveSelectedMemoryCapabilityRegistration(registry)?.capability.runtime; +function resolveMemoryRuntimeFromRegistry(registry: PluginRegistry): MemoryRuntime | undefined { + const registration = resolveSelectedMemoryCapabilityRegistration(registry); + return registration + ? inspectSelectedMemoryCapability({ capability: registration.capability, registry }) + : undefined; } function listCurrentMemoryRuntimeOwners(): MemoryRuntimeOwner[] { @@ -81,31 +84,32 @@ function withMemoryRuntimeOwner( return withPluginRuntimeRegistryScope(owner.registry, () => run(owner.runtime)); } -function inspectSelectedMemoryRuntime(runtime: MemoryRuntime): MemoryRuntime { - // The inspection has no result-path effect: it only emits bounded shadow metadata once per - // selected runtime object and deliberately tolerates malformed/plugin-hostile surfaces. - const metadata = observeMemoryAuthorizationShadowSurface(runtime); +function inspectSelectedMemoryCapability(params: { + capability: MemoryPluginCapability; + registry: PluginRegistry; +}): MemoryRuntime | undefined { + // Inspection has no result-path effect: it emits bounded shadow metadata once per selected + // registry and deliberately tolerates malformed/plugin-hostile capability surfaces. + const metadata = observeMemoryAuthorizationShadowSurface(params); if (metadata) { try { log.debug("memory authorization backend surface evaluated", metadata); } catch { - // Shadow logging must not change selected runtime resolution or a legacy result path. + // Shadow logging must not change selected capability resolution or a legacy result path. } } - return runtime; + return params.capability.runtime; } -/** Reads the selected registered runtime through the canonical shadow-inspected seam. */ +/** Reads the selected capability runtime through the canonical shadow-inspected seam. */ export function getSelectedMemoryRuntime(): MemoryRuntime | undefined { - const runtime = getMemoryRuntime(); - return runtime ? inspectSelectedMemoryRuntime(runtime) : undefined; + return resolveMemoryRuntimeFromRegistry(requireActivePluginRegistry()); } function toMemoryRuntimeOwner( runtime: MemoryRuntime, registry?: PluginRegistry, ): MemoryRuntimeOwner { - inspectSelectedMemoryRuntime(runtime); return registry ? { runtime, registry } : { runtime }; }