diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f2c3b78f2..42a8bb0f136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ Docs: https://docs.openclaw.ai - Security/audit: read channel exposure and plugin allowlist ownership from read-only plugin index metadata so cold audits do not depend on loaded channel runtime. Thanks @shakkernerd. - Plugins/chat: keep `/plugins list`, `/plugins enable`, and `/plugins disable` on the persisted plugin index path so chat plugin management does not load diagnostic/runtime plugin registries before execution. Thanks @shakkernerd. - Plugins/doctor: read workspace plugin status and legacy web-search ownership through installed-index manifest metadata instead of broad manifest registry scans. Thanks @shakkernerd. +- CLI/agents: read channel provider status from read-only plugin index metadata for text `agents list` output instead of the loaded channel registry. Thanks @shakkernerd. - Logging: redact configured secret patterns at console and file-log sink exits so credentials that reach the logger are masked before terminal display or JSONL persistence. Fixes #67953. Thanks @Ziy1-Tan. diff --git a/src/commands/agents.commands.list.ts b/src/commands/agents.commands.list.ts index 41b8de474cb..edef5576f23 100644 --- a/src/commands/agents.commands.list.ts +++ b/src/commands/agents.commands.list.ts @@ -99,13 +99,12 @@ export async function agentsListCommand( } } - // `buildProviderStatusIndex` triggers on-demand plugin loads and is only - // used for human text output (`summary.providers` is rendered in the text - // formatter). JSON callers (dashboards, monitors, IDE plugins) poll the - // config-derived fields, so skip the plugin load unless they explicitly ask - // for binding/provider enrichment with --bindings. Combined with - // `loadPlugins: "text-only"` in the catalog entry, this keeps - // `agents list --json` on the config-only path. (#71739) + // Provider details are only used for human text output + // (`summary.providers` is rendered in the text formatter). JSON callers + // (dashboards, monitors, IDE plugins) poll the config-derived fields, so skip + // the provider detail pass unless they explicitly ask for binding/provider + // enrichment with --bindings. Combined with `loadPlugins: "text-only"` in the + // catalog entry, this keeps `agents list --json` on the config-only path. const includeProviderDetails = !opts.json || opts.bindings === true; const providerStatus = includeProviderDetails ? await buildProviderStatusIndex(cfg) : null; diff --git a/src/commands/agents.providers.test.ts b/src/commands/agents.providers.test.ts index 1e170d703de..a0ad613a791 100644 --- a/src/commands/agents.providers.test.ts +++ b/src/commands/agents.providers.test.ts @@ -3,7 +3,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { buildProviderStatusIndex } from "./agents.providers.js"; const mocks = vi.hoisted(() => ({ - listChannelPlugins: vi.fn(), + listReadOnlyChannelPluginsForConfig: vi.fn(), getChannelPlugin: vi.fn(), normalizeChannelId: vi.fn((value: unknown) => typeof value === "string" && value.trim().length > 0 ? value : null, @@ -13,14 +13,18 @@ const mocks = vi.hoisted(() => ({ })); vi.mock("../channels/plugins/index.js", () => ({ - listChannelPlugins: (...args: Parameters) => - mocks.listChannelPlugins(...args), getChannelPlugin: (...args: Parameters) => mocks.getChannelPlugin(...args), normalizeChannelId: (...args: Parameters) => mocks.normalizeChannelId(...args), })); +vi.mock("../channels/plugins/read-only.js", () => ({ + listReadOnlyChannelPluginsForConfig: ( + ...args: Parameters + ) => mocks.listReadOnlyChannelPluginsForConfig(...args), +})); + vi.mock("../channels/plugins/helpers.js", () => ({ resolveChannelDefaultAccountId: ( ...args: Parameters @@ -55,11 +59,15 @@ describe("buildProviderStatusIndex", () => { status: {}, } as never; - mocks.listChannelPlugins.mockReturnValue([plugin]); + mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([plugin]); mocks.getChannelPlugin.mockReturnValue(plugin); const map = await buildProviderStatusIndex({} as OpenClawConfig); + expect(mocks.listReadOnlyChannelPluginsForConfig).toHaveBeenCalledWith( + {}, + { includeSetupRuntimeFallback: false }, + ); expect(resolveAccount).not.toHaveBeenCalled(); expect(inspectAccount).toHaveBeenCalledWith({}, "work"); expect(map.get("workchat:work")).toMatchObject({ @@ -85,7 +93,7 @@ describe("buildProviderStatusIndex", () => { status: {}, } as never; - mocks.listChannelPlugins.mockReturnValue([plugin]); + mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([plugin]); mocks.getChannelPlugin.mockReturnValue(plugin); await expect(buildProviderStatusIndex({} as OpenClawConfig)).resolves.toEqual( @@ -116,7 +124,7 @@ describe("buildProviderStatusIndex", () => { status: {}, } as never; - mocks.listChannelPlugins.mockReturnValue([plugin]); + mocks.listReadOnlyChannelPluginsForConfig.mockReturnValue([plugin]); mocks.getChannelPlugin.mockReturnValue(plugin); await expect(buildProviderStatusIndex({} as OpenClawConfig)).rejects.toThrow("plugin crash"); diff --git a/src/commands/agents.providers.ts b/src/commands/agents.providers.ts index f2aa438ea48..b0a9bd28cec 100644 --- a/src/commands/agents.providers.ts +++ b/src/commands/agents.providers.ts @@ -1,10 +1,8 @@ import { isChannelVisibleInConfiguredLists } from "../channels/plugins/exposure.js"; import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js"; -import { - getChannelPlugin, - listChannelPlugins, - normalizeChannelId, -} from "../channels/plugins/index.js"; +import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js"; +import { listReadOnlyChannelPluginsForConfig } from "../channels/plugins/read-only.js"; +import type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; import type { ChannelId } from "../channels/plugins/types.public.js"; import type { AgentBinding } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -12,11 +10,13 @@ import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; type ProviderAccountStatus = { provider: ChannelId; + providerLabel?: string; accountId: string; name?: string; state: "linked" | "not linked" | "configured" | "not configured" | "enabled" | "disabled"; enabled?: boolean; configured?: boolean; + visibleInConfiguredLists?: boolean; }; function providerAccountKey(provider: ChannelId, accountId?: string) { @@ -33,10 +33,12 @@ function isUnresolvedSecretRefResolutionError(error: unknown): boolean { function formatChannelAccountLabel(params: { provider: ChannelId; + providerLabel?: string; accountId: string; name?: string; }): string { - const label = getChannelPlugin(params.provider)?.meta.label ?? params.provider; + const label = + params.providerLabel ?? getChannelPlugin(params.provider)?.meta.label ?? params.provider; const account = params.name?.trim() ? `${params.accountId} (${params.name.trim()})` : params.accountId; @@ -52,7 +54,7 @@ function formatProviderState(entry: ProviderAccountStatus): string { } async function resolveReadOnlyAccount(params: { - plugin: ReturnType[number]; + plugin: ChannelPlugin; cfg: OpenClawConfig; accountId: string; }): Promise { @@ -67,7 +69,9 @@ export async function buildProviderStatusIndex( ): Promise> { const map = new Map(); - for (const plugin of listChannelPlugins()) { + for (const plugin of listReadOnlyChannelPluginsForConfig(cfg, { + includeSetupRuntimeFallback: false, + })) { const accountIds = plugin.config.listAccountIds(cfg); for (const accountId of accountIds) { let account: unknown; @@ -116,11 +120,13 @@ export async function buildProviderStatusIndex( const name = snapshot?.name ?? (account as { name?: string }).name; map.set(providerAccountKey(plugin.id, accountId), { provider: plugin.id, + providerLabel: plugin.meta.label, accountId, name, state, enabled, configured, + visibleInConfiguredLists: isChannelVisibleInConfiguredLists(plugin.meta), }); } } @@ -137,6 +143,13 @@ function resolveDefaultAccountId(cfg: OpenClawConfig, provider: ChannelId): stri } function shouldShowProviderEntry(entry: ProviderAccountStatus, cfg: OpenClawConfig): boolean { + if (entry.visibleInConfiguredLists !== undefined) { + if (!entry.visibleInConfiguredLists) { + const providerConfig = (cfg as Record)[entry.provider]; + return Boolean(entry.configured) || Boolean(providerConfig); + } + return Boolean(entry.configured); + } const plugin = getChannelPlugin(entry.provider); if (!plugin) { return Boolean(entry.configured); @@ -151,6 +164,7 @@ function shouldShowProviderEntry(entry: ProviderAccountStatus, cfg: OpenClawConf function formatProviderEntry(entry: ProviderAccountStatus): string { const label = formatChannelAccountLabel({ provider: entry.provider, + providerLabel: entry.providerLabel, accountId: entry.accountId, name: entry.name, });