diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index fba4e7293f4..a06fd98c806 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -f9276d60f9fed5813979adf2c91a9145a3fb4101c17ae214439cd47fc1cbd2ae plugin-sdk-api-baseline.json -ee74172a0a8685cec1847095589ada102fac7877e92afe6b19206f88805fa62d plugin-sdk-api-baseline.jsonl +8c38d3e2d0a61c02db70070e0d032b54b1de474000e1c1221332efc495e8681d plugin-sdk-api-baseline.json +d057310712f83b27f64b53dbe45ef2e92795407e56503a255de0f29d915c1ee4 plugin-sdk-api-baseline.jsonl diff --git a/extensions/googlechat/src/setup-core.ts b/extensions/googlechat/src/setup-core.ts index ba9e59788c2..14639f66c20 100644 --- a/extensions/googlechat/src/setup-core.ts +++ b/extensions/googlechat/src/setup-core.ts @@ -2,7 +2,7 @@ import { createPatchedAccountSetupAdapter, createSetupInputPresenceValidator, DEFAULT_ACCOUNT_ID, -} from "openclaw/plugin-sdk/setup"; +} from "openclaw/plugin-sdk/setup-runtime"; const channel = "googlechat" as const; diff --git a/extensions/imessage/src/setup-core.ts b/extensions/imessage/src/setup-core.ts index 741b73c7871..0f0c473f5c0 100644 --- a/extensions/imessage/src/setup-core.ts +++ b/extensions/imessage/src/setup-core.ts @@ -11,12 +11,12 @@ import { setSetupChannelEnabled, type OpenClawConfig, type WizardPrompter, -} from "openclaw/plugin-sdk/setup"; +} from "openclaw/plugin-sdk/setup-runtime"; import type { ChannelSetupAdapter, ChannelSetupWizard, ChannelSetupWizardTextInput, -} from "openclaw/plugin-sdk/setup"; +} from "openclaw/plugin-sdk/setup-runtime"; import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools"; import { listIMessageAccountIds, diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index 714d82e1203..05ce14a169e 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -10,8 +10,7 @@ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime"; import { createChatChannelPlugin, type ChannelPlugin } from "openclaw/plugin-sdk/core"; import { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/media-runtime"; import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-runtime"; -import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime"; -import { chunkText } from "openclaw/plugin-sdk/reply-runtime"; +import { chunkText, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-chunking"; import { buildOutboundBaseSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing"; import { buildBaseChannelStatusSummary, diff --git a/extensions/slack/src/message-actions.ts b/extensions/slack/src/message-actions.ts index c0e701b331b..474e2c42147 100644 --- a/extensions/slack/src/message-actions.ts +++ b/extensions/slack/src/message-actions.ts @@ -1,4 +1,4 @@ -import { createActionGate } from "openclaw/plugin-sdk/agent-runtime"; +import { createActionGate } from "openclaw/plugin-sdk/channel-actions"; import type { ChannelMessageActionName } from "openclaw/plugin-sdk/channel-contract"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { ChannelToolSend } from "openclaw/plugin-sdk/tool-send"; diff --git a/extensions/slack/src/setup-core.ts b/extensions/slack/src/setup-core.ts index 1d6632d7b65..f2c2820baf6 100644 --- a/extensions/slack/src/setup-core.ts +++ b/extensions/slack/src/setup-core.ts @@ -1,3 +1,4 @@ +import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input-runtime"; import { createAllowlistSetupWizardProxy, createAccountScopedAllowFromSection, @@ -6,18 +7,17 @@ import { createStandardChannelSetupStatus, DEFAULT_ACCOUNT_ID, createEnvPatchedAccountSetupAdapter, - hasConfiguredSecretInput, type OpenClawConfig, parseMentionOrPrefixedId, patchChannelConfigForAccount, setSetupChannelEnabled, -} from "openclaw/plugin-sdk/setup"; +} from "openclaw/plugin-sdk/setup-runtime"; import { type ChannelSetupAdapter, type ChannelSetupDmPolicy, type ChannelSetupWizard, type ChannelSetupWizardAllowFromEntry, -} from "openclaw/plugin-sdk/setup"; +} from "openclaw/plugin-sdk/setup-runtime"; import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools"; import { inspectSlackAccount } from "./account-inspect.js"; import { listSlackAccountIds, resolveSlackAccount, type ResolvedSlackAccount } from "./accounts.js"; diff --git a/extensions/slack/src/setup-surface.ts b/extensions/slack/src/setup-surface.ts index ed66533031f..71a69536477 100644 --- a/extensions/slack/src/setup-surface.ts +++ b/extensions/slack/src/setup-surface.ts @@ -7,11 +7,11 @@ import { parseMentionOrPrefixedId, promptLegacyChannelAllowFromForAccount, type WizardPrompter, -} from "openclaw/plugin-sdk/setup"; +} from "openclaw/plugin-sdk/setup-runtime"; import type { ChannelSetupWizard, ChannelSetupWizardAllowFromEntry, -} from "openclaw/plugin-sdk/setup"; +} from "openclaw/plugin-sdk/setup-runtime"; import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools"; import { resolveDefaultSlackAccountId, diff --git a/extensions/slack/src/shared.ts b/extensions/slack/src/shared.ts index aa55cf779d8..3c8bb98ae78 100644 --- a/extensions/slack/src/shared.ts +++ b/extensions/slack/src/shared.ts @@ -4,11 +4,9 @@ import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, } from "openclaw/plugin-sdk/channel-config-helpers"; -import { - formatDocsLink, - hasConfiguredSecretInput, - patchChannelConfigForAccount, -} from "openclaw/plugin-sdk/setup"; +import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input-runtime"; +import { patchChannelConfigForAccount } from "openclaw/plugin-sdk/setup-runtime"; +import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools"; import { inspectSlackAccount } from "./account-inspect.js"; import { listSlackAccountIds, diff --git a/extensions/synology-chat/src/channel.ts b/extensions/synology-chat/src/channel.ts index f1c5f7476fa..9a8fdc2fe65 100644 --- a/extensions/synology-chat/src/channel.ts +++ b/extensions/synology-chat/src/channel.ts @@ -4,6 +4,7 @@ * Implements the ChannelPlugin interface following the LINE pattern. */ +import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; import type { OpenClawConfig } from "openclaw/plugin-sdk/account-resolution"; import { createHybridChannelConfigAdapter, @@ -19,7 +20,6 @@ import { import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result"; import { createChatChannelPlugin, type ChannelPlugin } from "openclaw/plugin-sdk/core"; import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime"; -import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup"; import { listAccountIds, resolveAccount } from "./accounts.js"; import { synologyChatApprovalAuth } from "./approval-auth.js"; import { sendMessage, sendFileUrl } from "./client.js"; diff --git a/extensions/telegram/src/setup-core.ts b/extensions/telegram/src/setup-core.ts index 77d99503401..77ababd017e 100644 --- a/extensions/telegram/src/setup-core.ts +++ b/extensions/telegram/src/setup-core.ts @@ -7,8 +7,8 @@ import { splitSetupEntries, type OpenClawConfig, type WizardPrompter, -} from "openclaw/plugin-sdk/setup"; -import type { ChannelSetupAdapter, ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup"; +} from "openclaw/plugin-sdk/setup-runtime"; +import type { ChannelSetupAdapter, ChannelSetupDmPolicy } from "openclaw/plugin-sdk/setup-runtime"; import { formatCliCommand, formatDocsLink } from "openclaw/plugin-sdk/setup-tools"; import { resolveDefaultTelegramAccountId, resolveTelegramAccount } from "./accounts.js"; import { lookupTelegramChatId } from "./api-fetch.js"; diff --git a/extensions/whatsapp/src/channel-outbound.ts b/extensions/whatsapp/src/channel-outbound.ts index 393448ac8f8..0c7b88ffe55 100644 --- a/extensions/whatsapp/src/channel-outbound.ts +++ b/extensions/whatsapp/src/channel-outbound.ts @@ -1,4 +1,4 @@ -import { chunkText } from "openclaw/plugin-sdk/reply-runtime"; +import { chunkText } from "openclaw/plugin-sdk/reply-chunking"; import { createWhatsAppOutboundBase } from "./outbound-base.js"; import { resolveWhatsAppOutboundTarget } from "./resolve-outbound-target.js"; import { getWhatsAppRuntime } from "./runtime.js"; diff --git a/extensions/whatsapp/src/shared.ts b/extensions/whatsapp/src/shared.ts index cfcea8d9d77..f236e43b15a 100644 --- a/extensions/whatsapp/src/shared.ts +++ b/extensions/whatsapp/src/shared.ts @@ -11,7 +11,7 @@ import type { ChannelPlugin } from "openclaw/plugin-sdk/core"; import { createDelegatedSetupWizardProxy, type ChannelSetupWizard, -} from "openclaw/plugin-sdk/setup"; +} from "openclaw/plugin-sdk/setup-runtime"; import { listWhatsAppAccountIds, resolveDefaultWhatsAppAccountId, diff --git a/extensions/zalouser/src/setup-core.ts b/extensions/zalouser/src/setup-core.ts index e1f9e9fd27c..42a2b9ba589 100644 --- a/extensions/zalouser/src/setup-core.ts +++ b/extensions/zalouser/src/setup-core.ts @@ -1,4 +1,4 @@ -import { createPatchedAccountSetupAdapter } from "openclaw/plugin-sdk/setup"; +import { createPatchedAccountSetupAdapter } from "openclaw/plugin-sdk/setup-runtime"; const channel = "zalouser" as const; diff --git a/src/agents/live-model-switch.test.ts b/src/agents/live-model-switch.test.ts index 07210555740..21bde558ffb 100644 --- a/src/agents/live-model-switch.test.ts +++ b/src/agents/live-model-switch.test.ts @@ -10,13 +10,16 @@ const state = vi.hoisted(() => ({ loadSessionStoreMock: vi.fn(), resolveStorePathMock: vi.fn(), updateSessionStoreMock: vi.fn(), + piEmbeddedModuleImported: false, })); -vi.mock("./pi-embedded.js", () => ({ - abortEmbeddedPiRun: (...args: unknown[]) => state.abortEmbeddedPiRunMock(...args), -})); +vi.mock("./pi-embedded.js", () => { + state.piEmbeddedModuleImported = true; + return {}; +}); vi.mock("./pi-embedded-runner/runs.js", () => ({ + abortEmbeddedPiRun: (...args: unknown[]) => state.abortEmbeddedPiRunMock(...args), requestEmbeddedRunModelSwitch: (...args: unknown[]) => state.requestEmbeddedRunModelSwitchMock(...args), consumeEmbeddedRunModelSwitch: (...args: unknown[]) => @@ -56,6 +59,7 @@ describe("live model switch", () => { state.abortEmbeddedPiRunMock.mockReset().mockReturnValue(false); state.requestEmbeddedRunModelSwitchMock.mockReset(); state.consumeEmbeddedRunModelSwitchMock.mockReset(); + state.piEmbeddedModuleImported = false; state.resolveDefaultModelForAgentMock .mockReset() .mockReturnValue({ provider: "anthropic", model: "claude-opus-4-6" }); @@ -247,6 +251,12 @@ describe("live model switch", () => { }); }); + it("does not import the broad pi-embedded barrel on module load", async () => { + await loadModule(); + + expect(state.piEmbeddedModuleImported).toBe(false); + }); + it("treats auth-profile-source changes as no-op when no auth profile is selected", async () => { const { hasDifferentLiveSessionModelSelection } = await loadModule(); diff --git a/src/agents/live-model-switch.ts b/src/agents/live-model-switch.ts index ad869cd7121..bf6765c23ba 100644 --- a/src/agents/live-model-switch.ts +++ b/src/agents/live-model-switch.ts @@ -4,11 +4,11 @@ import type { SessionEntry } from "../config/sessions/types.js"; import { LiveSessionModelSwitchError } from "./live-model-switch-error.js"; import { resolveDefaultModelForAgent, resolvePersistedModelRef } from "./model-selection.js"; import { + abortEmbeddedPiRun, consumeEmbeddedRunModelSwitch, requestEmbeddedRunModelSwitch, type EmbeddedRunModelSwitchRequest, } from "./pi-embedded-runner/runs.js"; -import { abortEmbeddedPiRun } from "./pi-embedded.js"; export { LiveSessionModelSwitchError } from "./live-model-switch-error.js"; export type LiveSessionModelSelection = EmbeddedRunModelSwitchRequest; diff --git a/src/channels/plugins/bundled.ts b/src/channels/plugins/bundled.ts index 054327f40b8..551b7a89a05 100644 --- a/src/channels/plugins/bundled.ts +++ b/src/channels/plugins/bundled.ts @@ -279,7 +279,6 @@ function getBundledChannelState(): BundledChannelState { if (cachedBundledChannelState) { return cachedBundledChannelState; } - const entries = loadGeneratedBundledChannelEntries(); const plugins = entries.map(({ entry }) => entry.channelPlugin); const setupPlugins = entries.flatMap(({ setupEntry }) => { diff --git a/src/channels/plugins/setup-helpers.import-safety.test.ts b/src/channels/plugins/setup-helpers.import-safety.test.ts new file mode 100644 index 00000000000..6003e035d74 --- /dev/null +++ b/src/channels/plugins/setup-helpers.import-safety.test.ts @@ -0,0 +1,34 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { importFreshModule } from "../../../test/helpers/import-fresh.ts"; +import { clearSetupPromotionRuntimeModuleCache } from "./setup-helpers.js"; + +afterEach(() => { + vi.doUnmock("../../plugins/discovery.js"); + clearSetupPromotionRuntimeModuleCache(); +}); + +describe("setup helper import safety", () => { + it("does not load contract-surface discovery on module import", async () => { + const state = { + discoveryLoaded: false, + }; + + vi.doMock("../../plugins/discovery.js", () => { + state.discoveryLoaded = true; + throw new Error("contract surface discovery should stay lazy on import"); + }); + + const helpers = await importFreshModule( + import.meta.url, + "./setup-helpers.js?scope=import-safety", + ); + + expect(state.discoveryLoaded).toBe(false); + expect( + helpers.createPatchedAccountSetupAdapter({ + channelKey: "demo-setup", + buildPatch: () => ({}), + }), + ).toBeDefined(); + }); +}); diff --git a/src/channels/plugins/setup-helpers.test.ts b/src/channels/plugins/setup-helpers.test.ts index 66296eac4da..27d9c00f1eb 100644 --- a/src/channels/plugins/setup-helpers.test.ts +++ b/src/channels/plugins/setup-helpers.test.ts @@ -1,8 +1,9 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import { applySetupAccountConfigPatch, + clearSetupPromotionRuntimeModuleCache, createEnvPatchedAccountSetupAdapter, createPatchedAccountSetupAdapter, moveSingleAccountChannelSectionToDefaultAccount, @@ -13,6 +14,10 @@ function asConfig(value: unknown): OpenClawConfig { return value as OpenClawConfig; } +afterEach(() => { + clearSetupPromotionRuntimeModuleCache(); +}); + describe("applySetupAccountConfigPatch", () => { it("patches top-level config for default account and enables channel", () => { const next = applySetupAccountConfigPatch({ diff --git a/src/channels/plugins/setup-helpers.ts b/src/channels/plugins/setup-helpers.ts index 36859160d64..2ef78d38fe7 100644 --- a/src/channels/plugins/setup-helpers.ts +++ b/src/channels/plugins/setup-helpers.ts @@ -1,10 +1,53 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { createJiti } from "jiti"; import { z, type ZodType } from "zod"; import type { OpenClawConfig } from "../../config/config.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; -import { getBundledChannelContractSurfaceEntries } from "./contract-surfaces.js"; import type { ChannelSetupAdapter } from "./types.adapters.js"; import type { ChannelSetupInput } from "./types.core.js"; +const SETUP_PROMOTION_SURFACE_MODULE_BASENAMES = [ + "contract-surfaces.ts", + "contract-surfaces.js", +] as const; + +type SetupPromotionRuntimeModule = Pick< + typeof import("./contract-surfaces.js"), + "getBundledChannelContractSurfaceEntries" +>; + +let cachedSetupPromotionRuntimeModule: SetupPromotionRuntimeModule | null = null; + +export function clearSetupPromotionRuntimeModuleCache(): void { + cachedSetupPromotionRuntimeModule = null; +} + +function resolveSetupPromotionRuntimeModulePath(): string { + for (const basename of SETUP_PROMOTION_SURFACE_MODULE_BASENAMES) { + const candidatePath = fileURLToPath(new URL(basename, import.meta.url)); + const resolvedPath = candidatePath.replace( + `${path.sep}dist-runtime${path.sep}`, + `${path.sep}dist${path.sep}`, + ); + if (fs.existsSync(resolvedPath)) { + return resolvedPath; + } + if (fs.existsSync(candidatePath)) { + return candidatePath; + } + } + throw new Error("missing setup promotion runtime module"); +} + +function loadSetupPromotionRuntimeModule(): SetupPromotionRuntimeModule { + cachedSetupPromotionRuntimeModule ??= createJiti(import.meta.url)( + resolveSetupPromotionRuntimeModulePath(), + ) as SetupPromotionRuntimeModule; + return cachedSetupPromotionRuntimeModule; +} + type ChannelSectionBase = { name?: string; defaultAccount?: string; @@ -415,9 +458,9 @@ type ChannelSetupPromotionSurface = { }; function getChannelSetupPromotionSurface(channelKey: string): ChannelSetupPromotionSurface | null { - const entry = getBundledChannelContractSurfaceEntries().find( - (candidate) => candidate.pluginId === channelKey, - ); + const entry = loadSetupPromotionRuntimeModule() + .getBundledChannelContractSurfaceEntries() + .find((candidate) => candidate.pluginId === channelKey); if (!entry || !entry.surface || typeof entry.surface !== "object") { return null; } @@ -473,14 +516,22 @@ export function resolveSingleAccountPromotionTarget(params: { channelKey: string; channel: ChannelSectionBase; }): string { + const accounts = params.channel.accounts ?? {}; + const resolveExistingAccountId = (targetAccountId: string): string => { + const normalizedTargetAccountId = normalizeAccountId(targetAccountId); + const matchedAccountId = Object.keys(accounts).find( + (accountId) => normalizeAccountId(accountId) === normalizedTargetAccountId, + ); + return matchedAccountId ?? normalizedTargetAccountId; + }; const surface = getChannelSetupPromotionSurface(params.channelKey); const resolved = surface?.resolveSingleAccountPromotionTarget?.({ channel: params.channel, }); if (typeof resolved === "string" && resolved.trim()) { - return normalizeAccountId(resolved); + return resolveExistingAccountId(resolved); } - return DEFAULT_ACCOUNT_ID; + return resolveExistingAccountId(DEFAULT_ACCOUNT_ID); } function cloneIfObject(value: T): T { diff --git a/src/commands/models/list.probe.test.ts b/src/commands/models/list.probe.test.ts index 70ffde1dd65..a57df70c4ad 100644 --- a/src/commands/models/list.probe.test.ts +++ b/src/commands/models/list.probe.test.ts @@ -1,7 +1,22 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; +import { importFreshModule } from "../../../test/helpers/import-fresh.js"; import { mapFailoverReasonToProbeStatus } from "./list.probe.js"; describe("mapFailoverReasonToProbeStatus", () => { + it("does not import the embedded runner on module load", async () => { + vi.doMock("../../agents/pi-embedded.js", () => { + throw new Error("pi-embedded should stay lazy for probe imports"); + }); + try { + await importFreshModule( + import.meta.url, + `./list.probe.js?scope=${Math.random().toString(36).slice(2)}`, + ); + } finally { + vi.doUnmock("../../agents/pi-embedded.js"); + } + }); + it("maps auth_permanent to auth", () => { expect(mapFailoverReasonToProbeStatus("auth_permanent")).toBe("auth"); }); diff --git a/src/commands/models/list.probe.ts b/src/commands/models/list.probe.ts index df2da96d72e..1cd2f66d4e4 100644 --- a/src/commands/models/list.probe.ts +++ b/src/commands/models/list.probe.ts @@ -19,7 +19,6 @@ import { normalizeProviderId, parseModelRef, } from "../../agents/model-selection.js"; -import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js"; import type { OpenClawConfig } from "../../config/config.js"; import { @@ -33,6 +32,13 @@ import { DEFAULT_PROVIDER, formatMs } from "./shared.js"; const PROBE_PROMPT = "Reply with OK. Do not use tools."; +let embeddedRunnerModulePromise: Promise | undefined; + +function loadEmbeddedRunnerModule() { + embeddedRunnerModulePromise ??= import("../../agents/pi-embedded.js"); + return embeddedRunnerModulePromise; +} + export type AuthProbeStatus = | "ok" | "auth" @@ -450,6 +456,7 @@ async function probeTarget(params: { latencyMs: Date.now() - start, }); try { + const { runEmbeddedPiAgent } = await loadEmbeddedRunnerModule(); await runEmbeddedPiAgent({ sessionId, sessionFile, diff --git a/src/plugin-sdk/setup-runtime.ts b/src/plugin-sdk/setup-runtime.ts index 6d4b61f56f0..8068204ed66 100644 --- a/src/plugin-sdk/setup-runtime.ts +++ b/src/plugin-sdk/setup-runtime.ts @@ -23,15 +23,19 @@ export { createLegacyCompatChannelDmPolicy, createStandardChannelSetupStatus, mergeAllowFromEntries, + noteChannelLookupFailure, + noteChannelLookupSummary, parseSetupEntriesAllowingWildcard, parseMentionOrPrefixedId, patchChannelConfigForAccount, + promptResolvedAllowFrom, promptLegacyChannelAllowFromForAccount, promptParsedAllowFromForAccount, resolveEntriesWithOptionalToken, resolveSetupAccountId, setAccountAllowFromForChannel, setSetupChannelEnabled, + splitSetupEntries, } from "../channels/plugins/setup-wizard-helpers.js"; export { createAllowlistSetupWizardProxy } from "../channels/plugins/setup-wizard-proxy.js";