diff --git a/src/channels/plugins/read-only.ts b/src/channels/plugins/read-only.ts index a53914023dc..338a63f9e06 100644 --- a/src/channels/plugins/read-only.ts +++ b/src/channels/plugins/read-only.ts @@ -1,7 +1,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { - listConfiguredChannelIdsForPluginScope, + listConfiguredChannelIdsForReadOnlyScope, resolveDiscoverableScopedChannelPluginIds, } from "../../plugins/channel-plugin-ids.js"; import { loadOpenClawPlugins } from "../../plugins/loader.js"; @@ -141,7 +141,7 @@ export function listReadOnlyChannelPluginsForConfig( }); const configuredChannelIds = [ ...new Set( - listConfiguredChannelIdsForPluginScope({ + listConfiguredChannelIdsForReadOnlyScope({ config: cfg, workspaceDir, env, diff --git a/src/commands/channels.status.command-flow.test.ts b/src/commands/channels.status.command-flow.test.ts index 6004a57713b..82e165930ad 100644 --- a/src/commands/channels.status.command-flow.test.ts +++ b/src/commands/channels.status.command-flow.test.ts @@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({ readConfigFileSnapshot: vi.fn(async () => ({ path: "/tmp/openclaw.json" })), requireValidConfigSnapshot: vi.fn(), listChannelPlugins: vi.fn(), + listConfiguredChannelIdsForReadOnlyScope: vi.fn((_params: unknown) => ["discord"]), withProgress: vi.fn(async (_opts: unknown, run: () => Promise) => await run()), })); @@ -33,8 +34,9 @@ vi.mock("../config/config.js", () => ({ readConfigFileSnapshot: () => mocks.readConfigFileSnapshot(), })); -vi.mock("../channels/config-presence.js", () => ({ - listPotentialConfiguredChannelIds: () => ["discord"], +vi.mock("../plugins/channel-plugin-ids.js", () => ({ + listConfiguredChannelIdsForReadOnlyScope: (params: unknown) => + mocks.listConfiguredChannelIdsForReadOnlyScope(params), })); vi.mock("./channels/shared.js", () => ({ @@ -192,6 +194,8 @@ describe("channelsStatusCommand SecretRef fallback flow", () => { mocks.readConfigFileSnapshot.mockClear(); mocks.requireValidConfigSnapshot.mockReset(); mocks.listChannelPlugins.mockReset(); + mocks.listConfiguredChannelIdsForReadOnlyScope.mockClear(); + mocks.listConfiguredChannelIdsForReadOnlyScope.mockReturnValue(["discord"]); mocks.withProgress.mockClear(); mocks.listChannelPlugins.mockReturnValue([createTokenOnlyPlugin()]); }); @@ -259,6 +263,12 @@ describe("channelsStatusCommand SecretRef fallback flow", () => { await channelsStatusCommand({ json: true, probe: false }, runtime as never); expect(mocks.listChannelPlugins).not.toHaveBeenCalled(); + expect(mocks.listConfiguredChannelIdsForReadOnlyScope).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ secretResolved: true }), + includePersistedAuthState: false, + }), + ); const payload = JSON.parse(logs.at(-1) ?? "{}"); expect(payload).toEqual( expect.objectContaining({ diff --git a/src/commands/channels.status.external-env.test.ts b/src/commands/channels.status.external-env.test.ts new file mode 100644 index 00000000000..94b97386bc6 --- /dev/null +++ b/src/commands/channels.status.external-env.test.ts @@ -0,0 +1,150 @@ +import fs from "node:fs"; +import path from "node:path"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { + cleanupPluginLoaderFixturesForTest, + EMPTY_PLUGIN_SCHEMA, + makeTempDir, + resetPluginLoaderTestStateForTest, + useNoBundledPlugins, +} from "../plugins/loader.test-fixtures.js"; +import { withEnvAsync } from "../test-utils/env.js"; +import { channelsStatusCommand } from "./channels/status.js"; + +const mocks = vi.hoisted(() => ({ + callGateway: vi.fn(), + readConfigFileSnapshot: vi.fn(async () => ({ path: "/tmp/openclaw.json" })), + requireValidConfigSnapshot: vi.fn(), + resolveCommandConfigWithSecrets: vi.fn(), +})); + +vi.mock("../gateway/call.js", () => ({ + callGateway: (opts: unknown) => mocks.callGateway(opts), +})); + +vi.mock("../config/config.js", () => ({ + readConfigFileSnapshot: () => mocks.readConfigFileSnapshot(), +})); + +vi.mock("../cli/command-config-resolution.js", () => ({ + resolveCommandConfigWithSecrets: (opts: unknown) => mocks.resolveCommandConfigWithSecrets(opts), +})); + +vi.mock("./channels/shared.js", () => ({ + requireValidConfigSnapshot: (runtime: unknown) => mocks.requireValidConfigSnapshot(runtime), + formatChannelAccountLabel: ({ channel, accountId }: { channel: string; accountId: string }) => + `${channel} ${accountId}`, + appendBaseUrlBit: () => undefined, + appendEnabledConfiguredLinkedBits: () => undefined, + appendModeBit: () => undefined, + appendTokenSourceBits: () => undefined, + buildChannelAccountLine: () => "", +})); + +vi.mock("../cli/progress.js", () => ({ + withProgress: async (_opts: unknown, run: () => Promise) => await run(), +})); + +function writeExternalEnvChannelPlugin() { + useNoBundledPlugins(); + const pluginDir = makeTempDir(); + const fullMarker = path.join(pluginDir, "full-loaded.txt"); + fs.writeFileSync( + path.join(pluginDir, "package.json"), + JSON.stringify( + { + name: "@example/openclaw-external-env-channel", + version: "1.0.0", + openclaw: { + extensions: ["./index.cjs"], + }, + }, + null, + 2, + ), + "utf-8", + ); + fs.writeFileSync( + path.join(pluginDir, "openclaw.plugin.json"), + JSON.stringify( + { + id: "external-env-channel-plugin", + configSchema: EMPTY_PLUGIN_SCHEMA, + channels: ["external-env-channel"], + channelEnvVars: { + "external-env-channel": ["EXTERNAL_ENV_CHANNEL_TOKEN"], + }, + }, + null, + 2, + ), + "utf-8", + ); + fs.writeFileSync( + path.join(pluginDir, "index.cjs"), + `require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");`, + "utf-8", + ); + return { pluginDir, fullMarker }; +} + +function createRuntimeCapture() { + const logs: string[] = []; + const errors: string[] = []; + const runtime = { + log: (message: unknown) => logs.push(String(message)), + error: (message: unknown) => errors.push(String(message)), + exit: (_code?: number) => undefined, + }; + return { runtime, logs, errors }; +} + +describe("channelsStatusCommand external env-only channel fallback", () => { + beforeEach(() => { + mocks.callGateway.mockReset(); + mocks.callGateway.mockRejectedValue(new Error("gateway closed")); + mocks.readConfigFileSnapshot.mockClear(); + mocks.requireValidConfigSnapshot.mockReset(); + mocks.resolveCommandConfigWithSecrets.mockReset(); + }); + + afterEach(() => { + resetPluginLoaderTestStateForTest(); + }); + + it("reports env-only external manifest channels in JSON fallback without full runtime load", async () => { + const { pluginDir, fullMarker } = writeExternalEnvChannelPlugin(); + const config = { + plugins: { + load: { paths: [pluginDir] }, + allow: ["external-env-channel-plugin"], + }, + } as OpenClawConfig; + mocks.requireValidConfigSnapshot.mockResolvedValue(config); + mocks.resolveCommandConfigWithSecrets.mockResolvedValue({ + resolvedConfig: config, + effectiveConfig: config, + diagnostics: [], + }); + const { runtime, logs } = createRuntimeCapture(); + + await withEnvAsync({ EXTERNAL_ENV_CHANNEL_TOKEN: "token" }, async () => { + await channelsStatusCommand({ json: true, probe: false }, runtime as never); + }); + + expect(fs.existsSync(fullMarker)).toBe(false); + const payload = JSON.parse(logs.at(-1) ?? "{}"); + expect(payload).toEqual( + expect.objectContaining({ + gatewayReachable: false, + configOnly: true, + configuredChannels: ["external-env-channel"], + }), + ); + }); +}); + +afterAll(() => { + cleanupPluginLoaderFixturesForTest(); +}); diff --git a/src/commands/channels/status.ts b/src/commands/channels/status.ts index 828cac14c6a..52c394e160f 100644 --- a/src/commands/channels/status.ts +++ b/src/commands/channels/status.ts @@ -1,4 +1,3 @@ -import { listPotentialConfiguredChannelIds } from "../../channels/config-presence.js"; import { resolveCommandConfigWithSecrets } from "../../cli/command-config-resolution.js"; import { formatCliCommand } from "../../cli/command-format.js"; import { getConfiguredChannelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js"; @@ -7,6 +6,7 @@ import { readConfigFileSnapshot } from "../../config/config.js"; import { callGateway } from "../../gateway/call.js"; import { collectChannelStatusIssues } from "../../infra/channels-status-issues.js"; import { formatTimeAgo } from "../../infra/format-time/format-relative.ts"; +import { listConfiguredChannelIdsForReadOnlyScope } from "../../plugins/channel-plugin-ids.js"; import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js"; import { formatDocsLink } from "../../terminal/links.js"; import { theme } from "../../terminal/theme.js"; @@ -196,9 +196,11 @@ export async function channelsStatusCommand( path: snapshot.path, mode, }, - configuredChannels: listPotentialConfiguredChannelIds(resolvedConfig, process.env, { + configuredChannels: listConfiguredChannelIdsForReadOnlyScope({ + config: resolvedConfig, + env: process.env, includePersistedAuthState: false, - }).toSorted(), + }), }); return; } diff --git a/src/commands/status.scan-overview.ts b/src/commands/status.scan-overview.ts index 2cbc075828b..e2ba85e6c37 100644 --- a/src/commands/status.scan-overview.ts +++ b/src/commands/status.scan-overview.ts @@ -1,8 +1,8 @@ -import { hasPotentialConfiguredChannels } from "../channels/config-presence.js"; import type { OpenClawConfig } from "../config/types.js"; import type { collectChannelStatusIssues as collectChannelStatusIssuesFn } from "../infra/channels-status-issues.js"; import { resolveOsSummary } from "../infra/os-summary.js"; import type { UpdateCheckResult } from "../infra/update-check.js"; +import { hasConfiguredChannelsForReadOnlyScope } from "../plugins/channel-plugin-ids.js"; import type { RuntimeEnv } from "../runtime.js"; import type { buildChannelsTable as buildChannelsTableFn } from "./status-all/channels.js"; import type { getAgentLocalStatuses as getAgentLocalStatusesFn } from "./status.agent-local.js"; @@ -178,7 +178,7 @@ export async function collectStatusScanOverview(params: { params.progress?.tick(); const hasConfiguredChannels = params.resolveHasConfiguredChannels ? params.resolveHasConfiguredChannels(cfg) - : hasPotentialConfiguredChannels(cfg); + : hasConfiguredChannelsForReadOnlyScope({ config: cfg }); const osSummary = resolveOsSummary(); const bootstrap = await createStatusScanCoreBootstrap< Awaited> diff --git a/src/commands/status.scan.fast-json.ts b/src/commands/status.scan.fast-json.ts index 13053e4c431..edd58db00e7 100644 --- a/src/commands/status.scan.fast-json.ts +++ b/src/commands/status.scan.fast-json.ts @@ -1,4 +1,5 @@ -import { hasPotentialConfiguredChannels } from "../channels/config-presence.js"; +import type { OpenClawConfig } from "../config/types.js"; +import { hasConfiguredChannelsForReadOnlyScope } from "../plugins/channel-plugin-ids.js"; import type { RuntimeEnv } from "../runtime.js"; import { executeStatusScanFromOverview } from "./status.scan-execute.ts"; import { @@ -12,9 +13,7 @@ type StatusJsonScanPolicy = { commandName: string; allowMissingConfigFastPath?: boolean; includeChannelSummary?: boolean; - resolveHasConfiguredChannels: ( - cfg: Parameters[0], - ) => boolean; + resolveHasConfiguredChannels: (cfg: OpenClawConfig) => boolean; resolveMemory: Parameters[0]["resolveMemory"]; }; @@ -60,7 +59,9 @@ export async function scanStatusJsonFast( allowMissingConfigFastPath: true, includeChannelSummary: false, resolveHasConfiguredChannels: (cfg) => - hasPotentialConfiguredChannels(cfg, process.env, { + hasConfiguredChannelsForReadOnlyScope({ + config: cfg, + env: process.env, includePersistedAuthState: false, }), resolveMemory: async ({ cfg, agentStatus, memoryPlugin }) => diff --git a/src/commands/status.scan.test-helpers.ts b/src/commands/status.scan.test-helpers.ts index eb7e1327168..a8d1755d89d 100644 --- a/src/commands/status.scan.test-helpers.ts +++ b/src/commands/status.scan.test-helpers.ts @@ -182,6 +182,36 @@ export async function loadStatusScanModuleForTest( vi.doMock("../channels/config-presence.js", () => ({ hasPotentialConfiguredChannels: mocks.hasPotentialConfiguredChannels, })); + vi.doMock("../plugins/channel-plugin-ids.js", () => ({ + hasConfiguredChannelsForReadOnlyScope: (params: { + config: OpenClawConfig; + env?: NodeJS.ProcessEnv; + includePersistedAuthState?: boolean; + }) => + Boolean( + mocks.hasPotentialConfiguredChannels( + params.config, + params.env, + params.includePersistedAuthState === undefined + ? undefined + : { includePersistedAuthState: params.includePersistedAuthState }, + ), + ), + listConfiguredChannelIdsForReadOnlyScope: (params: { + config: OpenClawConfig; + env?: NodeJS.ProcessEnv; + includePersistedAuthState?: boolean; + }) => + mocks.hasPotentialConfiguredChannels( + params.config, + params.env, + params.includePersistedAuthState === undefined + ? undefined + : { includePersistedAuthState: params.includePersistedAuthState }, + ) + ? ["mock-channel"] + : [], + })); vi.doMock("../config/io.js", () => ({ readBestEffortConfig: mocks.readBestEffortConfig, diff --git a/src/commands/status.scan.ts b/src/commands/status.scan.ts index a58164ca4e5..cce6c103f6c 100644 --- a/src/commands/status.scan.ts +++ b/src/commands/status.scan.ts @@ -1,5 +1,5 @@ -import { hasPotentialConfiguredChannels } from "../channels/config-presence.js"; import { withProgress } from "../cli/progress.js"; +import { hasConfiguredChannelsForReadOnlyScope } from "../plugins/channel-plugin-ids.js"; import { buildPluginCompatibilitySnapshotNotices } from "../plugins/status.js"; import type { RuntimeEnv } from "../runtime.js"; import { executeStatusScanFromOverview } from "./status.scan-execute.ts"; @@ -25,7 +25,8 @@ export async function scanStatus( _runtime, { commandName: "status --json", - resolveHasConfiguredChannels: (cfg) => hasPotentialConfiguredChannels(cfg), + resolveHasConfiguredChannels: (cfg) => + hasConfiguredChannelsForReadOnlyScope({ config: cfg }), resolveMemory: async ({ cfg, agentStatus, memoryPlugin }) => await resolveStatusMemoryStatusSnapshot({ cfg, diff --git a/src/commands/status.summary.test.ts b/src/commands/status.summary.test.ts index e5e3348096b..4eab0d1b3e9 100644 --- a/src/commands/status.summary.test.ts +++ b/src/commands/status.summary.test.ts @@ -1,12 +1,12 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const statusSummaryMocks = vi.hoisted(() => ({ - hasPotentialConfiguredChannels: vi.fn(() => true), + hasConfiguredChannelsForReadOnlyScope: vi.fn(() => true), buildChannelSummary: vi.fn(async () => ["ok"]), })); -vi.mock("../channels/config-presence.js", () => ({ - hasPotentialConfiguredChannels: statusSummaryMocks.hasPotentialConfiguredChannels, +vi.mock("../plugins/channel-plugin-ids.js", () => ({ + hasConfiguredChannelsForReadOnlyScope: statusSummaryMocks.hasConfiguredChannelsForReadOnlyScope, })); vi.mock("./status.summary.runtime.js", () => ({ @@ -125,7 +125,7 @@ describe("getStatusSummary", () => { beforeEach(() => { vi.clearAllMocks(); - statusSummaryMocks.hasPotentialConfiguredChannels.mockReturnValue(true); + statusSummaryMocks.hasConfiguredChannelsForReadOnlyScope.mockReturnValue(true); statusSummaryMocks.buildChannelSummary.mockResolvedValue(["ok"]); }); @@ -140,12 +140,15 @@ describe("getStatusSummary", () => { }); it("skips channel summary imports when no channels are configured", async () => { - statusSummaryMocks.hasPotentialConfiguredChannels.mockReturnValue(false); + statusSummaryMocks.hasConfiguredChannelsForReadOnlyScope.mockReturnValue(false); const summary = await getStatusSummary(); expect(summary.channelSummary).toEqual([]); expect(summary.linkChannel).toBeUndefined(); + expect(statusSummaryMocks.hasConfiguredChannelsForReadOnlyScope).toHaveBeenCalledWith({ + config: {}, + }); expect(buildChannelSummary).not.toHaveBeenCalled(); expect(resolveLinkChannelContext).not.toHaveBeenCalled(); }); @@ -155,7 +158,7 @@ describe("getStatusSummary", () => { expect(summary.channelSummary).toEqual([]); expect(summary.linkChannel).toBeUndefined(); - expect(statusSummaryMocks.hasPotentialConfiguredChannels).not.toHaveBeenCalled(); + expect(statusSummaryMocks.hasConfiguredChannelsForReadOnlyScope).not.toHaveBeenCalled(); expect(buildChannelSummary).not.toHaveBeenCalled(); expect(resolveLinkChannelContext).not.toHaveBeenCalled(); }); diff --git a/src/commands/status.summary.ts b/src/commands/status.summary.ts index 7ba77ea14c2..f808cefbb6b 100644 --- a/src/commands/status.summary.ts +++ b/src/commands/status.summary.ts @@ -1,5 +1,4 @@ import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; -import { hasPotentialConfiguredChannels } from "../channels/config-presence.js"; import { resolveMainSessionKey } from "../config/sessions/main-session.js"; import { resolveStorePath } from "../config/sessions/paths.js"; import { readSessionStoreReadOnly } from "../config/sessions/store-read.js"; @@ -8,6 +7,7 @@ import type { OpenClawConfig } from "../config/types.js"; import { listGatewayAgentsBasic } from "../gateway/agent-list.js"; import { resolveHeartbeatSummaryForAgent } from "../infra/heartbeat-summary.js"; import { peekSystemEvents } from "../infra/system-events.js"; +import { hasConfiguredChannelsForReadOnlyScope } from "../plugins/channel-plugin-ids.js"; import { parseAgentSessionKey } from "../routing/session-key.js"; import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js"; import { resolveRuntimeServiceVersion } from "../version.js"; @@ -118,7 +118,11 @@ export async function getStatusSummary( resolveSessionModelRef, } = await loadStatusSummaryRuntimeModule(); const cfg = options.config ?? (await loadConfigIoModule()).loadConfig(); - const needsChannelPlugins = includeChannelSummary && hasPotentialConfiguredChannels(cfg); + const needsChannelPlugins = + includeChannelSummary && + hasConfiguredChannelsForReadOnlyScope({ + config: cfg, + }); const linkContext = needsChannelPlugins ? await loadLinkChannelModule().then(({ resolveLinkChannelContext }) => resolveLinkChannelContext(cfg), diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index 14f827662a4..3166cdb3189 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -19,6 +19,8 @@ vi.mock("./manifest-registry.js", async (importOriginal) => { }); import { + hasConfiguredChannelsForReadOnlyScope, + listConfiguredChannelIdsForReadOnlyScope, resolveConfiguredChannelPluginIds, resolveGatewayStartupPluginIds, } from "./channel-plugin-ids.js"; @@ -634,3 +636,48 @@ describe("resolveConfiguredChannelPluginIds", () => { ).toEqual([]); }); }); + +describe("listConfiguredChannelIdsForReadOnlyScope", () => { + beforeEach(() => { + listPotentialConfiguredChannelIds.mockReset().mockReturnValue([]); + hasPotentialConfiguredChannels.mockReset().mockReturnValue(false); + loadPluginManifestRegistry.mockReset().mockReturnValue(createManifestRegistryFixture()); + }); + + it("uses manifest env vars as read-only configured channel triggers", () => { + expect( + listConfiguredChannelIdsForReadOnlyScope({ + config: { + plugins: { + allow: ["external-env-channel-plugin"], + }, + } as OpenClawConfig, + workspaceDir: "/tmp", + env: { + EXTERNAL_ENV_CHANNEL_TOKEN: "token", + } as NodeJS.ProcessEnv, + includePersistedAuthState: false, + }), + ).toEqual(["external-env-channel"]); + }); + + it("uses manifest env vars for read-only channel presence checks", () => { + listPotentialConfiguredChannelIds.mockReturnValue([]); + hasPotentialConfiguredChannels.mockReturnValue(false); + + expect( + hasConfiguredChannelsForReadOnlyScope({ + config: { + plugins: { + allow: ["external-env-channel-plugin"], + }, + } as OpenClawConfig, + workspaceDir: "/tmp", + env: { + EXTERNAL_ENV_CHANNEL_TOKEN: "token", + } as NodeJS.ProcessEnv, + includePersistedAuthState: false, + }), + ).toBe(true); + }); +}); diff --git a/src/plugins/channel-plugin-ids.ts b/src/plugins/channel-plugin-ids.ts index 7eab9d4ebc3..7edbfa1dec2 100644 --- a/src/plugins/channel-plugin-ids.ts +++ b/src/plugins/channel-plugin-ids.ts @@ -1,5 +1,9 @@ +import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js"; -import { listPotentialConfiguredChannelIds } from "../channels/config-presence.js"; +import { + hasPotentialConfiguredChannels, + listPotentialConfiguredChannelIds, +} from "../channels/config-presence.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveMemoryDreamingConfig, @@ -111,6 +115,52 @@ export function listConfiguredChannelIdsForPluginScope(params: { ].toSorted((left, right) => left.localeCompare(right)); } +export function listConfiguredChannelIdsForReadOnlyScope(params: { + config: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + cache?: boolean; + includePersistedAuthState?: boolean; + manifestRecords?: readonly PluginManifestRecord[]; +}): string[] { + const env = params.env ?? process.env; + const workspaceDir = + params.workspaceDir ?? + resolveAgentWorkspaceDir(params.config, resolveDefaultAgentId(params.config)); + return listConfiguredChannelIdsForPluginScope({ + config: params.config, + workspaceDir, + env, + cache: params.cache, + includePersistedAuthState: params.includePersistedAuthState, + manifestRecords: params.manifestRecords, + }); +} + +export function hasConfiguredChannelsForReadOnlyScope(params: { + config: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + cache?: boolean; + includePersistedAuthState?: boolean; + manifestRecords?: readonly PluginManifestRecord[]; +}): boolean { + const env = params.env ?? process.env; + if ( + hasPotentialConfiguredChannels(params.config, env, { + includePersistedAuthState: params.includePersistedAuthState, + }) + ) { + return true; + } + return ( + listConfiguredChannelIdsForReadOnlyScope({ + ...params, + env, + }).length > 0 + ); +} + function isChannelPluginEligibleForScopedOwnership(params: { plugin: PluginManifestRecord; normalizedConfig: ReturnType;