mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Agents: harden embedded error observations
This commit is contained in:
@@ -3,6 +3,7 @@ import * as loggingConfigModule from "../logging/config.js";
|
||||
import {
|
||||
buildApiErrorObservationFields,
|
||||
buildTextObservationFields,
|
||||
sanitizeForConsole,
|
||||
} from "./pi-embedded-error-observation.js";
|
||||
|
||||
afterEach(() => {
|
||||
@@ -110,6 +111,16 @@ describe("buildApiErrorObservationFields", () => {
|
||||
expect(observed.providerErrorMessagePreview?.endsWith("…")).toBe(true);
|
||||
});
|
||||
|
||||
it("caps oversized raw inputs before hashing and fingerprinting", () => {
|
||||
const oversized = "X".repeat(70_000);
|
||||
const bounded = "X".repeat(64_000);
|
||||
|
||||
expect(buildApiErrorObservationFields(oversized)).toMatchObject({
|
||||
rawErrorHash: buildApiErrorObservationFields(bounded).rawErrorHash,
|
||||
rawErrorFingerprint: buildApiErrorObservationFields(bounded).rawErrorFingerprint,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns empty observation fields for empty input", () => {
|
||||
expect(buildApiErrorObservationFields(undefined)).toEqual({});
|
||||
expect(buildApiErrorObservationFields("")).toEqual({});
|
||||
@@ -163,3 +174,9 @@ describe("buildApiErrorObservationFields", () => {
|
||||
expect(observed.rawErrorPreview).toContain("custom");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitizeForConsole", () => {
|
||||
it("strips control characters from console-facing values", () => {
|
||||
expect(sanitizeForConsole("run-1\nprovider\tmodel\rtest")).toBe("run-1 provider model test");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ import { getDefaultRedactPatterns, redactSensitiveText } from "../logging/redact
|
||||
import { getApiErrorPayloadFingerprint, parseApiErrorInfo } from "./pi-embedded-helpers.js";
|
||||
import { stableStringify } from "./stable-stringify.js";
|
||||
|
||||
const MAX_OBSERVATION_INPUT_CHARS = 64_000;
|
||||
const MAX_FINGERPRINT_MESSAGE_CHARS = 8_000;
|
||||
const RAW_ERROR_PREVIEW_MAX_CHARS = 400;
|
||||
const PROVIDER_ERROR_PREVIEW_MAX_CHARS = 200;
|
||||
const REQUEST_ID_RE = /\brequest[_ ]?id\b\s*[:=]\s*["'()]*([A-Za-z0-9._:-]+)/i;
|
||||
@@ -29,6 +31,40 @@ function truncateForObservation(text: string | undefined, maxChars: number): str
|
||||
return trimmed.length > maxChars ? `${trimmed.slice(0, maxChars)}…` : trimmed;
|
||||
}
|
||||
|
||||
function boundObservationInput(text: string | undefined): string | undefined {
|
||||
const trimmed = text?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return trimmed.length > MAX_OBSERVATION_INPUT_CHARS
|
||||
? trimmed.slice(0, MAX_OBSERVATION_INPUT_CHARS)
|
||||
: trimmed;
|
||||
}
|
||||
|
||||
export function sanitizeForConsole(text: string | undefined, maxChars = 200): string | undefined {
|
||||
const trimmed = text?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const withoutControlChars = Array.from(trimmed)
|
||||
.filter((char) => {
|
||||
const code = char.charCodeAt(0);
|
||||
return !(
|
||||
code <= 0x08 ||
|
||||
code === 0x0b ||
|
||||
code === 0x0c ||
|
||||
(code >= 0x0e && code <= 0x1f) ||
|
||||
code === 0x7f
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
const sanitized = withoutControlChars
|
||||
.replace(/[\r\n\t]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return sanitized.length > maxChars ? `${sanitized.slice(0, maxChars)}…` : sanitized;
|
||||
}
|
||||
|
||||
function replaceRequestIdPreview(
|
||||
text: string | undefined,
|
||||
requestId: string | undefined,
|
||||
@@ -71,12 +107,16 @@ function buildObservationFingerprint(params: {
|
||||
type?: string;
|
||||
message?: string;
|
||||
}): string | null {
|
||||
const boundedMessage =
|
||||
params.message && params.message.length > MAX_FINGERPRINT_MESSAGE_CHARS
|
||||
? params.message.slice(0, MAX_FINGERPRINT_MESSAGE_CHARS)
|
||||
: params.message;
|
||||
const structured =
|
||||
params.httpCode || params.type || params.message
|
||||
params.httpCode || params.type || boundedMessage
|
||||
? stableStringify({
|
||||
httpCode: params.httpCode,
|
||||
type: params.type,
|
||||
message: params.message,
|
||||
message: boundedMessage,
|
||||
})
|
||||
: null;
|
||||
if (structured) {
|
||||
@@ -97,7 +137,7 @@ export function buildApiErrorObservationFields(rawError?: string): {
|
||||
providerErrorMessagePreview?: string;
|
||||
requestIdHash?: string;
|
||||
} {
|
||||
const trimmed = rawError?.trim();
|
||||
const trimmed = boundObservationInput(rawError);
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { redactIdentifier } from "../../../logging/redact-identifier.js";
|
||||
import type { AuthProfileFailureReason } from "../../auth-profiles.js";
|
||||
import { buildApiErrorObservationFields } from "../../pi-embedded-error-observation.js";
|
||||
import {
|
||||
buildApiErrorObservationFields,
|
||||
sanitizeForConsole,
|
||||
} from "../../pi-embedded-error-observation.js";
|
||||
import type { FailoverReason } from "../../pi-embedded-helpers.js";
|
||||
import { log } from "../logger.js";
|
||||
|
||||
@@ -42,6 +45,9 @@ export function createFailoverDecisionLogger(
|
||||
const safeProfileId = normalizedBase.profileId
|
||||
? redactIdentifier(normalizedBase.profileId, { len: 12 })
|
||||
: undefined;
|
||||
const safeRunId = sanitizeForConsole(normalizedBase.runId) ?? "-";
|
||||
const safeProvider = sanitizeForConsole(normalizedBase.provider) ?? "-";
|
||||
const safeModel = sanitizeForConsole(normalizedBase.model) ?? "-";
|
||||
const profileText = safeProfileId ?? "-";
|
||||
const reasonText = normalizedBase.failoverReason ?? "none";
|
||||
return (decision, extra) => {
|
||||
@@ -63,8 +69,8 @@ export function createFailoverDecisionLogger(
|
||||
status: extra?.status,
|
||||
...observedError,
|
||||
consoleMessage:
|
||||
`embedded run failover decision: runId=${normalizedBase.runId ?? "-"} stage=${normalizedBase.stage} decision=${decision} ` +
|
||||
`reason=${reasonText} provider=${normalizedBase.provider}/${normalizedBase.model} profile=${profileText}`,
|
||||
`embedded run failover decision: runId=${safeRunId} stage=${normalizedBase.stage} decision=${decision} ` +
|
||||
`reason=${reasonText} provider=${safeProvider}/${safeModel} profile=${profileText}`,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("handleAgentEnd", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("redacts logged error text while keeping lifecycle events unchanged", () => {
|
||||
it("redacts logged error text before emitting lifecycle events", () => {
|
||||
const onAgentEvent = vi.fn();
|
||||
const ctx = createContext(
|
||||
{
|
||||
@@ -118,7 +118,7 @@ describe("handleAgentEnd", () => {
|
||||
stream: "lifecycle",
|
||||
data: {
|
||||
phase: "error",
|
||||
error: "x-api-key: sk-abcdefghijklmnopqrstuvwxyz123456",
|
||||
error: "x-api-key: ***",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createInlineCodeState } from "../markdown/code-spans.js";
|
||||
import {
|
||||
buildApiErrorObservationFields,
|
||||
buildTextObservationFields,
|
||||
sanitizeForConsole,
|
||||
} from "./pi-embedded-error-observation.js";
|
||||
import { classifyFailoverReason, formatAssistantErrorText } from "./pi-embedded-helpers.js";
|
||||
import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js";
|
||||
@@ -46,6 +47,7 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) {
|
||||
const observedError = buildApiErrorObservationFields(rawError);
|
||||
const safeErrorText =
|
||||
buildTextObservationFields(errorText).textPreview ?? "LLM request failed.";
|
||||
const safeRunId = sanitizeForConsole(ctx.params.runId) ?? "-";
|
||||
ctx.log.warn("embedded run agent end", {
|
||||
event: "embedded_run_agent_end",
|
||||
tags: ["error_handling", "lifecycle", "agent_end", "assistant_error"],
|
||||
@@ -56,14 +58,14 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) {
|
||||
provider: lastAssistant.provider,
|
||||
model: lastAssistant.model,
|
||||
...observedError,
|
||||
consoleMessage: `embedded run agent end: runId=${ctx.params.runId} isError=true error=${safeErrorText}`,
|
||||
consoleMessage: `embedded run agent end: runId=${safeRunId} isError=true error=${safeErrorText}`,
|
||||
});
|
||||
emitAgentEvent({
|
||||
runId: ctx.params.runId,
|
||||
stream: "lifecycle",
|
||||
data: {
|
||||
phase: "error",
|
||||
error: errorText,
|
||||
error: safeErrorText,
|
||||
endedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
@@ -71,7 +73,7 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) {
|
||||
stream: "lifecycle",
|
||||
data: {
|
||||
phase: "error",
|
||||
error: errorText,
|
||||
error: safeErrorText,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user