mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
fix(gateway): include redacted startup bundle errors
This commit is contained in:
@@ -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:<service>:<account>` `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.
|
||||
|
||||
@@ -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<string, unknown>, {
|
||||
@@ -284,7 +293,9 @@ describe("diagnostic stability bundles", () => {
|
||||
}
|
||||
expect(result.bundle.reason).toBe("unknown");
|
||||
expect(result.bundle.host).toEqual({ hostname: "<redacted-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",
|
||||
|
||||
@@ -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 = "<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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user