mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix: preserve external capability providers
This commit is contained in:
@@ -28,12 +28,13 @@ function createAuthStore(providers: string[] = []): AuthProfileStore {
|
||||
|
||||
function createPlugin(params: {
|
||||
id: string;
|
||||
origin?: PluginManifestRecord["origin"];
|
||||
contracts: NonNullable<PluginManifestRecord["contracts"]>;
|
||||
setupProviders?: Array<{ id: string; envVars?: string[] }>;
|
||||
}): PluginManifestRecord {
|
||||
return {
|
||||
id: params.id,
|
||||
origin: "bundled",
|
||||
origin: params.origin ?? "bundled",
|
||||
rootDir: `/plugins/${params.id}`,
|
||||
source: `/plugins/${params.id}/index.js`,
|
||||
manifestPath: `/plugins/${params.id}/openclaw.plugin.json`,
|
||||
@@ -47,10 +48,22 @@ function createPlugin(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function installSnapshot(config: OpenClawConfig, plugins: PluginManifestRecord[]) {
|
||||
function installSnapshot(
|
||||
config: OpenClawConfig,
|
||||
plugins: PluginManifestRecord[],
|
||||
enabledPluginIds = plugins
|
||||
.filter((plugin) => plugin.origin !== "bundled")
|
||||
.map((plugin) => plugin.id),
|
||||
) {
|
||||
const snapshot = {
|
||||
policyHash: resolveInstalledPluginIndexPolicyHash(config),
|
||||
index: { plugins: [] },
|
||||
index: {
|
||||
plugins: plugins.map((plugin) => ({
|
||||
pluginId: plugin.id,
|
||||
origin: plugin.origin,
|
||||
enabled: plugin.origin === "bundled" || enabledPluginIds.includes(plugin.id),
|
||||
})),
|
||||
},
|
||||
registryDiagnostics: [],
|
||||
manifestRegistry: { plugins, diagnostics: [] },
|
||||
plugins,
|
||||
@@ -217,6 +230,81 @@ describe("optional media tool factory planning", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps enabled external manifest capability providers on the factory path", () => {
|
||||
const config: OpenClawConfig = {};
|
||||
installSnapshot(config, [
|
||||
createPlugin({
|
||||
id: "external-image",
|
||||
origin: "global",
|
||||
contracts: { imageGenerationProviders: ["external-image"] },
|
||||
setupProviders: [{ id: "external-image", envVars: ["EXTERNAL_IMAGE_API_KEY"] }],
|
||||
}),
|
||||
createPlugin({
|
||||
id: "external-video",
|
||||
origin: "global",
|
||||
contracts: { videoGenerationProviders: ["external-video"] },
|
||||
setupProviders: [{ id: "external-video", envVars: ["EXTERNAL_VIDEO_API_KEY"] }],
|
||||
}),
|
||||
createPlugin({
|
||||
id: "external-music",
|
||||
origin: "global",
|
||||
contracts: { musicGenerationProviders: ["external-music"] },
|
||||
setupProviders: [{ id: "external-music", envVars: ["EXTERNAL_MUSIC_API_KEY"] }],
|
||||
}),
|
||||
createPlugin({
|
||||
id: "external-media",
|
||||
origin: "global",
|
||||
contracts: { mediaUnderstandingProviders: ["external-media"] },
|
||||
setupProviders: [{ id: "external-media", envVars: ["EXTERNAL_MEDIA_API_KEY"] }],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(
|
||||
__testing.resolveOptionalMediaToolFactoryPlan({
|
||||
config,
|
||||
authStore: createAuthStore([
|
||||
"external-image",
|
||||
"external-video",
|
||||
"external-music",
|
||||
"external-media",
|
||||
]),
|
||||
}),
|
||||
).toEqual({
|
||||
imageGenerate: true,
|
||||
videoGenerate: true,
|
||||
musicGenerate: true,
|
||||
pdf: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores external manifest capability providers excluded by plugin policy", () => {
|
||||
const config: OpenClawConfig = {
|
||||
plugins: {
|
||||
allow: ["other-plugin"],
|
||||
},
|
||||
};
|
||||
installSnapshot(config, [
|
||||
createPlugin({
|
||||
id: "external-image",
|
||||
origin: "global",
|
||||
contracts: { imageGenerationProviders: ["external-image"] },
|
||||
setupProviders: [{ id: "external-image", envVars: ["EXTERNAL_IMAGE_API_KEY"] }],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(
|
||||
__testing.resolveOptionalMediaToolFactoryPlan({
|
||||
config,
|
||||
authStore: createAuthStore(["external-image"]),
|
||||
}),
|
||||
).toEqual({
|
||||
imageGenerate: false,
|
||||
videoGenerate: false,
|
||||
musicGenerate: false,
|
||||
pdf: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to existing factory checks when snapshot or auth store proof is missing", () => {
|
||||
expect(__testing.resolveOptionalMediaToolFactoryPlan({ config: {} })).toEqual({
|
||||
imageGenerate: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { isEmbeddedMode } from "../infra/embedded-mode.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { isManifestPluginAvailableForControlPlane } from "../plugins/manifest-contract-eligibility.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { getActiveRuntimeWebToolsMetadata } from "../secrets/runtime.js";
|
||||
@@ -109,9 +110,16 @@ function hasAuthSignalForSnapshotCapability(params: {
|
||||
snapshot: PluginMetadataSnapshot;
|
||||
authStore: AuthProfileStore;
|
||||
key: CapabilityContractKey;
|
||||
config?: OpenClawConfig;
|
||||
}): boolean {
|
||||
for (const plugin of params.snapshot.plugins) {
|
||||
if (plugin.origin !== "bundled") {
|
||||
if (
|
||||
!isManifestPluginAvailableForControlPlane({
|
||||
snapshot: params.snapshot,
|
||||
plugin,
|
||||
config: params.config,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const providerId of plugin.contracts?.[params.key] ?? []) {
|
||||
@@ -147,7 +155,13 @@ function hasConfiguredVisionModelAuthSignal(params: {
|
||||
return true;
|
||||
}
|
||||
for (const plugin of params.snapshot.plugins) {
|
||||
if (plugin.origin !== "bundled") {
|
||||
if (
|
||||
!isManifestPluginAvailableForControlPlane({
|
||||
snapshot: params.snapshot,
|
||||
plugin,
|
||||
config: params.config,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (hasNonEmptyEnvCandidate(pluginSetupProviderEnvVars(plugin, providerId))) {
|
||||
@@ -208,6 +222,7 @@ function resolveOptionalMediaToolFactoryPlan(params: {
|
||||
snapshot,
|
||||
authStore: params.authStore,
|
||||
key: "imageGenerationProviders",
|
||||
config: params.config,
|
||||
})),
|
||||
videoGenerate:
|
||||
allowVideoGenerate &&
|
||||
@@ -216,6 +231,7 @@ function resolveOptionalMediaToolFactoryPlan(params: {
|
||||
snapshot,
|
||||
authStore: params.authStore,
|
||||
key: "videoGenerationProviders",
|
||||
config: params.config,
|
||||
})),
|
||||
musicGenerate:
|
||||
allowMusicGenerate &&
|
||||
@@ -224,6 +240,7 @@ function resolveOptionalMediaToolFactoryPlan(params: {
|
||||
snapshot,
|
||||
authStore: params.authStore,
|
||||
key: "musicGenerationProviders",
|
||||
config: params.config,
|
||||
})),
|
||||
pdf:
|
||||
allowPdf &&
|
||||
@@ -232,6 +249,7 @@ function resolveOptionalMediaToolFactoryPlan(params: {
|
||||
snapshot,
|
||||
authStore: params.authStore,
|
||||
key: "mediaUnderstandingProviders",
|
||||
config: params.config,
|
||||
}) ||
|
||||
hasConfiguredVisionModelAuthSignal({
|
||||
config: params.config,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { SsrFPolicy } from "../../infra/net/ssrf.js";
|
||||
import { getDefaultLocalRoots } from "../../media/web-media.js";
|
||||
import { readSnakeCaseParamRaw } from "../../param-key.js";
|
||||
import { resolveBundledCapabilityProviderIds } from "../../plugins/capability-provider-runtime.js";
|
||||
import { resolveManifestCapabilityProviderIds } from "../../plugins/capability-provider-runtime.js";
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
@@ -317,7 +317,7 @@ export function hasGenerationToolAvailability(params: {
|
||||
}),
|
||||
);
|
||||
}
|
||||
return resolveBundledCapabilityProviderIds({
|
||||
return resolveManifestCapabilityProviderIds({
|
||||
key: params.providerKey,
|
||||
cfg: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
|
||||
@@ -85,6 +85,7 @@ vi.mock("./bundled-compat.js", () => ({
|
||||
let resolvePluginCapabilityProviders: typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProviders;
|
||||
let resolvePluginCapabilityProvider: typeof import("./capability-provider-runtime.js").resolvePluginCapabilityProvider;
|
||||
let resolveBundledCapabilityProviderIds: typeof import("./capability-provider-runtime.js").resolveBundledCapabilityProviderIds;
|
||||
let resolveManifestCapabilityProviderIds: typeof import("./capability-provider-runtime.js").resolveManifestCapabilityProviderIds;
|
||||
let clearCurrentPluginMetadataSnapshot: typeof import("./current-plugin-metadata-snapshot.js").clearCurrentPluginMetadataSnapshot;
|
||||
let setCurrentPluginMetadataSnapshot: typeof import("./current-plugin-metadata-snapshot.js").setCurrentPluginMetadataSnapshot;
|
||||
|
||||
@@ -106,10 +107,14 @@ function expectBundledCompatLoadPath(params: {
|
||||
};
|
||||
};
|
||||
}) {
|
||||
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({
|
||||
config: params.cfg,
|
||||
env: process.env,
|
||||
});
|
||||
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: params.cfg,
|
||||
env: process.env,
|
||||
includeDisabled: true,
|
||||
index: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
expect(mocks.withBundledPluginEnablementCompat).toHaveBeenCalledWith({
|
||||
config: params.allowlistCompat,
|
||||
pluginIds: ["openai"],
|
||||
@@ -197,6 +202,7 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
beforeAll(async () => {
|
||||
({
|
||||
resolveBundledCapabilityProviderIds,
|
||||
resolveManifestCapabilityProviderIds,
|
||||
resolvePluginCapabilityProvider,
|
||||
resolvePluginCapabilityProviders,
|
||||
} = await import("./capability-provider-runtime.js"));
|
||||
@@ -281,6 +287,62 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves enabled external capability ids from the current metadata snapshot", () => {
|
||||
setCurrentPluginMetadataSnapshot({
|
||||
policyHash: "policy",
|
||||
workspaceDir: "/workspace",
|
||||
index: {
|
||||
plugins: [
|
||||
{ pluginId: "external-image", origin: "global", enabled: true },
|
||||
{ pluginId: "external-disabled", origin: "global", enabled: false },
|
||||
],
|
||||
},
|
||||
registryDiagnostics: [],
|
||||
manifestRegistry: { plugins: [], diagnostics: [] },
|
||||
plugins: [
|
||||
{
|
||||
id: "external-image",
|
||||
origin: "global",
|
||||
contracts: { imageGenerationProviders: ["external-image"] },
|
||||
},
|
||||
{
|
||||
id: "external-disabled",
|
||||
origin: "global",
|
||||
contracts: { imageGenerationProviders: ["external-disabled"] },
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
byPluginId: new Map(),
|
||||
normalizePluginId: (id: string) => id,
|
||||
owners: {
|
||||
channels: new Map(),
|
||||
channelConfigs: new Map(),
|
||||
providers: new Map(),
|
||||
modelCatalogProviders: new Map(),
|
||||
cliBackends: new Map(),
|
||||
setupProviders: new Map(),
|
||||
commandAliases: new Map(),
|
||||
contracts: new Map(),
|
||||
},
|
||||
metrics: {
|
||||
registrySnapshotMs: 0,
|
||||
manifestRegistryMs: 0,
|
||||
ownerMapsMs: 0,
|
||||
totalMs: 0,
|
||||
indexPluginCount: 2,
|
||||
manifestPluginCount: 2,
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(
|
||||
resolveManifestCapabilityProviderIds({
|
||||
key: "imageGenerationProviders",
|
||||
workspaceDir: "/workspace",
|
||||
}),
|
||||
).toEqual(["external-image"]);
|
||||
expect(mocks.loadPluginManifestRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the active registry when capability providers are already loaded", () => {
|
||||
const active = createEmptyPluginRegistry();
|
||||
active.speechProviders.push({
|
||||
@@ -308,6 +370,51 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("targets enabled external capability plugins without bundled fallback capture", () => {
|
||||
const loaded = createEmptyPluginRegistry();
|
||||
loaded.imageGenerationProviders.push({
|
||||
pluginId: "external-image",
|
||||
pluginName: "external-image",
|
||||
source: "test",
|
||||
provider: {
|
||||
id: "external-image",
|
||||
label: "External Image",
|
||||
isConfigured: () => true,
|
||||
generate: async () => ({
|
||||
kind: "image",
|
||||
images: [],
|
||||
}),
|
||||
},
|
||||
} as never);
|
||||
mocks.loadPluginRegistrySnapshot.mockReturnValue({
|
||||
plugins: [{ pluginId: "external-image", origin: "global", enabled: true }],
|
||||
});
|
||||
mocks.loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "external-image",
|
||||
origin: "global",
|
||||
contracts: { imageGenerationProviders: ["external-image"] },
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
mocks.resolveRuntimePluginRegistry.mockImplementation((options?: unknown) =>
|
||||
options ? loaded : undefined,
|
||||
);
|
||||
|
||||
expectResolvedCapabilityProviderIds(
|
||||
resolvePluginCapabilityProviders({ key: "imageGenerationProviders" }),
|
||||
["external-image"],
|
||||
);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenLastCalledWith({
|
||||
config: expect.any(Object),
|
||||
onlyPluginIds: ["external-image"],
|
||||
activate: false,
|
||||
});
|
||||
expect(mocks.loadBundledCapabilityRuntimeRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses active non-speech capability providers even when cfg has explicit plugin entries", () => {
|
||||
const active = createEmptyPluginRegistry();
|
||||
active.mediaUnderstandingProviders.push({
|
||||
@@ -964,10 +1071,14 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
const providers = resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders" });
|
||||
|
||||
expectResolvedCapabilityProviderIds(providers, ["google"]);
|
||||
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({
|
||||
config: undefined,
|
||||
env: process.env,
|
||||
});
|
||||
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: undefined,
|
||||
env: process.env,
|
||||
includeDisabled: true,
|
||||
index: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: compatConfig,
|
||||
onlyPluginIds: ["google"],
|
||||
@@ -1088,10 +1199,14 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
});
|
||||
|
||||
expectResolvedCapabilityProviderIds(providers, ["microsoft"]);
|
||||
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
});
|
||||
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
includeDisabled: true,
|
||||
index: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledWith({
|
||||
config: cfg,
|
||||
pluginIds: ["microsoft"],
|
||||
@@ -1119,10 +1234,14 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
});
|
||||
|
||||
expectNoResolvedCapabilityProviders(providers as Array<{ id: string }>);
|
||||
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith({
|
||||
config: {},
|
||||
env: process.env,
|
||||
});
|
||||
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: {},
|
||||
env: process.env,
|
||||
includeDisabled: true,
|
||||
index: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith();
|
||||
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
|
||||
config: expect.anything(),
|
||||
|
||||
@@ -5,17 +5,24 @@ import {
|
||||
withBundledPluginEnablementCompat,
|
||||
withBundledPluginVitestCompat,
|
||||
} from "./bundled-compat.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
|
||||
import {
|
||||
resolvePluginRegistryLoadCacheKey,
|
||||
resolveRuntimePluginRegistry,
|
||||
type PluginLoadOptions,
|
||||
} from "./loader.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
|
||||
import {
|
||||
resolveConfigScopedRuntimeCacheValue,
|
||||
type ConfigScopedRuntimeCache,
|
||||
} from "./plugin-cache-primitives.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
import {
|
||||
hasManifestContractValue,
|
||||
isManifestPluginAvailableForControlPlane,
|
||||
listAvailableManifestContractValues,
|
||||
} from "./manifest-contract-eligibility.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
|
||||
import { loadPluginRegistrySnapshot } from "./plugin-registry.js";
|
||||
import type { PluginRegistry } from "./registry-types.js";
|
||||
|
||||
type CapabilityProviderRegistryKey =
|
||||
@@ -41,6 +48,10 @@ type CapabilityContractKey =
|
||||
type CapabilityProviderForKey<K extends CapabilityProviderRegistryKey> =
|
||||
PluginRegistry[K][number] extends { provider: infer T } ? T : never;
|
||||
type CapabilityProviderEntries = PluginRegistry[CapabilityProviderRegistryKey];
|
||||
type CapabilityPluginResolution = {
|
||||
runtimePluginIds: string[];
|
||||
bundledCompatPluginIds: string[];
|
||||
};
|
||||
|
||||
const capabilityProviderSnapshotCache: ConfigScopedRuntimeCache<CapabilityProviderEntries> =
|
||||
new WeakMap();
|
||||
@@ -70,35 +81,92 @@ function shouldSkipCapabilityResolution(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function uniqueSorted(values: Iterable<string>): string[] {
|
||||
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function loadCapabilityManifestSnapshot(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
}): Pick<PluginMetadataSnapshot, "index" | "plugins"> {
|
||||
const current = getCurrentPluginMetadataSnapshot({
|
||||
config: params.cfg,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
});
|
||||
if (current) {
|
||||
return current;
|
||||
}
|
||||
const env = process.env;
|
||||
const index = loadPluginRegistrySnapshot({
|
||||
config: params.cfg,
|
||||
env,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
});
|
||||
return {
|
||||
index,
|
||||
plugins: loadPluginManifestRegistryForInstalledIndex({
|
||||
index,
|
||||
config: params.cfg,
|
||||
env,
|
||||
includeDisabled: true,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
}).plugins,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCapabilityPluginIds(params: {
|
||||
key: CapabilityProviderRegistryKey;
|
||||
cfg?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
providerId?: string;
|
||||
}): CapabilityPluginResolution {
|
||||
const contractKey = CAPABILITY_CONTRACT_KEY[params.key];
|
||||
const snapshot = loadCapabilityManifestSnapshot(params);
|
||||
const contractPlugins = snapshot.plugins.filter((plugin) =>
|
||||
hasManifestContractValue({
|
||||
plugin,
|
||||
contract: contractKey,
|
||||
value: params.providerId,
|
||||
}),
|
||||
);
|
||||
return {
|
||||
runtimePluginIds: uniqueSorted(
|
||||
contractPlugins
|
||||
.filter((plugin) =>
|
||||
isManifestPluginAvailableForControlPlane({
|
||||
snapshot,
|
||||
plugin,
|
||||
config: params.cfg,
|
||||
}),
|
||||
)
|
||||
.map((plugin) => plugin.id),
|
||||
),
|
||||
bundledCompatPluginIds: uniqueSorted(
|
||||
contractPlugins.filter((plugin) => plugin.origin === "bundled").map((plugin) => plugin.id),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBundledCapabilityCompatPluginIds(params: {
|
||||
key: CapabilityProviderRegistryKey;
|
||||
cfg?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
providerId?: string;
|
||||
}): string[] {
|
||||
const env = process.env;
|
||||
return resolveCapabilityPluginIds(params).bundledCompatPluginIds;
|
||||
}
|
||||
|
||||
export function resolveManifestCapabilityProviderIds(params: {
|
||||
key: CapabilityProviderRegistryKey;
|
||||
cfg?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
}): string[] {
|
||||
const contractKey = CAPABILITY_CONTRACT_KEY[params.key];
|
||||
const snapshot = getCurrentPluginMetadataSnapshot({
|
||||
return listAvailableManifestContractValues({
|
||||
snapshot: loadCapabilityManifestSnapshot(params),
|
||||
contract: contractKey,
|
||||
config: params.cfg,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
});
|
||||
const plugins =
|
||||
snapshot?.plugins ??
|
||||
loadPluginManifestRegistryForPluginRegistry({
|
||||
config: params.cfg,
|
||||
env,
|
||||
includeDisabled: true,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
}).plugins;
|
||||
return plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" &&
|
||||
(plugin.contracts?.[contractKey]?.length ?? 0) > 0 &&
|
||||
(!params.providerId || (plugin.contracts?.[contractKey] ?? []).includes(params.providerId)),
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function resolveBundledCapabilityProviderIds(params: {
|
||||
@@ -106,27 +174,13 @@ export function resolveBundledCapabilityProviderIds(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
}): string[] {
|
||||
const env = process.env;
|
||||
const contractKey = CAPABILITY_CONTRACT_KEY[params.key];
|
||||
const snapshot = getCurrentPluginMetadataSnapshot({
|
||||
config: params.cfg,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
});
|
||||
const plugins =
|
||||
snapshot?.plugins ??
|
||||
loadPluginManifestRegistryForPluginRegistry({
|
||||
config: params.cfg,
|
||||
env,
|
||||
includeDisabled: true,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
}).plugins;
|
||||
return [
|
||||
...new Set(
|
||||
plugins.flatMap((plugin) =>
|
||||
plugin.origin === "bundled" ? (plugin.contracts?.[contractKey] ?? []) : [],
|
||||
),
|
||||
const snapshot = loadCapabilityManifestSnapshot(params);
|
||||
return uniqueSorted(
|
||||
snapshot.plugins.flatMap((plugin) =>
|
||||
plugin.origin === "bundled" ? (plugin.contracts?.[contractKey] ?? []) : [],
|
||||
),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
);
|
||||
}
|
||||
|
||||
function resolveCapabilityProviderConfig(params: {
|
||||
@@ -347,32 +401,40 @@ function filterLoadedProvidersForRequestedConfig<K extends CapabilityProviderReg
|
||||
}) as PluginRegistry[K];
|
||||
}
|
||||
|
||||
function resolveRequestedCapabilityCompatPluginIds(params: {
|
||||
function resolveRequestedCapabilityPluginIds(params: {
|
||||
key: CapabilityProviderRegistryKey;
|
||||
cfg?: OpenClawConfig;
|
||||
requested?: Set<string>;
|
||||
}): string[] | undefined {
|
||||
}): CapabilityPluginResolution | undefined {
|
||||
if (params.key !== "speechProviders" || !params.requested || params.requested.size === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const pluginIds = new Set<string>();
|
||||
const runtimePluginIds = new Set<string>();
|
||||
const bundledCompatPluginIds = new Set<string>();
|
||||
for (const providerId of params.requested) {
|
||||
for (const pluginId of resolveBundledCapabilityCompatPluginIds({
|
||||
const resolution = resolveCapabilityPluginIds({
|
||||
key: params.key,
|
||||
cfg: params.cfg,
|
||||
providerId,
|
||||
})) {
|
||||
pluginIds.add(pluginId);
|
||||
});
|
||||
for (const pluginId of resolution.runtimePluginIds) {
|
||||
runtimePluginIds.add(pluginId);
|
||||
}
|
||||
for (const pluginId of resolution.bundledCompatPluginIds) {
|
||||
bundledCompatPluginIds.add(pluginId);
|
||||
}
|
||||
}
|
||||
return pluginIds.size > 0
|
||||
? [...pluginIds].toSorted((left, right) => left.localeCompare(right))
|
||||
return runtimePluginIds.size > 0
|
||||
? {
|
||||
runtimePluginIds: uniqueSorted(runtimePluginIds),
|
||||
bundledCompatPluginIds: uniqueSorted(bundledCompatPluginIds),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function loadCapabilityProviderEntries<K extends CapabilityProviderRegistryKey>(params: {
|
||||
key: K;
|
||||
pluginIds: string[];
|
||||
bundledCompatPluginIds: string[];
|
||||
loadOptions: PluginLoadOptions;
|
||||
requested?: Set<string>;
|
||||
}): PluginRegistry[K] {
|
||||
@@ -388,11 +450,11 @@ function loadCapabilityProviderEntries<K extends CapabilityProviderRegistryKey>(
|
||||
if (entries.length > 0 && (!missingRequested || missingRequested.size === 0)) {
|
||||
return entries;
|
||||
}
|
||||
if (params.pluginIds.length === 0) {
|
||||
if (params.bundledCompatPluginIds.length === 0) {
|
||||
return entries;
|
||||
}
|
||||
const captured = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: params.pluginIds,
|
||||
pluginIds: params.bundledCompatPluginIds,
|
||||
env: process.env,
|
||||
pluginSdkResolution: params.loadOptions.pluginSdkResolution,
|
||||
})[params.key] as PluginRegistry[K];
|
||||
@@ -414,23 +476,23 @@ export function resolvePluginCapabilityProvider<K extends CapabilityProviderRegi
|
||||
return activeProvider;
|
||||
}
|
||||
|
||||
const pluginIds = resolveBundledCapabilityCompatPluginIds({
|
||||
const pluginIds = resolveCapabilityPluginIds({
|
||||
key: params.key,
|
||||
cfg: params.cfg,
|
||||
providerId: params.providerId,
|
||||
});
|
||||
if (pluginIds.length === 0) {
|
||||
if (pluginIds.runtimePluginIds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const compatConfig = resolveCapabilityProviderConfig({
|
||||
key: params.key,
|
||||
cfg: params.cfg,
|
||||
pluginIds,
|
||||
pluginIds: pluginIds.bundledCompatPluginIds,
|
||||
});
|
||||
const loadOptions = createCapabilityProviderFallbackLoadOptions({
|
||||
compatConfig,
|
||||
pluginIds,
|
||||
pluginIds: pluginIds.runtimePluginIds,
|
||||
});
|
||||
const loadedProviders = resolveConfigScopedRuntimeCacheValue({
|
||||
cache: capabilityProviderSnapshotCache,
|
||||
@@ -439,7 +501,7 @@ export function resolvePluginCapabilityProvider<K extends CapabilityProviderRegi
|
||||
load: () =>
|
||||
loadCapabilityProviderEntries({
|
||||
key: params.key,
|
||||
pluginIds,
|
||||
bundledCompatPluginIds: pluginIds.bundledCompatPluginIds,
|
||||
loadOptions,
|
||||
requested: new Set([params.providerId.toLowerCase()]),
|
||||
}) as CapabilityProviderEntries,
|
||||
@@ -450,7 +512,7 @@ export function resolvePluginCapabilityProvider<K extends CapabilityProviderRegi
|
||||
function resolveCachedCapabilityProviderEntries<K extends CapabilityProviderRegistryKey>(params: {
|
||||
key: K;
|
||||
cfg?: OpenClawConfig;
|
||||
pluginIds: string[];
|
||||
bundledCompatPluginIds: string[];
|
||||
loadOptions: PluginLoadOptions;
|
||||
requested?: Set<string>;
|
||||
}): PluginRegistry[K] {
|
||||
@@ -464,7 +526,7 @@ function resolveCachedCapabilityProviderEntries<K extends CapabilityProviderRegi
|
||||
load: () =>
|
||||
loadCapabilityProviderEntries({
|
||||
key: params.key,
|
||||
pluginIds: params.pluginIds,
|
||||
bundledCompatPluginIds: params.bundledCompatPluginIds,
|
||||
loadOptions: params.loadOptions,
|
||||
requested: params.requested,
|
||||
}) as CapabilityProviderEntries,
|
||||
@@ -501,28 +563,28 @@ export function resolvePluginCapabilityProviders<K extends CapabilityProviderReg
|
||||
(activeProviders.length === 0 ? collectRequestedSpeechProviderIds(params.cfg) : undefined);
|
||||
}
|
||||
const pluginIds =
|
||||
resolveRequestedCapabilityCompatPluginIds({
|
||||
resolveRequestedCapabilityPluginIds({
|
||||
key: params.key,
|
||||
cfg: params.cfg,
|
||||
requested: requestedSpeechProviders,
|
||||
}) ??
|
||||
resolveBundledCapabilityCompatPluginIds({
|
||||
resolveCapabilityPluginIds({
|
||||
key: params.key,
|
||||
cfg: params.cfg,
|
||||
});
|
||||
const compatConfig = resolveCapabilityProviderConfig({
|
||||
key: params.key,
|
||||
cfg: params.cfg,
|
||||
pluginIds,
|
||||
pluginIds: pluginIds.bundledCompatPluginIds,
|
||||
});
|
||||
const loadOptions = createCapabilityProviderFallbackLoadOptions({
|
||||
compatConfig,
|
||||
pluginIds,
|
||||
pluginIds: pluginIds.runtimePluginIds,
|
||||
});
|
||||
const loadedProviders = resolveCachedCapabilityProviderEntries({
|
||||
key: params.key,
|
||||
cfg: params.cfg,
|
||||
pluginIds,
|
||||
bundledCompatPluginIds: pluginIds.bundledCompatPluginIds,
|
||||
loadOptions,
|
||||
requested: requestedSpeechProviders,
|
||||
});
|
||||
|
||||
59
src/plugins/manifest-contract-eligibility.ts
Normal file
59
src/plugins/manifest-contract-eligibility.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { isInstalledPluginEnabled } from "./installed-plugin-index.js";
|
||||
import type { PluginManifestContractListKey, PluginManifestRecord } from "./manifest-registry.js";
|
||||
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js";
|
||||
|
||||
export function isManifestPluginAvailableForControlPlane(params: {
|
||||
snapshot: Pick<PluginMetadataSnapshot, "index">;
|
||||
plugin: Pick<PluginManifestRecord, "id" | "origin" | "enabledByDefault">;
|
||||
config?: OpenClawConfig;
|
||||
}): boolean {
|
||||
if (params.plugin.origin === "bundled") {
|
||||
return true;
|
||||
}
|
||||
return isInstalledPluginEnabled(params.snapshot.index, params.plugin.id, params.config);
|
||||
}
|
||||
|
||||
export function hasManifestContractValue(params: {
|
||||
plugin: Pick<PluginManifestRecord, "contracts">;
|
||||
contract: PluginManifestContractListKey;
|
||||
value?: string;
|
||||
}): boolean {
|
||||
const values = params.plugin.contracts?.[params.contract] ?? [];
|
||||
return values.length > 0 && (!params.value || values.includes(params.value));
|
||||
}
|
||||
|
||||
export function listAvailableManifestContractPlugins(params: {
|
||||
snapshot: Pick<PluginMetadataSnapshot, "index" | "plugins">;
|
||||
contract: PluginManifestContractListKey;
|
||||
value?: string;
|
||||
config?: OpenClawConfig;
|
||||
}): PluginManifestRecord[] {
|
||||
return params.snapshot.plugins.filter(
|
||||
(plugin) =>
|
||||
hasManifestContractValue({
|
||||
plugin,
|
||||
contract: params.contract,
|
||||
value: params.value,
|
||||
}) &&
|
||||
isManifestPluginAvailableForControlPlane({
|
||||
snapshot: params.snapshot,
|
||||
plugin,
|
||||
config: params.config,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function listAvailableManifestContractValues(params: {
|
||||
snapshot: Pick<PluginMetadataSnapshot, "index" | "plugins">;
|
||||
contract: PluginManifestContractListKey;
|
||||
config?: OpenClawConfig;
|
||||
}): string[] {
|
||||
const values = new Set<string>();
|
||||
for (const plugin of listAvailableManifestContractPlugins(params)) {
|
||||
for (const value of plugin.contracts?.[params.contract] ?? []) {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
return [...values].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
hasManifestContractValue,
|
||||
listAvailableManifestContractPlugins,
|
||||
} from "./manifest-contract-eligibility.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import type { PluginManifestContractListKey, PluginManifestRecord } from "./manifest-registry.js";
|
||||
import type { PluginManifestContractListKey } from "./manifest-registry.js";
|
||||
import { loadPluginRegistrySnapshot } from "./plugin-registry.js";
|
||||
|
||||
export type ManifestContractRuntimePluginResolution = {
|
||||
@@ -12,15 +16,6 @@ const DEMAND_ONLY_CONTRACT_LOOKUP_OPTIONS = {
|
||||
preferPersisted: false,
|
||||
} as const;
|
||||
|
||||
function hasManifestContractValue(
|
||||
plugin: PluginManifestRecord,
|
||||
contract: PluginManifestContractListKey,
|
||||
value?: string,
|
||||
): boolean {
|
||||
const values = plugin.contracts?.[contract] ?? [];
|
||||
return values.length > 0 && (!value || values.includes(value));
|
||||
}
|
||||
|
||||
export function resolveManifestContractRuntimePluginResolution(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
contract: PluginManifestContractListKey;
|
||||
@@ -36,16 +31,22 @@ export function resolveManifestContractRuntimePluginResolution(params: {
|
||||
config: params.cfg,
|
||||
env: process.env,
|
||||
includeDisabled: true,
|
||||
}).plugins.filter((plugin) => hasManifestContractValue(plugin, params.contract, params.value));
|
||||
}).plugins.filter((plugin) =>
|
||||
hasManifestContractValue({
|
||||
plugin,
|
||||
contract: params.contract,
|
||||
value: params.value,
|
||||
}),
|
||||
);
|
||||
const bundledCompatPluginIds = allContractPlugins
|
||||
.filter((plugin) => plugin.origin === "bundled")
|
||||
.map((plugin) => plugin.id);
|
||||
const enabledPluginIds = new Set(
|
||||
index.plugins.filter((plugin) => plugin.enabled).map((plugin) => plugin.pluginId),
|
||||
);
|
||||
const pluginIds = allContractPlugins
|
||||
.filter((plugin) => plugin.origin === "bundled" || enabledPluginIds.has(plugin.id))
|
||||
.map((plugin) => plugin.id);
|
||||
const pluginIds = listAvailableManifestContractPlugins({
|
||||
snapshot: { index, plugins: allContractPlugins },
|
||||
contract: params.contract,
|
||||
value: params.value,
|
||||
config: params.cfg,
|
||||
}).map((plugin) => plugin.id);
|
||||
return {
|
||||
pluginIds: [...new Set(pluginIds)].toSorted((left, right) => left.localeCompare(right)),
|
||||
bundledCompatPluginIds: [...new Set(bundledCompatPluginIds)].toSorted((left, right) =>
|
||||
|
||||
Reference in New Issue
Block a user