From 1086acf3c24ee5d83481ac121b554a9dc33fdebf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 27 Mar 2026 17:56:50 +0000 Subject: [PATCH] fix: repair latest-main ci gate --- extensions/matrix/src/config-schema.ts | 4 +- extensions/mattermost/src/config-runtime.ts | 7 + extensions/mattermost/src/config-schema.ts | 126 ++++++++++++++- .../nextcloud-talk/src/config-schema.ts | 32 ++-- extensions/signal/src/accounts.ts | 2 +- extensions/zalo/src/config-schema.ts | 2 +- extensions/zalouser/src/config-schema.ts | 4 +- scripts/generate-bundled-plugin-metadata.mjs | 144 +++++++++-------- src/config/zod-schema.providers.ts | 45 ++++++ src/infra/exec-approval-forwarder.test.ts | 2 +- ...rovider-usage.auth.normalizes-keys.test.ts | 148 +++++++++++++++--- src/infra/provider-usage.auth.plugin.test.ts | 14 +- src/infra/provider-usage.auth.ts | 6 +- src/infra/restart.test.ts | 20 ++- src/library.test.ts | 91 ++++++----- src/library.ts | 69 ++++---- src/logging/config.ts | 12 +- src/plugin-sdk/index.bundle.test.ts | 10 +- .../bundled-plugin-metadata.generated.ts | 8 +- src/plugins/channel-plugin-ids.ts | 21 +-- src/plugins/commands.ts | 48 ++++-- .../contracts/catalog.contract.test.ts | 1 + src/plugins/provider-auth-storage.ts | 14 +- src/plugins/provider-runtime.test-support.ts | 2 +- src/plugins/provider-zai-endpoint.ts | 10 +- test/openclaw-npm-postpublish-verify.test.ts | 11 +- test/scripts/test-parallel.test.ts | 10 +- 27 files changed, 610 insertions(+), 253 deletions(-) create mode 100644 extensions/mattermost/src/config-runtime.ts diff --git a/extensions/matrix/src/config-schema.ts b/extensions/matrix/src/config-schema.ts index 1ffd5622955..cad6702daee 100644 --- a/extensions/matrix/src/config-schema.ts +++ b/extensions/matrix/src/config-schema.ts @@ -1,11 +1,11 @@ -import { ToolPolicySchema } from "openclaw/plugin-sdk/agent-config-primitives"; import { AllowFromListSchema, buildNestedDmConfigSchema, DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, -} from "openclaw/plugin-sdk/channel-config-primitives"; + ToolPolicySchema, +} from "openclaw/plugin-sdk/channel-config-schema"; import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input"; import { z } from "openclaw/plugin-sdk/zod"; diff --git a/extensions/mattermost/src/config-runtime.ts b/extensions/mattermost/src/config-runtime.ts new file mode 100644 index 00000000000..e1086453cdb --- /dev/null +++ b/extensions/mattermost/src/config-runtime.ts @@ -0,0 +1,7 @@ +export { + BlockStreamingCoalesceSchema, + DmPolicySchema, + GroupPolicySchema, + MarkdownConfigSchema, + requireOpenAllowFrom, +} from "openclaw/plugin-sdk/channel-config-schema"; diff --git a/extensions/mattermost/src/config-schema.ts b/extensions/mattermost/src/config-schema.ts index 97f44dabc49..1723e58011f 100644 --- a/extensions/mattermost/src/config-schema.ts +++ b/extensions/mattermost/src/config-schema.ts @@ -1 +1,125 @@ -export { MattermostConfigSchema } from "./config-schema-core.js"; +import { z } from "openclaw/plugin-sdk/zod"; +import { + BlockStreamingCoalesceSchema, + DmPolicySchema, + GroupPolicySchema, + MarkdownConfigSchema, + requireOpenAllowFrom, +} from "./config-runtime.js"; +import { buildSecretInputSchema } from "./secret-input.js"; + +function requireMattermostOpenAllowFrom(params: { + policy?: string; + allowFrom?: Array; + ctx: z.RefinementCtx; +}) { + requireOpenAllowFrom({ + policy: params.policy, + allowFrom: params.allowFrom, + ctx: params.ctx, + path: ["allowFrom"], + message: + 'channels.mattermost.dmPolicy="open" requires channels.mattermost.allowFrom to include "*"', + }); +} + +const DmChannelRetrySchema = z + .object({ + /** Maximum number of retry attempts for DM channel creation (default: 3) */ + maxRetries: z.number().int().min(0).max(10).optional(), + /** Initial delay in milliseconds before first retry (default: 1000) */ + initialDelayMs: z.number().int().min(100).max(60000).optional(), + /** Maximum delay in milliseconds between retries (default: 10000) */ + maxDelayMs: z.number().int().min(1000).max(60000).optional(), + /** Timeout for each individual DM channel creation request in milliseconds (default: 30000) */ + timeoutMs: z.number().int().min(5000).max(120000).optional(), + }) + .strict() + .refine( + (data) => { + if (data.initialDelayMs !== undefined && data.maxDelayMs !== undefined) { + return data.initialDelayMs <= data.maxDelayMs; + } + return true; + }, + { + message: "initialDelayMs must be less than or equal to maxDelayMs", + path: ["initialDelayMs"], + }, + ) + .optional(); + +const MattermostSlashCommandsSchema = z + .object({ + /** Enable native slash commands. "auto" resolves to false (opt-in). */ + native: z.union([z.boolean(), z.literal("auto")]).optional(), + /** Also register skill-based commands. */ + nativeSkills: z.union([z.boolean(), z.literal("auto")]).optional(), + /** Path for the callback endpoint on the gateway HTTP server. */ + callbackPath: z.string().optional(), + /** Explicit callback URL (e.g. behind reverse proxy). */ + callbackUrl: z.string().optional(), + }) + .strict() + .optional(); + +const MattermostAccountSchemaBase = z + .object({ + name: z.string().optional(), + capabilities: z.array(z.string()).optional(), + dangerouslyAllowNameMatching: z.boolean().optional(), + markdown: MarkdownConfigSchema, + enabled: z.boolean().optional(), + configWrites: z.boolean().optional(), + botToken: buildSecretInputSchema().optional(), + baseUrl: z.string().optional(), + chatmode: z.enum(["oncall", "onmessage", "onchar"]).optional(), + oncharPrefixes: z.array(z.string()).optional(), + requireMention: z.boolean().optional(), + dmPolicy: DmPolicySchema.optional().default("pairing"), + allowFrom: z.array(z.union([z.string(), z.number()])).optional(), + groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(), + groupPolicy: GroupPolicySchema.optional().default("allowlist"), + textChunkLimit: z.number().int().positive().optional(), + chunkMode: z.enum(["length", "newline"]).optional(), + blockStreaming: z.boolean().optional(), + blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), + replyToMode: z.enum(["off", "first", "all"]).optional(), + responsePrefix: z.string().optional(), + actions: z + .object({ + reactions: z.boolean().optional(), + }) + .optional(), + commands: MattermostSlashCommandsSchema, + interactions: z + .object({ + callbackBaseUrl: z.string().optional(), + allowedSourceIps: z.array(z.string()).optional(), + }) + .optional(), + /** Allow fetching from private/internal IP addresses (e.g. localhost). Required for self-hosted Mattermost on LAN/VPN. */ + allowPrivateNetwork: z.boolean().optional(), + /** Retry configuration for DM channel creation */ + dmChannelRetry: DmChannelRetrySchema, + }) + .strict(); + +const MattermostAccountSchema = MattermostAccountSchemaBase.superRefine((value, ctx) => { + requireMattermostOpenAllowFrom({ + policy: value.dmPolicy, + allowFrom: value.allowFrom, + ctx, + }); +}); + +export const MattermostConfigSchema = MattermostAccountSchemaBase.extend({ + accounts: z.record(z.string(), MattermostAccountSchema.optional()).optional(), + defaultAccount: z.string().optional(), +}).superRefine((value, ctx) => { + requireMattermostOpenAllowFrom({ + policy: value.dmPolicy, + allowFrom: value.allowFrom, + ctx, + }); +}); diff --git a/extensions/nextcloud-talk/src/config-schema.ts b/extensions/nextcloud-talk/src/config-schema.ts index 65367b29fb1..f05eb6fe344 100644 --- a/extensions/nextcloud-talk/src/config-schema.ts +++ b/extensions/nextcloud-talk/src/config-schema.ts @@ -1,33 +1,17 @@ -import { - ReplyRuntimeConfigSchemaShape, - ToolPolicySchema, -} from "openclaw/plugin-sdk/agent-config-primitives"; import { BlockStreamingCoalesceSchema, DmConfigSchema, DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, + ReplyRuntimeConfigSchemaShape, + ToolPolicySchema, requireOpenAllowFrom, -} from "openclaw/plugin-sdk/channel-config-primitives"; +} from "openclaw/plugin-sdk/channel-config-schema"; +import { requireChannelOpenAllowFrom } from "openclaw/plugin-sdk/extension-shared"; import { z } from "openclaw/plugin-sdk/zod"; import { buildSecretInputSchema } from "./secret-input.js"; -function requireNextcloudTalkOpenAllowFrom(params: { - policy?: string; - allowFrom?: string[]; - ctx: z.RefinementCtx; -}) { - requireOpenAllowFrom({ - policy: params.policy, - allowFrom: params.allowFrom, - ctx: params.ctx, - path: ["allowFrom"], - message: - 'channels.nextcloud-talk.dmPolicy="open" requires channels.nextcloud-talk.allowFrom to include "*"', - }); -} - export const NextcloudTalkRoomSchema = z .object({ requireMention: z.boolean().optional(), @@ -67,10 +51,12 @@ export const NextcloudTalkAccountSchemaBase = z export const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRefine( (value, ctx) => { - requireNextcloudTalkOpenAllowFrom({ + requireChannelOpenAllowFrom({ + channel: "nextcloud-talk", policy: value.dmPolicy, allowFrom: value.allowFrom, ctx, + requireOpenAllowFrom, }); }, ); @@ -79,9 +65,11 @@ export const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({ accounts: z.record(z.string(), NextcloudTalkAccountSchema.optional()).optional(), defaultAccount: z.string().optional(), }).superRefine((value, ctx) => { - requireNextcloudTalkOpenAllowFrom({ + requireChannelOpenAllowFrom({ + channel: "nextcloud-talk", policy: value.dmPolicy, allowFrom: value.allowFrom, ctx, + requireOpenAllowFrom, }); }); diff --git a/extensions/signal/src/accounts.ts b/extensions/signal/src/accounts.ts index da5c3509f7a..755a3c39a5a 100644 --- a/extensions/signal/src/accounts.ts +++ b/extensions/signal/src/accounts.ts @@ -4,7 +4,7 @@ import { resolveMergedAccountConfig, type OpenClawConfig, } from "openclaw/plugin-sdk/account-resolution"; -import type { SignalAccountConfig } from "openclaw/plugin-sdk/signal-core"; +import type { SignalAccountConfig } from "./runtime-api.js"; export type ResolvedSignalAccount = { accountId: string; diff --git a/extensions/zalo/src/config-schema.ts b/extensions/zalo/src/config-schema.ts index c387045a347..ba8a39c1ef1 100644 --- a/extensions/zalo/src/config-schema.ts +++ b/extensions/zalo/src/config-schema.ts @@ -4,7 +4,7 @@ import { DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, -} from "openclaw/plugin-sdk/channel-config-primitives"; +} from "openclaw/plugin-sdk/channel-config-schema"; import { z } from "openclaw/plugin-sdk/zod"; import { buildSecretInputSchema } from "./secret-input.js"; diff --git a/extensions/zalouser/src/config-schema.ts b/extensions/zalouser/src/config-schema.ts index 39a51a7b2d9..3c7f5efc32c 100644 --- a/extensions/zalouser/src/config-schema.ts +++ b/extensions/zalouser/src/config-schema.ts @@ -1,11 +1,11 @@ -import { ToolPolicySchema } from "openclaw/plugin-sdk/agent-config-primitives"; import { AllowFromListSchema, buildCatchallMultiAccountChannelSchema, DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, -} from "openclaw/plugin-sdk/channel-config-primitives"; + ToolPolicySchema, +} from "openclaw/plugin-sdk/channel-config-schema"; import { z } from "openclaw/plugin-sdk/zod"; const groupConfigSchema = z.object({ diff --git a/scripts/generate-bundled-plugin-metadata.mjs b/scripts/generate-bundled-plugin-metadata.mjs index c320006e805..db200ba4f3d 100644 --- a/scripts/generate-bundled-plugin-metadata.mjs +++ b/scripts/generate-bundled-plugin-metadata.mjs @@ -1,7 +1,6 @@ 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"; @@ -27,7 +26,8 @@ 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([ +const PUBLIC_SURFACE_SOURCE_EXTENSIONS = new Set([".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"]); +const RUNTIME_SIDECAR_ARTIFACTS = new Set([ "helper-api.js", "light-runtime-api.js", "runtime-api.js", @@ -42,6 +42,53 @@ function rewriteEntryToBuiltPath(entry) { return normalized.replace(/\.[^.]+$/u, ".js"); } +function isTopLevelPublicSurfaceSource(name) { + if (!PUBLIC_SURFACE_SOURCE_EXTENSIONS.has(path.extname(name))) { + return false; + } + if (name.startsWith(".")) { + return false; + } + if (name.startsWith("test-")) { + return false; + } + if (name.includes(".test-")) { + return false; + } + if (name.endsWith(".d.ts")) { + return false; + } + return !/(\.test|\.spec)(\.[cm]?[jt]s)$/u.test(name); +} + +function collectTopLevelPublicSurfaceArtifacts(params) { + const excluded = new Set( + [params.sourceEntry, params.setupEntry] + .filter((entry) => typeof entry === "string" && entry.trim().length > 0) + .map((entry) => path.basename(entry)), + ); + const artifacts = fs + .readdirSync(params.pluginDir, { withFileTypes: true }) + .filter((entry) => entry.isFile()) + .map((entry) => entry.name) + .filter(isTopLevelPublicSurfaceSource) + .filter((entry) => !excluded.has(entry)) + .map(rewriteEntryToBuiltPath) + .filter((entry) => typeof entry === "string" && entry.length > 0) + .toSorted((left, right) => left.localeCompare(right)); + return artifacts.length > 0 ? artifacts : undefined; +} + +function collectRuntimeSidecarArtifacts(publicSurfaceArtifacts) { + if (!publicSurfaceArtifacts) { + return undefined; + } + const runtimeSidecarArtifacts = publicSurfaceArtifacts.filter((artifact) => + RUNTIME_SIDECAR_ARTIFACTS.has(artifact), + ); + return runtimeSidecarArtifacts.length > 0 ? runtimeSidecarArtifacts : undefined; +} + function deriveIdHint({ filePath, manifestId, packageName, hasMultipleExtensions }) { const base = path.basename(filePath, path.extname(filePath)); const normalizedManifestId = manifestId?.trim(); @@ -140,8 +187,10 @@ function normalizePluginManifest(raw) { id: raw.id.trim(), configSchema: raw.configSchema, ...(raw.enabledByDefault === true ? { enabledByDefault: true } : {}), - ...(normalizeStringList(raw.legacyPluginIds) - ? { legacyPluginIds: normalizeStringList(raw.legacyPluginIds) } + ...(typeof raw.kind === "string" ? { kind: raw.kind.trim() } : {}), + ...(normalizeStringList(raw.channels) ? { channels: normalizeStringList(raw.channels) } : {}), + ...(normalizeStringList(raw.providers) + ? { providers: normalizeStringList(raw.providers) } : {}), ...(normalizeStringList(raw.autoEnableWhenConfiguredProviders) ? { @@ -150,14 +199,12 @@ function normalizePluginManifest(raw) { ), } : {}), - ...(typeof raw.kind === "string" ? { kind: raw.kind.trim() } : {}), - ...(normalizeStringList(raw.channels) ? { channels: normalizeStringList(raw.channels) } : {}), - ...(normalizeStringList(raw.providers) - ? { providers: normalizeStringList(raw.providers) } - : {}), ...(normalizeStringList(raw.cliBackends) ? { cliBackends: normalizeStringList(raw.cliBackends) } : {}), + ...(normalizeStringList(raw.legacyPluginIds) + ? { legacyPluginIds: normalizeStringList(raw.legacyPluginIds) } + : {}), ...(normalizeObject(raw.providerAuthEnvVars) ? { providerAuthEnvVars: raw.providerAuthEnvVars } : {}), @@ -196,10 +243,6 @@ 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"), @@ -262,16 +305,28 @@ async function collectBundledChannelConfigsForSource({ source, manifest }) { return Object.keys(existingChannelConfigs).length > 0 ? existingChannelConfigs : undefined; } - const surfaceJson = execFileSync( - process.execPath, - ["--import", "tsx", "scripts/load-channel-config-surface.ts", modulePath], - { + const runSurfaceLoader = (command, args) => + execFileSync(command, args, { // Run from the host repo so the generator always resolves its own loader/tooling, // even when inspecting a temporary or alternate repo root. cwd: FORMATTER_CWD, encoding: "utf8", - }, - ); + }); + + let surfaceJson; + try { + surfaceJson = runSurfaceLoader("bun", ["scripts/load-channel-config-surface.ts", modulePath]); + } catch (error) { + if (!error || typeof error !== "object" || error.code !== "ENOENT") { + throw error; + } + surfaceJson = runSurfaceLoader(process.execPath, [ + "--import", + "tsx", + "scripts/load-channel-config-surface.ts", + modulePath, + ]); + } const surface = JSON.parse(surfaceJson); if (!surface?.schema) { return Object.keys(existingChannelConfigs).length > 0 ? existingChannelConfigs : undefined; @@ -332,25 +387,6 @@ 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 @@ -369,9 +405,6 @@ 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); @@ -401,27 +434,12 @@ 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 publicSurfaceArtifacts = collectTopLevelPublicSurfaceArtifacts({ + pluginDir: source.pluginDir, + sourceEntry, + setupEntry: setupEntry?.source, + }); + const runtimeSidecarArtifacts = collectRuntimeSidecarArtifacts(publicSurfaceArtifacts); const channelConfigs = await collectBundledChannelConfigsForSource({ source, manifest }); if (channelConfigs) { manifest.channelConfigs = channelConfigs; @@ -443,7 +461,7 @@ export async function collectBundledPluginMetadata(params = {}) { ? { setupSource: { source: setupEntry.source, built: setupEntry.built } } : {}), ...(publicSurfaceArtifacts ? { publicSurfaceArtifacts } : {}), - ...(runtimeSidecarArtifacts?.length ? { runtimeSidecarArtifacts } : {}), + ...(runtimeSidecarArtifacts ? { runtimeSidecarArtifacts } : {}), ...(typeof packageJson.name === "string" ? { packageName: packageJson.name.trim() } : {}), ...(typeof packageJson.version === "string" ? { packageVersion: packageJson.version.trim() } diff --git a/src/config/zod-schema.providers.ts b/src/config/zod-schema.providers.ts index 4c053ec8334..ebec848be39 100644 --- a/src/config/zod-schema.providers.ts +++ b/src/config/zod-schema.providers.ts @@ -3,6 +3,17 @@ import { getBundledChannelRuntimeMap } from "./bundled-channel-config-runtime.js import type { ChannelsConfig } from "./types.channels.js"; import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js"; import { GroupPolicySchema } from "./zod-schema.core.js"; +import { + BlueBubblesConfigSchema, + DiscordConfigSchema, + GoogleChatConfigSchema, + IMessageConfigSchema, + MSTeamsConfigSchema, + SignalConfigSchema, + SlackConfigSchema, + TelegramConfigSchema, +} from "./zod-schema.providers-core.js"; +import { WhatsAppConfigSchema } from "./zod-schema.providers-whatsapp.js"; export * from "./zod-schema.providers-core.js"; export * from "./zod-schema.providers-whatsapp.js"; @@ -12,6 +23,21 @@ const ChannelModelByChannelSchema = z .record(z.string(), z.record(z.string(), z.string())) .optional(); +const directChannelRuntimeSchemas = new Map< + string, + { safeParse: (value: unknown) => ReturnType } +>([ + ["bluebubbles", { safeParse: (value) => BlueBubblesConfigSchema.safeParse(value) }], + ["discord", { safeParse: (value) => DiscordConfigSchema.safeParse(value) }], + ["googlechat", { safeParse: (value) => GoogleChatConfigSchema.safeParse(value) }], + ["imessage", { safeParse: (value) => IMessageConfigSchema.safeParse(value) }], + ["msteams", { safeParse: (value) => MSTeamsConfigSchema.safeParse(value) }], + ["signal", { safeParse: (value) => SignalConfigSchema.safeParse(value) }], + ["slack", { safeParse: (value) => SlackConfigSchema.safeParse(value) }], + ["telegram", { safeParse: (value) => TelegramConfigSchema.safeParse(value) }], + ["whatsapp", { safeParse: (value) => WhatsAppConfigSchema.safeParse(value) }], +]); + function addLegacyChannelAcpBindingIssues( value: unknown, ctx: z.RefinementCtx, @@ -53,6 +79,25 @@ function normalizeBundledChannelConfigs( } let next: ChannelsConfig | undefined; + for (const [channelId, runtimeSchema] of directChannelRuntimeSchemas) { + if (!Object.prototype.hasOwnProperty.call(value, channelId)) { + continue; + } + const parsed = runtimeSchema.safeParse(value[channelId]); + if (!parsed.success) { + for (const issue of parsed.error.issues) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: issue.message ?? `Invalid channels.${channelId} config.`, + path: [channelId, ...(Array.isArray(issue.path) ? issue.path : [])], + }); + } + continue; + } + next ??= { ...value }; + next[channelId] = parsed.data as ChannelsConfig[string]; + } + for (const [channelId, runtimeSchema] of getBundledChannelRuntimeMap()) { if (!Object.prototype.hasOwnProperty.call(value, channelId)) { continue; diff --git a/src/infra/exec-approval-forwarder.test.ts b/src/infra/exec-approval-forwarder.test.ts index 132c7d7c0e5..465d9e08b69 100644 --- a/src/infra/exec-approval-forwarder.test.ts +++ b/src/infra/exec-approval-forwarder.test.ts @@ -312,7 +312,7 @@ describe("exec approval forwarder", () => { buttons: [ [ { text: "Allow Once", callback_data: "/approve req-1 allow-once" }, - { text: "Allow Always", callback_data: "/approve req-1 allow-always" }, + { text: "Allow Always", callback_data: "/approve req-1 always" }, ], [{ text: "Deny", callback_data: "/approve req-1 deny" }], ], diff --git a/src/infra/provider-usage.auth.normalizes-keys.test.ts b/src/infra/provider-usage.auth.normalizes-keys.test.ts index 7248f75cab8..bcdfdc17fe0 100644 --- a/src/infra/provider-usage.auth.normalizes-keys.test.ts +++ b/src/infra/provider-usage.auth.normalizes-keys.test.ts @@ -7,17 +7,20 @@ import { NON_ENV_SECRETREF_MARKER } from "../agents/model-auth-markers.js"; import type { OpenClawConfig } from "../config/config.js"; import type { ModelDefinitionConfig } from "../config/types.models.js"; -vi.mock("../agents/auth-profiles.js", async () => { - const profiles = await vi.importActual( - "../agents/auth-profiles/profiles.js", - ); - const order = await vi.importActual( - "../agents/auth-profiles/order.js", - ); - const oauth = await vi.importActual( - "../agents/auth-profiles/oauth.js", - ); - +vi.mock("../agents/auth-profiles.js", () => { + const normalizeProvider = (provider?: string | null): string => + String(provider ?? "") + .trim() + .toLowerCase() + .replace(/^z-ai$/, "zai"); + const dedupeProfileIds = (profileIds: string[]): string[] => [...new Set(profileIds)]; + const listProfilesForProvider = ( + store: { profiles?: Record }, + provider: string, + ): string[] => + Object.entries(store.profiles ?? {}) + .filter(([, profile]) => normalizeProvider(profile?.provider) === normalizeProvider(provider)) + .map(([profileId]) => profileId); const readStore = (agentDir?: string) => { if (!agentDir) { return { version: 1, profiles: {} }; @@ -43,24 +46,123 @@ vi.mock("../agents/auth-profiles.js", async () => { } }; + const resolveAuthProfileOrder = (params: { + cfg?: { auth?: { profiles?: Record } }; + store: { + profiles: Record; + order?: Record; + }; + provider: string; + }): string[] => { + const provider = normalizeProvider(params.provider); + const configured = Object.entries(params.cfg?.auth?.profiles ?? {}) + .filter(([, profile]) => normalizeProvider(profile?.provider) === provider) + .map(([profileId]) => profileId); + if (configured.length > 0) { + return dedupeProfileIds(configured); + } + const ordered = params.store.order?.[params.provider] ?? params.store.order?.[provider]; + if (ordered?.length) { + return dedupeProfileIds(ordered); + } + return dedupeProfileIds(listProfilesForProvider(params.store, provider)); + }; + + const resolveApiKeyForProfile = async (params: { + store: { + profiles: Record< + string, + | { + type?: string; + provider?: string; + key?: string; + token?: string; + accessToken?: string; + email?: string; + expires?: number; + } + | undefined + >; + }; + profileId: string; + }): Promise<{ apiKey: string; provider: string; email?: string } | null> => { + const cred = params.store.profiles[params.profileId]; + if (!cred) { + return null; + } + const profileProvider = normalizeProvider(params.profileId.split(":")[0] ?? ""); + const credentialProvider = normalizeProvider(cred.provider); + if (profileProvider && credentialProvider && profileProvider !== credentialProvider) { + return null; + } + if (cred.type === "api_key") { + return cred.key ? { apiKey: cred.key, provider: cred.provider ?? profileProvider } : null; + } + if (cred.type === "token") { + if (typeof cred.expires === "number" && cred.expires <= Date.now()) { + return null; + } + return cred.token + ? { apiKey: cred.token, provider: cred.provider ?? profileProvider, email: cred.email } + : null; + } + if (cred.type === "oauth") { + if (typeof cred.expires === "number" && cred.expires <= Date.now()) { + return null; + } + const token = cred.accessToken ?? cred.token; + return token + ? { apiKey: token, provider: cred.provider ?? profileProvider, email: cred.email } + : null; + } + return null; + }; + return { clearRuntimeAuthProfileStoreSnapshots: () => {}, ensureAuthProfileStore: (agentDir?: string) => readStore(agentDir), - dedupeProfileIds: profiles.dedupeProfileIds, - listProfilesForProvider: profiles.listProfilesForProvider, - resolveApiKeyForProfile: oauth.resolveApiKeyForProfile, - resolveAuthProfileOrder: order.resolveAuthProfileOrder, + dedupeProfileIds, + listProfilesForProvider, + resolveApiKeyForProfile, + resolveAuthProfileOrder, }; }); -const resolveProviderUsageAuthWithPluginMock = vi.fn(async (..._args: unknown[]) => null); - -vi.mock("../plugins/provider-runtime.js", () => ({ - resolveProviderUsageAuthWithPlugin: resolveProviderUsageAuthWithPluginMock, +const providerRuntimeMocks = vi.hoisted(() => ({ + resolveProviderUsageAuthWithPluginMock: vi.fn(async (..._args: unknown[]) => null), + providerRuntimeMock: { + augmentModelCatalogWithProviderPlugins: vi.fn((catalog: unknown) => catalog), + buildProviderAuthDoctorHintWithPlugin: vi.fn(() => undefined), + buildProviderMissingAuthMessageWithPlugin: vi.fn(() => undefined), + buildProviderUnknownModelHintWithPlugin: vi.fn(() => undefined), + clearProviderRuntimeHookCache: vi.fn(() => {}), + createProviderEmbeddingProvider: vi.fn(() => undefined), + formatProviderAuthProfileApiKeyWithPlugin: vi.fn(() => undefined), + normalizeProviderResolvedModelWithPlugin: vi.fn(() => undefined), + prepareProviderDynamicModel: vi.fn(async () => {}), + prepareProviderExtraParams: vi.fn(() => undefined), + prepareProviderRuntimeAuth: vi.fn(async () => undefined), + refreshProviderOAuthCredentialWithPlugin: vi.fn(async () => undefined), + resetProviderRuntimeHookCacheForTest: vi.fn(() => {}), + resolveProviderBinaryThinking: vi.fn(() => undefined), + resolveProviderBuiltInModelSuppression: vi.fn(() => undefined), + resolveProviderCacheTtlEligibility: vi.fn(() => undefined), + resolveProviderCapabilitiesWithPlugin: vi.fn(() => undefined), + resolveProviderDefaultThinkingLevel: vi.fn(() => undefined), + resolveProviderModernModelRef: vi.fn(() => undefined), + resolveProviderRuntimePlugin: vi.fn(() => undefined), + resolveProviderStreamFn: vi.fn(() => undefined), + resolveProviderSyntheticAuthWithPlugin: vi.fn(() => undefined), + resolveProviderUsageSnapshotWithPlugin: vi.fn(async () => undefined), + resolveProviderXHighThinking: vi.fn(() => undefined), + runProviderDynamicModel: vi.fn(() => undefined), + wrapProviderStreamFn: vi.fn(() => undefined), + }, })); -vi.mock("../plugins/provider-runtime.ts", () => ({ - resolveProviderUsageAuthWithPlugin: resolveProviderUsageAuthWithPluginMock, +vi.mock("../plugins/provider-runtime.js", () => ({ + ...providerRuntimeMocks.providerRuntimeMock, + resolveProviderUsageAuthWithPlugin: providerRuntimeMocks.resolveProviderUsageAuthWithPluginMock, })); vi.mock("../agents/cli-credentials.js", () => ({ @@ -104,8 +206,8 @@ describe("resolveProviderAuths key normalization", () => { ({ clearConfigCache } = await import("../config/config.js")); clearConfigCache(); clearRuntimeAuthProfileStoreSnapshots(); - resolveProviderUsageAuthWithPluginMock.mockReset(); - resolveProviderUsageAuthWithPluginMock.mockResolvedValue(null); + providerRuntimeMocks.resolveProviderUsageAuthWithPluginMock.mockReset(); + providerRuntimeMocks.resolveProviderUsageAuthWithPluginMock.mockResolvedValue(null); }); afterEach(() => { diff --git a/src/infra/provider-usage.auth.plugin.test.ts b/src/infra/provider-usage.auth.plugin.test.ts index a9770e2c3c5..89e861e591b 100644 --- a/src/infra/provider-usage.auth.plugin.test.ts +++ b/src/infra/provider-usage.auth.plugin.test.ts @@ -4,13 +4,13 @@ const resolveProviderUsageAuthWithPluginMock = vi.fn( async (..._args: unknown[]): Promise => null, ); -const resolveProviderCapabilitiesWithPluginMock = vi.fn(() => undefined); - -vi.mock("../plugins/provider-runtime.js", async (importOriginal) => ({ - ...(await importOriginal()), - resolveProviderCapabilitiesWithPlugin: resolveProviderCapabilitiesWithPluginMock, - resolveProviderUsageAuthWithPlugin: resolveProviderUsageAuthWithPluginMock, -})); +vi.mock("../plugins/provider-runtime.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolveProviderUsageAuthWithPlugin: resolveProviderUsageAuthWithPluginMock, + }; +}); let resolveProviderAuths: typeof import("./provider-usage.auth.js").resolveProviderAuths; diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts index c503779b6f5..0ddd94ac939 100644 --- a/src/infra/provider-usage.auth.ts +++ b/src/infra/provider-usage.auth.ts @@ -110,9 +110,9 @@ async function resolveOAuthToken(params: { } try { const resolved = await resolveApiKeyForProfile({ - // Usage snapshots should work even if config profile metadata is stale. - // (e.g. config says api_key but the store has a token profile.) - cfg: undefined, + // Reuse the already-resolved config snapshot for token/ref resolution so + // usage snapshots don't trigger a second ambient loadConfig() call. + cfg: params.state.cfg, store: params.state.store, profileId, agentDir: params.state.agentDir, diff --git a/src/infra/restart.test.ts b/src/infra/restart.test.ts index bd58e43aa58..a47f34732ff 100644 --- a/src/infra/restart.test.ts +++ b/src/infra/restart.test.ts @@ -16,9 +16,13 @@ vi.mock("./ports-lsof.js", () => ({ resolveLsofCommandSync: (...args: unknown[]) => resolveLsofCommandSyncMock(...args), })); -vi.mock("../config/paths.js", () => ({ - resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args), -})); +vi.mock("../config/paths.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args), + }; +}); let __testing: typeof import("./restart-stale-pids.js").__testing; let cleanStaleGatewayProcessesSync: typeof import("./restart-stale-pids.js").cleanStaleGatewayProcessesSync; @@ -38,9 +42,13 @@ beforeEach(async () => { vi.doMock("./ports-lsof.js", () => ({ resolveLsofCommandSync: (...args: unknown[]) => resolveLsofCommandSyncMock(...args), })); - vi.doMock("../config/paths.js", () => ({ - resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args), - })); + vi.doMock("../config/paths.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args), + }; + }); ({ __testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } = await import("./restart-stale-pids.js")); spawnSyncMock.mockReset(); diff --git a/src/library.test.ts b/src/library.test.ts index 44bf652cfcf..0f21bca3864 100644 --- a/src/library.test.ts +++ b/src/library.test.ts @@ -1,43 +1,58 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import ts from "typescript"; +import { describe, expect, it } from "vitest"; + +const libraryPath = resolve(dirname(fileURLToPath(import.meta.url)), "library.ts"); +const lazyRuntimeSpecifiers = [ + "./auto-reply/reply.runtime.js", + "./cli/prompt.js", + "./infra/binaries.js", + "./process/exec.js", + "./plugins/runtime/runtime-whatsapp-boundary.js", +] as const; + +function readLibraryModuleImports() { + const sourceText = readFileSync(libraryPath, "utf8"); + const sourceFile = ts.createSourceFile(libraryPath, sourceText, ts.ScriptTarget.Latest, true); + const staticImports = new Set(); + const dynamicImports = new Set(); + + function visit(node: ts.Node) { + if ( + ts.isImportDeclaration(node) && + ts.isStringLiteral(node.moduleSpecifier) && + !node.importClause?.isTypeOnly + ) { + staticImports.add(node.moduleSpecifier.text); + } + + if ( + ts.isCallExpression(node) && + node.expression.kind === ts.SyntaxKind.ImportKeyword && + node.arguments.length === 1 && + ts.isStringLiteral(node.arguments[0]) + ) { + dynamicImports.add(node.arguments[0].text); + } + + ts.forEachChild(node, visit); + } + + visit(sourceFile); + return { dynamicImports, staticImports }; +} describe("library module imports", () => { - beforeEach(() => { - vi.resetModules(); - }); + it("keeps lazy runtime boundaries on dynamic imports", () => { + const { dynamicImports, staticImports } = readLibraryModuleImports(); - it("does not load lazy runtimes on module import", async () => { - const replyRuntimeLoads = vi.fn(); - const promptRuntimeLoads = vi.fn(); - const binariesRuntimeLoads = vi.fn(); - const whatsappRuntimeLoads = vi.fn(); - vi.doMock("./auto-reply/reply.runtime.js", async (importOriginal) => { - replyRuntimeLoads(); - return await importOriginal(); - }); - vi.doMock("./cli/prompt.runtime.js", async (importOriginal) => { - promptRuntimeLoads(); - return await importOriginal(); - }); - vi.doMock("./infra/binaries.runtime.js", async (importOriginal) => { - binariesRuntimeLoads(); - return await importOriginal(); - }); - vi.doMock("./plugins/runtime/runtime-whatsapp-boundary.js", async (importOriginal) => { - whatsappRuntimeLoads(); - return await importOriginal< - typeof import("./plugins/runtime/runtime-whatsapp-boundary.js") - >(); - }); - - await import("./library.js"); - - expect(replyRuntimeLoads).not.toHaveBeenCalled(); - // Vitest eagerly resolves some manual mocks for runtime-boundary modules - // even when the lazy wrapper is not invoked. Keep the assertion on the - // reply runtime, which is the stable import-time contract this test cares about. - vi.doUnmock("./auto-reply/reply.runtime.js"); - vi.doUnmock("./cli/prompt.runtime.js"); - vi.doUnmock("./infra/binaries.runtime.js"); - vi.doUnmock("./plugins/runtime/runtime-whatsapp-boundary.js"); + for (const specifier of lazyRuntimeSpecifiers) { + expect(staticImports.has(specifier), `${specifier} should stay lazy`).toBe(false); + expect(dynamicImports.has(specifier), `${specifier} should remain dynamically imported`).toBe( + true, + ); + } }); }); diff --git a/src/library.ts b/src/library.ts index 7a6590eadc1..69a93bd11e8 100644 --- a/src/library.ts +++ b/src/library.ts @@ -1,61 +1,76 @@ +import type { getReplyFromConfig as getReplyFromConfigRuntime } from "./auto-reply/reply.runtime.js"; import { applyTemplate } from "./auto-reply/templating.js"; import { createDefaultDeps } from "./cli/deps.js"; +import type { promptYesNo as promptYesNoRuntime } from "./cli/prompt.js"; import { waitForever } from "./cli/wait.js"; import { loadConfig } from "./config/config.js"; import { resolveStorePath } from "./config/sessions/paths.js"; import { deriveSessionKey, resolveSessionKey } from "./config/sessions/session-key.js"; import { loadSessionStore, saveSessionStore } from "./config/sessions/store.js"; +import type { ensureBinary as ensureBinaryRuntime } from "./infra/binaries.js"; import { describePortOwner, ensurePortAvailable, handlePortError, PortInUseError, } from "./infra/ports.js"; +import type { monitorWebChannel as monitorWebChannelRuntime } from "./plugins/runtime/runtime-whatsapp-boundary.js"; +import type { + runCommandWithTimeout as runCommandWithTimeoutRuntime, + runExec as runExecRuntime, +} from "./process/exec.js"; import { assertWebChannel, normalizeE164, toWhatsappJid } from "./utils.js"; -type ReplyRuntimeModule = typeof import("./auto-reply/reply.runtime.js"); -type PromptRuntimeModule = typeof import("./cli/prompt.runtime.js"); -type BinariesRuntimeModule = typeof import("./infra/binaries.runtime.js"); -type ExecRuntimeModule = typeof import("./process/exec.js"); -type WhatsAppRuntimeModule = typeof import("./plugins/runtime/runtime-whatsapp-boundary.js"); +type GetReplyFromConfig = typeof getReplyFromConfigRuntime; +type PromptYesNo = typeof promptYesNoRuntime; +type EnsureBinary = typeof ensureBinaryRuntime; +type RunExec = typeof runExecRuntime; +type RunCommandWithTimeout = typeof runCommandWithTimeoutRuntime; +type MonitorWebChannel = typeof monitorWebChannelRuntime; -let replyRuntimePromise: Promise | undefined; -let promptRuntimePromise: Promise | undefined; -let binariesRuntimePromise: Promise | undefined; -let execRuntimePromise: Promise | undefined; -let whatsappRuntimePromise: Promise | undefined; +let replyRuntimePromise: Promise | null = null; +let promptRuntimePromise: Promise | null = null; +let binariesRuntimePromise: Promise | null = null; +let execRuntimePromise: Promise | null = null; +let whatsappRuntimePromise: Promise< + typeof import("./plugins/runtime/runtime-whatsapp-boundary.js") +> | null = null; -function loadReplyRuntime(): Promise { - return (replyRuntimePromise ??= import("./auto-reply/reply.runtime.js")); +function loadReplyRuntime() { + replyRuntimePromise ??= import("./auto-reply/reply.runtime.js"); + return replyRuntimePromise; } -function loadPromptRuntime(): Promise { - return (promptRuntimePromise ??= import("./cli/prompt.runtime.js")); +function loadPromptRuntime() { + promptRuntimePromise ??= import("./cli/prompt.js"); + return promptRuntimePromise; } -function loadBinariesRuntime(): Promise { - return (binariesRuntimePromise ??= import("./infra/binaries.runtime.js")); +function loadBinariesRuntime() { + binariesRuntimePromise ??= import("./infra/binaries.js"); + return binariesRuntimePromise; } -function loadExecRuntime(): Promise { - return (execRuntimePromise ??= import("./process/exec.js")); +function loadExecRuntime() { + execRuntimePromise ??= import("./process/exec.js"); + return execRuntimePromise; } -function loadWhatsAppRuntime(): Promise { - return (whatsappRuntimePromise ??= import("./plugins/runtime/runtime-whatsapp-boundary.js")); +function loadWhatsAppRuntime() { + whatsappRuntimePromise ??= import("./plugins/runtime/runtime-whatsapp-boundary.js"); + return whatsappRuntimePromise; } -export const getReplyFromConfig: ReplyRuntimeModule["getReplyFromConfig"] = async (...args) => +export const getReplyFromConfig: GetReplyFromConfig = async (...args) => (await loadReplyRuntime()).getReplyFromConfig(...args); -export const promptYesNo: PromptRuntimeModule["promptYesNo"] = async (...args) => +export const promptYesNo: PromptYesNo = async (...args) => (await loadPromptRuntime()).promptYesNo(...args); -export const ensureBinary: BinariesRuntimeModule["ensureBinary"] = async (...args) => +export const ensureBinary: EnsureBinary = async (...args) => (await loadBinariesRuntime()).ensureBinary(...args); -export const runExec: ExecRuntimeModule["runExec"] = async (...args) => - (await loadExecRuntime()).runExec(...args); -export const runCommandWithTimeout: ExecRuntimeModule["runCommandWithTimeout"] = async (...args) => +export const runExec: RunExec = async (...args) => (await loadExecRuntime()).runExec(...args); +export const runCommandWithTimeout: RunCommandWithTimeout = async (...args) => (await loadExecRuntime()).runCommandWithTimeout(...args); -export const monitorWebChannel: WhatsAppRuntimeModule["monitorWebChannel"] = async (...args) => +export const monitorWebChannel: MonitorWebChannel = async (...args) => (await loadWhatsAppRuntime()).monitorWebChannel(...args); export { diff --git a/src/logging/config.ts b/src/logging/config.ts index fc02e9fda3e..cc54d7d83d7 100644 --- a/src/logging/config.ts +++ b/src/logging/config.ts @@ -1,8 +1,11 @@ import { getCommandPathWithRootOptions } from "../cli/argv.js"; -import { loadConfig, type OpenClawConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { resolveNodeRequireFromMeta } from "./node-require.js"; type LoggingConfig = OpenClawConfig["logging"]; +const requireConfig = resolveNodeRequireFromMeta(import.meta.url); + export function shouldSkipMutatingLoggingConfigRead(argv: string[] = process.argv): boolean { const [primary, secondary] = getCommandPathWithRootOptions(argv, 2); return primary === "config" && (secondary === "schema" || secondary === "validate"); @@ -13,7 +16,12 @@ export function readLoggingConfig(): LoggingConfig | undefined { return undefined; } try { - const parsed = loadConfig(); + const loaded = requireConfig?.("../config/config.js") as + | { + loadConfig?: () => OpenClawConfig; + } + | undefined; + const parsed = loaded?.loadConfig?.(); const logging = parsed?.logging; if (!logging || typeof logging !== "object" || Array.isArray(logging)) { return undefined; diff --git a/src/plugin-sdk/index.bundle.test.ts b/src/plugin-sdk/index.bundle.test.ts index df75515fc93..c79abc4b7a0 100644 --- a/src/plugin-sdk/index.bundle.test.ts +++ b/src/plugin-sdk/index.bundle.test.ts @@ -12,13 +12,11 @@ const bundledCoverageEntrySources = buildPluginSdkEntrySources(bundledRepresenta describe("plugin-sdk bundled exports", () => { it("emits importable bundled subpath entries", { timeout: 120_000 }, async () => { - const bundleTempRoot = path.join( - process.cwd(), - "node_modules", - ".cache", - "openclaw-plugin-sdk-build", + const bundleCacheRoot = path.join(process.cwd(), "node_modules", ".cache"); + await fs.mkdir(bundleCacheRoot, { recursive: true }); + const bundleTempRoot = await fs.mkdtemp( + path.join(bundleCacheRoot, "openclaw-plugin-sdk-build-"), ); - await fs.mkdir(bundleTempRoot, { recursive: true }); const outDir = path.join(bundleTempRoot, "bundle"); await fs.rm(outDir, { recursive: true, force: true }); await fs.mkdir(outDir, { recursive: true }); diff --git a/src/plugins/bundled-plugin-metadata.generated.ts b/src/plugins/bundled-plugin-metadata.generated.ts index 3ce0b4c20da..c8b8ebee443 100644 --- a/src/plugins/bundled-plugin-metadata.generated.ts +++ b/src/plugins/bundled-plugin-metadata.generated.ts @@ -1027,8 +1027,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ properties: {}, }, enabledByDefault: true, - autoEnableWhenConfiguredProviders: ["copilot-proxy"], providers: ["copilot-proxy"], + autoEnableWhenConfiguredProviders: ["copilot-proxy"], providerAuthChoices: [ { provider: "copilot-proxy", @@ -5487,8 +5487,8 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ }, }, enabledByDefault: true, - autoEnableWhenConfiguredProviders: ["google-gemini-cli"], providers: ["google", "google-gemini-cli"], + autoEnableWhenConfiguredProviders: ["google-gemini-cli"], cliBackends: ["google-gemini-cli"], providerAuthEnvVars: { google: ["GEMINI_API_KEY", "GOOGLE_API_KEY"], @@ -9586,9 +9586,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ properties: {}, }, enabledByDefault: true, - legacyPluginIds: ["minimax-portal-auth"], - autoEnableWhenConfiguredProviders: ["minimax-portal"], providers: ["minimax", "minimax-portal"], + autoEnableWhenConfiguredProviders: ["minimax-portal"], + legacyPluginIds: ["minimax-portal-auth"], providerAuthEnvVars: { minimax: ["MINIMAX_API_KEY"], "minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"], diff --git a/src/plugins/channel-plugin-ids.ts b/src/plugins/channel-plugin-ids.ts index 4879a083b1e..51c7177f63c 100644 --- a/src/plugins/channel-plugin-ids.ts +++ b/src/plugins/channel-plugin-ids.ts @@ -310,6 +310,17 @@ export function resolveGatewayStartupPluginIds(params: { if (plugin.channels.length > 0) { return false; } + if ( + plugin.origin === "bundled" && + (plugin.providers.some((providerId) => + configuredActivationIds.has(normalizeProviderId(providerId)), + ) || + plugin.cliBackends.some((backendId) => + configuredActivationIds.has(normalizeProviderId(backendId)), + )) + ) { + return true; + } const enabled = resolveEffectiveEnableState({ id: plugin.id, origin: plugin.origin, @@ -323,16 +334,6 @@ export function resolveGatewayStartupPluginIds(params: { if (plugin.origin !== "bundled") { return true; } - if ( - plugin.providers.some((providerId) => - configuredActivationIds.has(normalizeProviderId(providerId)), - ) || - plugin.cliBackends.some((backendId) => - configuredActivationIds.has(normalizeProviderId(backendId)), - ) - ) { - return true; - } return ( pluginsConfig.allow.includes(plugin.id) || pluginsConfig.entries[plugin.id]?.enabled === true || diff --git a/src/plugins/commands.ts b/src/plugins/commands.ts index 682586b6cac..33a2cfb9027 100644 --- a/src/plugins/commands.ts +++ b/src/plugins/commands.ts @@ -8,6 +8,7 @@ import { parseExplicitTargetForChannel } from "../channels/plugins/target-parsing.js"; import type { OpenClawConfig } from "../config/config.js"; import { logVerbose } from "../globals.js"; +import { parseTelegramTarget } from "../plugin-sdk/telegram-runtime.js"; import { clearPluginCommands, clearPluginCommandsForPlugin, @@ -118,6 +119,30 @@ function stripPrefix(raw: string | undefined, prefix: string): string | undefine return raw.startsWith(prefix) ? raw.slice(prefix.length) : raw; } +function parseDiscordBindingTarget(raw: string | undefined): { + conversationId: string; +} | null { + if (!raw) { + return null; + } + if (raw.startsWith("slash:")) { + return null; + } + const normalized = raw.startsWith("discord:") ? raw.slice("discord:".length) : raw; + if (!normalized) { + return null; + } + if (normalized.startsWith("channel:")) { + const id = normalized.slice("channel:".length).trim(); + return id ? { conversationId: `channel:${id}` } : null; + } + if (normalized.startsWith("user:")) { + const id = normalized.slice("user:".length).trim(); + return id ? { conversationId: `user:${id}` } : null; + } + return /^\d+$/.test(normalized.trim()) ? { conversationId: `user:${normalized.trim()}` } : null; +} + function resolveBindingConversationFromCommand(params: { channel: string; from?: string; @@ -138,34 +163,35 @@ function resolveBindingConversationFromCommand(params: { return null; } const target = parseExplicitTargetForChannel("telegram", rawTarget); - if (!target) { + const fallbackTarget = target ? null : parseTelegramTarget(rawTarget); + if (!target && !fallbackTarget) { return null; } return { channel: "telegram", accountId, - conversationId: target.to, - threadId: params.messageThreadId ?? target.threadId, + conversationId: target?.to ?? fallbackTarget?.chatId ?? "", + threadId: params.messageThreadId ?? target?.threadId ?? fallbackTarget?.messageThreadId, }; } if (params.channel === "discord") { const source = params.from ?? params.to; - const rawTarget = source?.startsWith("discord:channel:") - ? stripPrefix(source, "discord:") - : source?.startsWith("discord:user:") - ? stripPrefix(source, "discord:") - : source; - if (!rawTarget || rawTarget.startsWith("slash:")) { + const rawTarget = source?.startsWith("discord:") ? stripPrefix(source, "discord:") : source; + if (!rawTarget) { return null; } - const target = parseExplicitTargetForChannel("discord", rawTarget); + const target = + parseExplicitTargetForChannel("discord", rawTarget) ?? parseDiscordBindingTarget(rawTarget); if (!target) { return null; } return { channel: "discord", accountId, - conversationId: `${target.chatType === "direct" ? "user" : "channel"}:${target.to}`, + conversationId: + "conversationId" in target + ? target.conversationId + : `${target.chatType === "direct" ? "user" : "channel"}:${target.to}`, }; } return null; diff --git a/src/plugins/contracts/catalog.contract.test.ts b/src/plugins/contracts/catalog.contract.test.ts index 3c5ff9b8868..adf376bb0d1 100644 --- a/src/plugins/contracts/catalog.contract.test.ts +++ b/src/plugins/contracts/catalog.contract.test.ts @@ -45,6 +45,7 @@ let openaiProvider: ProviderPlugin; describe("provider catalog contract", { timeout: PROVIDER_CATALOG_CONTRACT_TIMEOUT_MS }, () => { beforeAll(async () => { + vi.resetModules(); const openaiPlugin = await import("../../../extensions/openai/index.ts"); openaiProviders = registerProviderPlugin({ plugin: openaiPlugin.default, diff --git a/src/plugins/provider-auth-storage.ts b/src/plugins/provider-auth-storage.ts index 04076286e21..55e315554e0 100644 --- a/src/plugins/provider-auth-storage.ts +++ b/src/plugins/provider-auth-storage.ts @@ -1,10 +1,3 @@ -import { HUGGINGFACE_DEFAULT_MODEL_REF } from "../../extensions/huggingface/api.js"; -import { LITELLM_DEFAULT_MODEL_REF } from "../../extensions/litellm/api.js"; -import { OPENROUTER_DEFAULT_MODEL_REF } from "../../extensions/openrouter/api.js"; -import { TOGETHER_DEFAULT_MODEL_REF } from "../../extensions/together/api.js"; -import { VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF } from "../../extensions/vercel-ai-gateway/api.js"; -import { XIAOMI_DEFAULT_MODEL_REF } from "../../extensions/xiaomi/api.js"; -import { ZAI_DEFAULT_MODEL_REF } from "../../extensions/zai/api.js"; import { resolveOpenClawAgentDir } from "../agents/agent-paths.js"; import { upsertAuthProfile } from "../agents/auth-profiles.js"; import type { SecretInput } from "../config/types.secrets.js"; @@ -17,6 +10,13 @@ import { import { KILOCODE_DEFAULT_MODEL_REF } from "./provider-model-kilocode.js"; const resolveAuthAgentDir = (agentDir?: string) => agentDir ?? resolveOpenClawAgentDir(); +const ZAI_DEFAULT_MODEL_REF = "zai/glm-5"; +const XIAOMI_DEFAULT_MODEL_REF = "xiaomi/mimo-v2-flash"; +const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto"; +const HUGGINGFACE_DEFAULT_MODEL_REF = "huggingface/deepseek-ai/DeepSeek-R1"; +const TOGETHER_DEFAULT_MODEL_REF = "together/moonshotai/Kimi-K2.5"; +const LITELLM_DEFAULT_MODEL_REF = "litellm/claude-opus-4-6"; +const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.6"; type ProviderApiKeySetter = ( key: SecretInput, diff --git a/src/plugins/provider-runtime.test-support.ts b/src/plugins/provider-runtime.test-support.ts index 9aa8cbd8892..9e9fb0bb877 100644 --- a/src/plugins/provider-runtime.test-support.ts +++ b/src/plugins/provider-runtime.test-support.ts @@ -5,7 +5,7 @@ export const openaiCodexCatalogEntries = [ { provider: "openai", id: "gpt-5.2-pro", name: "GPT-5.2 Pro" }, { provider: "openai", id: "gpt-5-mini", name: "GPT-5 mini" }, { provider: "openai", id: "gpt-5-nano", name: "GPT-5 nano" }, - { provider: "openai-codex", id: "gpt-5.4", name: "GPT-5.4" }, + { provider: "openai-codex", id: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, ]; export const expectedAugmentedOpenaiCodexCatalogEntries = [ diff --git a/src/plugins/provider-zai-endpoint.ts b/src/plugins/provider-zai-endpoint.ts index 0eca8076f86..cab21e16759 100644 --- a/src/plugins/provider-zai-endpoint.ts +++ b/src/plugins/provider-zai-endpoint.ts @@ -1,12 +1,10 @@ -import { - ZAI_CN_BASE_URL, - ZAI_CODING_CN_BASE_URL, - ZAI_CODING_GLOBAL_BASE_URL, - ZAI_GLOBAL_BASE_URL, -} from "../../extensions/zai/api.js"; import { fetchWithTimeout } from "../utils/fetch-timeout.js"; export type ZaiEndpointId = "global" | "cn" | "coding-global" | "coding-cn"; +const ZAI_CODING_GLOBAL_BASE_URL = "https://api.z.ai/api/coding/paas/v4"; +const ZAI_CODING_CN_BASE_URL = "https://open.bigmodel.cn/api/coding/paas/v4"; +const ZAI_GLOBAL_BASE_URL = "https://api.z.ai/api/paas/v4"; +const ZAI_CN_BASE_URL = "https://open.bigmodel.cn/api/paas/v4"; export type ZaiDetectedEndpoint = { endpoint: ZaiEndpointId; diff --git a/test/openclaw-npm-postpublish-verify.test.ts b/test/openclaw-npm-postpublish-verify.test.ts index 577ebf924a2..12363b67204 100644 --- a/test/openclaw-npm-postpublish-verify.test.ts +++ b/test/openclaw-npm-postpublish-verify.test.ts @@ -3,6 +3,7 @@ import { buildPublishedInstallScenarios, collectInstalledPackageErrors, } from "../scripts/openclaw-npm-postpublish-verify.ts"; +import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/public-artifacts.ts"; describe("buildPublishedInstallScenarios", () => { it("uses a single fresh scenario for plain stable releases", () => { @@ -41,12 +42,10 @@ describe("collectInstalledPackageErrors", () => { }), ).toEqual([ "installed package version mismatch: expected 2026.3.23-2, found 2026.3.23.", - "installed package is missing required bundled runtime sidecar: dist/extensions/whatsapp/light-runtime-api.js", - "installed package is missing required bundled runtime sidecar: dist/extensions/whatsapp/runtime-api.js", - "installed package is missing required bundled runtime sidecar: dist/extensions/matrix/helper-api.js", - "installed package is missing required bundled runtime sidecar: dist/extensions/matrix/runtime-api.js", - "installed package is missing required bundled runtime sidecar: dist/extensions/matrix/thread-bindings-runtime.js", - "installed package is missing required bundled runtime sidecar: dist/extensions/msteams/runtime-api.js", + ...BUNDLED_RUNTIME_SIDECAR_PATHS.map( + (relativePath) => + `installed package is missing required bundled runtime sidecar: ${relativePath}`, + ), ]); }); }); diff --git a/test/scripts/test-parallel.test.ts b/test/scripts/test-parallel.test.ts index 55ba5cbcae7..abad9e690a9 100644 --- a/test/scripts/test-parallel.test.ts +++ b/test/scripts/test-parallel.test.ts @@ -371,9 +371,13 @@ describe("scripts/test-parallel lane planning", () => { }), ); - expect(output).toContain("unit-batch-1 filters=50"); - expect(output).toContain("unit-batch-2 filters=49"); - expect(output).not.toContain("unit-batch-3"); + const unitBatchLines = getPlanLines(output, "unit-batch-"); + const unitBatchFilterCounts = unitBatchLines.map((line) => + parseNumericPlanField(line, "filters"), + ); + + expect(unitBatchLines.length).toBe(2); + expect(unitBatchFilterCounts).toEqual([50, 50]); }); it("explains targeted file ownership and execution policy", () => {