Merge branch 'main' into fix/tts-tool-no-channel-hang
This commit is contained in:
commit
cbc3a93ccd
@ -93,6 +93,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc.
|
||||
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
|
||||
- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc.
|
||||
- Slack/startup: harden `@slack/bolt` import interop across current bundled runtime shapes so Slack monitors no longer crash with `App is not a constructor` after plugin-sdk bundling changes. (#45953) thanks @merc1305.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ const PROVIDER_ID = "ollama";
|
||||
const DEFAULT_API_KEY = "ollama-local";
|
||||
|
||||
async function loadProviderSetup() {
|
||||
return await import("openclaw/plugin-sdk/provider-setup");
|
||||
return await import("openclaw/plugin-sdk/ollama-setup");
|
||||
}
|
||||
|
||||
const ollamaPlugin = {
|
||||
|
||||
@ -10,8 +10,8 @@ import { ensureAuthProfileStore } from "../../src/agents/auth-profiles/store.js"
|
||||
import type { OAuthCredential } from "../../src/agents/auth-profiles/types.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../src/agents/defaults.js";
|
||||
import { normalizeModelCompat } from "../../src/agents/model-compat.js";
|
||||
import { normalizeProviderId } from "../../src/agents/model-selection.js";
|
||||
import { buildOpenAICodexProvider } from "../../src/agents/models-config.providers.static.js";
|
||||
import { normalizeProviderId } from "../../src/agents/provider-id.js";
|
||||
import { loginOpenAICodexOAuth } from "../../src/commands/openai-codex-oauth.js";
|
||||
import { fetchCodexUsage } from "../../src/infra/provider-usage.fetch.js";
|
||||
import { buildOauthProviderAuthResult } from "../../src/plugin-sdk/provider-auth-result.js";
|
||||
|
||||
@ -3,7 +3,7 @@ import {
|
||||
type ProviderRuntimeModel,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import { normalizeModelCompat } from "../../src/agents/model-compat.js";
|
||||
import { normalizeProviderId } from "../../src/agents/model-selection.js";
|
||||
import { normalizeProviderId } from "../../src/agents/provider-id.js";
|
||||
import {
|
||||
applyOpenAIConfig,
|
||||
OPENAI_DEFAULT_MODEL,
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
const PROVIDER_ID = "sglang";
|
||||
|
||||
async function loadProviderSetup() {
|
||||
return await import("openclaw/plugin-sdk/provider-setup");
|
||||
return await import("openclaw/plugin-sdk/self-hosted-provider-setup");
|
||||
}
|
||||
|
||||
const sglangPlugin = {
|
||||
|
||||
94
extensions/slack/src/monitor/provider.interop.test.ts
Normal file
94
extensions/slack/src/monitor/provider.interop.test.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { __testing } from "./provider.js";
|
||||
|
||||
describe("resolveSlackBoltInterop", () => {
|
||||
class FakeApp {}
|
||||
class FakeHTTPReceiver {}
|
||||
|
||||
it("uses the default import when it already exposes named exports", () => {
|
||||
const resolved = __testing.resolveSlackBoltInterop({
|
||||
defaultImport: {
|
||||
App: FakeApp,
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
},
|
||||
namespaceImport: {},
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
App: FakeApp,
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses nested default export when the default import is a wrapper object", () => {
|
||||
const resolved = __testing.resolveSlackBoltInterop({
|
||||
defaultImport: {
|
||||
default: {
|
||||
App: FakeApp,
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
},
|
||||
},
|
||||
namespaceImport: {},
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
App: FakeApp,
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the namespace receiver when the default import is the App constructor itself", () => {
|
||||
const resolved = __testing.resolveSlackBoltInterop({
|
||||
defaultImport: FakeApp,
|
||||
namespaceImport: {
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
App: FakeApp,
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses namespace.default when it exposes named exports", () => {
|
||||
const resolved = __testing.resolveSlackBoltInterop({
|
||||
defaultImport: undefined,
|
||||
namespaceImport: {
|
||||
default: {
|
||||
App: FakeApp,
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
App: FakeApp,
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the namespace import when it exposes named exports", () => {
|
||||
const resolved = __testing.resolveSlackBoltInterop({
|
||||
defaultImport: undefined,
|
||||
namespaceImport: {
|
||||
App: FakeApp,
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({
|
||||
App: FakeApp,
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
});
|
||||
});
|
||||
|
||||
it("throws when the module cannot be resolved", () => {
|
||||
expect(() =>
|
||||
__testing.resolveSlackBoltInterop({
|
||||
defaultImport: null,
|
||||
namespaceImport: {},
|
||||
}),
|
||||
).toThrow("Unable to resolve @slack/bolt App/HTTPReceiver exports");
|
||||
});
|
||||
});
|
||||
@ -1,5 +1,5 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import SlackBolt from "@slack/bolt";
|
||||
import SlackBolt, * as SlackBoltNamespace from "@slack/bolt";
|
||||
import { resolveTextChunkLimit } from "../../../../src/auto-reply/chunk.js";
|
||||
import { DEFAULT_GROUP_HISTORY_LIMIT } from "../../../../src/auto-reply/reply/history.js";
|
||||
import {
|
||||
@ -46,14 +46,77 @@ import {
|
||||
import { registerSlackMonitorSlashCommands } from "./slash.js";
|
||||
import type { MonitorSlackOpts } from "./types.js";
|
||||
|
||||
const slackBoltModule = SlackBolt as typeof import("@slack/bolt") & {
|
||||
default?: typeof import("@slack/bolt");
|
||||
type SlackAppConstructor = typeof import("@slack/bolt").App;
|
||||
type SlackHttpReceiverConstructor = typeof import("@slack/bolt").HTTPReceiver;
|
||||
type SlackBoltResolvedExports = {
|
||||
App: SlackAppConstructor;
|
||||
HTTPReceiver: SlackHttpReceiverConstructor;
|
||||
};
|
||||
// Bun allows named imports from CJS; Node ESM doesn't. Use default+fallback for compatibility.
|
||||
// Fix: Check if module has App property directly (Node 25.x ESM/CJS compat issue)
|
||||
const slackBolt =
|
||||
(slackBoltModule.App ? slackBoltModule : slackBoltModule.default) ?? slackBoltModule;
|
||||
const { App, HTTPReceiver } = slackBolt;
|
||||
type Constructor = abstract new (...args: never[]) => unknown;
|
||||
|
||||
function isConstructorFunction<T extends Constructor>(value: unknown): value is T {
|
||||
return typeof value === "function";
|
||||
}
|
||||
|
||||
function resolveSlackBoltModule(value: unknown): SlackBoltResolvedExports | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const app = Reflect.get(value, "App");
|
||||
const httpReceiver = Reflect.get(value, "HTTPReceiver");
|
||||
if (
|
||||
!isConstructorFunction<SlackAppConstructor>(app) ||
|
||||
!isConstructorFunction<SlackHttpReceiverConstructor>(httpReceiver)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
App: app,
|
||||
HTTPReceiver: httpReceiver,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSlackBoltInterop(params: {
|
||||
defaultImport: unknown;
|
||||
namespaceImport: unknown;
|
||||
}): SlackBoltResolvedExports {
|
||||
const { defaultImport, namespaceImport } = params;
|
||||
const nestedDefault =
|
||||
defaultImport && typeof defaultImport === "object"
|
||||
? Reflect.get(defaultImport, "default")
|
||||
: undefined;
|
||||
const namespaceDefault =
|
||||
namespaceImport && typeof namespaceImport === "object"
|
||||
? Reflect.get(namespaceImport, "default")
|
||||
: undefined;
|
||||
const namespaceReceiver =
|
||||
namespaceImport && typeof namespaceImport === "object"
|
||||
? Reflect.get(namespaceImport, "HTTPReceiver")
|
||||
: undefined;
|
||||
const directModule =
|
||||
resolveSlackBoltModule(defaultImport) ??
|
||||
resolveSlackBoltModule(nestedDefault) ??
|
||||
resolveSlackBoltModule(namespaceDefault) ??
|
||||
resolveSlackBoltModule(namespaceImport);
|
||||
if (directModule) {
|
||||
return directModule;
|
||||
}
|
||||
if (
|
||||
isConstructorFunction<SlackAppConstructor>(defaultImport) &&
|
||||
isConstructorFunction<SlackHttpReceiverConstructor>(namespaceReceiver)
|
||||
) {
|
||||
return {
|
||||
App: defaultImport,
|
||||
HTTPReceiver: namespaceReceiver,
|
||||
};
|
||||
}
|
||||
throw new TypeError("Unable to resolve @slack/bolt App/HTTPReceiver exports");
|
||||
}
|
||||
|
||||
const { App, HTTPReceiver } = resolveSlackBoltInterop({
|
||||
defaultImport: SlackBolt,
|
||||
namespaceImport: SlackBoltNamespace,
|
||||
});
|
||||
|
||||
const SLACK_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
||||
const SLACK_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
|
||||
@ -515,6 +578,7 @@ export const __testing = {
|
||||
publishSlackDisconnectedStatus,
|
||||
resolveSlackRuntimeGroupPolicy: resolveOpenProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
resolveSlackBoltInterop,
|
||||
getSocketEmitter,
|
||||
waitForSlackSocketDisconnect,
|
||||
};
|
||||
|
||||
@ -4,10 +4,13 @@ import type {
|
||||
OpenClawConfig,
|
||||
PluginRuntime,
|
||||
} from "openclaw/plugin-sdk/telegram";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRuntimeEnv } from "../../test-utils/runtime-env.js";
|
||||
import type { ResolvedTelegramAccount } from "./accounts.js";
|
||||
import * as auditModule from "./audit.js";
|
||||
import { telegramPlugin } from "./channel.js";
|
||||
import * as monitorModule from "./monitor.js";
|
||||
import * as probeModule from "./probe.js";
|
||||
import { setTelegramRuntime } from "./runtime.js";
|
||||
|
||||
function createCfg(): OpenClawConfig {
|
||||
@ -53,32 +56,34 @@ function createStartAccountCtx(params: {
|
||||
}
|
||||
|
||||
function installGatewayRuntime(params?: { probeOk?: boolean; botUsername?: string }) {
|
||||
const monitorTelegramProvider = vi.fn(async () => undefined);
|
||||
const probeTelegram = vi.fn(async () =>
|
||||
params?.probeOk ? { ok: true, bot: { username: params.botUsername ?? "bot" } } : { ok: false },
|
||||
);
|
||||
const collectUnmentionedGroupIds = vi.fn(() => ({
|
||||
groupIds: [] as string[],
|
||||
unresolvedGroups: 0,
|
||||
hasWildcardUnmentionedGroups: false,
|
||||
}));
|
||||
const auditGroupMembership = vi.fn(async () => ({
|
||||
ok: true,
|
||||
checkedGroups: 0,
|
||||
unresolvedGroups: 0,
|
||||
hasWildcardUnmentionedGroups: false,
|
||||
groups: [],
|
||||
elapsedMs: 0,
|
||||
}));
|
||||
const monitorTelegramProvider = vi
|
||||
.spyOn(monitorModule, "monitorTelegramProvider")
|
||||
.mockImplementation(async () => undefined);
|
||||
const probeTelegram = vi
|
||||
.spyOn(probeModule, "probeTelegram")
|
||||
.mockImplementation(async () =>
|
||||
params?.probeOk
|
||||
? { ok: true, bot: { username: params.botUsername ?? "bot" } }
|
||||
: { ok: false },
|
||||
);
|
||||
const collectUnmentionedGroupIds = vi
|
||||
.spyOn(auditModule, "collectTelegramUnmentionedGroupIds")
|
||||
.mockImplementation(() => ({
|
||||
groupIds: [] as string[],
|
||||
unresolvedGroups: 0,
|
||||
hasWildcardUnmentionedGroups: false,
|
||||
}));
|
||||
const auditGroupMembership = vi
|
||||
.spyOn(auditModule, "auditTelegramGroupMembership")
|
||||
.mockImplementation(async () => ({
|
||||
ok: true,
|
||||
checkedGroups: 0,
|
||||
unresolvedGroups: 0,
|
||||
hasWildcardUnmentionedGroups: false,
|
||||
groups: [],
|
||||
elapsedMs: 0,
|
||||
}));
|
||||
setTelegramRuntime({
|
||||
channel: {
|
||||
telegram: {
|
||||
monitorTelegramProvider,
|
||||
probeTelegram,
|
||||
collectUnmentionedGroupIds,
|
||||
auditGroupMembership,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
shouldLogVerbose: () => false,
|
||||
},
|
||||
@ -115,6 +120,10 @@ function installSendMessageRuntime(
|
||||
return sendMessageTelegram;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("telegramPlugin duplicate token guard", () => {
|
||||
it("marks secondary account as not configured when token is shared", async () => {
|
||||
const cfg = createCfg();
|
||||
|
||||
@ -47,15 +47,17 @@ import {
|
||||
type ResolvedTelegramAccount,
|
||||
} from "./accounts.js";
|
||||
import { buildTelegramExecApprovalButtons } from "./approval-buttons.js";
|
||||
import { auditTelegramGroupMembership, collectTelegramUnmentionedGroupIds } from "./audit.js";
|
||||
import { buildTelegramGroupPeerId } from "./bot/helpers.js";
|
||||
import {
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
resolveTelegramExecApprovalTarget,
|
||||
} from "./exec-approvals.js";
|
||||
import { monitorTelegramProvider } from "./monitor.js";
|
||||
import { looksLikeTelegramTargetId, normalizeTelegramMessagingTarget } from "./normalize.js";
|
||||
import { sendTelegramPayloadMessages } from "./outbound-adapter.js";
|
||||
import { parseTelegramReplyToMessageId, parseTelegramThreadId } from "./outbound-params.js";
|
||||
import type { TelegramProbe } from "./probe.js";
|
||||
import { probeTelegram, type TelegramProbe } from "./probe.js";
|
||||
import { getTelegramRuntime } from "./runtime.js";
|
||||
import { sendTypingTelegram } from "./send.js";
|
||||
import { telegramSetupAdapter } from "./setup-core.js";
|
||||
@ -697,7 +699,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
collectStatusIssues: collectTelegramStatusIssues,
|
||||
buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot),
|
||||
probeAccount: async ({ account, timeoutMs }) =>
|
||||
getTelegramRuntime().channel.telegram.probeTelegram(account.token, timeoutMs, {
|
||||
probeTelegram(account.token, timeoutMs, {
|
||||
accountId: account.accountId,
|
||||
proxyUrl: account.config.proxy,
|
||||
network: account.config.network,
|
||||
@ -731,7 +733,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
cfg.channels?.telegram?.accounts?.[account.accountId]?.groups ??
|
||||
cfg.channels?.telegram?.groups;
|
||||
const { groupIds, unresolvedGroups, hasWildcardUnmentionedGroups } =
|
||||
getTelegramRuntime().channel.telegram.collectUnmentionedGroupIds(groups);
|
||||
collectTelegramUnmentionedGroupIds(groups);
|
||||
if (!groupIds.length && unresolvedGroups === 0 && !hasWildcardUnmentionedGroups) {
|
||||
return undefined;
|
||||
}
|
||||
@ -746,7 +748,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
elapsedMs: 0,
|
||||
};
|
||||
}
|
||||
const audit = await getTelegramRuntime().channel.telegram.auditGroupMembership({
|
||||
const audit = await auditTelegramGroupMembership({
|
||||
token: account.token,
|
||||
botId,
|
||||
groupIds,
|
||||
@ -815,7 +817,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
const token = (account.token ?? "").trim();
|
||||
let telegramBotLabel = "";
|
||||
try {
|
||||
const probe = await getTelegramRuntime().channel.telegram.probeTelegram(token, 2500, {
|
||||
const probe = await probeTelegram(token, 2500, {
|
||||
accountId: account.accountId,
|
||||
proxyUrl: account.config.proxy,
|
||||
network: account.config.network,
|
||||
@ -830,7 +832,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
}
|
||||
}
|
||||
ctx.log?.info(`[${account.accountId}] starting provider${telegramBotLabel}`);
|
||||
return getTelegramRuntime().channel.telegram.monitorTelegramProvider({
|
||||
return monitorTelegramProvider({
|
||||
token,
|
||||
accountId: account.accountId,
|
||||
config: ctx.cfg,
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
const PROVIDER_ID = "vllm";
|
||||
|
||||
async function loadProviderSetup() {
|
||||
return await import("openclaw/plugin-sdk/provider-setup");
|
||||
return await import("openclaw/plugin-sdk/self-hosted-provider-setup");
|
||||
}
|
||||
|
||||
const vllmPlugin = {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { normalizeProviderId } from "../../src/agents/model-selection.js";
|
||||
import { normalizeProviderId } from "../../src/agents/provider-id.js";
|
||||
import {
|
||||
createPluginBackedWebSearchProvider,
|
||||
getScopedCredentialValue,
|
||||
|
||||
@ -50,6 +50,10 @@
|
||||
"types": "./dist/plugin-sdk/compat.d.ts",
|
||||
"default": "./dist/plugin-sdk/compat.js"
|
||||
},
|
||||
"./plugin-sdk/ollama-setup": {
|
||||
"types": "./dist/plugin-sdk/ollama-setup.d.ts",
|
||||
"default": "./dist/plugin-sdk/ollama-setup.js"
|
||||
},
|
||||
"./plugin-sdk/provider-setup": {
|
||||
"types": "./dist/plugin-sdk/provider-setup.d.ts",
|
||||
"default": "./dist/plugin-sdk/provider-setup.js"
|
||||
@ -58,6 +62,10 @@
|
||||
"types": "./dist/plugin-sdk/sandbox.d.ts",
|
||||
"default": "./dist/plugin-sdk/sandbox.js"
|
||||
},
|
||||
"./plugin-sdk/self-hosted-provider-setup": {
|
||||
"types": "./dist/plugin-sdk/self-hosted-provider-setup.d.ts",
|
||||
"default": "./dist/plugin-sdk/self-hosted-provider-setup.js"
|
||||
},
|
||||
"./plugin-sdk/routing": {
|
||||
"types": "./dist/plugin-sdk/routing.d.ts",
|
||||
"default": "./dist/plugin-sdk/routing.js"
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
"index",
|
||||
"core",
|
||||
"compat",
|
||||
"ollama-setup",
|
||||
"provider-setup",
|
||||
"sandbox",
|
||||
"self-hosted-provider-setup",
|
||||
"routing",
|
||||
"telegram",
|
||||
"discord",
|
||||
|
||||
@ -3,14 +3,13 @@ import { resolveStateDir } from "../config/paths.js";
|
||||
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
|
||||
export function resolveOpenClawAgentDir(): string {
|
||||
const override =
|
||||
process.env.OPENCLAW_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim();
|
||||
export function resolveOpenClawAgentDir(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const override = env.OPENCLAW_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim();
|
||||
if (override) {
|
||||
return resolveUserPath(override);
|
||||
return resolveUserPath(override, env);
|
||||
}
|
||||
const defaultAgentDir = path.join(resolveStateDir(), "agents", DEFAULT_AGENT_ID, "agent");
|
||||
return resolveUserPath(defaultAgentDir);
|
||||
const defaultAgentDir = path.join(resolveStateDir(env), "agents", DEFAULT_AGENT_ID, "agent");
|
||||
return resolveUserPath(defaultAgentDir, env);
|
||||
}
|
||||
|
||||
export function ensureOpenClawAgentEnv(): string {
|
||||
|
||||
@ -327,12 +327,16 @@ export function resolveAgentIdByWorkspacePath(
|
||||
return resolveAgentIdsByWorkspacePath(cfg, workspacePath)[0];
|
||||
}
|
||||
|
||||
export function resolveAgentDir(cfg: OpenClawConfig, agentId: string) {
|
||||
export function resolveAgentDir(
|
||||
cfg: OpenClawConfig,
|
||||
agentId: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
) {
|
||||
const id = normalizeAgentId(agentId);
|
||||
const configured = resolveAgentConfig(cfg, id)?.agentDir?.trim();
|
||||
if (configured) {
|
||||
return resolveUserPath(configured);
|
||||
return resolveUserPath(configured, env);
|
||||
}
|
||||
const root = resolveStateDir(process.env);
|
||||
const root = resolveStateDir(env);
|
||||
return path.join(root, "agents", id, "agent");
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeEach, vi } from "vitest";
|
||||
import { resetProviderRuntimeHookCacheForTest } from "../plugins/provider-runtime.js";
|
||||
import { __setModelCatalogImportForTest, resetModelCatalogCacheForTest } from "./model-catalog.js";
|
||||
|
||||
export type PiSdkModule = typeof import("./pi-model-discovery.js");
|
||||
@ -14,11 +15,13 @@ vi.mock("./agent-paths.js", () => ({
|
||||
export function installModelCatalogTestHooks() {
|
||||
beforeEach(() => {
|
||||
resetModelCatalogCacheForTest();
|
||||
resetProviderRuntimeHookCacheForTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__setModelCatalogImportForTest();
|
||||
resetModelCatalogCacheForTest();
|
||||
resetProviderRuntimeHookCacheForTest();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { resolveProviderBuiltInModelSuppression } from "../plugins/provider-runtime.js";
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
import { normalizeProviderId } from "./provider-id.js";
|
||||
|
||||
function resolveBuiltInModelSuppression(params: { provider?: string | null; id?: string | null }) {
|
||||
const provider = normalizeProviderId(params.provider?.trim().toLowerCase() ?? "");
|
||||
|
||||
@ -1,2 +1,5 @@
|
||||
export { resolveProviderPluginChoice } from "../../../plugins/provider-wizard.js";
|
||||
export { resolvePluginProviders } from "../../../plugins/providers.js";
|
||||
export {
|
||||
resolveOwningPluginIdsForProvider,
|
||||
resolvePluginProviders,
|
||||
} from "../../../plugins/providers.js";
|
||||
|
||||
@ -7,9 +7,11 @@ vi.mock("../../auth-choice.preferred-provider.js", () => ({
|
||||
resolvePreferredProviderForAuthChoice,
|
||||
}));
|
||||
|
||||
const resolveOwningPluginIdsForProvider = vi.hoisted(() => vi.fn(() => undefined));
|
||||
const resolveProviderPluginChoice = vi.hoisted(() => vi.fn());
|
||||
const resolvePluginProviders = vi.hoisted(() => vi.fn(() => []));
|
||||
vi.mock("./auth-choice.plugin-providers.runtime.js", () => ({
|
||||
resolveOwningPluginIdsForProvider,
|
||||
resolveProviderPluginChoice,
|
||||
resolvePluginProviders,
|
||||
PROVIDER_PLUGIN_CHOICE_PREFIX: "provider-plugin:",
|
||||
@ -30,6 +32,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
it("loads plugin providers for provider-plugin auth choices", async () => {
|
||||
const runtime = createRuntime();
|
||||
const runNonInteractive = vi.fn(async () => ({ plugins: { allow: ["vllm"] } }));
|
||||
resolveOwningPluginIdsForProvider.mockReturnValue(["vllm"] as never);
|
||||
resolvePluginProviders.mockReturnValue([{ id: "vllm", pluginId: "vllm" }] as never);
|
||||
resolveProviderPluginChoice.mockReturnValue({
|
||||
provider: { id: "vllm", pluginId: "vllm", label: "vLLM" },
|
||||
@ -46,7 +49,18 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
|
||||
expect(resolveOwningPluginIdsForProvider).toHaveBeenCalledOnce();
|
||||
expect(resolveOwningPluginIdsForProvider).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "vllm",
|
||||
}),
|
||||
);
|
||||
expect(resolvePluginProviders).toHaveBeenCalledOnce();
|
||||
expect(resolvePluginProviders).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["vllm"],
|
||||
}),
|
||||
);
|
||||
expect(resolveProviderPluginChoice).toHaveBeenCalledOnce();
|
||||
expect(runNonInteractive).toHaveBeenCalledOnce();
|
||||
expect(result).toEqual({ plugins: { allow: ["vllm"] } });
|
||||
|
||||
@ -79,11 +79,20 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
||||
params.nextConfig,
|
||||
preferredProviderId,
|
||||
);
|
||||
const { resolveProviderPluginChoice, resolvePluginProviders } = await loadPluginProviderRuntime();
|
||||
const { resolveOwningPluginIdsForProvider, resolveProviderPluginChoice, resolvePluginProviders } =
|
||||
await loadPluginProviderRuntime();
|
||||
const owningPluginIds = preferredProviderId
|
||||
? resolveOwningPluginIdsForProvider({
|
||||
provider: preferredProviderId,
|
||||
config: resolutionConfig,
|
||||
workspaceDir,
|
||||
})
|
||||
: undefined;
|
||||
const providerChoice = resolveProviderPluginChoice({
|
||||
providers: resolvePluginProviders({
|
||||
config: resolutionConfig,
|
||||
workspaceDir,
|
||||
onlyPluginIds: owningPluginIds,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
}),
|
||||
|
||||
@ -73,7 +73,7 @@ async function runTelegramAnnounceTurn(params: {
|
||||
|
||||
describe("runCronIsolatedAgentTurn", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue(undefined);
|
||||
vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue("off");
|
||||
setupIsolatedAgentTurnMocks({ fast: true });
|
||||
});
|
||||
|
||||
|
||||
@ -126,7 +126,7 @@ async function expectInvalidModel(home: string, model: string) {
|
||||
|
||||
describe("cron model formatting and precedence edge cases", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue(undefined);
|
||||
vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue("off");
|
||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
||||
vi.mocked(loadModelCatalog).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
@ -262,7 +262,7 @@ async function assertExplicitTelegramTargetDelivery(params: {
|
||||
|
||||
describe("runCronIsolatedAgentTurn", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue(undefined);
|
||||
vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue("off");
|
||||
setupIsolatedAgentTurnMocks();
|
||||
});
|
||||
|
||||
|
||||
@ -16,10 +16,6 @@ import {
|
||||
} from "./isolated-agent.test-harness.js";
|
||||
import type { CronJob } from "./types.js";
|
||||
|
||||
let resolveThinkingDefaultSpy: ReturnType<
|
||||
typeof vi.spyOn<typeof modelSelection, "resolveThinkingDefault">
|
||||
>;
|
||||
|
||||
function makeDeps(): CliDeps {
|
||||
return {
|
||||
sendMessageSlack: vi.fn(),
|
||||
@ -168,9 +164,7 @@ async function runStoredOverrideAndExpectModel(params: {
|
||||
|
||||
describe("runCronIsolatedAgentTurn", () => {
|
||||
beforeEach(() => {
|
||||
resolveThinkingDefaultSpy = vi
|
||||
.spyOn(modelSelection, "resolveThinkingDefault")
|
||||
.mockReturnValue(undefined);
|
||||
vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue("off");
|
||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
||||
vi.mocked(loadModelCatalog).mockResolvedValue([]);
|
||||
});
|
||||
@ -513,7 +507,7 @@ describe("runCronIsolatedAgentTurn", () => {
|
||||
|
||||
it("passes through the resolved default thinking level", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
resolveThinkingDefaultSpy.mockReturnValueOnce("low");
|
||||
vi.mocked(modelSelection.resolveThinkingDefault).mockReturnValueOnce("low");
|
||||
|
||||
await runCronTurn(home, {
|
||||
jobPayload: DEFAULT_AGENT_TURN_PAYLOAD,
|
||||
|
||||
@ -363,7 +363,7 @@ export function resetRunCronIsolatedAgentTurnHarness(): void {
|
||||
resolveConfiguredModelRefMock.mockReturnValue({ provider: "openai", model: "gpt-4" });
|
||||
resolveAllowedModelRefMock.mockReturnValue({ ref: { provider: "openai", model: "gpt-4" } });
|
||||
resolveHooksGmailModelMock.mockReturnValue(null);
|
||||
resolveThinkingDefaultMock.mockReturnValue(undefined);
|
||||
resolveThinkingDefaultMock.mockReturnValue("off");
|
||||
getModelRefStatusMock.mockReturnValue({ allowed: false });
|
||||
isCliProviderMock.mockReturnValue(false);
|
||||
|
||||
|
||||
@ -9,6 +9,10 @@ async function readRepoFile(path: string): Promise<string> {
|
||||
return readFile(resolve(repoRoot, path), "utf8");
|
||||
}
|
||||
|
||||
function indexOfPattern(source: string, pattern: RegExp): number {
|
||||
return source.search(pattern);
|
||||
}
|
||||
|
||||
describe("docker build cache layout", () => {
|
||||
it("keeps the root dependency layer independent from scripts changes", async () => {
|
||||
const dockerfile = await readRepoFile("Dockerfile");
|
||||
@ -29,8 +33,11 @@ describe("docker build cache layout", () => {
|
||||
"scripts/docker/cleanup-smoke/Dockerfile",
|
||||
]) {
|
||||
const dockerfile = await readRepoFile(path);
|
||||
expect(dockerfile, `${path} should use a shared pnpm store cache`).toContain(
|
||||
"--mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked",
|
||||
expect(
|
||||
dockerfile,
|
||||
`${path} should use a shared pnpm store cache under the active user's home`,
|
||||
).toMatch(
|
||||
/--mount=type=cache,id=openclaw-pnpm-store,target=\/(?:root|home\/appuser)\/\.local\/share\/pnpm\/store,sharing=locked/,
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -87,23 +94,41 @@ describe("docker build cache layout", () => {
|
||||
const installIndex = dockerfile.indexOf("pnpm install --frozen-lockfile");
|
||||
|
||||
expect(
|
||||
dockerfile.indexOf("COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./"),
|
||||
).toBeLessThan(installIndex);
|
||||
expect(dockerfile.indexOf("COPY ui/package.json ./ui/package.json")).toBeLessThan(installIndex);
|
||||
expect(
|
||||
dockerfile.indexOf(
|
||||
"COPY extensions/memory-core/package.json ./extensions/memory-core/package.json",
|
||||
indexOfPattern(
|
||||
dockerfile,
|
||||
/^COPY(?:\s+--chown=\S+)?\s+package\.json pnpm-lock\.yaml pnpm-workspace\.yaml \.\/$/m,
|
||||
),
|
||||
).toBeLessThan(installIndex);
|
||||
expect(
|
||||
dockerfile.indexOf(
|
||||
"COPY tsconfig.json tsconfig.plugin-sdk.dts.json tsdown.config.ts vitest.config.ts vitest.e2e.config.ts openclaw.mjs ./",
|
||||
indexOfPattern(
|
||||
dockerfile,
|
||||
/^COPY(?:\s+--chown=\S+)?\s+ui\/package\.json \.\/ui\/package\.json$/m,
|
||||
),
|
||||
).toBeLessThan(installIndex);
|
||||
expect(
|
||||
indexOfPattern(
|
||||
dockerfile,
|
||||
/^COPY(?:\s+--chown=\S+)?\s+extensions\/memory-core\/package\.json \.\/extensions\/memory-core\/package\.json$/m,
|
||||
),
|
||||
).toBeLessThan(installIndex);
|
||||
expect(
|
||||
indexOfPattern(
|
||||
dockerfile,
|
||||
/^COPY(?:\s+--chown=\S+)?\s+tsconfig\.json tsconfig\.plugin-sdk\.dts\.json tsdown\.config\.ts vitest\.config\.ts vitest\.e2e\.config\.ts openclaw\.mjs \.\/$/m,
|
||||
),
|
||||
).toBeGreaterThan(installIndex);
|
||||
expect(dockerfile.indexOf("COPY src ./src")).toBeGreaterThan(installIndex);
|
||||
expect(dockerfile.indexOf("COPY test ./test")).toBeGreaterThan(installIndex);
|
||||
expect(dockerfile.indexOf("COPY scripts ./scripts")).toBeGreaterThan(installIndex);
|
||||
expect(dockerfile.indexOf("COPY ui ./ui")).toBeGreaterThan(installIndex);
|
||||
expect(indexOfPattern(dockerfile, /^COPY(?:\s+--chown=\S+)?\s+src \.\/src$/m)).toBeGreaterThan(
|
||||
installIndex,
|
||||
);
|
||||
expect(
|
||||
indexOfPattern(dockerfile, /^COPY(?:\s+--chown=\S+)?\s+test \.\/test$/m),
|
||||
).toBeGreaterThan(installIndex);
|
||||
expect(
|
||||
indexOfPattern(dockerfile, /^COPY(?:\s+--chown=\S+)?\s+scripts \.\/scripts$/m),
|
||||
).toBeGreaterThan(installIndex);
|
||||
expect(indexOfPattern(dockerfile, /^COPY(?:\s+--chown=\S+)?\s+ui \.\/ui$/m)).toBeGreaterThan(
|
||||
installIndex,
|
||||
);
|
||||
});
|
||||
|
||||
it("copies manifests before install in the qr-import image", async () => {
|
||||
@ -111,17 +136,28 @@ describe("docker build cache layout", () => {
|
||||
const installIndex = dockerfile.indexOf("pnpm install --frozen-lockfile");
|
||||
|
||||
expect(
|
||||
dockerfile.indexOf("COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./"),
|
||||
indexOfPattern(
|
||||
dockerfile,
|
||||
/^COPY(?:\s+--chown=\S+)?\s+package\.json pnpm-lock\.yaml pnpm-workspace\.yaml \.\/$/m,
|
||||
),
|
||||
).toBeLessThan(installIndex);
|
||||
expect(
|
||||
indexOfPattern(
|
||||
dockerfile,
|
||||
/^COPY(?:\s+--chown=\S+)?\s+ui\/package\.json \.\/ui\/package\.json$/m,
|
||||
),
|
||||
).toBeLessThan(installIndex);
|
||||
expect(dockerfile.indexOf("COPY ui/package.json ./ui/package.json")).toBeLessThan(installIndex);
|
||||
expect(dockerfile).toContain(
|
||||
"This image only exercises the root qrcode-terminal dependency path.",
|
||||
);
|
||||
expect(
|
||||
dockerfile.indexOf(
|
||||
"COPY extensions/memory-core/package.json ./extensions/memory-core/package.json",
|
||||
indexOfPattern(
|
||||
dockerfile,
|
||||
/^COPY(?:\s+--chown=\S+)?\s+extensions\/memory-core\/package\.json \.\/extensions\/memory-core\/package\.json$/m,
|
||||
),
|
||||
).toBe(-1);
|
||||
expect(dockerfile.indexOf("COPY . .")).toBeGreaterThan(installIndex);
|
||||
expect(indexOfPattern(dockerfile, /^COPY(?:\s+--chown=\S+)?\s+\.\s+\.$/m)).toBeGreaterThan(
|
||||
installIndex,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -13,14 +13,14 @@ import { clearMediaUnderstandingBinaryCacheForTests } from "./runner.js";
|
||||
import { createSafeAudioFixtureBuffer } from "./runner.test-utils.js";
|
||||
|
||||
const resolveApiKeyForProviderMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({
|
||||
vi.fn<typeof resolveApiKeyForProvider>(async () => ({
|
||||
apiKey: "test-key", // pragma: allowlist secret
|
||||
source: "test",
|
||||
mode: "api-key",
|
||||
})),
|
||||
);
|
||||
const hasAvailableAuthForProviderMock = vi.hoisted(() =>
|
||||
vi.fn(async (...args: unknown[]) => {
|
||||
vi.fn(async (...args: Parameters<typeof resolveApiKeyForProvider>) => {
|
||||
const resolved = await resolveApiKeyForProviderMock(...args);
|
||||
return Boolean(resolved?.apiKey);
|
||||
}),
|
||||
|
||||
17
src/plugin-sdk/ollama-setup.ts
Normal file
17
src/plugin-sdk/ollama-setup.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export type {
|
||||
OpenClawPluginApi,
|
||||
ProviderAuthContext,
|
||||
ProviderAuthMethodNonInteractiveContext,
|
||||
ProviderAuthResult,
|
||||
ProviderDiscoveryContext,
|
||||
} from "../plugins/types.js";
|
||||
|
||||
export {
|
||||
OLLAMA_DEFAULT_BASE_URL,
|
||||
OLLAMA_DEFAULT_MODEL,
|
||||
configureOllamaNonInteractive,
|
||||
ensureOllamaModelPulled,
|
||||
promptAndConfigureOllama,
|
||||
} from "../commands/ollama-setup.js";
|
||||
|
||||
export { buildOllamaProvider } from "../agents/models-config.providers.discovery.js";
|
||||
@ -169,9 +169,8 @@ rootExports = new Proxy(target, {
|
||||
},
|
||||
ownKeys() {
|
||||
const keys = new Set(Reflect.ownKeys(target));
|
||||
const monolithic = getMonolithicSdk();
|
||||
if (monolithic) {
|
||||
for (const key of Reflect.ownKeys(monolithic)) {
|
||||
if (monolithicSdk && typeof monolithicSdk === "object") {
|
||||
for (const key of Reflect.ownKeys(monolithicSdk)) {
|
||||
if (!keys.has(key)) {
|
||||
keys.add(key);
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ export type {
|
||||
SshSandboxSession,
|
||||
SshSandboxSettings,
|
||||
} from "../agents/sandbox.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
export {
|
||||
buildExecRemoteCommand,
|
||||
|
||||
23
src/plugin-sdk/self-hosted-provider-setup.ts
Normal file
23
src/plugin-sdk/self-hosted-provider-setup.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export type {
|
||||
OpenClawPluginApi,
|
||||
ProviderAuthContext,
|
||||
ProviderAuthMethodNonInteractiveContext,
|
||||
ProviderAuthResult,
|
||||
ProviderDiscoveryContext,
|
||||
} from "../plugins/types.js";
|
||||
|
||||
export {
|
||||
applyProviderDefaultModel,
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||
discoverOpenAICompatibleSelfHostedProvider,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProviderAuth,
|
||||
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||
SELF_HOSTED_DEFAULT_COST,
|
||||
SELF_HOSTED_DEFAULT_MAX_TOKENS,
|
||||
} from "../commands/self-hosted-provider-setup.js";
|
||||
|
||||
export {
|
||||
buildSglangProvider,
|
||||
buildVllmProvider,
|
||||
} from "../agents/models-config.providers.discovery.js";
|
||||
@ -11,8 +11,10 @@ import * as imessageSdk from "openclaw/plugin-sdk/imessage";
|
||||
import * as lineSdk from "openclaw/plugin-sdk/line";
|
||||
import * as msteamsSdk from "openclaw/plugin-sdk/msteams";
|
||||
import * as nostrSdk from "openclaw/plugin-sdk/nostr";
|
||||
import * as ollamaSetupSdk from "openclaw/plugin-sdk/ollama-setup";
|
||||
import * as providerSetupSdk from "openclaw/plugin-sdk/provider-setup";
|
||||
import * as sandboxSdk from "openclaw/plugin-sdk/sandbox";
|
||||
import * as selfHostedProviderSetupSdk from "openclaw/plugin-sdk/self-hosted-provider-setup";
|
||||
import * as signalSdk from "openclaw/plugin-sdk/signal";
|
||||
import * as slackSdk from "openclaw/plugin-sdk/slack";
|
||||
import * as telegramSdk from "openclaw/plugin-sdk/telegram";
|
||||
@ -62,6 +64,23 @@ describe("plugin-sdk subpath exports", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("exports narrow self-hosted provider setup helpers", () => {
|
||||
expect(typeof selfHostedProviderSetupSdk.buildVllmProvider).toBe("function");
|
||||
expect(typeof selfHostedProviderSetupSdk.buildSglangProvider).toBe("function");
|
||||
expect(typeof selfHostedProviderSetupSdk.discoverOpenAICompatibleSelfHostedProvider).toBe(
|
||||
"function",
|
||||
);
|
||||
expect(
|
||||
typeof selfHostedProviderSetupSdk.configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||
).toBe("function");
|
||||
});
|
||||
|
||||
it("exports narrow Ollama setup helpers", () => {
|
||||
expect(typeof ollamaSetupSdk.buildOllamaProvider).toBe("function");
|
||||
expect(typeof ollamaSetupSdk.configureOllamaNonInteractive).toBe("function");
|
||||
expect(typeof ollamaSetupSdk.ensureOllamaModelPulled).toBe("function");
|
||||
});
|
||||
|
||||
it("exports sandbox helpers from the dedicated subpath", () => {
|
||||
expect(typeof sandboxSdk.registerSandboxBackend).toBe("function");
|
||||
expect(typeof sandboxSdk.runPluginCommandWithTimeout).toBe("function");
|
||||
|
||||
@ -32,6 +32,23 @@ vi.mock("openclaw/plugin-sdk/provider-setup", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/self-hosted-provider-setup", async () => {
|
||||
const actual = await vi.importActual<object>("openclaw/plugin-sdk/self-hosted-provider-setup");
|
||||
return {
|
||||
...actual,
|
||||
buildVllmProvider: (...args: unknown[]) => buildVllmProviderMock(...args),
|
||||
buildSglangProvider: (...args: unknown[]) => buildSglangProviderMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/ollama-setup", async () => {
|
||||
const actual = await vi.importActual<object>("openclaw/plugin-sdk/ollama-setup");
|
||||
return {
|
||||
...actual,
|
||||
buildOllamaProvider: (...args: unknown[]) => buildOllamaProviderMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
const qwenPortalPlugin = (await import("../../../extensions/qwen-portal-auth/index.js")).default;
|
||||
const githubCopilotPlugin = (await import("../../../extensions/github-copilot/index.js")).default;
|
||||
const ollamaPlugin = (await import("../../../extensions/ollama/index.js")).default;
|
||||
|
||||
97
src/plugins/provider-catalog-metadata.ts
Normal file
97
src/plugins/provider-catalog-metadata.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type {
|
||||
ProviderAugmentModelCatalogContext,
|
||||
ProviderBuiltInModelSuppressionContext,
|
||||
} from "./types.js";
|
||||
|
||||
const OPENAI_PROVIDER_ID = "openai";
|
||||
const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
||||
const OPENAI_DIRECT_SPARK_MODEL_ID = "gpt-5.3-codex-spark";
|
||||
const SUPPRESSED_SPARK_PROVIDERS = new Set(["openai", "azure-openai-responses"]);
|
||||
|
||||
function findCatalogTemplate(params: {
|
||||
entries: ReadonlyArray<{ provider: string; id: string }>;
|
||||
providerId: string;
|
||||
templateIds: readonly string[];
|
||||
}) {
|
||||
return params.templateIds
|
||||
.map((templateId) =>
|
||||
params.entries.find(
|
||||
(entry) =>
|
||||
entry.provider.toLowerCase() === params.providerId.toLowerCase() &&
|
||||
entry.id.toLowerCase() === templateId.toLowerCase(),
|
||||
),
|
||||
)
|
||||
.find((entry) => entry !== undefined);
|
||||
}
|
||||
|
||||
export function resolveBundledProviderBuiltInModelSuppression(
|
||||
context: ProviderBuiltInModelSuppressionContext,
|
||||
) {
|
||||
if (
|
||||
!SUPPRESSED_SPARK_PROVIDERS.has(normalizeProviderId(context.provider)) ||
|
||||
context.modelId.toLowerCase() !== OPENAI_DIRECT_SPARK_MODEL_ID
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
suppress: true,
|
||||
errorMessage: `Unknown model: ${context.provider}/${OPENAI_DIRECT_SPARK_MODEL_ID}. ${OPENAI_DIRECT_SPARK_MODEL_ID} is only supported via openai-codex OAuth. Use openai-codex/${OPENAI_DIRECT_SPARK_MODEL_ID}.`,
|
||||
};
|
||||
}
|
||||
|
||||
export function augmentBundledProviderCatalog(
|
||||
context: ProviderAugmentModelCatalogContext,
|
||||
): ProviderAugmentModelCatalogContext["entries"] {
|
||||
const openAiGpt54Template = findCatalogTemplate({
|
||||
entries: context.entries,
|
||||
providerId: OPENAI_PROVIDER_ID,
|
||||
templateIds: ["gpt-5.2"],
|
||||
});
|
||||
const openAiGpt54ProTemplate = findCatalogTemplate({
|
||||
entries: context.entries,
|
||||
providerId: OPENAI_PROVIDER_ID,
|
||||
templateIds: ["gpt-5.2-pro", "gpt-5.2"],
|
||||
});
|
||||
const openAiCodexGpt54Template = findCatalogTemplate({
|
||||
entries: context.entries,
|
||||
providerId: OPENAI_CODEX_PROVIDER_ID,
|
||||
templateIds: ["gpt-5.3-codex", "gpt-5.2-codex"],
|
||||
});
|
||||
const openAiCodexSparkTemplate = findCatalogTemplate({
|
||||
entries: context.entries,
|
||||
providerId: OPENAI_CODEX_PROVIDER_ID,
|
||||
templateIds: ["gpt-5.3-codex", "gpt-5.2-codex"],
|
||||
});
|
||||
|
||||
return [
|
||||
openAiGpt54Template
|
||||
? {
|
||||
...openAiGpt54Template,
|
||||
id: "gpt-5.4",
|
||||
name: "gpt-5.4",
|
||||
}
|
||||
: undefined,
|
||||
openAiGpt54ProTemplate
|
||||
? {
|
||||
...openAiGpt54ProTemplate,
|
||||
id: "gpt-5.4-pro",
|
||||
name: "gpt-5.4-pro",
|
||||
}
|
||||
: undefined,
|
||||
openAiCodexGpt54Template
|
||||
? {
|
||||
...openAiCodexGpt54Template,
|
||||
id: "gpt-5.4",
|
||||
name: "gpt-5.4",
|
||||
}
|
||||
: undefined,
|
||||
openAiCodexSparkTemplate
|
||||
? {
|
||||
...openAiCodexSparkTemplate,
|
||||
id: OPENAI_DIRECT_SPARK_MODEL_ID,
|
||||
name: OPENAI_DIRECT_SPARK_MODEL_ID,
|
||||
}
|
||||
: undefined,
|
||||
].filter((entry): entry is NonNullable<typeof entry> => entry !== undefined);
|
||||
}
|
||||
@ -1,13 +1,24 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderPlugin, ProviderRuntimeModel } from "./types.js";
|
||||
|
||||
const resolvePluginProvidersMock = vi.fn((_: unknown) => [] as ProviderPlugin[]);
|
||||
const resolveOwningPluginIdsForProviderMock = vi.fn(
|
||||
(_: unknown) => undefined as string[] | undefined,
|
||||
type ResolvePluginProviders = typeof import("./providers.js").resolvePluginProviders;
|
||||
type ResolveNonBundledProviderPluginIds =
|
||||
typeof import("./providers.js").resolveNonBundledProviderPluginIds;
|
||||
type ResolveOwningPluginIdsForProvider =
|
||||
typeof import("./providers.js").resolveOwningPluginIdsForProvider;
|
||||
|
||||
const resolvePluginProvidersMock = vi.fn<ResolvePluginProviders>((_) => [] as ProviderPlugin[]);
|
||||
const resolveNonBundledProviderPluginIdsMock = vi.fn<ResolveNonBundledProviderPluginIds>(
|
||||
(_) => [] as string[],
|
||||
);
|
||||
const resolveOwningPluginIdsForProviderMock = vi.fn<ResolveOwningPluginIdsForProvider>(
|
||||
(_) => undefined as string[] | undefined,
|
||||
);
|
||||
|
||||
vi.mock("./providers.js", () => ({
|
||||
resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never),
|
||||
resolveNonBundledProviderPluginIds: (params: unknown) =>
|
||||
resolveNonBundledProviderPluginIdsMock(params as never),
|
||||
resolveOwningPluginIdsForProvider: (params: unknown) =>
|
||||
resolveOwningPluginIdsForProviderMock(params as never),
|
||||
}));
|
||||
@ -30,6 +41,7 @@ import {
|
||||
normalizeProviderResolvedModelWithPlugin,
|
||||
prepareProviderDynamicModel,
|
||||
prepareProviderRuntimeAuth,
|
||||
resetProviderRuntimeHookCacheForTest,
|
||||
refreshProviderOAuthCredentialWithPlugin,
|
||||
resolveProviderRuntimePlugin,
|
||||
runProviderDynamicModel,
|
||||
@ -51,8 +63,11 @@ const MODEL: ProviderRuntimeModel = {
|
||||
|
||||
describe("provider-runtime", () => {
|
||||
beforeEach(() => {
|
||||
resetProviderRuntimeHookCacheForTest();
|
||||
resolvePluginProvidersMock.mockReset();
|
||||
resolvePluginProvidersMock.mockReturnValue([]);
|
||||
resolveNonBundledProviderPluginIdsMock.mockReset();
|
||||
resolveNonBundledProviderPluginIdsMock.mockReturnValue([]);
|
||||
resolveOwningPluginIdsForProviderMock.mockReset();
|
||||
resolveOwningPluginIdsForProviderMock.mockReturnValue(undefined);
|
||||
});
|
||||
@ -98,7 +113,7 @@ describe("provider-runtime", () => {
|
||||
});
|
||||
|
||||
it("dispatches runtime hooks for the matched provider", async () => {
|
||||
resolveOwningPluginIdsForProviderMock.mockImplementation((params: { provider?: string }) => {
|
||||
resolveOwningPluginIdsForProviderMock.mockImplementation((params) => {
|
||||
if (params.provider === "demo") {
|
||||
return ["demo"];
|
||||
}
|
||||
@ -450,4 +465,44 @@ describe("provider-runtime", () => {
|
||||
expect(resolveUsageAuth).toHaveBeenCalledTimes(1);
|
||||
expect(fetchUsageSnapshot).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("resolves bundled catalog hooks without loading provider plugins", async () => {
|
||||
expect(
|
||||
resolveProviderBuiltInModelSuppression({
|
||||
env: process.env,
|
||||
context: {
|
||||
env: process.env,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.3-codex-spark",
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
suppress: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
augmentModelCatalogWithProviderPlugins({
|
||||
env: process.env,
|
||||
context: {
|
||||
env: process.env,
|
||||
entries: [
|
||||
{ provider: "openai", id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ provider: "openai", id: "gpt-5.2-pro", name: "GPT-5.2 Pro" },
|
||||
{ provider: "openai-codex", id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
|
||||
{ provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" },
|
||||
{ provider: "openai-codex", id: "gpt-5.4", name: "gpt-5.4" },
|
||||
{
|
||||
provider: "openai-codex",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
name: "gpt-5.3-codex-spark",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(resolvePluginProvidersMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import type { AuthProfileCredential, OAuthCredential } from "../agents/auth-profiles/types.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveOwningPluginIdsForProvider, resolvePluginProviders } from "./providers.js";
|
||||
import {
|
||||
augmentBundledProviderCatalog,
|
||||
resolveBundledProviderBuiltInModelSuppression,
|
||||
} from "./provider-catalog-metadata.js";
|
||||
import {
|
||||
resolveNonBundledProviderPluginIds,
|
||||
resolveOwningPluginIdsForProvider,
|
||||
resolvePluginProviders,
|
||||
} from "./providers.js";
|
||||
import type {
|
||||
ProviderAuthDoctorHintContext,
|
||||
ProviderAugmentModelCatalogContext,
|
||||
@ -33,19 +41,104 @@ function matchesProviderId(provider: ProviderPlugin, providerId: string): boolea
|
||||
return (provider.aliases ?? []).some((alias) => normalizeProviderId(alias) === normalized);
|
||||
}
|
||||
|
||||
let cachedHookProvidersWithoutConfig = new WeakMap<
|
||||
NodeJS.ProcessEnv,
|
||||
Map<string, ProviderPlugin[]>
|
||||
>();
|
||||
let cachedHookProvidersByConfig = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>
|
||||
>();
|
||||
|
||||
function resolveHookProviderCacheBucket(params: {
|
||||
config?: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}) {
|
||||
if (!params.config) {
|
||||
let bucket = cachedHookProvidersWithoutConfig.get(params.env);
|
||||
if (!bucket) {
|
||||
bucket = new Map<string, ProviderPlugin[]>();
|
||||
cachedHookProvidersWithoutConfig.set(params.env, bucket);
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
|
||||
let envBuckets = cachedHookProvidersByConfig.get(params.config);
|
||||
if (!envBuckets) {
|
||||
envBuckets = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
|
||||
cachedHookProvidersByConfig.set(params.config, envBuckets);
|
||||
}
|
||||
let bucket = envBuckets.get(params.env);
|
||||
if (!bucket) {
|
||||
bucket = new Map<string, ProviderPlugin[]>();
|
||||
envBuckets.set(params.env, bucket);
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
|
||||
function buildHookProviderCacheKey(params: { workspaceDir?: string; onlyPluginIds?: string[] }) {
|
||||
return `${params.workspaceDir ?? ""}::${JSON.stringify(params.onlyPluginIds ?? [])}`;
|
||||
}
|
||||
|
||||
export function resetProviderRuntimeHookCacheForTest(): void {
|
||||
cachedHookProvidersWithoutConfig = new WeakMap<
|
||||
NodeJS.ProcessEnv,
|
||||
Map<string, ProviderPlugin[]>
|
||||
>();
|
||||
cachedHookProvidersByConfig = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>
|
||||
>();
|
||||
}
|
||||
|
||||
function resolveProviderPluginsForHooks(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: string[];
|
||||
}): ProviderPlugin[] {
|
||||
return resolvePluginProviders({
|
||||
const env = params.env ?? process.env;
|
||||
const cacheBucket = resolveHookProviderCacheBucket({
|
||||
config: params.config,
|
||||
env,
|
||||
});
|
||||
const cacheKey = buildHookProviderCacheKey({
|
||||
workspaceDir: params.workspaceDir,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
});
|
||||
const cached = cacheBucket.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const resolved = resolvePluginProviders({
|
||||
...params,
|
||||
env,
|
||||
activate: false,
|
||||
cache: false,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
cacheBucket.set(cacheKey, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function resolveProviderPluginsForCatalogHooks(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderPlugin[] {
|
||||
const onlyPluginIds = resolveNonBundledProviderPluginIds({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
if (onlyPluginIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return resolveProviderPluginsForHooks({
|
||||
...params,
|
||||
onlyPluginIds,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveProviderRuntimePlugin(params: {
|
||||
@ -265,7 +358,11 @@ export function resolveProviderBuiltInModelSuppression(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderBuiltInModelSuppressionContext;
|
||||
}) {
|
||||
for (const plugin of resolveProviderPluginsForHooks(params)) {
|
||||
const bundledResult = resolveBundledProviderBuiltInModelSuppression(params.context);
|
||||
if (bundledResult?.suppress) {
|
||||
return bundledResult;
|
||||
}
|
||||
for (const plugin of resolveProviderPluginsForCatalogHooks(params)) {
|
||||
const result = plugin.suppressBuiltInModel?.(params.context);
|
||||
if (result?.suppress) {
|
||||
return result;
|
||||
@ -280,8 +377,10 @@ export async function augmentModelCatalogWithProviderPlugins(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderAugmentModelCatalogContext;
|
||||
}) {
|
||||
const supplemental = [] as ProviderAugmentModelCatalogContext["entries"];
|
||||
for (const plugin of resolveProviderPluginsForHooks(params)) {
|
||||
const supplemental = [
|
||||
...augmentBundledProviderCatalog(params.context),
|
||||
] as ProviderAugmentModelCatalogContext["entries"];
|
||||
for (const plugin of resolveProviderPluginsForCatalogHooks(params)) {
|
||||
const next = await plugin.augmentModelCatalog?.(params.context);
|
||||
if (!next || next.length === 0) {
|
||||
continue;
|
||||
|
||||
@ -125,6 +125,34 @@ describe("resolvePluginProviders", () => {
|
||||
expect(allow).not.toContain("workspace-provider");
|
||||
});
|
||||
|
||||
it("scopes bundled provider compat expansion to the requested plugin ids", () => {
|
||||
resolvePluginProviders({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["openrouter"],
|
||||
},
|
||||
},
|
||||
bundledProviderAllowlistCompat: true,
|
||||
onlyPluginIds: ["moonshot"],
|
||||
});
|
||||
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["moonshot"],
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
allow: expect.arrayContaining(["openrouter", "moonshot"]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const call = loadOpenClawPluginsMock.mock.calls.at(-1)?.[0];
|
||||
const allow = call?.config?.plugins?.allow;
|
||||
expect(allow).not.toContain("google");
|
||||
expect(allow).not.toContain("kilocode");
|
||||
});
|
||||
|
||||
it("maps provider ids to owning plugin ids via manifests", () => {
|
||||
loadPluginManifestRegistryMock.mockReturnValue({
|
||||
plugins: [
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { withBundledPluginAllowlistCompat } from "./bundled-compat.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
||||
import { loadOpenClawPlugins, type PluginLoadOptions } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
@ -62,14 +63,21 @@ function resolveBundledProviderCompatPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
onlyPluginIds?: string[];
|
||||
}): string[] {
|
||||
const onlyPluginIdSet = params.onlyPluginIds ? new Set(params.onlyPluginIds) : null;
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
return registry.plugins
|
||||
.filter((plugin) => plugin.origin === "bundled" && plugin.providers.length > 0)
|
||||
.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" &&
|
||||
plugin.providers.length > 0 &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)),
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
@ -99,6 +107,33 @@ export function resolveOwningPluginIdsForProvider(params: {
|
||||
return pluginIds.length > 0 ? pluginIds : undefined;
|
||||
}
|
||||
|
||||
export function resolveNonBundledProviderPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
|
||||
return registry.plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
plugin.origin !== "bundled" &&
|
||||
plugin.providers.length > 0 &&
|
||||
resolveEffectiveEnableState({
|
||||
id: plugin.id,
|
||||
origin: plugin.origin,
|
||||
config: normalizedConfig,
|
||||
rootConfig: params.config,
|
||||
}).enabled,
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function resolvePluginProviders(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
@ -116,6 +151,7 @@ export function resolvePluginProviders(params: {
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
})
|
||||
: [];
|
||||
const maybeAllowlistCompat = params.bundledProviderAllowlistCompat
|
||||
|
||||
@ -849,6 +849,10 @@ export type WebSearchProviderPlugin = {
|
||||
createTool: (ctx: WebSearchProviderContext) => WebSearchProviderToolDefinition | null;
|
||||
};
|
||||
|
||||
export type PluginWebSearchProviderEntry = WebSearchProviderPlugin & {
|
||||
pluginId: string;
|
||||
};
|
||||
|
||||
export type OpenClawPluginGatewayMethod = {
|
||||
method: string;
|
||||
handler: GatewayRequestHandler;
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
} from "./bundled-compat.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import type { WebSearchProviderPlugin } from "./types.js";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
|
||||
const BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS = [
|
||||
"brave",
|
||||
@ -114,7 +114,7 @@ export function resolvePluginWebSearchProviders(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
}): WebSearchProviderPlugin[] {
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const allowlistCompat = params.bundledAllowlistCompat
|
||||
? withBundledPluginAllowlistCompat({
|
||||
config: params.config,
|
||||
|
||||
@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { runSecretsApply } from "./apply.js";
|
||||
import type { SecretsApplyPlan } from "./plan.js";
|
||||
import { clearSecretsRuntimeSnapshot } from "./runtime.js";
|
||||
|
||||
const OPENAI_API_KEY_ENV_REF = {
|
||||
source: "env",
|
||||
@ -173,11 +174,13 @@ describe("secrets apply", () => {
|
||||
let fixture: ApplyFixture;
|
||||
|
||||
beforeEach(async () => {
|
||||
clearSecretsRuntimeSnapshot();
|
||||
fixture = await createApplyFixture();
|
||||
await seedDefaultApplyFixture(fixture);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
clearSecretsRuntimeSnapshot();
|
||||
await fs.rm(fixture.rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import * as webSearchProviders from "../plugins/web-search-providers.js";
|
||||
import * as secretResolve from "./resolve.js";
|
||||
import { createResolverContext } from "./runtime-shared.js";
|
||||
import { resolveRuntimeWebTools } from "./runtime-web-tools.js";
|
||||
@ -88,6 +89,32 @@ describe("runtime web tools resolution", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("skips loading web search providers when search config is absent", async () => {
|
||||
const providerSpy = vi.spyOn(webSearchProviders, "resolvePluginWebSearchProviders");
|
||||
|
||||
const { metadata } = await runRuntimeWebTools({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
firecrawl: {
|
||||
apiKey: { source: "env", provider: "default", id: "FIRECRAWL_API_KEY_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
FIRECRAWL_API_KEY: "firecrawl-runtime-key", // pragma: allowlist secret
|
||||
},
|
||||
});
|
||||
|
||||
expect(providerSpy).not.toHaveBeenCalled();
|
||||
expect(metadata.search.providerSource).toBe("none");
|
||||
expect(metadata.fetch.firecrawl.active).toBe(true);
|
||||
expect(metadata.fetch.firecrawl.apiKeySource).toBe("env");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
provider: "brave" as const,
|
||||
|
||||
@ -315,11 +315,13 @@ export async function resolveRuntimeWebTools(params: {
|
||||
const tools = isRecord(params.sourceConfig.tools) ? params.sourceConfig.tools : undefined;
|
||||
const web = isRecord(tools?.web) ? tools.web : undefined;
|
||||
const search = isRecord(web?.search) ? web.search : undefined;
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: params.context.env,
|
||||
bundledAllowlistCompat: true,
|
||||
});
|
||||
const providers = search
|
||||
? resolvePluginWebSearchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: params.context.env,
|
||||
bundledAllowlistCompat: true,
|
||||
})
|
||||
: [];
|
||||
|
||||
const searchMetadata: RuntimeWebSearchMetadata = {
|
||||
providerSource: "none",
|
||||
|
||||
@ -79,11 +79,14 @@ function clearActiveSecretsRuntimeState(): void {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
}
|
||||
|
||||
function collectCandidateAgentDirs(config: OpenClawConfig): string[] {
|
||||
function collectCandidateAgentDirs(
|
||||
config: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string[] {
|
||||
const dirs = new Set<string>();
|
||||
dirs.add(resolveUserPath(resolveOpenClawAgentDir()));
|
||||
dirs.add(resolveUserPath(resolveOpenClawAgentDir(env), env));
|
||||
for (const agentId of listAgentIds(config)) {
|
||||
dirs.add(resolveUserPath(resolveAgentDir(config, agentId)));
|
||||
dirs.add(resolveUserPath(resolveAgentDir(config, agentId, env), env));
|
||||
}
|
||||
return [...dirs];
|
||||
}
|
||||
@ -92,7 +95,7 @@ function resolveRefreshAgentDirs(
|
||||
config: OpenClawConfig,
|
||||
context: SecretsRuntimeRefreshContext,
|
||||
): string[] {
|
||||
const configDerived = collectCandidateAgentDirs(config);
|
||||
const configDerived = collectCandidateAgentDirs(config, context.env);
|
||||
if (!context.explicitAgentDirs || context.explicitAgentDirs.length === 0) {
|
||||
return configDerived;
|
||||
}
|
||||
@ -119,8 +122,12 @@ export async function prepareSecretsRuntimeSnapshot(params: {
|
||||
|
||||
const loadAuthStore = params.loadAuthStore ?? loadAuthProfileStoreForSecretsRuntime;
|
||||
const candidateDirs = params.agentDirs?.length
|
||||
? [...new Set(params.agentDirs.map((entry) => resolveUserPath(entry)))]
|
||||
: collectCandidateAgentDirs(resolvedConfig);
|
||||
? [
|
||||
...new Set(
|
||||
params.agentDirs.map((entry) => resolveUserPath(entry, params.env ?? process.env)),
|
||||
),
|
||||
]
|
||||
: collectCandidateAgentDirs(resolvedConfig, params.env ?? process.env);
|
||||
|
||||
const authStores: Array<{ agentDir: string; store: AuthProfileStore }> = [];
|
||||
for (const agentDir of candidateDirs) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user