mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
fix: keep agent provider status on plugin index
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user