fix(module): harden executor with resource limits, async context, and concurrency safety#206
Conversation
…nd concurrency safety - Fix AsyncLocalStorage context loss through singleton RPC server by using per-execution snapshots via AsyncLocalStorage.snapshot() - Fix concurrent execute() calls overwriting shared fns/onReturn by introducing per-execution ExecutionContext keyed by execId - Add bounded RPC body reader (1MB default, HTTP 413) - Add wall-clock execution timeout (60s default, HTTP 408) - Add per-execution RPC call quota (200 default, HTTP 429) - Add per-tool response size limit (1MB default, truncation) - Sanitize error messages to strip file paths and stack traces - Add server-side logging to all catch blocks - Fix exec.returned set before callback completes - Replace void handleRpcRequest with .catch() safety net
|
@Mat4m0 is attempting to deploy a commit to the Nuxt Team on Vercel. A member of the Team first needs to authorize it. |
|
Thank you for following the naming conventions! 🙏 |
commit: |
|
A scope question for review: this PR currently bundles the fix for AsyncLocalStorage context restoration and per-execution RPC scoping together with some executor hardening in the same area (limits, timeout/quotas, response truncation, error sanitization/logging, and docs/tests). I kept it together because it all touches the same executor surface and I think this is the best end state for code mode. That said, if you’d prefer a narrower review, I can scope this PR down to just the correctness fix (ALS restoration + concurrent safety + focused tests) and move the hardening pieces into a follow-up. |
…ntext-and-concurrency
The problems
1. AsyncLocalStorage context was silently lost (correctness bug)
When a Nuxt middleware sets up per-request context (e.g.
event.context.user), that context lives in Node'sAsyncLocalStorage. But the singleton RPC server runs in its own async context — so when a sandbox tool call arrived, the dispatched function could no longer see the caller's store. This meant middleware-injected auth context, request-scoped databases, and similar patterns silently broke inside Code Mode.2. Concurrent executions corrupted each other (race condition)
The executor stored a single
fnsmap and a singleonReturncallback directly on the singletonRpcState. When twoexecute()calls ran concurrently, the second overwrote the first's function map and return callback. This could cause tool calls to dispatch to the wrong handler or deliver results to the wrong caller.3. Sandbox could DoS the host (resource exhaustion — 4 vectors)
for await (const chunk of req) body += chunkJSON.stringify(result)runs on full result before any limit appliescpuTimeLimitMsonly covers CPU in the isolate;while(true) await codemode.tool()yields between calls4. Error messages leaked internal details (information disclosure)
Infrastructure errors (file paths, stack traces) were returned verbatim to the sandbox and surfaced to the MCP client — exposing server internals.
5. Zero server-side logging (observability gap)
All 10 catch blocks in the executor swallowed errors silently. When something went wrong, there was no server-side trace to diagnose it.
Solution
Async context restoration
Each
execute()call now capturesAsyncLocalStorage.snapshot()at entry and stores it on theExecutionContext. The RPC handler callsexec.restoreContext(fn, args)before dispatching, re-entering the caller's async context. This requires Node.js >=18.16.0 (documented, with a clear error if unavailable).Per-execution isolation
Replaced the shared
fns/onReturnon the singleton with aMap<string, ExecutionContext>keyed by a randomexecId. Each execution gets its own frozen function map, return callback, deadline, and call counter. The sandbox sendsexecIdwith every RPC call; the server routes to the correct context. Cleanup happens in afinallyblock.####Resource limits (4 new configurable options)
maxRequestBodyBytesmaxToolResponseSizetruncateResult()wallTimeLimitMsmaxToolCalls__return__)All limits are configurable via
CodeModeOptionsand documented.Error sanitization
New
sanitizeErrorMessage()strips Unix/Windows file paths and stack trace lines, caps at 500 chars. Applied at both boundaries: RPC catch block andexecute()catch block. Full errors are logged server-side before sanitization.Logging
Every catch block now logs with
console.error(operational failures) orconsole.warn(best-effort/teardown failures), all prefixed with[nuxt-mcp-toolkit].Bug fixes
exec.returned = truemoved after the callback succeeds (was set before, causing inconsistent state on throw)void handleRpcRequest(...)replaced with.catch()that logs and sends 500Files changed
executor.tstypes.tsCodeModeOptionsfieldshandlers.tscodemode-executor.test.ts8.code-mode.md5.handlers.md2.installation.mdTest coverage
All changes are covered by 18 unit tests in
test/codemode-executor.test.ts:Concurrency & context (3 tests)
execute()calls dispatch to isolated function mapsAsyncLocalStoragecontext preserved through RPC dispatchHardening — new (8 tests)
execIdrejection (400)execIdrejection (400)Hardening — existing (7 tests)
AsyncLocalStorage.snapshot()unavailability fallbackAI Tools used