Files
PaperClipAI/packages
b070181aba fix(adapter-utils): serialize duplex gateway stdout writes and fix reserve release order
The Stage-2 security re-review found two remaining defects in the
in-sandbox duplex gateway.

First, the gateway wrote every frame to process.stdout and ignored the
boolean write() returns. A writable pipe returns false at its
high-water mark, and Node then queues every later write in the
stream's own buffer. A slow host reader let one admitted read retain
its whole body's worth of encoded frames, not the one-frame margin the
retained-peak comment claimed. Fix: route every outbound frame -- the
heartbeat, the READY frame, the request envelope, and each body_chunk
-- through one serialized writer. The writer issues at most one write
at a time and waits for the stream to drain (or to error) before it
starts the next queued line, so a slow reader can no longer make the
gateway queue more than about one frame per active send.

Second, readBody held the chunks array and the Buffer.concat result at
the same time, so the transient peak was 2x the raw body while
admission charged 1x. Fix: readBody now preallocates one buffer sized
to the admission charge and copies each chunk into it in place, then
returns a view over the written bytes. This removes the double
retention; the tradeoff (a small body now retains the full
charge-sized buffer for the life of the request) is documented in the
function comment.

Third, the admission reserve released inside a finally block on the
response promise, so a response timeout could release the reserve
while the outbound send still held the body. Fix: the reserve now
releases only after both the response settled and the send finished
(drained fully or ended by a stream error), tracked independently and
released exactly once through an idempotent release function. The
send now starts after the pending entry and its timer are registered,
so a response timeout no longer stops or races the send.

The retained-peak comment is rewritten to describe what the new code
actually holds: one body buffer, plus at most one in-flight frame
bounded by the frame size limit, times the admitted read count.

Tests: a pipe-backpressure regression test proves the writer never
queues more than about one bounded frame per active send under a
stalled pipe; two isolated writer tests (extracted the same way the
frame codec already is) prove the writer serializes writes and ends a
stalled write on a stream error; an ordering test proves one request's
chunks stay in sequence while a concurrent request interleaves its
own; two reserve tests prove the reserve stays held through a stalled
send and releases only once the send and the response both finish,
for both a resumed pipe and a broken one.

Kept unchanged: the 10 MiB + 1 body cap, the 64 MiB inflight bound,
the host spill behavior, and the observable 413 / 503 / 502 statuses.

Every existing test on the three touched files stays green (189 tests
total). tsc is clean.

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:42:07 +00:00
..