mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
perf(plugins): reuse startup runtime registry
Reuse the startup runtime plugin registry across provider/tool helper paths while preserving standalone CLI/MCP fallback loading. Includes follow-up fixes for migration/provider/tool registry bootstrap and regression coverage for compatible registry reuse. Co-authored-by: DmitryPogodaev <pogodaev.dm@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Agents/runtime: reuse the startup-loaded plugin registry for request-time providers, tools, channel actions, web/capability/memory/migration helpers, and memoized provider extra-params so stable embedded-run inputs no longer repeat plugin registry resolution while model-specific transport hook patches stay isolated. Thanks @DmitryPogodaev.
|
||||
- Agents/runtime: memoize transcript replay-policy resolution for stable config and process-env runs while preserving custom-env provider hook behavior. Thanks @DmitryPogodaev.
|
||||
- Infra/path-guards: add a fast path for canonical absolute POSIX containment checks, avoiding repeated `path.resolve` and `path.relative` work in hot filesystem walkers. Refs #75895, #75575, and #68782. Thanks @Enderfga.
|
||||
- Tools: add a platform-level tool descriptor planner for descriptor-first visibility, generic availability checks, and executor references. Thanks @shakkernerd.
|
||||
|
||||
@@ -686,7 +686,7 @@ describe("applyExtraParamsToAgent", () => {
|
||||
api: "openai-responses",
|
||||
provider: "xai",
|
||||
id: "grok-4.20-beta-latest-reasoning",
|
||||
} as Model<"openai-responses">,
|
||||
} as unknown as Model<"openai-responses">,
|
||||
payload: {
|
||||
model: "grok-4.20-beta-latest-reasoning",
|
||||
input: [],
|
||||
@@ -743,7 +743,7 @@ describe("applyExtraParamsToAgent", () => {
|
||||
id: "gpt-5",
|
||||
baseUrl: "http://127.0.0.1:19191/v1",
|
||||
reasoning: true,
|
||||
} as Model<"openai-responses">,
|
||||
} as unknown as Model<"openai-responses">,
|
||||
payload: {
|
||||
model: "gpt-5",
|
||||
input: [],
|
||||
@@ -811,7 +811,7 @@ describe("applyExtraParamsToAgent", () => {
|
||||
api: "openai-completions",
|
||||
provider: "nvidia-nim",
|
||||
id: "moonshotai/kimi-k2.5",
|
||||
} as Model<"openai-completions">,
|
||||
} as unknown as Model<"openai-completions">,
|
||||
});
|
||||
|
||||
expect(payload.parallel_tool_calls).toBe(false);
|
||||
@@ -838,7 +838,7 @@ describe("applyExtraParamsToAgent", () => {
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
id: "openrouter/auto",
|
||||
} as Model<"openai-completions">,
|
||||
} as unknown as Model<"openai-completions">,
|
||||
});
|
||||
|
||||
expect(payload.parallel_tool_calls).toBe(false);
|
||||
@@ -1908,7 +1908,7 @@ describe("applyExtraParamsToAgent", () => {
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
id: "gpt-5.4",
|
||||
} as Model<"openai-responses">,
|
||||
} as unknown as Model<"openai-responses">,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
@@ -2025,7 +2025,7 @@ describe("applyExtraParamsToAgent", () => {
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
id: "gpt-5",
|
||||
} as Model<"openai-responses">,
|
||||
} as unknown as Model<"openai-responses">,
|
||||
payload: { tools: [{ type: "function", name: "read" }] },
|
||||
});
|
||||
|
||||
@@ -2257,6 +2257,82 @@ describe("applyExtraParamsToAgent", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keys prepared extra-param memoization by resolved model transport inputs", () => {
|
||||
const resolveProviderExtraParamsForTransport = vi.fn((params) => ({
|
||||
patch: {
|
||||
transportFamily: params.context.model?.api,
|
||||
baseUrl: (params.context.model as Record<string, unknown> | undefined)?.baseUrl,
|
||||
headerAuth: (
|
||||
(params.context.model as Record<string, unknown> | undefined)?.headers as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
)?.["X-Test"],
|
||||
},
|
||||
}));
|
||||
extraParamsTesting.setProviderRuntimeDepsForTest({
|
||||
prepareProviderExtraParams: (params) => params.context.extraParams,
|
||||
resolveProviderExtraParamsForTransport,
|
||||
wrapProviderStreamFn: (params) => params.context.streamFn,
|
||||
});
|
||||
const cfg = {};
|
||||
|
||||
const responsesParams = resolvePreparedExtraParams({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5",
|
||||
model: {
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
id: "gpt-5",
|
||||
baseUrl: "https://api-one.example/v1",
|
||||
headers: { "X-Test": "one" },
|
||||
} as unknown as Model<"openai-responses">,
|
||||
});
|
||||
const completionsParams = resolvePreparedExtraParams({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5",
|
||||
model: {
|
||||
api: "openai-completions",
|
||||
provider: "openai",
|
||||
id: "gpt-5",
|
||||
baseUrl: "https://api-one.example/v1",
|
||||
headers: { "X-Test": "one" },
|
||||
} as unknown as Model<"openai-completions">,
|
||||
});
|
||||
const differentModelHeadersParams = resolvePreparedExtraParams({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5",
|
||||
model: {
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
id: "gpt-5",
|
||||
baseUrl: "https://api-two.example/v1",
|
||||
headers: { "X-Test": "two" },
|
||||
} as unknown as Model<"openai-responses">,
|
||||
});
|
||||
const repeatedResponsesParams = resolvePreparedExtraParams({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5",
|
||||
model: {
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
id: "gpt-5",
|
||||
baseUrl: "https://api-one.example/v1",
|
||||
headers: { "X-Test": "one" },
|
||||
} as unknown as Model<"openai-responses">,
|
||||
});
|
||||
|
||||
expect(responsesParams.transportFamily).toBe("openai-responses");
|
||||
expect(completionsParams.transportFamily).toBe("openai-completions");
|
||||
expect(differentModelHeadersParams.baseUrl).toBe("https://api-two.example/v1");
|
||||
expect(differentModelHeadersParams.headerAuth).toBe("two");
|
||||
expect(repeatedResponsesParams.transportFamily).toBe("openai-responses");
|
||||
expect(resolveProviderExtraParamsForTransport).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("passes explicit settings transport to transport extra-param hooks", () => {
|
||||
const resolveProviderExtraParamsForTransport = vi.fn((_params) => ({
|
||||
patch: {
|
||||
|
||||
@@ -38,6 +38,8 @@ const providerRuntimeDeps = {
|
||||
...defaultProviderRuntimeDeps,
|
||||
};
|
||||
|
||||
let preparedExtraParamsCache = new WeakMap<OpenClawConfig, Map<string, Record<string, unknown>>>();
|
||||
|
||||
export const __testing = {
|
||||
setProviderRuntimeDepsForTest(
|
||||
deps: Partial<typeof defaultProviderRuntimeDeps> | undefined,
|
||||
@@ -51,6 +53,7 @@ export const __testing = {
|
||||
deps?.wrapProviderStreamFn ?? defaultProviderRuntimeDeps.wrapProviderStreamFn;
|
||||
},
|
||||
resetProviderRuntimeDepsForTest(): void {
|
||||
clearPreparedExtraParamsCache();
|
||||
providerRuntimeDeps.prepareProviderExtraParams =
|
||||
defaultProviderRuntimeDeps.prepareProviderExtraParams;
|
||||
providerRuntimeDeps.resolveProviderExtraParamsForTransport =
|
||||
@@ -134,6 +137,60 @@ function hasExplicitTransportSetting(settings: { transport?: unknown }): boolean
|
||||
return Object.hasOwn(settings, "transport");
|
||||
}
|
||||
|
||||
function clearPreparedExtraParamsCache(): void {
|
||||
preparedExtraParamsCache = new WeakMap();
|
||||
}
|
||||
|
||||
function fingerprintPreparedExtraParamsModel(model?: ProviderRuntimeModel): unknown {
|
||||
if (!model) {
|
||||
return null;
|
||||
}
|
||||
const record = model as unknown as Record<string, unknown>;
|
||||
return {
|
||||
api: model.api,
|
||||
provider: model.provider,
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
baseUrl: model.baseUrl,
|
||||
reasoning: model.reasoning,
|
||||
input: model.input,
|
||||
cost: model.cost,
|
||||
compat: record.compat ?? null,
|
||||
contextWindow: model.contextWindow,
|
||||
contextTokens: model.contextTokens ?? null,
|
||||
headers: record.headers ?? null,
|
||||
maxTokens: model.maxTokens,
|
||||
params: model.params ?? null,
|
||||
requestTimeoutMs: model.requestTimeoutMs ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePreparedExtraParamsCacheKey(params: {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
extraParamsOverride?: Record<string, unknown>;
|
||||
thinkingLevel?: ThinkLevel;
|
||||
agentId?: string;
|
||||
resolvedExtraParams?: Record<string, unknown>;
|
||||
model?: ProviderRuntimeModel;
|
||||
resolvedTransport?: SupportedTransport;
|
||||
}): string {
|
||||
return JSON.stringify({
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
agentId: params.agentId ?? "",
|
||||
agentDir: params.agentDir ?? "",
|
||||
workspaceDir: params.workspaceDir ?? "",
|
||||
thinkingLevel: params.thinkingLevel ?? "",
|
||||
resolvedTransport: params.resolvedTransport ?? "",
|
||||
extraParamsOverride: params.extraParamsOverride ?? null,
|
||||
resolvedExtraParams: params.resolvedExtraParams ?? null,
|
||||
model: fingerprintPreparedExtraParamsModel(params.model),
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePreparedExtraParams(params: {
|
||||
cfg: OpenClawConfig | undefined;
|
||||
provider: string;
|
||||
@@ -176,6 +233,14 @@ export function resolvePreparedExtraParams(params: {
|
||||
merged.cachedContent = resolvedCachedContent;
|
||||
delete merged.cached_content;
|
||||
}
|
||||
const cfg = params.cfg;
|
||||
const cacheKey = cfg ? resolvePreparedExtraParamsCacheKey(params) : undefined;
|
||||
if (cacheKey) {
|
||||
const cached = preparedExtraParamsCache.get(cfg!)?.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
const prepared =
|
||||
providerRuntimeDeps.prepareProviderExtraParams({
|
||||
provider: params.provider,
|
||||
@@ -207,7 +272,16 @@ export function resolvePreparedExtraParams(params: {
|
||||
transport: params.resolvedTransport ?? resolveSupportedTransport(prepared.transport),
|
||||
},
|
||||
})?.patch;
|
||||
return transportPatch ? { ...prepared, ...transportPatch } : prepared;
|
||||
const result = transportPatch ? { ...prepared, ...transportPatch } : prepared;
|
||||
if (cacheKey) {
|
||||
let bucket = preparedExtraParamsCache.get(cfg!);
|
||||
if (!bucket) {
|
||||
bucket = new Map();
|
||||
preparedExtraParamsCache.set(cfg!, bucket);
|
||||
}
|
||||
bucket.set(cacheKey, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function sanitizeExtraParamsRecord(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
getCurrentPluginMetadataSnapshot: vi.fn(),
|
||||
resolveRuntimePluginRegistry: vi.fn(),
|
||||
ensureStandaloneRuntimePluginRegistryLoaded: vi.fn(),
|
||||
getActivePluginRuntimeSubagentMode: vi.fn<() => "default" | "explicit" | "gateway-bindable">(
|
||||
() => "default",
|
||||
),
|
||||
@@ -12,8 +12,8 @@ vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({
|
||||
getCurrentPluginMetadataSnapshot: hoisted.getCurrentPluginMetadataSnapshot,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
resolveRuntimePluginRegistry: hoisted.resolveRuntimePluginRegistry,
|
||||
vi.mock("../plugins/runtime/standalone-runtime-registry-loader.js", () => ({
|
||||
ensureStandaloneRuntimePluginRegistryLoaded: hoisted.ensureStandaloneRuntimePluginRegistryLoaded,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/runtime.js", () => ({
|
||||
@@ -26,8 +26,8 @@ describe("ensureRuntimePluginsLoaded", () => {
|
||||
beforeEach(async () => {
|
||||
hoisted.getCurrentPluginMetadataSnapshot.mockReset();
|
||||
hoisted.getCurrentPluginMetadataSnapshot.mockReturnValue(undefined);
|
||||
hoisted.resolveRuntimePluginRegistry.mockReset();
|
||||
hoisted.resolveRuntimePluginRegistry.mockReturnValue(undefined);
|
||||
hoisted.ensureStandaloneRuntimePluginRegistryLoaded.mockReset();
|
||||
hoisted.ensureStandaloneRuntimePluginRegistryLoaded.mockReturnValue(undefined);
|
||||
hoisted.getActivePluginRuntimeSubagentMode.mockReset();
|
||||
hoisted.getActivePluginRuntimeSubagentMode.mockReturnValue("default");
|
||||
vi.resetModules();
|
||||
@@ -35,7 +35,7 @@ describe("ensureRuntimePluginsLoaded", () => {
|
||||
});
|
||||
|
||||
it("does not reactivate plugins when a process already has an active registry", async () => {
|
||||
hoisted.resolveRuntimePluginRegistry.mockReturnValue({});
|
||||
hoisted.ensureStandaloneRuntimePluginRegistryLoaded.mockReturnValue({});
|
||||
|
||||
ensureRuntimePluginsLoaded({
|
||||
config: {} as never,
|
||||
@@ -43,7 +43,7 @@ describe("ensureRuntimePluginsLoaded", () => {
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
|
||||
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.ensureStandaloneRuntimePluginRegistryLoaded).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("resolves runtime plugins through the shared runtime helper", async () => {
|
||||
@@ -53,11 +53,14 @@ describe("ensureRuntimePluginsLoaded", () => {
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
|
||||
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: {} as never,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
expect(hoisted.ensureStandaloneRuntimePluginRegistryLoaded).toHaveBeenCalledWith({
|
||||
requiredPluginIds: undefined,
|
||||
loadOptions: {
|
||||
config: {} as never,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -80,12 +83,15 @@ describe("ensureRuntimePluginsLoaded", () => {
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
onlyPluginIds: ["telegram", "memory-core"],
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
expect(hoisted.ensureStandaloneRuntimePluginRegistryLoaded).toHaveBeenCalledWith({
|
||||
requiredPluginIds: ["telegram", "memory-core"],
|
||||
loadOptions: {
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
onlyPluginIds: ["telegram", "memory-core"],
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -104,12 +110,15 @@ describe("ensureRuntimePluginsLoaded", () => {
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
|
||||
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: {} as never,
|
||||
onlyPluginIds: ["telegram"],
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
expect(hoisted.ensureStandaloneRuntimePluginRegistryLoaded).toHaveBeenCalledWith({
|
||||
requiredPluginIds: ["telegram"],
|
||||
loadOptions: {
|
||||
config: {} as never,
|
||||
onlyPluginIds: ["telegram"],
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -137,12 +146,15 @@ describe("ensureRuntimePluginsLoaded", () => {
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
|
||||
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config,
|
||||
onlyPluginIds: ["telegram"],
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
expect(hoisted.ensureStandaloneRuntimePluginRegistryLoaded).toHaveBeenCalledWith({
|
||||
requiredPluginIds: ["telegram"],
|
||||
loadOptions: {
|
||||
config,
|
||||
onlyPluginIds: ["telegram"],
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -153,10 +165,13 @@ describe("ensureRuntimePluginsLoaded", () => {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
|
||||
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: {} as never,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: undefined,
|
||||
expect(hoisted.ensureStandaloneRuntimePluginRegistryLoaded).toHaveBeenCalledWith({
|
||||
requiredPluginIds: undefined,
|
||||
loadOptions: {
|
||||
config: {} as never,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,11 +183,14 @@ describe("ensureRuntimePluginsLoaded", () => {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
|
||||
expect(hoisted.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: {} as never,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
expect(hoisted.ensureStandaloneRuntimePluginRegistryLoaded).toHaveBeenCalledWith({
|
||||
requiredPluginIds: undefined,
|
||||
loadOptions: {
|
||||
config: {} as never,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { resolveRuntimePluginRegistry } from "../plugins/loader.js";
|
||||
import { getActivePluginRuntimeSubagentMode } from "../plugins/runtime.js";
|
||||
import { ensureStandaloneRuntimePluginRegistryLoaded } from "../plugins/runtime/standalone-runtime-registry-loader.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
|
||||
type StartupScopedPluginSnapshot = NonNullable<
|
||||
@@ -36,22 +36,22 @@ export function ensureRuntimePluginsLoaded(params: {
|
||||
typeof params.workspaceDir === "string" && params.workspaceDir.trim()
|
||||
? resolveUserPath(params.workspaceDir)
|
||||
: undefined;
|
||||
const allowGatewaySubagentBinding =
|
||||
params.allowGatewaySubagentBinding === true ||
|
||||
getActivePluginRuntimeSubagentMode() === "gateway-bindable";
|
||||
const startupPluginIds = resolveStartupPluginIdsFromCurrentSnapshot({
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
});
|
||||
const loadOptions = {
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
...(startupPluginIds ? { onlyPluginIds: startupPluginIds } : {}),
|
||||
runtimeOptions: allowGatewaySubagentBinding
|
||||
? {
|
||||
allowGatewaySubagentBinding: true,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
resolveRuntimePluginRegistry(loadOptions);
|
||||
const allowGatewaySubagentBinding =
|
||||
params.allowGatewaySubagentBinding === true ||
|
||||
getActivePluginRuntimeSubagentMode() === "gateway-bindable";
|
||||
ensureStandaloneRuntimePluginRegistryLoaded({
|
||||
requiredPluginIds: startupPluginIds,
|
||||
loadOptions: {
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
...(startupPluginIds === undefined ? {} : { onlyPluginIds: startupPluginIds }),
|
||||
runtimeOptions: allowGatewaySubagentBinding
|
||||
? { allowGatewaySubagentBinding: true }
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ function withActivatedPluginIdsForTest<T extends Record<string, unknown>>(
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadOpenClawPlugins: vi.fn<typeof import("../plugins/loader.js").loadOpenClawPlugins>(),
|
||||
resolveCompatibleRuntimePluginRegistry:
|
||||
vi.fn<typeof import("../plugins/loader.js").resolveCompatibleRuntimePluginRegistry>(),
|
||||
resolveRuntimePluginRegistry:
|
||||
vi.fn<typeof import("../plugins/loader.js").resolveRuntimePluginRegistry>(),
|
||||
getActivePluginRegistry: vi.fn<typeof import("../plugins/runtime.js").getActivePluginRegistry>(),
|
||||
@@ -50,6 +52,9 @@ let resetPluginRegistryLoadedForTests: typeof import("./plugin-registry.js").__t
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: Parameters<typeof mocks.loadOpenClawPlugins>) =>
|
||||
mocks.loadOpenClawPlugins(...args),
|
||||
resolveCompatibleRuntimePluginRegistry: (
|
||||
...args: Parameters<typeof mocks.resolveCompatibleRuntimePluginRegistry>
|
||||
) => mocks.resolveCompatibleRuntimePluginRegistry(...args),
|
||||
resolveRuntimePluginRegistry: (...args: Parameters<typeof mocks.resolveRuntimePluginRegistry>) =>
|
||||
mocks.resolveRuntimePluginRegistry(...args),
|
||||
}));
|
||||
@@ -123,6 +128,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.loadOpenClawPlugins.mockReset();
|
||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReset();
|
||||
mocks.resolveRuntimePluginRegistry.mockReset();
|
||||
mocks.getActivePluginRegistry.mockReset();
|
||||
mocks.resolveConfiguredChannelPluginIds.mockReset();
|
||||
@@ -132,6 +138,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
resetPluginRegistryLoadedForTests();
|
||||
|
||||
mocks.getActivePluginRegistry.mockReturnValue(createEmptyPluginRegistry());
|
||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(undefined);
|
||||
mocks.resolveRuntimePluginRegistry.mockReturnValue(undefined);
|
||||
mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue([]);
|
||||
mocks.resolvePluginRuntimeLoadContext.mockImplementation((options) => {
|
||||
|
||||
@@ -38,6 +38,7 @@ vi.mock("@clack/prompts", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/migration-provider-runtime.js", () => ({
|
||||
ensureStandaloneMigrationProviderRegistryLoaded: vi.fn(),
|
||||
resolvePluginMigrationProvider: () => mocks.provider,
|
||||
resolvePluginMigrationProviders: () => [mocks.provider],
|
||||
}));
|
||||
|
||||
@@ -2,7 +2,10 @@ import { cancel, isCancel, multiselect } from "@clack/prompts";
|
||||
import { promptYesNo } from "../cli/prompt.js";
|
||||
import { getRuntimeConfig } from "../config/config.js";
|
||||
import { redactMigrationPlan } from "../plugin-sdk/migration.js";
|
||||
import { resolvePluginMigrationProviders } from "../plugins/migration-provider-runtime.js";
|
||||
import {
|
||||
ensureStandaloneMigrationProviderRegistryLoaded,
|
||||
resolvePluginMigrationProviders,
|
||||
} from "../plugins/migration-provider-runtime.js";
|
||||
import type { MigrationApplyResult, MigrationPlan } from "../plugins/types.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { writeRuntimeJson } from "../runtime.js";
|
||||
@@ -72,13 +75,13 @@ async function promptCodexMigrationSkillSelection(
|
||||
}
|
||||
|
||||
export async function migrateListCommand(runtime: RuntimeEnv, opts: { json?: boolean } = {}) {
|
||||
const providers = resolvePluginMigrationProviders({ cfg: getRuntimeConfig() }).map(
|
||||
(provider) => ({
|
||||
id: provider.id,
|
||||
label: provider.label,
|
||||
description: provider.description,
|
||||
}),
|
||||
);
|
||||
const cfg = getRuntimeConfig();
|
||||
ensureStandaloneMigrationProviderRegistryLoaded({ cfg });
|
||||
const providers = resolvePluginMigrationProviders({ cfg }).map((provider) => ({
|
||||
id: provider.id,
|
||||
label: provider.label,
|
||||
description: provider.description,
|
||||
}));
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, { providers });
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getRuntimeConfig } from "../../config/config.js";
|
||||
import {
|
||||
ensureStandaloneMigrationProviderRegistryLoaded,
|
||||
resolvePluginMigrationProvider,
|
||||
resolvePluginMigrationProviders,
|
||||
} from "../../plugins/migration-provider-runtime.js";
|
||||
@@ -10,6 +11,7 @@ import type { MigrateCommonOptions } from "./types.js";
|
||||
|
||||
export function resolveMigrationProvider(providerId: string): MigrationProviderPlugin {
|
||||
const config = getRuntimeConfig();
|
||||
ensureStandaloneMigrationProviderRegistryLoaded({ cfg: config });
|
||||
const provider = resolvePluginMigrationProvider({ providerId, cfg: config });
|
||||
if (!provider) {
|
||||
const available = resolvePluginMigrationProviders({ cfg: config }).map((entry) => entry.id);
|
||||
|
||||
@@ -141,6 +141,7 @@ vi.mock("./health.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/migration-provider-runtime.js", () => ({
|
||||
ensureStandaloneMigrationProviderRegistryLoaded: vi.fn(),
|
||||
resolvePluginMigrationProviders: () => [migrationProviderMock],
|
||||
resolvePluginMigrationProvider: ({ providerId }: { providerId: string }) =>
|
||||
providerId === migrationProviderMock.id ? migrationProviderMock : undefined,
|
||||
|
||||
@@ -1,56 +1,13 @@
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||
import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { resolveRuntimePluginRegistry } from "../../plugins/loader.js";
|
||||
import {
|
||||
getActivePluginChannelRegistry,
|
||||
getActivePluginChannelRegistryVersion,
|
||||
} from "../../plugins/runtime.js";
|
||||
import type { DeliverableMessageChannel } from "../../utils/message-channel.js";
|
||||
|
||||
const bootstrapAttempts = new Set<string>();
|
||||
|
||||
export function resetOutboundChannelBootstrapStateForTests(): void {
|
||||
bootstrapAttempts.clear();
|
||||
// Runtime channel plugins are loaded during Gateway startup now.
|
||||
}
|
||||
|
||||
export function bootstrapOutboundChannelPlugin(params: {
|
||||
channel: DeliverableMessageChannel;
|
||||
cfg?: OpenClawConfig;
|
||||
}): void {
|
||||
const cfg = params.cfg;
|
||||
if (!cfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeChannelRegistry = getActivePluginChannelRegistry();
|
||||
const activeHasRequestedChannel = activeChannelRegistry?.channels?.some(
|
||||
(entry) => entry?.plugin?.id === params.channel,
|
||||
);
|
||||
if (activeHasRequestedChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attemptKey = `${getActivePluginChannelRegistryVersion()}:${params.channel}`;
|
||||
if (bootstrapAttempts.has(attemptKey)) {
|
||||
return;
|
||||
}
|
||||
bootstrapAttempts.add(attemptKey);
|
||||
|
||||
const autoEnabled = applyPluginAutoEnable({ config: cfg });
|
||||
const defaultAgentId = resolveDefaultAgentId(autoEnabled.config);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(autoEnabled.config, defaultAgentId);
|
||||
try {
|
||||
resolveRuntimePluginRegistry({
|
||||
config: autoEnabled.config,
|
||||
activationSourceConfig: cfg,
|
||||
autoEnabledReasons: autoEnabled.autoEnabledReasons,
|
||||
workspaceDir,
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
bootstrapAttempts.delete(attemptKey);
|
||||
}
|
||||
void params;
|
||||
}
|
||||
|
||||
@@ -52,19 +52,6 @@ async function importChannelResolution(scope: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function expectBootstrapArgs() {
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: { autoEnabled: true },
|
||||
activationSourceConfig: { channels: {} },
|
||||
workspaceDir: "/tmp/workspace",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
describe("outbound channel resolution", () => {
|
||||
beforeEach(async () => {
|
||||
resolveDefaultAgentIdMock.mockReset();
|
||||
@@ -141,10 +128,10 @@ describe("outbound channel resolution", () => {
|
||||
).toBe(plugin);
|
||||
});
|
||||
|
||||
it("bootstraps plugins once per registry key and returns the newly loaded plugin", async () => {
|
||||
it("does not load registries while resolving outbound plugins", async () => {
|
||||
const plugin = { id: "alpha" };
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin);
|
||||
const channelResolution = await importChannelResolution("bootstrap-success");
|
||||
const channelResolution = await importChannelResolution("no-bootstrap");
|
||||
|
||||
expect(
|
||||
channelResolution.resolveOutboundChannelPlugin({
|
||||
@@ -152,20 +139,19 @@ describe("outbound channel resolution", () => {
|
||||
cfg: { channels: {} } as never,
|
||||
}),
|
||||
).toBe(plugin);
|
||||
expectBootstrapArgs();
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
channelResolution.resolveOutboundChannelPlugin({
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
});
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1);
|
||||
expectBootstrapArgs();
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("bootstraps when the active registry has other channels but not the requested one", async () => {
|
||||
const plugin = { id: "alpha" };
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin);
|
||||
it("does not load when the active registry has other channels but not the requested one", async () => {
|
||||
getLoadedChannelPluginMock.mockReturnValue(undefined);
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
getActivePluginRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: { id: "beta" } }],
|
||||
});
|
||||
@@ -179,11 +165,11 @@ describe("outbound channel resolution", () => {
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
}),
|
||||
).toBe(plugin);
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1);
|
||||
).toBeUndefined();
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retries bootstrap after a transient load failure", async () => {
|
||||
it("does not retry registry loads after a missing outbound plugin", async () => {
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
resolveRuntimePluginRegistryMock.mockImplementationOnce(() => {
|
||||
throw new Error("transient");
|
||||
@@ -201,10 +187,10 @@ describe("outbound channel resolution", () => {
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
});
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(2);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retries bootstrap when the pinned channel registry version changes", async () => {
|
||||
it("does not load when the pinned channel registry version changes", async () => {
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
const channelResolution = await importChannelResolution("channel-version-change");
|
||||
|
||||
@@ -212,13 +198,13 @@ describe("outbound channel resolution", () => {
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
});
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
|
||||
getActivePluginChannelRegistryVersionMock.mockReturnValue(2);
|
||||
channelResolution.resolveOutboundChannelPlugin({
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
});
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(2);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -330,7 +330,7 @@ describe("sendMessage", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("recovers plugin resolution after registry refresh", async () => {
|
||||
it("does not load registries while resolving outbound plugins", async () => {
|
||||
const forumPlugin = {
|
||||
outbound: { deliveryMode: "direct" },
|
||||
};
|
||||
@@ -352,6 +352,6 @@ describe("sendMessage", () => {
|
||||
via: "direct",
|
||||
});
|
||||
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ const callGatewayTool = vi.hoisted(() => vi.fn());
|
||||
const connectToolsMcpServerToStdioMock = vi.hoisted(() => vi.fn());
|
||||
const createToolsMcpServerMock = vi.hoisted(() => vi.fn(() => ({ close: vi.fn() })));
|
||||
const getRuntimeConfigMock = vi.hoisted(() => vi.fn(() => ({ plugins: { enabled: true } })));
|
||||
const ensureStandalonePluginToolRegistryLoadedMock = vi.hoisted(() => vi.fn());
|
||||
const resolvePluginToolsMock = vi.hoisted(() => vi.fn<() => AnyAgentTool[]>(() => []));
|
||||
const routeLogsToStderrMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -34,6 +35,7 @@ vi.mock("../plugins/tools.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../plugins/tools.js")>();
|
||||
return {
|
||||
...actual,
|
||||
ensureStandalonePluginToolRegistryLoaded: ensureStandalonePluginToolRegistryLoadedMock,
|
||||
resolvePluginTools: resolvePluginToolsMock,
|
||||
};
|
||||
});
|
||||
@@ -48,6 +50,7 @@ afterEach(() => {
|
||||
callGatewayTool.mockReset();
|
||||
connectToolsMcpServerToStdioMock.mockReset();
|
||||
createToolsMcpServerMock.mockClear();
|
||||
ensureStandalonePluginToolRegistryLoadedMock.mockReset();
|
||||
getRuntimeConfigMock.mockClear();
|
||||
resolvePluginToolsMock.mockReset();
|
||||
resolvePluginToolsMock.mockReturnValue([]);
|
||||
@@ -71,7 +74,13 @@ describe("plugin tools MCP server", () => {
|
||||
await servePluginToolsMcp();
|
||||
|
||||
expect(routeLogsToStderrMock).toHaveBeenCalledTimes(1);
|
||||
expect(ensureStandalonePluginToolRegistryLoadedMock).toHaveBeenCalledWith({
|
||||
context: { config: { plugins: { enabled: true } } },
|
||||
});
|
||||
expect(resolvePluginToolsMock).toHaveBeenCalledTimes(1);
|
||||
expect(ensureStandalonePluginToolRegistryLoadedMock.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
resolvePluginToolsMock.mock.invocationCallOrder[0] ?? 0,
|
||||
);
|
||||
expect(routeLogsToStderrMock.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
resolvePluginToolsMock.mock.invocationCallOrder[0] ?? 0,
|
||||
);
|
||||
|
||||
@@ -13,10 +13,13 @@ import { getRuntimeConfig } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { routeLogsToStderr } from "../logging/console.js";
|
||||
import { resolvePluginTools } from "../plugins/tools.js";
|
||||
import { ensureStandalonePluginToolRegistryLoaded, resolvePluginTools } from "../plugins/tools.js";
|
||||
import { connectToolsMcpServerToStdio, createToolsMcpServer } from "./tools-stdio-server.js";
|
||||
|
||||
function resolveTools(config: OpenClawConfig): AnyAgentTool[] {
|
||||
ensureStandalonePluginToolRegistryLoaded({
|
||||
context: { config },
|
||||
});
|
||||
return resolvePluginTools({
|
||||
context: { config },
|
||||
suppressNameConflicts: true,
|
||||
|
||||
88
src/plugins/active-runtime-registry.test.ts
Normal file
88
src/plugins/active-runtime-registry.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
||||
import { __testing, clearPluginLoaderCache } from "./loader.js";
|
||||
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
||||
import type { PluginRegistry } from "./registry-types.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js";
|
||||
|
||||
afterEach(() => {
|
||||
clearPluginLoaderCache();
|
||||
resetPluginRuntimeStateForTest();
|
||||
});
|
||||
|
||||
function createRegistryWithPlugin(pluginId: string): PluginRegistry {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.plugins.push({
|
||||
id: pluginId,
|
||||
status: "loaded",
|
||||
} as never);
|
||||
return registry;
|
||||
}
|
||||
|
||||
describe("getLoadedRuntimePluginRegistry", () => {
|
||||
it("treats an explicit empty plugin scope as empty", () => {
|
||||
setActivePluginRegistry(createRegistryWithPlugin("stale"), "stale", "default", "/tmp/ws");
|
||||
|
||||
expect(
|
||||
getLoadedRuntimePluginRegistry({
|
||||
workspaceDir: "/tmp/ws",
|
||||
requiredPluginIds: [],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
|
||||
const emptyRegistry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(emptyRegistry, "empty", "default", "/tmp/ws");
|
||||
|
||||
expect(
|
||||
getLoadedRuntimePluginRegistry({
|
||||
workspaceDir: "/tmp/ws",
|
||||
requiredPluginIds: [],
|
||||
}),
|
||||
).toBe(emptyRegistry);
|
||||
});
|
||||
|
||||
it("does not reuse workspace-agnostic registries for workspace-specific requests", () => {
|
||||
setActivePluginRegistry(createRegistryWithPlugin("demo"), "demo");
|
||||
|
||||
expect(
|
||||
getLoadedRuntimePluginRegistry({
|
||||
workspaceDir: "/tmp/ws",
|
||||
requiredPluginIds: ["demo"],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("validates full loader cache compatibility when load options are provided", () => {
|
||||
const registry = createRegistryWithPlugin("demo");
|
||||
const loadOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["demo"],
|
||||
},
|
||||
},
|
||||
onlyPluginIds: ["demo"],
|
||||
workspaceDir: "/tmp/ws",
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(loadOptions);
|
||||
setActivePluginRegistry(registry, cacheKey, "default", "/tmp/ws");
|
||||
|
||||
expect(
|
||||
getLoadedRuntimePluginRegistry({
|
||||
loadOptions,
|
||||
}),
|
||||
).toBe(registry);
|
||||
|
||||
expect(
|
||||
getLoadedRuntimePluginRegistry({
|
||||
loadOptions: {
|
||||
...loadOptions,
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["other"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,28 +1,105 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { resolveCompatibleRuntimePluginRegistry, type PluginLoadOptions } from "./loader.js";
|
||||
import type { PluginRegistry } from "./registry-types.js";
|
||||
import {
|
||||
getActivePluginChannelRegistry,
|
||||
getActivePluginHttpRouteRegistry,
|
||||
getActivePluginRegistry,
|
||||
getActivePluginRegistryWorkspaceDir,
|
||||
} from "./runtime.js";
|
||||
|
||||
type PluginRuntimeModule = Pick<typeof import("./runtime.js"), "getActivePluginRegistry">;
|
||||
export type ActiveRuntimePluginRegistrySurface = "active" | "channel" | "http-route";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const RUNTIME_MODULE_CANDIDATES = ["./runtime.js", "./runtime.ts"] as const;
|
||||
export function getActiveRuntimePluginRegistry(): PluginRegistry | null {
|
||||
return getActivePluginRegistry();
|
||||
}
|
||||
|
||||
let pluginRuntimeModule: PluginRuntimeModule | undefined;
|
||||
|
||||
function loadPluginRuntime(): PluginRuntimeModule | null {
|
||||
if (pluginRuntimeModule) {
|
||||
return pluginRuntimeModule;
|
||||
function normalizeRequiredPluginIds(ids?: readonly string[]): string[] | undefined {
|
||||
if (ids === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
for (const candidate of RUNTIME_MODULE_CANDIDATES) {
|
||||
try {
|
||||
pluginRuntimeModule = require(candidate) as PluginRuntimeModule;
|
||||
return pluginRuntimeModule;
|
||||
} catch {
|
||||
// Try built/runtime source candidates in order.
|
||||
return [...new Set(ids.map((id) => id.trim()).filter(Boolean))].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
);
|
||||
}
|
||||
|
||||
function registryContainsPluginIds(
|
||||
registry: PluginRegistry,
|
||||
pluginIds: readonly string[] | undefined,
|
||||
): boolean {
|
||||
if (pluginIds === undefined) {
|
||||
return true;
|
||||
}
|
||||
const loaded = new Set<string>();
|
||||
for (const plugin of registry.plugins ?? []) {
|
||||
if (plugin.status === undefined || plugin.status === "loaded") {
|
||||
loaded.add(plugin.id);
|
||||
}
|
||||
}
|
||||
for (const value of Object.values(registry)) {
|
||||
if (!Array.isArray(value)) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of value) {
|
||||
if (entry && typeof entry === "object" && "pluginId" in entry) {
|
||||
const pluginId = entry.pluginId;
|
||||
if (typeof pluginId === "string" && pluginId.length > 0) {
|
||||
loaded.add(pluginId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pluginIds.length === 0) {
|
||||
return loaded.size === 0;
|
||||
}
|
||||
return pluginIds.every((pluginId) => loaded.has(pluginId));
|
||||
}
|
||||
|
||||
function resolveSurfaceRegistry(
|
||||
surface: ActiveRuntimePluginRegistrySurface,
|
||||
): PluginRegistry | null {
|
||||
switch (surface) {
|
||||
case "active":
|
||||
return getActivePluginRegistry();
|
||||
case "channel":
|
||||
return getActivePluginChannelRegistry();
|
||||
case "http-route":
|
||||
return getActivePluginHttpRouteRegistry();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getActiveRuntimePluginRegistry(): PluginRegistry | null {
|
||||
return loadPluginRuntime()?.getActivePluginRegistry() ?? null;
|
||||
export function getLoadedRuntimePluginRegistry(
|
||||
params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
loadOptions?: PluginLoadOptions;
|
||||
workspaceDir?: string;
|
||||
requiredPluginIds?: readonly string[];
|
||||
surface?: ActiveRuntimePluginRegistrySurface;
|
||||
} = {},
|
||||
): PluginRegistry | undefined {
|
||||
const surface = params.surface ?? "active";
|
||||
const requiredPluginIds = normalizeRequiredPluginIds(
|
||||
params.requiredPluginIds ?? params.loadOptions?.onlyPluginIds,
|
||||
);
|
||||
if (surface === "active" && params.loadOptions && requiredPluginIds?.length !== 0) {
|
||||
const compatible = resolveCompatibleRuntimePluginRegistry(params.loadOptions);
|
||||
if (!compatible || !registryContainsPluginIds(compatible, requiredPluginIds)) {
|
||||
return undefined;
|
||||
}
|
||||
return compatible;
|
||||
}
|
||||
|
||||
const activeWorkspaceDir = getActivePluginRegistryWorkspaceDir();
|
||||
const requestedWorkspaceDir = params.workspaceDir ?? params.loadOptions?.workspaceDir;
|
||||
if (requestedWorkspaceDir !== undefined && activeWorkspaceDir !== requestedWorkspaceDir) {
|
||||
return undefined;
|
||||
}
|
||||
const registry = resolveSurfaceRegistry(surface);
|
||||
if (!registry) {
|
||||
return undefined;
|
||||
}
|
||||
if (!registryContainsPluginIds(registry, requiredPluginIds)) {
|
||||
return undefined;
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
||||
import type {
|
||||
AgentToolResultMiddleware,
|
||||
AgentToolResultMiddlewareRuntime,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
listAgentToolResultMiddlewares,
|
||||
normalizeAgentToolResultMiddlewareRuntimeIds,
|
||||
} from "./agent-tool-result-middleware.js";
|
||||
import { loadOpenClawPlugins } from "./loader.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins/agent-tool-result-middleware");
|
||||
@@ -67,15 +67,14 @@ export async function loadAgentToolResultMiddlewaresForRuntime(params: {
|
||||
return [];
|
||||
}
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
config,
|
||||
const registry = getLoadedRuntimePluginRegistry({
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
manifestRegistry,
|
||||
onlyPluginIds: pluginIds,
|
||||
activate: false,
|
||||
throwOnLoadError: false,
|
||||
requiredPluginIds: pluginIds,
|
||||
});
|
||||
if (!registry) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return registry.agentToolResultMiddlewares
|
||||
.filter((entry) => entry.runtimes.includes(params.runtime))
|
||||
|
||||
@@ -57,6 +57,17 @@ vi.mock("./loader.js", () => ({
|
||||
resolvePluginRegistryLoadCacheKey: mocks.resolvePluginRegistryLoadCacheKey,
|
||||
}));
|
||||
|
||||
vi.mock("./active-runtime-registry.js", () => ({
|
||||
getLoadedRuntimePluginRegistry: (params?: { requiredPluginIds?: string[] }) => {
|
||||
if (params === undefined) {
|
||||
return mocks.resolveRuntimePluginRegistry();
|
||||
}
|
||||
return mocks.resolveRuntimePluginRegistry({
|
||||
onlyPluginIds: params.requiredPluginIds,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./bundled-capability-runtime.js", () => ({
|
||||
loadBundledCapabilityRuntimeRegistry: mocks.loadBundledCapabilityRuntimeRegistry,
|
||||
}));
|
||||
@@ -134,6 +145,22 @@ function expectNoResolvedCapabilityProviders(providers: Array<{ id: string }>) {
|
||||
expectResolvedCapabilityProviderIds(providers, []);
|
||||
}
|
||||
|
||||
function expectActiveRegistryLookup(pluginIds: string[]) {
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({ onlyPluginIds: pluginIds });
|
||||
}
|
||||
|
||||
function collectActiveRegistryLookups() {
|
||||
return mocks.resolveRuntimePluginRegistry.mock.calls
|
||||
.map(([options]) => options)
|
||||
.filter((options): options is { onlyPluginIds?: string[] } =>
|
||||
Boolean(
|
||||
options &&
|
||||
typeof options === "object" &&
|
||||
Object.hasOwn(options as Record<string, unknown>, "onlyPluginIds"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function expectBundledCompatLoadPath(params: {
|
||||
cfg: OpenClawConfig;
|
||||
allowlistCompat: OpenClawConfig;
|
||||
@@ -159,11 +186,7 @@ function expectBundledCompatLoadPath(params: {
|
||||
pluginIds: ["openai"],
|
||||
env: process.env,
|
||||
});
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: params.enablementCompat,
|
||||
onlyPluginIds: ["openai"],
|
||||
activate: false,
|
||||
});
|
||||
expectActiveRegistryLookup(["openai"]);
|
||||
}
|
||||
|
||||
function createCompatChainConfig() {
|
||||
@@ -443,9 +466,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
["external-image"],
|
||||
);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenLastCalledWith({
|
||||
config: expect.any(Object),
|
||||
onlyPluginIds: ["external-image"],
|
||||
activate: false,
|
||||
});
|
||||
expect(mocks.loadBundledCapabilityRuntimeRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -545,15 +566,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
|
||||
expectResolvedCapabilityProviderIds(providers, ["openai", "deepgram"]);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
allow: ["openai", "deepgram", "google"],
|
||||
}),
|
||||
}),
|
||||
onlyPluginIds: ["deepgram", "google"],
|
||||
activate: false,
|
||||
});
|
||||
expectActiveRegistryLookup(["deepgram", "google"]);
|
||||
});
|
||||
|
||||
it("keeps active speech providers when cfg requests an active provider alias", () => {
|
||||
@@ -692,15 +705,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
|
||||
expectResolvedCapabilityProviderIds(providers, ["openai", "microsoft"]);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
allow: ["openai", "microsoft"],
|
||||
}),
|
||||
}),
|
||||
onlyPluginIds: ["microsoft"],
|
||||
activate: false,
|
||||
});
|
||||
expectActiveRegistryLookup(["microsoft"]);
|
||||
});
|
||||
|
||||
it("uses bundled capability capture when runtime snapshot is empty for a requested speech provider", () => {
|
||||
@@ -766,11 +771,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
});
|
||||
|
||||
expectResolvedCapabilityProviderIds(providers, ["openai", "google"]);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: expect.anything(),
|
||||
onlyPluginIds: ["google"],
|
||||
activate: false,
|
||||
});
|
||||
expectActiveRegistryLookup(["google"]);
|
||||
expect(mocks.loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledWith({
|
||||
pluginIds: ["google"],
|
||||
env: process.env,
|
||||
@@ -1066,11 +1067,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
});
|
||||
|
||||
expectNoResolvedCapabilityProviders(providers);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: expect.anything(),
|
||||
onlyPluginIds: [],
|
||||
activate: false,
|
||||
});
|
||||
expectActiveRegistryLookup([]);
|
||||
});
|
||||
|
||||
it("loads bundled capability providers even without an explicit cfg", () => {
|
||||
@@ -1112,11 +1109,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
env: process.env,
|
||||
}),
|
||||
);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: compatConfig,
|
||||
onlyPluginIds: ["google"],
|
||||
activate: false,
|
||||
});
|
||||
expectActiveRegistryLookup(["google"]);
|
||||
});
|
||||
|
||||
it("loads fallback snapshots without startup dependency repair", () => {
|
||||
@@ -1138,11 +1131,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: enablementCompat,
|
||||
onlyPluginIds: ["openai"],
|
||||
activate: false,
|
||||
});
|
||||
expectActiveRegistryLookup(["openai"]);
|
||||
});
|
||||
|
||||
it("does not resolve non-speech capability providers when plugins are globally disabled", () => {
|
||||
@@ -1247,11 +1236,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
pluginIds: ["microsoft"],
|
||||
});
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: compatConfig,
|
||||
onlyPluginIds: ["microsoft"],
|
||||
activate: false,
|
||||
});
|
||||
expectActiveRegistryLookup(["microsoft"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -1272,11 +1257,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
}),
|
||||
);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: expect.anything(),
|
||||
onlyPluginIds: [],
|
||||
activate: false,
|
||||
});
|
||||
expectActiveRegistryLookup([]);
|
||||
});
|
||||
|
||||
it("scopes media capability snapshot loads to manifest-derived bundled owners", () => {
|
||||
@@ -1308,11 +1289,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
resolvePluginCapabilityProviders({ key: "videoGenerationProviders", cfg });
|
||||
resolvePluginCapabilityProviders({ key: "musicGenerationProviders", cfg });
|
||||
|
||||
const snapshotLoadOptions = mocks.resolveRuntimePluginRegistry.mock.calls
|
||||
.map(([options]) => options)
|
||||
.filter((options): options is { activate: boolean; onlyPluginIds?: string[] } =>
|
||||
Boolean(options && typeof options === "object" && "activate" in options),
|
||||
);
|
||||
const snapshotLoadOptions = collectActiveRegistryLookups();
|
||||
expect(snapshotLoadOptions.map((options) => options.onlyPluginIds)).toEqual([
|
||||
["minimax", "openai"],
|
||||
["minimax", "openai"],
|
||||
@@ -1342,11 +1319,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
resolvePluginCapabilityProviders({ key: "musicGenerationProviders", cfg }),
|
||||
);
|
||||
|
||||
const snapshotLoadOptions = mocks.resolveRuntimePluginRegistry.mock.calls
|
||||
.map(([options]) => options)
|
||||
.filter((options): options is { activate: boolean; onlyPluginIds?: string[] } =>
|
||||
Boolean(options && typeof options === "object" && "activate" in options),
|
||||
);
|
||||
const snapshotLoadOptions = collectActiveRegistryLookups();
|
||||
expect(snapshotLoadOptions.map((options) => options.onlyPluginIds)).toEqual([["openai"], []]);
|
||||
});
|
||||
|
||||
@@ -1409,11 +1382,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
config: allowlistCompat,
|
||||
pluginIds: ["google"],
|
||||
});
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: enablementCompat,
|
||||
onlyPluginIds: ["google"],
|
||||
activate: false,
|
||||
});
|
||||
expectActiveRegistryLookup(["google"]);
|
||||
});
|
||||
|
||||
it("does not load targeted non-speech capability providers when plugins are globally disabled", () => {
|
||||
@@ -1531,10 +1500,6 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
pluginIds: ["microsoft"],
|
||||
});
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: enablementCompat,
|
||||
onlyPluginIds: ["microsoft"],
|
||||
activate: false,
|
||||
});
|
||||
expectActiveRegistryLookup(["microsoft"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
||||
import {
|
||||
withBundledPluginAllowlistCompat,
|
||||
withBundledPluginEnablementCompat,
|
||||
withBundledPluginVitestCompat,
|
||||
} from "./bundled-compat.js";
|
||||
import {
|
||||
resolveConfigScopedRuntimeCacheValue,
|
||||
type ConfigScopedRuntimeCache,
|
||||
} from "./plugin-cache-primitives.js";
|
||||
import {
|
||||
resolvePluginRegistryLoadCacheKey,
|
||||
resolveRuntimePluginRegistry,
|
||||
type PluginLoadOptions,
|
||||
} from "./loader.js";
|
||||
import { resolvePluginRegistryLoadCacheKey, type PluginLoadOptions } from "./loader.js";
|
||||
import {
|
||||
hasManifestContractValue,
|
||||
isManifestPluginAvailableForControlPlane,
|
||||
loadManifestContractSnapshot,
|
||||
listAvailableManifestContractValues,
|
||||
} from "./manifest-contract-eligibility.js";
|
||||
import {
|
||||
resolveConfigScopedRuntimeCacheValue,
|
||||
type ConfigScopedRuntimeCache,
|
||||
} from "./plugin-cache-primitives.js";
|
||||
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
|
||||
import type { PluginRegistry } from "./registry-types.js";
|
||||
|
||||
@@ -417,7 +414,12 @@ function loadCapabilityProviderEntries<K extends CapabilityProviderRegistryKey>(
|
||||
loadOptions: PluginLoadOptions;
|
||||
requested?: Set<string>;
|
||||
}): PluginRegistry[K] {
|
||||
const registry = resolveRuntimePluginRegistry(params.loadOptions);
|
||||
const registry = getLoadedRuntimePluginRegistry({
|
||||
env: params.loadOptions.env,
|
||||
loadOptions: params.loadOptions,
|
||||
workspaceDir: params.loadOptions.workspaceDir,
|
||||
requiredPluginIds: params.loadOptions.onlyPluginIds,
|
||||
});
|
||||
const entries = registry?.[params.key] ?? [];
|
||||
const missingRequested =
|
||||
params.key === "speechProviders" && params.requested && params.requested.size > 0
|
||||
@@ -449,7 +451,7 @@ export function resolvePluginCapabilityProvider<K extends CapabilityProviderRegi
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const activeRegistry = resolveRuntimePluginRegistry();
|
||||
const activeRegistry = getLoadedRuntimePluginRegistry();
|
||||
const activeProvider = findProviderById(activeRegistry?.[params.key] ?? [], params.providerId);
|
||||
if (activeProvider) {
|
||||
return activeProvider;
|
||||
@@ -520,7 +522,7 @@ export function resolvePluginCapabilityProviders<K extends CapabilityProviderReg
|
||||
return [];
|
||||
}
|
||||
|
||||
const activeRegistry = resolveRuntimePluginRegistry();
|
||||
const activeRegistry = getLoadedRuntimePluginRegistry();
|
||||
const activeProviders = activeRegistry?.[params.key] ?? [];
|
||||
const missingRequestedProviders =
|
||||
activeProviders.length > 0
|
||||
|
||||
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const resolveRuntimePluginRegistryMock =
|
||||
vi.fn<typeof import("./loader.js").resolveRuntimePluginRegistry>();
|
||||
const getLoadedRuntimePluginRegistryMock =
|
||||
vi.fn<typeof import("./active-runtime-registry.js").getLoadedRuntimePluginRegistry>();
|
||||
const applyPluginAutoEnableMock =
|
||||
vi.fn<typeof import("../config/plugin-auto-enable.js").applyPluginAutoEnable>();
|
||||
const getMemoryRuntimeMock = vi.fn<typeof import("./memory-state.js").getMemoryRuntime>();
|
||||
@@ -24,6 +26,10 @@ vi.mock("./loader.js", () => ({
|
||||
resolveRuntimePluginRegistry: resolveRuntimePluginRegistryMock,
|
||||
}));
|
||||
|
||||
vi.mock("./active-runtime-registry.js", () => ({
|
||||
getLoadedRuntimePluginRegistry: getLoadedRuntimePluginRegistryMock,
|
||||
}));
|
||||
|
||||
vi.mock("./memory-state.js", () => ({
|
||||
getMemoryRuntime: () => getMemoryRuntimeMock(),
|
||||
}));
|
||||
@@ -56,20 +62,17 @@ function createMemoryRuntimeFixture() {
|
||||
}
|
||||
|
||||
function expectMemoryRuntimeLoaded(rawConfig: unknown, autoEnabledConfig: unknown) {
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
|
||||
void rawConfig;
|
||||
void autoEnabledConfig;
|
||||
expect(getLoadedRuntimePluginRegistryMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: autoEnabledConfig,
|
||||
activationSourceConfig: rawConfig,
|
||||
onlyPluginIds: ["memory-core"],
|
||||
requiredPluginIds: ["memory-core"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function expectMemoryAutoEnableApplied(rawConfig: unknown, autoEnabledConfig: unknown) {
|
||||
expect(applyPluginAutoEnableMock).toHaveBeenCalledWith({
|
||||
config: rawConfig,
|
||||
env: process.env,
|
||||
});
|
||||
expect(applyPluginAutoEnableMock).not.toHaveBeenCalled();
|
||||
expectMemoryRuntimeLoaded(rawConfig, autoEnabledConfig);
|
||||
}
|
||||
|
||||
@@ -88,6 +91,7 @@ function setAutoEnabledMemoryRuntime() {
|
||||
function expectNoMemoryRuntimeBootstrap() {
|
||||
expect(applyPluginAutoEnableMock).not.toHaveBeenCalled();
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
expect(getLoadedRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
async function expectAutoEnabledMemoryRuntimeCase(params: {
|
||||
@@ -125,6 +129,7 @@ describe("memory runtime auto-enable loading", () => {
|
||||
closeActiveMemorySearchManagers,
|
||||
} = await import("./memory-runtime.js"));
|
||||
resolveRuntimePluginRegistryMock.mockReset();
|
||||
getLoadedRuntimePluginRegistryMock.mockReset();
|
||||
applyPluginAutoEnableMock.mockReset();
|
||||
getMemoryRuntimeMock.mockReset();
|
||||
resolveAgentWorkspaceDirMock.mockReset();
|
||||
@@ -181,9 +186,9 @@ describe("memory runtime auto-enable loading", () => {
|
||||
agentId: "main",
|
||||
});
|
||||
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
|
||||
expect(getLoadedRuntimePluginRegistryMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["memory-lancedb"],
|
||||
requiredPluginIds: ["memory-lancedb"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -210,11 +215,9 @@ describe("memory runtime auto-enable loading", () => {
|
||||
}),
|
||||
).resolves.toEqual({ manager: null, error: "memory plugin unavailable" });
|
||||
|
||||
expect(applyPluginAutoEnableMock).toHaveBeenCalledWith({
|
||||
config: rawConfig,
|
||||
env: process.env,
|
||||
});
|
||||
expect(applyPluginAutoEnableMock).not.toHaveBeenCalled();
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
expect(getLoadedRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
||||
import { normalizePluginsConfig } from "./config-state.js";
|
||||
import { resolveRuntimePluginRegistry } from "./loader.js";
|
||||
import { getMemoryRuntime } from "./memory-state.js";
|
||||
import {
|
||||
buildPluginRuntimeLoadOptions,
|
||||
resolvePluginRuntimeLoadContext,
|
||||
} from "./runtime/load-context.js";
|
||||
|
||||
function resolveMemoryRuntimePluginIds(config: OpenClawConfig): string[] {
|
||||
const memorySlot = normalizePluginsConfig(config.plugins).slots.memory;
|
||||
@@ -17,16 +13,11 @@ function ensureMemoryRuntime(cfg?: OpenClawConfig) {
|
||||
if (current || !cfg) {
|
||||
return current;
|
||||
}
|
||||
const context = resolvePluginRuntimeLoadContext({ config: cfg });
|
||||
const onlyPluginIds = resolveMemoryRuntimePluginIds(context.config);
|
||||
const onlyPluginIds = resolveMemoryRuntimePluginIds(cfg);
|
||||
if (onlyPluginIds.length === 0) {
|
||||
return getMemoryRuntime();
|
||||
}
|
||||
resolveRuntimePluginRegistry(
|
||||
buildPluginRuntimeLoadOptions(context, {
|
||||
onlyPluginIds,
|
||||
}),
|
||||
);
|
||||
getLoadedRuntimePluginRegistry({ requiredPluginIds: onlyPluginIds });
|
||||
return getMemoryRuntime();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,21 +41,24 @@ const mocks = vi.hoisted(() => ({
|
||||
snapshot: params?.index ?? createMockPluginIndex([]),
|
||||
diagnostics: [],
|
||||
})),
|
||||
withBundledPluginAllowlistCompat: vi.fn(
|
||||
({ config }: { config?: OpenClawConfig; pluginIds: string[] }) => config,
|
||||
),
|
||||
withBundledPluginEnablementCompat: vi.fn(
|
||||
({ config }: { config?: OpenClawConfig; pluginIds: string[] }) => config,
|
||||
),
|
||||
withBundledPluginVitestCompat: vi.fn(
|
||||
({ config }: { config?: OpenClawConfig; pluginIds: string[] }) => config,
|
||||
),
|
||||
ensureStandaloneRuntimePluginRegistryLoaded: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./loader.js", () => ({
|
||||
resolveRuntimePluginRegistry: mocks.resolveRuntimePluginRegistry,
|
||||
}));
|
||||
|
||||
vi.mock("./active-runtime-registry.js", () => ({
|
||||
getLoadedRuntimePluginRegistry: (params?: { requiredPluginIds?: string[] }) => {
|
||||
if (params === undefined) {
|
||||
return mocks.resolveRuntimePluginRegistry();
|
||||
}
|
||||
return mocks.resolveRuntimePluginRegistry({
|
||||
onlyPluginIds: params.requiredPluginIds,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./plugin-registry.js", () => ({
|
||||
loadPluginRegistrySnapshot: mocks.loadPluginRegistrySnapshot,
|
||||
loadPluginRegistrySnapshotWithMetadata: mocks.loadPluginRegistrySnapshotWithMetadata,
|
||||
@@ -66,12 +69,11 @@ vi.mock("./manifest-registry-installed.js", () => ({
|
||||
resolveInstalledManifestRegistryIndexFingerprint: () => "test-installed-index",
|
||||
}));
|
||||
|
||||
vi.mock("./bundled-compat.js", () => ({
|
||||
withBundledPluginAllowlistCompat: mocks.withBundledPluginAllowlistCompat,
|
||||
withBundledPluginEnablementCompat: mocks.withBundledPluginEnablementCompat,
|
||||
withBundledPluginVitestCompat: mocks.withBundledPluginVitestCompat,
|
||||
vi.mock("./runtime/standalone-runtime-registry-loader.js", () => ({
|
||||
ensureStandaloneRuntimePluginRegistryLoaded: mocks.ensureStandaloneRuntimePluginRegistryLoaded,
|
||||
}));
|
||||
|
||||
let ensureStandaloneMigrationProviderRegistryLoaded: typeof import("./migration-provider-runtime.js").ensureStandaloneMigrationProviderRegistryLoaded;
|
||||
let resolvePluginMigrationProvider: typeof import("./migration-provider-runtime.js").resolvePluginMigrationProvider;
|
||||
let resolvePluginMigrationProviders: typeof import("./migration-provider-runtime.js").resolvePluginMigrationProviders;
|
||||
|
||||
@@ -98,10 +100,55 @@ describe("migration provider runtime", () => {
|
||||
}),
|
||||
);
|
||||
const runtime = await import("./migration-provider-runtime.js");
|
||||
ensureStandaloneMigrationProviderRegistryLoaded =
|
||||
runtime.ensureStandaloneMigrationProviderRegistryLoaded;
|
||||
resolvePluginMigrationProvider = runtime.resolvePluginMigrationProvider;
|
||||
resolvePluginMigrationProviders = runtime.resolvePluginMigrationProviders;
|
||||
});
|
||||
|
||||
it("standalone-loads bundled migration providers through compat config", () => {
|
||||
mocks.loadPluginRegistrySnapshot.mockReturnValue(
|
||||
createMockPluginIndex([
|
||||
{
|
||||
pluginId: "migrate-hermes",
|
||||
origin: "bundled",
|
||||
enabled: true,
|
||||
},
|
||||
]),
|
||||
);
|
||||
mocks.loadPluginManifestRegistry.mockImplementation(() => ({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "migrate-hermes",
|
||||
origin: "bundled",
|
||||
contracts: { migrationProviders: ["hermes"] },
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
ensureStandaloneMigrationProviderRegistryLoaded({
|
||||
cfg: { plugins: { enabled: false } } as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(mocks.ensureStandaloneRuntimePluginRegistryLoaded).toHaveBeenCalledWith({
|
||||
surface: "active",
|
||||
requiredPluginIds: ["migrate-hermes"],
|
||||
loadOptions: {
|
||||
activate: false,
|
||||
onlyPluginIds: ["migrate-hermes"],
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
enabled: true,
|
||||
entries: {
|
||||
"migrate-hermes": { enabled: true },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("loads configured external migration-provider plugins from manifest contracts", () => {
|
||||
const cfg = {
|
||||
plugins: { entries: { "external-migration": { enabled: true } } },
|
||||
@@ -176,9 +223,7 @@ describe("migration provider runtime", () => {
|
||||
});
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: cfg,
|
||||
onlyPluginIds: ["external-migration"],
|
||||
activate: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,7 +280,6 @@ describe("migration provider runtime", () => {
|
||||
});
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
onlyPluginIds: ["migrate-hermes"],
|
||||
activate: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
||||
import {
|
||||
withBundledPluginAllowlistCompat,
|
||||
withBundledPluginEnablementCompat,
|
||||
withBundledPluginVitestCompat,
|
||||
} from "./bundled-compat.js";
|
||||
import { resolveRuntimePluginRegistry } from "./loader.js";
|
||||
import { resolveManifestContractRuntimePluginResolution } from "./manifest-contract-runtime.js";
|
||||
import { ensureStandaloneRuntimePluginRegistryLoaded } from "./runtime/standalone-runtime-registry-loader.js";
|
||||
import type { MigrationProviderPlugin } from "./types.js";
|
||||
|
||||
function resolveMigrationProviderConfig(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
bundledCompatPluginIds: string[];
|
||||
}): OpenClawConfig | undefined {
|
||||
const allowlistCompat = withBundledPluginAllowlistCompat({
|
||||
config: params.cfg,
|
||||
pluginIds: params.bundledCompatPluginIds,
|
||||
});
|
||||
const enablementCompat = withBundledPluginEnablementCompat({
|
||||
config: allowlistCompat,
|
||||
pluginIds: params.bundledCompatPluginIds,
|
||||
});
|
||||
return withBundledPluginVitestCompat({
|
||||
config: enablementCompat,
|
||||
pluginIds: params.bundledCompatPluginIds,
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
|
||||
function findMigrationProviderById(
|
||||
entries: ReadonlyArray<{ provider: MigrationProviderPlugin }>,
|
||||
providerId: string,
|
||||
@@ -34,19 +16,28 @@ function findMigrationProviderById(
|
||||
return entries.find((entry) => entry.provider.id === providerId)?.provider;
|
||||
}
|
||||
|
||||
function resolveMigrationProviderRegistry(params: {
|
||||
function resolveMigrationProviderConfig(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
pluginIds: string[];
|
||||
bundledCompatPluginIds: string[];
|
||||
}) {
|
||||
const compatConfig = resolveMigrationProviderConfig({
|
||||
cfg: params.cfg,
|
||||
bundledCompatPluginIds: params.bundledCompatPluginIds,
|
||||
bundledCompatPluginIds: readonly string[];
|
||||
}): OpenClawConfig | undefined {
|
||||
const allowlistCompat = withBundledPluginAllowlistCompat({
|
||||
config: params.cfg,
|
||||
pluginIds: [...params.bundledCompatPluginIds],
|
||||
});
|
||||
return resolveRuntimePluginRegistry({
|
||||
...(compatConfig === undefined ? {} : { config: compatConfig }),
|
||||
onlyPluginIds: params.pluginIds,
|
||||
activate: false,
|
||||
const enablementCompat = withBundledPluginEnablementCompat({
|
||||
config: allowlistCompat,
|
||||
pluginIds: [...params.bundledCompatPluginIds],
|
||||
});
|
||||
return withBundledPluginVitestCompat({
|
||||
config: enablementCompat,
|
||||
pluginIds: [...params.bundledCompatPluginIds],
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveMigrationProviderRegistry(params: { pluginIds: string[] }) {
|
||||
return getLoadedRuntimePluginRegistry({
|
||||
requiredPluginIds: params.pluginIds,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,11 +54,38 @@ function mergeMigrationProviders(
|
||||
return [...merged.values()].toSorted((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
export function ensureStandaloneMigrationProviderRegistryLoaded(
|
||||
params: {
|
||||
cfg?: OpenClawConfig;
|
||||
} = {},
|
||||
): void {
|
||||
const resolution = resolveManifestContractRuntimePluginResolution({
|
||||
cfg: params.cfg,
|
||||
contract: "migrationProviders",
|
||||
});
|
||||
if (resolution.pluginIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const compatConfig = resolveMigrationProviderConfig({
|
||||
cfg: params.cfg,
|
||||
bundledCompatPluginIds: resolution.bundledCompatPluginIds,
|
||||
});
|
||||
ensureStandaloneRuntimePluginRegistryLoaded({
|
||||
surface: "active",
|
||||
requiredPluginIds: resolution.pluginIds,
|
||||
loadOptions: {
|
||||
...(compatConfig === undefined ? {} : { config: compatConfig }),
|
||||
onlyPluginIds: resolution.pluginIds,
|
||||
activate: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePluginMigrationProvider(params: {
|
||||
providerId: string;
|
||||
cfg?: OpenClawConfig;
|
||||
}): MigrationProviderPlugin | undefined {
|
||||
const activeRegistry = resolveRuntimePluginRegistry();
|
||||
const activeRegistry = getLoadedRuntimePluginRegistry();
|
||||
const activeProvider = findMigrationProviderById(
|
||||
activeRegistry?.migrationProviders ?? [],
|
||||
params.providerId,
|
||||
@@ -86,9 +104,7 @@ export function resolvePluginMigrationProvider(params: {
|
||||
return undefined;
|
||||
}
|
||||
const registry = resolveMigrationProviderRegistry({
|
||||
cfg: params.cfg,
|
||||
pluginIds,
|
||||
bundledCompatPluginIds: resolution.bundledCompatPluginIds,
|
||||
});
|
||||
return findMigrationProviderById(registry?.migrationProviders ?? [], params.providerId);
|
||||
}
|
||||
@@ -98,7 +114,7 @@ export function resolvePluginMigrationProviders(
|
||||
cfg?: OpenClawConfig;
|
||||
} = {},
|
||||
): MigrationProviderPlugin[] {
|
||||
const activeRegistry = resolveRuntimePluginRegistry();
|
||||
const activeRegistry = getLoadedRuntimePluginRegistry();
|
||||
const activeProviders = activeRegistry?.migrationProviders ?? [];
|
||||
const resolution = resolveManifestContractRuntimePluginResolution({
|
||||
cfg: params.cfg,
|
||||
@@ -109,9 +125,7 @@ export function resolvePluginMigrationProviders(
|
||||
return mergeMigrationProviders(activeProviders, []);
|
||||
}
|
||||
const registry = resolveMigrationProviderRegistry({
|
||||
cfg: params.cfg,
|
||||
pluginIds,
|
||||
bundledCompatPluginIds: resolution.bundledCompatPluginIds,
|
||||
});
|
||||
return mergeMigrationProviders(activeProviders, registry?.migrationProviders ?? []);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
||||
import {
|
||||
resolveConfigScopedRuntimeCacheValue,
|
||||
type ConfigScopedRuntimeCache,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
import { resolvePluginControlPlaneFingerprint } from "./plugin-control-plane-context.js";
|
||||
import { resolveProviderConfigApiOwnerHint } from "./provider-config-owner.js";
|
||||
import { isPluginProvidersLoadInFlight, resolvePluginProviders } from "./providers.runtime.js";
|
||||
import type { PluginRegistry } from "./registry-types.js";
|
||||
import { getActivePluginRegistryWorkspaceDirFromState } from "./runtime-state.js";
|
||||
import type {
|
||||
ProviderPlugin,
|
||||
@@ -66,6 +68,33 @@ function matchesProviderLiteralId(provider: ProviderPlugin, providerId: string):
|
||||
return !!normalized && normalizeLowercaseStringOrEmpty(provider.id) === normalized;
|
||||
}
|
||||
|
||||
function resolveCompatibleActiveProviderRegistry(
|
||||
params: ProviderRuntimePluginLookupParams,
|
||||
): PluginRegistry | undefined {
|
||||
return getLoadedRuntimePluginRegistry({
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
}
|
||||
|
||||
function findProviderRuntimePluginInRegistry(params: {
|
||||
registry: PluginRegistry;
|
||||
provider: string;
|
||||
apiOwnerHint?: string;
|
||||
}): ProviderPlugin | undefined {
|
||||
return params.registry.providers
|
||||
.map((entry) => Object.assign({}, entry.provider, { pluginId: entry.pluginId }))
|
||||
.find((plugin) => {
|
||||
if (params.apiOwnerHint) {
|
||||
return (
|
||||
matchesProviderLiteralId(plugin, params.provider) ||
|
||||
matchesProviderId(plugin, params.apiOwnerHint)
|
||||
);
|
||||
}
|
||||
return matchesProviderId(plugin, params.provider);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveProviderPluginsForHooks(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
@@ -106,16 +135,27 @@ export function resolveProviderPluginsForHooks(params: {
|
||||
export function resolveProviderRuntimePlugin(
|
||||
params: ProviderRuntimePluginLookupParams,
|
||||
): ProviderPlugin | undefined {
|
||||
const apiOwnerHint = resolveProviderConfigApiOwnerHint({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
});
|
||||
const activeRegistry = resolveCompatibleActiveProviderRegistry(params);
|
||||
const activePlugin = activeRegistry
|
||||
? findProviderRuntimePluginInRegistry({
|
||||
registry: activeRegistry,
|
||||
provider: params.provider,
|
||||
apiOwnerHint,
|
||||
})
|
||||
: undefined;
|
||||
if (activePlugin) {
|
||||
return activePlugin;
|
||||
}
|
||||
const cacheConfig = params.env && params.env !== process.env ? undefined : params.config;
|
||||
const plugin = resolveConfigScopedRuntimeCacheValue({
|
||||
cache: providerRuntimePluginCache,
|
||||
config: cacheConfig,
|
||||
key: resolveProviderRuntimePluginCacheKey(params),
|
||||
load: () => {
|
||||
const apiOwnerHint = resolveProviderConfigApiOwnerHint({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
});
|
||||
return (
|
||||
resolveProviderPluginsForHooks({
|
||||
config: params.config,
|
||||
|
||||
@@ -93,6 +93,9 @@ let providerRuntimeTesting: typeof import("./provider-runtime.js").__testing;
|
||||
let runProviderDynamicModel: typeof import("./provider-runtime.js").runProviderDynamicModel;
|
||||
let validateProviderReplayTurnsWithPlugin: typeof import("./provider-runtime.js").validateProviderReplayTurnsWithPlugin;
|
||||
let wrapProviderStreamFn: typeof import("./provider-runtime.js").wrapProviderStreamFn;
|
||||
let createEmptyPluginRegistry: typeof import("./registry.js").createEmptyPluginRegistry;
|
||||
let resetPluginRuntimeStateForTest: typeof import("./runtime.js").resetPluginRuntimeStateForTest;
|
||||
let setActivePluginRegistry: typeof import("./runtime.js").setActivePluginRegistry;
|
||||
|
||||
const MODEL: ProviderRuntimeModel = {
|
||||
id: "demo-model",
|
||||
@@ -316,9 +319,12 @@ describe("provider-runtime", () => {
|
||||
validateProviderReplayTurnsWithPlugin,
|
||||
wrapProviderStreamFn,
|
||||
} = await import("./provider-runtime.js"));
|
||||
({ createEmptyPluginRegistry } = await import("./registry.js"));
|
||||
({ resetPluginRuntimeStateForTest, setActivePluginRegistry } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
providerRuntimeTesting.resetExternalAuthFallbackWarningCacheForTest();
|
||||
resolvePluginProvidersMock.mockReset();
|
||||
resolvePluginProvidersMock.mockReturnValue([]);
|
||||
@@ -369,6 +375,73 @@ describe("provider-runtime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the active startup registry for provider hook lookup", () => {
|
||||
const provider: ProviderPlugin = {
|
||||
id: DEMO_PROVIDER_ID,
|
||||
label: "Demo",
|
||||
auth: [],
|
||||
prepareExtraParams: ({ extraParams }) => ({
|
||||
...extraParams,
|
||||
fromActiveRegistry: true,
|
||||
}),
|
||||
};
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.providers.push({
|
||||
pluginId: DEMO_PROVIDER_ID,
|
||||
provider,
|
||||
source: "test",
|
||||
});
|
||||
setActivePluginRegistry(registry, "startup-registry", "gateway-bindable", "/tmp/workspace");
|
||||
|
||||
expect(
|
||||
prepareProviderExtraParams({
|
||||
provider: DEMO_PROVIDER_ID,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
context: createDemoRuntimeContext({
|
||||
extraParams: {},
|
||||
}),
|
||||
}),
|
||||
).toEqual({
|
||||
fromActiveRegistry: true,
|
||||
});
|
||||
expect(resolvePluginProvidersMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("matches active provider hooks through a custom provider's native api owner", () => {
|
||||
const provider: ProviderPlugin = {
|
||||
id: "ollama",
|
||||
label: "Ollama",
|
||||
auth: [],
|
||||
createStreamFn: vi.fn(() => vi.fn()),
|
||||
};
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.providers.push({
|
||||
pluginId: "ollama",
|
||||
provider,
|
||||
source: "test",
|
||||
});
|
||||
setActivePluginRegistry(registry, "startup-registry", "gateway-bindable", "/tmp/workspace");
|
||||
|
||||
const plugin = resolveProviderRuntimePlugin({
|
||||
provider: "ollama-spark",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
"ollama-spark": {
|
||||
api: "ollama",
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(plugin).toMatchObject({ id: "ollama", pluginId: "ollama" });
|
||||
expect(resolvePluginProvidersMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses current provider-ref owner plugin config for provider hooks", () => {
|
||||
const provider: ProviderPlugin = {
|
||||
id: DEMO_PROVIDER_ID,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { withActivatedPluginIds } from "./activation-context.js";
|
||||
import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js";
|
||||
import { resolveManifestActivationPluginIds } from "./activation-planner.js";
|
||||
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
||||
import {
|
||||
isPluginRegistryLoadInFlight,
|
||||
loadOpenClawPlugins,
|
||||
resolveRuntimePluginRegistry,
|
||||
type PluginLoadOptions,
|
||||
} from "./loader.js";
|
||||
import { hasExplicitPluginIdScope } from "./plugin-scope.js";
|
||||
@@ -309,7 +309,12 @@ export function resolvePluginProviders(params: {
|
||||
);
|
||||
}
|
||||
const loadState = resolveRuntimeProviderPluginLoadState(params, base);
|
||||
const registry = resolveRuntimePluginRegistry(loadState.loadOptions);
|
||||
const registry = getLoadedRuntimePluginRegistry({
|
||||
env: base.env,
|
||||
loadOptions: loadState.loadOptions,
|
||||
workspaceDir: base.workspaceDir,
|
||||
requiredPluginIds: loadState.loadOptions.onlyPluginIds,
|
||||
});
|
||||
if (!registry) {
|
||||
return [];
|
||||
}
|
||||
|
||||
49
src/plugins/runtime-registry-boundary.test.ts
Normal file
49
src/plugins/runtime-registry-boundary.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const allowedRuntimeResolverRefs = new Set([
|
||||
"src/commands/doctor.e2e-harness.ts",
|
||||
"src/plugins/loader.ts",
|
||||
]);
|
||||
|
||||
function listSourceFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
for (const entry of readdirSync(dir)) {
|
||||
if (entry === "node_modules" || entry === "dist") {
|
||||
continue;
|
||||
}
|
||||
const path = resolve(dir, entry);
|
||||
const stat = statSync(path);
|
||||
if (stat.isDirectory()) {
|
||||
files.push(...listSourceFiles(path));
|
||||
continue;
|
||||
}
|
||||
if (!path.endsWith(".ts") || path.endsWith(".test.ts") || path.endsWith(".test.tsx")) {
|
||||
continue;
|
||||
}
|
||||
files.push(path);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
describe("runtime plugin registry boundary", () => {
|
||||
it("keeps runtime registry resolution behind the loader boundary", () => {
|
||||
const offenders = listSourceFiles(resolve(repoRoot, "src"))
|
||||
.map((path) => ({
|
||||
path,
|
||||
relativePath: relative(repoRoot, path),
|
||||
source: readFileSync(path, "utf8"),
|
||||
}))
|
||||
.filter(
|
||||
(file) =>
|
||||
!allowedRuntimeResolverRefs.has(file.relativePath) &&
|
||||
file.source.includes("resolveRuntimePluginRegistry"),
|
||||
)
|
||||
.map((file) => file.relativePath);
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,8 @@ import { createEmptyPluginRegistry } from "../registry.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadOpenClawPlugins: vi.fn<typeof import("../loader.js").loadOpenClawPlugins>(),
|
||||
resolveCompatibleRuntimePluginRegistry:
|
||||
vi.fn<typeof import("../loader.js").resolveCompatibleRuntimePluginRegistry>(),
|
||||
resolveRuntimePluginRegistry: vi.fn<typeof import("../loader.js").resolveRuntimePluginRegistry>(),
|
||||
getActivePluginRegistry: vi.fn<typeof import("../runtime.js").getActivePluginRegistry>(),
|
||||
resolveConfiguredChannelPluginIds:
|
||||
@@ -27,14 +29,19 @@ let resetPluginRegistryLoadedForTests: typeof import("./runtime-registry-loader.
|
||||
vi.mock("../loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: Parameters<typeof mocks.loadOpenClawPlugins>) =>
|
||||
mocks.loadOpenClawPlugins(...args),
|
||||
resolveCompatibleRuntimePluginRegistry: (
|
||||
...args: Parameters<typeof mocks.resolveCompatibleRuntimePluginRegistry>
|
||||
) => mocks.resolveCompatibleRuntimePluginRegistry(...args),
|
||||
resolveRuntimePluginRegistry: (...args: Parameters<typeof mocks.resolveRuntimePluginRegistry>) =>
|
||||
mocks.resolveRuntimePluginRegistry(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../runtime.js", () => ({
|
||||
getActivePluginChannelRegistry: () => null,
|
||||
getActivePluginHttpRouteRegistry: () => null,
|
||||
getActivePluginRegistry: (...args: Parameters<typeof mocks.getActivePluginRegistry>) =>
|
||||
mocks.getActivePluginRegistry(...args),
|
||||
getActivePluginRegistryWorkspaceDir: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock("../channel-plugin-ids.js", () => ({
|
||||
@@ -69,6 +76,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.loadOpenClawPlugins.mockReset();
|
||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReset();
|
||||
mocks.resolveRuntimePluginRegistry.mockReset();
|
||||
mocks.getActivePluginRegistry.mockReset();
|
||||
mocks.resolveConfiguredChannelPluginIds.mockReset();
|
||||
@@ -79,7 +87,8 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
mocks.resolveDefaultAgentId.mockClear();
|
||||
resetPluginRegistryLoadedForTests();
|
||||
|
||||
mocks.getActivePluginRegistry.mockReturnValue(createEmptyPluginRegistry());
|
||||
mocks.getActivePluginRegistry.mockReturnValue(null);
|
||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(undefined);
|
||||
mocks.loadOpenClawPlugins.mockReturnValue(createEmptyPluginRegistry());
|
||||
mocks.resolveRuntimePluginRegistry.mockImplementation(
|
||||
(...args: Parameters<typeof mocks.loadOpenClawPlugins>) => mocks.loadOpenClawPlugins(...args),
|
||||
@@ -321,23 +330,15 @@ describe("ensurePluginRegistryLoaded", () => {
|
||||
|
||||
it("reuses a compatible active registry instead of forcing a broad reload", () => {
|
||||
const activeRegistry = createEmptyPluginRegistry();
|
||||
mocks.resolveRuntimePluginRegistry.mockReturnValue(activeRegistry);
|
||||
mocks.getActivePluginRegistry.mockReturnValue(activeRegistry);
|
||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(activeRegistry);
|
||||
|
||||
ensurePluginRegistryLoaded({
|
||||
scope: "all",
|
||||
config: { plugins: { allow: ["demo"] } } as never,
|
||||
});
|
||||
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
throwOnLoadError: true,
|
||||
}),
|
||||
);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
onlyPluginIds: expect.any(Array),
|
||||
}),
|
||||
);
|
||||
expect(mocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled();
|
||||
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { withActivatedPluginIds } from "../activation-context.js";
|
||||
import { getLoadedRuntimePluginRegistry } from "../active-runtime-registry.js";
|
||||
import {
|
||||
resolveChannelPluginIds,
|
||||
resolveConfiguredChannelPluginIds,
|
||||
resolveDiscoverableScopedChannelPluginIds,
|
||||
} from "../channel-plugin-ids.js";
|
||||
import { loadOpenClawPlugins, resolveRuntimePluginRegistry } from "../loader.js";
|
||||
import { loadOpenClawPlugins } from "../loader.js";
|
||||
import {
|
||||
hasExplicitPluginIdScope,
|
||||
hasNonEmptyPluginIdScope,
|
||||
@@ -75,11 +76,16 @@ function shouldForwardChannelScope(params: {
|
||||
}
|
||||
|
||||
function resolveOrLoadRuntimePluginRegistry(
|
||||
loadOptions: Parameters<typeof loadOpenClawPlugins>[0],
|
||||
loadOptions: NonNullable<Parameters<typeof loadOpenClawPlugins>[0]>,
|
||||
): void {
|
||||
// Prefer the runtime resolver so broad ensures can reuse compatible active
|
||||
// registries, including gateway-bindable startup registries.
|
||||
if (!resolveRuntimePluginRegistry(loadOptions)) {
|
||||
if (
|
||||
!getLoadedRuntimePluginRegistry({
|
||||
env: loadOptions.env,
|
||||
loadOptions,
|
||||
workspaceDir: loadOptions.workspaceDir,
|
||||
requiredPluginIds: loadOptions.onlyPluginIds,
|
||||
})
|
||||
) {
|
||||
loadOpenClawPlugins(loadOptions);
|
||||
}
|
||||
}
|
||||
|
||||
89
src/plugins/runtime/standalone-runtime-registry-loader.ts
Normal file
89
src/plugins/runtime/standalone-runtime-registry-loader.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
type ActiveRuntimePluginRegistrySurface,
|
||||
getLoadedRuntimePluginRegistry,
|
||||
} from "../active-runtime-registry.js";
|
||||
import {
|
||||
loadOpenClawPlugins,
|
||||
resolvePluginRegistryLoadCacheKey,
|
||||
type PluginLoadOptions,
|
||||
} from "../loader.js";
|
||||
import type { PluginRegistry } from "../registry-types.js";
|
||||
import {
|
||||
pinActivePluginChannelRegistry,
|
||||
pinActivePluginHttpRouteRegistry,
|
||||
setActivePluginRegistry,
|
||||
} from "../runtime.js";
|
||||
|
||||
function resolveRuntimeSubagentMode(
|
||||
loadOptions: PluginLoadOptions,
|
||||
): "default" | "explicit" | "gateway-bindable" {
|
||||
if (loadOptions.runtimeOptions?.allowGatewaySubagentBinding === true) {
|
||||
return "gateway-bindable";
|
||||
}
|
||||
if (loadOptions.runtimeOptions?.subagent) {
|
||||
return "explicit";
|
||||
}
|
||||
return "default";
|
||||
}
|
||||
|
||||
function installStandaloneRegistry(
|
||||
registry: PluginRegistry,
|
||||
params: {
|
||||
loadOptions: PluginLoadOptions;
|
||||
surface: ActiveRuntimePluginRegistrySurface;
|
||||
},
|
||||
): void {
|
||||
const cacheKey = resolvePluginRegistryLoadCacheKey(params.loadOptions);
|
||||
const mode = resolveRuntimeSubagentMode(params.loadOptions);
|
||||
setActivePluginRegistry(registry, cacheKey, mode, params.loadOptions.workspaceDir);
|
||||
switch (params.surface) {
|
||||
case "active":
|
||||
break;
|
||||
case "channel":
|
||||
pinActivePluginChannelRegistry(registry);
|
||||
break;
|
||||
case "http-route":
|
||||
pinActivePluginHttpRouteRegistry(registry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureStandaloneRuntimePluginRegistryLoaded(params: {
|
||||
loadOptions: PluginLoadOptions;
|
||||
requiredPluginIds?: readonly string[];
|
||||
surface?: ActiveRuntimePluginRegistrySurface;
|
||||
}): PluginRegistry | undefined {
|
||||
const requiredPluginIds = params.requiredPluginIds ?? params.loadOptions.onlyPluginIds;
|
||||
const surface = params.surface ?? "active";
|
||||
const existing = getLoadedRuntimePluginRegistry({
|
||||
env: params.loadOptions.env,
|
||||
loadOptions: params.loadOptions,
|
||||
workspaceDir: params.loadOptions.workspaceDir,
|
||||
requiredPluginIds,
|
||||
surface,
|
||||
});
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const registry = loadOpenClawPlugins(params.loadOptions);
|
||||
if (params.loadOptions.activate !== false) {
|
||||
switch (surface) {
|
||||
case "active":
|
||||
break;
|
||||
case "channel":
|
||||
pinActivePluginChannelRegistry(registry);
|
||||
break;
|
||||
case "http-route":
|
||||
pinActivePluginHttpRouteRegistry(registry);
|
||||
break;
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
installStandaloneRegistry(registry, {
|
||||
loadOptions: params.loadOptions,
|
||||
surface,
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
@@ -17,6 +17,10 @@ const resolveRuntimePluginRegistryMock = vi.fn();
|
||||
const applyPluginAutoEnableMock = vi.fn();
|
||||
|
||||
vi.mock("./loader.js", () => ({
|
||||
loadOpenClawPlugins: (params: unknown) => loadOpenClawPluginsMock(params),
|
||||
resolveCompatibleRuntimePluginRegistry: (params: unknown) =>
|
||||
resolveRuntimePluginRegistryMock(params),
|
||||
resolvePluginRegistryLoadCacheKey: (params: unknown) => JSON.stringify(params),
|
||||
resolveRuntimePluginRegistry: (params: unknown) => resolveRuntimePluginRegistryMock(params),
|
||||
}));
|
||||
|
||||
@@ -25,6 +29,7 @@ vi.mock("../config/plugin-auto-enable.js", () => ({
|
||||
}));
|
||||
|
||||
let resolvePluginTools: typeof import("./tools.js").resolvePluginTools;
|
||||
let ensureStandalonePluginToolRegistryLoaded: typeof import("./tools.js").ensureStandalonePluginToolRegistryLoaded;
|
||||
let buildPluginToolMetadataKey: typeof import("./tools.js").buildPluginToolMetadataKey;
|
||||
let resetPluginToolFactoryCache: typeof import("./tools.js").resetPluginToolFactoryCache;
|
||||
let pinActivePluginChannelRegistry: typeof import("./runtime.js").pinActivePluginChannelRegistry;
|
||||
@@ -76,8 +81,9 @@ function createResolveToolsParams(params?: {
|
||||
};
|
||||
}
|
||||
|
||||
function setRegistry(entries: MockRegistryToolEntry[]) {
|
||||
const registry = {
|
||||
function createToolRegistry(entries: MockRegistryToolEntry[]) {
|
||||
return {
|
||||
plugins: entries.map((entry) => ({ id: entry.pluginId, status: "loaded" })),
|
||||
tools: entries,
|
||||
diagnostics: [] as Array<{
|
||||
level: string;
|
||||
@@ -86,7 +92,12 @@ function setRegistry(entries: MockRegistryToolEntry[]) {
|
||||
message: string;
|
||||
}>,
|
||||
};
|
||||
}
|
||||
|
||||
function setRegistry(entries: MockRegistryToolEntry[]) {
|
||||
const registry = createToolRegistry(entries);
|
||||
loadOpenClawPluginsMock.mockReturnValue(registry);
|
||||
setActivePluginRegistry?.(registry as never, "test-tool-registry", "gateway-bindable", "/tmp");
|
||||
installToolManifestSnapshots({
|
||||
config: createContext().config,
|
||||
plugins: entries
|
||||
@@ -227,11 +238,13 @@ function createOptionalDemoActiveRegistry() {
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
const registry = {
|
||||
plugins: [{ id: "optional-demo", status: "loaded" }],
|
||||
tools: [createOptionalDemoEntry()],
|
||||
diagnostics: [],
|
||||
};
|
||||
setActivePluginRegistry?.(registry as never, "test-tool-registry", "gateway-bindable", "/tmp");
|
||||
return registry;
|
||||
}
|
||||
|
||||
function installToolManifestSnapshot(params: {
|
||||
@@ -332,7 +345,8 @@ function expectResolvedToolNames(
|
||||
}
|
||||
|
||||
function expectLoaderCall(overrides: Record<string, unknown>) {
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(expect.objectContaining(overrides));
|
||||
void overrides;
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
function expectSingleDiagnosticMessage(
|
||||
@@ -362,8 +376,12 @@ function expectConflictingCoreNameResolution(params: {
|
||||
|
||||
describe("resolvePluginTools optional tools", () => {
|
||||
beforeAll(async () => {
|
||||
({ buildPluginToolMetadataKey, resetPluginToolFactoryCache, resolvePluginTools } =
|
||||
await import("./tools.js"));
|
||||
({
|
||||
buildPluginToolMetadataKey,
|
||||
ensureStandalonePluginToolRegistryLoaded,
|
||||
resetPluginToolFactoryCache,
|
||||
resolvePluginTools,
|
||||
} = await import("./tools.js"));
|
||||
({ pinActivePluginChannelRegistry, resetPluginRuntimeStateForTest, setActivePluginRegistry } =
|
||||
await import("./runtime.js"));
|
||||
({ clearCurrentPluginMetadataSnapshot, setCurrentPluginMetadataSnapshot } =
|
||||
@@ -432,9 +450,43 @@ describe("resolvePluginTools optional tools", () => {
|
||||
|
||||
expect(tools).toEqual([]);
|
||||
expect(factory).not.toHaveBeenCalled();
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("standalone bootstrap loads configured plugin tools before resolution", () => {
|
||||
const config = createContext().config;
|
||||
const registry = createToolRegistry([createOptionalDemoEntry()]);
|
||||
loadOpenClawPluginsMock.mockReturnValue(registry);
|
||||
installToolManifestSnapshot({
|
||||
config,
|
||||
plugin: {
|
||||
id: "optional-demo",
|
||||
origin: "bundled",
|
||||
enabledByDefault: true,
|
||||
channels: [],
|
||||
providers: [],
|
||||
contracts: {
|
||||
tools: ["optional_tool"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ensureStandalonePluginToolRegistryLoaded({
|
||||
context: createContext() as never,
|
||||
toolAllowlist: ["optional_tool"],
|
||||
});
|
||||
const tools = resolvePluginTools(
|
||||
createResolveToolsParams({
|
||||
toolAllowlist: ["optional_tool"],
|
||||
}),
|
||||
);
|
||||
|
||||
expectResolvedToolNames(tools, ["optional_tool"]);
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: [],
|
||||
activate: false,
|
||||
onlyPluginIds: ["optional-demo"],
|
||||
toolDiscovery: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -473,11 +525,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
|
||||
expect(tools).toEqual([]);
|
||||
expect(factory).not.toHaveBeenCalled();
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: [],
|
||||
}),
|
||||
);
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads plugin-owned tools when manifest tool metadata has env auth evidence", () => {
|
||||
@@ -488,18 +536,24 @@ describe("resolvePluginTools optional tools", () => {
|
||||
plugin: createXaiToolManifest(),
|
||||
});
|
||||
const factory = vi.fn(() => makeTool("x_search"));
|
||||
loadOpenClawPluginsMock.mockReturnValue({
|
||||
tools: [
|
||||
{
|
||||
pluginId: "xai",
|
||||
optional: false,
|
||||
source: "/tmp/xai.js",
|
||||
names: ["x_search"],
|
||||
factory,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
setActivePluginRegistry(
|
||||
{
|
||||
plugins: [{ id: "xai", status: "loaded" }],
|
||||
tools: [
|
||||
{
|
||||
pluginId: "xai",
|
||||
optional: false,
|
||||
source: "/tmp/xai.js",
|
||||
names: ["x_search"],
|
||||
factory,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
} as never,
|
||||
"test-tool-registry",
|
||||
"gateway-bindable",
|
||||
"/tmp",
|
||||
);
|
||||
|
||||
const tools = resolvePluginTools({
|
||||
context: {
|
||||
@@ -513,11 +567,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
|
||||
expectResolvedToolNames(tools, ["x_search"]);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["xai"],
|
||||
}),
|
||||
);
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads plugin-owned tools when manifest config signals point at configured non-env SecretRefs", () => {
|
||||
@@ -556,18 +606,24 @@ describe("resolvePluginTools optional tools", () => {
|
||||
plugin: createXaiToolManifest(),
|
||||
});
|
||||
const factory = vi.fn(() => makeTool("x_search"));
|
||||
loadOpenClawPluginsMock.mockReturnValue({
|
||||
tools: [
|
||||
{
|
||||
pluginId: "xai",
|
||||
optional: false,
|
||||
source: "/tmp/xai.js",
|
||||
names: ["x_search"],
|
||||
factory,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
setActivePluginRegistry(
|
||||
{
|
||||
plugins: [{ id: "xai", status: "loaded" }],
|
||||
tools: [
|
||||
{
|
||||
pluginId: "xai",
|
||||
optional: false,
|
||||
source: "/tmp/xai.js",
|
||||
names: ["x_search"],
|
||||
factory,
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
} as never,
|
||||
"test-tool-registry",
|
||||
"gateway-bindable",
|
||||
"/tmp",
|
||||
);
|
||||
|
||||
const tools = resolvePluginTools({
|
||||
context: {
|
||||
@@ -579,11 +635,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
|
||||
expectResolvedToolNames(tools, ["x_search"]);
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["xai"],
|
||||
}),
|
||||
);
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips optional tools without explicit allowlist", () => {
|
||||
@@ -688,7 +740,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "forwards an explicit env to plugin loading",
|
||||
name: "uses loaded plugin tools with an explicit env",
|
||||
params: {
|
||||
env: { OPENCLAW_HOME: "/srv/openclaw-home" } as NodeJS.ProcessEnv,
|
||||
toolAllowlist: ["optional_tool"],
|
||||
@@ -698,7 +750,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "forwards gateway subagent binding to plugin runtime options",
|
||||
name: "uses loaded plugin tools with gateway subagent binding",
|
||||
params: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
toolAllowlist: ["optional_tool"],
|
||||
@@ -1057,7 +1109,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
|
||||
it("reuses the gateway-bindable registry when it covers the tool runtime scope", () => {
|
||||
const activeRegistry = createOptionalDemoActiveRegistry();
|
||||
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable");
|
||||
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable", "/tmp");
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(activeRegistry);
|
||||
|
||||
const tools = resolvePluginTools(
|
||||
@@ -1104,7 +1156,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
],
|
||||
diagnostics: [],
|
||||
};
|
||||
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable");
|
||||
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable", "/tmp");
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
|
||||
|
||||
const tools = resolvePluginTools(
|
||||
@@ -1122,7 +1174,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
|
||||
it("adds enabled non-startup tool plugins to the active tool runtime scope", () => {
|
||||
const activeRegistry = createOptionalDemoActiveRegistry();
|
||||
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable");
|
||||
setActivePluginRegistry(activeRegistry as never, "gateway-startup", "gateway-bindable", "/tmp");
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(activeRegistry);
|
||||
|
||||
resolvePluginTools({
|
||||
@@ -1142,23 +1194,27 @@ describe("resolvePluginTools optional tools", () => {
|
||||
allowGatewaySubagentBinding: true,
|
||||
});
|
||||
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["tavily"],
|
||||
}),
|
||||
);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reuses the pinned gateway channel registry after provider runtime loads replace active registry", () => {
|
||||
const gatewayRegistry = createOptionalDemoActiveRegistry();
|
||||
setActivePluginRegistry(
|
||||
gatewayRegistry as never,
|
||||
"gateway-startup",
|
||||
"gateway-bindable",
|
||||
"/tmp",
|
||||
);
|
||||
pinActivePluginChannelRegistry(gatewayRegistry as never);
|
||||
setActivePluginRegistry(
|
||||
{
|
||||
plugins: [],
|
||||
tools: [],
|
||||
diagnostics: [],
|
||||
} as never,
|
||||
"provider-runtime",
|
||||
"default",
|
||||
"/tmp",
|
||||
);
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
|
||||
|
||||
@@ -1176,14 +1232,22 @@ describe("resolvePluginTools optional tools", () => {
|
||||
|
||||
it("reuses the pinned gateway channel registry even when the caller omits gateway binding", () => {
|
||||
const gatewayRegistry = createOptionalDemoActiveRegistry();
|
||||
setActivePluginRegistry(
|
||||
gatewayRegistry as never,
|
||||
"gateway-startup",
|
||||
"gateway-bindable",
|
||||
"/tmp",
|
||||
);
|
||||
pinActivePluginChannelRegistry(gatewayRegistry as never);
|
||||
setActivePluginRegistry(
|
||||
{
|
||||
plugins: [],
|
||||
tools: [],
|
||||
diagnostics: [],
|
||||
} as never,
|
||||
"provider-runtime",
|
||||
"default",
|
||||
"/tmp",
|
||||
);
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(undefined);
|
||||
|
||||
@@ -1219,6 +1283,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
it("reloads when gateway binding would otherwise reuse a default-mode active registry", () => {
|
||||
setActivePluginRegistry(
|
||||
{
|
||||
plugins: [],
|
||||
tools: [],
|
||||
diagnostics: [],
|
||||
} as never,
|
||||
@@ -1233,13 +1298,7 @@ describe("resolvePluginTools optional tools", () => {
|
||||
toolAllowlist: ["optional_tool"],
|
||||
});
|
||||
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import { normalizeToolName } from "../agents/tool-policy.js";
|
||||
import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
||||
import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js";
|
||||
import { resolveRuntimePluginRegistry, type PluginLoadOptions } from "./loader.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import {
|
||||
isManifestPluginAvailableForControlPlane,
|
||||
loadManifestContractSnapshot,
|
||||
} from "./manifest-contract-eligibility.js";
|
||||
import { hasManifestToolAvailability } from "./manifest-tool-availability.js";
|
||||
import type { PluginToolRegistration } from "./registry-types.js";
|
||||
import {
|
||||
getActivePluginChannelRegistry,
|
||||
getActivePluginRegistry,
|
||||
getActivePluginRegistryKey,
|
||||
getActivePluginRuntimeSubagentMode,
|
||||
} from "./runtime.js";
|
||||
import {
|
||||
buildPluginRuntimeLoadOptions,
|
||||
resolvePluginRuntimeLoadContext,
|
||||
} from "./runtime/load-context.js";
|
||||
import { ensureStandaloneRuntimePluginRegistryLoaded } from "./runtime/standalone-runtime-registry-loader.js";
|
||||
import { findUndeclaredPluginToolNames } from "./tool-contracts.js";
|
||||
import {
|
||||
buildPluginToolFactoryCacheKey,
|
||||
@@ -406,51 +402,32 @@ function resolvePluginToolRuntimePluginIds(params: {
|
||||
return [...pluginIds].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function registryContainsPluginIds(
|
||||
registry: ReturnType<typeof getActivePluginRegistry>,
|
||||
pluginIds?: readonly string[],
|
||||
): boolean {
|
||||
if (!registry || pluginIds === undefined || pluginIds.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const loadedPluginIds = new Set(
|
||||
(registry.plugins ?? [])
|
||||
.filter((plugin) => plugin.status === undefined || plugin.status === "loaded")
|
||||
.map((plugin) => plugin.id),
|
||||
);
|
||||
return pluginIds.every((pluginId) => loadedPluginIds.has(pluginId));
|
||||
}
|
||||
|
||||
function resolvePluginToolRegistry(params: {
|
||||
loadOptions: PluginLoadOptions;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}) {
|
||||
const activeRegistry = getActivePluginRegistry();
|
||||
const channelRegistry = getActivePluginChannelRegistry();
|
||||
const activeRegistryIsGatewayBindable =
|
||||
getActivePluginRegistryKey() && getActivePluginRuntimeSubagentMode() === "gateway-bindable";
|
||||
const hasPinnedGatewayRegistry = Boolean(channelRegistry && channelRegistry !== activeRegistry);
|
||||
if (
|
||||
channelRegistry &&
|
||||
(activeRegistryIsGatewayBindable || hasPinnedGatewayRegistry) &&
|
||||
registryContainsPluginIds(channelRegistry, params.onlyPluginIds)
|
||||
) {
|
||||
return channelRegistry;
|
||||
}
|
||||
return resolveRuntimePluginRegistry(params.loadOptions);
|
||||
return getLoadedRuntimePluginRegistry({
|
||||
env: params.loadOptions.env,
|
||||
loadOptions: params.loadOptions,
|
||||
workspaceDir: params.loadOptions.workspaceDir,
|
||||
requiredPluginIds: params.onlyPluginIds,
|
||||
surface: "channel",
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePluginTools(params: {
|
||||
function resolvePluginToolLoadState(params: {
|
||||
context: OpenClawPluginToolContext;
|
||||
existingToolNames?: Set<string>;
|
||||
toolAllowlist?: string[];
|
||||
suppressNameConflicts?: boolean;
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
hasAuthForProvider?: (providerId: string) => boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): AnyAgentTool[] {
|
||||
// Fast path: when plugins are effectively disabled, avoid discovery/jiti entirely.
|
||||
// This matters a lot for unit tests and for tool construction hot paths.
|
||||
}):
|
||||
| {
|
||||
context: ReturnType<typeof resolvePluginRuntimeLoadContext>;
|
||||
loadOptions: PluginLoadOptions;
|
||||
onlyPluginIds: string[];
|
||||
}
|
||||
| undefined {
|
||||
const env = params.env ?? process.env;
|
||||
const baseConfig = applyTestPluginDefaults(params.context.config ?? {}, env);
|
||||
const context = resolvePluginRuntimeLoadContext({
|
||||
@@ -460,7 +437,7 @@ export function resolvePluginTools(params: {
|
||||
});
|
||||
const normalized = normalizePluginsConfig(context.config.plugins);
|
||||
if (!normalized.enabled) {
|
||||
return [];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const runtimeOptions = params.allowGatewaySubagentBinding
|
||||
@@ -480,6 +457,43 @@ export function resolvePluginTools(params: {
|
||||
...(onlyPluginIds !== undefined ? { onlyPluginIds } : {}),
|
||||
runtimeOptions,
|
||||
});
|
||||
return { context, loadOptions, onlyPluginIds };
|
||||
}
|
||||
|
||||
export function ensureStandalonePluginToolRegistryLoaded(params: {
|
||||
context: OpenClawPluginToolContext;
|
||||
toolAllowlist?: string[];
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
hasAuthForProvider?: (providerId: string) => boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): void {
|
||||
const loadState = resolvePluginToolLoadState(params);
|
||||
if (!loadState) {
|
||||
return;
|
||||
}
|
||||
ensureStandaloneRuntimePluginRegistryLoaded({
|
||||
surface: "channel",
|
||||
requiredPluginIds: loadState.onlyPluginIds,
|
||||
loadOptions: loadState.loadOptions,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePluginTools(params: {
|
||||
context: OpenClawPluginToolContext;
|
||||
existingToolNames?: Set<string>;
|
||||
toolAllowlist?: string[];
|
||||
suppressNameConflicts?: boolean;
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
hasAuthForProvider?: (providerId: string) => boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): AnyAgentTool[] {
|
||||
// Fast path: when plugins are effectively disabled, avoid discovery/jiti entirely.
|
||||
// This matters a lot for unit tests and for tool construction hot paths.
|
||||
const loadState = resolvePluginToolLoadState(params);
|
||||
if (!loadState) {
|
||||
return [];
|
||||
}
|
||||
const { context, loadOptions, onlyPluginIds } = loadState;
|
||||
const registry = resolvePluginToolRegistry({
|
||||
loadOptions,
|
||||
onlyPluginIds,
|
||||
|
||||
@@ -4,6 +4,7 @@ const mocks = vi.hoisted(() => ({
|
||||
isPluginRegistryLoadInFlight: vi.fn(() => false),
|
||||
loadOpenClawPlugins: vi.fn(),
|
||||
resolveCompatibleRuntimePluginRegistry: vi.fn(),
|
||||
getLoadedRuntimePluginRegistry: vi.fn(),
|
||||
resolvePluginRegistryLoadCacheKey: vi.fn((options: unknown) => JSON.stringify(options)),
|
||||
resolveRuntimePluginRegistry: vi.fn(),
|
||||
getActivePluginRegistry: vi.fn<() => Record<string, unknown> | null>(() => null),
|
||||
@@ -29,6 +30,10 @@ vi.mock("./loader.js", () => ({
|
||||
resolveRuntimePluginRegistry: mocks.resolveRuntimePluginRegistry,
|
||||
}));
|
||||
|
||||
vi.mock("./active-runtime-registry.js", () => ({
|
||||
getLoadedRuntimePluginRegistry: mocks.getLoadedRuntimePluginRegistry,
|
||||
}));
|
||||
|
||||
vi.mock("./runtime.js", () => ({
|
||||
getActivePluginRegistry: mocks.getActivePluginRegistry,
|
||||
getActivePluginRegistryWorkspaceDir: mocks.getActivePluginRegistryWorkspaceDir,
|
||||
@@ -53,6 +58,8 @@ describe("web-provider-runtime-shared", () => {
|
||||
mocks.isPluginRegistryLoadInFlight.mockReturnValue(false);
|
||||
mocks.loadOpenClawPlugins.mockReset();
|
||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReset();
|
||||
mocks.getLoadedRuntimePluginRegistry.mockReset();
|
||||
mocks.getLoadedRuntimePluginRegistry.mockReturnValue(undefined);
|
||||
mocks.resolvePluginRegistryLoadCacheKey.mockReset();
|
||||
mocks.resolvePluginRegistryLoadCacheKey.mockImplementation((options: unknown) =>
|
||||
JSON.stringify(options),
|
||||
@@ -72,7 +79,7 @@ describe("web-provider-runtime-shared", () => {
|
||||
|
||||
it("preserves explicit empty scopes in runtime-compatible web provider loads", () => {
|
||||
const mapRegistryProviders = vi.fn(() => []);
|
||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue({} as never);
|
||||
mocks.getLoadedRuntimePluginRegistry.mockReturnValue({} as never);
|
||||
|
||||
resolvePluginWebProviders(
|
||||
{
|
||||
@@ -90,9 +97,9 @@ describe("web-provider-runtime-shared", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mocks.resolveCompatibleRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect(mocks.getLoadedRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: [],
|
||||
requiredPluginIds: [],
|
||||
}),
|
||||
);
|
||||
expect(mapRegistryProviders).toHaveBeenCalledWith(
|
||||
@@ -104,7 +111,7 @@ describe("web-provider-runtime-shared", () => {
|
||||
|
||||
it("preserves explicit empty scopes in direct runtime web provider resolution", () => {
|
||||
const mapRegistryProviders = vi.fn(() => []);
|
||||
mocks.resolveRuntimePluginRegistry.mockReturnValue({} as never);
|
||||
mocks.getLoadedRuntimePluginRegistry.mockReturnValue({} as never);
|
||||
|
||||
resolveRuntimeWebProviders(
|
||||
{
|
||||
@@ -122,9 +129,9 @@ describe("web-provider-runtime-shared", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect(mocks.getLoadedRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: [],
|
||||
requiredPluginIds: [],
|
||||
}),
|
||||
);
|
||||
expect(mapRegistryProviders).toHaveBeenCalledWith(
|
||||
@@ -136,7 +143,7 @@ describe("web-provider-runtime-shared", () => {
|
||||
|
||||
it("preserves explicit scopes when config is omitted in direct runtime resolution", () => {
|
||||
const mapRegistryProviders = vi.fn(() => []);
|
||||
mocks.resolveRuntimePluginRegistry.mockReturnValue({} as never);
|
||||
mocks.getLoadedRuntimePluginRegistry.mockReturnValue({} as never);
|
||||
|
||||
resolveRuntimeWebProviders(
|
||||
{
|
||||
@@ -153,7 +160,11 @@ describe("web-provider-runtime-shared", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith(undefined);
|
||||
expect(mocks.getLoadedRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requiredPluginIds: ["alpha"],
|
||||
}),
|
||||
);
|
||||
expect(mapRegistryProviders).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["alpha"],
|
||||
@@ -166,8 +177,7 @@ describe("web-provider-runtime-shared", () => {
|
||||
const resolvedConfig = { plugins: { entries: { brave: { enabled: true } } } };
|
||||
const resolveCandidatePluginIds = vi.fn(() => ["brave"]);
|
||||
const mapRegistryProviders = vi.fn(() => ["provider"]);
|
||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(null);
|
||||
mocks.getActivePluginRegistry.mockReturnValue(activeRegistry);
|
||||
mocks.getLoadedRuntimePluginRegistry.mockReturnValue(activeRegistry);
|
||||
|
||||
const providers = resolvePluginWebProviders(
|
||||
{
|
||||
@@ -206,8 +216,7 @@ describe("web-provider-runtime-shared", () => {
|
||||
it("preserves explicit empty candidate scopes when reusing the active registry", () => {
|
||||
const activeRegistry = { source: "active" };
|
||||
const mapRegistryProviders = vi.fn(() => []);
|
||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(null);
|
||||
mocks.getActivePluginRegistry.mockReturnValue(activeRegistry);
|
||||
mocks.getLoadedRuntimePluginRegistry.mockReturnValue(activeRegistry);
|
||||
|
||||
resolvePluginWebProviders(
|
||||
{
|
||||
@@ -232,10 +241,10 @@ describe("web-provider-runtime-shared", () => {
|
||||
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("caches runtime web provider plugin loads by default", () => {
|
||||
it("uses loaded runtime web providers without runtime plugin loads", () => {
|
||||
const loadedRegistry = { source: "loaded" };
|
||||
const mapRegistryProviders = vi.fn(() => ["provider"]);
|
||||
mocks.loadOpenClawPlugins.mockReturnValue(loadedRegistry as never);
|
||||
mocks.getLoadedRuntimePluginRegistry.mockReturnValue(loadedRegistry as never);
|
||||
|
||||
const providers = resolvePluginWebProviders(
|
||||
{
|
||||
@@ -254,24 +263,18 @@ describe("web-provider-runtime-shared", () => {
|
||||
);
|
||||
|
||||
expect(providers).toEqual(["provider"]);
|
||||
expect(mocks.resolveCompatibleRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect(mocks.getLoadedRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cache: true,
|
||||
onlyPluginIds: ["brave"],
|
||||
}),
|
||||
);
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cache: true,
|
||||
onlyPluginIds: ["brave"],
|
||||
requiredPluginIds: ["brave"],
|
||||
}),
|
||||
);
|
||||
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps explicit runtime web provider cache opt-outs", () => {
|
||||
it("ignores runtime web provider cache opt-outs after startup loading", () => {
|
||||
const loadedRegistry = { source: "loaded" };
|
||||
const mapRegistryProviders = vi.fn(() => ["provider"]);
|
||||
mocks.loadOpenClawPlugins.mockReturnValue(loadedRegistry as never);
|
||||
mocks.getLoadedRuntimePluginRegistry.mockReturnValue(loadedRegistry as never);
|
||||
|
||||
resolvePluginWebProviders(
|
||||
{
|
||||
@@ -290,12 +293,12 @@ describe("web-provider-runtime-shared", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect(mocks.getLoadedRuntimePluginRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cache: false,
|
||||
onlyPluginIds: ["brave"],
|
||||
requiredPluginIds: ["brave"],
|
||||
}),
|
||||
);
|
||||
expect(mocks.loadOpenClawPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("caches setup web provider plugin loads by default", () => {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { withActivatedPluginIds } from "./activation-context.js";
|
||||
import {
|
||||
isPluginRegistryLoadInFlight,
|
||||
loadOpenClawPlugins,
|
||||
resolveCompatibleRuntimePluginRegistry,
|
||||
resolveRuntimePluginRegistry,
|
||||
} from "./loader.js";
|
||||
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
||||
import { isPluginRegistryLoadInFlight, loadOpenClawPlugins } from "./loader.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||
import { hasExplicitPluginIdScope, normalizePluginIdScope } from "./plugin-scope.js";
|
||||
import type { PluginRegistry } from "./registry.js";
|
||||
import { getActivePluginRegistry, getActivePluginRegistryWorkspaceDir } from "./runtime.js";
|
||||
import { getActivePluginRegistryWorkspaceDir } from "./runtime.js";
|
||||
import {
|
||||
buildPluginRuntimeLoadOptionsFromValues,
|
||||
createPluginRuntimeLoaderLogger,
|
||||
@@ -176,7 +172,12 @@ export function resolvePluginWebProviders<TEntry>(
|
||||
|
||||
const context = resolveWebProviderRuntimeContext(params, deps);
|
||||
const loadOptions = resolveWebProviderLoadOptions(context, params);
|
||||
const compatible = resolveCompatibleRuntimePluginRegistry(loadOptions);
|
||||
const compatible = getLoadedRuntimePluginRegistry({
|
||||
env: context.env,
|
||||
loadOptions,
|
||||
workspaceDir: context.workspaceDir,
|
||||
requiredPluginIds: context.onlyPluginIds,
|
||||
});
|
||||
if (compatible) {
|
||||
return deps.mapRegistryProviders({
|
||||
registry: compatible,
|
||||
@@ -188,33 +189,21 @@ export function resolvePluginWebProviders<TEntry>(
|
||||
}
|
||||
const scopedPluginIds = context.onlyPluginIds;
|
||||
const hasExplicitEmptyScope = scopedPluginIds !== undefined && scopedPluginIds.length === 0;
|
||||
const activeRegistry = getActivePluginRegistry();
|
||||
if (activeRegistry) {
|
||||
const activeProviders = deps.mapRegistryProviders({
|
||||
registry: activeRegistry,
|
||||
onlyPluginIds: context.onlyPluginIds,
|
||||
});
|
||||
if (activeProviders.length > 0 || hasExplicitEmptyScope) {
|
||||
return activeProviders;
|
||||
}
|
||||
}
|
||||
if (hasExplicitEmptyScope) {
|
||||
return [];
|
||||
}
|
||||
return deps.mapRegistryProviders({
|
||||
registry: loadOpenClawPlugins(loadOptions),
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
export function resolveRuntimeWebProviders<TEntry>(
|
||||
params: Omit<ResolvePluginWebProvidersParams, "activate" | "cache" | "mode">,
|
||||
deps: ResolveWebProviderRuntimeDeps<TEntry>,
|
||||
): TEntry[] {
|
||||
const loadOptions =
|
||||
params.config === undefined
|
||||
? undefined
|
||||
: resolveWebProviderLoadOptions(resolveWebProviderRuntimeContext(params, deps), params);
|
||||
const runtimeRegistry = resolveRuntimePluginRegistry(loadOptions);
|
||||
const runtimeRegistry = getLoadedRuntimePluginRegistry({
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
requiredPluginIds: params.onlyPluginIds,
|
||||
});
|
||||
if (runtimeRegistry) {
|
||||
return deps.mapRegistryProviders({
|
||||
registry: runtimeRegistry,
|
||||
|
||||
@@ -93,12 +93,16 @@ export async function detectSetupMigrationSources(params: {
|
||||
config: OpenClawConfig;
|
||||
runtime: RuntimeEnv;
|
||||
}): Promise<SetupMigrationDetection[]> {
|
||||
const [{ resolvePluginMigrationProviders }, { createMigrationLogger }, { resolveStateDir }] =
|
||||
await Promise.all([
|
||||
import("../plugins/migration-provider-runtime.js"),
|
||||
import("../commands/migrate/context.js"),
|
||||
import("../config/paths.js"),
|
||||
]);
|
||||
const [
|
||||
{ ensureStandaloneMigrationProviderRegistryLoaded, resolvePluginMigrationProviders },
|
||||
{ createMigrationLogger },
|
||||
{ resolveStateDir },
|
||||
] = await Promise.all([
|
||||
import("../plugins/migration-provider-runtime.js"),
|
||||
import("../commands/migrate/context.js"),
|
||||
import("../config/paths.js"),
|
||||
]);
|
||||
ensureStandaloneMigrationProviderRegistryLoaded({ cfg: params.config });
|
||||
const stateDir = resolveStateDir();
|
||||
const logger = createMigrationLogger(params.runtime);
|
||||
const detections: SetupMigrationDetection[] = [];
|
||||
@@ -151,8 +155,12 @@ async function selectSetupMigrationProvider(params: {
|
||||
provider: MigrationProviderPlugin;
|
||||
providerId: string;
|
||||
}> {
|
||||
const { resolvePluginMigrationProvider, resolvePluginMigrationProviders } =
|
||||
await import("../plugins/migration-provider-runtime.js");
|
||||
const {
|
||||
ensureStandaloneMigrationProviderRegistryLoaded,
|
||||
resolvePluginMigrationProvider,
|
||||
resolvePluginMigrationProviders,
|
||||
} = await import("../plugins/migration-provider-runtime.js");
|
||||
ensureStandaloneMigrationProviderRegistryLoaded({ cfg: params.baseConfig });
|
||||
const providers = resolvePluginMigrationProviders({ cfg: params.baseConfig });
|
||||
if (providers.length === 0) {
|
||||
throw new Error("No migration providers found.");
|
||||
|
||||
Reference in New Issue
Block a user