mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix: keep status channel metadata cold
This commit is contained in:
@@ -92,6 +92,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/plugins: stop persisted WhatsApp auth state from activating bundled channel runtime-dependency repair during startup when `channels.whatsapp` is absent, avoiding npm/git stalls on packaged Linux installs. Fixes #71994. Thanks @xiao398008.
|
||||
- Gateway/device tokens: enforce caller-scope containment inside token rotation and revocation so pairing-only sessions cannot mutate higher-scope operator tokens. Fixes #71990. Thanks @coygeek.
|
||||
- Plugins/channels: keep security checks, thread-binding placement, provider summaries, health formatting, and message action labels on read-only or already-loaded channel metadata instead of importing full channel runtime. Thanks @shakkernerd.
|
||||
- Plugins/status: keep config-only channel labels and status security summaries from importing plugin runtime modules just to render metadata. Thanks @shakkernerd.
|
||||
- Sessions/channels: stop group-session metadata from loading bundled channel runtime just to classify `#channel` subjects, using only already-loaded channel capabilities on that path. Thanks @shakkernerd.
|
||||
- Plugins/channels: keep native command and native skill `auto` defaults on static channel metadata so config, audit, and command-list checks do not load channel runtime just to read those defaults. Thanks @shakkernerd.
|
||||
- CLI/channels: keep channel remove selection and all-channel capabilities summaries on read-only plugin metadata, loading channel runtime only for the selected mutation path. Thanks @shakkernerd.
|
||||
|
||||
@@ -15,7 +15,8 @@ import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
||||
import { applyAgentBindings, describeBinding } from "../agents.bindings.js";
|
||||
import type { ChannelChoice } from "../onboard-types.js";
|
||||
import { applyAccountName, applyChannelAccountConfig } from "./add-mutators.js";
|
||||
import { channelLabel, requireValidConfigFileSnapshot, shouldUseWizard } from "./shared.js";
|
||||
import { channelLabel } from "./runtime-label.js";
|
||||
import { requireValidConfigFileSnapshot, shouldUseWizard } from "./shared.js";
|
||||
|
||||
type ChannelSetupPluginInstallModule = typeof import("../channel-setup/plugin-install.js");
|
||||
type OnboardChannelsModule = typeof import("../onboard-channels.js");
|
||||
|
||||
@@ -317,6 +317,7 @@ export async function channelsCapabilitiesCommand(
|
||||
channel: report.channel,
|
||||
accountId: report.accountId,
|
||||
name: report.accountName,
|
||||
channelLabel: report.plugin.meta.label ?? report.channel,
|
||||
channelStyle: theme.accent,
|
||||
accountStyle: theme.heading,
|
||||
});
|
||||
|
||||
@@ -61,6 +61,7 @@ function formatAccountLine(params: {
|
||||
channel: channel.id,
|
||||
accountId: snapshot.accountId,
|
||||
name: snapshot.name,
|
||||
channelLabel: channel.meta.label ?? channel.id,
|
||||
channelStyle: theme.accent,
|
||||
accountStyle: theme.heading,
|
||||
});
|
||||
|
||||
@@ -9,12 +9,8 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-ke
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { createClackPrompter } from "../../wizard/clack-prompter.js";
|
||||
import {
|
||||
type ChatChannel,
|
||||
channelLabel,
|
||||
requireValidConfigFileSnapshot,
|
||||
shouldUseWizard,
|
||||
} from "./shared.js";
|
||||
import { channelLabel } from "./runtime-label.js";
|
||||
import { type ChatChannel, requireValidConfigFileSnapshot, shouldUseWizard } from "./shared.js";
|
||||
|
||||
export type ChannelsRemoveOptions = {
|
||||
channel?: string;
|
||||
|
||||
11
src/commands/channels/runtime-label.ts
Normal file
11
src/commands/channels/runtime-label.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { getBundledChannelSetupPlugin } from "../../channels/plugins/bundled.js";
|
||||
import { getChannelPlugin, getLoadedChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import type { ChatChannel } from "./shared.js";
|
||||
|
||||
export const channelLabel = (channel: ChatChannel) => {
|
||||
const plugin =
|
||||
getLoadedChannelPlugin(channel) ??
|
||||
getBundledChannelSetupPlugin(channel) ??
|
||||
getChannelPlugin(channel);
|
||||
return plugin?.meta.label ?? channel;
|
||||
};
|
||||
@@ -1,10 +1,5 @@
|
||||
import { hasConfiguredUnavailableCredentialStatus } from "../../channels/account-snapshot-fields.js";
|
||||
import { getBundledChannelSetupPlugin } from "../../channels/plugins/bundled.js";
|
||||
import {
|
||||
type ChannelId,
|
||||
getChannelPlugin,
|
||||
getLoadedChannelPlugin,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.public.js";
|
||||
import { resolveCommandConfigWithSecrets } from "../../cli/command-config-resolution.js";
|
||||
import type { CommandSecretResolutionMode } from "../../cli/command-secret-gateway.js";
|
||||
import { getChannelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
|
||||
@@ -50,22 +45,15 @@ export function formatAccountLabel(params: { accountId: string; name?: string })
|
||||
return base;
|
||||
}
|
||||
|
||||
export const channelLabel = (channel: ChatChannel) => {
|
||||
const plugin =
|
||||
getLoadedChannelPlugin(channel) ??
|
||||
getBundledChannelSetupPlugin(channel) ??
|
||||
getChannelPlugin(channel);
|
||||
return plugin?.meta.label ?? channel;
|
||||
};
|
||||
|
||||
export function formatChannelAccountLabel(params: {
|
||||
channel: ChatChannel;
|
||||
accountId: string;
|
||||
name?: string;
|
||||
channelLabel?: string;
|
||||
channelStyle?: (value: string) => string;
|
||||
accountStyle?: (value: string) => string;
|
||||
}): string {
|
||||
const channelText = channelLabel(params.channel);
|
||||
const channelText = params.channelLabel ?? params.channel;
|
||||
const accountText = formatAccountLabel({
|
||||
accountId: params.accountId,
|
||||
name: params.name,
|
||||
@@ -130,6 +118,7 @@ export function buildChannelAccountLine(
|
||||
provider: ChatChannel,
|
||||
account: Record<string, unknown>,
|
||||
bits: string[],
|
||||
opts?: { channelLabel?: string },
|
||||
): string {
|
||||
const accountId = typeof account.accountId === "string" ? account.accountId : DEFAULT_ACCOUNT_ID;
|
||||
const name = typeof account.name === "string" ? account.name : undefined;
|
||||
@@ -137,6 +126,7 @@ export function buildChannelAccountLine(
|
||||
channel: provider,
|
||||
accountId,
|
||||
name,
|
||||
channelLabel: opts?.channelLabel,
|
||||
});
|
||||
return `- ${labelText}: ${bits.join(", ")}`;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ import {
|
||||
type ChatChannel,
|
||||
} from "./shared.js";
|
||||
|
||||
type ChannelStatusPluginLabel = {
|
||||
id: ChatChannel;
|
||||
meta: { label?: string };
|
||||
};
|
||||
|
||||
export async function formatConfigChannelsStatusLines(
|
||||
cfg: OpenClawConfig,
|
||||
meta: { path?: string; mode?: "local" | "remote" },
|
||||
@@ -37,14 +42,19 @@ export async function formatConfigChannelsStatusLines(
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
const accountLines = (provider: ChatChannel, accounts: Array<Record<string, unknown>>) =>
|
||||
const accountLines = (
|
||||
plugin: ChannelStatusPluginLabel,
|
||||
accounts: Array<Record<string, unknown>>,
|
||||
) =>
|
||||
accounts.map((account) => {
|
||||
const bits: string[] = [];
|
||||
appendEnabledConfiguredLinkedBits(bits, account);
|
||||
appendModeBit(bits, account);
|
||||
appendTokenSourceBits(bits, account);
|
||||
appendBaseUrlBit(bits, account);
|
||||
return buildChannelAccountLine(provider, account, bits);
|
||||
return buildChannelAccountLine(plugin.id, account, bits, {
|
||||
channelLabel: plugin.meta.label ?? plugin.id,
|
||||
});
|
||||
});
|
||||
|
||||
const sourceConfig = opts?.sourceConfig ?? cfg;
|
||||
@@ -79,7 +89,7 @@ export async function formatConfigChannelsStatusLines(
|
||||
);
|
||||
}
|
||||
if (snapshots.length > 0) {
|
||||
lines.push(...accountLines(plugin.id, snapshots));
|
||||
lines.push(...accountLines(plugin, snapshots));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,10 @@ function formatChannelsStatusError(err: unknown): string {
|
||||
export function formatGatewayChannelsStatusLines(payload: Record<string, unknown>): string[] {
|
||||
const lines: string[] = [];
|
||||
lines.push(theme.success("Gateway reachable."));
|
||||
const channelLabels =
|
||||
payload.channelLabels && typeof payload.channelLabels === "object"
|
||||
? (payload.channelLabels as Record<string, unknown>)
|
||||
: {};
|
||||
const accountLines = (provider: ChatChannel, accounts: Array<Record<string, unknown>>) =>
|
||||
accounts.map((account) => {
|
||||
const bits: string[] = [];
|
||||
@@ -118,7 +122,10 @@ export function formatGatewayChannelsStatusLines(payload: Record<string, unknown
|
||||
if (typeof account.lastError === "string" && account.lastError) {
|
||||
bits.push(`error:${account.lastError}`);
|
||||
}
|
||||
return buildChannelAccountLine(provider, account, bits);
|
||||
const rawChannelLabel = channelLabels[provider];
|
||||
return buildChannelAccountLine(provider, account, bits, {
|
||||
channelLabel: typeof rawChannelLabel === "string" ? rawChannelLabel : provider,
|
||||
});
|
||||
});
|
||||
|
||||
const accountsByChannel = payload.channelAccounts as Record<string, unknown> | undefined;
|
||||
|
||||
@@ -67,6 +67,7 @@ describe("status-runtime-shared", () => {
|
||||
deep: false,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: true,
|
||||
loadPluginSecurityCollectors: false,
|
||||
plugins: expect.any(Array),
|
||||
});
|
||||
expect(mocks.resolveReadOnlyChannelPluginsForConfig).toHaveBeenCalledWith(
|
||||
@@ -96,6 +97,7 @@ describe("status-runtime-shared", () => {
|
||||
deep: false,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: true,
|
||||
loadPluginSecurityCollectors: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -283,6 +285,7 @@ describe("status-runtime-shared", () => {
|
||||
deep: false,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: true,
|
||||
loadPluginSecurityCollectors: false,
|
||||
plugins: expect.any(Array),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ export async function resolveStatusSecurityAudit(params: {
|
||||
deep: false,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: true,
|
||||
loadPluginSecurityCollectors: false,
|
||||
...(readOnlyPlugins.missingConfiguredChannelIds.length === 0
|
||||
? { plugins: readOnlyPlugins.plugins }
|
||||
: {}),
|
||||
|
||||
@@ -740,6 +740,7 @@ vi.mock("./status-runtime-shared.ts", () => ({
|
||||
deep: false,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: true,
|
||||
loadPluginSecurityCollectors: false,
|
||||
}),
|
||||
),
|
||||
resolveStatusUsageSummary: vi.fn(async () => undefined),
|
||||
@@ -759,6 +760,7 @@ vi.mock("./status-runtime-shared.ts", () => ({
|
||||
deep: false,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: true,
|
||||
loadPluginSecurityCollectors: false,
|
||||
}))
|
||||
)({
|
||||
config: params.config,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const applyPluginAutoEnableMock = vi.hoisted(() => vi.fn());
|
||||
const getActivePluginRegistryMock = vi.hoisted(() => vi.fn());
|
||||
const loadPluginMetadataRegistrySnapshotMock = vi.hoisted(() => vi.fn());
|
||||
const resolveConfiguredChannelPluginIdsMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -13,6 +14,10 @@ vi.mock("../plugins/channel-plugin-ids.js", () => ({
|
||||
resolveConfiguredChannelPluginIdsMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/runtime.js", () => ({
|
||||
getActivePluginRegistry: (...args: unknown[]) => getActivePluginRegistryMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/runtime/metadata-registry-loader.js", () => ({
|
||||
loadPluginMetadataRegistrySnapshot: (...args: unknown[]) =>
|
||||
loadPluginMetadataRegistrySnapshotMock(...args),
|
||||
@@ -36,6 +41,7 @@ function createAuditContext(params: {
|
||||
stateDir: "/tmp/openclaw-test-state",
|
||||
configPath: "/tmp/openclaw-test-config.json",
|
||||
plugins: params.plugins,
|
||||
loadPluginSecurityCollectors: true,
|
||||
configSnapshot: null,
|
||||
codeSafetySummaryCache: new Map<string, Promise<unknown>>(),
|
||||
};
|
||||
@@ -48,8 +54,10 @@ describe("security audit read-only plugin scope", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
applyPluginAutoEnableMock.mockReset();
|
||||
getActivePluginRegistryMock.mockReset();
|
||||
loadPluginMetadataRegistrySnapshotMock.mockReset();
|
||||
resolveConfiguredChannelPluginIdsMock.mockReset();
|
||||
getActivePluginRegistryMock.mockReturnValue(null);
|
||||
applyPluginAutoEnableMock.mockImplementation((params: { config: unknown }) => ({
|
||||
config: params.config,
|
||||
changes: [],
|
||||
@@ -127,4 +135,25 @@ describe("security audit read-only plugin scope", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips plugin runtime and collector discovery when collector loading is disabled", async () => {
|
||||
const sourceConfig = {
|
||||
plugins: {
|
||||
allow: ["audit-plugin"],
|
||||
},
|
||||
};
|
||||
|
||||
const findings = await collectPluginSecurityAuditFindings({
|
||||
...createAuditContext({
|
||||
sourceConfig,
|
||||
plugins: [],
|
||||
}),
|
||||
loadPluginSecurityCollectors: false,
|
||||
});
|
||||
|
||||
expect(findings).toEqual([]);
|
||||
expect(getActivePluginRegistryMock).not.toHaveBeenCalled();
|
||||
expect(applyPluginAutoEnableMock).not.toHaveBeenCalled();
|
||||
expect(loadPluginMetadataRegistrySnapshotMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,8 @@ export type SecurityAuditOptions = {
|
||||
deepTimeoutMs?: number;
|
||||
/** Dependency injection for tests. */
|
||||
plugins?: ChannelPlugin[];
|
||||
/** Whether to import plugin modules to discover plugin security audit collectors. */
|
||||
loadPluginSecurityCollectors?: boolean;
|
||||
/** Dependency injection for tests (Windows ACL checks). */
|
||||
execIcacls?: ExecFn;
|
||||
/** Dependency injection for tests (Docker label checks). */
|
||||
@@ -89,6 +91,7 @@ export type AuditExecutionContext = {
|
||||
execDockerRawFn?: ExecDockerRawFn;
|
||||
probeGatewayFn?: ProbeGatewayFn;
|
||||
plugins?: ChannelPlugin[];
|
||||
loadPluginSecurityCollectors: boolean;
|
||||
configSnapshot: ConfigFileSnapshot | null;
|
||||
codeSafetySummaryCache: Map<string, Promise<unknown>>;
|
||||
deepProbeAuth?: { token?: string; password?: string };
|
||||
@@ -338,6 +341,9 @@ export function collectGatewayConfigFindings(
|
||||
export async function collectPluginSecurityAuditFindings(
|
||||
context: AuditExecutionContext,
|
||||
): Promise<SecurityAuditFinding[]> {
|
||||
if (!context.loadPluginSecurityCollectors) {
|
||||
return [];
|
||||
}
|
||||
const { getActivePluginRegistry } = await loadPluginRuntimeModule();
|
||||
let collectors = getActivePluginRegistry()?.securityAuditCollectors ?? [];
|
||||
if (collectors.length === 0) {
|
||||
@@ -940,6 +946,7 @@ async function createAuditExecutionContext(
|
||||
execDockerRawFn: opts.execDockerRawFn,
|
||||
probeGatewayFn: opts.probeGatewayFn,
|
||||
plugins: opts.plugins,
|
||||
loadPluginSecurityCollectors: opts.loadPluginSecurityCollectors !== false,
|
||||
workspaceDir,
|
||||
configSnapshot,
|
||||
codeSafetySummaryCache: opts.codeSafetySummaryCache ?? new Map<string, Promise<unknown>>(),
|
||||
|
||||
Reference in New Issue
Block a user