From 18664077b009af979784555a3f48c3f7cec349a4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 06:40:17 +0100 Subject: [PATCH] fix(channels): defer setup runtime deps until login --- src/commands/channel-setup/plugin-install.ts | 4 ++++ src/commands/channels.add.test.ts | 6 ++++++ src/commands/channels/add.ts | 13 +++++++------ src/flows/channel-setup.ts | 5 +++-- src/plugins/loader.ts | 20 +++++++++++++++++--- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/commands/channel-setup/plugin-install.ts b/src/commands/channel-setup/plugin-install.ts index a7896b01c3f..cf13b81abf3 100644 --- a/src/commands/channel-setup/plugin-install.ts +++ b/src/commands/channel-setup/plugin-install.ts @@ -71,6 +71,7 @@ function loadChannelSetupPluginRegistry(params: { workspaceDir?: string; onlyPluginIds?: string[]; activate?: boolean; + installRuntimeDeps?: boolean; }): PluginRegistry { clearPluginDiscoveryCache(); const autoEnabled = applyPluginAutoEnable({ config: params.cfg, env: process.env }); @@ -88,7 +89,9 @@ function loadChannelSetupPluginRegistry(params: { logger: createPluginLoaderLogger(log), onlyPluginIds: params.onlyPluginIds, includeSetupOnlyChannelPlugins: true, + forceSetupOnlyChannelPlugins: params.installRuntimeDeps === false, activate: params.activate, + installBundledRuntimeDeps: params.installRuntimeDeps !== false, }); } @@ -156,6 +159,7 @@ export function loadChannelSetupPluginRegistrySnapshotForChannel(params: { channel: string; pluginId?: string; workspaceDir?: string; + installRuntimeDeps?: boolean; }): PluginRegistry { const scopedPluginId = resolveScopedChannelPluginId({ cfg: params.cfg, diff --git a/src/commands/channels.add.test.ts b/src/commands/channels.add.test.ts index 3c6a9df30e2..8003ed0668c 100644 --- a/src/commands/channels.add.test.ts +++ b/src/commands/channels.add.test.ts @@ -327,6 +327,9 @@ describe("channelsAddCommand", () => { expect.objectContaining({ entry: catalogEntry }), ); expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledTimes(1); + expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith( + expect.objectContaining({ installRuntimeDeps: false }), + ); expectExternalChatEnabledConfigWrite(); expect(runtime.error).not.toHaveBeenCalled(); expect(runtime.exit).not.toHaveBeenCalled(); @@ -348,6 +351,9 @@ describe("channelsAddCommand", () => { expect(ensureChannelSetupPluginInstalled).not.toHaveBeenCalled(); expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledTimes(1); + expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith( + expect.objectContaining({ installRuntimeDeps: false }), + ); expectExternalChatEnabledConfigWrite(); }); diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index 76c1adaaf2b..0b41344bd92 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -1,6 +1,6 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { parseOptionalDelimitedEntries } from "../../channels/plugins/helpers.js"; -import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; +import { getLoadedChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import { moveSingleAccountChannelSectionToDefaultAccount } from "../../channels/plugins/setup-helpers.js"; import type { ChannelSetupPlugin } from "../../channels/plugins/setup-wizard-types.js"; import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js"; @@ -141,7 +141,7 @@ export async function channelsAddCommand( if (wantsNames) { for (const channel of selection) { const accountId = accountIds[channel] ?? DEFAULT_ACCOUNT_ID; - const plugin = resolvedPlugins.get(channel) ?? getChannelPlugin(channel); + const plugin = resolvedPlugins.get(channel) ?? getLoadedChannelPlugin(channel); const account = plugin?.config.resolveAccount(nextConfig, accountId) as | { name?: string } | undefined; @@ -248,7 +248,7 @@ export async function channelsAddCommand( channelId: ChannelId, pluginId?: string, ): Promise => { - const existing = getChannelPlugin(channelId); + const existing = getLoadedChannelPlugin(channelId); if (existing) { return existing; } @@ -260,10 +260,11 @@ export async function channelsAddCommand( channel: channelId, ...(pluginId ? { pluginId } : {}), workspaceDir: resolveWorkspaceDir(), + installRuntimeDeps: false, }); return ( - snapshot.channels.find((entry) => entry.plugin.id === channelId)?.plugin ?? - snapshot.channelSetups.find((entry) => entry.plugin.id === channelId)?.plugin + snapshot.channelSetups.find((entry) => entry.plugin.id === channelId)?.plugin ?? + snapshot.channels.find((entry) => entry.plugin.id === channelId)?.plugin ); }; @@ -359,7 +360,7 @@ export async function channelsAddCommand( nextConfig, ...(baseHash !== undefined ? { baseHash } : {}), }); - runtime.log(`Added ${channelLabel(channel)} account "${accountId}".`); + runtime.log(`Added ${plugin.meta.label ?? channelLabel(channel)} account "${accountId}".`); const afterAccountConfigWritten = plugin.setup?.afterAccountConfigWritten; if (afterAccountConfigWritten) { const { runCollectedChannelOnboardingPostWriteHooks } = await loadOnboardChannels(); diff --git a/src/flows/channel-setup.ts b/src/flows/channel-setup.ts index be4ec804ce9..65e00541f4a 100644 --- a/src/flows/channel-setup.ts +++ b/src/flows/channel-setup.ts @@ -174,10 +174,11 @@ export async function setupChannels( channel, ...(pluginId ? { pluginId } : {}), workspaceDir: resolveWorkspaceDir(), + installRuntimeDeps: false, }); const plugin = - snapshot.channels.find((entry) => entry.plugin.id === channel)?.plugin ?? - snapshot.channelSetups.find((entry) => entry.plugin.id === channel)?.plugin; + snapshot.channelSetups.find((entry) => entry.plugin.id === channel)?.plugin ?? + snapshot.channels.find((entry) => entry.plugin.id === channel)?.plugin; if (plugin) { rememberScopedPlugin(plugin); return plugin; diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 829af937ce1..7454d105676 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -145,6 +145,7 @@ export type PluginLoadOptions = { preferSetupRuntimeForChannelPlugins?: boolean; activate?: boolean; loadModules?: boolean; + installBundledRuntimeDeps?: boolean; throwOnLoadError?: boolean; bundledRuntimeDepsInstaller?: (params: BundledRuntimeDepsInstallParams) => void; }; @@ -788,6 +789,7 @@ function buildCacheKey(params: { requireSetupEntryForSetupOnlyChannelPlugins?: boolean; preferSetupRuntimeForChannelPlugins?: boolean; loadModules?: boolean; + installBundledRuntimeDeps?: boolean; runtimeSubagentMode?: "default" | "explicit" | "gateway-bindable"; pluginSdkResolution?: PluginSdkResolutionPreference; coreGatewayMethodNames?: string[]; @@ -824,6 +826,8 @@ function buildCacheKey(params: { const startupChannelMode = params.preferSetupRuntimeForChannelPlugins === true ? "prefer-setup" : "full"; const moduleLoadMode = params.loadModules === false ? "manifest-only" : "load-modules"; + const bundledRuntimeDepsMode = + params.installBundledRuntimeDeps === false ? "skip-runtime-deps" : "install-runtime-deps"; const runtimeSubagentMode = params.runtimeSubagentMode ?? "default"; const gatewayMethodsKey = JSON.stringify(params.coreGatewayMethodNames ?? []); return `${roots.workspace ?? ""}::${roots.global ?? ""}::${roots.stock ?? ""}::${JSON.stringify({ @@ -831,7 +835,7 @@ function buildCacheKey(params: { installs, loadPaths, activationMetadataKey: params.activationMetadataKey ?? "", - })}::${scopeKey}::${setupOnlyKey}::${setupOnlyModeKey}::${setupOnlyRequirementKey}::${startupChannelMode}::${moduleLoadMode}::${runtimeSubagentMode}::${params.pluginSdkResolution ?? "auto"}::${gatewayMethodsKey}`; + })}::${scopeKey}::${setupOnlyKey}::${setupOnlyModeKey}::${setupOnlyRequirementKey}::${startupChannelMode}::${moduleLoadMode}::${bundledRuntimeDepsMode}::${runtimeSubagentMode}::${params.pluginSdkResolution ?? "auto"}::${gatewayMethodsKey}`; } function matchesScopedPluginRequest(params: { @@ -909,6 +913,7 @@ function hasExplicitCompatibilityInputs(options: PluginLoadOptions): boolean { options.forceSetupOnlyChannelPlugins === true || options.requireSetupEntryForSetupOnlyChannelPlugins === true || options.preferSetupRuntimeForChannelPlugins === true || + options.installBundledRuntimeDeps === false || options.loadModules === false ); } @@ -934,6 +939,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { const requireSetupEntryForSetupOnlyChannelPlugins = options.requireSetupEntryForSetupOnlyChannelPlugins === true; const preferSetupRuntimeForChannelPlugins = options.preferSetupRuntimeForChannelPlugins === true; + const shouldInstallBundledRuntimeDeps = options.installBundledRuntimeDeps !== false; const runtimeSubagentMode = resolveRuntimeSubagentMode(options.runtimeOptions); const coreGatewayMethodNames = Object.keys(options.coreGatewayHandlers ?? {}).toSorted(); const cacheKey = buildCacheKey({ @@ -951,6 +957,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { requireSetupEntryForSetupOnlyChannelPlugins, preferSetupRuntimeForChannelPlugins, loadModules: options.loadModules, + installBundledRuntimeDeps: options.installBundledRuntimeDeps, runtimeSubagentMode, pluginSdkResolution: options.pluginSdkResolution, coreGatewayMethodNames, @@ -969,6 +976,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { preferSetupRuntimeForChannelPlugins, shouldActivate: options.activate !== false, shouldLoadModules: options.loadModules !== false, + shouldInstallBundledRuntimeDeps, runtimeSubagentMode, cacheKey, }; @@ -1847,6 +1855,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi preferSetupRuntimeForChannelPlugins, shouldActivate, shouldLoadModules, + shouldInstallBundledRuntimeDeps, cacheKey, runtimeSubagentMode, } = resolvePluginLoadCacheContext(options); @@ -2191,7 +2200,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi markPluginActivationDisabled(record, enableState.reason); } - if (shouldLoadModules && candidate.origin === "bundled" && enableState.enabled) { + if ( + shouldLoadModules && + shouldInstallBundledRuntimeDeps && + candidate.origin === "bundled" && + enableState.enabled + ) { try { const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, { env }); const retainSpecs = bundledRuntimeDepsRetainSpecsByInstallRoot.get(installRoot) ?? []; @@ -2434,7 +2448,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi manifestRecord.setupSource ) { const setupRegistration = resolveSetupChannelRegistration(mod, { - installRuntimeDeps: enableState.enabled, + installRuntimeDeps: shouldInstallBundledRuntimeDeps && enableState.enabled, }); if (setupRegistration.loadError) { recordPluginError({