fix: keep agent provider status on plugin index

This commit is contained in:
Shakker
2026-04-26 06:42:01 +01:00
parent bf2c992a86
commit d5eae0d959
4 changed files with 43 additions and 21 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -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<typeof mocks.listChannelPlugins>) =>
mocks.listChannelPlugins(...args),
getChannelPlugin: (...args: Parameters<typeof mocks.getChannelPlugin>) =>
mocks.getChannelPlugin(...args),
normalizeChannelId: (...args: Parameters<typeof mocks.normalizeChannelId>) =>
mocks.normalizeChannelId(...args),
}));
vi.mock("../channels/plugins/read-only.js", () => ({
listReadOnlyChannelPluginsForConfig: (
...args: Parameters<typeof mocks.listReadOnlyChannelPluginsForConfig>
) => mocks.listReadOnlyChannelPluginsForConfig(...args),
}));
vi.mock("../channels/plugins/helpers.js", () => ({
resolveChannelDefaultAccountId: (
...args: Parameters<typeof mocks.resolveChannelDefaultAccountId>
@@ -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");

View File

@@ -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<typeof listChannelPlugins>[number];
plugin: ChannelPlugin;
cfg: OpenClawConfig;
accountId: string;
}): Promise<unknown> {
@@ -67,7 +69,9 @@ export async function buildProviderStatusIndex(
): Promise<Map<string, ProviderAccountStatus>> {
const map = new Map<string, ProviderAccountStatus>();
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<string, unknown>)[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,
});