Skip to content

fix(stream): scan the whole new chunk, not just the window tail - #4

Merged
aymandakirgh merged 1 commit into
mainfrom
fix/stream-window-evasion
Jun 18, 2026
Merged

fix(stream): scan the whole new chunk, not just the window tail#4
aymandakirgh merged 1 commit into
mainfrom
fix/stream-window-evasion

Conversation

@aymandakirgh

Copy link
Copy Markdown
Owner

Why

createStreamGuard / guardTextStream scan only the last window (8192) chars of the accumulated buffer on each push(). The window is anchored to the tail of the buffer, so when a single push is larger than the window the leading bytes of that chunk are never scanned.

This is a realistic path, not a contrived one: the guard is documented to compose with "any async iterable of strings", so adapting a non-streaming completion into it yields the entire output as one chunk. A secret/PII near the start of a >8 KB answer then slips through.

Reproduced — identical content, opposite verdicts:

Delivery Verdict
"Contact alice@example.com " + 8300×"x" as one chunk blocked = false ❌ (PII leaks)
same text split into small chunks blocked = true

Detection depended on chunk size, not content.

What

Scan the last chunk.length + window chars: the full chunk just appended (leading bytes always inspected) plus window chars of prior context to preserve cross-chunk straddle detection (<scr + ipt>). Per-push cost stays O(window) for normal token-sized chunks and is O(window + chunk) only for an outsized single push — the cost of actually inspecting content you just emitted.

Verification

  • npm run typecheck ✅ · npm run lint ✅ · npm test307 passed / 1 skipped
  • 2 new regression tests: PII in a single >window chunk is blocked; verdict is chunk-size-independent for identical content.

The streaming guard scanned only the last `window` (8192) chars of the
accumulated buffer on every push. When a single push is larger than the
window — e.g. a non-streaming completion fed through `guardTextStream` as
one chunk, a documented "any async iterable of strings" use — the leading
bytes of that chunk were never scanned, so a secret or PII near the start
slipped through. Detection depended on chunk size, not content: identical
text was blocked when split into small chunks but passed as one big chunk
(reproduced).

Scan the last `chunk.length + window` chars instead: the full chunk just
appended (its leading bytes are always inspected) plus `window` chars of
prior context for cross-chunk straddles. Per-push cost stays O(window) for
normal token-sized chunks and is O(window + chunk) only for an outsized one.

Adds 2 regression tests: PII in a single >window chunk is blocked, and the
verdict is independent of chunk size for identical content.

Co-authored-by: mattia-mamini-gh <281593356+mattia-mamini-gh@users.noreply.github.com>
@aymandakirgh
aymandakirgh merged commit 9504956 into main Jun 18, 2026
3 checks passed
@aymandakirgh
aymandakirgh deleted the fix/stream-window-evasion branch June 18, 2026 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant