Skip to content

feat(ai): extend the prompt injection defense to streamed suggestions - #668

Open
Reversean wants to merge 1 commit into
feat/ai-streamingfrom
fix/ai-stream-prompt-injection
Open

feat(ai): extend the prompt injection defense to streamed suggestions#668
Reversean wants to merge 1 commit into
feat/ai-streamingfrom
fix/ai-stream-prompt-injection

Conversation

@Reversean

@Reversean Reversean commented Jul 29, 2026

Copy link
Copy Markdown
Member

The one-shot path scans the finished answer before returning it. A stream cannot be checked
that way: it leaves the server while still being written, and a nonce split across two deltas
passes a per-chunk scan untouched.

Add a holdback guard. It withholds the last nonce.length − 1 characters and scans
withheld + new chunk on every delta, releasing only text that cannot be the start of the
nonce. The bound is exact — for the nonce to straddle the send boundary, more than
nonce.length − 1 characters after its first one must already be sent — so every occurrence
falls inside one scanned window and the nonce never reaches the client. Detection itself is
unchanged and shared with the one-shot path.

The guard runs in streamText's experimental_transform, on typed TextStreamPart chunks:
in the encoded SSE bytes the nonce is split by JSON envelopes and escaping, where no substring
search can find it. The withheld tail is released on text-end rather than in the
TransformStream's flush, so deltas stay inside the block they belong to.

stopStream is not used: it requires synthesising finish-step and finish chunks whose
shape is tied to the SDK version. Suppressing text instead keeps the stream well-formed, at
the cost of tokens the model goes on producing unheard — paid only when the tripwire fires.

On detection the guard emits the fallback message once and stays silent, logging eventId
and originalEventId. The prefix already sent cannot be retracted, which is acceptable: the
nonce is what triggered detection and is still withheld when it fires.

The transport takes a StreamGuard port declared in the domain layer and does not know what
a leak is.

@Reversean
Reversean force-pushed the fix/ai-stream-prompt-injection branch 2 times, most recently from a08550b to 18f7aa1 Compare July 29, 2026 16:21
@Reversean
Reversean force-pushed the fix/ai-stream-prompt-injection branch from 18f7aa1 to 571263c Compare July 29, 2026 17:22
@Reversean Reversean changed the title feat(ai): defend streamed Ask AI suggestions against prompt injection feat(ai): extend the prompt injection defense to streamed suggestions Jul 29, 2026
The one-shot path scans the finished answer before returning it. A streamed answer leaves the server while it is still being written, so the same scan cannot be applied: a nonce split across two deltas passes a per-delta check untouched and the marker reaches the client.

The stream now runs through a guard holding back the last nonce.length - 1 characters and scanning them together with each new delta, releasing only text that can no longer begin the nonce. That length is the exact minimum for an occurrence to always fall inside a single scanned window.

The guard is applied to the model's typed stream parts rather than the encoded SSE bytes, where JSON envelopes and escaping would split the nonce beyond the reach of a substring scan.

On detection the rest of the answer is replaced by the fallback message and the event ids are logged. The stream is not aborted: aborting obliges the caller to synthesize finish chunks whose shape follows the SDK version, and suppressing text keeps the stream well formed instead.

What counts as a leak stays in the domain layer behind a port, so the provider adapter still knows nothing about it.
@Reversean
Reversean force-pushed the fix/ai-stream-prompt-injection branch from 571263c to e64c1a7 Compare July 29, 2026 17:50
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