refactor: move bundled plugin policy into manifests

This commit is contained in:
Peter Steinberger
2026-03-27 16:38:41 +00:00
parent ed055f44ae
commit ef1784d264
80 changed files with 874 additions and 459 deletions

View File

@@ -1,5 +1,6 @@
{
"id": "amazon-bedrock",
"enabledByDefault": true,
"providers": ["amazon-bedrock"],
"configSchema": {
"type": "object",

View File

@@ -1,5 +1,6 @@
{
"id": "anthropic",
"enabledByDefault": true,
"providers": ["anthropic"],
"cliBackends": ["claude-cli"],
"providerAuthEnvVars": {

View File

@@ -580,6 +580,7 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
id: "brave",
label: "Brave Search",
hint: "Structured results · country/language/time filters",
onboardingScopes: ["text-inference"],
credentialLabel: "Brave Search API key",
envVars: ["BRAVE_API_KEY"],
placeholder: "BSA...",

View File

@@ -1,5 +1,6 @@
{
"id": "byteplus",
"enabledByDefault": true,
"providers": ["byteplus", "byteplus-plan"],
"providerAuthEnvVars": {
"byteplus": ["BYTEPLUS_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "cloudflare-ai-gateway",
"enabledByDefault": true,
"providers": ["cloudflare-ai-gateway"],
"providerAuthEnvVars": {
"cloudflare-ai-gateway": ["CLOUDFLARE_AI_GATEWAY_API_KEY"]

View File

@@ -1,6 +1,8 @@
{
"id": "copilot-proxy",
"enabledByDefault": true,
"providers": ["copilot-proxy"],
"autoEnableWhenConfiguredProviders": ["copilot-proxy"],
"providerAuthChoices": [
{
"provider": "copilot-proxy",

View File

@@ -1,5 +1,6 @@
{
"id": "deepseek",
"enabledByDefault": true,
"providers": ["deepseek"],
"providerAuthEnvVars": {
"deepseek": ["DEEPSEEK_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "device-pair",
"enabledByDefault": true,
"name": "Device Pairing",
"description": "Generate setup codes and approve device pairing requests.",
"configSchema": {

View File

@@ -1,5 +1,6 @@
{
"id": "fal",
"enabledByDefault": true,
"providers": ["fal"],
"providerAuthEnvVars": {
"fal": ["FAL_KEY"]

View File

@@ -28,6 +28,7 @@ export function createFirecrawlWebSearchProvider(): WebSearchProviderPlugin {
id: "firecrawl",
label: "Firecrawl Search",
hint: "Structured results with optional result scraping",
onboardingScopes: ["text-inference"],
credentialLabel: "Firecrawl API key",
envVars: ["FIRECRAWL_API_KEY"],
placeholder: "fc-...",

View File

@@ -1,5 +1,6 @@
{
"id": "github-copilot",
"enabledByDefault": true,
"providers": ["github-copilot"],
"providerAuthEnvVars": {
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]

View File

@@ -1,6 +1,8 @@
{
"id": "google",
"enabledByDefault": true,
"providers": ["google", "google-gemini-cli"],
"autoEnableWhenConfiguredProviders": ["google-gemini-cli"],
"cliBackends": ["google-gemini-cli"],
"providerAuthEnvVars": {
"google": ["GEMINI_API_KEY", "GOOGLE_API_KEY"]

View File

@@ -247,6 +247,7 @@ export function createGeminiWebSearchProvider(): WebSearchProviderPlugin {
id: "gemini",
label: "Gemini (Google Search)",
hint: "Requires Google Gemini API key · Google Search grounding",
onboardingScopes: ["text-inference"],
credentialLabel: "Google Gemini API key",
envVars: ["GEMINI_API_KEY"],
placeholder: "AIza...",

View File

@@ -1,5 +1,6 @@
{
"id": "huggingface",
"enabledByDefault": true,
"providers": ["huggingface"],
"providerAuthEnvVars": {
"huggingface": ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"]

View File

@@ -1,5 +1,6 @@
{
"id": "kilocode",
"enabledByDefault": true,
"providers": ["kilocode"],
"providerAuthEnvVars": {
"kilocode": ["KILOCODE_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "kimi",
"enabledByDefault": true,
"providers": ["kimi", "kimi-coding"],
"providerAuthEnvVars": {
"kimi": ["KIMI_API_KEY", "KIMICODE_API_KEY"],

View File

@@ -1,5 +1,6 @@
{
"id": "litellm",
"enabledByDefault": true,
"providers": ["litellm"],
"providerAuthEnvVars": {
"litellm": ["LITELLM_API_KEY"]

View File

@@ -0,0 +1,2 @@
export { matrixPlugin } from "./src/channel.js";
export { setMatrixRuntime } from "./src/runtime.js";

View File

@@ -0,0 +1 @@
export type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core";

View File

@@ -1 +1,6 @@
export { getMemorySearchManager, MemoryIndexManager } from "./src/memory/index.js";
export {
getBuiltinMemoryEmbeddingProviderDoctorMetadata,
listBuiltinAutoSelectMemoryEmbeddingProviderDoctorMetadata,
} from "./src/memory/provider-adapters.js";
export type { BuiltinMemoryEmbeddingProviderDoctorMetadata } from "./src/memory/provider-adapters.js";

View File

@@ -22,6 +22,15 @@ import {
type MemoryEmbeddingProviderAdapter,
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { resolveUserPath } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
import { getProviderEnvVars } from "openclaw/plugin-sdk/provider-env-vars";
export type BuiltinMemoryEmbeddingProviderDoctorMetadata = {
providerId: string;
authProviderId: string;
envVars: string[];
transport: "local" | "remote";
autoSelectPriority?: number;
};
function formatErrorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
@@ -107,6 +116,10 @@ function supportsGeminiMultimodalEmbeddings(model: string): boolean {
return normalized === "gemini-embedding-2-preview";
}
function resolveMemoryEmbeddingAuthProviderId(providerId: string): string {
return providerId === "gemini" ? "google" : providerId;
}
const openAiAdapter: MemoryEmbeddingProviderAdapter = {
id: "openai",
defaultModel: DEFAULT_OPENAI_EMBEDDING_MODEL,
@@ -356,6 +369,36 @@ export function registerBuiltInMemoryEmbeddingProviders(register: {
}
}
export function getBuiltinMemoryEmbeddingProviderDoctorMetadata(
providerId: string,
): BuiltinMemoryEmbeddingProviderDoctorMetadata | null {
const adapter = getBuiltinMemoryEmbeddingProviderAdapter(providerId);
if (!adapter) {
return null;
}
const authProviderId = resolveMemoryEmbeddingAuthProviderId(adapter.id);
return {
providerId: adapter.id,
authProviderId,
envVars: getProviderEnvVars(authProviderId),
transport: adapter.transport === "local" ? "local" : "remote",
autoSelectPriority: adapter.autoSelectPriority,
};
}
export function listBuiltinAutoSelectMemoryEmbeddingProviderDoctorMetadata(): Array<BuiltinMemoryEmbeddingProviderDoctorMetadata> {
return builtinMemoryEmbeddingProviderAdapters
.filter((adapter) => typeof adapter.autoSelectPriority === "number")
.toSorted((a, b) => (a.autoSelectPriority ?? 0) - (b.autoSelectPriority ?? 0))
.map((adapter) => ({
providerId: adapter.id,
authProviderId: resolveMemoryEmbeddingAuthProviderId(adapter.id),
envVars: getProviderEnvVars(resolveMemoryEmbeddingAuthProviderId(adapter.id)),
transport: adapter.transport === "local" ? "local" : "remote",
autoSelectPriority: adapter.autoSelectPriority,
}));
}
export {
DEFAULT_GEMINI_EMBEDDING_MODEL,
DEFAULT_LOCAL_MODEL,

View File

@@ -1,5 +1,5 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core";
import { expect } from "vitest";
import type { OpenClawConfig } from "../api.js";
import { createMemoryGetTool, createMemorySearchTool } from "./tools.js";
export function asOpenClawConfig(config: Partial<OpenClawConfig>): OpenClawConfig {

View File

@@ -1,6 +1,9 @@
{
"id": "minimax",
"enabledByDefault": true,
"legacyPluginIds": ["minimax-portal-auth"],
"providers": ["minimax", "minimax-portal"],
"autoEnableWhenConfiguredProviders": ["minimax-portal"],
"providerAuthEnvVars": {
"minimax": ["MINIMAX_API_KEY"],
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"]
@@ -20,6 +23,7 @@
"provider": "minimax",
"method": "api-global",
"choiceId": "minimax-global-api",
"deprecatedChoiceIds": ["minimax", "minimax-api", "minimax-cloud", "minimax-api-lightning"],
"choiceLabel": "MiniMax API key (Global)",
"choiceHint": "Global endpoint - api.minimax.io",
"groupId": "minimax",
@@ -44,6 +48,7 @@
"provider": "minimax",
"method": "api-cn",
"choiceId": "minimax-cn-api",
"deprecatedChoiceIds": ["minimax-api-key-cn"],
"choiceLabel": "MiniMax API key (CN)",
"choiceHint": "CN endpoint - api.minimaxi.com",
"groupId": "minimax",

View File

@@ -1,5 +1,6 @@
{
"id": "mistral",
"enabledByDefault": true,
"providers": ["mistral"],
"providerAuthEnvVars": {
"mistral": ["MISTRAL_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "modelstudio",
"enabledByDefault": true,
"providers": ["modelstudio"],
"providerAuthEnvVars": {
"modelstudio": ["MODELSTUDIO_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "moonshot",
"enabledByDefault": true,
"providers": ["moonshot"],
"providerAuthEnvVars": {
"moonshot": ["MOONSHOT_API_KEY"]

View File

@@ -318,6 +318,7 @@ export function createKimiWebSearchProvider(): WebSearchProviderPlugin {
id: "kimi",
label: "Kimi (Moonshot)",
hint: "Requires Moonshot / Kimi API key · Moonshot web search",
onboardingScopes: ["text-inference"],
credentialLabel: "Moonshot / Kimi API key",
envVars: ["KIMI_API_KEY", "MOONSHOT_API_KEY"],
placeholder: "sk-...",

View File

@@ -1,5 +1,6 @@
{
"id": "nvidia",
"enabledByDefault": true,
"providers": ["nvidia"],
"providerAuthEnvVars": {
"nvidia": ["NVIDIA_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "ollama",
"enabledByDefault": true,
"providers": ["ollama"],
"providerAuthEnvVars": {
"ollama": ["OLLAMA_API_KEY"]

View File

@@ -69,4 +69,19 @@ describe("openai codex provider", () => {
expires: expect.any(Number),
});
});
it("returns deprecated-profile doctor guidance for legacy Codex CLI ids", () => {
const provider = buildOpenAICodexProviderPlugin();
expect(
provider.buildAuthDoctorHint?.({
provider: "openai-codex",
profileId: "openai-codex:codex-cli",
config: undefined,
store: { version: 1, profiles: {} },
}),
).toBe(
"Deprecated profile. Run `openclaw models auth login --provider openai-codex` or `openclaw configure`.",
);
});
});

View File

@@ -195,6 +195,13 @@ async function runOpenAICodexOAuth(ctx: ProviderAuthContext) {
});
}
function buildOpenAICodexAuthDoctorHint(ctx: { profileId?: string }) {
if (ctx.profileId !== CODEX_CLI_PROFILE_ID) {
return undefined;
}
return "Deprecated profile. Run `openclaw models auth login --provider openai-codex` or `openclaw configure`.";
}
export function buildOpenAICodexProviderPlugin(): ProviderPlugin {
return {
id: PROVIDER_ID,
@@ -233,6 +240,7 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin {
},
},
resolveDynamicModel: (ctx) => resolveCodexForwardCompatModel(ctx),
buildAuthDoctorHint: (ctx) => buildOpenAICodexAuthDoctorHint(ctx),
capabilities: {
providerFamily: "openai",
},

View File

@@ -1,5 +1,6 @@
{
"id": "openai",
"enabledByDefault": true,
"providers": ["openai", "openai-codex"],
"cliBackends": ["codex-cli"],
"providerAuthEnvVars": {

View File

@@ -1,5 +1,6 @@
{
"id": "opencode-go",
"enabledByDefault": true,
"providers": ["opencode-go"],
"providerAuthEnvVars": {
"opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "opencode",
"enabledByDefault": true,
"providers": ["opencode"],
"providerAuthEnvVars": {
"opencode": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "openrouter",
"enabledByDefault": true,
"providers": ["openrouter"],
"providerAuthEnvVars": {
"openrouter": ["OPENROUTER_API_KEY"]

View File

@@ -654,6 +654,7 @@ export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin {
id: "perplexity",
label: "Perplexity Search",
hint: "Requires Perplexity API key or OpenRouter API key · structured results",
onboardingScopes: ["text-inference"],
credentialLabel: "Perplexity API key",
envVars: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
placeholder: "pplx-...",

View File

@@ -1,5 +1,6 @@
{
"id": "phone-control",
"enabledByDefault": true,
"name": "Phone Control",
"description": "Arm/disarm high-risk phone node commands (camera/screen/writes) with an optional auto-expiry.",
"configSchema": {

View File

@@ -1,5 +1,6 @@
{
"id": "qianfan",
"enabledByDefault": true,
"providers": ["qianfan"],
"providerAuthEnvVars": {
"qianfan": ["QIANFAN_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "sglang",
"enabledByDefault": true,
"providers": ["sglang"],
"providerAuthEnvVars": {
"sglang": ["SGLANG_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "synthetic",
"enabledByDefault": true,
"providers": ["synthetic"],
"providerAuthEnvVars": {
"synthetic": ["SYNTHETIC_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "talk-voice",
"enabledByDefault": true,
"name": "Talk Voice",
"description": "Manage Talk voice selection (list/set).",
"configSchema": {

View File

@@ -28,6 +28,7 @@ export function createTavilyWebSearchProvider(): WebSearchProviderPlugin {
id: "tavily",
label: "Tavily Search",
hint: "Structured results with domain filters and AI answer summaries",
onboardingScopes: ["text-inference"],
credentialLabel: "Tavily API key",
envVars: ["TAVILY_API_KEY"],
placeholder: "tvly-...",

View File

@@ -1,5 +1,6 @@
{
"id": "together",
"enabledByDefault": true,
"providers": ["together"],
"providerAuthEnvVars": {
"together": ["TOGETHER_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "venice",
"enabledByDefault": true,
"providers": ["venice"],
"providerAuthEnvVars": {
"venice": ["VENICE_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "vercel-ai-gateway",
"enabledByDefault": true,
"providers": ["vercel-ai-gateway"],
"providerAuthEnvVars": {
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "vllm",
"enabledByDefault": true,
"providers": ["vllm"],
"providerAuthEnvVars": {
"vllm": ["VLLM_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "volcengine",
"enabledByDefault": true,
"providers": ["volcengine", "volcengine-plan"],
"providerAuthEnvVars": {
"volcengine": ["VOLCANO_ENGINE_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "xai",
"enabledByDefault": true,
"providers": ["xai"],
"providerAuthEnvVars": {
"xai": ["XAI_API_KEY"]

View File

@@ -72,6 +72,7 @@ export function createXaiWebSearchProvider(): WebSearchProviderPlugin {
id: "grok",
label: "Grok (xAI)",
hint: "Requires xAI API key · xAI web-grounded responses",
onboardingScopes: ["text-inference"],
credentialLabel: "xAI API key",
envVars: ["XAI_API_KEY"],
placeholder: "xai-...",

View File

@@ -1,5 +1,6 @@
{
"id": "xiaomi",
"enabledByDefault": true,
"providers": ["xiaomi"],
"providerAuthEnvVars": {
"xiaomi": ["XIAOMI_API_KEY"]

View File

@@ -1,5 +1,6 @@
{
"id": "zai",
"enabledByDefault": true,
"providers": ["zai"],
"providerAuthEnvVars": {
"zai": ["ZAI_API_KEY", "Z_AI_API_KEY"]

View File

@@ -1,6 +1,7 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { collectBundledPluginBuildEntries } from "./lib/bundled-plugin-build-entries.mjs";
import { collectBundledPluginSources } from "./lib/bundled-plugin-source-utils.mjs";
import { formatGeneratedModule } from "./lib/format-generated-module.mjs";
import { writeGeneratedOutput } from "./lib/generated-output-utils.mjs";
@@ -26,6 +27,12 @@ const DEFAULT_BUNDLED_CHANNEL_ENTRY_IDS = [
];
const MANIFEST_KEY = "openclaw";
const FORMATTER_CWD = path.resolve(import.meta.dirname, "..");
const RUNTIME_SIDECAR_PUBLIC_SURFACE_BASENAMES = new Set([
"helper-api.js",
"light-runtime-api.js",
"runtime-api.js",
"thread-bindings-runtime.js",
]);
function rewriteEntryToBuiltPath(entry) {
if (typeof entry !== "string" || entry.trim().length === 0) {
@@ -133,6 +140,16 @@ function normalizePluginManifest(raw) {
id: raw.id.trim(),
configSchema: raw.configSchema,
...(raw.enabledByDefault === true ? { enabledByDefault: true } : {}),
...(normalizeStringList(raw.legacyPluginIds)
? { legacyPluginIds: normalizeStringList(raw.legacyPluginIds) }
: {}),
...(normalizeStringList(raw.autoEnableWhenConfiguredProviders)
? {
autoEnableWhenConfiguredProviders: normalizeStringList(
raw.autoEnableWhenConfiguredProviders,
),
}
: {}),
...(typeof raw.kind === "string" ? { kind: raw.kind.trim() } : {}),
...(normalizeStringList(raw.channels) ? { channels: normalizeStringList(raw.channels) } : {}),
...(normalizeStringList(raw.providers)
@@ -179,6 +196,10 @@ function resolvePackageChannelMeta(packageJson) {
function resolveChannelConfigSchemaModulePath(rootDir) {
const candidates = [
path.join(rootDir, "src", "config-surface.ts"),
path.join(rootDir, "src", "config-surface.js"),
path.join(rootDir, "src", "config-surface.mts"),
path.join(rootDir, "src", "config-surface.mjs"),
path.join(rootDir, "src", "config-schema.ts"),
path.join(rootDir, "src", "config-schema.js"),
path.join(rootDir, "src", "config-schema.mts"),
@@ -311,6 +332,25 @@ function normalizeGeneratedImportPath(dirName, builtPath) {
return `../../extensions/${dirName}/${String(builtPath).replace(/^\.\//u, "")}`;
}
function normalizeEntryPath(entry) {
return String(entry).replace(/^\.\//u, "");
}
function isPublicSurfaceArtifactSourceEntry(entry) {
const baseName = path.posix.basename(normalizeEntryPath(entry));
if (baseName.startsWith("test-")) {
return false;
}
if (baseName.includes(".test-")) {
return false;
}
return !baseName.endsWith(".test.ts") && !baseName.endsWith(".test.js");
}
function isRuntimeSidecarPublicSurfaceArtifact(artifact) {
return RUNTIME_SIDECAR_PUBLIC_SURFACE_BASENAMES.has(path.posix.basename(String(artifact)));
}
function resolveBundledChannelEntries(entries) {
const orderById = new Map(DEFAULT_BUNDLED_CHANNEL_ENTRY_IDS.map((id, index) => [id, index]));
return entries
@@ -329,6 +369,9 @@ function resolveBundledChannelEntries(entries) {
export async function collectBundledPluginMetadata(params = {}) {
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
const buildEntriesById = new Map(
collectBundledPluginBuildEntries({ cwd: repoRoot }).map((entry) => [entry.id, entry]),
);
const entries = [];
for (const source of collectBundledPluginSources({ repoRoot, requirePackageJson: true })) {
const manifest = normalizePluginManifest(source.manifest);
@@ -358,6 +401,27 @@ export async function collectBundledPluginMetadata(params = {}) {
built: rewriteEntryToBuiltPath(packageManifest.setupEntry.trim()),
}
: undefined;
const publicSurfaceArtifacts = (() => {
const buildEntry = buildEntriesById.get(source.dirName);
if (!buildEntry) {
return undefined;
}
const excludedEntries = new Set(
[sourceEntry, setupEntry?.source]
.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
.map(normalizeEntryPath),
);
const artifacts = buildEntry.sourceEntries
.map(normalizeEntryPath)
.filter((entry) => !excludedEntries.has(entry))
.filter(isPublicSurfaceArtifactSourceEntry)
.map(rewriteEntryToBuiltPath)
.filter((entry) => typeof entry === "string" && entry.length > 0)
.toSorted((left, right) => left.localeCompare(right));
return artifacts.length > 0 ? artifacts : undefined;
})();
const runtimeSidecarArtifacts =
publicSurfaceArtifacts?.filter(isRuntimeSidecarPublicSurfaceArtifact) ?? undefined;
const channelConfigs = await collectBundledChannelConfigsForSource({ source, manifest });
if (channelConfigs) {
manifest.channelConfigs = channelConfigs;
@@ -378,6 +442,8 @@ export async function collectBundledPluginMetadata(params = {}) {
...(setupEntry?.built
? { setupSource: { source: setupEntry.source, built: setupEntry.built } }
: {}),
...(publicSurfaceArtifacts ? { publicSurfaceArtifacts } : {}),
...(runtimeSidecarArtifacts?.length ? { runtimeSidecarArtifacts } : {}),
...(typeof packageJson.name === "string" ? { packageName: packageJson.name.trim() } : {}),
...(typeof packageJson.version === "string"
? { packageVersion: packageJson.version.trim() }

View File

@@ -7,12 +7,8 @@ const mocks = vi.hoisted(() => ({
clackSelect: vi.fn(),
clackText: vi.fn(),
clackConfirm: vi.fn(),
applySearchKey: vi.fn(),
applySearchProviderSelection: vi.fn(),
hasExistingKey: vi.fn(),
hasKeyInEnv: vi.fn(),
resolveExistingKey: vi.fn(),
resolveSearchProviderOptions: vi.fn(),
setupSearch: vi.fn(),
readConfigFileSnapshot: vi.fn(),
writeConfigFile: vi.fn(),
resolveGatewayPort: vi.fn(),
@@ -103,23 +99,7 @@ vi.mock("./onboard-channels.js", () => ({
vi.mock("./onboard-search.js", () => ({
resolveSearchProviderOptions: mocks.resolveSearchProviderOptions,
SEARCH_PROVIDER_OPTIONS: [
{
id: "firecrawl",
label: "Firecrawl Search",
hint: "Structured results with optional result scraping",
credentialLabel: "Firecrawl API key",
envVars: ["FIRECRAWL_API_KEY"],
placeholder: "fc-...",
signupUrl: "https://www.firecrawl.dev/",
credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey",
},
],
resolveExistingKey: mocks.resolveExistingKey,
hasExistingKey: mocks.hasExistingKey,
applySearchKey: mocks.applySearchKey,
applySearchProviderSelection: mocks.applySearchProviderSelection,
hasKeyInEnv: mocks.hasKeyInEnv,
setupSearch: mocks.setupSearch,
}));
import { WizardCancelledError } from "../wizard/prompts.js";
@@ -173,7 +153,16 @@ function setupBaseWizardState() {
mocks.probeGatewayReachable.mockResolvedValue({ ok: false });
mocks.resolveControlUiLinks.mockReturnValue({ wsUrl: "ws://127.0.0.1:18789" });
mocks.summarizeExistingConfig.mockReturnValue("");
mocks.createClackPrompter.mockReturnValue({});
mocks.createClackPrompter.mockReturnValue({
intro: vi.fn(async () => {}),
outro: vi.fn(async () => {}),
note: vi.fn(async () => {}),
select: vi.fn(async () => "firecrawl"),
multiselect: vi.fn(async () => []),
text: vi.fn(async () => ""),
confirm: vi.fn(async () => true),
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
});
}
function queueWizardPrompts(params: { select: string[]; confirm: boolean[]; text?: string }) {
@@ -194,9 +183,6 @@ describe("runConfigureWizard", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.ensureControlUiAssetsBuilt.mockResolvedValue({ ok: true });
mocks.resolveExistingKey.mockReturnValue(undefined);
mocks.hasExistingKey.mockReturnValue(false);
mocks.hasKeyInEnv.mockReturnValue(false);
mocks.resolveSearchProviderOptions.mockReturnValue([
{
id: "firecrawl",
@@ -209,9 +195,8 @@ describe("runConfigureWizard", () => {
credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey",
},
]);
mocks.applySearchKey.mockReset();
mocks.applySearchProviderSelection.mockReset();
mocks.applySearchProviderSelection.mockImplementation((cfg: OpenClawConfig) => cfg);
mocks.setupSearch.mockReset();
mocks.setupSearch.mockImplementation(async (cfg: OpenClawConfig) => cfg);
});
it("persists gateway.mode=local when only the run mode is selected", async () => {
@@ -240,21 +225,17 @@ describe("runConfigureWizard", () => {
expect(runtime.exit).toHaveBeenCalledWith(1);
});
it("persists provider-owned web search config changes returned by applySearchKey", async () => {
it("persists provider-owned web search config changes returned by setupSearch", async () => {
setupBaseWizardState();
mocks.resolveExistingKey.mockReturnValue(undefined);
mocks.hasExistingKey.mockReturnValue(false);
mocks.hasKeyInEnv.mockReturnValue(false);
mocks.applySearchKey.mockImplementation((cfg: OpenClawConfig, provider: string, key: string) =>
createEnabledWebSearchConfig(provider, {
mocks.setupSearch.mockImplementation(async (cfg: OpenClawConfig) =>
createEnabledWebSearchConfig("firecrawl", {
enabled: true,
config: { webSearch: { apiKey: key } },
config: { webSearch: { apiKey: "fc-entered-key" } },
})(cfg),
);
queueWizardPrompts({
select: ["local", "firecrawl"],
select: ["local"],
confirm: [true, false],
text: "fc-entered-key",
});
await runWebConfigureWizard();
@@ -281,35 +262,29 @@ describe("runConfigureWizard", () => {
}),
}),
);
expect(mocks.clackText).toHaveBeenCalledWith(
expect.objectContaining({
message: "Firecrawl API key (paste it here; leave blank to use FIRECRAWL_API_KEY)",
}),
);
expect(mocks.setupSearch).toHaveBeenCalledOnce();
});
it("applies provider selection side effects when a key already exists via secret ref or env", async () => {
it("delegates provider selection to the shared search setup flow", async () => {
setupBaseWizardState();
mocks.resolveExistingKey.mockReturnValue(undefined);
mocks.hasExistingKey.mockReturnValue(true);
mocks.hasKeyInEnv.mockReturnValue(false);
mocks.applySearchProviderSelection.mockImplementation((cfg: OpenClawConfig, provider: string) =>
createEnabledWebSearchConfig(provider, {
mocks.setupSearch.mockImplementation(async (cfg: OpenClawConfig) =>
createEnabledWebSearchConfig("firecrawl", {
enabled: true,
})(cfg),
);
queueWizardPrompts({
select: ["local", "firecrawl"],
select: ["local"],
confirm: [true, false],
});
await runWebConfigureWizard();
expect(mocks.applySearchProviderSelection).toHaveBeenCalledWith(
expect(mocks.setupSearch).toHaveBeenCalledWith(
expect.objectContaining({
gateway: expect.objectContaining({ mode: "local" }),
}),
"firecrawl",
expect.anything(),
expect.anything(),
);
expect(mocks.writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
@@ -322,53 +297,6 @@ describe("runConfigureWizard", () => {
}),
}),
);
expect(mocks.clackText).toHaveBeenCalledWith(
expect.objectContaining({
message: "Firecrawl API key (leave blank to keep current or use FIRECRAWL_API_KEY)",
}),
);
});
it("uses provider-specific credential copy for Gemini web search", async () => {
const originalGeminiApiKey = process.env.GEMINI_API_KEY;
delete process.env.GEMINI_API_KEY;
try {
setupBaseWizardState();
mocks.resolveSearchProviderOptions.mockReturnValue([
createSearchProviderOption({
id: "gemini",
label: "Gemini (Google Search)",
hint: "Requires Google Gemini API key · Google Search grounding",
credentialLabel: "Google Gemini API key",
envVars: ["GEMINI_API_KEY"],
placeholder: "AIza...",
signupUrl: "https://aistudio.google.com/apikey",
credentialPath: "plugins.entries.google.config.webSearch.apiKey",
}),
]);
queueWizardPrompts({
select: ["local", "gemini"],
confirm: [true, false],
});
await runWebConfigureWizard();
expect(mocks.clackText).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining("Google Gemini API key"),
}),
);
expect(mocks.note).toHaveBeenCalledWith(
expect.stringContaining("Store your Google Gemini API key here or set GEMINI_API_KEY"),
"Web search",
);
} finally {
if (originalGeminiApiKey === undefined) {
delete process.env.GEMINI_API_KEY;
} else {
process.env.GEMINI_API_KEY = originalGeminiApiKey;
}
}
});
it("does not crash when web search providers are unavailable under plugin policy", async () => {
@@ -400,7 +328,7 @@ describe("runConfigureWizard", () => {
);
});
it("skips the API key prompt for keyless web search providers", async () => {
it("still supports keyless web search providers through the shared setup flow", async () => {
setupBaseWizardState();
mocks.resolveSearchProviderOptions.mockReturnValue([
createSearchProviderOption({
@@ -415,28 +343,19 @@ describe("runConfigureWizard", () => {
credentialPath: "",
}),
]);
mocks.applySearchProviderSelection.mockImplementation((cfg: OpenClawConfig, provider: string) =>
createEnabledWebSearchConfig(provider, {
mocks.setupSearch.mockImplementation(async (cfg: OpenClawConfig) =>
createEnabledWebSearchConfig("duckduckgo", {
enabled: true,
})(cfg),
);
queueWizardPrompts({
select: ["local", "duckduckgo"],
select: ["local"],
confirm: [true, false],
});
await runWebConfigureWizard();
expect(mocks.clackText).not.toHaveBeenCalled();
expect(mocks.applySearchProviderSelection).toHaveBeenCalledWith(
expect.objectContaining({
gateway: expect.objectContaining({ mode: "local" }),
}),
"duckduckgo",
);
expect(mocks.note).toHaveBeenCalledWith(
expect.stringContaining("works without an API key"),
"Web search",
);
expect(mocks.setupSearch).toHaveBeenCalledOnce();
});
});

View File

@@ -158,53 +158,17 @@ async function promptChannelMode(runtime: RuntimeEnv): Promise<ChannelsWizardMod
async function promptWebToolsConfig(
nextConfig: OpenClawConfig,
runtime: RuntimeEnv,
prompter: ReturnType<typeof createClackPrompter>,
): Promise<OpenClawConfig> {
const existingSearch = nextConfig.tools?.web?.search;
const existingFetch = nextConfig.tools?.web?.fetch;
const {
resolveSearchProviderOptions,
resolveExistingKey,
hasExistingKey,
applySearchKey,
applySearchProviderSelection,
hasKeyInEnv,
} = await import("./onboard-search.js");
const { resolveSearchProviderOptions, setupSearch } = await import("./onboard-search.js");
const searchProviderOptions = resolveSearchProviderOptions(nextConfig);
const defaultProvider = searchProviderOptions[0]?.id;
const hasKeyForProvider = (provider: string): boolean => {
const entry = searchProviderOptions.find((e) => e.id === provider);
if (!entry) {
return false;
}
if (entry.requiresCredential === false) {
return true;
}
return hasExistingKey(nextConfig, provider) || hasKeyInEnv(entry);
};
const existingProvider = (() => {
const stored = existingSearch?.provider;
if (stored && searchProviderOptions.some((e) => e.id === stored)) {
return stored;
}
return searchProviderOptions.find((e) => hasKeyForProvider(e.id))?.id ?? defaultProvider;
})();
note(
[
"Web search lets your agent look things up online using the `web_search` tool.",
"Choose a provider. Some providers need an API key, and some work key-free.",
"Docs: https://docs.openclaw.ai/tools/web",
].join("\n"),
"Web search",
);
const enableSearch = guardCancel(
await confirm({
message: "Enable web_search?",
initialValue:
existingSearch?.enabled ?? searchProviderOptions.some((e) => hasKeyForProvider(e.id)),
initialValue: existingSearch?.enabled ?? searchProviderOptions.length > 0,
}),
runtime,
);
@@ -230,85 +194,11 @@ async function promptWebToolsConfig(
enabled: false,
};
} else {
const providerOptions = searchProviderOptions.map((entry) => {
const configured = hasKeyForProvider(entry.id);
return {
value: entry.id,
label: entry.label,
hint:
entry.requiresCredential === false
? `${entry.hint} · key-free`
: configured
? `${entry.hint} · configured`
: entry.hint,
};
});
const providerChoice = guardCancel(
await select({
message: "Choose web search provider",
options: providerOptions,
initialValue: existingProvider,
}),
runtime,
);
nextSearch = { ...nextSearch, provider: providerChoice };
const entry = searchProviderOptions.find((e) => e.id === providerChoice)!;
const credentialLabel = entry.credentialLabel?.trim() || `${entry.label} API key`;
const existingKey = resolveExistingKey(nextConfig, providerChoice);
const keyConfigured = hasExistingKey(nextConfig, providerChoice);
const envAvailable = entry.envVars.some((k) => Boolean(process.env[k]?.trim()));
const envVarNames = entry.envVars.join(" / ");
const needsCredential = entry.requiresCredential !== false;
if (!needsCredential) {
workingConfig = applySearchProviderSelection(workingConfig, providerChoice);
nextSearch = { ...workingConfig.tools?.web?.search };
note(
[
`${entry.label} works without an API key.`,
"OpenClaw enabled the plugin and selected it as your web_search provider.",
`Docs: ${entry.docsUrl ?? "https://docs.openclaw.ai/tools/web"}`,
].join("\n"),
"Web search",
);
} else {
const keyInput = guardCancel(
await text({
message: keyConfigured
? envAvailable
? `${credentialLabel} (leave blank to keep current or use ${envVarNames})`
: `${credentialLabel} (leave blank to keep current)`
: envAvailable
? `${credentialLabel} (paste it here; leave blank to use ${envVarNames})`
: credentialLabel,
placeholder: keyConfigured ? "Leave blank to keep current" : entry.placeholder,
}),
runtime,
);
const key = String(keyInput ?? "").trim();
if (key || existingKey) {
workingConfig = applySearchKey(workingConfig, providerChoice, (key || existingKey)!);
nextSearch = { ...workingConfig.tools?.web?.search };
} else if (keyConfigured || envAvailable) {
workingConfig = applySearchProviderSelection(workingConfig, providerChoice);
nextSearch = { ...workingConfig.tools?.web?.search };
} else {
nextSearch = { ...nextSearch, provider: providerChoice };
note(
[
"No key stored yet — web_search won't work until a key is available.",
`Store your ${credentialLabel} here or set ${envVarNames} in the Gateway environment.`,
`Get your API key at: ${entry.signupUrl}`,
"Docs: https://docs.openclaw.ai/tools/web",
].join("\n"),
"Web search",
);
}
}
workingConfig = await setupSearch(workingConfig, runtime, prompter);
nextSearch = {
...workingConfig.tools?.web?.search,
enabled: workingConfig.tools?.web?.search?.provider ? true : existingSearch?.enabled,
};
}
}
@@ -555,7 +445,7 @@ export async function runConfigureWizard(
}
if (selected.includes("web")) {
nextConfig = await promptWebToolsConfig(nextConfig, runtime);
nextConfig = await promptWebToolsConfig(nextConfig, runtime, prompter);
}
if (selected.includes("gateway")) {
@@ -608,7 +498,7 @@ export async function runConfigureWizard(
}
if (choice === "web") {
nextConfig = await promptWebToolsConfig(nextConfig, runtime);
nextConfig = await promptWebToolsConfig(nextConfig, runtime, prompter);
await persistConfig();
}

View File

@@ -5,13 +5,12 @@ import {
} from "../agents/auth-health.js";
import {
type AuthCredentialReasonCode,
CLAUDE_CLI_PROFILE_ID,
CODEX_CLI_PROFILE_ID,
ensureAuthProfileStore,
repairOAuthProfileIdMismatch,
resolveApiKeyForProfile,
resolveProfileUnusableUntilForDisplay,
} from "../agents/auth-profiles.js";
import { formatAuthDoctorHint } from "../agents/auth-profiles/doctor.js";
import { updateAuthProfileStoreWithLock } from "../agents/auth-profiles/store.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/config.js";
@@ -234,29 +233,36 @@ export function resolveUnusableProfileHint(params: {
return "Wait for cooldown or switch provider.";
}
function formatAuthIssueHint(issue: AuthIssue): string | null {
export async function resolveAuthIssueHint(
issue: AuthIssue,
cfg: OpenClawConfig,
store: ReturnType<typeof ensureAuthProfileStore>,
): Promise<string | null> {
if (issue.reasonCode === "invalid_expires") {
return "Invalid token expires metadata. Set a future Unix ms timestamp or remove expires.";
}
if (issue.provider === "anthropic" && issue.profileId === CLAUDE_CLI_PROFILE_ID) {
return `Deprecated profile. ${buildProviderAuthRecoveryHint({
provider: "anthropic",
})}`;
}
if (issue.provider === "openai-codex" && issue.profileId === CODEX_CLI_PROFILE_ID) {
return `Deprecated profile. ${buildProviderAuthRecoveryHint({
provider: "openai-codex",
})}`;
const providerHint = await formatAuthDoctorHint({
cfg,
store,
provider: issue.provider,
profileId: issue.profileId,
});
if (providerHint.trim()) {
return providerHint;
}
return buildProviderAuthRecoveryHint({
provider: issue.provider,
}).replace(/^Run /, "Re-auth via ");
}
function formatAuthIssueLine(issue: AuthIssue): string {
async function formatAuthIssueLine(
issue: AuthIssue,
cfg: OpenClawConfig,
store: ReturnType<typeof ensureAuthProfileStore>,
): Promise<string> {
const remaining =
issue.remainingMs !== undefined ? ` (${formatRemainingShort(issue.remainingMs)})` : "";
const hint = formatAuthIssueHint(issue);
const hint = await resolveAuthIssueHint(issue, cfg, store);
const reason = issue.reasonCode ? ` [${issue.reasonCode}]` : "";
return `- ${issue.profileId}: ${issue.status}${reason}${remaining}${hint ? `${hint}` : ""}`;
}
@@ -352,19 +358,21 @@ export async function noteAuthProfileHealth(params: {
}
if (issues.length > 0) {
note(
issues
.map((issue) =>
formatAuthIssueLine({
const issueLines = await Promise.all(
issues.map((issue) =>
formatAuthIssueLine(
{
profileId: issue.profileId,
provider: issue.provider,
status: issue.status,
reasonCode: issue.reasonCode,
remainingMs: issue.remainingMs,
}),
)
.join("\n"),
"Model auth",
},
params.cfg,
store,
),
),
);
note(issueLines.join("\n"), "Model auth");
}
}

View File

@@ -291,6 +291,37 @@ describe("noteMemorySearchHealth", () => {
const providersChecked = providerCalls.map(([arg]) => arg.provider);
expect(providersChecked).toEqual(["openai", "google", "voyage", "mistral"]);
});
it("uses runtime-derived env var hints for explicit providers", async () => {
resolveMemorySearchConfig.mockReturnValue({
provider: "gemini",
local: {},
remote: {},
});
await noteMemorySearchHealth(cfg);
const message = String(note.mock.calls[0]?.[0] ?? "");
expect(message).toContain("GEMINI_API_KEY");
expect(message).toContain('provider is set to "gemini"');
});
it("uses runtime-derived env var hints in auto mode", async () => {
resolveMemorySearchConfig.mockReturnValue({
provider: "auto",
local: {},
remote: {},
});
await noteMemorySearchHealth(cfg);
const message = String(note.mock.calls[0]?.[0] ?? "");
expect(message).toContain("OPENAI_API_KEY");
expect(message).toContain("GEMINI_API_KEY");
expect(message).toContain("GOOGLE_API_KEY");
expect(message).toContain("VOYAGE_API_KEY");
expect(message).toContain("MISTRAL_API_KEY");
});
});
describe("detectLegacyWorkspaceDirs", () => {

View File

@@ -4,6 +4,10 @@ import { resolveMemorySearchConfig } from "../agents/memory-search.js";
import { resolveApiKeyForProvider } from "../agents/model-auth.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/config.js";
import {
getBuiltinMemoryEmbeddingProviderDoctorMetadata,
listBuiltinAutoSelectMemoryEmbeddingProviderDoctorMetadata,
} from "../plugin-sdk/memory-core-engine-runtime.js";
import { DEFAULT_LOCAL_MODEL } from "../plugin-sdk/memory-core-host-engine-embeddings.js";
import { hasConfiguredMemorySecretInput } from "../plugin-sdk/memory-core-host-secret.js";
import { resolveActiveMemoryBackendConfig } from "../plugins/memory-runtime.js";
@@ -99,7 +103,7 @@ export async function noteMemorySearchHealth(
return;
}
const gatewayProbeWarning = buildGatewayProbeWarning(opts?.gatewayMemoryProbe);
const envVar = providerEnvVar(resolved.provider);
const envVar = resolvePrimaryMemoryProviderEnvVar(resolved.provider);
note(
[
`Memory search provider is set to "${resolved.provider}" but no API key was found.`,
@@ -122,8 +126,11 @@ export async function noteMemorySearchHealth(
if (hasLocalEmbeddings(resolved.local)) {
return;
}
for (const provider of ["openai", "gemini", "voyage", "mistral"] as const) {
if (hasRemoteApiKey || (await hasApiKeyForProvider(provider, cfg, agentDir))) {
const autoSelectProviders = listBuiltinAutoSelectMemoryEmbeddingProviderDoctorMetadata().filter(
(provider) => provider.transport === "remote",
);
for (const provider of autoSelectProviders) {
if (hasRemoteApiKey || (await hasApiKeyForProvider(provider.authProviderId, cfg, agentDir))) {
return;
}
}
@@ -148,7 +155,7 @@ export async function noteMemorySearchHealth(
gatewayProbeWarning ? gatewayProbeWarning : null,
"",
"Fix (pick one):",
"- Set OPENAI_API_KEY, GEMINI_API_KEY, VOYAGE_API_KEY, or MISTRAL_API_KEY in your environment",
`- Set ${formatMemoryProviderEnvVarList(autoSelectProviders)} in your environment`,
`- Configure credentials: ${formatCliCommand("openclaw configure --section model")}`,
`- For local embeddings: configure agents.defaults.memorySearch.provider and local model path`,
`- To disable: ${formatCliCommand("openclaw config set agents.defaults.memorySearch.enabled false")}`,
@@ -195,27 +202,26 @@ async function hasApiKeyForProvider(
cfg: OpenClawConfig,
agentDir: string,
): Promise<boolean> {
// Map embedding provider names to model-auth provider names
const authProvider = provider === "gemini" ? "google" : provider;
const metadata = getBuiltinMemoryEmbeddingProviderDoctorMetadata(provider);
try {
await resolveApiKeyForProvider({ provider: authProvider, cfg, agentDir });
await resolveApiKeyForProvider({
provider: metadata?.authProviderId ?? provider,
cfg,
agentDir,
});
return true;
} catch {
return false;
}
}
function providerEnvVar(provider: string): string {
switch (provider) {
case "openai":
return "OPENAI_API_KEY";
case "gemini":
return "GEMINI_API_KEY";
case "voyage":
return "VOYAGE_API_KEY";
default:
return `${provider.toUpperCase()}_API_KEY`;
}
function resolvePrimaryMemoryProviderEnvVar(provider: string): string {
const metadata = getBuiltinMemoryEmbeddingProviderDoctorMetadata(provider);
return metadata?.envVars[0] ?? `${provider.toUpperCase()}_API_KEY`;
}
function formatMemoryProviderEnvVarList(providers: Array<{ envVars: string[] }>): string {
return [...new Set(providers.flatMap((provider) => provider.envVars).filter(Boolean))].join(", ");
}
function buildGatewayProbeWarning(

View File

@@ -1,67 +0,0 @@
import type { OpenClawConfig } from "../../../config/config.js";
import type { SecretInput } from "../../../config/types.secrets.js";
import { applyLitellmConfig } from "../../../plugin-sdk/litellm.js";
import { applyAuthProfileConfig } from "../../../plugins/provider-auth-helpers.js";
import { setLitellmApiKey } from "../../../plugins/provider-auth-storage.js";
import type { RuntimeEnv } from "../../../runtime.js";
import type { AuthChoice, OnboardOptions } from "../../onboard-types.js";
type ApiKeyStorageOptions = {
secretInputMode: "plaintext" | "ref";
};
type ResolvedNonInteractiveApiKey = {
key: string;
source: "profile" | "env" | "flag";
};
export async function applySimpleNonInteractiveApiKeyChoice(params: {
authChoice: AuthChoice;
nextConfig: OpenClawConfig;
baseConfig: OpenClawConfig;
opts: OnboardOptions;
runtime: RuntimeEnv;
apiKeyStorageOptions?: ApiKeyStorageOptions;
resolveApiKey: (input: {
provider: string;
cfg: OpenClawConfig;
flagValue?: string;
flagName: `--${string}`;
envVar: string;
runtime: RuntimeEnv;
}) => Promise<ResolvedNonInteractiveApiKey | null>;
maybeSetResolvedApiKey: (
resolved: ResolvedNonInteractiveApiKey,
setter: (value: SecretInput) => Promise<void> | void,
) => Promise<boolean>;
}): Promise<OpenClawConfig | null | undefined> {
if (params.authChoice !== "litellm-api-key") {
return undefined;
}
const resolved = await params.resolveApiKey({
provider: "litellm",
cfg: params.baseConfig,
flagValue: params.opts.litellmApiKey,
flagName: "--litellm-api-key",
envVar: "LITELLM_API_KEY",
runtime: params.runtime,
});
if (!resolved) {
return null;
}
if (
!(await params.maybeSetResolvedApiKey(resolved, (value) =>
setLitellmApiKey(value, undefined, params.apiKeyStorageOptions),
))
) {
return null;
}
return applyLitellmConfig(
applyAuthProfileConfig(params.nextConfig, {
profileId: "litellm:default",
provider: "litellm",
mode: "api_key",
}),
);
}

View File

@@ -12,6 +12,11 @@ vi.mock("../api-keys.js", () => ({
resolveNonInteractiveApiKey,
}));
const resolveManifestDeprecatedProviderAuthChoice = vi.hoisted(() => vi.fn(() => undefined));
vi.mock("../../../plugins/provider-auth-choices.js", () => ({
resolveManifestDeprecatedProviderAuthChoice,
}));
beforeEach(() => {
vi.clearAllMocks();
});
@@ -42,4 +47,27 @@ describe("applyNonInteractiveAuthChoice", () => {
expect(result).toBe(resolvedConfig);
expect(applyNonInteractivePluginProviderChoice).toHaveBeenCalledOnce();
});
it("fails with manifest-owned replacement guidance for deprecated auth choices", async () => {
const runtime = createRuntime();
const nextConfig = { agents: { defaults: {} } } as OpenClawConfig;
resolveManifestDeprecatedProviderAuthChoice.mockReturnValueOnce({
choiceId: "minimax-global-api",
} as never);
const result = await applyNonInteractiveAuthChoice({
nextConfig,
authChoice: "minimax",
opts: {} as never,
runtime: runtime as never,
baseConfig: nextConfig,
});
expect(result).toBeNull();
expect(runtime.error).toHaveBeenCalledWith(
'"minimax" is no longer supported. Use --auth-choice minimax-global-api instead.',
);
expect(runtime.exit).toHaveBeenCalledWith(1);
expect(applyNonInteractivePluginProviderChoice).toHaveBeenCalledOnce();
});
});

View File

@@ -1,6 +1,7 @@
import type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js";
import type { OpenClawConfig } from "../../../config/config.js";
import type { SecretInput } from "../../../config/types.secrets.js";
import { resolveManifestDeprecatedProviderAuthChoice } from "../../../plugins/provider-auth-choices.js";
import type { RuntimeEnv } from "../../../runtime.js";
import { resolveDefaultSecretProviderAlias } from "../../../secrets/ref-contract.js";
import {
@@ -17,7 +18,6 @@ import {
} from "../../onboard-custom.js";
import type { AuthChoice, OnboardOptions } from "../../onboard-types.js";
import { resolveNonInteractiveApiKey } from "../api-keys.js";
import { applySimpleNonInteractiveApiKeyChoice } from "./auth-choice.api-key-providers.js";
import { applyNonInteractivePluginProviderChoice } from "./auth-choice.plugin-providers.js";
type ResolvedNonInteractiveApiKey = NonNullable<
@@ -45,9 +45,6 @@ export async function applyNonInteractiveAuthChoice(params: {
runtime.exit(1);
return null;
}
const apiKeyStorageOptions = requestedSecretInputMode
? { secretInputMode: requestedSecretInputMode }
: undefined;
const toStoredSecretInput = (resolved: ResolvedNonInteractiveApiKey): SecretInput | null => {
const storePlaintextSecret = requestedSecretInputMode !== "ref"; // pragma: allowlist secret
if (storePlaintextSecret) {
@@ -79,20 +76,6 @@ export async function applyNonInteractiveAuthChoice(params: {
...input,
secretInputMode: requestedSecretInputMode,
});
const maybeSetResolvedApiKey = async (
resolved: ResolvedNonInteractiveApiKey,
setter: (value: SecretInput) => Promise<void> | void,
): Promise<boolean> => {
if (resolved.source === "profile") {
return true;
}
const stored = toStoredSecretInput(resolved);
if (!stored) {
return false;
}
await setter(stored);
return true;
};
const toApiKeyCredential = (params: {
provider: string;
resolved: ResolvedNonInteractiveApiKey;
@@ -168,32 +151,13 @@ export async function applyNonInteractiveAuthChoice(params: {
return pluginProviderChoice;
}
const simpleApiKeyChoice = await applySimpleNonInteractiveApiKeyChoice({
authChoice,
nextConfig,
baseConfig,
opts,
runtime,
apiKeyStorageOptions,
resolveApiKey,
maybeSetResolvedApiKey,
const deprecatedChoice = resolveManifestDeprecatedProviderAuthChoice(authChoice as string, {
config: nextConfig,
env: process.env,
});
if (simpleApiKeyChoice !== undefined) {
return simpleApiKeyChoice;
}
// Legacy aliases: these choice values were removed; fail with an actionable message so
// existing CI automation gets a clear error instead of silently exiting 0 with no auth.
const REMOVED_MINIMAX_CHOICES: Record<string, string> = {
minimax: "minimax-global-api",
"minimax-api": "minimax-global-api",
"minimax-cloud": "minimax-global-api",
"minimax-api-lightning": "minimax-global-api",
"minimax-api-key-cn": "minimax-cn-api",
};
if (Object.prototype.hasOwnProperty.call(REMOVED_MINIMAX_CHOICES, authChoice as string)) {
const replacement = REMOVED_MINIMAX_CHOICES[authChoice as string];
if (deprecatedChoice) {
runtime.error(
`"${authChoice as string}" is no longer supported. Use --auth-choice ${replacement} instead.`,
`"${authChoice as string}" is no longer supported. Use --auth-choice ${deprecatedChoice.choiceId} instead.`,
);
runtime.exit(1);
return null;

View File

@@ -59,6 +59,7 @@ function createBundledFirecrawlEntry(): PluginWebSearchProviderEntry {
pluginId: "firecrawl",
label: "Firecrawl Search",
hint: "Structured results",
onboardingScopes: ["text-inference"],
envVars: ["FIRECRAWL_API_KEY"],
placeholder: "fc-...",
signupUrl: "https://example.com/firecrawl",

View File

@@ -43,6 +43,7 @@ function makeRegistry(
plugins: Array<{
id: string;
channels: string[];
autoEnableWhenConfiguredProviders?: string[];
channelConfigs?: Record<string, { schema: Record<string, unknown>; preferOver?: string[] }>;
}>,
): PluginManifestRegistry {
@@ -50,6 +51,7 @@ function makeRegistry(
plugins: plugins.map((p) => ({
id: p.id,
channels: p.channels,
autoEnableWhenConfiguredProviders: p.autoEnableWhenConfiguredProviders,
channelConfigs: p.channelConfigs,
providers: [],
cliBackends: [],
@@ -376,6 +378,50 @@ describe("applyPluginAutoEnable", () => {
expect(result.config.plugins?.entries?.["minimax-portal-auth"]).toBeUndefined();
});
it("does not auto-enable unrelated provider plugins just because auth profiles exist", () => {
const result = applyPluginAutoEnable({
config: {
auth: {
profiles: {
"openai:default": {
provider: "openai",
mode: "api_key",
},
},
},
},
env: {},
});
expect(result.config.plugins?.entries?.openai).toBeUndefined();
expect(result.changes).toEqual([]);
});
it("uses manifest-owned provider auto-enable metadata for third-party plugins", () => {
const result = applyPluginAutoEnable({
config: {
auth: {
profiles: {
"acme-oauth:default": {
provider: "acme-oauth",
mode: "oauth",
},
},
},
},
env: {},
manifestRegistry: makeRegistry([
{
id: "acme",
channels: [],
autoEnableWhenConfiguredProviders: ["acme-oauth"],
},
]),
});
expect(result.config.plugins?.entries?.acme?.enabled).toBe(true);
});
it("auto-enables acpx plugin when ACP is configured", () => {
const result = applyPluginAutoEnable({
config: {

View File

@@ -6,6 +6,7 @@ import {
listChatChannels,
normalizeChatChannelId,
} from "../channels/registry.js";
import { BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS } from "../plugins/bundled-capability-metadata.js";
import {
loadPluginManifestRegistry,
type PluginManifestRegistry,
@@ -30,13 +31,22 @@ const EMPTY_PLUGIN_MANIFEST_REGISTRY: PluginManifestRegistry = {
diagnostics: [],
};
const PROVIDER_PLUGIN_IDS: Array<{ pluginId: string; providerId: string }> = [
{ pluginId: "google", providerId: "google-gemini-cli" },
{ pluginId: "copilot-proxy", providerId: "copilot-proxy" },
{ pluginId: "minimax", providerId: "minimax-portal" },
];
const ENV_CATALOG_PATHS = ["OPENCLAW_PLUGIN_CATALOG_PATHS", "OPENCLAW_MPM_CATALOG_PATHS"];
function resolveAutoEnableProviderPluginIds(
registry: PluginManifestRegistry,
): Readonly<Record<string, string>> {
const entries = new Map<string, string>(Object.entries(BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS));
for (const plugin of registry.plugins) {
for (const providerId of plugin.autoEnableWhenConfiguredProviders ?? []) {
if (!entries.has(providerId)) {
entries.set(providerId, plugin.id);
}
}
}
return Object.fromEntries(entries);
}
function collectModelRefs(cfg: OpenClawConfig): string[] {
const refs: string[] = [];
const pushModelRef = (value: unknown) => {
@@ -286,11 +296,13 @@ function resolveConfiguredPlugins(
}
}
for (const mapping of PROVIDER_PLUGIN_IDS) {
if (isProviderConfigured(cfg, mapping.providerId)) {
for (const [providerId, pluginId] of Object.entries(
resolveAutoEnableProviderPluginIds(registry),
)) {
if (isProviderConfigured(cfg, providerId)) {
changes.push({
pluginId: mapping.pluginId,
reason: `${mapping.providerId} auth configured`,
pluginId,
reason: `${providerId} auth configured`,
});
}
}

View File

@@ -47,19 +47,15 @@ function resolveSearchProviderCredentialLabel(
return entry.credentialLabel?.trim() || `${entry.label} API key`;
}
const DEFAULT_ONBOARD_SEARCH_PROVIDER_IDS = new Set<SearchProvider>([
"brave",
"firecrawl",
"gemini",
"grok",
"kimi",
"perplexity",
"tavily",
]);
export const SEARCH_PROVIDER_OPTIONS: readonly PluginWebSearchProviderEntry[] =
resolveSearchProviderSetupContributions().map((contribution) => contribution.provider);
function showsSearchProviderInSetup(
entry: Pick<PluginWebSearchProviderEntry, "onboardingScopes">,
): boolean {
return entry.onboardingScopes?.includes("text-inference") ?? false;
}
function canRepairBundledProviderSelection(
config: OpenClawConfig,
provider: Pick<PluginWebSearchProviderEntry, "id" | "pluginId">,
@@ -107,7 +103,7 @@ export function resolveSearchProviderSetupContributions(
if (!config) {
return sortFlowContributionsByLabel(
sortWebSearchProviders(listBundledWebSearchProviders())
.filter((entry) => DEFAULT_ONBOARD_SEARCH_PROVIDER_IDS.has(entry.id))
.filter(showsSearchProviderInSetup)
.map((provider) => buildSearchProviderSetupContribution({ provider, source: "bundled" })),
);
}

View File

@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../extensions/public-artifacts.js";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../plugins/public-artifacts.js";
import { captureEnv } from "../test-utils/env.js";
import {
canResolveRegistryVersionForPackageTarget,

View File

@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../extensions/public-artifacts.js";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../plugins/public-artifacts.js";
import { withEnvAsync } from "../test-utils/env.js";
import { pathExists } from "../utils.js";
import { resolveStableNodePath } from "./stable-node-path.js";

View File

@@ -53,9 +53,7 @@ export const ensureBinary: BinariesRuntimeModule["ensureBinary"] = async (...arg
(await loadBinariesRuntime()).ensureBinary(...args);
export const runExec: ExecRuntimeModule["runExec"] = async (...args) =>
(await loadExecRuntime()).runExec(...args);
export const runCommandWithTimeout: ExecRuntimeModule["runCommandWithTimeout"] = async (
...args
) =>
export const runCommandWithTimeout: ExecRuntimeModule["runCommandWithTimeout"] = async (...args) =>
(await loadExecRuntime()).runCommandWithTimeout(...args);
export const monitorWebChannel: WhatsAppRuntimeModule["monitorWebChannel"] = async (...args) =>
(await loadWhatsAppRuntime()).monitorWebChannel(...args);

View File

@@ -2,6 +2,9 @@
// Keep extension-owned engine exports isolated behind a dedicated SDK subpath.
export {
getBuiltinMemoryEmbeddingProviderDoctorMetadata,
getMemorySearchManager,
listBuiltinAutoSelectMemoryEmbeddingProviderDoctorMetadata,
MemoryIndexManager,
} from "../../extensions/memory-core/runtime-api.js";
export type { BuiltinMemoryEmbeddingProviderDoctorMetadata } from "../../extensions/memory-core/runtime-api.js";

View File

@@ -1,6 +1,7 @@
// Public provider auth environment variable helpers for plugin runtimes.
export {
getProviderEnvVars,
listKnownProviderAuthEnvVarNames,
omitEnvKeysCaseInsensitive,
} from "../secrets/provider-env-vars.js";

View File

@@ -90,3 +90,28 @@ export const BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS = Object.fromEntries(
entry.webSearchProviderIds.map((providerId) => [providerId, entry.pluginId] as const),
).toSorted(([left], [right]) => left.localeCompare(right)),
) as Readonly<Record<string, string>>;
export const BUNDLED_PROVIDER_PLUGIN_ID_ALIASES = Object.fromEntries(
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.flatMap((entry) =>
entry.providerIds
.filter((providerId) => providerId !== entry.pluginId)
.map((providerId) => [providerId, entry.pluginId] as const),
).toSorted(([left], [right]) => left.localeCompare(right)),
) as Readonly<Record<string, string>>;
export const BUNDLED_LEGACY_PLUGIN_ID_ALIASES = Object.fromEntries(
BUNDLED_PLUGIN_METADATA.flatMap(({ manifest }) =>
(manifest.legacyPluginIds ?? []).map(
(legacyPluginId) => [legacyPluginId, manifest.id] as const,
),
).toSorted(([left], [right]) => left.localeCompare(right)),
) as Readonly<Record<string, string>>;
export const BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS = Object.fromEntries(
BUNDLED_PLUGIN_METADATA.flatMap(({ manifest }) =>
(manifest.autoEnableWhenConfiguredProviders ?? []).map((providerId) => [
providerId,
manifest.id,
]),
).toSorted(([left], [right]) => left.localeCompare(right)),
) as Readonly<Record<string, string>>;

View File

@@ -8,6 +8,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/acpx",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw ACP runtime backend via acpx",
@@ -149,6 +151,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["amazon-bedrock"],
},
},
@@ -159,6 +162,12 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: [
"cli-backend.js",
"cli-migration.js",
"cli-shared.js",
"media-understanding-provider.js",
],
packageName: "@openclaw/anthropic-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Anthropic provider plugin",
@@ -172,6 +181,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["anthropic"],
cliBackends: ["claude-cli"],
providerAuthEnvVars: {
@@ -228,6 +238,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "channel-config-api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/bluebubbles",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw BlueBubbles channel plugin",
@@ -777,6 +789,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["web-search-provider.js"],
packageName: "@openclaw/brave-plugin",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Brave plugin",
@@ -831,6 +844,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["browser-runtime-api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/browser-plugin",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw browser tool plugin",
@@ -854,6 +869,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["provider-catalog.js"],
packageName: "@openclaw/byteplus-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw BytePlus provider plugin",
@@ -867,6 +883,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["byteplus", "byteplus-plan"],
providerAuthEnvVars: {
byteplus: ["BYTEPLUS_API_KEY"],
@@ -895,6 +912,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js"],
packageName: "@openclaw/chutes-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Chutes.ai provider plugin",
@@ -948,6 +966,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js"],
packageName: "@openclaw/cloudflare-ai-gateway-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Cloudflare AI Gateway provider plugin",
@@ -961,6 +980,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["cloudflare-ai-gateway"],
providerAuthEnvVars: {
"cloudflare-ai-gateway": ["CLOUDFLARE_AI_GATEWAY_API_KEY"],
@@ -990,6 +1010,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/copilot-proxy",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Copilot Proxy provider plugin",
@@ -1003,6 +1025,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
autoEnableWhenConfiguredProviders: ["copilot-proxy"],
providers: ["copilot-proxy"],
providerAuthChoices: [
{
@@ -1025,6 +1049,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["audio.js", "media-understanding-provider.js"],
packageName: "@openclaw/deepgram-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Deepgram media-understanding provider",
@@ -1050,6 +1075,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js"],
packageName: "@openclaw/deepseek-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw DeepSeek provider plugin",
@@ -1063,6 +1089,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["deepseek"],
providerAuthEnvVars: {
deepseek: ["DEEPSEEK_API_KEY"],
@@ -1091,6 +1118,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["api.js"],
packageName: "@openclaw/diagnostics-otel",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw diagnostics OpenTelemetry exporter",
@@ -1113,6 +1141,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["api.js"],
packageName: "@openclaw/diffs",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw diff viewer plugin",
@@ -1313,6 +1342,15 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: [
"action-runtime-api.js",
"api.js",
"channel-config-api.js",
"runtime-api.js",
"session-key-api.js",
"timeouts.js",
],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/discord",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Discord channel plugin",
@@ -3977,6 +4015,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["web-search-provider.js"],
packageName: "@openclaw/duckduckgo-plugin",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw DuckDuckGo plugin",
@@ -4026,6 +4065,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["speech-provider.js", "tts.js"],
packageName: "@openclaw/elevenlabs-speech",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw ElevenLabs speech plugin",
@@ -4051,6 +4091,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["web-search-provider.js"],
packageName: "@openclaw/exa-plugin",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Exa plugin",
@@ -4097,6 +4138,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["image-generation-provider.js", "onboard.js"],
packageName: "@openclaw/fal-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw fal provider plugin",
@@ -4110,6 +4152,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["fal"],
providerAuthEnvVars: {
fal: ["FAL_KEY"],
@@ -4146,6 +4189,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js", "setup-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/feishu",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
@@ -5301,6 +5346,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["web-search-provider.js"],
packageName: "@openclaw/firecrawl-plugin",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Firecrawl plugin",
@@ -5355,6 +5401,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["login.js", "models-defaults.js", "models.js", "token.js", "usage.js"],
packageName: "@openclaw/github-copilot-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw GitHub Copilot provider plugin",
@@ -5368,6 +5415,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["github-copilot"],
providerAuthEnvVars: {
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
@@ -5393,6 +5441,24 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: [
"cli-backend.js",
"gemini-cli-provider.js",
"image-generation-provider.js",
"media-understanding-provider.js",
"oauth.credentials.js",
"oauth.flow.js",
"oauth.http.js",
"oauth.js",
"oauth.project.js",
"oauth.runtime.js",
"oauth.shared.js",
"oauth.token.js",
"provider-models.js",
"runtime-api.js",
"web-search-provider.js",
],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/google-plugin",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Google plugin",
@@ -5419,6 +5485,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
},
},
},
enabledByDefault: true,
autoEnableWhenConfiguredProviders: ["google-gemini-cli"],
providers: ["google", "google-gemini-cli"],
cliBackends: ["google-gemini-cli"],
providerAuthEnvVars: {
@@ -5479,6 +5547,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "channel-config-api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/googlechat",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Google Chat channel plugin",
@@ -6298,6 +6368,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["media-understanding-provider.js"],
packageName: "@openclaw/groq-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Groq media-understanding provider",
@@ -6323,6 +6394,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js"],
packageName: "@openclaw/huggingface-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Hugging Face provider plugin",
@@ -6336,6 +6408,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["huggingface"],
providerAuthEnvVars: {
huggingface: ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"],
@@ -6369,6 +6442,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "channel-config-api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/imessage",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw iMessage channel plugin",
@@ -6992,6 +7067,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "channel-config-api.js"],
packageName: "@openclaw/irc",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw IRC channel plugin",
@@ -7653,6 +7729,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js", "shared.js"],
packageName: "@openclaw/kilocode-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Kilo Gateway provider plugin",
@@ -7666,6 +7743,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["kilocode"],
providerAuthEnvVars: {
kilocode: ["KILOCODE_API_KEY"],
@@ -7695,6 +7773,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js"],
packageName: "@openclaw/kimi-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Kimi provider plugin",
@@ -7708,6 +7787,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["kimi", "kimi-coding"],
providerAuthEnvVars: {
kimi: ["KIMI_API_KEY", "KIMICODE_API_KEY"],
@@ -7741,6 +7821,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js", "setup-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/line",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw LINE channel plugin",
@@ -8017,6 +8099,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js"],
packageName: "@openclaw/litellm-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw LiteLLM provider plugin",
@@ -8030,6 +8113,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["litellm"],
providerAuthEnvVars: {
litellm: ["LITELLM_API_KEY"],
@@ -8059,6 +8143,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["api.js"],
packageName: "@openclaw/llm-task",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw JSON-only LLM task plugin",
@@ -8106,6 +8191,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/lobster",
packageVersion: "2026.3.26",
packageDescription: "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
@@ -8134,6 +8221,14 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: [
"api.js",
"helper-api.js",
"legacy-crypto-inspector.js",
"runtime-api.js",
"thread-bindings-runtime.js",
],
runtimeSidecarArtifacts: ["helper-api.js", "runtime-api.js", "thread-bindings-runtime.js"],
packageName: "@openclaw/matrix",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Matrix channel plugin",
@@ -8631,6 +8726,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/mattermost",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Mattermost channel plugin",
@@ -9246,6 +9343,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/memory-core",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw core memory search plugin",
@@ -9269,6 +9368,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["api.js", "config.js", "lancedb-runtime.js"],
packageName: "@openclaw/memory-lancedb",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw LanceDB-backed long-term memory plugin with auto-recall/capture",
@@ -9377,6 +9477,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["speech-provider.js", "tts.js"],
packageName: "@openclaw/microsoft-speech",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Microsoft speech plugin",
@@ -9402,6 +9503,15 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: [
"auth.js",
"cli.js",
"onboard.js",
"provider.js",
"runtime.js",
"shared-runtime.js",
"shared.js",
],
packageName: "@openclaw/microsoft-foundry",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Microsoft Foundry provider plugin",
@@ -9451,6 +9561,15 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: [
"image-generation-provider.js",
"media-understanding-provider.js",
"model-definitions.js",
"oauth.js",
"oauth.runtime.js",
"onboard.js",
"provider-catalog.js",
],
packageName: "@openclaw/minimax-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw MiniMax provider and OAuth plugin",
@@ -9464,6 +9583,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
legacyPluginIds: ["minimax-portal-auth"],
autoEnableWhenConfiguredProviders: ["minimax-portal"],
providers: ["minimax", "minimax-portal"],
providerAuthEnvVars: {
minimax: ["MINIMAX_API_KEY"],
@@ -9484,6 +9606,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
provider: "minimax",
method: "api-global",
choiceId: "minimax-global-api",
deprecatedChoiceIds: ["minimax", "minimax-api", "minimax-cloud", "minimax-api-lightning"],
choiceLabel: "MiniMax API key (Global)",
choiceHint: "Global endpoint - api.minimax.io",
groupId: "minimax",
@@ -9508,6 +9631,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
provider: "minimax",
method: "api-cn",
choiceId: "minimax-cn-api",
deprecatedChoiceIds: ["minimax-api-key-cn"],
choiceLabel: "MiniMax API key (CN)",
choiceHint: "CN endpoint - api.minimaxi.com",
groupId: "minimax",
@@ -9532,6 +9656,12 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: [
"media-understanding-provider.js",
"model-definitions.js",
"onboard.js",
"provider-catalog.js",
],
packageName: "@openclaw/mistral-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Mistral provider plugin",
@@ -9545,6 +9675,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["mistral"],
providerAuthEnvVars: {
mistral: ["MISTRAL_API_KEY"],
@@ -9576,6 +9707,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["model-definitions.js", "onboard.js", "provider-catalog.js"],
packageName: "@openclaw/modelstudio-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Model Studio provider plugin",
@@ -9589,6 +9721,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["modelstudio"],
providerAuthEnvVars: {
modelstudio: ["MODELSTUDIO_API_KEY"],
@@ -9660,6 +9793,12 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: [
"media-understanding-provider.js",
"onboard.js",
"provider-catalog.js",
"web-search-provider.js",
],
packageName: "@openclaw/moonshot-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Moonshot provider plugin",
@@ -9689,6 +9828,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
},
},
},
enabledByDefault: true,
providers: ["moonshot"],
providerAuthEnvVars: {
moonshot: ["MOONSHOT_API_KEY"],
@@ -9753,6 +9893,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "channel-config-api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/msteams",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Microsoft Teams channel plugin",
@@ -10236,6 +10378,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/nextcloud-talk",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Nextcloud Talk channel plugin",
@@ -10957,6 +11101,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js", "setup-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/nostr",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
@@ -11091,6 +11237,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["provider-catalog.js"],
packageName: "@openclaw/nvidia-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw NVIDIA provider plugin",
@@ -11104,6 +11251,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["nvidia"],
providerAuthEnvVars: {
nvidia: ["NVIDIA_API_KEY"],
@@ -11117,6 +11265,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/ollama-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Ollama provider plugin",
@@ -11130,6 +11280,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["ollama"],
providerAuthEnvVars: {
ollama: ["OLLAMA_API_KEY"],
@@ -11155,6 +11306,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/open-prose",
packageVersion: "2026.3.26",
packageDescription: "OpenProse VM skill pack plugin (slash command + telemetry).",
@@ -11180,6 +11333,19 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: [
"cli-backend.js",
"image-generation-provider.js",
"media-understanding-provider.js",
"openai-codex-auth-identity.js",
"openai-codex-catalog.js",
"openai-codex-provider.js",
"openai-codex-provider.runtime.js",
"openai-provider.js",
"shared.js",
"speech-provider.js",
"tts.js",
],
packageName: "@openclaw/openai-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw OpenAI provider plugins",
@@ -11193,6 +11359,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["openai", "openai-codex"],
cliBackends: ["codex-cli"],
providerAuthEnvVars: {
@@ -11237,6 +11404,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js"],
packageName: "@openclaw/opencode-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw OpenCode Zen provider plugin",
@@ -11250,6 +11418,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["opencode"],
providerAuthEnvVars: {
opencode: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
@@ -11278,6 +11447,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js"],
packageName: "@openclaw/opencode-go-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw OpenCode Go provider plugin",
@@ -11291,6 +11461,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["opencode-go"],
providerAuthEnvVars: {
"opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
@@ -11319,6 +11490,11 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: [
"media-understanding-provider.js",
"onboard.js",
"provider-catalog.js",
],
packageName: "@openclaw/openrouter-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw OpenRouter provider plugin",
@@ -11332,6 +11508,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["openrouter"],
providerAuthEnvVars: {
openrouter: ["OPENROUTER_API_KEY"],
@@ -11493,6 +11670,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["web-search-provider.js"],
packageName: "@openclaw/perplexity-plugin",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Perplexity plugin",
@@ -11553,6 +11731,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js"],
packageName: "@openclaw/qianfan-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Qianfan provider plugin",
@@ -11566,6 +11745,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["qianfan"],
providerAuthEnvVars: {
qianfan: ["QIANFAN_API_KEY"],
@@ -11607,6 +11787,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["sglang"],
providerAuthEnvVars: {
sglang: ["SGLANG_API_KEY"],
@@ -11636,6 +11817,13 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: [
"api.js",
"channel-config-api.js",
"reaction-runtime-api.js",
"runtime-api.js",
],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/signal",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Signal channel plugin",
@@ -12330,6 +12518,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "channel-config-api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/slack",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Slack channel plugin",
@@ -14075,6 +14265,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["setup-api.js"],
packageName: "@openclaw/synology-chat",
packageVersion: "2026.3.26",
packageDescription: "Synology Chat channel plugin for OpenClaw",
@@ -14133,6 +14324,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js"],
packageName: "@openclaw/synthetic-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Synthetic provider plugin",
@@ -14146,6 +14338,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["synthetic"],
providerAuthEnvVars: {
synthetic: ["SYNTHETIC_API_KEY"],
@@ -14174,6 +14367,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["web-search-provider.js"],
packageName: "@openclaw/tavily-plugin",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Tavily plugin",
@@ -14233,6 +14427,14 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: [
"allow-from.js",
"api.js",
"channel-config-api.js",
"runtime-api.js",
"update-offset-runtime-api.js",
],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/telegram",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Telegram channel plugin",
@@ -16314,6 +16516,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js", "setup-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/tlon",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Tlon/Urbit channel plugin",
@@ -16520,6 +16724,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js"],
packageName: "@openclaw/together-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Together provider plugin",
@@ -16533,6 +16738,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["together"],
providerAuthEnvVars: {
together: ["TOGETHER_API_KEY"],
@@ -16561,6 +16767,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/twitch",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Twitch channel plugin",
@@ -16793,6 +17001,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js"],
packageName: "@openclaw/venice-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Venice provider plugin",
@@ -16806,6 +17015,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["venice"],
providerAuthEnvVars: {
venice: ["VENICE_API_KEY"],
@@ -16834,6 +17044,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js"],
packageName: "@openclaw/vercel-ai-gateway-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Vercel AI Gateway provider plugin",
@@ -16847,6 +17058,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["vercel-ai-gateway"],
providerAuthEnvVars: {
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"],
@@ -16888,6 +17100,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["vllm"],
providerAuthEnvVars: {
vllm: ["VLLM_API_KEY"],
@@ -16913,6 +17126,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js", "runtime-entry.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/voice-call",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw voice-call plugin",
@@ -17540,6 +17755,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["provider-catalog.js"],
packageName: "@openclaw/volcengine-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Volcengine provider plugin",
@@ -17553,6 +17769,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["volcengine", "volcengine-plan"],
providerAuthEnvVars: {
volcengine: ["VOLCANO_ENGINE_API_KEY"],
@@ -17585,6 +17802,17 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: [
"action-runtime-api.js",
"action-runtime.runtime.js",
"api.js",
"auth-presence.js",
"channel-config-api.js",
"light-runtime-api.js",
"login-qr-api.js",
"runtime-api.js",
],
runtimeSidecarArtifacts: ["light-runtime-api.js", "runtime-api.js"],
packageName: "@openclaw/whatsapp",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw WhatsApp channel plugin",
@@ -18183,6 +18411,14 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: [
"model-definitions.js",
"onboard.js",
"provider-catalog.js",
"provider-models.js",
"stream.js",
"web-search.js",
],
packageName: "@openclaw/xai-plugin",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw xAI plugin",
@@ -18212,6 +18448,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
},
},
},
enabledByDefault: true,
providers: ["xai"],
providerAuthEnvVars: {
xai: ["XAI_API_KEY"],
@@ -18258,6 +18495,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: ["onboard.js", "provider-catalog.js"],
packageName: "@openclaw/xiaomi-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Xiaomi provider plugin",
@@ -18271,6 +18509,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["xiaomi"],
providerAuthEnvVars: {
xiaomi: ["XIAOMI_API_KEY"],
@@ -18299,6 +18538,14 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./index.ts",
built: "index.js",
},
publicSurfaceArtifacts: [
"detect.js",
"media-understanding-provider.js",
"model-definitions.js",
"onboard.js",
"runtime-api.js",
],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/zai-provider",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Z.AI provider plugin",
@@ -18312,6 +18559,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
additionalProperties: false,
properties: {},
},
enabledByDefault: true,
providers: ["zai"],
providerAuthEnvVars: {
zai: ["ZAI_API_KEY", "Z_AI_API_KEY"],
@@ -18403,6 +18651,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/zalo",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Zalo channel plugin",
@@ -18874,6 +19124,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
source: "./setup-entry.ts",
built: "setup-entry.js",
},
publicSurfaceArtifacts: ["api.js", "runtime-api.js"],
runtimeSidecarArtifacts: ["runtime-api.js"],
packageName: "@openclaw/zalouser",
packageVersion: "2026.3.26",
packageDescription: "OpenClaw Zalo Personal Account plugin via native zca-js integration",

View File

@@ -35,6 +35,11 @@ describe("bundled plugin metadata", () => {
const discord = BUNDLED_PLUGIN_METADATA.find((entry) => entry.dirName === "discord");
expect(discord?.source).toEqual({ source: "./index.ts", built: "index.js" });
expect(discord?.setupSource).toEqual({ source: "./setup-entry.ts", built: "setup-entry.js" });
expect(discord?.publicSurfaceArtifacts).toContain("api.js");
expect(discord?.publicSurfaceArtifacts).toContain("runtime-api.js");
expect(discord?.publicSurfaceArtifacts).toContain("session-key-api.js");
expect(discord?.publicSurfaceArtifacts).not.toContain("test-api.js");
expect(discord?.runtimeSidecarArtifacts).toContain("runtime-api.js");
expect(discord?.manifest.id).toBe("discord");
expect(discord?.manifest.channelConfigs?.discord).toEqual(
expect.objectContaining({
@@ -43,6 +48,16 @@ describe("bundled plugin metadata", () => {
);
});
it("excludes test-only public surface artifacts", () => {
for (const entry of BUNDLED_PLUGIN_METADATA) {
for (const artifact of entry.publicSurfaceArtifacts ?? []) {
expect(artifact).not.toMatch(/^test-/);
expect(artifact).not.toContain(".test-");
expect(artifact).not.toMatch(/\.test\.js$/);
}
}
});
it("prefers built generated paths when present and falls back to source paths", () => {
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-metadata-");
@@ -183,4 +198,47 @@ describe("bundled plugin metadata", () => {
},
});
});
it("captures top-level public surface artifacts without duplicating the primary entrypoints", async () => {
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-public-artifacts-");
writeJson(path.join(tempRoot, "extensions", "alpha", "package.json"), {
name: "@openclaw/alpha",
version: "0.0.1",
openclaw: {
extensions: ["./index.ts"],
setupEntry: "./setup-entry.ts",
},
});
writeJson(path.join(tempRoot, "extensions", "alpha", "openclaw.plugin.json"), {
id: "alpha",
configSchema: { type: "object" },
});
fs.writeFileSync(
path.join(tempRoot, "extensions", "alpha", "index.ts"),
"export {};\n",
"utf8",
);
fs.writeFileSync(
path.join(tempRoot, "extensions", "alpha", "setup-entry.ts"),
"export {};\n",
"utf8",
);
fs.writeFileSync(path.join(tempRoot, "extensions", "alpha", "api.ts"), "export {};\n", "utf8");
fs.writeFileSync(
path.join(tempRoot, "extensions", "alpha", "runtime-api.ts"),
"export {};\n",
"utf8",
);
const entries = await collectBundledPluginMetadata({ repoRoot: tempRoot });
const firstEntry = entries[0] as
| {
publicSurfaceArtifacts?: string[];
runtimeSidecarArtifacts?: string[];
}
| undefined;
expect(firstEntry?.publicSurfaceArtifacts).toEqual(["api.js", "runtime-api.js"]);
expect(firstEntry?.runtimeSidecarArtifacts).toEqual(["runtime-api.js"]);
});
});

View File

@@ -13,6 +13,8 @@ export type GeneratedBundledPluginMetadata = {
idHint: string;
source: GeneratedBundledPluginPathPair;
setupSource?: GeneratedBundledPluginPathPair;
publicSurfaceArtifacts?: readonly string[];
runtimeSidecarArtifacts?: readonly string[];
packageName?: string;
packageVersion?: string;
packageDescription?: string;

View File

@@ -132,21 +132,25 @@ describe("normalizePluginsConfig", () => {
it("normalizes legacy plugin ids to their merged bundled plugin id", () => {
const result = normalizePluginsConfig({
allow: ["openai-codex", "minimax-portal-auth"],
deny: ["openai-codex", "minimax-portal-auth"],
allow: ["openai-codex", "google-gemini-cli", "minimax-portal-auth"],
deny: ["openai-codex", "google-gemini-cli", "minimax-portal-auth"],
entries: {
"openai-codex": {
enabled: true,
},
"google-gemini-cli": {
enabled: true,
},
"minimax-portal-auth": {
enabled: false,
},
},
});
expect(result.allow).toEqual(["openai", "minimax"]);
expect(result.deny).toEqual(["openai", "minimax"]);
expect(result.allow).toEqual(["openai", "google", "minimax"]);
expect(result.deny).toEqual(["openai", "google", "minimax"]);
expect(result.entries.openai?.enabled).toBe(true);
expect(result.entries.google?.enabled).toBe(true);
expect(result.entries.minimax?.enabled).toBe(false);
});
});
@@ -189,6 +193,16 @@ describe("resolveEffectiveEnableState", () => {
});
describe("resolveEnableState", () => {
it("enables bundled plugins only when manifest metadata marks them enabled by default", () => {
expect(resolveEnableState("openai", "bundled", normalizePluginsConfig({}))).toEqual({
enabled: false,
reason: "bundled (disabled by default)",
});
expect(resolveEnableState("openai", "bundled", normalizePluginsConfig({}), true)).toEqual({
enabled: true,
});
});
it("keeps the selected memory slot plugin enabled even when omitted from plugins.allow", () => {
const state = resolveEnableState(
"memory-core",
@@ -266,8 +280,8 @@ describe("resolveEnableState", () => {
});
});
it("keeps bundled provider plugins enabled when they are bundled-default providers", () => {
const state = resolveEnableState("google", "bundled", normalizePluginsConfig({}));
it("keeps bundled plugins enabled when manifest metadata marks them enabled by default", () => {
const state = resolveEnableState("google", "bundled", normalizePluginsConfig({}), true);
expect(state).toEqual({ enabled: true });
});

View File

@@ -1,5 +1,9 @@
import { normalizeChatChannelId } from "../channels/registry.js";
import type { OpenClawConfig } from "../config/config.js";
import {
BUNDLED_LEGACY_PLUGIN_ID_ALIASES,
BUNDLED_PROVIDER_PLUGIN_ID_ALIASES,
} from "./bundled-capability-metadata.js";
import type { PluginRecord } from "./registry.js";
import { defaultSlotIdForKey } from "./slots.js";
@@ -28,52 +32,13 @@ export type NormalizedPluginsConfig = {
>;
};
export const BUNDLED_ENABLED_BY_DEFAULT = new Set<string>([
"amazon-bedrock",
"anthropic",
"byteplus",
"cloudflare-ai-gateway",
"deepseek",
"device-pair",
"github-copilot",
"google",
"huggingface",
"kilocode",
"kimi",
"minimax",
"mistral",
"modelstudio",
"moonshot",
"nvidia",
"ollama",
"openai",
"opencode",
"opencode-go",
"openrouter",
"phone-control",
"qianfan",
"sglang",
"synthetic",
"talk-voice",
"together",
"venice",
"vercel-ai-gateway",
"vllm",
"volcengine",
"xai",
"xiaomi",
"zai",
]);
const PLUGIN_ID_ALIASES: Readonly<Record<string, string>> = {
"openai-codex": "openai",
"kimi-coding": "kimi",
"minimax-portal-auth": "minimax",
};
export function normalizePluginId(id: string): string {
const trimmed = id.trim();
return PLUGIN_ID_ALIASES[trimmed] ?? trimmed;
return (
BUNDLED_LEGACY_PLUGIN_ID_ALIASES[trimmed] ??
BUNDLED_PROVIDER_PLUGIN_ID_ALIASES[trimmed] ??
trimmed
);
}
const normalizeList = (value: unknown): string[] => {
@@ -299,7 +264,7 @@ export function resolveEnableState(
if (entry?.enabled === true) {
return { enabled: true };
}
if (origin === "bundled" && (enabledByDefault ?? BUNDLED_ENABLED_BY_DEFAULT.has(id))) {
if (origin === "bundled" && enabledByDefault === true) {
return { enabled: true };
}
if (origin === "bundled") {

View File

@@ -45,6 +45,7 @@ export type PluginManifestRecord = {
description?: string;
version?: string;
enabledByDefault?: boolean;
autoEnableWhenConfiguredProviders?: string[];
format?: PluginFormat;
bundleFormat?: PluginBundleFormat;
bundleCapabilities?: string[];
@@ -220,6 +221,7 @@ function buildRecord(params: {
normalizeManifestLabel(params.manifest.description) ?? params.candidate.packageDescription,
version: normalizeManifestLabel(params.manifest.version) ?? params.candidate.packageVersion,
enabledByDefault: params.manifest.enabledByDefault === true ? true : undefined,
autoEnableWhenConfiguredProviders: params.manifest.autoEnableWhenConfiguredProviders,
format: params.candidate.format ?? "openclaw",
bundleFormat: params.candidate.bundleFormat,
kind: params.manifest.kind,

View File

@@ -20,6 +20,10 @@ export type PluginManifest = {
id: string;
configSchema: Record<string, unknown>;
enabledByDefault?: boolean;
/** Legacy plugin ids that should normalize to this plugin id. */
legacyPluginIds?: string[];
/** Provider ids that should auto-enable this plugin when referenced in auth/config/models. */
autoEnableWhenConfiguredProviders?: string[];
kind?: PluginKind;
channels?: string[];
providers?: string[];
@@ -63,6 +67,8 @@ export type PluginManifestProviderAuthChoice = {
/** Optional user-facing choice label/hint for grouped onboarding UI. */
choiceLabel?: string;
choiceHint?: string;
/** Legacy choice ids that should point users at this replacement choice. */
deprecatedChoiceIds?: string[];
/** Optional grouping metadata for auth-choice pickers. */
groupId?: string;
groupLabel?: string;
@@ -151,6 +157,7 @@ function normalizeProviderAuthChoices(
}
const choiceLabel = typeof entry.choiceLabel === "string" ? entry.choiceLabel.trim() : "";
const choiceHint = typeof entry.choiceHint === "string" ? entry.choiceHint.trim() : "";
const deprecatedChoiceIds = normalizeStringList(entry.deprecatedChoiceIds);
const groupId = typeof entry.groupId === "string" ? entry.groupId.trim() : "";
const groupLabel = typeof entry.groupLabel === "string" ? entry.groupLabel.trim() : "";
const groupHint = typeof entry.groupHint === "string" ? entry.groupHint.trim() : "";
@@ -169,6 +176,7 @@ function normalizeProviderAuthChoices(
choiceId,
...(choiceLabel ? { choiceLabel } : {}),
...(choiceHint ? { choiceHint } : {}),
...(deprecatedChoiceIds.length > 0 ? { deprecatedChoiceIds } : {}),
...(groupId ? { groupId } : {}),
...(groupLabel ? { groupLabel } : {}),
...(groupHint ? { groupHint } : {}),
@@ -276,6 +284,10 @@ export function loadPluginManifest(
const kind = typeof raw.kind === "string" ? (raw.kind as PluginKind) : undefined;
const enabledByDefault = raw.enabledByDefault === true;
const legacyPluginIds = normalizeStringList(raw.legacyPluginIds);
const autoEnableWhenConfiguredProviders = normalizeStringList(
raw.autoEnableWhenConfiguredProviders,
);
const name = typeof raw.name === "string" ? raw.name.trim() : undefined;
const description = typeof raw.description === "string" ? raw.description.trim() : undefined;
const version = typeof raw.version === "string" ? raw.version.trim() : undefined;
@@ -299,6 +311,10 @@ export function loadPluginManifest(
id,
configSchema,
...(enabledByDefault ? { enabledByDefault } : {}),
...(legacyPluginIds.length > 0 ? { legacyPluginIds } : {}),
...(autoEnableWhenConfiguredProviders.length > 0
? { autoEnableWhenConfiguredProviders }
: {}),
kind,
channels,
providers,

View File

@@ -7,6 +7,7 @@ vi.mock("./manifest-registry.js", () => ({
}));
import {
resolveManifestDeprecatedProviderAuthChoice,
resolveManifestProviderAuthChoice,
resolveManifestProviderAuthChoices,
resolveManifestProviderOnboardAuthFlags,
@@ -91,4 +92,30 @@ describe("provider auth choice manifest helpers", () => {
},
]);
});
it("resolves deprecated auth-choice aliases through manifest metadata", () => {
loadPluginManifestRegistry.mockReturnValue({
plugins: [
{
id: "minimax",
providerAuthChoices: [
{
provider: "minimax",
method: "api-global",
choiceId: "minimax-global-api",
deprecatedChoiceIds: ["minimax", "minimax-api"],
},
],
},
],
});
expect(resolveManifestDeprecatedProviderAuthChoice("minimax")?.choiceId).toBe(
"minimax-global-api",
);
expect(resolveManifestDeprecatedProviderAuthChoice("minimax-api")?.choiceId).toBe(
"minimax-global-api",
);
expect(resolveManifestDeprecatedProviderAuthChoice("openai")).toBeUndefined();
});
});

View File

@@ -9,6 +9,7 @@ export type ProviderAuthChoiceMetadata = {
choiceId: string;
choiceLabel: string;
choiceHint?: string;
deprecatedChoiceIds?: string[];
groupId?: string;
groupLabel?: string;
groupHint?: string;
@@ -46,6 +47,7 @@ export function resolveManifestProviderAuthChoices(params?: {
choiceId: choice.choiceId,
choiceLabel: choice.choiceLabel ?? choice.choiceId,
...(choice.choiceHint ? { choiceHint: choice.choiceHint } : {}),
...(choice.deprecatedChoiceIds ? { deprecatedChoiceIds: choice.deprecatedChoiceIds } : {}),
...(choice.groupId ? { groupId: choice.groupId } : {}),
...(choice.groupLabel ? { groupLabel: choice.groupLabel } : {}),
...(choice.groupHint ? { groupHint: choice.groupHint } : {}),
@@ -94,6 +96,23 @@ export function resolveManifestProviderApiKeyChoice(params: {
});
}
export function resolveManifestDeprecatedProviderAuthChoice(
choiceId: string,
params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
},
): ProviderAuthChoiceMetadata | undefined {
const normalized = choiceId.trim();
if (!normalized) {
return undefined;
}
return resolveManifestProviderAuthChoices(params).find((choice) =>
choice.deprecatedChoiceIds?.includes(normalized),
);
}
export function resolveManifestProviderOnboardAuthFlags(params?: {
config?: OpenClawConfig;
workspaceDir?: string;

View File

@@ -1052,6 +1052,15 @@ export type WebSearchProviderPlugin = {
id: WebSearchProviderId;
label: string;
hint: string;
/**
* Interactive onboarding surfaces where this search provider should appear
* when OpenClaw has no config-aware runtime context yet.
*
* Unlike provider auth, search setup historically exposed only a curated
* quickstart subset. Keep this plugin-owned so core does not hardcode the
* default bundled provider list.
*/
onboardingScopes?: Array<"text-inference">;
requiresCredential?: boolean;
credentialLabel?: string;
envVars: string[];