Audit metadata
- Priority: P1
- Estimated effort: S
- Implementation risk: LOW
- Category: security / perf
- Evidence baseline:
07be5be7ce69bea0c3118744ab90d148b010fce0 (origin/main on 2026-07-17)
Dependencies
- P1 proposal 039 — Reapprove changed plugin artifacts and minimize MCP child environments. Included in the current P1 issue wave.
Description and impact
A faulty or malicious stdio MCP server can write indefinitely without a newline. The frontend concatenates every chunk into an unbounded string, allowing memory exhaustion and potentially quadratic copying. Bound frames and terminate protocol-invalid connections while rejecting every pending call promptly.
Existing issue overlap: #200 covers Windows .cmd spawning and child cleanup, not protocol framing or buffer limits.
Current state and reproduction
services/agent-runtime/src/mcp-client.ts:62-82 caps stderr at 2,000 characters but leaves buffer unbounded. Lines 96-115 append until newline and swallow parse/schema errors:
this.buffer += chunk.toString("utf8");
let newline = this.buffer.indexOf("\n");
Configure a stdio connector whose process continuously writes bytes without \n; initialization never completes and memory grows with stdout.
Solution design
Use a byte-based newline framer with a documented 4 MiB maximum frame and leftover buffer. Preserve split UTF-8 and multiple frames per chunk without repeated whole-string concatenation. On overflow or malformed JSON/schema, raise a typed MCP protocol error, clear the buffer, reject and clear all pending requests/timers, and terminate the child once. Normal JSON-RPC notifications remain accepted. Add no source comments.
Verification commands
| Purpose |
Command |
Expected on success |
| MCP tests |
cd frontend && bun test ../tests/frontend/agent-runtime/mcp-client.test.ts |
exit 0 |
| Runtime build |
npm --prefix services/agent-runtime run build |
exit 0 |
| Frontend quality |
npm --prefix frontend run check:quality |
exit 0 |
| Full integration |
npm run test:integration |
exit 0 |
Scope
In scope: mcp-client.ts; an extracted stdio-json-line-framer.ts if it keeps framing pure; a focused test; and a synthetic child fixture only if end-to-end close behavior needs it.
Out of scope: connector approval/permissions, Windows command resolution (#200), HTTP MCP transport, JSON-RPC feature additions, stderr retention, and changing the request timeout.
Implementation plan
Step 1: Characterize framing and overflow
Test one frame split across chunks/UTF-8 boundaries, multiple frames in one chunk, empty lines, a frame exactly at the limit, limit plus one byte, endless no-newline output, malformed JSON, schema-invalid JSON, and child close with pending requests.
Verify: the overflow case grows/remains pending on baseline and fails the new expectation.
Step 2: Implement a bounded byte framer
Accumulate bytes only up to the first newline/limit, emit complete frame bytes, retain a bounded remainder, and decode UTF-8 only for a complete frame. Avoid exported test-only APIs; keep the framer a real production abstraction used by the connection.
Verify: pure framing tests pass with buffer size never above 4 MiB.
Step 3: Close the connection on protocol failure
Centralize terminal failure so overflow/malformed frames, spawn close, and explicit close settle once, reject all pending promises, clear timers and buffers, and terminate the child. Do not wait for the normal 60-second timeout.
Verify: end-to-end fixture test observes child termination and immediate typed rejection.
Step 4: Verify build and desktop bundle
Run all gates, build desktop:dist, perform the canonical AGENTS.md reinstall, relaunch, and verify desktop-health returns HTTP 200.
Verify: runtime/frontend/integration gates and packaged health all succeed.
Test plan
Prefer a pure framer test plus one synthetic child-process integration. Assert byte—not character—limits, split UTF-8 correctness, all pending timers cleared, no double rejection, and normal initialize/list/call traffic still works.
Acceptance criteria
Risks and stop conditions
- A supported MCP server demonstrably emits valid frames larger than 4 MiB; report measured sizes and choose a documented cap.
- Framing is not newline-delimited for a supported stdio transport.
- The change requires connector permission or spawn portability work.
- Source drift or two verification failures occur.
Maintenance considerations
Keep stdout and decoded JSON limits explicit if binary/content payloads grow. Reviewers should inspect timer rejection and child-close idempotence, not only buffer arithmetic.
Generated from Local Studio 2.0 main-branch audit proposal 045.
Audit metadata
07be5be7ce69bea0c3118744ab90d148b010fce0(origin/mainon 2026-07-17)Dependencies
Description and impact
A faulty or malicious stdio MCP server can write indefinitely without a newline. The frontend concatenates every chunk into an unbounded string, allowing memory exhaustion and potentially quadratic copying. Bound frames and terminate protocol-invalid connections while rejecting every pending call promptly.
Existing issue overlap: #200 covers Windows
.cmdspawning and child cleanup, not protocol framing or buffer limits.Current state and reproduction
services/agent-runtime/src/mcp-client.ts:62-82caps stderr at 2,000 characters but leavesbufferunbounded. Lines 96-115 append until newline and swallow parse/schema errors:Configure a stdio connector whose process continuously writes bytes without
\n; initialization never completes and memory grows with stdout.Solution design
Use a byte-based newline framer with a documented 4 MiB maximum frame and leftover buffer. Preserve split UTF-8 and multiple frames per chunk without repeated whole-string concatenation. On overflow or malformed JSON/schema, raise a typed MCP protocol error, clear the buffer, reject and clear all pending requests/timers, and terminate the child once. Normal JSON-RPC notifications remain accepted. Add no source comments.
Verification commands
cd frontend && bun test ../tests/frontend/agent-runtime/mcp-client.test.tsnpm --prefix services/agent-runtime run buildnpm --prefix frontend run check:qualitynpm run test:integrationScope
In scope:
mcp-client.ts; an extractedstdio-json-line-framer.tsif it keeps framing pure; a focused test; and a synthetic child fixture only if end-to-end close behavior needs it.Out of scope: connector approval/permissions, Windows command resolution (#200), HTTP MCP transport, JSON-RPC feature additions, stderr retention, and changing the request timeout.
Implementation plan
Step 1: Characterize framing and overflow
Test one frame split across chunks/UTF-8 boundaries, multiple frames in one chunk, empty lines, a frame exactly at the limit, limit plus one byte, endless no-newline output, malformed JSON, schema-invalid JSON, and child close with pending requests.
Verify: the overflow case grows/remains pending on baseline and fails the new expectation.
Step 2: Implement a bounded byte framer
Accumulate bytes only up to the first newline/limit, emit complete frame bytes, retain a bounded remainder, and decode UTF-8 only for a complete frame. Avoid exported test-only APIs; keep the framer a real production abstraction used by the connection.
Verify: pure framing tests pass with buffer size never above 4 MiB.
Step 3: Close the connection on protocol failure
Centralize terminal failure so overflow/malformed frames, spawn close, and explicit close settle once, reject all pending promises, clear timers and buffers, and terminate the child. Do not wait for the normal 60-second timeout.
Verify: end-to-end fixture test observes child termination and immediate typed rejection.
Step 4: Verify build and desktop bundle
Run all gates, build
desktop:dist, perform the canonical AGENTS.md reinstall, relaunch, and verify desktop-health returns HTTP 200.Verify: runtime/frontend/integration gates and packaged health all succeed.
Test plan
Prefer a pure framer test plus one synthetic child-process integration. Assert byte—not character—limits, split UTF-8 correctness, all pending timers cleared, no double rejection, and normal initialize/list/call traffic still works.
Acceptance criteria
npm run check, andnpm run test:integrationexit 0.Risks and stop conditions
Maintenance considerations
Keep stdout and decoded JSON limits explicit if binary/content payloads grow. Reviewers should inspect timer rejection and child-close idempotence, not only buffer arithmetic.
Generated from Local Studio 2.0 main-branch audit proposal 045.