mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
refactor: route bundled catalogs through plugin registry
This commit is contained in:
@@ -27,13 +27,35 @@ import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
|
||||
import { listBundledChannelCatalogEntries } from "./bundled-channel-catalog-read.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const originalTrustBundledPluginsDir = process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
|
||||
}
|
||||
if (originalTrustBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = originalTrustBundledPluginsDir;
|
||||
}
|
||||
cleanupTempDirs(tempDirs);
|
||||
vi.restoreAllMocks();
|
||||
vi.mocked(resolveBundledPluginsDir).mockReset();
|
||||
});
|
||||
|
||||
function useBundledPluginsDir(extensionsRoot: string | undefined): void {
|
||||
if (extensionsRoot) {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = extensionsRoot;
|
||||
process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = "1";
|
||||
} else {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
}
|
||||
vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot);
|
||||
}
|
||||
|
||||
function seedRoot(prefix: string): string {
|
||||
const root = makeTempRepoRoot(tempDirs, prefix);
|
||||
writeJsonFile(path.join(root, "package.json"), { name: "openclaw" });
|
||||
@@ -45,6 +67,7 @@ function seedChannelPkg(
|
||||
pkgJsonPath: string,
|
||||
opts: { id: string; docsPath: string; label?: string; blurb?: string },
|
||||
): void {
|
||||
const pluginDir = path.dirname(pkgJsonPath);
|
||||
writeJsonFile(pkgJsonPath, {
|
||||
name: `@openclaw/${opts.id}`,
|
||||
openclaw: {
|
||||
@@ -56,6 +79,12 @@ function seedChannelPkg(
|
||||
},
|
||||
},
|
||||
});
|
||||
writeJsonFile(path.join(pluginDir, "openclaw.plugin.json"), {
|
||||
id: opts.id,
|
||||
configSchema: { type: "object" },
|
||||
channels: [opts.id],
|
||||
});
|
||||
fs.writeFileSync(path.join(pluginDir, "index.js"), "export default { register() {} };\n", "utf8");
|
||||
}
|
||||
|
||||
describe("listBundledChannelCatalogEntries", () => {
|
||||
@@ -75,7 +104,7 @@ describe("listBundledChannelCatalogEntries", () => {
|
||||
id: "imessage",
|
||||
docsPath: "/channels/imessage",
|
||||
});
|
||||
vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot);
|
||||
useBundledPluginsDir(extensionsRoot);
|
||||
|
||||
const entries = listBundledChannelCatalogEntries();
|
||||
|
||||
@@ -109,7 +138,7 @@ describe("listBundledChannelCatalogEntries", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot);
|
||||
useBundledPluginsDir(extensionsRoot);
|
||||
|
||||
const entries = listBundledChannelCatalogEntries();
|
||||
expect(entries.map((entry) => entry.id)).toEqual(expect.arrayContaining(["qqbot", "telegram"]));
|
||||
@@ -136,7 +165,7 @@ describe("listBundledChannelCatalogEntries", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
vi.mocked(resolveBundledPluginsDir).mockReturnValue(undefined);
|
||||
useBundledPluginsDir(undefined);
|
||||
|
||||
const entries = listBundledChannelCatalogEntries();
|
||||
expect(entries.map((entry) => entry.id)).toContain("fallback-channel");
|
||||
@@ -165,7 +194,7 @@ describe("listBundledChannelCatalogEntries", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot);
|
||||
useBundledPluginsDir(extensionsRoot);
|
||||
|
||||
const entries = listBundledChannelCatalogEntries();
|
||||
expect(entries.map((entry) => entry.id)).toContain("fallback-channel");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
|
||||
import { listChannelCatalogEntries } from "../plugins/channel-catalog-registry.js";
|
||||
import type { PluginPackageChannel } from "../plugins/manifest.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
|
||||
@@ -27,43 +27,8 @@ function listPackageRoots(): string[] {
|
||||
].filter((entry, index, all): entry is string => Boolean(entry) && all.indexOf(entry) === index);
|
||||
}
|
||||
|
||||
function listBundledExtensionPackageJsonPaths(env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
// Delegate to the plugin loader's resolver so channel metadata stays in lock
|
||||
// step with whichever bundled plugin tree is actually loaded at runtime
|
||||
// (source extensions/ in dev/test, dist/extensions in published installs,
|
||||
// dist-runtime/extensions when paired with dist, etc.). See
|
||||
// src/plugins/bundled-dir.ts for the full candidate-order policy and
|
||||
// src/plugins/bundled-dir.test.ts for the precedence coverage. Reusing the
|
||||
// resolver also picks up OPENCLAW_BUNDLED_PLUGINS_DIR overrides and the
|
||||
// bun --compile sibling layout for free.
|
||||
const extensionsRoot = resolveBundledPluginsDir(env);
|
||||
if (!extensionsRoot) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
return fs
|
||||
.readdirSync(extensionsRoot, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(extensionsRoot, entry.name, "package.json"))
|
||||
.filter((entry) => fs.existsSync(entry));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function readBundledExtensionCatalogEntriesSync(): ChannelCatalogEntryLike[] {
|
||||
const entries: ChannelCatalogEntryLike[] = [];
|
||||
for (const packageJsonPath of listBundledExtensionPackageJsonPaths()) {
|
||||
try {
|
||||
const payload = JSON.parse(
|
||||
fs.readFileSync(packageJsonPath, "utf8"),
|
||||
) as ChannelCatalogEntryLike;
|
||||
entries.push(payload);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
function readBundledExtensionCatalogEntriesSync(): PluginPackageChannel[] {
|
||||
return listChannelCatalogEntries({ origin: "bundled" }).map((entry) => entry.channel);
|
||||
}
|
||||
|
||||
function readOfficialCatalogFileSync(): ChannelCatalogEntryLike[] {
|
||||
@@ -84,8 +49,18 @@ function readOfficialCatalogFileSync(): ChannelCatalogEntryLike[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
function toBundledChannelEntry(entry: ChannelCatalogEntryLike): BundledChannelCatalogEntry | null {
|
||||
const channel = entry.openclaw?.channel;
|
||||
function isChannelCatalogEntryLike(
|
||||
entry: ChannelCatalogEntryLike | PluginPackageChannel,
|
||||
): entry is ChannelCatalogEntryLike {
|
||||
return "openclaw" in entry;
|
||||
}
|
||||
|
||||
function toBundledChannelEntry(
|
||||
entry: ChannelCatalogEntryLike | PluginPackageChannel,
|
||||
): BundledChannelCatalogEntry | null {
|
||||
const channel: PluginPackageChannel | undefined = isChannelCatalogEntryLike(entry)
|
||||
? entry.openclaw?.channel
|
||||
: entry;
|
||||
const id = normalizeOptionalLowercaseString(channel?.id);
|
||||
if (!id || !channel) {
|
||||
return null;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { BundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
|
||||
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import type { PluginManifestChannelConfig } from "../plugins/manifest.js";
|
||||
|
||||
const listBundledPluginMetadataMock = vi.hoisted(() =>
|
||||
vi.fn<(options?: unknown) => readonly BundledPluginMetadata[]>(() => []),
|
||||
const loadPluginManifestRegistryMock = vi.hoisted(() =>
|
||||
vi.fn<(options?: Record<string, unknown>) => PluginManifestRegistry>(() => ({
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
})),
|
||||
);
|
||||
const collectBundledChannelConfigsMock = vi.hoisted(() =>
|
||||
vi.fn<(params: unknown) => Record<string, PluginManifestChannelConfig> | undefined>(
|
||||
@@ -14,10 +17,15 @@ const collectBundledChannelConfigsMock = vi.hoisted(() =>
|
||||
|
||||
describe("ChannelsSchema bundled runtime loading", () => {
|
||||
beforeEach(() => {
|
||||
listBundledPluginMetadataMock.mockClear();
|
||||
loadPluginManifestRegistryMock.mockClear();
|
||||
loadPluginManifestRegistryMock.mockReturnValue({
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
collectBundledChannelConfigsMock.mockClear();
|
||||
vi.doMock("../plugins/bundled-plugin-metadata.js", () => ({
|
||||
listBundledPluginMetadata: (options?: unknown) => listBundledPluginMetadataMock(options),
|
||||
vi.doMock("../plugins/plugin-registry.js", () => ({
|
||||
loadPluginManifestRegistryForPluginRegistry: (options?: Record<string, unknown>) =>
|
||||
loadPluginManifestRegistryMock(options),
|
||||
}));
|
||||
vi.doMock("../plugins/bundled-channel-config-metadata.js", () => ({
|
||||
collectBundledChannelConfigs: (params: unknown) => collectBundledChannelConfigsMock(params),
|
||||
@@ -42,18 +50,20 @@ describe("ChannelsSchema bundled runtime loading", () => {
|
||||
});
|
||||
|
||||
expect(parsed?.defaults?.groupPolicy).toBe("open");
|
||||
expect(listBundledPluginMetadataMock).not.toHaveBeenCalledWith(
|
||||
expect(loadPluginManifestRegistryMock).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
includeChannelConfigs: true,
|
||||
bundledChannelConfigCollector: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads bundled channel runtime discovery only when plugin-owned channel config is present", async () => {
|
||||
listBundledPluginMetadataMock.mockReturnValueOnce([
|
||||
{
|
||||
dirName: "discord",
|
||||
manifest: {
|
||||
loadPluginManifestRegistryMock.mockReturnValueOnce({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "discord",
|
||||
origin: "bundled",
|
||||
channels: ["discord"],
|
||||
channelConfigs: {
|
||||
discord: {
|
||||
@@ -62,9 +72,9 @@ describe("ChannelsSchema bundled runtime loading", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as BundledPluginMetadata,
|
||||
]);
|
||||
} as unknown as PluginManifestRegistry["plugins"][number],
|
||||
],
|
||||
});
|
||||
|
||||
const runtime = await importFreshModule<typeof import("./zod-schema.providers.js")>(
|
||||
import.meta.url,
|
||||
@@ -75,24 +85,16 @@ describe("ChannelsSchema bundled runtime loading", () => {
|
||||
discord: {},
|
||||
});
|
||||
|
||||
expect(listBundledPluginMetadataMock.mock.calls).toContainEqual([
|
||||
expect(loadPluginManifestRegistryMock.mock.calls).toContainEqual([
|
||||
expect.objectContaining({
|
||||
includeChannelConfigs: false,
|
||||
includeSyntheticChannelConfigs: false,
|
||||
includeDisabled: true,
|
||||
bundledChannelConfigCollector: expect.any(Function),
|
||||
}),
|
||||
]);
|
||||
expect(collectBundledChannelConfigsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads a single plugin-owned runtime surface when the manifest omits runtime metadata", async () => {
|
||||
listBundledPluginMetadataMock.mockReturnValueOnce([
|
||||
{
|
||||
dirName: "discord",
|
||||
manifest: {
|
||||
channels: ["discord"],
|
||||
},
|
||||
} as unknown as BundledPluginMetadata,
|
||||
]);
|
||||
collectBundledChannelConfigsMock.mockReturnValueOnce({
|
||||
discord: {
|
||||
schema: {},
|
||||
@@ -101,6 +103,24 @@ describe("ChannelsSchema bundled runtime loading", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
loadPluginManifestRegistryMock.mockImplementationOnce((options) => ({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "discord",
|
||||
origin: "bundled",
|
||||
channels: ["discord"],
|
||||
channelConfigs: (
|
||||
options?.bundledChannelConfigCollector as
|
||||
| ((params: unknown) => Record<string, PluginManifestChannelConfig> | undefined)
|
||||
| undefined
|
||||
)?.({
|
||||
pluginDir: "/repo/extensions/discord",
|
||||
manifest: { id: "discord", channels: ["discord"] },
|
||||
}),
|
||||
} as unknown as PluginManifestRegistry["plugins"][number],
|
||||
],
|
||||
}));
|
||||
|
||||
const runtime = await importFreshModule<typeof import("./zod-schema.providers.js")>(
|
||||
import.meta.url,
|
||||
@@ -111,10 +131,10 @@ describe("ChannelsSchema bundled runtime loading", () => {
|
||||
discord: {},
|
||||
});
|
||||
|
||||
expect(listBundledPluginMetadataMock.mock.calls).toContainEqual([
|
||||
expect(loadPluginManifestRegistryMock.mock.calls).toContainEqual([
|
||||
expect.objectContaining({
|
||||
includeChannelConfigs: false,
|
||||
includeSyntheticChannelConfigs: false,
|
||||
includeDisabled: true,
|
||||
bundledChannelConfigCollector: expect.any(Function),
|
||||
}),
|
||||
]);
|
||||
expect(collectBundledChannelConfigsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { z } from "zod";
|
||||
import type { ChannelConfigRuntimeSchema } from "../channels/plugins/types.config.js";
|
||||
import { collectBundledChannelConfigs } from "../plugins/bundled-channel-config-metadata.js";
|
||||
import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
|
||||
import { resolveLoaderPackageRoot } from "../plugins/sdk-alias.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js";
|
||||
import type { ChannelsConfig } from "./types.channels.js";
|
||||
import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js";
|
||||
import { ContextVisibilityModeSchema, GroupPolicySchema } from "./zod-schema.core.js";
|
||||
@@ -17,36 +13,12 @@ const ChannelModelByChannelSchema = z
|
||||
.record(z.string(), z.record(z.string(), z.string()))
|
||||
.optional();
|
||||
|
||||
const OPENCLAW_PACKAGE_ROOT =
|
||||
resolveLoaderPackageRoot({
|
||||
modulePath: fileURLToPath(import.meta.url),
|
||||
moduleUrl: import.meta.url,
|
||||
}) ?? fileURLToPath(new URL("../..", import.meta.url));
|
||||
|
||||
function getDirectChannelRuntimeSchema(channelId: string): ChannelConfigRuntimeSchema | undefined {
|
||||
for (const entry of listBundledPluginMetadata({
|
||||
includeChannelConfigs: false,
|
||||
includeSyntheticChannelConfigs: false,
|
||||
})) {
|
||||
const manifestRuntime = entry.manifest.channelConfigs?.[channelId]?.runtime;
|
||||
if (manifestRuntime) {
|
||||
return manifestRuntime;
|
||||
}
|
||||
if (!entry.manifest.channels?.includes(channelId)) {
|
||||
continue;
|
||||
}
|
||||
const collectedChannelConfigs = collectBundledChannelConfigs({
|
||||
pluginDir: path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions", entry.dirName),
|
||||
manifest: entry.manifest,
|
||||
...(entry.packageManifest ? { packageManifest: entry.packageManifest } : {}),
|
||||
});
|
||||
const collectedRuntime = collectedChannelConfigs?.[channelId]?.runtime;
|
||||
if (collectedRuntime) {
|
||||
return collectedRuntime;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
function getDirectChannelRuntimeSchema(channelId: string) {
|
||||
return loadPluginManifestRegistryForPluginRegistry({
|
||||
includeDisabled: true,
|
||||
bundledChannelConfigCollector: collectBundledChannelConfigs,
|
||||
}).plugins.find((plugin) => plugin.origin === "bundled" && plugin.channelConfigs?.[channelId])
|
||||
?.channelConfigs?.[channelId]?.runtime;
|
||||
}
|
||||
|
||||
function hasPluginOwnedChannelConfig(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanupTempDirs, makeTempRepoRoot, writeJsonFile } from "../../test/helpers/temp-repo.js";
|
||||
@@ -10,13 +11,31 @@ import { resolveBundledPluginsDir } from "./bundled-dir.js";
|
||||
import { findBundledPackageChannelMetadata } from "./bundled-package-channel-metadata.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const originalTrustBundledPluginsDir = process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
|
||||
}
|
||||
if (originalTrustBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = originalTrustBundledPluginsDir;
|
||||
}
|
||||
cleanupTempDirs(tempDirs);
|
||||
vi.restoreAllMocks();
|
||||
vi.mocked(resolveBundledPluginsDir).mockReset();
|
||||
});
|
||||
|
||||
function useBundledPluginsDir(extensionsRoot: string): void {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = extensionsRoot;
|
||||
process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = "1";
|
||||
vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot);
|
||||
}
|
||||
|
||||
describe("bundled package channel metadata", () => {
|
||||
it("reads doctor capabilities from the resolved bundled plugin dir", () => {
|
||||
const root = makeTempRepoRoot(tempDirs, "bpcm-");
|
||||
@@ -37,7 +56,17 @@ describe("bundled package channel metadata", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot);
|
||||
writeJsonFile(path.join(extensionsRoot, "matrix", "openclaw.plugin.json"), {
|
||||
id: "matrix",
|
||||
configSchema: { type: "object" },
|
||||
channels: ["matrix"],
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(extensionsRoot, "matrix", "index.js"),
|
||||
"export default {};\n",
|
||||
"utf8",
|
||||
);
|
||||
useBundledPluginsDir(extensionsRoot);
|
||||
|
||||
const matrix = findBundledPackageChannelMetadata("matrix");
|
||||
|
||||
@@ -53,7 +82,7 @@ describe("bundled package channel metadata", () => {
|
||||
const root = makeTempRepoRoot(tempDirs, "bpcm-fresh-");
|
||||
const extensionsRoot = path.join(root, "dist", "extensions");
|
||||
const packagePath = path.join(extensionsRoot, "matrix", "package.json");
|
||||
vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot);
|
||||
useBundledPluginsDir(extensionsRoot);
|
||||
|
||||
writeJsonFile(packagePath, {
|
||||
name: "@openclaw/matrix",
|
||||
@@ -64,6 +93,16 @@ describe("bundled package channel metadata", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
writeJsonFile(path.join(extensionsRoot, "matrix", "openclaw.plugin.json"), {
|
||||
id: "matrix",
|
||||
configSchema: { type: "object" },
|
||||
channels: ["matrix"],
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(extensionsRoot, "matrix", "index.js"),
|
||||
"export default {};\n",
|
||||
"utf8",
|
||||
);
|
||||
expect(findBundledPackageChannelMetadata("matrix")?.label).toBe("Before");
|
||||
|
||||
writeJsonFile(packagePath, {
|
||||
|
||||
@@ -1,35 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveBundledPluginsDir } from "./bundled-dir.js";
|
||||
import {
|
||||
getPackageManifestMetadata,
|
||||
type PackageManifest,
|
||||
type PluginPackageChannel,
|
||||
} from "./manifest.js";
|
||||
|
||||
function readPackageManifest(pluginDir: string): PackageManifest | undefined {
|
||||
const packagePath = path.join(pluginDir, "package.json");
|
||||
if (!fs.existsSync(packagePath)) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(packagePath, "utf-8")) as PackageManifest;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
import { listChannelCatalogEntries } from "./channel-catalog-registry.js";
|
||||
import type { PluginPackageChannel } from "./manifest.js";
|
||||
|
||||
export function listBundledPackageChannelMetadata(): readonly PluginPackageChannel[] {
|
||||
const scanDir = resolveBundledPluginsDir();
|
||||
if (!scanDir || !fs.existsSync(scanDir)) {
|
||||
return [];
|
||||
}
|
||||
return fs
|
||||
.readdirSync(scanDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => readPackageManifest(path.join(scanDir, entry.name)))
|
||||
.map((manifest) => getPackageManifestMetadata(manifest)?.channel)
|
||||
.filter((channel): channel is PluginPackageChannel => Boolean(channel?.id));
|
||||
return listChannelCatalogEntries({ origin: "bundled" }).map((entry) => entry.channel);
|
||||
}
|
||||
|
||||
export function findBundledPackageChannelMetadata(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
listExplicitlyDisabledChannelIdsForConfig,
|
||||
listPotentialConfiguredChannelIds,
|
||||
@@ -7,7 +5,6 @@ import {
|
||||
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import { resolveBundledPluginsDir } from "./bundled-dir.js";
|
||||
import {
|
||||
listExplicitConfiguredChannelIdsForConfig,
|
||||
resolveConfiguredChannelPluginIds,
|
||||
@@ -15,7 +12,7 @@ import {
|
||||
} from "./channel-plugin-ids.js";
|
||||
import { normalizePluginsConfig } from "./config-state.js";
|
||||
import { passesManifestOwnerBasePolicy } from "./manifest-owner-policy.js";
|
||||
import { loadPluginManifest } from "./manifest.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
|
||||
function collectConfiguredChannelIds(
|
||||
config: OpenClawConfig,
|
||||
@@ -45,6 +42,7 @@ function collectBundledChannelOwnerPluginIds(params: {
|
||||
config: OpenClawConfig;
|
||||
channelIds: readonly string[];
|
||||
env: NodeJS.ProcessEnv;
|
||||
workspaceDir?: string;
|
||||
bundledPluginsDir?: string;
|
||||
}): string[] {
|
||||
const plugins = normalizePluginsConfig(params.config.plugins);
|
||||
@@ -56,32 +54,32 @@ function collectBundledChannelOwnerPluginIds(params: {
|
||||
if (channelIds.size === 0) {
|
||||
return [];
|
||||
}
|
||||
const bundledDir = params.bundledPluginsDir ?? resolveBundledPluginsDir(params.env);
|
||||
if (!bundledDir) {
|
||||
return [];
|
||||
}
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(bundledDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const env = params.bundledPluginsDir
|
||||
? {
|
||||
...params.env,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: params.bundledPluginsDir,
|
||||
...(params.env.VITEST || process.env.VITEST
|
||||
? { OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1" }
|
||||
: {}),
|
||||
}
|
||||
: params.env;
|
||||
const registry = loadPluginManifestRegistryForPluginRegistry({
|
||||
config: params.config,
|
||||
env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
includeDisabled: true,
|
||||
});
|
||||
const pluginIds = new Set<string>();
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const pluginDir = path.join(bundledDir, entry.name);
|
||||
const manifest = loadPluginManifest(pluginDir, false);
|
||||
if (!manifest.ok) {
|
||||
for (const plugin of registry.plugins) {
|
||||
if (plugin.origin !== "bundled") {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(manifest.manifest.channels ?? []).some((channelId) =>
|
||||
plugin.channels.some((channelId) =>
|
||||
channelIds.has(normalizeOptionalLowercaseString(channelId) ?? ""),
|
||||
)
|
||||
) {
|
||||
const pluginId = normalizeOptionalLowercaseString(manifest.manifest.id);
|
||||
const pluginId = normalizeOptionalLowercaseString(plugin.id);
|
||||
if (
|
||||
pluginId &&
|
||||
passesManifestOwnerBasePolicy({
|
||||
@@ -152,6 +150,7 @@ export function resolveEffectivePluginIds(params: {
|
||||
config: effectiveConfig,
|
||||
channelIds: configuredChannelIds,
|
||||
env: params.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
...(params.bundledPluginsDir ? { bundledPluginsDir: params.bundledPluginsDir } : {}),
|
||||
})) {
|
||||
ids.add(pluginId);
|
||||
|
||||
@@ -7,7 +7,20 @@ import type { ModelProviderConfig } from "../config/types.models.js";
|
||||
import { resolveBundledProviderPolicySurface } from "./provider-public-artifacts.js";
|
||||
|
||||
describe("provider public artifacts", () => {
|
||||
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const originalTrustBundledPluginsDir = process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
|
||||
}
|
||||
if (originalTrustBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = originalTrustBundledPluginsDir;
|
||||
}
|
||||
vi.doUnmock("./bundled-dir.js");
|
||||
vi.doUnmock("./public-surface-loader.js");
|
||||
vi.resetModules();
|
||||
@@ -36,7 +49,16 @@ describe("provider public artifacts", () => {
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({ providers: ["openai", "openai-codex"] }),
|
||||
JSON.stringify({
|
||||
id: "openai",
|
||||
configSchema: { type: "object" },
|
||||
providers: ["openai", "openai-codex"],
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "index.js"),
|
||||
"export default { register() {} };\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const resolveThinkingProfile = vi.fn(({ modelId }: { modelId: string }) => ({
|
||||
@@ -52,6 +74,8 @@ describe("provider public artifacts", () => {
|
||||
vi.doMock("./bundled-dir.js", () => ({
|
||||
resolveBundledPluginsDir: () => bundledPluginsDir,
|
||||
}));
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledPluginsDir;
|
||||
process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = "1";
|
||||
vi.doMock("./public-surface-loader.js", () => ({
|
||||
loadBundledPluginPublicArtifactModuleSync,
|
||||
}));
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type { ModelProviderConfig } from "../config/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveBundledPluginsDir } from "./bundled-dir.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type {
|
||||
ProviderApplyConfigDefaultsContext,
|
||||
ProviderNormalizeConfigContext,
|
||||
@@ -76,34 +75,24 @@ function resolveBundledProviderPolicyPluginId(providerId: string): string | null
|
||||
return providerPolicyPluginIdsByProviderId.get(cacheKey) ?? null;
|
||||
}
|
||||
|
||||
if (!bundledPluginsDir || !fs.existsSync(bundledPluginsDir)) {
|
||||
if (!bundledPluginsDir) {
|
||||
providerPolicyPluginIdsByProviderId.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const entry of fs
|
||||
.readdirSync(bundledPluginsDir, { withFileTypes: true })
|
||||
.filter((candidate) => candidate.isDirectory())
|
||||
.map((candidate) => candidate.name)
|
||||
.toSorted((left, right) => left.localeCompare(right))) {
|
||||
const manifestPath = path.join(bundledPluginsDir, entry, "openclaw.plugin.json");
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
const registry = loadPluginManifestRegistry();
|
||||
for (const plugin of registry.plugins.toSorted((left, right) =>
|
||||
left.id.localeCompare(right.id),
|
||||
)) {
|
||||
if (plugin.origin !== "bundled") {
|
||||
continue;
|
||||
}
|
||||
let manifest: { providers?: unknown };
|
||||
try {
|
||||
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as { providers?: unknown };
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const providers = Array.isArray(manifest.providers) ? manifest.providers : [];
|
||||
const ownsProvider = providers.some(
|
||||
(candidate) =>
|
||||
typeof candidate === "string" && normalizeProviderId(candidate) === normalizedProviderId,
|
||||
const ownsProvider = plugin.providers.some(
|
||||
(provider) => normalizeProviderId(provider) === normalizedProviderId,
|
||||
);
|
||||
if (ownsProvider) {
|
||||
providerPolicyPluginIdsByProviderId.set(cacheKey, entry);
|
||||
return entry;
|
||||
providerPolicyPluginIdsByProviderId.set(cacheKey, plugin.id);
|
||||
return plugin.id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js";
|
||||
import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js";
|
||||
@@ -70,16 +69,14 @@ function listBundledWebProviderSecretTargetRegistryEntries(): SecretTargetRegist
|
||||
function listBundledPluginConfigSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] {
|
||||
const entries: SecretTargetRegistryEntry[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const record of listBundledPluginMetadata({
|
||||
includeChannelConfigs: false,
|
||||
includeSyntheticChannelConfigs: false,
|
||||
})) {
|
||||
const secretInputs = record.manifest.configContracts?.secretInputs?.paths ?? [];
|
||||
for (const record of loadPluginManifestRegistryForPluginRegistry({ includeDisabled: true })
|
||||
.plugins) {
|
||||
if (record.origin !== "bundled") {
|
||||
continue;
|
||||
}
|
||||
const secretInputs = record.configContracts?.secretInputs?.paths ?? [];
|
||||
for (const secretInput of secretInputs) {
|
||||
const entry = createPluginOpenClawConfigSecretTargetEntry(
|
||||
record.manifest.id,
|
||||
secretInput.path,
|
||||
);
|
||||
const entry = createPluginOpenClawConfigSecretTargetEntry(record.id, secretInput.path);
|
||||
const key = `${entry.configFile}:${entry.pathPattern}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user