fix(cli): render errors through redacting formatter (#123929)

This commit is contained in:
Peter Steinberger
2026-08-14 19:18:58 -07:00
committed by GitHub
parent f570022e14
commit 2463ea0a5e
19 changed files with 77 additions and 38 deletions
+1 -1
View File
@@ -475,7 +475,7 @@ describe("serveAcpGateway startup", () => {
opts: { verbose: true },
expected: [
"openclaw acp: gateway event chat failed\n",
"openclaw acp: gateway event chat error: Error: handler boom\n",
"openclaw acp: gateway event chat error: handler boom\n",
],
},
])("contains rejected gateway event handling with $name", async ({ opts, expected }) => {
+7 -4
View File
@@ -21,6 +21,7 @@ import { getRuntimeConfig } from "../config/config.js";
import { resolveGatewayClientBootstrap } from "../gateway/client-bootstrap.js";
import { startGatewayClientWhenEventLoopReady } from "../gateway/client-start-readiness.js";
import { GatewayClient } from "../gateway/client.js";
import { formatErrorMessage } from "../infra/errors.js";
import { isMainModule } from "../infra/is-main.js";
import { routeLogsToStderr } from "../logging/console.js";
import { closeOpenClawStateDatabase } from "../state/openclaw-state-db.js";
@@ -137,7 +138,7 @@ export async function serveAcpGateway(opts: AcpServerOptions = {}): Promise<void
try {
closeOpenClawStateDatabase();
} catch (err) {
console.warn(`acp: state database close failed during shutdown: ${String(err)}`);
console.warn(`acp: state database close failed during shutdown: ${formatErrorMessage(err)}`);
}
};
@@ -161,7 +162,9 @@ export async function serveAcpGateway(opts: AcpServerOptions = {}): Promise<void
void agent?.handleGatewayEvent(evt).catch((err: unknown) => {
process.stderr.write(`openclaw acp: gateway event ${evt.event} failed\n`);
if (opts.verbose) {
process.stderr.write(`openclaw acp: gateway event ${evt.event} error: ${String(err)}\n`);
process.stderr.write(
`openclaw acp: gateway event ${evt.event} error: ${formatErrorMessage(err)}\n`,
);
}
});
},
@@ -202,7 +205,7 @@ export async function serveAcpGateway(opts: AcpServerOptions = {}): Promise<void
agent = null;
activeAgent?.shutdown();
const gatewayStop = gateway.stopAndWait().catch((err: unknown) => {
console.warn(`acp: gateway stop failed during shutdown: ${String(err)}`);
console.warn(`acp: gateway stop failed during shutdown: ${formatErrorMessage(err)}`);
});
await gatewayStop;
closeStateDatabase();
@@ -420,7 +423,7 @@ if (isMainModule({ currentFile: fileURLToPath(import.meta.url) })) {
}
const opts = parseArgs(argv);
serveAcpGateway(opts).catch((err: unknown) => {
console.error(String(err));
console.error(formatErrorMessage(err));
process.exit(1);
});
}
+2 -2
View File
@@ -80,7 +80,7 @@ describe("runGatewayHealthJsonRoute", () => {
);
});
it("preserves the existing error contract when local config resolution fails", async () => {
it("formats local config resolution failures", async () => {
const runtime = createRuntime();
const error = new Error("config unavailable");
const callGateway = vi.fn();
@@ -101,7 +101,7 @@ describe("runGatewayHealthJsonRoute", () => {
expect(callGateway).not.toHaveBeenCalled();
expect(runtime.writeJson).not.toHaveBeenCalled();
expect(runtime.error).toHaveBeenCalledWith(String(error));
expect(runtime.error).toHaveBeenCalledWith(error.message);
expect(runtime.exit).toHaveBeenCalledWith(1);
});
+2 -1
View File
@@ -1,5 +1,6 @@
// Route-first machine-readable Gateway health command.
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
type GatewayHealthRpcOpts = Parameters<
@@ -62,7 +63,7 @@ export async function runGatewayHealthJsonRoute(
);
} catch (error) {
if (!rpc) {
runtime.error(String(error));
runtime.error(formatErrorMessage(error));
runtime.exit(1);
return;
}
+2 -4
View File
@@ -149,9 +149,7 @@ describe("cli program (smoke)", () => {
it("rejects partial tui history limits", async () => {
await expect(runProgram(["tui", "--history-limit", "10x"])).rejects.toThrow("exit");
expect(runtime.error).toHaveBeenCalledWith(
"Error: --history-limit must be a positive integer.",
);
expect(runtime.error).toHaveBeenCalledWith("--history-limit must be a positive integer.");
expect(tuiRunMock).not.toHaveBeenCalled();
});
@@ -175,7 +173,7 @@ describe("cli program (smoke)", () => {
it("rejects tui history limits above the Gateway maximum", async () => {
await expect(runProgram(["tui", "--history-limit", "1001"])).rejects.toThrow("exit");
expect(runtime.error).toHaveBeenCalledWith("Error: --history-limit must be at most 1000.");
expect(runtime.error).toHaveBeenCalledWith("--history-limit must be at most 1000.");
expect(tuiRunMock).not.toHaveBeenCalled();
});
+1 -3
View File
@@ -274,9 +274,7 @@ describe("registerQrCli", () => {
});
await expect(runQr(["--setup-code-only", "--limited", "--voice-node"])).rejects.toThrow("exit");
expect(runtime.error).toHaveBeenCalledWith(
"Error: Use either --limited or --voice-node, not both.",
);
expect(runtime.error).toHaveBeenCalledWith("Use either --limited or --voice-node, not both.");
});
it("renders ASCII QR by default", async () => {
+2 -1
View File
@@ -7,6 +7,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
import { trimToUndefined } from "../gateway/credentials.js";
import { resolveRequiredConfiguredSecretRefInputString } from "../gateway/resolve-configured-secret-input-string.js";
import { formatErrorMessage } from "../infra/errors.js";
import { loadGatewayTlsRuntime } from "../infra/tls/gateway.js";
import { renderQrTerminal } from "../media/qr-terminal.ts";
import { resolvePairingSetupFromConfig, encodePairingSetupCode } from "../pairing/setup-code.js";
@@ -282,7 +283,7 @@ export function registerQrCli(program: Command) {
defaultRuntime.log(lines.join("\n"));
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
});
+2 -1
View File
@@ -2,6 +2,7 @@
import type { Command } from "commander";
import { formatDocsLink } from "../../packages/terminal-core/src/links.js";
import { theme } from "../../packages/terminal-core/src/theme.js";
import { formatErrorMessage } from "../infra/errors.js";
import { defaultRuntime } from "../runtime.js";
import { addTuiOptions } from "./tui-cli-options.js";
@@ -31,7 +32,7 @@ export function registerResumeCli(program: Command) {
const { runResumeCommand } = await import("./resume-cli.runtime.js");
await runResumeCommand(query, opts);
} catch (error) {
defaultRuntime.error(String(error));
defaultRuntime.error(formatErrorMessage(error));
defaultRuntime.exit(1);
}
});
+2 -1
View File
@@ -4,6 +4,7 @@ import { formatDocsLink } from "../../packages/terminal-core/src/links.js";
import { theme } from "../../packages/terminal-core/src/theme.js";
import { sandboxExplainCommand } from "../commands/sandbox-explain.js";
import { sandboxListCommand, sandboxRecreateCommand } from "../commands/sandbox.js";
import { formatErrorMessage } from "../infra/errors.js";
import { defaultRuntime } from "../runtime.js";
import { formatHelpExamples } from "./help-format.js";
@@ -50,7 +51,7 @@ function createRunner(
try {
await commandFn(opts, defaultRuntime);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
};
+27
View File
@@ -1477,6 +1477,33 @@ describe("skills cli commands", () => {
expectStatusWorkspaceCall("/tmp/workspace-main");
});
it("renders named agent-selection errors without the internal class name", async () => {
const error = new Error(
"Multiple agents are configured, but this operation has no explicit owner.",
);
error.name = "AgentSelectionRequiredError";
resolveDefaultAgentIdMock.mockImplementationOnce(() => {
throw error;
});
await expect(runCommand(["skills", "list"])).rejects.toThrow("__exit__:1");
expect(runtimeErrors).toStrictEqual([error.message]);
});
it("redacts secrets from rendered skills CLI errors", async () => {
const secret = "sk-abcdefghijklmnopqrstuv";
resolveDefaultAgentIdMock.mockImplementationOnce(() => {
throw new Error(`Skill lookup failed with token=${secret}`);
});
await expect(runCommand(["skills", "list"])).rejects.toThrow("__exit__:1");
expect(runtimeErrors).toHaveLength(1);
expect(runtimeErrors[0]).toContain("Skill lookup failed");
expect(runtimeErrors[0]).not.toContain(secret);
});
it("keeps non-JSON skills list output on stdout with human-readable formatting", async () => {
await runCommand(["skills", "list"]);
+1 -1
View File
@@ -110,7 +110,7 @@ describe("skills curator cli", () => {
from: "user",
}),
).rejects.toThrow("__exit__:1");
expect(mocks.defaultRuntime.error).toHaveBeenCalledWith("Error: remote unavailable");
expect(mocks.defaultRuntime.error).toHaveBeenCalledWith("remote unavailable");
});
it("disambiguates duplicate skill keys in text status", async () => {
+10 -9
View File
@@ -23,6 +23,7 @@ import {
fetchClawHubSkillVerification,
type ClawHubSkillVerificationResponse,
} from "../infra/clawhub-skills.js";
import { formatErrorMessage } from "../infra/errors.js";
import { defaultRuntime } from "../runtime.js";
import {
installSkillFromClawHub,
@@ -209,7 +210,7 @@ async function runSkillsAction(
const report = await loadSkillsStatusReport(options);
defaultRuntime.writeStdout(render(report));
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
}
@@ -601,7 +602,7 @@ export function registerSkillsCli(program: Command) {
defaultRuntime.log(`${skillRef}${version} ${displayName}${summary}${trust}`);
}
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
});
@@ -734,7 +735,7 @@ export function registerSkillsCli(program: Command) {
}
defaultRuntime.log(`Installed ${result.slug}@${result.version} -> ${result.targetDir}`);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
},
@@ -834,7 +835,7 @@ export function registerSkillsCli(program: Command) {
defaultRuntime.exit(1);
}
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
},
@@ -917,7 +918,7 @@ export function registerSkillsCli(program: Command) {
}
}
} catch (err) {
reportError(String(err));
reportError(formatErrorMessage(err));
defaultRuntime.exit(1);
return;
}
@@ -941,7 +942,7 @@ export function registerSkillsCli(program: Command) {
}
defaultRuntime.writeStdout(formatSkillCuratorStatus(status));
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
};
@@ -967,7 +968,7 @@ export function registerSkillsCli(program: Command) {
`${action[0]?.toUpperCase()}${action.slice(1)} ${result.skillKey}\n`,
);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
});
@@ -996,7 +997,7 @@ export function registerSkillsCli(program: Command) {
}
defaultRuntime.writeStdout(format(result));
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
};
@@ -1061,7 +1062,7 @@ export function registerSkillsCli(program: Command) {
}
defaultRuntime.writeStdout(formatSkillProposalInspect(proposal));
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
});
+1 -1
View File
@@ -284,7 +284,7 @@ describe("skills verify CLI", () => {
).rejects.toThrow("__exit__:1");
expect(JSON.parse(mocks.runtimeStdout.at(-1) ?? "{}")).toEqual({
error: "Error: ClawHub verification unavailable",
error: "ClawHub verification unavailable",
});
expect(mocks.runtimeErrors).toStrictEqual([]);
});
+2 -1
View File
@@ -4,6 +4,7 @@ import type { Command } from "commander";
import { CHAT_HISTORY_MAX_ENTRIES } from "../../packages/gateway-protocol/src/schema/chat-history-constants.js";
import { formatDocsLink } from "../../packages/terminal-core/src/links.js";
import { theme } from "../../packages/terminal-core/src/theme.js";
import { formatErrorMessage } from "../infra/errors.js";
import { defaultRuntime } from "../runtime.js";
import { parseTimeoutMs } from "./parse-timeout.js";
import { resolveSessionTarget } from "./session-target.js";
@@ -123,7 +124,7 @@ export function registerTuiCli(program: Command) {
const invokedSubcommand = cmd.parent?.args[0];
await runTuiCliAction(target, opts, invokedSubcommand);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
});
+5 -4
View File
@@ -2,6 +2,7 @@
import type { Command } from "commander";
import { formatDocsLink } from "../../packages/terminal-core/src/links.js";
import { theme } from "../../packages/terminal-core/src/theme.js";
import { formatErrorMessage } from "../infra/errors.js";
import { defaultRuntime } from "../runtime.js";
import { inheritOptionFromParent } from "./command-options.js";
import { formatHelpExamples } from "./help-format.js";
@@ -118,7 +119,7 @@ function registerUpdateFinalizationCommand(update: Command, name: string, hidden
normalizeCommanderClawHubRiskOption(opts) || inheritedUpdateClawHubRisk(actionCommand),
});
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
});
@@ -209,7 +210,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/update", "docs.openclaw.ai/cli/up
acknowledgeClawHubRisk: normalizeCommanderClawHubRiskOption(opts),
});
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
});
@@ -235,7 +236,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/update", "docs.openclaw.ai/cli/up
timeout: inheritedUpdateTimeout(opts, command),
});
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
});
@@ -265,7 +266,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/update", "docs.openclaw.ai/cli/up
timeout: inheritedUpdateTimeout(opts, command),
});
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
});
@@ -3,6 +3,7 @@ import { theme } from "../../../packages/terminal-core/src/theme.js";
import { readConfigFileSnapshot } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { readGatewayServiceState, resolveGatewayService } from "../../daemon/service.js";
import { formatErrorMessage } from "../../infra/errors.js";
import type { UpdateChannel } from "../../infra/update-channels.js";
import { compareSemverStrings } from "../../infra/update-check.js";
import {
@@ -437,7 +438,7 @@ export async function finishUpdate(params: {
}
} catch (err) {
if (err instanceof GatewayServiceUpdateOwnershipError) {
defaultRuntime.error(err.message);
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
return;
}
+2 -1
View File
@@ -5,6 +5,7 @@ import { selectStyled } from "../../../packages/terminal-core/src/prompt-select-
import { stylePromptMessage } from "../../../packages/terminal-core/src/prompt-style.js";
import { theme } from "../../../packages/terminal-core/src/theme.js";
import { readConfigFileSnapshot } from "../../config/config.js";
import { formatErrorMessage } from "../../infra/errors.js";
import {
formatUpdateChannelLabel,
normalizeUpdateChannel,
@@ -155,7 +156,7 @@ export async function updateWizardCommand(opts: UpdateWizardOptions = {}): Promi
timeout: opts.timeout,
});
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.error(formatErrorMessage(err));
defaultRuntime.exit(1);
}
}
+2 -1
View File
@@ -15,6 +15,7 @@ import { migratePersistedImplicitMainRoster } from "../config/legacy.roster.js";
import { logConfigUpdated } from "../config/logging.js";
import type { AgentConfig, IdentityConfig } from "../config/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { formatErrorMessage } from "../infra/errors.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
@@ -136,7 +137,7 @@ export async function agentsSetIdentityCommand(
try {
identityFromFile = await loadAgentIdentityFromFile(identityFilePath);
} catch (error) {
runtime.error(String(error instanceof Error ? error.message : error));
runtime.error(formatErrorMessage(error));
runtime.exit(1);
return;
}
+4 -1
View File
@@ -352,9 +352,12 @@ describe("agents set-identity command", () => {
await agentsSetIdentityCommand({ agent: "main", identityFile: identityPath }, runtime);
expect(runtime.error).toHaveBeenCalledWith(
const renderedError = String(runtime.error.mock.calls[0]?.[0]);
expect(renderedError).toContain(
`Identity file ${identityPath} exceeds the maximum size of ${TEST_MAX_IDENTITY_FILE_BYTES} bytes`,
);
expect(renderedError).toContain(`File exceeds ${TEST_MAX_IDENTITY_FILE_BYTES} bytes:`);
expect(renderedError).toContain("too-large");
expect(runtime.exit).toHaveBeenCalledWith(1);
expect(configMocks.writeConfigFile).not.toHaveBeenCalled();
});