mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Gateway/Dashboard: surface config validation issues (#42664)
Merged via squash.
Prepared head SHA: 43f66cdcf0
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Reviewed-by: @huntharo
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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<ReturnType<typeof readConfigFileSnapshot>>,
|
||||
@@ -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<ConfigValidationIssue>): 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 },
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user