diff --git a/src/server/live.ts b/src/server/live.ts index c430e0fdb..da81a0f0b 100644 --- a/src/server/live.ts +++ b/src/server/live.ts @@ -80,20 +80,10 @@ export const LIVE_CLIENT_PROTOCOL_HEADERS = [ * * When `OCX_LIVE_FRAME_LOG` is set to a file path, every relayed sideband frame appends one * JSONL record: direction, frame kind, byte length, and whether the payload contains U+FFFD. - * Privacy: full frame payloads are never written — only when U+FFFD is present, a short - * excerpt around the first replacement character is included so the corruption point can be - * attributed (upstream vs relay vs client). Disabled entirely when the env var is unset. + * Privacy: frame payloads are never written. The log is created with owner-only permissions and + * is disabled entirely when the env var is unset. */ export const LIVE_FRAME_LOG_ENV = "OCX_LIVE_FRAME_LOG"; -const LIVE_FRAME_LOG_CONTEXT_CHARS = 24; - -function fffdContext(text: string): string | undefined { - const idx = text.indexOf("\uFFFD"); - if (idx < 0) return undefined; - const start = Math.max(0, idx - LIVE_FRAME_LOG_CONTEXT_CHARS); - const end = Math.min(text.length, idx + LIVE_FRAME_LOG_CONTEXT_CHARS); - return text.slice(start, end); -} export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void { const logPath = process.env[LIVE_FRAME_LOG_ENV]; @@ -101,18 +91,18 @@ export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void { try { let kind: "text" | "binary" = "binary"; let bytes = 0; - let context: string | undefined; + let fffd = false; if (typeof data === "string") { kind = "text"; bytes = Buffer.byteLength(data); - context = fffdContext(data); + fffd = data.includes("\uFFFD"); } else if (data instanceof ArrayBuffer) { bytes = data.byteLength; - context = fffdContext(new TextDecoder().decode(new Uint8Array(data))); + fffd = new TextDecoder().decode(new Uint8Array(data)).includes("\uFFFD"); } else if (ArrayBuffer.isView(data)) { const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); bytes = data.byteLength; - context = fffdContext(new TextDecoder().decode(view)); + fffd = new TextDecoder().decode(view).includes("\uFFFD"); } else { return; } @@ -121,10 +111,9 @@ export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void { dir, kind, bytes, - fffd: context !== undefined, - ...(context !== undefined ? { context } : {}), + fffd, }; - appendFileSync(logPath, `${JSON.stringify(record)}\n`); + appendFileSync(logPath, `${JSON.stringify(record)}\n`, { mode: 0o600 }); } catch { // Frame forensics must never break the relay. } diff --git a/tests/server-live.test.ts b/tests/server-live.test.ts index 8197527fb..ca83e2167 100644 --- a/tests/server-live.test.ts +++ b/tests/server-live.test.ts @@ -3,7 +3,7 @@ * so the proxy must relay it to an OpenAI upstream instead of the /v1/* JSON-404 guard. */ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs"; import { join } from "node:path"; import { saveCodexAccountCredential } from "../src/codex/account-store"; import { clearAccountNeedsReauth, clearAccountQuota } from "../src/codex/auth-api"; @@ -760,7 +760,7 @@ test("sideband relay preserves multibyte UTF-8 frames byte-identically in both d // The env-gated frame forensic log (OCX_LIVE_FRAME_LOG) records per-frame metadata and // U+FFFD presence without writing full payloads — the attribution tool for multibyte // transcript corruption reports. -test("sideband frame log records direction, kind, and U+FFFD context without full payloads", async () => { +test("sideband frame log records metadata without payload content", async () => { const frameLogPath = join(TEST_DIR, "frames.jsonl"); process.env.OCX_LIVE_FRAME_LOG = frameLogPath; const FFFD_TEXT = "가볍게 ��기핼봐요"; @@ -839,13 +839,15 @@ test("sideband frame log records direction, kind, and U+FFFD context without ful expect(u2cFffd).toBeDefined(); expect(u2cFffd.kind).toBe("text"); expect(u2cFffd.bytes).toBeGreaterThan(0); - expect(u2cFffd.context).toContain("�"); + expect(u2cFffd).not.toHaveProperty("context"); expect(c2uClean).toBeDefined(); expect(c2uClean.fffd).toBe(false); - // Full payloads must never be logged — only short FFFD context excerpts. + // No payload content is logged, including frames containing U+FFFD. for (const line of lines) { expect(JSON.stringify(line)).not.toContain("clean-frame"); + expect(JSON.stringify(line)).not.toContain(FFFD_TEXT); } + if (process.platform !== "win32") expect(statSync(frameLogPath).mode & 0o777).toBe(0o600); client.close(); } finally {