Plugins: reserve context engine ownership

This commit is contained in:
Vincent Koc
2026-03-15 12:27:29 -07:00
parent 42837a04bf
commit 51631e5797
4 changed files with 133 additions and 12 deletions

View File

@@ -231,18 +231,36 @@ describe("Registry tests", () => {
expect(Array.isArray(ids)).toBe(true);
});
it("registering the same id overwrites the previous factory", () => {
it("registering the same id with the same owner refreshes the factory", () => {
const factory1 = () => new MockContextEngine();
const factory2 = () => new MockContextEngine();
registerContextEngine("reg-overwrite", factory1);
expect(registerContextEngine("reg-overwrite", factory1, { owner: "owner-a" })).toEqual({
ok: true,
});
expect(getContextEngineFactory("reg-overwrite")).toBe(factory1);
registerContextEngine("reg-overwrite", factory2);
expect(registerContextEngine("reg-overwrite", factory2, { owner: "owner-a" })).toEqual({
ok: true,
});
expect(getContextEngineFactory("reg-overwrite")).toBe(factory2);
expect(getContextEngineFactory("reg-overwrite")).not.toBe(factory1);
});
it("rejects context engine registrations from a different owner", () => {
const factory1 = () => new MockContextEngine();
const factory2 = () => new MockContextEngine();
expect(registerContextEngine("reg-owner-guard", factory1, { owner: "owner-a" })).toEqual({
ok: true,
});
expect(registerContextEngine("reg-owner-guard", factory2, { owner: "owner-b" })).toEqual({
ok: false,
existingOwner: "owner-a",
});
expect(getContextEngineFactory("reg-owner-guard")).toBe(factory1);
});
it("shares registered engines across duplicate module copies", async () => {
const registryUrl = new URL("./registry.ts", import.meta.url).href;
const suffix = Date.now().toString(36);

View File

@@ -7,6 +7,7 @@ import type { ContextEngine } from "./types.js";
* Supports async creation for engines that need DB connections etc.
*/
export type ContextEngineFactory = () => ContextEngine | Promise<ContextEngine>;
export type ContextEngineRegistrationResult = { ok: true } | { ok: false; existingOwner: string };
// ---------------------------------------------------------------------------
// Registry (module-level singleton)
@@ -15,7 +16,13 @@ export type ContextEngineFactory = () => ContextEngine | Promise<ContextEngine>;
const CONTEXT_ENGINE_REGISTRY_STATE = Symbol.for("openclaw.contextEngineRegistryState");
type ContextEngineRegistryState = {
engines: Map<string, ContextEngineFactory>;
engines: Map<
string,
{
factory: ContextEngineFactory;
owner: string;
}
>;
};
// Keep context-engine registrations process-global so duplicated dist chunks
@@ -26,7 +33,7 @@ function getContextEngineRegistryState(): ContextEngineRegistryState {
};
if (!globalState[CONTEXT_ENGINE_REGISTRY_STATE]) {
globalState[CONTEXT_ENGINE_REGISTRY_STATE] = {
engines: new Map<string, ContextEngineFactory>(),
engines: new Map(),
};
}
return globalState[CONTEXT_ENGINE_REGISTRY_STATE];
@@ -35,15 +42,26 @@ function getContextEngineRegistryState(): ContextEngineRegistryState {
/**
* Register a context engine implementation under the given id.
*/
export function registerContextEngine(id: string, factory: ContextEngineFactory): void {
getContextEngineRegistryState().engines.set(id, factory);
export function registerContextEngine(
id: string,
factory: ContextEngineFactory,
opts?: { owner?: string },
): ContextEngineRegistrationResult {
const owner = opts?.owner?.trim() || "core";
const registry = getContextEngineRegistryState().engines;
const existing = registry.get(id);
if (existing && existing.owner !== owner) {
return { ok: false, existingOwner: existing.owner };
}
registry.set(id, { factory, owner });
return { ok: true };
}
/**
* Return the factory for a registered engine, or undefined.
*/
export function getContextEngineFactory(id: string): ContextEngineFactory | undefined {
return getContextEngineRegistryState().engines.get(id);
return getContextEngineRegistryState().engines.get(id)?.factory;
}
/**
@@ -73,13 +91,13 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise<Con
? slotValue.trim()
: defaultSlotIdForKey("contextEngine");
const factory = getContextEngineRegistryState().engines.get(engineId);
if (!factory) {
const entry = getContextEngineRegistryState().engines.get(engineId);
if (!entry) {
throw new Error(
`Context engine "${engineId}" is not registered. ` +
`Available engines: ${listContextEngineIds().join(", ") || "(none)"}`,
);
}
return factory();
return entry.factory();
}