diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e372d2a3dc..e27d431eeec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/diagnostics: include a bounded redacted startup error message in stability bundles, so crash-loop reports identify the failing plugin or contract without exposing secrets. Refs #75797. Thanks @ymebosma. - Control UI/Talk: allow the OpenAI Realtime WebRTC offer endpoint through the Control UI CSP, configure browser sessions with explicit VAD/transcription input settings, and surface OpenAI realtime error/lifecycle events instead of leaving Talk stuck as live with no diagnostic. Fixes #73427. - Providers/OpenAI: resolve `keychain::` `OPENAI_API_KEY` refs before creating OpenAI Realtime browser sessions or voice bridges, with a bounded cached Keychain lookup. Fixes #72120. Thanks @ctbritt. - Discord/gateway: reconnect when the gateway socket closes while waiting for the shared IDENTIFY concurrency window, instead of silently skipping IDENTIFY and leaving the bot online but unresponsive. Fixes #74617. Thanks @zeeskdr-ai. diff --git a/src/logging/diagnostic-stability-bundle.test.ts b/src/logging/diagnostic-stability-bundle.test.ts index 1453cba001a..97e59939ac3 100644 --- a/src/logging/diagnostic-stability-bundle.test.ts +++ b/src/logging/diagnostic-stability-bundle.test.ts @@ -88,7 +88,13 @@ describe("diagnostic stability bundles", () => { reason: "json_body_limit", }); - const error = Object.assign(new Error("contains secret message"), { code: "ERR_TEST" }); + const secret = "sk-1234567890abcdef"; + const error = Object.assign( + new Error( + `Startup failed: OPENAI_API_KEY=${secret} while opening google/web-search-contract-api.js`, + ), + { code: "ERR_TEST" }, + ); const result = writeDiagnosticStabilityBundleSync({ reason: "gateway.restart_startup_failed", error, @@ -122,9 +128,11 @@ describe("diagnostic stability bundles", () => { }); expect(bundle.snapshot.events[0]).not.toHaveProperty("chatId"); expect(bundle.snapshot.events[0]).not.toHaveProperty("error"); + expect(bundle.error?.message).toContain("google/web-search-contract-api.js"); + expect(bundle.error?.message).not.toContain(secret); expect(raw).not.toContain("chat-secret"); expect(raw).not.toContain("message body"); - expect(raw).not.toContain("contains secret message"); + expect(raw).not.toContain(secret); expect(raw).not.toContain(os.hostname()); }); @@ -158,13 +166,14 @@ describe("diagnostic stability bundles", () => { error: { name: "Error", code: "ERR_CONFIG_PARSE", + message: "raw startup config payload", }, snapshot: { count: 0, events: [], }, }); - expect(raw).not.toContain("raw startup config payload"); + expect(raw).not.toContain("stack"); }); it("registers a fatal hook only while installed", () => { @@ -242,7 +251,7 @@ describe("diagnostic stability bundles", () => { error: { name: "private error name", code: "ERR_TEST", - message: "error-message-secret", + message: "OPENAI_API_KEY=sk-1234567890abcdef", }, }); Object.assign(bundle.process as Record, { @@ -284,7 +293,9 @@ describe("diagnostic stability bundles", () => { } expect(result.bundle.reason).toBe("unknown"); expect(result.bundle.host).toEqual({ hostname: "" }); - expect(result.bundle.error).toEqual({ code: "ERR_TEST" }); + expect(result.bundle.error?.code).toBe("ERR_TEST"); + expect(result.bundle.error?.message).toContain("OPENAI_API_KEY="); + expect(result.bundle.error?.message).not.toContain("sk-1234567890abcdef"); expect(result.bundle.snapshot.events[0]).toEqual({ seq: 1, ts: 1, @@ -297,7 +308,7 @@ describe("diagnostic stability bundles", () => { "private reason", "top-level-secret", "private error name", - "error-message-secret", + "sk-1234567890abcdef", "process-command-secret", "private-hostname", "host-extra-secret", diff --git a/src/logging/diagnostic-stability-bundle.ts b/src/logging/diagnostic-stability-bundle.ts index 88c35343f43..b8b4984ccbf 100644 --- a/src/logging/diagnostic-stability-bundle.ts +++ b/src/logging/diagnostic-stability-bundle.ts @@ -8,6 +8,7 @@ import { MAX_DIAGNOSTIC_STABILITY_LIMIT, type DiagnosticStabilitySnapshot, } from "./diagnostic-stability.js"; +import { redactSensitiveText } from "./redact.js"; export const DIAGNOSTIC_STABILITY_BUNDLE_VERSION = 1; export const DEFAULT_DIAGNOSTIC_STABILITY_BUNDLE_LIMIT = MAX_DIAGNOSTIC_STABILITY_LIMIT; @@ -18,6 +19,7 @@ const SAFE_REASON_CODE = /^[A-Za-z0-9_.:-]{1,120}$/u; const BUNDLE_PREFIX = "openclaw-stability-"; const BUNDLE_SUFFIX = ".json"; const REDACTED_HOSTNAME = ""; +const MAX_SAFE_ERROR_MESSAGE_LENGTH = 500; export type DiagnosticStabilityBundle = { version: typeof DIAGNOSTIC_STABILITY_BUNDLE_VERSION; @@ -36,6 +38,7 @@ export type DiagnosticStabilityBundle = { error?: { name?: string; code?: string; + message?: string; }; snapshot: DiagnosticStabilitySnapshot; }; @@ -113,15 +116,34 @@ function readErrorName(error: unknown): string | undefined { return typeof name === "string" && SAFE_REASON_CODE.test(name) ? name : undefined; } +function readErrorMessage(error: unknown): string | undefined { + if (!error || typeof error !== "object" || !("message" in error)) { + return undefined; + } + const message = (error as { message?: unknown }).message; + if (typeof message !== "string") { + return undefined; + } + const sanitized = redactSensitiveText(message, { mode: "tools" }).replace(/\s+/gu, " ").trim(); + if (!sanitized) { + return undefined; + } + return sanitized.length > MAX_SAFE_ERROR_MESSAGE_LENGTH + ? `${sanitized.slice(0, MAX_SAFE_ERROR_MESSAGE_LENGTH)}...` + : sanitized; +} + function readSafeErrorMetadata(error: unknown): DiagnosticStabilityBundle["error"] | undefined { const name = readErrorName(error); const code = readErrorCode(error); - if (!name && !code) { + const message = readErrorMessage(error); + if (!name && !code && !message) { return undefined; } return { ...(name ? { name } : {}), ...(code ? { code } : {}), + ...(message ? { message } : {}), }; }