From 20d097ac2f48309d515370ae08b92c6ddcee86f3 Mon Sep 17 00:00:00 2001 From: Harold Hunt Date: Wed, 11 Mar 2026 17:32:41 -0400 Subject: [PATCH] Gateway/Dashboard: surface config validation issues (#42664) Merged via squash. Prepared head SHA: 43f66cdcf04a14d5381a5a2f14e291a52a8b7389 Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com> Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com> Reviewed-by: @huntharo --- CHANGELOG.md | 1 + src/gateway/server-methods/config.ts | 23 +++++++++++++++--- src/gateway/server.config-patch.test.ts | 32 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f8d76aa82..e88bd0d4638 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,6 +110,7 @@ Docs: https://docs.openclaw.ai - Agents/error rendering: ignore stale assistant `errorMessage` fields on successful turns so background/tool-side failures no longer prepend synthetic billing errors over valid replies. (#40616) Thanks @ingyukoh. - Agents/fallback: recognize Venice `402 Insufficient USD or Diem balance` billing errors so configured model fallbacks trigger instead of surfacing the raw provider error. (#43205) Thanks @Squabble9. - Dependencies: refresh workspace dependencies except the pinned Carbon package, and harden ACP session-config writes against non-string SDK values so newer ACP clients fail fast instead of tripping type/runtime mismatches. +- Gateway/config errors: surface up to three validation issues in top-level `config.set`, `config.patch`, and `config.apply` error messages while preserving structured issue details. (#42664) Thanks @huntharo. ## 2026.3.8 diff --git a/src/gateway/server-methods/config.ts b/src/gateway/server-methods/config.ts index 9b57a126e5f..1d3d1c85977 100644 --- a/src/gateway/server-methods/config.ts +++ b/src/gateway/server-methods/config.ts @@ -10,6 +10,7 @@ import { validateConfigObjectWithPlugins, writeConfigFile, } from "../../config/config.js"; +import { formatConfigIssueLines } from "../../config/issue-format.js"; import { applyLegacyMigrations } from "../../config/legacy.js"; import { applyMergePatch } from "../../config/merge-patch.js"; import { @@ -23,7 +24,7 @@ import { type ConfigSchemaResponse, } from "../../config/schema.js"; import { extractDeliveryInfo } from "../../config/sessions.js"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import type { ConfigValidationIssue, OpenClawConfig } from "../../config/types.openclaw.js"; import { formatDoctorNonInteractiveHint, type RestartSentinelPayload, @@ -54,6 +55,8 @@ import { parseRestartRequestParams } from "./restart-request.js"; import type { GatewayRequestHandlers, RespondFn } from "./types.js"; import { assertValidParams } from "./validation.js"; +const MAX_CONFIG_ISSUES_IN_ERROR_MESSAGE = 3; + function requireConfigBaseHash( params: unknown, snapshot: Awaited>, @@ -158,7 +161,7 @@ function parseValidateConfigFromRawOrRespond( respond( false, undefined, - errorShape(ErrorCodes.INVALID_REQUEST, "invalid config", { + errorShape(ErrorCodes.INVALID_REQUEST, summarizeConfigValidationIssues(validated.issues), { details: { issues: validated.issues }, }), ); @@ -167,6 +170,20 @@ function parseValidateConfigFromRawOrRespond( return { config: validated.config, schema }; } +function summarizeConfigValidationIssues(issues: ReadonlyArray): string { + const trimmed = issues.slice(0, MAX_CONFIG_ISSUES_IN_ERROR_MESSAGE); + const lines = formatConfigIssueLines(trimmed, "", { normalizeRoot: true }) + .map((line) => line.trim()) + .filter(Boolean); + if (lines.length === 0) { + return "invalid config"; + } + const hiddenCount = Math.max(0, issues.length - lines.length); + return `invalid config: ${lines.join("; ")}${ + hiddenCount > 0 ? ` (+${hiddenCount} more issue${hiddenCount === 1 ? "" : "s"})` : "" + }`; +} + function resolveConfigRestartRequest(params: unknown): { sessionKey: string | undefined; note: string | undefined; @@ -398,7 +415,7 @@ export const configHandlers: GatewayRequestHandlers = { respond( false, undefined, - errorShape(ErrorCodes.INVALID_REQUEST, "invalid config", { + errorShape(ErrorCodes.INVALID_REQUEST, summarizeConfigValidationIssues(validated.issues), { details: { issues: validated.issues }, }), ); diff --git a/src/gateway/server.config-patch.test.ts b/src/gateway/server.config-patch.test.ts index 1f2d465b4da..67efe9b79be 100644 --- a/src/gateway/server.config-patch.test.ts +++ b/src/gateway/server.config-patch.test.ts @@ -72,6 +72,38 @@ describe("gateway config methods", () => { expect(res.payload?.config).toBeTruthy(); }); + it("returns config.set validation details in the top-level error message", async () => { + const current = await rpcReq<{ + hash?: string; + }>(requireWs(), "config.get", {}); + expect(current.ok).toBe(true); + expect(typeof current.payload?.hash).toBe("string"); + + const res = await rpcReq<{ + ok?: boolean; + error?: { + message?: string; + }; + }>(requireWs(), "config.set", { + raw: JSON.stringify({ gateway: { bind: 123 } }), + baseHash: current.payload?.hash, + }); + const error = res.error as + | { + message?: string; + details?: { + issues?: Array<{ path?: string; message?: string }>; + }; + } + | undefined; + + expect(res.ok).toBe(false); + expect(error?.message ?? "").toContain("invalid config:"); + expect(error?.message ?? "").toContain("gateway.bind"); + expect(error?.message ?? "").toContain("allowed:"); + expect(error?.details?.issues?.[0]?.path).toBe("gateway.bind"); + }); + it("returns a path-scoped config schema lookup", async () => { const res = await rpcReq<{ path: string;