From d01015b243c7ea9c8cf2a9f6d783ae71f07dac71 Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Tue, 7 Jul 2026 17:43:45 +0800
Subject: [PATCH] Fix UTF-16-safe audit context truncation (#101298)
---
src/media-understanding/shared.test.ts | 17 +++++++++++++++++
src/media-understanding/shared.ts | 3 ++-
2 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/media-understanding/shared.test.ts b/src/media-understanding/shared.test.ts
index 92dad93d7f06..03d339dbe121 100644
--- a/src/media-understanding/shared.test.ts
+++ b/src/media-understanding/shared.test.ts
@@ -637,6 +637,23 @@ describe("fetchWithTimeoutGuarded", () => {
expect(call.timeoutMs).toBe(5000);
});
+ it("truncates auditContext without leaving a lone surrogate at the max boundary", async () => {
+ fetchWithSsrFGuardMock.mockResolvedValue({
+ response: new Response(null, { status: 200 }),
+ finalUrl: "https://example.com",
+ release: async () => {},
+ });
+
+ const prefix = "a".repeat(79);
+ await fetchWithTimeoutGuarded("https://example.com", {}, 5000, fetch, {
+ auditContext: `${prefix}${String.fromCodePoint(0x1f600)}tail`,
+ });
+
+ const call = getFirstGuardedFetchCall();
+ expect(call.auditContext).toBe(prefix);
+ expect(call.auditContext).not.toContain(String.fromCharCode(0xd83d));
+ });
+
it("passes configured explicit proxy policy through the SSRF guard", async () => {
fetchWithSsrFGuardMock.mockResolvedValue({
response: new Response(null, { status: 200 }),
diff --git a/src/media-understanding/shared.ts b/src/media-understanding/shared.ts
index dd37d956db09..19fff480f9e9 100644
--- a/src/media-understanding/shared.ts
+++ b/src/media-understanding/shared.ts
@@ -1,6 +1,7 @@
// Shared provider HTTP/audio helpers for media-understanding integrations,
// including guarded fetches, deadlines, retries, and multipart upload bodies.
import path from "node:path";
+import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import {
assertOkOrThrowHttpError,
createProviderHttpError,
@@ -321,7 +322,7 @@ function sanitizeAuditContext(auditContext: string | undefined): string | undefi
if (!cleaned) {
return undefined;
}
- return cleaned.slice(0, MAX_AUDIT_CONTEXT_CHARS);
+ return truncateUtf16Safe(cleaned, MAX_AUDIT_CONTEXT_CHARS);
}
type ResolvedProviderHttpRequestConfig = {