diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f4b9baf8ef..c2abfbb3978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/secrets: include the caught error message in `secrets.reload` and `secrets.resolve` warning logs while keeping RPC errors generic, so operators can diagnose reload and permission failures. Thanks @davidangularme. - fix(infra): block workspace state-directory env override [AI]. (#75940) Thanks @pgondhi987. - MCP/OpenAI: normalize parameter-free tool schemas whose top-level object `properties` is missing, null, or invalid before sending tools to OpenAI, so MCP tools without params stay usable. Fixes #75362. Thanks @tolkonepiu and @SymbolStar. - TTS: honor explicit short `[[tts:text]]...[[/tts:text]]` blocks while keeping untagged short auto-TTS suppressed, so tagged voice replies are synthesized instead of being dropped as empty voice-only payloads. Fixes #73758. Thanks @yfge. diff --git a/src/gateway/server-methods/secrets.test.ts b/src/gateway/server-methods/secrets.test.ts index 772725b1919..bee48252250 100644 --- a/src/gateway/server-methods/secrets.test.ts +++ b/src/gateway/server-methods/secrets.test.ts @@ -50,6 +50,7 @@ describe("secrets handlers", () => { diagnostics: string[]; inactiveRefPaths: string[]; }>; + log?: { warn?: (message: string) => void }; }) { const reloadSecrets = overrides?.reloadSecrets ?? (async () => ({ warningCount: 0 })); const resolveSecrets = @@ -62,6 +63,7 @@ describe("secrets handlers", () => { return createSecretsHandlers({ reloadSecrets, resolveSecrets, + log: overrides?.log, }); } @@ -75,8 +77,10 @@ describe("secrets handlers", () => { }); it("returns unavailable when reload fails", async () => { + const warn = vi.fn(); const handlers = createHandlers({ - reloadSecrets: vi.fn().mockRejectedValue(new Error("reload failed")), + reloadSecrets: vi.fn().mockRejectedValue(new Error("disk full")), + log: { warn }, }); const respond = vi.fn(); await invokeSecretsReload({ handlers, respond }); @@ -88,6 +92,7 @@ describe("secrets handlers", () => { message: "secrets.reload failed", }), ); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("disk full")); }); it("resolves requested command secret assignments from the active snapshot", async () => { @@ -189,12 +194,13 @@ describe("secrets handlers", () => { }); it("returns unavailable when secrets.resolve handler returns an invalid payload shape", async () => { + const warn = vi.fn(); const resolveSecrets = vi.fn().mockResolvedValue({ assignments: [{ path: TALK_TEST_PROVIDER_API_KEY_PATH, pathSegments: [""], value: "sk" }], diagnostics: [], inactiveRefPaths: [], }); - const handlers = createHandlers({ resolveSecrets }); + const handlers = createHandlers({ resolveSecrets, log: { warn } }); const respond = vi.fn(); await invokeSecretsResolve({ handlers, @@ -210,5 +216,32 @@ describe("secrets handlers", () => { message: "secrets.resolve failed", }), ); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("secrets.resolve returned invalid payload."), + ); + }); + + it("logs error details when secrets.resolve throws", async () => { + const warn = vi.fn(); + const handlers = createHandlers({ + resolveSecrets: vi.fn().mockRejectedValue(new Error("EACCES: permission denied")), + log: { warn }, + }); + const respond = vi.fn(); + await invokeSecretsResolve({ + handlers, + respond, + commandName: "memory status", + targetIds: ["talk.providers.*.apiKey"], + }); + expect(respond).toHaveBeenCalledWith( + false, + undefined, + expect.objectContaining({ + code: "UNAVAILABLE", + message: "secrets.resolve failed", + }), + ); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("EACCES: permission denied")); }); }); diff --git a/src/gateway/server-methods/secrets.ts b/src/gateway/server-methods/secrets.ts index af070e9d075..a7ac32491a1 100644 --- a/src/gateway/server-methods/secrets.ts +++ b/src/gateway/server-methods/secrets.ts @@ -8,6 +8,10 @@ import { } from "../protocol/index.js"; import type { GatewayRequestHandlers } from "./types.js"; +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + function invalidSecretsResolveField( errors: ErrorObject[] | null | undefined, ): "commandName" | "targetIds" { @@ -43,8 +47,8 @@ export function createSecretsHandlers(params: { try { const result = await params.reloadSecrets(); respond(true, { ok: true, warningCount: result.warningCount }); - } catch { - params.log?.warn?.("secrets.reload failed"); + } catch (error) { + params.log?.warn?.(`secrets.reload failed: ${errorMessage(error)}`); respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "secrets.reload failed")); } }, @@ -100,8 +104,8 @@ export function createSecretsHandlers(params: { throw new Error("secrets.resolve returned invalid payload."); } respond(true, payload); - } catch { - params.log?.warn?.("secrets.resolve failed"); + } catch (error) { + params.log?.warn?.(`secrets.resolve failed: ${errorMessage(error)}`); respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "secrets.resolve failed")); } },