Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/15507-pii-snapshots-warn-bytes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- fix(sse): PII transform in warn/off mode now passes Responses text deltas through byte-identical (no buffering/re-chunking); redact snapshot sanitization guarded by test (#15507)
5 changes: 5 additions & 0 deletions src/lib/piiSanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ const getMode = (): PiiMode => {
return "redact";
};

/** Current response-sanitization mode (redact | warn | block | off). */
export function getPiiResponseMode(): PiiMode {
return getMode();
}

// ── PII Patterns ──

interface PIIPattern {
Expand Down
9 changes: 8 additions & 1 deletion src/lib/streamingPiiTransform.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createSseTextTransform, FieldCategory, classifyField } from "./sseTextTransform";
import { sanitizePII } from "./piiSanitizer";
import { sanitizePII, getPiiResponseMode } from "./piiSanitizer";

export interface PiiTransformOptions {
windowSize?: number;
Expand Down Expand Up @@ -42,6 +42,13 @@ export function createPiiSseTransform(options?: PiiTransformOptions): TransformS
if (field === "toolArgs" || field === "partialJson") {
return text;
}
// warn/off never mutate the payload: detect-only, emit the original bytes immediately
// (no sliding-window buffering/re-chunking) so the stream stays byte-identical (#15507).
const mode = getPiiResponseMode();
if (mode === "warn" || mode === "off") {
if (mode === "warn") sanitizePII(text);
return text;
}
if (isSnapshot) {
return sanitizePII(text).text;
}
Expand Down
89 changes: 89 additions & 0 deletions tests/unit/issue-15507-pii-responses-text-snapshots.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-issue-15507-b-"));
process.env.DATA_DIR = tmpDir;
process.env.PII_RESPONSE_SANITIZATION = "true";

import { createPiiSseTransform } from "../../src/lib/streamingPiiTransform.ts";

async function run(chunks: string[]): Promise<string> {
const t = createPiiSseTransform();
const writer = t.writable.getWriter();
const reader = t.readable.getReader();
const w = (async () => {
for (const c of chunks) await writer.write(new TextEncoder().encode(c));
await writer.close();
})();
const out: string[] = [];
let r = await reader.read();
while (!r.done) {
out.push(new TextDecoder().decode(r.value));
r = await reader.read();
}
await w;
return out.join("");
}

const EMAIL = "jane.doe@example.com";
const parts = ["Contact me at ", EMAIL, " please. ", "x".repeat(300), " bye"];
const full = parts.join("");

function textStream(): string[] {
const evs: Array<Record<string, unknown>> = parts.map((p, i) => ({
type: "response.output_text.delta",
sequence_number: i,
item_id: "msg_1",
output_index: 0,
content_index: 0,
delta: p,
}));
evs.push({
type: "response.output_text.done",
sequence_number: 10,
item_id: "msg_1",
output_index: 0,
content_index: 0,
text: full,
});
evs.push({
type: "response.completed",
sequence_number: 11,
response: {
id: "resp_1",
status: "completed",
output: [
{
type: "message",
id: "msg_1",
role: "assistant",
content: [{ type: "output_text", text: full }],
},
],
},
});
return evs.map((e) => `data: ${JSON.stringify(e)}\n\n`);
}

test("#15507: warn mode passes Responses text deltas through byte-identical", async () => {
process.env.PII_RESPONSE_SANITIZATION_MODE = "warn";
const input = textStream();
assert.equal(await run(input), input.join(""));
});

test("#15507: redact mode sanitizes the response.completed text snapshot", async () => {
process.env.PII_RESPONSE_SANITIZATION_MODE = "redact";
const out = await run(textStream());
assert.ok(!out.includes(EMAIL), "email must not leak anywhere in the stream");
const completed = out
.split("\n")
.filter((l) => l.startsWith("data: "))
.map((l) => JSON.parse(l.slice(6)))
.find((e) => e.type === "response.completed");
assert.ok(completed.response.output[0].content[0].text.includes("[EMAIL_REDACTED]"));
const dones = out.match(/"type":"response.completed"/g) || [];
assert.equal(dones.length, 1);
});
Loading