mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
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>