fix(agents): return schema lookup misses in-band

Return unknown config.schema.lookup paths as an in-band agent gateway tool result instead of throwing into channel warning surfaces.

The direct gateway RPC still reports INVALID_REQUEST, preserving the existing protocol contract, while the agent-facing gateway tool returns schema_path_not_found for exploratory misses.

Fixes #88813.
Thanks @ksj3421.
Reported by @cjalden.
This commit is contained in:
ksj3421
2026-06-01 11:10:02 +09:00
committed by GitHub
parent 91ca036717
commit 45bdaa2f7b
2 changed files with 50 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { GatewayClientRequestError } from "../gateway/client.js";
import { testing as restartTesting } from "../infra/restart.js";
import { withEnvAsync } from "../test-utils/env.js";
import { createGatewayTool } from "./tools/gateway-tool.js";
@@ -725,4 +726,29 @@ describe("gateway tool", () => {
?.schema;
expect(schema?.properties).toBeUndefined();
});
it("returns an in-band schema lookup miss for unknown paths", async () => {
vi.mocked(callGatewayTool).mockRejectedValueOnce(
new GatewayClientRequestError({
code: "INVALID_REQUEST",
message: "config schema path not found",
}),
);
const tool = requireGatewayTool();
const result = await tool.execute("call6", {
action: "config.schema.lookup",
path: "agents.main.authorizedSenders",
});
expect(gatewayCall("config.schema.lookup")[2]).toEqual({
path: "agents.main.authorizedSenders",
});
expect(result.details).toEqual({
ok: false,
code: "schema_path_not_found",
path: "agents.main.authorizedSenders",
message: "config schema path not found",
});
});
});

View File

@@ -10,6 +10,7 @@ import { parseConfigJson5, resolveConfigSnapshotHash } from "../../config/io.js"
import { applyMergePatch } from "../../config/merge-patch.js";
import { extractDeliveryInfo } from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { GatewayClientRequestError } from "../../gateway/client.js";
import {
buildRestartSuccessContinuation,
formatDoctorNonInteractiveHint,
@@ -33,6 +34,7 @@ import { callGatewayTool, readGatewayCallOptions } from "./gateway.js";
const log = createSubsystemLogger("gateway-tool");
const DEFAULT_UPDATE_TIMEOUT_MS = 20 * 60_000;
const CONFIG_SCHEMA_PATH_NOT_FOUND_MESSAGE = "config schema path not found";
// Per SECURITY.md the model/agent itself is not a trusted principal.
// `assertGatewayConfigMutationAllowed` is the explicit model -> operator
// trust-boundary control on `config.apply`/`config.patch`, so the runtime tool
@@ -114,6 +116,14 @@ function stripConfigWriteResultPayload(result: unknown): unknown {
return stripped;
}
function isConfigSchemaPathNotFoundError(error: unknown): boolean {
return (
error instanceof GatewayClientRequestError &&
error.gatewayCode === "INVALID_REQUEST" &&
error.message.includes(CONFIG_SCHEMA_PATH_NOT_FOUND_MESSAGE)
);
}
function parseGatewayConfigMutationRaw(
raw: string,
action: "config.apply" | "config.patch",
@@ -475,8 +485,20 @@ export function createGatewayTool(opts?: {
required: true,
label: "path",
});
const result = await callGatewayTool("config.schema.lookup", gatewayOpts, { path });
return jsonResult({ ok: true, result });
try {
const result = await callGatewayTool("config.schema.lookup", gatewayOpts, { path });
return jsonResult({ ok: true, result });
} catch (error) {
if (isConfigSchemaPathNotFoundError(error)) {
return jsonResult({
ok: false,
code: "schema_path_not_found",
path,
message: CONFIG_SCHEMA_PATH_NOT_FOUND_MESSAGE,
});
}
throw error;
}
}
if (action === "config.apply") {
const { raw, baseHash, snapshotConfig, sessionKey, note, restartDelayMs } =