From 9b1f1036acb286b4d0400da62f09bcb7d0437e4f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 23:43:45 +0100 Subject: [PATCH] fix(channels): isolate bundled load failures --- CHANGELOG.md | 1 + .../bundled-channel-runtime-deps-docker.sh | 165 ++++++++++++++++++ src/channels/plugins/bootstrap-registry.ts | 31 +++- .../plugins/bundled-root-caches.test.ts | 74 ++++++++ .../plugins/bundled.shape-guard.test.ts | 89 ++++++++++ src/channels/plugins/bundled.ts | 71 +++++--- 6 files changed, 403 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68882d75ad6..ee9e6fa16fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai - Diffs/viewer: re-read remote viewer access policy from live runtime config on each request, so toggling `plugins.entries.diffs.config.security.allowRemoteViewer` closes proxied viewer access immediately instead of waiting for a restart. Thanks @vincentkoc. - Agents/subagents: drop bare `NO_REPLY` from the parent turn when the session still has pending spawned children, so direct-conversation surfaces such as Telegram DMs no longer rewrite the sentinel into visible fallback chatter while waiting for the child completion event. (#69942) Thanks @neeravmakwana. - Plugins/install: keep bundled plugin dependencies off npm install while repairing them when plugins activate from a packaged install, including Feishu/Lark, Browser, and direct bundled channel setup-entry loads. +- CLI/channels: skip and cache bundled channel plugin, setup, and secrets load failures during read-only discovery, so one broken unused bundled channel cannot crash `openclaw status` or bootstrap secret scans. - Memory/LanceDB: retry initialization after a failed LanceDB load and report unsupported Intel macOS native runtime clearly instead of caching the failure or repeatedly attempting an install that cannot work. - CLI/Claude: hash only static extra system prompt parts when deciding whether to reuse a CLI session, so per-message inbound metadata no longer resets Claude CLI conversations on every turn. (#70122) Thanks @zijunl. - Hooks/Slack: standardize shared message hook routing fields (`threadId` / `replyToId`) and stop Slack outbound delivery from re-running `message_sending` inside the channel adapter, so plugins like thread-ownership make one outbound routing decision per reply. Thanks @vincentkoc. diff --git a/scripts/e2e/bundled-channel-runtime-deps-docker.sh b/scripts/e2e/bundled-channel-runtime-deps-docker.sh index 3cea4300147..1acb753fe3e 100644 --- a/scripts/e2e/bundled-channel-runtime-deps-docker.sh +++ b/scripts/e2e/bundled-channel-runtime-deps-docker.sh @@ -10,6 +10,7 @@ RUN_CHANNEL_SCENARIOS="${OPENCLAW_BUNDLED_CHANNEL_SCENARIOS:-1}" RUN_UPDATE_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO:-1}" RUN_ROOT_OWNED_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO:-1}" RUN_SETUP_ENTRY_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO:-1}" +RUN_LOAD_FAILURE_SCENARIO="${OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO:-1}" echo "Building Docker image..." run_logged bundled-channel-deps-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" @@ -1001,6 +1002,167 @@ EOF rm -f "$run_log" } +run_load_failure_scenario() { + local run_log + run_log="$(mktemp "${TMPDIR:-/tmp}/openclaw-bundled-channel-load-failure.XXXXXX")" + + echo "Running bundled channel load-failure isolation Docker E2E..." + if ! docker run --rm \ + -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + -i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF' +set -euo pipefail + +export HOME="$(mktemp -d "/tmp/openclaw-bundled-channel-load-failure.XXXXXX")" +export NPM_CONFIG_PREFIX="$HOME/.npm-global" +export PATH="$NPM_CONFIG_PREFIX/bin:$PATH" +export OPENCLAW_NO_ONBOARD=1 + +package_root() { + printf "%s/openclaw" "$(npm root -g)" +} + +echo "Packing and installing current OpenClaw build..." +pack_dir="$(mktemp -d "/tmp/openclaw-load-failure-pack.XXXXXX")" +npm pack --ignore-scripts --pack-destination "$pack_dir" >/tmp/openclaw-load-failure-pack.log 2>&1 +package_tgz="$(find "$pack_dir" -maxdepth 1 -name 'openclaw-*.tgz' -print -quit)" +if [ -z "$package_tgz" ]; then + cat /tmp/openclaw-load-failure-pack.log + echo "missing packed OpenClaw tarball" >&2 + exit 1 +fi +npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-load-failure-install.log 2>&1 + +root="$(package_root)" +plugin_dir="$root/dist/extensions/load-failure-alpha" +mkdir -p "$plugin_dir" +cat >"$plugin_dir/package.json" <<'JSON' +{ + "name": "@openclaw/load-failure-alpha", + "version": "2026.4.21", + "private": true, + "type": "module", + "openclaw": { + "extensions": ["./index.js"], + "setupEntry": "./setup-entry.js" + } +} +JSON +cat >"$plugin_dir/openclaw.plugin.json" <<'JSON' +{ + "id": "load-failure-alpha", + "channels": ["load-failure-alpha"], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} +JSON +cat >"$plugin_dir/index.js" <<'JS' +export default { + kind: "bundled-channel-entry", + id: "load-failure-alpha", + name: "Load Failure Alpha", + description: "Load Failure Alpha", + register() {}, + loadChannelSecrets() { + globalThis.__loadFailureSecrets = (globalThis.__loadFailureSecrets ?? 0) + 1; + throw new Error("synthetic channel secrets failure"); + }, + loadChannelPlugin() { + globalThis.__loadFailurePlugin = (globalThis.__loadFailurePlugin ?? 0) + 1; + throw new Error("synthetic channel plugin failure"); + } +}; +JS +cat >"$plugin_dir/setup-entry.js" <<'JS' +export default { + kind: "bundled-channel-setup-entry", + loadSetupSecrets() { + globalThis.__loadFailureSetupSecrets = (globalThis.__loadFailureSetupSecrets ?? 0) + 1; + throw new Error("synthetic setup secrets failure"); + }, + loadSetupPlugin() { + globalThis.__loadFailureSetup = (globalThis.__loadFailureSetup ?? 0) + 1; + throw new Error("synthetic setup plugin failure"); + } +}; +JS + +echo "Loading synthetic failing bundled channel through packaged loader..." +( + cd "$root" + OPENCLAW_BUNDLED_PLUGINS_DIR="$root/dist/extensions" node --input-type=module - <<'NODE' +import fs from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const root = process.cwd(); +const distDir = path.join(root, "dist"); +const bundledPath = fs + .readdirSync(distDir) + .filter((entry) => /^bundled-[A-Za-z0-9_-]+\.js$/.test(entry)) + .map((entry) => path.join(distDir, entry)) + .find((entry) => fs.readFileSync(entry, "utf8").includes("src/channels/plugins/bundled.ts")); +if (!bundledPath) { + throw new Error("missing packaged bundled channel loader artifact"); +} +const bundled = await import(pathToFileURL(bundledPath)); +const oneArgExports = Object.entries(bundled).filter( + ([, value]) => typeof value === "function" && value.length === 1, +); +if (oneArgExports.length === 0) { + throw new Error(`missing one-argument bundled loader exports; exports=${Object.keys(bundled).join(",")}`); +} + +const id = "load-failure-alpha"; +for (let i = 0; i < 2; i += 1) { + for (const [name, fn] of oneArgExports) { + try { + fn(id); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (message.includes("synthetic")) { + throw new Error(`bundled export ${name} leaked synthetic load failure: ${message}`); + } + } + } +} + +const counts = { + plugin: globalThis.__loadFailurePlugin, + setup: globalThis.__loadFailureSetup, + secrets: globalThis.__loadFailureSecrets, + setupSecrets: globalThis.__loadFailureSetupSecrets, +}; +for (const [key, value] of Object.entries({ + plugin: counts.plugin, + setup: counts.setup, + setupSecrets: counts.setupSecrets, +})) { + if (value !== 1) { + throw new Error(`expected ${key} failure to be cached after one load, got ${value}`); + } +} +if (counts.secrets !== undefined && counts.secrets !== 1) { + throw new Error(`expected secrets failure to be cached after one load when exercised, got ${counts.secrets}`); +} +console.log("synthetic bundled channel load failures were isolated and cached"); +NODE +) + +echo "bundled channel load-failure isolation Docker E2E passed" +EOF + then + cat "$run_log" + rm -f "$run_log" + exit 1 + fi + + cat "$run_log" + rm -f "$run_log" +} + if [ "$RUN_CHANNEL_SCENARIOS" != "0" ]; then run_channel_scenario telegram grammy run_channel_scenario discord discord-api-types @@ -1017,3 +1179,6 @@ fi if [ "$RUN_SETUP_ENTRY_SCENARIO" != "0" ]; then run_setup_entry_scenario fi +if [ "$RUN_LOAD_FAILURE_SCENARIO" != "0" ]; then + run_load_failure_scenario +fi diff --git a/src/channels/plugins/bootstrap-registry.ts b/src/channels/plugins/bootstrap-registry.ts index 93cf31d5431..183a840e4b6 100644 --- a/src/channels/plugins/bootstrap-registry.ts +++ b/src/channels/plugins/bootstrap-registry.ts @@ -127,8 +127,15 @@ export function getBootstrapChannelPlugin(id: ChannelId): ChannelPlugin | undefi if (registry.missingIds.has(resolvedId)) { return undefined; } - const runtimePlugin = getBundledChannelPlugin(resolvedId); - const setupPlugin = getBundledChannelSetupPlugin(resolvedId); + let runtimePlugin: ChannelPlugin | undefined; + let setupPlugin: ChannelPlugin | undefined; + try { + runtimePlugin = getBundledChannelPlugin(resolvedId); + setupPlugin = getBundledChannelSetupPlugin(resolvedId); + } catch { + registry.missingIds.add(resolvedId); + return undefined; + } const merged = runtimePlugin && setupPlugin ? mergeBootstrapPlugin(runtimePlugin, setupPlugin) @@ -154,11 +161,21 @@ export function getBootstrapChannelSecrets(id: ChannelId): ChannelPlugin["secret if (registry.secretsById.has(resolvedId)) { return undefined; } - const runtimeSecrets = getBundledChannelSecrets(resolvedId); - const setupSecrets = getBundledChannelSetupSecrets(resolvedId); - const merged = mergePluginSection(runtimeSecrets, setupSecrets); - registry.secretsById.set(resolvedId, merged ?? null); - return merged; + if (registry.missingIds.has(resolvedId)) { + registry.secretsById.set(resolvedId, null); + return undefined; + } + try { + const runtimeSecrets = getBundledChannelSecrets(resolvedId); + const setupSecrets = getBundledChannelSetupSecrets(resolvedId); + const merged = mergePluginSection(runtimeSecrets, setupSecrets); + registry.secretsById.set(resolvedId, merged ?? null); + return merged; + } catch { + registry.missingIds.add(resolvedId); + registry.secretsById.set(resolvedId, null); + return undefined; + } } export function clearBootstrapChannelPluginCache(): void { diff --git a/src/channels/plugins/bundled-root-caches.test.ts b/src/channels/plugins/bundled-root-caches.test.ts index 704d3a1255e..8b379297b87 100644 --- a/src/channels/plugins/bundled-root-caches.test.ts +++ b/src/channels/plugins/bundled-root-caches.test.ts @@ -144,4 +144,78 @@ describe("bundled root-aware caches", () => { bootstrapRegistry.getBootstrapChannelSecrets("beta")?.secretTargetRegistryEntries?.[0]?.id, ).toBe("setup-beta-B"); }); + + it("marks bundled plugin ids missing when bootstrap plugin loading throws", async () => { + const root = makeBundledRoot("openclaw-bootstrap-plugin-throw-"); + + vi.doMock("./bundled-ids.js", () => ({ + listBundledChannelPluginIdsForRoot: (cacheKey: string) => + cacheKey === root.pluginsDir ? ["alpha"] : [], + })); + + const getBundledChannelPluginMock = vi.fn(() => { + throw new Error("Cannot find module 'nostr-tools'"); + }); + const getBundledChannelSecretsMock = vi.fn(() => { + throw new Error("secrets should not load after plugin is marked missing"); + }); + + vi.doMock("./bundled.js", () => ({ + getBundledChannelPlugin: getBundledChannelPluginMock, + getBundledChannelSetupPlugin: vi.fn(() => undefined), + getBundledChannelSecrets: getBundledChannelSecretsMock, + getBundledChannelSetupSecrets: vi.fn(() => undefined), + })); + + const bootstrapRegistry = await importFreshModule( + import.meta.url, + "./bootstrap-registry.js?scope=bootstrap-plugin-load-guard", + ); + + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = root.pluginsDir; + expect(bootstrapRegistry.listBootstrapChannelPluginIds()).toEqual(["alpha"]); + expect(bootstrapRegistry.getBootstrapChannelPlugin("alpha")).toBeUndefined(); + expect(bootstrapRegistry.getBootstrapChannelPlugin("alpha")).toBeUndefined(); + expect(bootstrapRegistry.getBootstrapChannelSecrets("alpha")).toBeUndefined(); + expect(getBundledChannelPluginMock).toHaveBeenCalledTimes(1); + expect(getBundledChannelSecretsMock).not.toHaveBeenCalled(); + }); + + it("marks bundled plugin ids missing when bootstrap secrets loading throws", async () => { + const root = makeBundledRoot("openclaw-bootstrap-secrets-throw-"); + + vi.doMock("./bundled-ids.js", () => ({ + listBundledChannelPluginIdsForRoot: (cacheKey: string) => + cacheKey === root.pluginsDir ? ["alpha"] : [], + })); + + const getBundledChannelSecretsMock = vi.fn(() => { + throw new Error("Cannot find module '@larksuiteoapi/node-sdk'"); + }); + const getBundledChannelPluginMock = vi.fn(() => ({ + id: "alpha", + meta: { id: "alpha", label: "Alpha" }, + capabilities: {}, + config: {}, + })); + + vi.doMock("./bundled.js", () => ({ + getBundledChannelPlugin: getBundledChannelPluginMock, + getBundledChannelSetupPlugin: vi.fn(() => undefined), + getBundledChannelSecrets: getBundledChannelSecretsMock, + getBundledChannelSetupSecrets: vi.fn(() => undefined), + })); + + const bootstrapRegistry = await importFreshModule( + import.meta.url, + "./bootstrap-registry.js?scope=bootstrap-secrets-load-guard", + ); + + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = root.pluginsDir; + expect(bootstrapRegistry.getBootstrapChannelSecrets("alpha")).toBeUndefined(); + expect(bootstrapRegistry.getBootstrapChannelSecrets("alpha")).toBeUndefined(); + expect(bootstrapRegistry.getBootstrapChannelPlugin("alpha")).toBeUndefined(); + expect(getBundledChannelSecretsMock).toHaveBeenCalledTimes(1); + expect(getBundledChannelPluginMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/channels/plugins/bundled.shape-guard.test.ts b/src/channels/plugins/bundled.shape-guard.test.ts index 61399d18c7a..2eb0b3cfed4 100644 --- a/src/channels/plugins/bundled.shape-guard.test.ts +++ b/src/channels/plugins/bundled.shape-guard.test.ts @@ -635,6 +635,95 @@ describe("bundled channel entry shape guards", () => { } }); + it("swallows and caches bundled plugin and setup load failures", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-load-failure-")); + const previousBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; + const pluginDir = path.join(root, "dist", "extensions", "alpha"); + const testGlobal = globalThis as typeof globalThis & { + __bundledPluginFailureLoads?: number; + __bundledSetupFailureLoads?: number; + __bundledSecretsFailureLoads?: number; + __bundledSetupSecretsFailureLoads?: number; + }; + fs.mkdirSync(pluginDir, { recursive: true }); + fs.writeFileSync( + path.join(root, "package.json"), + JSON.stringify({ name: "openclaw", version: "2026.4.21" }), + "utf8", + ); + fs.writeFileSync( + path.join(pluginDir, "index.js"), + [ + "export default {", + " kind: 'bundled-channel-entry',", + " id: 'alpha',", + " name: 'Alpha',", + " description: 'Alpha',", + " register() {},", + " loadChannelSecrets() {", + " globalThis.__bundledSecretsFailureLoads = (globalThis.__bundledSecretsFailureLoads ?? 0) + 1;", + " throw new Error('missing channel secrets dep');", + " },", + " loadChannelPlugin() {", + " globalThis.__bundledPluginFailureLoads = (globalThis.__bundledPluginFailureLoads ?? 0) + 1;", + " throw new Error('missing channel plugin dep');", + " },", + "};", + "", + ].join("\n"), + "utf8", + ); + fs.writeFileSync( + path.join(pluginDir, "setup-entry.js"), + [ + "export default {", + " kind: 'bundled-channel-setup-entry',", + " loadSetupSecrets() {", + " globalThis.__bundledSetupSecretsFailureLoads = (globalThis.__bundledSetupSecretsFailureLoads ?? 0) + 1;", + " throw new Error('missing setup secrets dep');", + " },", + " loadSetupPlugin() {", + " globalThis.__bundledSetupFailureLoads = (globalThis.__bundledSetupFailureLoads ?? 0) + 1;", + " throw new Error('missing setup plugin dep');", + " },", + "};", + "", + ].join("\n"), + "utf8", + ); + + mockAlphaDistExtensionRuntime(); + + try { + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = path.join(root, "dist", "extensions"); + + const bundled = await importFreshModule( + import.meta.url, + "./bundled.js?scope=bundled-load-failure", + ); + + expect(bundled.getBundledChannelPlugin("alpha")).toBeUndefined(); + expect(bundled.getBundledChannelPlugin("alpha")).toBeUndefined(); + expect(bundled.getBundledChannelSetupPlugin("alpha")).toBeUndefined(); + expect(bundled.getBundledChannelSetupPlugin("alpha")).toBeUndefined(); + expect(bundled.getBundledChannelSecrets("alpha")).toBeUndefined(); + expect(bundled.getBundledChannelSecrets("alpha")).toBeUndefined(); + expect(bundled.getBundledChannelSetupSecrets("alpha")).toBeUndefined(); + expect(bundled.getBundledChannelSetupSecrets("alpha")).toBeUndefined(); + expect(testGlobal.__bundledPluginFailureLoads).toBe(1); + expect(testGlobal.__bundledSetupFailureLoads).toBe(1); + expect(testGlobal.__bundledSecretsFailureLoads).toBe(1); + expect(testGlobal.__bundledSetupSecretsFailureLoads).toBe(1); + } finally { + restoreBundledPluginsDir(previousBundledPluginsDir); + fs.rmSync(root, { recursive: true, force: true }); + delete testGlobal.__bundledPluginFailureLoads; + delete testGlobal.__bundledSetupFailureLoads; + delete testGlobal.__bundledSecretsFailureLoads; + delete testGlobal.__bundledSetupSecretsFailureLoads; + } + }); + it("keeps channel entrypoints on the dedicated entry-contract SDK surface", () => { const offenders = collectBundledChannelEntrypointOffenders( bundledPluginRoots, diff --git a/src/channels/plugins/bundled.ts b/src/channels/plugins/bundled.ts index 24c60fa2f21..92f79dc7341 100644 --- a/src/channels/plugins/bundled.ts +++ b/src/channels/plugins/bundled.ts @@ -66,8 +66,8 @@ type BundledChannelCacheContext = { setupEntryLoadInProgressIds: Set; lazyEntriesById: Map; lazySetupEntriesById: Map; - lazyPluginsById: Map; - lazySetupPluginsById: Map; + lazyPluginsById: Map; + lazySetupPluginsById: Map; lazySecretsById: Map; lazySetupSecretsById: Map; lazyAccountInspectorsById: Map< @@ -461,9 +461,8 @@ function getBundledChannelPluginForRoot( rootScope: BundledChannelRootScope, cacheContext: BundledChannelCacheContext, ): ChannelPlugin | undefined { - const cached = cacheContext.lazyPluginsById.get(id); - if (cached) { - return cached; + if (cacheContext.lazyPluginsById.has(id)) { + return cacheContext.lazyPluginsById.get(id) ?? undefined; } if (cacheContext.pluginLoadInProgressIds.has(id)) { return undefined; @@ -486,6 +485,11 @@ function getBundledChannelPluginForRoot( }; cacheContext.lazyPluginsById.set(id, normalizedPlugin); return normalizedPlugin; + } catch (error) { + const detail = formatErrorMessage(error); + log.warn(`[channels] failed to load bundled channel ${id}: ${detail}`); + cacheContext.lazyPluginsById.set(id, null); + return undefined; } finally { cacheContext.pluginLoadInProgressIds.delete(id); } @@ -503,11 +507,18 @@ function getBundledChannelSecretsForRoot( if (!entry) { return undefined; } - const secrets = - entry.loadChannelSecrets?.() ?? - getBundledChannelPluginForRoot(id, rootScope, cacheContext)?.secrets; - cacheContext.lazySecretsById.set(id, secrets ?? null); - return secrets; + try { + const secrets = + entry.loadChannelSecrets?.() ?? + getBundledChannelPluginForRoot(id, rootScope, cacheContext)?.secrets; + cacheContext.lazySecretsById.set(id, secrets ?? null); + return secrets; + } catch (error) { + const detail = formatErrorMessage(error); + log.warn(`[channels] failed to load bundled channel secrets ${id}: ${detail}`); + cacheContext.lazySecretsById.set(id, null); + return undefined; + } } function getBundledChannelAccountInspectorForRoot( @@ -523,9 +534,16 @@ function getBundledChannelAccountInspectorForRoot( cacheContext.lazyAccountInspectorsById.set(id, null); return undefined; } - const inspector = entry.loadChannelAccountInspector(); - cacheContext.lazyAccountInspectorsById.set(id, inspector); - return inspector; + try { + const inspector = entry.loadChannelAccountInspector(); + cacheContext.lazyAccountInspectorsById.set(id, inspector); + return inspector; + } catch (error) { + const detail = formatErrorMessage(error); + log.warn(`[channels] failed to load bundled channel account inspector ${id}: ${detail}`); + cacheContext.lazyAccountInspectorsById.set(id, null); + return undefined; + } } function getBundledChannelSetupPluginForRoot( @@ -533,9 +551,8 @@ function getBundledChannelSetupPluginForRoot( rootScope: BundledChannelRootScope, cacheContext: BundledChannelCacheContext, ): ChannelPlugin | undefined { - const cached = cacheContext.lazySetupPluginsById.get(id); - if (cached) { - return cached; + if (cacheContext.lazySetupPluginsById.has(id)) { + return cacheContext.lazySetupPluginsById.get(id) ?? undefined; } if (cacheContext.setupPluginLoadInProgressIds.has(id)) { return undefined; @@ -549,6 +566,11 @@ function getBundledChannelSetupPluginForRoot( const plugin = entry.loadSetupPlugin(); cacheContext.lazySetupPluginsById.set(id, plugin); return plugin; + } catch (error) { + const detail = formatErrorMessage(error); + log.warn(`[channels] failed to load bundled channel setup ${id}: ${detail}`); + cacheContext.lazySetupPluginsById.set(id, null); + return undefined; } finally { cacheContext.setupPluginLoadInProgressIds.delete(id); } @@ -566,11 +588,18 @@ function getBundledChannelSetupSecretsForRoot( if (!entry) { return undefined; } - const secrets = - entry.loadSetupSecrets?.() ?? - getBundledChannelSetupPluginForRoot(id, rootScope, cacheContext)?.secrets; - cacheContext.lazySetupSecretsById.set(id, secrets ?? null); - return secrets; + try { + const secrets = + entry.loadSetupSecrets?.() ?? + getBundledChannelSetupPluginForRoot(id, rootScope, cacheContext)?.secrets; + cacheContext.lazySetupSecretsById.set(id, secrets ?? null); + return secrets; + } catch (error) { + const detail = formatErrorMessage(error); + log.warn(`[channels] failed to load bundled channel setup secrets ${id}: ${detail}`); + cacheContext.lazySetupSecretsById.set(id, null); + return undefined; + } } export function listBundledChannelPlugins(): readonly ChannelPlugin[] {