mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
refactor: scope plugin capabilities to manifests
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/startup: move gateway startup plugin planning onto the versioned cold registry index, with postinstall repair for older registry files that predate startup metadata. Thanks @vincentkoc.
|
||||
- Plugins/startup: normalize startup and provider plugin enablement through registry aliases so boot paths do not need the legacy manifest alias scan. Thanks @vincentkoc.
|
||||
- Providers/plugins: resolve provider ownership, provider discovery scopes, and catalog-hook provider ids from the cold plugin registry instead of rescanning manifests on those paths. Thanks @vincentkoc.
|
||||
- Plugins/registry: keep installed plugin index records focused on install/state/load paths and resolve plugin capabilities from manifests scoped to indexed plugins. Thanks @shakkernerd.
|
||||
- Plugins/chat commands: refresh the persisted plugin registry after `/plugins enable` and `/plugins disable`, matching the CLI mutation path. Thanks @vincentkoc.
|
||||
- Plugins/compat: mark `OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY` as a deprecated break-glass switch and point operators at registry repair instead. Thanks @vincentkoc.
|
||||
- Plugins/registry: ignore stale persisted registry reads when plugin policy no longer matches current config, and stamp generated registry files with a do-not-edit warning. Thanks @vincentkoc.
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { ExternalizedBundledPluginBridge } from "../plugins/externalized-bundled-plugins.js";
|
||||
import { readPersistedInstalledPluginIndex } from "../plugins/installed-plugin-index-store.js";
|
||||
import type { InstalledPluginIndexRecord } from "../plugins/installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "../plugins/manifest-registry-installed.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
|
||||
function buildBridgeFromPersistedBundledRecord(
|
||||
record: InstalledPluginIndexRecord,
|
||||
manifest?: PluginManifestRecord,
|
||||
): ExternalizedBundledPluginBridge | null {
|
||||
// Relocation is derived from the previous persisted registry, not a hardcoded
|
||||
// table. A plugin moving from bundled to npm keeps the same plugin id; the old
|
||||
@@ -20,7 +23,7 @@ function buildBridgeFromPersistedBundledRecord(
|
||||
pluginId: record.pluginId,
|
||||
npmSpec,
|
||||
...(record.enabledByDefault ? { enabledByDefault: true } : {}),
|
||||
channelIds: record.contributions.channels,
|
||||
...(manifest?.channels.length ? { channelIds: manifest.channels } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,8 +38,18 @@ export async function listPersistedBundledPluginLocationBridges(options: {
|
||||
if (!index) {
|
||||
return [];
|
||||
}
|
||||
const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({
|
||||
index,
|
||||
workspaceDir: options.workspaceDir,
|
||||
env: options.env,
|
||||
includeDisabled: true,
|
||||
});
|
||||
const manifestByPluginId = new Map(manifestRegistry.plugins.map((plugin) => [plugin.id, plugin]));
|
||||
return index.plugins.flatMap((record) => {
|
||||
const bridge = buildBridgeFromPersistedBundledRecord(record);
|
||||
const bridge = buildBridgeFromPersistedBundledRecord(
|
||||
record,
|
||||
manifestByPluginId.get(record.pluginId),
|
||||
);
|
||||
return bridge ? [bridge] : [];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("listManifestInstalledChannelIds", () => {
|
||||
},
|
||||
});
|
||||
loadPluginRegistrySnapshot.mockReturnValue({
|
||||
plugins: [{ pluginId: "slack", contributions: { channels: ["slack"] } }],
|
||||
plugins: [{ pluginId: "slack" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
listPluginContributionIds.mockReturnValue(["slack"]);
|
||||
@@ -89,7 +89,7 @@ describe("listManifestInstalledChannelIds", () => {
|
||||
});
|
||||
expect(listPluginContributionIds).toHaveBeenCalledWith({
|
||||
index: {
|
||||
plugins: [{ pluginId: "slack", contributions: { channels: ["slack"] } }],
|
||||
plugins: [{ pluginId: "slack" }],
|
||||
diagnostics: [],
|
||||
},
|
||||
contribution: "channels",
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
type InstalledPluginIndexRecord,
|
||||
type LoadInstalledPluginIndexParams,
|
||||
} from "../../../plugins/installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "../../../plugins/manifest-registry-installed.js";
|
||||
import type { PluginManifestRecord } from "../../../plugins/manifest-registry.js";
|
||||
|
||||
export const DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION";
|
||||
export const FORCE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_FORCE_PLUGIN_REGISTRY_MIGRATION";
|
||||
@@ -124,6 +126,7 @@ function normalizeRegistryReference(value: unknown): string | undefined {
|
||||
|
||||
function createMigrationPluginIdNormalizer(
|
||||
index: InstalledPluginIndex,
|
||||
manifests: readonly PluginManifestRecord[],
|
||||
): (pluginId: string) => string {
|
||||
const aliases = new Map<string, string>();
|
||||
for (const plugin of index.plugins) {
|
||||
@@ -132,16 +135,25 @@ function createMigrationPluginIdNormalizer(
|
||||
continue;
|
||||
}
|
||||
aliases.set(pluginId, plugin.pluginId);
|
||||
}
|
||||
for (const plugin of manifests) {
|
||||
const pluginId = normalizeRegistryReference(plugin.id);
|
||||
if (!pluginId) {
|
||||
continue;
|
||||
}
|
||||
aliases.set(pluginId, plugin.id);
|
||||
for (const alias of [
|
||||
...plugin.contributions.providers,
|
||||
...plugin.contributions.channels,
|
||||
...plugin.contributions.setupProviders,
|
||||
...plugin.contributions.cliBackends,
|
||||
...plugin.contributions.modelCatalogProviders,
|
||||
...plugin.providers,
|
||||
...plugin.channels,
|
||||
...(plugin.setup?.providers?.map((provider) => provider.id) ?? []),
|
||||
...plugin.cliBackends,
|
||||
...(plugin.setup?.cliBackends ?? []),
|
||||
...Object.keys(plugin.modelCatalog?.providers ?? {}),
|
||||
...(plugin.legacyPluginIds ?? []),
|
||||
]) {
|
||||
const normalizedAlias = normalizeRegistryReference(alias);
|
||||
if (normalizedAlias && !aliases.has(normalizedAlias)) {
|
||||
aliases.set(normalizedAlias, plugin.pluginId);
|
||||
aliases.set(normalizedAlias, plugin.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,8 +205,21 @@ export function listMigrationRelevantPluginRecords(params: {
|
||||
index: InstalledPluginIndex;
|
||||
config: OpenClawConfig;
|
||||
installRecords: Record<string, unknown>;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): readonly InstalledPluginIndexRecord[] {
|
||||
const normalizePluginId = createMigrationPluginIdNormalizer(params.index);
|
||||
const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({
|
||||
index: params.index,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
includeDisabled: true,
|
||||
});
|
||||
const manifestByPluginId = new Map(manifestRegistry.plugins.map((plugin) => [plugin.id, plugin]));
|
||||
const normalizePluginId = createMigrationPluginIdNormalizer(
|
||||
params.index,
|
||||
manifestRegistry.plugins,
|
||||
);
|
||||
const referencedPluginIds = new Set<string>();
|
||||
const installedPluginIds = new Set<string>();
|
||||
|
||||
@@ -226,20 +251,21 @@ export function listMigrationRelevantPluginRecords(params: {
|
||||
if (plugin.origin !== "bundled") {
|
||||
return true;
|
||||
}
|
||||
if (plugin.enabledByDefault && plugin.contributions.providers.length > 0) {
|
||||
const manifest = manifestByPluginId.get(plugin.pluginId);
|
||||
if (plugin.enabledByDefault && (manifest?.providers.length ?? 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
if (installedPluginIds.has(plugin.pluginId) || referencedPluginIds.has(plugin.pluginId)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
plugin.contributions.channels.some((channelId) =>
|
||||
(manifest?.channels ?? []).some((channelId) =>
|
||||
configuredChannelIds.has(normalizeRegistryReference(channelId) ?? ""),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return plugin.contributions.providers.some((providerId) =>
|
||||
return (manifest?.providers ?? []).some((providerId) =>
|
||||
configuredModelProviderIds.has(normalizeProviderId(providerId)),
|
||||
);
|
||||
});
|
||||
@@ -282,6 +308,8 @@ export async function migratePluginRegistryForInstall(
|
||||
index: candidateIndex,
|
||||
config,
|
||||
installRecords,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}),
|
||||
};
|
||||
await writePersistedInstalledPluginIndex(current, params);
|
||||
|
||||
@@ -50,6 +50,17 @@ import {
|
||||
resolveGatewayStartupPluginIds,
|
||||
} from "./channel-plugin-ids.js";
|
||||
|
||||
function withManifestLoadPaths<T extends { id: string }>(plugin: T): T {
|
||||
return {
|
||||
rootDir: `/tmp/plugins/${plugin.id}`,
|
||||
source: `/tmp/plugins/${plugin.id}/index.ts`,
|
||||
manifestPath: `/tmp/plugins/${plugin.id}/openclaw.plugin.json`,
|
||||
skills: [],
|
||||
hooks: [],
|
||||
...plugin,
|
||||
};
|
||||
}
|
||||
|
||||
function createManifestRegistryFixture() {
|
||||
return {
|
||||
plugins: [
|
||||
@@ -185,7 +196,7 @@ function createManifestRegistryFixture() {
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
},
|
||||
],
|
||||
].map(withManifestLoadPaths),
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
@@ -205,7 +216,7 @@ function createManifestRegistryFixtureWithWorkspaceDemoChannel() {
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
},
|
||||
],
|
||||
].map(withManifestLoadPaths),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import { hasExplicitChannelConfig } from "./channel-presence-policy.js";
|
||||
import { resolveEffectivePluginActivationState } from "./config-state.js";
|
||||
import type { InstalledPluginIndexRecord } from "./installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import type { PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import {
|
||||
createPluginRegistryIdNormalizer,
|
||||
loadPluginRegistrySnapshot,
|
||||
@@ -93,13 +95,21 @@ function shouldConsiderForGatewayStartup(params: {
|
||||
|
||||
function hasConfiguredStartupChannel(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
manifestRegistry: PluginManifestRegistry;
|
||||
configuredChannelIds: ReadonlySet<string>;
|
||||
}): boolean {
|
||||
return params.plugin.contributions.channels.some((channelId) =>
|
||||
return listManifestChannelIds(params.manifestRegistry, params.plugin.pluginId).some((channelId) =>
|
||||
params.configuredChannelIds.has(channelId),
|
||||
);
|
||||
}
|
||||
|
||||
function listManifestChannelIds(
|
||||
manifestRegistry: PluginManifestRegistry,
|
||||
pluginId: string,
|
||||
): readonly string[] {
|
||||
return manifestRegistry.plugins.find((plugin) => plugin.id === pluginId)?.channels ?? [];
|
||||
}
|
||||
|
||||
function canStartConfiguredChannelPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
config: OpenClawConfig;
|
||||
@@ -108,6 +118,7 @@ function canStartConfiguredChannelPlugin(params: {
|
||||
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
rootConfig?: OpenClawConfig;
|
||||
};
|
||||
manifestRegistry: PluginManifestRegistry;
|
||||
}): boolean {
|
||||
if (!params.pluginsConfig.enabled) {
|
||||
return false;
|
||||
@@ -120,7 +131,7 @@ function canStartConfiguredChannelPlugin(params: {
|
||||
}
|
||||
const explicitBundledChannelConfig =
|
||||
params.plugin.origin === "bundled" &&
|
||||
params.plugin.contributions.channels.some((channelId) =>
|
||||
listManifestChannelIds(params.manifestRegistry, params.plugin.pluginId).some((channelId) =>
|
||||
hasExplicitChannelConfig({
|
||||
config: params.activationSource.rootConfig ?? params.config,
|
||||
channelId,
|
||||
@@ -157,9 +168,16 @@ export function resolveChannelPluginIds(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
return index.plugins
|
||||
.filter((plugin) => plugin.contributions.channels.length > 0)
|
||||
.map((plugin) => plugin.pluginId);
|
||||
const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({
|
||||
index,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
includeDisabled: true,
|
||||
});
|
||||
return manifestRegistry.plugins
|
||||
.filter((plugin) => plugin.channels.length > 0)
|
||||
.map((plugin) => plugin.id);
|
||||
}
|
||||
|
||||
export function resolveConfiguredDeferredChannelPluginIds(params: {
|
||||
@@ -177,6 +195,13 @@ export function resolveConfiguredDeferredChannelPluginIds(params: {
|
||||
env: params.env,
|
||||
});
|
||||
const pluginsConfig = normalizePluginsConfigWithRegistry(params.config.plugins, index);
|
||||
const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({
|
||||
index,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
includeDisabled: true,
|
||||
});
|
||||
const activationSource = {
|
||||
plugins: pluginsConfig,
|
||||
rootConfig: params.config,
|
||||
@@ -184,13 +209,14 @@ export function resolveConfiguredDeferredChannelPluginIds(params: {
|
||||
return index.plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
hasConfiguredStartupChannel({ plugin, configuredChannelIds }) &&
|
||||
hasConfiguredStartupChannel({ plugin, manifestRegistry, configuredChannelIds }) &&
|
||||
plugin.startup.deferConfiguredChannelFullLoadUntilAfterListen &&
|
||||
canStartConfiguredChannelPlugin({
|
||||
plugin,
|
||||
config: params.config,
|
||||
pluginsConfig,
|
||||
activationSource,
|
||||
manifestRegistry,
|
||||
}),
|
||||
)
|
||||
.map((plugin) => plugin.pluginId);
|
||||
@@ -209,6 +235,13 @@ export function resolveGatewayStartupPluginIds(params: {
|
||||
env: params.env,
|
||||
});
|
||||
const pluginsConfig = normalizePluginsConfigWithRegistry(params.config.plugins, index);
|
||||
const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({
|
||||
index,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
includeDisabled: true,
|
||||
});
|
||||
// Startup must classify allowlist exceptions against the raw config snapshot,
|
||||
// not the auto-enabled effective snapshot, or configured-only channels can be
|
||||
// misclassified as explicit enablement.
|
||||
@@ -227,12 +260,13 @@ export function resolveGatewayStartupPluginIds(params: {
|
||||
);
|
||||
return index.plugins
|
||||
.filter((plugin) => {
|
||||
if (hasConfiguredStartupChannel({ plugin, configuredChannelIds })) {
|
||||
if (hasConfiguredStartupChannel({ plugin, manifestRegistry, configuredChannelIds })) {
|
||||
return canStartConfiguredChannelPlugin({
|
||||
plugin,
|
||||
config: params.config,
|
||||
pluginsConfig,
|
||||
activationSource,
|
||||
manifestRegistry,
|
||||
});
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -39,16 +39,6 @@ function createIndex(overrides: Partial<InstalledPluginIndex> = {}): InstalledPl
|
||||
rootDir: "/plugins/demo",
|
||||
origin: "global",
|
||||
enabled: true,
|
||||
contributions: {
|
||||
providers: ["demo"],
|
||||
channels: ["demo-chat"],
|
||||
channelConfigs: ["demo-chat"],
|
||||
setupProviders: [],
|
||||
cliBackends: [],
|
||||
modelCatalogProviders: [],
|
||||
commandAliases: [],
|
||||
contracts: [],
|
||||
},
|
||||
startup: {
|
||||
sidecar: false,
|
||||
memory: false,
|
||||
@@ -221,7 +211,6 @@ describe("installed plugin index persistence", () => {
|
||||
plugins: [
|
||||
expect.objectContaining({
|
||||
pluginId: "demo",
|
||||
contributions: expect.objectContaining({ providers: ["demo", "demo-next"] }),
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -34,27 +34,14 @@ export type InstalledPluginIndexStoreInspection = {
|
||||
current: InstalledPluginIndex;
|
||||
};
|
||||
|
||||
const ContributionArraySchema = z.array(z.string());
|
||||
|
||||
const InstalledPluginIndexContributionsSchema = z
|
||||
.object({
|
||||
providers: ContributionArraySchema,
|
||||
channels: ContributionArraySchema,
|
||||
channelConfigs: ContributionArraySchema,
|
||||
setupProviders: ContributionArraySchema,
|
||||
cliBackends: ContributionArraySchema,
|
||||
modelCatalogProviders: ContributionArraySchema,
|
||||
commandAliases: ContributionArraySchema,
|
||||
contracts: ContributionArraySchema,
|
||||
})
|
||||
.passthrough();
|
||||
const StringArraySchema = z.array(z.string());
|
||||
|
||||
const InstalledPluginIndexStartupSchema = z
|
||||
.object({
|
||||
sidecar: z.boolean(),
|
||||
memory: z.boolean(),
|
||||
deferConfiguredChannelFullLoadUntilAfterListen: z.boolean(),
|
||||
agentHarnesses: ContributionArraySchema,
|
||||
agentHarnesses: StringArraySchema,
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
@@ -68,6 +55,8 @@ const InstalledPluginIndexRecordSchema = z
|
||||
packageInstall: z.unknown().optional(),
|
||||
manifestPath: z.string(),
|
||||
manifestHash: z.string(),
|
||||
source: z.string().optional(),
|
||||
setupSource: z.string().optional(),
|
||||
packageJson: z
|
||||
.object({
|
||||
path: z.string(),
|
||||
@@ -78,7 +67,6 @@ const InstalledPluginIndexRecordSchema = z
|
||||
origin: z.string(),
|
||||
enabled: z.boolean(),
|
||||
enabledByDefault: z.boolean().optional(),
|
||||
contributions: InstalledPluginIndexContributionsSchema,
|
||||
startup: InstalledPluginIndexStartupSchema,
|
||||
compat: z.array(z.string()),
|
||||
})
|
||||
|
||||
@@ -11,12 +11,9 @@ import {
|
||||
getInstalledPluginRecord,
|
||||
isInstalledPluginEnabled,
|
||||
listEnabledInstalledPluginRecords,
|
||||
listInstalledPluginContributionIds,
|
||||
listInstalledPluginRecords,
|
||||
loadInstalledPluginIndex,
|
||||
refreshInstalledPluginIndex,
|
||||
resolveInstalledPluginContributionOwners,
|
||||
resolveInstalledPluginContributions,
|
||||
} from "./installed-plugin-index.js";
|
||||
import { recordPluginInstall } from "./installs.js";
|
||||
import type { OpenClawPackageManifest } from "./manifest.js";
|
||||
@@ -168,6 +165,7 @@ describe("installed plugin index", () => {
|
||||
packageVersion: "1.2.3",
|
||||
origin: "global",
|
||||
rootDir: fixture.rootDir,
|
||||
source: path.join(fixture.rootDir, "index.ts"),
|
||||
enabled: true,
|
||||
packageInstall: {
|
||||
defaultChoice: "npm",
|
||||
@@ -182,16 +180,6 @@ describe("installed plugin index", () => {
|
||||
},
|
||||
warnings: [],
|
||||
},
|
||||
contributions: {
|
||||
providers: ["demo"],
|
||||
channels: ["demo-chat"],
|
||||
channelConfigs: ["demo-chat"],
|
||||
setupProviders: ["demo"],
|
||||
cliBackends: ["demo-cli", "setup-cli"],
|
||||
modelCatalogProviders: ["demo"],
|
||||
commandAliases: ["demo-command"],
|
||||
contracts: ["tools"],
|
||||
},
|
||||
compat: [
|
||||
"activation-channel-hint",
|
||||
"activation-provider-hint",
|
||||
@@ -208,11 +196,6 @@ describe("installed plugin index", () => {
|
||||
});
|
||||
expect(index.plugins[0]?.installRecord).toBeUndefined();
|
||||
expect(index.plugins[0]?.installRecordHash).toBeUndefined();
|
||||
|
||||
const contributions = resolveInstalledPluginContributions(index);
|
||||
expect(contributions.providers.get("demo")).toEqual(["demo"]);
|
||||
expect(contributions.channels.get("demo-chat")).toEqual(["demo"]);
|
||||
expect(contributions.contracts.get("tools")).toEqual(["demo"]);
|
||||
});
|
||||
|
||||
it("keeps packageJson paths root-relative when packageDir is reached through a symlink", () => {
|
||||
@@ -242,7 +225,7 @@ describe("installed plugin index", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("exposes cold registry records and owners for existing plugins without plugin indexs", () => {
|
||||
it("exposes cold registry records for existing plugins without plugin runtimes", () => {
|
||||
const fixture = createRichPluginFixture();
|
||||
const index = loadInstalledPluginIndex({
|
||||
candidates: [fixture.candidate],
|
||||
@@ -260,11 +243,6 @@ describe("installed plugin index", () => {
|
||||
});
|
||||
expect(record?.installRecord).toBeUndefined();
|
||||
expect(isInstalledPluginEnabled(index, "demo")).toBe(true);
|
||||
expect(listInstalledPluginContributionIds(index, "providers")).toEqual(["demo"]);
|
||||
expect(resolveInstalledPluginContributionOwners(index, "providers", "demo")).toEqual(["demo"]);
|
||||
expect(resolveInstalledPluginContributionOwners(index, "channels", "demo-chat")).toEqual([
|
||||
"demo",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps disabled plugins in inventory while excluding them from cold owner resolution", () => {
|
||||
@@ -299,18 +277,6 @@ describe("installed plugin index", () => {
|
||||
enabled: false,
|
||||
});
|
||||
expect(isInstalledPluginEnabled(index, "demo", config)).toBe(false);
|
||||
expect(listInstalledPluginContributionIds(index, "providers", { config })).toEqual([]);
|
||||
expect(
|
||||
listInstalledPluginContributionIds(index, "providers", { includeDisabled: true }),
|
||||
).toEqual(["demo"]);
|
||||
expect(
|
||||
resolveInstalledPluginContributionOwners(index, "providers", "demo", { config }),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
resolveInstalledPluginContributionOwners(index, "providers", "demo", {
|
||||
includeDisabled: true,
|
||||
}),
|
||||
).toEqual(["demo"]);
|
||||
});
|
||||
|
||||
it("uses runtime plugin id normalization for legacy enablement aliases", () => {
|
||||
@@ -735,7 +701,6 @@ describe("installed plugin index", () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(index.plugins[0]?.enabled).toBe(false);
|
||||
expect(index.plugins[0]?.contributions.providers).toEqual(["demo"]);
|
||||
});
|
||||
|
||||
it("tracks refresh reason without using the manifest cache", () => {
|
||||
@@ -793,13 +758,11 @@ describe("installed plugin index", () => {
|
||||
env: hermeticEnv({ OPENCLAW_VERSION: "2026.4.26" }),
|
||||
}),
|
||||
compatRegistryVersion: "different-compat-registry",
|
||||
migrationVersion: 2 as 1,
|
||||
};
|
||||
|
||||
expect(diffInstalledPluginIndexInvalidationReasons(previous, current)).toEqual([
|
||||
"compat-registry-changed",
|
||||
"host-contract-changed",
|
||||
"migration",
|
||||
"source-changed",
|
||||
"stale-manifest",
|
||||
"stale-package",
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
describePluginInstallSource,
|
||||
type PluginInstallSourceInfo,
|
||||
} from "./install-source-info.js";
|
||||
import type { PluginManifestCommandAlias } from "./manifest-command-aliases.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
type PluginManifestRecord,
|
||||
@@ -37,17 +36,6 @@ export type InstalledPluginIndexRefreshReason =
|
||||
| "compat-registry-changed"
|
||||
| "manual";
|
||||
|
||||
export type InstalledPluginIndexContributions = {
|
||||
providers: readonly string[];
|
||||
channels: readonly string[];
|
||||
channelConfigs: readonly string[];
|
||||
setupProviders: readonly string[];
|
||||
cliBackends: readonly string[];
|
||||
modelCatalogProviders: readonly string[];
|
||||
commandAliases: readonly string[];
|
||||
contracts: readonly string[];
|
||||
};
|
||||
|
||||
export type InstalledPluginStartupInfo = {
|
||||
sidecar: boolean;
|
||||
memory: boolean;
|
||||
@@ -96,6 +84,8 @@ export type InstalledPluginIndexRecord = {
|
||||
packageInstall?: PluginInstallSourceInfo;
|
||||
manifestPath: string;
|
||||
manifestHash: string;
|
||||
source?: string;
|
||||
setupSource?: string;
|
||||
packageJson?: {
|
||||
path: string;
|
||||
hash: string;
|
||||
@@ -104,7 +94,6 @@ export type InstalledPluginIndexRecord = {
|
||||
origin: PluginManifestRecord["origin"];
|
||||
enabled: boolean;
|
||||
enabledByDefault?: boolean;
|
||||
contributions: InstalledPluginIndexContributions;
|
||||
startup: InstalledPluginStartupInfo;
|
||||
compat: readonly PluginCompatCode[];
|
||||
};
|
||||
@@ -123,19 +112,6 @@ export type InstalledPluginIndex = {
|
||||
diagnostics: readonly PluginDiagnostic[];
|
||||
};
|
||||
|
||||
export type InstalledPluginContributions = {
|
||||
providers: ReadonlyMap<string, readonly string[]>;
|
||||
channels: ReadonlyMap<string, readonly string[]>;
|
||||
channelConfigs: ReadonlyMap<string, readonly string[]>;
|
||||
setupProviders: ReadonlyMap<string, readonly string[]>;
|
||||
cliBackends: ReadonlyMap<string, readonly string[]>;
|
||||
modelCatalogProviders: ReadonlyMap<string, readonly string[]>;
|
||||
commandAliases: ReadonlyMap<string, readonly string[]>;
|
||||
contracts: ReadonlyMap<string, readonly string[]>;
|
||||
};
|
||||
|
||||
export type InstalledPluginContributionKey = keyof InstalledPluginIndexContributions;
|
||||
|
||||
export type LoadInstalledPluginIndexParams = {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
@@ -193,28 +169,6 @@ function sortUnique(values: readonly string[] | undefined): readonly string[] {
|
||||
);
|
||||
}
|
||||
|
||||
function collectObjectKeys(value: Record<string, unknown> | undefined): readonly string[] {
|
||||
return sortUnique(value ? Object.keys(value) : []);
|
||||
}
|
||||
|
||||
function collectCommandAliasNames(
|
||||
aliases: readonly PluginManifestCommandAlias[] | undefined,
|
||||
): readonly string[] {
|
||||
return sortUnique(aliases?.map((alias) => alias.name) ?? []);
|
||||
}
|
||||
|
||||
function collectContractKeys(record: PluginManifestRecord): readonly string[] {
|
||||
const contracts = record.contracts;
|
||||
if (!contracts) {
|
||||
return [];
|
||||
}
|
||||
return sortUnique(
|
||||
Object.entries(contracts).flatMap(([key, value]) =>
|
||||
Array.isArray(value) && value.length > 0 ? [key] : [],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function hasRuntimeContractSurface(record: PluginManifestRecord): boolean {
|
||||
return Boolean(
|
||||
record.providers.length > 0 ||
|
||||
@@ -269,19 +223,6 @@ function collectCompatCodes(record: PluginManifestRecord): readonly PluginCompat
|
||||
return sortUnique(codes) as readonly PluginCompatCode[];
|
||||
}
|
||||
|
||||
function buildContributions(record: PluginManifestRecord): InstalledPluginIndexContributions {
|
||||
return {
|
||||
providers: sortUnique(record.providers),
|
||||
channels: sortUnique(record.channels),
|
||||
channelConfigs: collectObjectKeys(record.channelConfigs),
|
||||
setupProviders: sortUnique(record.setup?.providers?.map((provider) => provider.id) ?? []),
|
||||
cliBackends: sortUnique([...(record.cliBackends ?? []), ...(record.setup?.cliBackends ?? [])]),
|
||||
modelCatalogProviders: collectObjectKeys(record.modelCatalog?.providers),
|
||||
commandAliases: collectCommandAliasNames(record.commandAliases),
|
||||
contracts: collectContractKeys(record),
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePackageJsonPath(candidate: PluginCandidate | undefined): string | undefined {
|
||||
if (!candidate?.packageDir) {
|
||||
return undefined;
|
||||
@@ -568,16 +509,19 @@ function buildInstalledPluginIndex(
|
||||
pluginId: record.id,
|
||||
manifestPath: record.manifestPath,
|
||||
manifestHash,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
origin: record.origin,
|
||||
enabled,
|
||||
contributions: buildContributions(record),
|
||||
startup: buildStartupInfo(record),
|
||||
compat: collectCompatCodes(record),
|
||||
};
|
||||
if (record.enabledByDefault === true) {
|
||||
indexRecord.enabledByDefault = true;
|
||||
}
|
||||
if (record.setupSource) {
|
||||
indexRecord.setupSource = record.setupSource;
|
||||
}
|
||||
if (candidate?.packageName) {
|
||||
indexRecord.packageName = candidate.packageName;
|
||||
}
|
||||
@@ -678,118 +622,6 @@ export function isInstalledPluginEnabled(
|
||||
}).enabled;
|
||||
}
|
||||
|
||||
function resolveContributionRecordSet(
|
||||
index: InstalledPluginIndex,
|
||||
options: { includeDisabled?: boolean; config?: OpenClawConfig },
|
||||
): readonly InstalledPluginIndexRecord[] {
|
||||
return options.includeDisabled
|
||||
? index.plugins
|
||||
: listEnabledInstalledPluginRecords(index, options.config);
|
||||
}
|
||||
|
||||
export function listInstalledPluginContributionIds(
|
||||
index: InstalledPluginIndex,
|
||||
contribution: InstalledPluginContributionKey,
|
||||
options: { includeDisabled?: boolean; config?: OpenClawConfig } = {},
|
||||
): readonly string[] {
|
||||
return sortUnique(
|
||||
resolveContributionRecordSet(index, options).flatMap(
|
||||
(plugin) => plugin.contributions[contribution],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveInstalledPluginContributionOwners(
|
||||
index: InstalledPluginIndex,
|
||||
contribution: InstalledPluginContributionKey,
|
||||
matches: string | ((contributionId: string) => boolean),
|
||||
options: { includeDisabled?: boolean; config?: OpenClawConfig } = {},
|
||||
): readonly string[] {
|
||||
const matcher =
|
||||
typeof matches === "string" ? (contributionId: string) => contributionId === matches : matches;
|
||||
const owners: string[] = [];
|
||||
for (const plugin of resolveContributionRecordSet(index, options)) {
|
||||
if (plugin.contributions[contribution].some(matcher)) {
|
||||
owners.push(plugin.pluginId);
|
||||
}
|
||||
}
|
||||
return sortUnique(owners);
|
||||
}
|
||||
|
||||
function addContribution(
|
||||
target: Map<string, string[]>,
|
||||
contributionId: string,
|
||||
pluginId: string,
|
||||
): void {
|
||||
const existing = target.get(contributionId);
|
||||
if (existing) {
|
||||
existing.push(pluginId);
|
||||
} else {
|
||||
target.set(contributionId, [pluginId]);
|
||||
}
|
||||
}
|
||||
|
||||
function freezeContributionMap(
|
||||
source: Map<string, string[]>,
|
||||
): ReadonlyMap<string, readonly string[]> {
|
||||
const frozen = new Map<string, readonly string[]>();
|
||||
for (const [key, pluginIds] of source) {
|
||||
frozen.set(key, sortUnique(pluginIds));
|
||||
}
|
||||
return frozen;
|
||||
}
|
||||
|
||||
export function resolveInstalledPluginContributions(
|
||||
index: InstalledPluginIndex,
|
||||
): InstalledPluginContributions {
|
||||
const providers = new Map<string, string[]>();
|
||||
const channels = new Map<string, string[]>();
|
||||
const channelConfigs = new Map<string, string[]>();
|
||||
const setupProviders = new Map<string, string[]>();
|
||||
const cliBackends = new Map<string, string[]>();
|
||||
const modelCatalogProviders = new Map<string, string[]>();
|
||||
const commandAliases = new Map<string, string[]>();
|
||||
const contracts = new Map<string, string[]>();
|
||||
|
||||
for (const plugin of index.plugins) {
|
||||
for (const provider of plugin.contributions.providers) {
|
||||
addContribution(providers, provider, plugin.pluginId);
|
||||
}
|
||||
for (const channel of plugin.contributions.channels) {
|
||||
addContribution(channels, channel, plugin.pluginId);
|
||||
}
|
||||
for (const channelConfig of plugin.contributions.channelConfigs) {
|
||||
addContribution(channelConfigs, channelConfig, plugin.pluginId);
|
||||
}
|
||||
for (const setupProvider of plugin.contributions.setupProviders) {
|
||||
addContribution(setupProviders, setupProvider, plugin.pluginId);
|
||||
}
|
||||
for (const cliBackend of plugin.contributions.cliBackends) {
|
||||
addContribution(cliBackends, cliBackend, plugin.pluginId);
|
||||
}
|
||||
for (const modelCatalogProvider of plugin.contributions.modelCatalogProviders) {
|
||||
addContribution(modelCatalogProviders, modelCatalogProvider, plugin.pluginId);
|
||||
}
|
||||
for (const commandAlias of plugin.contributions.commandAliases) {
|
||||
addContribution(commandAliases, commandAlias, plugin.pluginId);
|
||||
}
|
||||
for (const contract of plugin.contributions.contracts) {
|
||||
addContribution(contracts, contract, plugin.pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
providers: freezeContributionMap(providers),
|
||||
channels: freezeContributionMap(channels),
|
||||
channelConfigs: freezeContributionMap(channelConfigs),
|
||||
setupProviders: freezeContributionMap(setupProviders),
|
||||
cliBackends: freezeContributionMap(cliBackends),
|
||||
modelCatalogProviders: freezeContributionMap(modelCatalogProviders),
|
||||
commandAliases: freezeContributionMap(commandAliases),
|
||||
contracts: freezeContributionMap(contracts),
|
||||
};
|
||||
}
|
||||
|
||||
export function diffInstalledPluginIndexInvalidationReasons(
|
||||
previous: InstalledPluginIndex,
|
||||
current: InstalledPluginIndex,
|
||||
|
||||
92
src/plugins/manifest-registry-installed.test.ts
Normal file
92
src/plugins/manifest-registry-installed.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTrackedTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
function makeTempDir() {
|
||||
return makeTrackedTempDir("openclaw-installed-manifest-registry", tempDirs);
|
||||
}
|
||||
|
||||
function writePlugin(rootDir: string, pluginId: string, modelPrefix: string) {
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "index.ts"),
|
||||
"throw new Error('runtime entry should not load while reading manifests');\n",
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: pluginId,
|
||||
configSchema: { type: "object" },
|
||||
providers: [pluginId],
|
||||
modelSupport: {
|
||||
modelPrefixes: [modelPrefix],
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
function createIndex(rootDir: string): InstalledPluginIndex {
|
||||
return {
|
||||
version: 1,
|
||||
hostContractVersion: "2026.4.25",
|
||||
compatRegistryVersion: "compat-v1",
|
||||
migrationVersion: 1,
|
||||
policyHash: "policy-v1",
|
||||
generatedAtMs: 1777118400000,
|
||||
installRecords: {},
|
||||
plugins: [
|
||||
{
|
||||
pluginId: "installed",
|
||||
manifestPath: path.join(rootDir, "openclaw.plugin.json"),
|
||||
manifestHash: "manifest-hash",
|
||||
source: path.join(rootDir, "index.ts"),
|
||||
rootDir,
|
||||
origin: "global",
|
||||
enabled: true,
|
||||
startup: {
|
||||
sidecar: false,
|
||||
memory: false,
|
||||
deferConfiguredChannelFullLoadUntilAfterListen: false,
|
||||
agentHarnesses: [],
|
||||
},
|
||||
compat: [],
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("loadPluginManifestRegistryForInstalledIndex", () => {
|
||||
it("loads manifest metadata only for plugins present in the installed index", () => {
|
||||
const installedRoot = makeTempDir();
|
||||
const unrelatedRoot = makeTempDir();
|
||||
writePlugin(installedRoot, "installed", "installed-");
|
||||
writePlugin(unrelatedRoot, "unrelated", "unrelated-");
|
||||
|
||||
const registry = loadPluginManifestRegistryForInstalledIndex({
|
||||
index: createIndex(installedRoot),
|
||||
env: {
|
||||
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
|
||||
OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
|
||||
OPENCLAW_VERSION: "2026.4.25",
|
||||
VITEST: "true",
|
||||
},
|
||||
includeDisabled: true,
|
||||
});
|
||||
|
||||
expect(registry.plugins.map((plugin) => plugin.id)).toEqual(["installed"]);
|
||||
expect(registry.plugins[0]?.modelSupport).toEqual({
|
||||
modelPrefixes: ["installed-"],
|
||||
});
|
||||
});
|
||||
});
|
||||
57
src/plugins/manifest-registry-installed.ts
Normal file
57
src/plugins/manifest-registry-installed.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { PluginCandidate } from "./discovery.js";
|
||||
import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
|
||||
import { extractPluginInstallRecordsFromInstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { DEFAULT_PLUGIN_ENTRY_CANDIDATES } from "./manifest.js";
|
||||
|
||||
function resolveFallbackPluginSource(record: InstalledPluginIndexRecord): string {
|
||||
for (const entry of DEFAULT_PLUGIN_ENTRY_CANDIDATES) {
|
||||
const candidate = path.join(record.rootDir, entry);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return path.join(record.rootDir, DEFAULT_PLUGIN_ENTRY_CANDIDATES[0]);
|
||||
}
|
||||
|
||||
function toPluginCandidate(record: InstalledPluginIndexRecord): PluginCandidate {
|
||||
return {
|
||||
idHint: record.pluginId,
|
||||
source: record.source ?? resolveFallbackPluginSource(record),
|
||||
...(record.setupSource ? { setupSource: record.setupSource } : {}),
|
||||
rootDir: record.rootDir,
|
||||
origin: record.origin,
|
||||
...(record.packageName ? { packageName: record.packageName } : {}),
|
||||
...(record.packageVersion ? { packageVersion: record.packageVersion } : {}),
|
||||
packageDir: record.rootDir,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadPluginManifestRegistryForInstalledIndex(params: {
|
||||
index: InstalledPluginIndex;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
pluginIds?: readonly string[];
|
||||
includeDisabled?: boolean;
|
||||
}): PluginManifestRegistry {
|
||||
if (params.pluginIds && params.pluginIds.length === 0) {
|
||||
return { plugins: [], diagnostics: [] };
|
||||
}
|
||||
const pluginIdSet = params.pluginIds?.length ? new Set(params.pluginIds) : null;
|
||||
const candidates = params.index.plugins
|
||||
.filter((plugin) => params.includeDisabled || plugin.enabled)
|
||||
.filter((plugin) => !pluginIdSet || pluginIdSet.has(plugin.pluginId))
|
||||
.map(toPluginCandidate);
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
cache: false,
|
||||
candidates,
|
||||
installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(params.index),
|
||||
});
|
||||
}
|
||||
@@ -117,16 +117,6 @@ function createIndex(
|
||||
rootDir: `/plugins/${pluginId}`,
|
||||
origin: "global",
|
||||
enabled: true,
|
||||
contributions: {
|
||||
providers: [pluginId],
|
||||
channels: [],
|
||||
channelConfigs: [],
|
||||
setupProviders: [],
|
||||
cliBackends: [],
|
||||
modelCatalogProviders: [],
|
||||
commandAliases: [],
|
||||
contracts: [],
|
||||
},
|
||||
startup: {
|
||||
sidecar: false,
|
||||
memory: false,
|
||||
@@ -210,17 +200,25 @@ describe("plugin registry facade", () => {
|
||||
});
|
||||
|
||||
it("normalizes plugin config ids through registry contribution aliases", () => {
|
||||
const baseIndex = createIndex("openai");
|
||||
const plugin = baseIndex.plugins[0];
|
||||
const rootDir = makeTempDir();
|
||||
fs.writeFileSync(path.join(rootDir, "index.ts"), "", "utf8");
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: "openai",
|
||||
configSchema: { type: "object" },
|
||||
providers: ["openai", "openai-codex"],
|
||||
channels: ["openai-chat"],
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const index = createIndex("openai", {
|
||||
plugins: [
|
||||
{
|
||||
...plugin,
|
||||
contributions: {
|
||||
...plugin.contributions,
|
||||
providers: ["openai", "openai-codex"],
|
||||
channels: ["openai-chat"],
|
||||
},
|
||||
...createIndex("openai").plugins[0],
|
||||
manifestPath: path.join(rootDir, "openclaw.plugin.json"),
|
||||
source: path.join(rootDir, "index.ts"),
|
||||
rootDir,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -13,17 +13,16 @@ import {
|
||||
getInstalledPluginRecord,
|
||||
extractPluginInstallRecordsFromInstalledPluginIndex,
|
||||
isInstalledPluginEnabled,
|
||||
listInstalledPluginContributionIds,
|
||||
listInstalledPluginRecords,
|
||||
loadInstalledPluginIndex,
|
||||
resolveInstalledPluginContributionOwners,
|
||||
resolveInstalledPluginIndexPolicyHash,
|
||||
type InstalledPluginContributionKey,
|
||||
type InstalledPluginIndex,
|
||||
type InstalledPluginIndexRecord,
|
||||
type LoadInstalledPluginIndexParams,
|
||||
type RefreshInstalledPluginIndexParams,
|
||||
} from "./installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
|
||||
|
||||
export type PluginRegistrySnapshot = InstalledPluginIndex;
|
||||
export type PluginRegistryRecord = InstalledPluginIndexRecord;
|
||||
@@ -66,13 +65,23 @@ export type GetPluginRecordParams = LoadPluginRegistryParams & {
|
||||
pluginId: string;
|
||||
};
|
||||
|
||||
export type PluginRegistryContributionKey =
|
||||
| "providers"
|
||||
| "channels"
|
||||
| "channelConfigs"
|
||||
| "setupProviders"
|
||||
| "cliBackends"
|
||||
| "modelCatalogProviders"
|
||||
| "commandAliases"
|
||||
| "contracts";
|
||||
|
||||
export type ResolvePluginContributionOwnersParams = PluginRegistryContributionOptions & {
|
||||
contribution: InstalledPluginContributionKey;
|
||||
contribution: PluginRegistryContributionKey;
|
||||
matches: string | ((contributionId: string) => boolean);
|
||||
};
|
||||
|
||||
export type ListPluginContributionIdsParams = PluginRegistryContributionOptions & {
|
||||
contribution: InstalledPluginContributionKey;
|
||||
contribution: PluginRegistryContributionKey;
|
||||
};
|
||||
|
||||
export type ResolveProviderOwnersParams = PluginRegistryContributionOptions & {
|
||||
@@ -103,24 +112,114 @@ function normalizePluginRegistryAliasKey(value: string): string {
|
||||
return normalizePluginRegistryAlias(value).toLowerCase();
|
||||
}
|
||||
|
||||
function sortUnique(values: Iterable<string>): string[] {
|
||||
return [...new Set([...values].map((value) => value.trim()).filter(Boolean))].toSorted(
|
||||
(left, right) => left.localeCompare(right),
|
||||
);
|
||||
}
|
||||
|
||||
function collectObjectKeys(value: Record<string, unknown> | undefined): readonly string[] {
|
||||
return value ? Object.keys(value) : [];
|
||||
}
|
||||
|
||||
function collectContractKeys(plugin: PluginManifestRecord): readonly string[] {
|
||||
const contracts = plugin.contracts;
|
||||
if (!contracts) {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(contracts).flatMap(([key, value]) =>
|
||||
Array.isArray(value) && value.length > 0 ? [key] : [],
|
||||
);
|
||||
}
|
||||
|
||||
function listManifestContributionIds(
|
||||
plugin: PluginManifestRecord,
|
||||
contribution: PluginRegistryContributionKey,
|
||||
): readonly string[] {
|
||||
switch (contribution) {
|
||||
case "providers":
|
||||
return plugin.providers;
|
||||
case "channels":
|
||||
return plugin.channels;
|
||||
case "channelConfigs":
|
||||
return collectObjectKeys(plugin.channelConfigs);
|
||||
case "setupProviders":
|
||||
return plugin.setup?.providers?.map((provider) => provider.id) ?? [];
|
||||
case "cliBackends":
|
||||
return [...plugin.cliBackends, ...(plugin.setup?.cliBackends ?? [])];
|
||||
case "modelCatalogProviders":
|
||||
return collectObjectKeys(plugin.modelCatalog?.providers);
|
||||
case "commandAliases":
|
||||
return plugin.commandAliases?.map((alias) => alias.name) ?? [];
|
||||
case "contracts":
|
||||
return collectContractKeys(plugin);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function resolveContributionPluginIds(params: {
|
||||
index: PluginRegistrySnapshot;
|
||||
includeDisabled?: boolean;
|
||||
config?: OpenClawConfig;
|
||||
}): readonly string[] {
|
||||
if (params.includeDisabled) {
|
||||
return params.index.plugins.map((plugin) => plugin.pluginId);
|
||||
}
|
||||
return params.index.plugins
|
||||
.filter((plugin) => isInstalledPluginEnabled(params.index, plugin.pluginId, params.config))
|
||||
.map((plugin) => plugin.pluginId);
|
||||
}
|
||||
|
||||
function loadContributionManifestRegistry(
|
||||
params: LoadPluginRegistryParams & {
|
||||
index: PluginRegistrySnapshot;
|
||||
includeDisabled?: boolean;
|
||||
},
|
||||
): PluginManifestRegistry {
|
||||
return loadPluginManifestRegistryForInstalledIndex({
|
||||
index: params.index,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
pluginIds: resolveContributionPluginIds({
|
||||
index: params.index,
|
||||
includeDisabled: params.includeDisabled,
|
||||
config: params.config,
|
||||
}),
|
||||
includeDisabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function createPluginRegistryIdNormalizer(
|
||||
index: PluginRegistrySnapshot,
|
||||
): (pluginId: string) => string {
|
||||
const aliases = new Map<string, string>();
|
||||
for (const plugin of [...index.plugins].toSorted((left, right) =>
|
||||
left.pluginId.localeCompare(right.pluginId),
|
||||
)) {
|
||||
for (const plugin of index.plugins) {
|
||||
const pluginId = normalizePluginRegistryAlias(plugin.pluginId);
|
||||
if (pluginId) {
|
||||
aliases.set(normalizePluginRegistryAliasKey(pluginId), plugin.pluginId);
|
||||
}
|
||||
}
|
||||
const registry = loadPluginManifestRegistryForInstalledIndex({
|
||||
index,
|
||||
includeDisabled: true,
|
||||
});
|
||||
for (const plugin of [...registry.plugins].toSorted((left, right) =>
|
||||
left.id.localeCompare(right.id),
|
||||
)) {
|
||||
const pluginId = normalizePluginRegistryAlias(plugin.id);
|
||||
if (!pluginId) {
|
||||
continue;
|
||||
}
|
||||
aliases.set(normalizePluginRegistryAliasKey(pluginId), pluginId);
|
||||
aliases.set(normalizePluginRegistryAliasKey(pluginId), plugin.id);
|
||||
for (const alias of [
|
||||
...plugin.contributions.providers,
|
||||
...plugin.contributions.channels,
|
||||
...plugin.contributions.setupProviders,
|
||||
...plugin.contributions.cliBackends,
|
||||
...plugin.contributions.modelCatalogProviders,
|
||||
plugin.id,
|
||||
...listManifestContributionIds(plugin, "providers"),
|
||||
...listManifestContributionIds(plugin, "channels"),
|
||||
...listManifestContributionIds(plugin, "setupProviders"),
|
||||
...listManifestContributionIds(plugin, "cliBackends"),
|
||||
...listManifestContributionIds(plugin, "modelCatalogProviders"),
|
||||
...(plugin.legacyPluginIds ?? []),
|
||||
]) {
|
||||
const normalizedAlias = normalizePluginRegistryAlias(alias);
|
||||
const normalizedAliasKey = normalizePluginRegistryAliasKey(alias);
|
||||
@@ -241,23 +340,32 @@ export function isPluginEnabled(params: GetPluginRecordParams): boolean {
|
||||
export function listPluginContributionIds(
|
||||
params: ListPluginContributionIdsParams,
|
||||
): readonly string[] {
|
||||
return listInstalledPluginContributionIds(resolveSnapshot(params), params.contribution, {
|
||||
includeDisabled: params.includeDisabled,
|
||||
config: params.config,
|
||||
const index = resolveSnapshot(params);
|
||||
const registry = loadContributionManifestRegistry({
|
||||
...params,
|
||||
index,
|
||||
});
|
||||
return sortUnique(
|
||||
registry.plugins.flatMap((plugin) => listManifestContributionIds(plugin, params.contribution)),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePluginContributionOwners(
|
||||
params: ResolvePluginContributionOwnersParams,
|
||||
): readonly string[] {
|
||||
return resolveInstalledPluginContributionOwners(
|
||||
resolveSnapshot(params),
|
||||
params.contribution,
|
||||
params.matches,
|
||||
{
|
||||
includeDisabled: params.includeDisabled,
|
||||
config: params.config,
|
||||
},
|
||||
const matcher =
|
||||
typeof params.matches === "string"
|
||||
? (contributionId: string) => contributionId === params.matches
|
||||
: params.matches;
|
||||
const index = resolveSnapshot(params);
|
||||
const registry = loadContributionManifestRegistry({
|
||||
...params,
|
||||
index,
|
||||
});
|
||||
return sortUnique(
|
||||
registry.plugins.flatMap((plugin) =>
|
||||
listManifestContributionIds(plugin, params.contribution).some(matcher) ? [plugin.id] : [],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,8 @@ import {
|
||||
isActivatedManifestOwner,
|
||||
passesManifestOwnerBasePolicy,
|
||||
} from "./manifest-owner-policy.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
type PluginManifestRecord,
|
||||
type PluginManifestRegistry,
|
||||
} from "./manifest-registry.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import { type PluginManifestRecord, type PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import {
|
||||
loadPluginRegistrySnapshot,
|
||||
normalizePluginsConfigWithRegistry,
|
||||
@@ -32,14 +29,6 @@ type ProviderRegistryLoadParams = ProviderManifestLoadParams & {
|
||||
onlyPluginIds?: readonly string[];
|
||||
};
|
||||
|
||||
function loadProviderManifestRegistry(params: ProviderManifestLoadParams): PluginManifestRegistry {
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
}
|
||||
|
||||
function loadProviderRegistrySnapshot(params: ProviderManifestLoadParams): PluginRegistrySnapshot {
|
||||
return loadPluginRegistrySnapshot({
|
||||
config: params.config,
|
||||
@@ -68,6 +57,19 @@ function listRegistryPluginIds(
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function resolveProviderSurfacePluginIdSet(
|
||||
params: ProviderManifestLoadParams & {
|
||||
registry: PluginRegistrySnapshot;
|
||||
},
|
||||
): ReadonlySet<string> {
|
||||
return new Set(
|
||||
resolveManifestRegistry({
|
||||
...params,
|
||||
includeDisabled: true,
|
||||
}).plugins.flatMap((plugin) => (plugin.providers.length > 0 ? [plugin.id] : [])),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveProviderOwnerPluginIds(
|
||||
params: ProviderRegistryLoadParams & {
|
||||
pluginIds: readonly string[];
|
||||
@@ -89,10 +91,6 @@ function resolveProviderOwnerPluginIds(
|
||||
);
|
||||
}
|
||||
|
||||
function recordHasProviderSurface(plugin: PluginRegistryRecord): boolean {
|
||||
return plugin.contributions.providers.length > 0;
|
||||
}
|
||||
|
||||
function resolveEffectiveRegistryPluginActivation(params: {
|
||||
plugin: PluginRegistryRecord;
|
||||
normalizedConfig: NormalizedPluginsConfig;
|
||||
@@ -130,11 +128,12 @@ export function resolveBundledProviderCompatPluginIds(params: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): string[] {
|
||||
const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params);
|
||||
const providerSurfacePluginIds = resolveProviderSurfacePluginIdSet({ ...params, registry });
|
||||
return listRegistryPluginIds(
|
||||
registry,
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" &&
|
||||
recordHasProviderSurface(plugin) &&
|
||||
providerSurfacePluginIds.has(plugin.pluginId) &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.pluginId)),
|
||||
);
|
||||
}
|
||||
@@ -146,11 +145,12 @@ export function resolveEnabledProviderPluginIds(params: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): string[] {
|
||||
const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params);
|
||||
const providerSurfacePluginIds = resolveProviderSurfacePluginIdSet({ ...params, registry });
|
||||
const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry);
|
||||
return listRegistryPluginIds(
|
||||
registry,
|
||||
(plugin) =>
|
||||
recordHasProviderSurface(plugin) &&
|
||||
providerSurfacePluginIds.has(plugin.pluginId) &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.pluginId)) &&
|
||||
resolveEffectiveRegistryPluginActivation({
|
||||
plugin,
|
||||
@@ -180,15 +180,25 @@ function resolveRegistryManifestContractPluginIds(params: {
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): string[] {
|
||||
const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params);
|
||||
return listRegistryPluginIds(registry, (plugin) => {
|
||||
if (params.origin && plugin.origin !== params.origin) {
|
||||
return false;
|
||||
}
|
||||
if (onlyPluginIdSet && !onlyPluginIdSet.has(plugin.pluginId)) {
|
||||
return false;
|
||||
}
|
||||
return plugin.contributions.contracts.includes(params.contract);
|
||||
});
|
||||
return resolveManifestRegistry({
|
||||
...params,
|
||||
registry,
|
||||
includeDisabled: true,
|
||||
})
|
||||
.plugins.filter((plugin) => {
|
||||
if (params.origin && plugin.origin !== params.origin) {
|
||||
return false;
|
||||
}
|
||||
if (onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
(plugin.contracts?.[params.contract as keyof NonNullable<typeof plugin.contracts>] ?? [])
|
||||
.length > 0
|
||||
);
|
||||
})
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function resolveExternalAuthProfileCompatFallbackPluginIds(params: {
|
||||
@@ -203,12 +213,13 @@ export function resolveExternalAuthProfileCompatFallbackPluginIds(params: {
|
||||
const declaredPluginIds =
|
||||
params.declaredPluginIds ?? new Set(resolveExternalAuthProfileProviderPluginIds(params));
|
||||
const registry = loadProviderRegistrySnapshot(params);
|
||||
const providerSurfacePluginIds = resolveProviderSurfacePluginIdSet({ ...params, registry });
|
||||
const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry);
|
||||
return listRegistryPluginIds(
|
||||
registry,
|
||||
(plugin) =>
|
||||
plugin.origin !== "bundled" &&
|
||||
recordHasProviderSurface(plugin) &&
|
||||
providerSurfacePluginIds.has(plugin.pluginId) &&
|
||||
!declaredPluginIds.has(plugin.pluginId) &&
|
||||
isProviderPluginEligibleForRuntimeOwnerActivation({
|
||||
plugin,
|
||||
@@ -226,12 +237,13 @@ export function resolveDiscoveredProviderPluginIds(params: {
|
||||
includeUntrustedWorkspacePlugins?: boolean;
|
||||
}): string[] {
|
||||
const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params);
|
||||
const providerSurfacePluginIds = resolveProviderSurfacePluginIdSet({ ...params, registry });
|
||||
const shouldFilterUntrustedWorkspacePlugins = params.includeUntrustedWorkspacePlugins === false;
|
||||
const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry);
|
||||
return listRegistryPluginIds(registry, (plugin) => {
|
||||
if (
|
||||
!(
|
||||
recordHasProviderSurface(plugin) &&
|
||||
providerSurfacePluginIds.has(plugin.pluginId) &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.pluginId))
|
||||
)
|
||||
) {
|
||||
@@ -349,8 +361,20 @@ function resolveManifestRegistry(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
manifestRegistry?: PluginManifestRegistry;
|
||||
registry?: PluginRegistrySnapshot;
|
||||
includeDisabled?: boolean;
|
||||
}): PluginManifestRegistry {
|
||||
return params.manifestRegistry ?? loadProviderManifestRegistry(params);
|
||||
if (params.manifestRegistry) {
|
||||
return params.manifestRegistry;
|
||||
}
|
||||
const registry = params.registry ?? loadProviderRegistrySnapshot(params);
|
||||
return loadPluginManifestRegistryForInstalledIndex({
|
||||
index: registry,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
includeDisabled: params.includeDisabled,
|
||||
});
|
||||
}
|
||||
|
||||
function stripModelProfileSuffix(value: string): string {
|
||||
@@ -482,6 +506,7 @@ export function resolveOwningPluginIdsForModelRef(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
manifestRegistry?: PluginManifestRegistry;
|
||||
registry?: PluginRegistrySnapshot;
|
||||
}): string[] | undefined {
|
||||
const parsed = splitExplicitModelRef(params.model);
|
||||
if (!parsed) {
|
||||
@@ -498,19 +523,25 @@ export function resolveOwningPluginIdsForModelRef(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const registry = resolveManifestRegistry(params);
|
||||
const matchedByPattern = registry.plugins
|
||||
const manifestRegistry = resolveManifestRegistry({
|
||||
...params,
|
||||
includeDisabled: true,
|
||||
});
|
||||
const matchedByPattern = manifestRegistry.plugins
|
||||
.filter((plugin) => resolveModelSupportMatchKind(plugin, parsed.modelId) === "pattern")
|
||||
.map((plugin) => plugin.id);
|
||||
const preferredPatternPluginIds = resolvePreferredManifestPluginIds(registry, matchedByPattern);
|
||||
const preferredPatternPluginIds = resolvePreferredManifestPluginIds(
|
||||
manifestRegistry,
|
||||
matchedByPattern,
|
||||
);
|
||||
if (preferredPatternPluginIds) {
|
||||
return preferredPatternPluginIds;
|
||||
}
|
||||
|
||||
const matchedByPrefix = registry.plugins
|
||||
const matchedByPrefix = manifestRegistry.plugins
|
||||
.filter((plugin) => resolveModelSupportMatchKind(plugin, parsed.modelId) === "prefix")
|
||||
.map((plugin) => plugin.id);
|
||||
return resolvePreferredManifestPluginIds(registry, matchedByPrefix);
|
||||
return resolvePreferredManifestPluginIds(manifestRegistry, matchedByPrefix);
|
||||
}
|
||||
|
||||
export function resolveOwningPluginIdsForModelRefs(params: {
|
||||
@@ -520,7 +551,8 @@ export function resolveOwningPluginIdsForModelRefs(params: {
|
||||
env?: PluginLoadOptions["env"];
|
||||
manifestRegistry?: PluginManifestRegistry;
|
||||
}): string[] {
|
||||
const registry = resolveManifestRegistry(params);
|
||||
const registry = params.manifestRegistry ? undefined : loadProviderRegistrySnapshot(params);
|
||||
const manifestRegistry = params.manifestRegistry;
|
||||
return dedupeSortedPluginIds(
|
||||
params.models.flatMap(
|
||||
(model) =>
|
||||
@@ -529,7 +561,8 @@ export function resolveOwningPluginIdsForModelRefs(params: {
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
manifestRegistry: registry,
|
||||
...(manifestRegistry ? { manifestRegistry } : {}),
|
||||
...(registry ? { registry } : {}),
|
||||
}) ?? [],
|
||||
),
|
||||
);
|
||||
@@ -541,12 +574,13 @@ export function resolveNonBundledProviderPluginIds(params: {
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
const registry = loadProviderRegistrySnapshot(params);
|
||||
const providerSurfacePluginIds = resolveProviderSurfacePluginIdSet({ ...params, registry });
|
||||
const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry);
|
||||
return listRegistryPluginIds(
|
||||
registry,
|
||||
(plugin) =>
|
||||
plugin.origin !== "bundled" &&
|
||||
recordHasProviderSurface(plugin) &&
|
||||
providerSurfacePluginIds.has(plugin.pluginId) &&
|
||||
resolveEffectiveRegistryPluginActivation({
|
||||
plugin,
|
||||
normalizedConfig,
|
||||
@@ -561,11 +595,12 @@ export function resolveCatalogHookProviderPluginIds(params: {
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
const registry = loadProviderRegistrySnapshot(params);
|
||||
const providerSurfacePluginIds = resolveProviderSurfacePluginIdSet({ ...params, registry });
|
||||
const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry);
|
||||
const enabledProviderPluginIds = listRegistryPluginIds(
|
||||
registry,
|
||||
(plugin) =>
|
||||
recordHasProviderSurface(plugin) &&
|
||||
providerSurfacePluginIds.has(plugin.pluginId) &&
|
||||
resolveEffectiveRegistryPluginActivation({
|
||||
plugin,
|
||||
normalizedConfig,
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadPluginRegistrySnapshotMock = vi.hoisted(() => vi.fn());
|
||||
const loadPluginManifestRegistryForInstalledIndexMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./plugin-registry.js", () => ({
|
||||
loadPluginRegistrySnapshot: loadPluginRegistrySnapshotMock,
|
||||
}));
|
||||
vi.mock("./manifest-registry-installed.js", () => ({
|
||||
loadPluginManifestRegistryForInstalledIndex: loadPluginManifestRegistryForInstalledIndexMock,
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
loadPluginRegistrySnapshotMock.mockReset();
|
||||
loadPluginManifestRegistryForInstalledIndexMock.mockReset();
|
||||
});
|
||||
|
||||
describe("setup-registry runtime fallback", () => {
|
||||
@@ -19,25 +24,26 @@ describe("setup-registry runtime fallback", () => {
|
||||
pluginId: "openai",
|
||||
origin: "bundled",
|
||||
enabled: true,
|
||||
contributions: {
|
||||
cliBackends: ["Codex-CLI", "legacy-openai-cli"],
|
||||
},
|
||||
},
|
||||
{
|
||||
pluginId: "disabled",
|
||||
origin: "bundled",
|
||||
enabled: false,
|
||||
contributions: {
|
||||
cliBackends: ["disabled-cli"],
|
||||
},
|
||||
},
|
||||
{
|
||||
pluginId: "local",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
contributions: {
|
||||
cliBackends: ["local-cli"],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
loadPluginManifestRegistryForInstalledIndexMock.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "openai",
|
||||
origin: "bundled",
|
||||
cliBackends: ["Codex-CLI", "legacy-openai-cli"],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -55,6 +61,11 @@ describe("setup-registry runtime fallback", () => {
|
||||
expect(resolvePluginSetupCliBackendRuntime({ backend: "disabled-cli" })).toBeUndefined();
|
||||
expect(loadPluginRegistrySnapshotMock).toHaveBeenCalledTimes(1);
|
||||
expect(loadPluginRegistrySnapshotMock).toHaveBeenCalledWith({ cache: true });
|
||||
expect(loadPluginManifestRegistryForInstalledIndexMock).toHaveBeenCalledWith({
|
||||
index: expect.objectContaining({
|
||||
plugins: expect.arrayContaining([expect.objectContaining({ pluginId: "openai" })]),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves fail-closed setup lookup when the runtime module explicitly declines to resolve", async () => {
|
||||
@@ -65,9 +76,6 @@ describe("setup-registry runtime fallback", () => {
|
||||
pluginId: "openai",
|
||||
origin: "bundled",
|
||||
enabled: true,
|
||||
contributions: {
|
||||
cliBackends: ["Codex-CLI", "legacy-openai-cli"],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import { loadPluginRegistrySnapshot } from "./plugin-registry.js";
|
||||
|
||||
type SetupRegistryRuntimeModule = Pick<
|
||||
@@ -34,20 +35,21 @@ function resolveBundledSetupCliBackends(): SetupCliBackendRuntimeEntry[] {
|
||||
if (bundledSetupCliBackendsCache) {
|
||||
return bundledSetupCliBackendsCache;
|
||||
}
|
||||
bundledSetupCliBackendsCache = loadPluginRegistrySnapshot({ cache: true }).plugins.flatMap(
|
||||
(plugin) => {
|
||||
if (plugin.origin !== "bundled" || !plugin.enabled) {
|
||||
return [];
|
||||
}
|
||||
return plugin.contributions.cliBackends.map(
|
||||
(backendId) =>
|
||||
({
|
||||
pluginId: plugin.pluginId,
|
||||
backend: { id: backendId },
|
||||
}) satisfies SetupCliBackendRuntimeEntry,
|
||||
);
|
||||
},
|
||||
);
|
||||
const index = loadPluginRegistrySnapshot({ cache: true });
|
||||
bundledSetupCliBackendsCache = loadPluginManifestRegistryForInstalledIndex({
|
||||
index,
|
||||
}).plugins.flatMap((plugin) => {
|
||||
if (plugin.origin !== "bundled") {
|
||||
return [];
|
||||
}
|
||||
return [...plugin.cliBackends, ...(plugin.setup?.cliBackends ?? [])].map(
|
||||
(backendId) =>
|
||||
({
|
||||
pluginId: plugin.id,
|
||||
backend: { id: backendId },
|
||||
}) satisfies SetupCliBackendRuntimeEntry,
|
||||
);
|
||||
});
|
||||
return bundledSetupCliBackendsCache;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
type PluginInspectShape,
|
||||
} from "./inspect-shape.js";
|
||||
import { loadOpenClawPlugins } from "./loader.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||
import type { PluginDiagnostic } from "./manifest-types.js";
|
||||
import {
|
||||
loadPluginRegistrySnapshotWithMetadata,
|
||||
@@ -155,6 +157,7 @@ type PluginReportParams = {
|
||||
|
||||
function buildPluginRecordFromInstalledIndex(
|
||||
plugin: import("./installed-plugin-index.js").InstalledPluginIndexRecord,
|
||||
manifest?: PluginManifestRecord,
|
||||
): PluginRecord {
|
||||
return {
|
||||
id: plugin.pluginId,
|
||||
@@ -168,9 +171,9 @@ function buildPluginRecordFromInstalledIndex(
|
||||
status: plugin.enabled ? "loaded" : "disabled",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [...plugin.contributions.channels],
|
||||
cliBackendIds: [...plugin.contributions.cliBackends],
|
||||
providerIds: [...plugin.contributions.providers],
|
||||
channelIds: [...(manifest?.channels ?? [])],
|
||||
cliBackendIds: [...(manifest?.cliBackends ?? []), ...(manifest?.setup?.cliBackends ?? [])],
|
||||
providerIds: [...(manifest?.providers ?? [])],
|
||||
speechProviderIds: [],
|
||||
realtimeTranscriptionProviderIds: [],
|
||||
realtimeVoiceProviderIds: [],
|
||||
@@ -186,7 +189,7 @@ function buildPluginRecordFromInstalledIndex(
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
gatewayDiscoveryServiceIds: [],
|
||||
commands: [...plugin.contributions.commandAliases],
|
||||
commands: [...(manifest?.commandAliases?.map((alias) => alias.name) ?? [])],
|
||||
httpRoutes: 0,
|
||||
hookCount: 0,
|
||||
configSchema: false,
|
||||
@@ -203,10 +206,20 @@ export function buildPluginRegistrySnapshotReport(
|
||||
env: params?.env,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
});
|
||||
const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({
|
||||
index: result.snapshot,
|
||||
config,
|
||||
env: params?.env,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
includeDisabled: true,
|
||||
});
|
||||
const manifestByPluginId = new Map(manifestRegistry.plugins.map((plugin) => [plugin.id, plugin]));
|
||||
return {
|
||||
workspaceDir: params?.workspaceDir,
|
||||
...createEmptyPluginRegistry(),
|
||||
plugins: result.snapshot.plugins.map(buildPluginRecordFromInstalledIndex),
|
||||
plugins: result.snapshot.plugins.map((plugin) =>
|
||||
buildPluginRecordFromInstalledIndex(plugin, manifestByPluginId.get(plugin.pluginId)),
|
||||
),
|
||||
diagnostics: [...result.snapshot.diagnostics],
|
||||
registrySource: result.source,
|
||||
registryDiagnostics: result.diagnostics,
|
||||
|
||||
@@ -122,16 +122,6 @@ describe("security audit install metadata findings", () => {
|
||||
rootDir: path.join(stateDir, "extensions", pluginId),
|
||||
origin: "global" as const,
|
||||
enabled: true,
|
||||
contributions: {
|
||||
providers: [],
|
||||
channels: [],
|
||||
channelConfigs: [],
|
||||
setupProviders: [],
|
||||
cliBackends: [],
|
||||
modelCatalogProviders: [],
|
||||
commandAliases: [],
|
||||
contracts: [],
|
||||
},
|
||||
startup: {
|
||||
sidecar: true,
|
||||
memory: false,
|
||||
|
||||
Reference in New Issue
Block a user