mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix(cli): trim plugin preloads for setup-safe commands
This commit is contained in:
@@ -64,6 +64,9 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/gateway: keep diagnostic probes from creating first-time read-only device
|
||||
pairings, while still reusing cached device tokens for detailed read probes.
|
||||
Fixes #71766. Thanks @SunboZ.
|
||||
- CLI/plugins: keep `message` startup, `channels logs`, `agents delete`, and
|
||||
`agents set-identity` off broad plugin preloading; message delivery still
|
||||
loads plugins when the action actually runs.
|
||||
- CLI/agents: keep `agents bind`, `agents unbind`, and `agents bindings` on
|
||||
setup-safe channel metadata paths so they do not preload bundled plugin
|
||||
runtimes or stage runtime dependencies. Fixes #71743.
|
||||
|
||||
@@ -37,7 +37,7 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
|
||||
policy: { bypassConfigGuard: true, loadPlugins: "never", ensureCliPath: false },
|
||||
},
|
||||
{ commandPath: ["agent"], policy: { loadPlugins: "always" } },
|
||||
{ commandPath: ["message"], policy: { loadPlugins: "always" } },
|
||||
{ commandPath: ["message"], policy: { loadPlugins: "never" } },
|
||||
{ commandPath: ["channels"], policy: { loadPlugins: "always" } },
|
||||
{ commandPath: ["directory"], policy: { loadPlugins: "always" } },
|
||||
{ commandPath: ["agents"], policy: { loadPlugins: "always" } },
|
||||
@@ -56,6 +56,16 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
|
||||
exact: true,
|
||||
policy: { loadPlugins: "never" },
|
||||
},
|
||||
{
|
||||
commandPath: ["agents", "set-identity"],
|
||||
exact: true,
|
||||
policy: { loadPlugins: "never" },
|
||||
},
|
||||
{
|
||||
commandPath: ["agents", "delete"],
|
||||
exact: true,
|
||||
policy: { loadPlugins: "never" },
|
||||
},
|
||||
{ commandPath: ["configure"], policy: { bypassConfigGuard: true, loadPlugins: "never" } },
|
||||
{
|
||||
commandPath: ["status"],
|
||||
@@ -168,4 +178,9 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
|
||||
policy: { loadPlugins: "never" },
|
||||
route: { id: "channels-list" },
|
||||
},
|
||||
{
|
||||
commandPath: ["channels", "logs"],
|
||||
exact: true,
|
||||
policy: { loadPlugins: "never" },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -41,13 +41,22 @@ describe("command-path-policy", () => {
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
});
|
||||
expect(resolveCliCommandPathPolicy(["channels", "logs"])).toEqual({
|
||||
bypassConfigGuard: false,
|
||||
routeConfigGuard: "never",
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps agent binding commands on config-only startup", () => {
|
||||
it("keeps config-only agent commands on config-only startup", () => {
|
||||
for (const commandPath of [
|
||||
["agents", "bind"],
|
||||
["agents", "bindings"],
|
||||
["agents", "unbind"],
|
||||
["agents", "set-identity"],
|
||||
["agents", "delete"],
|
||||
]) {
|
||||
expect(resolveCliCommandPathPolicy(commandPath)).toEqual({
|
||||
bypassConfigGuard: false,
|
||||
|
||||
@@ -74,6 +74,24 @@ describe("command-startup-policy", () => {
|
||||
jsonOutputMode: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["channels", "logs"],
|
||||
jsonOutputMode: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["message", "send"],
|
||||
jsonOutputMode: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["message", "send"],
|
||||
jsonOutputMode: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["agents", "list"],
|
||||
@@ -107,6 +125,18 @@ describe("command-startup-policy", () => {
|
||||
jsonOutputMode: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["agents", "set-identity"],
|
||||
jsonOutputMode: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["agents", "delete"],
|
||||
jsonOutputMode: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("matches banner suppression policy", () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ vi.mock("../../../globals.js", () => ({
|
||||
vi.mock("../../plugin-registry.js", () => ({
|
||||
ensurePluginRegistryLoaded: vi.fn(),
|
||||
}));
|
||||
const { ensurePluginRegistryLoaded } = await import("../../plugin-registry.js");
|
||||
|
||||
const hasHooksMock = vi.fn((_hookName: string) => false);
|
||||
const runGatewayStopMock = vi.fn(
|
||||
@@ -95,6 +96,7 @@ describe("runMessageAction", () => {
|
||||
it("calls exit(0) after successful message delivery", async () => {
|
||||
await runSendAction();
|
||||
|
||||
expect(ensurePluginRegistryLoaded).toHaveBeenCalledOnce();
|
||||
expect(exitMock).toHaveBeenCalledOnce();
|
||||
expect(exitMock).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
@@ -122,6 +122,7 @@ describe("registerPreActionHooks", () => {
|
||||
.command("agent")
|
||||
.requiredOption("-m, --message <text>")
|
||||
.option("--local")
|
||||
.option("--json")
|
||||
.action(() => {});
|
||||
program
|
||||
.command("status")
|
||||
@@ -214,15 +215,15 @@ describe("registerPreActionHooks", () => {
|
||||
|
||||
vi.clearAllMocks();
|
||||
await runPreAction({
|
||||
parseArgv: ["message", "send"],
|
||||
processArgv: ["node", "openclaw", "message", "send"],
|
||||
parseArgv: ["agents", "list"],
|
||||
processArgv: ["node", "openclaw", "agents", "list"],
|
||||
});
|
||||
|
||||
expect(setVerboseMock).toHaveBeenCalledWith(false);
|
||||
expect(process.env.NODE_NO_WARNINGS).toBe("1");
|
||||
expect(ensureConfigReadyMock).toHaveBeenCalledWith({
|
||||
runtime: runtimeMock,
|
||||
commandPath: ["message", "send"],
|
||||
commandPath: ["agents", "list"],
|
||||
});
|
||||
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "all" });
|
||||
processTitleSetSpy.mockRestore();
|
||||
@@ -393,8 +394,8 @@ describe("registerPreActionHooks", () => {
|
||||
|
||||
it("routes logs to stderr in --json mode so stdout stays clean", async () => {
|
||||
await runPreAction({
|
||||
parseArgv: ["message", "send"],
|
||||
processArgv: ["node", "openclaw", "message", "send", "--json"],
|
||||
parseArgv: ["agent"],
|
||||
processArgv: ["node", "openclaw", "agent", "--message", "hi", "--json"],
|
||||
});
|
||||
|
||||
expect(routeLogsToStderrMock).toHaveBeenCalledOnce();
|
||||
@@ -473,8 +474,8 @@ describe("registerPreActionHooks", () => {
|
||||
});
|
||||
|
||||
await runPreAction({
|
||||
parseArgv: ["message", "send"],
|
||||
processArgv: ["node", "openclaw", "message", "send", "--json"],
|
||||
parseArgv: ["agent"],
|
||||
processArgv: ["node", "openclaw", "agent", "--message", "hi", "--json"],
|
||||
});
|
||||
|
||||
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalled();
|
||||
|
||||
84
src/commands/channels.logs.test.ts
Normal file
84
src/commands/channels.logs.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { setLoggerOverride } from "../logging.js";
|
||||
import { createTestRuntime } from "./test-runtime-config-helpers.js";
|
||||
|
||||
const pluginRegistryMocks = vi.hoisted(() => ({
|
||||
loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })),
|
||||
listPluginContributionIds: vi.fn(() => ["external-chat"]),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/plugin-registry.js", () => ({
|
||||
loadPluginRegistrySnapshot: pluginRegistryMocks.loadPluginRegistrySnapshot,
|
||||
listPluginContributionIds: pluginRegistryMocks.listPluginContributionIds,
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/index.js", () => ({
|
||||
listChannelPlugins: vi.fn(() => {
|
||||
throw new Error("channels logs must not load channel plugins");
|
||||
}),
|
||||
}));
|
||||
|
||||
import { channelsLogsCommand } from "./channels/logs.js";
|
||||
|
||||
const runtime = createTestRuntime();
|
||||
|
||||
function logLine(params: { module: string; message: string }) {
|
||||
return JSON.stringify({
|
||||
time: "2026-04-25T12:00:00.000Z",
|
||||
0: params.message,
|
||||
_meta: {
|
||||
logLevelName: "INFO",
|
||||
name: JSON.stringify({ module: params.module }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("channelsLogsCommand", () => {
|
||||
let tempDir: string;
|
||||
let logPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-channels-logs-"));
|
||||
logPath = path.join(tempDir, "openclaw.log");
|
||||
setLoggerOverride({ file: logPath });
|
||||
runtime.log.mockClear();
|
||||
runtime.error.mockClear();
|
||||
runtime.exit.mockClear();
|
||||
pluginRegistryMocks.loadPluginRegistrySnapshot.mockClear();
|
||||
pluginRegistryMocks.listPluginContributionIds.mockClear();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
setLoggerOverride(null);
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("filters external plugin channel logs from the persisted manifest registry", async () => {
|
||||
await fs.writeFile(
|
||||
logPath,
|
||||
[
|
||||
logLine({ module: "gateway/channels/external-chat/send", message: "external sent" }),
|
||||
logLine({ module: "gateway/channels/slack/send", message: "slack sent" }),
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
await channelsLogsCommand({ channel: "external-chat", json: true }, runtime);
|
||||
|
||||
expect(pluginRegistryMocks.loadPluginRegistrySnapshot).toHaveBeenCalledOnce();
|
||||
expect(pluginRegistryMocks.listPluginContributionIds).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
contribution: "channels",
|
||||
includeDisabled: true,
|
||||
}),
|
||||
);
|
||||
const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0])) as {
|
||||
channel: string;
|
||||
lines: Array<{ message: string }>;
|
||||
};
|
||||
expect(payload.channel).toBe("external-chat");
|
||||
expect(payload.lines.map((line) => line.message)).toEqual(["external sent"]);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import { normalizeChannelId as normalizeBundledChannelId } from "../../channels/registry.js";
|
||||
import { getResolvedLoggerSettings } from "../../logging.js";
|
||||
import { parseLogLine } from "../../logging/parse-log-line.js";
|
||||
import {
|
||||
listPluginContributionIds,
|
||||
loadPluginRegistrySnapshot,
|
||||
} from "../../plugins/plugin-registry.js";
|
||||
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
@@ -17,15 +21,29 @@ type LogLine = ReturnType<typeof parseLogLine>;
|
||||
const DEFAULT_LIMIT = 200;
|
||||
const MAX_BYTES = 1_000_000;
|
||||
|
||||
const getChannelSet = () =>
|
||||
new Set<string>([...listChannelPlugins().map((plugin) => plugin.id), "all"]);
|
||||
function listManifestChannelIds(): Set<string> {
|
||||
const index = loadPluginRegistrySnapshot({
|
||||
env: process.env,
|
||||
});
|
||||
return new Set(
|
||||
listPluginContributionIds({
|
||||
index,
|
||||
contribution: "channels",
|
||||
includeDisabled: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function parseChannelFilter(raw?: string) {
|
||||
const trimmed = normalizeLowercaseStringOrEmpty(raw);
|
||||
if (!trimmed) {
|
||||
if (!trimmed || trimmed === "all") {
|
||||
return "all";
|
||||
}
|
||||
return getChannelSet().has(trimmed) ? trimmed : "all";
|
||||
const bundled = normalizeBundledChannelId(trimmed);
|
||||
if (bundled) {
|
||||
return bundled;
|
||||
}
|
||||
return listManifestChannelIds().has(trimmed) ? trimmed : "all";
|
||||
}
|
||||
|
||||
function matchesChannel(line: NonNullable<LogLine>, channel: string) {
|
||||
|
||||
Reference in New Issue
Block a user