mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
fix: harden diagnostics support export privacy
This commit is contained in:
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
- TUI: add local embedded mode for running terminal chats without a Gateway while keeping plugin approval gates enforced. (#66767) Thanks @fuller-stack-dev.
|
||||
- CLI/Claude: default `claude-cli` runs to warm stdio sessions, including custom configs that omit transport fields, and resume from the stored Claude session after Gateway restarts or idle exits. (#69679) Thanks @obviyus.
|
||||
- Control UI/settings+chat: add a browser-local personal identity for the operator (name plus local-safe avatar), route user identity rendering through the shared chat/avatar path used by assistant and agent surfaces, and tighten Quick Settings, agent fallback chips, and narrow-screen chat layouts so personalization no longer wastes space or clips controls. (#70362) Thanks @BunsDev.
|
||||
- Gateway/diagnostics: add a support-ready diagnostics export with sanitized logs, status, health, config, and stability snapshots for bug reports. (#70324) Thanks @gumadeiras.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -195,7 +195,6 @@ function formatStabilityEvent(record: DiagnosticStabilityEventRecord): string {
|
||||
record.surface ? `surface=${record.surface}` : "",
|
||||
record.channel ? `channel=${record.channel}` : "",
|
||||
record.pluginId ? `plugin=${record.pluginId}` : "",
|
||||
record.sessionId ? `session=${record.sessionId}` : "",
|
||||
record.reason ? `reason=${record.reason}` : "",
|
||||
record.bytes !== undefined ? `bytes=${formatBytes(record.bytes)}` : "",
|
||||
record.limitBytes !== undefined ? `limit=${formatBytes(record.limitBytes)}` : "",
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
resetDiagnosticStabilityRecorderForTest,
|
||||
startDiagnosticStabilityRecorder,
|
||||
stopDiagnosticStabilityRecorder,
|
||||
type DiagnosticStabilityEventRecord,
|
||||
} from "../logging/diagnostic-stability.js";
|
||||
|
||||
const MB = 1024 * 1024;
|
||||
@@ -95,18 +94,6 @@ function emitSyntheticGatewayStabilityLoad(): number {
|
||||
return maxRssBytes;
|
||||
}
|
||||
|
||||
function latestSessionStates(
|
||||
events: DiagnosticStabilityEventRecord[],
|
||||
): Map<string, DiagnosticStabilityEventRecord> {
|
||||
const latest = new Map<string, DiagnosticStabilityEventRecord>();
|
||||
for (const event of events) {
|
||||
if (event.type === "session.state" && event.sessionKey) {
|
||||
latest.set(event.sessionKey, event);
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
describe("gateway stability lane", () => {
|
||||
beforeEach(() => {
|
||||
resetDiagnosticEventsForTest();
|
||||
@@ -145,13 +132,16 @@ describe("gateway stability lane", () => {
|
||||
expect(snapshot.summary.payloadLarge?.chunked).toBeGreaterThan(0);
|
||||
expect(snapshot.summary.payloadLarge?.bySurface["gateway.stability.probe"]).toBeGreaterThan(0);
|
||||
|
||||
const latestStates = latestSessionStates(snapshot.events);
|
||||
expect(latestStates.size).toBe(SYNTHETIC_SESSION_COUNT);
|
||||
for (const event of latestStates.values()) {
|
||||
expect(event.outcome).toBe("idle");
|
||||
expect(event.queueDepth).toBe(0);
|
||||
expect(event.reason).toBe(STABILITY_REASON);
|
||||
const sessionEvents = snapshot.events.filter((event) => event.type === "session.state");
|
||||
expect(sessionEvents.length).toBeGreaterThan(0);
|
||||
for (const event of sessionEvents) {
|
||||
expect(event).not.toHaveProperty("sessionId");
|
||||
expect(event).not.toHaveProperty("sessionKey");
|
||||
}
|
||||
expect(sessionEvents.some((event) => event.outcome === "idle" && event.queueDepth === 0)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(sessionEvents.every((event) => event.reason === STABILITY_REASON)).toBe(true);
|
||||
|
||||
stopDiagnosticStabilityRecorder();
|
||||
emitDiagnosticEvent({
|
||||
|
||||
@@ -64,6 +64,8 @@ describe("diagnostic stability recorder", () => {
|
||||
count: 3,
|
||||
});
|
||||
expect(snapshot.events[1]).not.toHaveProperty("message");
|
||||
expect(snapshot.events[1]).not.toHaveProperty("sessionId");
|
||||
expect(snapshot.events[1]).not.toHaveProperty("sessionKey");
|
||||
});
|
||||
|
||||
it("keeps stable reason codes but drops free-form reason text", () => {
|
||||
|
||||
@@ -14,8 +14,6 @@ export type DiagnosticStabilityEventRecord = {
|
||||
seq: number;
|
||||
ts: number;
|
||||
type: DiagnosticEventPayload["type"];
|
||||
sessionKey?: string;
|
||||
sessionId?: string;
|
||||
channel?: string;
|
||||
pluginId?: string;
|
||||
source?: string;
|
||||
@@ -172,8 +170,6 @@ function sanitizeDiagnosticEvent(event: DiagnosticEventPayload): DiagnosticStabi
|
||||
|
||||
switch (event.type) {
|
||||
case "model.usage":
|
||||
record.sessionKey = event.sessionKey;
|
||||
record.sessionId = event.sessionId;
|
||||
record.channel = event.channel;
|
||||
record.provider = event.provider;
|
||||
record.model = event.model;
|
||||
@@ -193,30 +189,22 @@ function sanitizeDiagnosticEvent(event: DiagnosticEventPayload): DiagnosticStabi
|
||||
record.channel = event.channel;
|
||||
break;
|
||||
case "message.queued":
|
||||
record.sessionKey = event.sessionKey;
|
||||
record.sessionId = event.sessionId;
|
||||
record.channel = event.channel;
|
||||
record.source = event.source;
|
||||
record.queueDepth = event.queueDepth;
|
||||
break;
|
||||
case "message.processed":
|
||||
record.sessionKey = event.sessionKey;
|
||||
record.sessionId = event.sessionId;
|
||||
record.channel = event.channel;
|
||||
record.durationMs = event.durationMs;
|
||||
record.outcome = event.outcome;
|
||||
assignReasonCode(record, event.reason);
|
||||
break;
|
||||
case "session.state":
|
||||
record.sessionKey = event.sessionKey;
|
||||
record.sessionId = event.sessionId;
|
||||
record.outcome = event.state;
|
||||
assignReasonCode(record, event.reason);
|
||||
record.queueDepth = event.queueDepth;
|
||||
break;
|
||||
case "session.stuck":
|
||||
record.sessionKey = event.sessionKey;
|
||||
record.sessionId = event.sessionId;
|
||||
record.outcome = event.state;
|
||||
record.ageMs = event.ageMs;
|
||||
record.queueDepth = event.queueDepth;
|
||||
@@ -231,8 +219,6 @@ function sanitizeDiagnosticEvent(event: DiagnosticEventPayload): DiagnosticStabi
|
||||
record.waitMs = event.waitMs;
|
||||
break;
|
||||
case "run.attempt":
|
||||
record.sessionKey = event.sessionKey;
|
||||
record.sessionId = event.sessionId;
|
||||
record.count = event.attempt;
|
||||
break;
|
||||
case "diagnostic.heartbeat":
|
||||
@@ -242,8 +228,6 @@ function sanitizeDiagnosticEvent(event: DiagnosticEventPayload): DiagnosticStabi
|
||||
record.queued = event.queued;
|
||||
break;
|
||||
case "tool.loop":
|
||||
record.sessionKey = event.sessionKey;
|
||||
record.sessionId = event.sessionId;
|
||||
record.toolName = event.toolName;
|
||||
record.level = event.level;
|
||||
record.action = event.action;
|
||||
|
||||
@@ -66,6 +66,9 @@ describe("diagnostic support export", () => {
|
||||
token: fakeToken,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
redactSensitive: "off",
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
@@ -319,6 +322,9 @@ describe("diagnostic support export", () => {
|
||||
accounts?: Record<string, { botToken?: string; allowFrom?: { redacted?: boolean } }>;
|
||||
};
|
||||
};
|
||||
logging?: {
|
||||
redactSensitive?: string;
|
||||
};
|
||||
agents?: Array<{ name?: string; instructions?: string }>;
|
||||
};
|
||||
expect(sanitizedConfig.gateway).toMatchObject({
|
||||
@@ -329,6 +335,9 @@ describe("diagnostic support export", () => {
|
||||
token: "<redacted>",
|
||||
},
|
||||
});
|
||||
expect(sanitizedConfig.logging).toMatchObject({
|
||||
redactSensitive: "off",
|
||||
});
|
||||
expect(Object.keys(sanitizedConfig.channels?.telegram?.accounts ?? {})).toEqual([
|
||||
"<redacted-account-1>",
|
||||
]);
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
type SupportRedactionContext,
|
||||
} from "./diagnostic-support-redaction.js";
|
||||
import { readConfiguredLogTail, type LogTailPayload } from "./log-tail.js";
|
||||
import { redactSensitiveText } from "./redact.js";
|
||||
|
||||
export const DIAGNOSTIC_SUPPORT_EXPORT_VERSION = 1;
|
||||
|
||||
@@ -209,7 +208,7 @@ function safeScalar(value: unknown): unknown {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const redacted = redactSensitiveText(value);
|
||||
const redacted = redactTextForSupport(value);
|
||||
return redacted === value && /^[A-Za-z0-9_.:-]{1,120}$/u.test(value) ? value : "<redacted>";
|
||||
}
|
||||
return undefined;
|
||||
@@ -287,7 +286,7 @@ function configShapeReadFailure(params: {
|
||||
shape.mtime = params.stat.mtime.toISOString();
|
||||
}
|
||||
if (params.error) {
|
||||
shape.error = redactSensitiveText(params.error);
|
||||
shape.error = redactTextForSupport(params.error);
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export function redactPathForSupport(file: string, options: SupportRedactionCont
|
||||
return `${label}${next.slice(prefix.length)}`;
|
||||
}
|
||||
}
|
||||
return redactSensitiveText(next);
|
||||
return redactSensitiveTextForSupport(next);
|
||||
}
|
||||
|
||||
function redactKnownPathPrefixesForSupport(
|
||||
@@ -114,13 +114,17 @@ function redactKnownPathPrefixesForSupport(
|
||||
}
|
||||
|
||||
export function redactTextForSupport(value: string): string {
|
||||
let redacted = redactSensitiveText(value);
|
||||
let redacted = redactSensitiveTextForSupport(value);
|
||||
redacted = redactUrlSecretsForSupport(redacted);
|
||||
redacted = redactServiceIdentifiersForSupport(redacted);
|
||||
redacted = redactContactIdentifiersForSupport(redacted);
|
||||
return redactLongIdentifiersForSupport(redacted);
|
||||
}
|
||||
|
||||
function redactSensitiveTextForSupport(value: string): string {
|
||||
return redactSensitiveText(value, { mode: "tools" });
|
||||
}
|
||||
|
||||
function redactUrlSecretsForSupport(value: string): string {
|
||||
return value
|
||||
.replace(URL_USERINFO_RE, "$1<redacted>:<redacted>@")
|
||||
|
||||
@@ -146,9 +146,12 @@ describe("stuck session diagnostics threshold", () => {
|
||||
expect(getDiagnosticStabilitySnapshot({ limit: 10 }).events).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: "session.state",
|
||||
sessionId: "s1",
|
||||
outcome: "processing",
|
||||
}),
|
||||
);
|
||||
const [event] = getDiagnosticStabilitySnapshot({ limit: 10 }).events;
|
||||
expect(event).not.toHaveProperty("sessionId");
|
||||
expect(event).not.toHaveProperty("sessionKey");
|
||||
|
||||
resetDiagnosticStateForTest();
|
||||
emitDiagnosticEvent({ type: "webhook.received", channel: "telegram" });
|
||||
|
||||
Reference in New Issue
Block a user