@@ -5,9 +5,14 @@ import { v2CopyFileItemsContract } from '@/lib/api/contracts/v2/file-copy'
55import { markCopilotRequest } from '@/lib/api/server/routes/copilot-request'
66import { matchV2Route } from '@/lib/api/server/routes/in-process-transport'
77import { V2_PARSE_DEFAULTS } from '@/lib/api/server/routes/v2-json-route'
8- import { parseRequest } from '@/lib/api/server/validation'
8+ import { DEFAULT_MAX_JSON_BODY_BYTES , parseRequest } from '@/lib/api/server/validation'
99import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope'
1010import { asOrchestrationError , statusForOrchestrationError } from '@/lib/core/orchestration/types'
11+ import {
12+ assertContentLengthWithinLimit ,
13+ isPayloadSizeLimitError ,
14+ readStreamToBufferWithLimit ,
15+ } from '@/lib/core/utils/stream-limits'
1116import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
1217import { createResourceEffectTransport } from '@/lib/mothership/agent-cli/resource-effects'
1318import { resolveInvocationWorkspace } from '@/lib/mothership/application/workspace-target'
@@ -28,9 +33,10 @@ const logger = createLogger('MothershipSandboxResourceTransport')
2833
2934/** Private callback observes the real authenticated v2 request without changing its body or API contract. */
3035export async function proxySandboxResourceRequest (
31- request : Request ,
36+ callback : Request ,
3237 token : string
3338) : Promise < Response > {
39+ let request = callback
3440 const url = new URL ( request . url )
3541 const prefix = `/api/mothership/sandbox/${ token } `
3642 const path = url . pathname . slice ( prefix . length )
@@ -52,11 +58,13 @@ export async function proxySandboxResourceRequest(
5258 * owner is refused there; workspace copies and invalid bodies keep the workspace route and its
5359 * v2 validation response.
5460 */
55- if (
56- path === v2CopyFileItemsContract . path &&
57- ( scope . fileOwnerProtocolVersion === 1 || ( await copiesBeyondWorkspaces ( request ) ) )
58- )
59- return proxySandboxFileCopyRequest ( request , token , scope )
61+ if ( path === v2CopyFileItemsContract . path ) {
62+ const buffered = await bufferCopyRequest ( request )
63+ if ( buffered instanceof Response ) return buffered
64+ request = buffered
65+ if ( scope . fileOwnerProtocolVersion === 1 || ( await copiesBeyondWorkspaces ( request ) ) )
66+ return proxySandboxFileCopyRequest ( request , token , scope )
67+ }
6068 const ownerHeader = request . headers . get ( 'x-mothership-file-owner' )
6169 let owner : FileOperationOwner | undefined
6270 if ( ownerHeader !== null ) {
@@ -204,10 +212,31 @@ async function proxyAuthorizedSandboxRequest(
204212 } )
205213}
206214
215+ /**
216+ * Reads a copy body once, under the v2 body cap, so the protocol check and whichever copy path
217+ * handles it each read an in-memory copy. Parsing a clone of the live stream instead would hang on
218+ * an oversized body: cancelling one branch of a teed stream waits for the unread other branch.
219+ */
220+ async function bufferCopyRequest ( request : Request ) : Promise < Request | Response > {
221+ if ( ! request . body ) return request
222+ try {
223+ assertContentLengthWithinLimit ( request . headers , DEFAULT_MAX_JSON_BODY_BYTES , 'Request body' )
224+ const body = await readStreamToBufferWithLimit ( request . body , {
225+ maxBytes : DEFAULT_MAX_JSON_BODY_BYTES ,
226+ label : 'Request body' ,
227+ signal : request . signal ,
228+ } )
229+ return new Request ( request , { body : new Uint8Array ( body ) } )
230+ } catch ( error ) {
231+ if ( isPayloadSizeLimitError ( error ) ) return V2_PARSE_DEFAULTS . payloadTooLargeResponse ( )
232+ throw error
233+ }
234+ }
235+
207236/**
208237 * A valid copy naming a non-workspace source or destination needs the file-owner protocol. Parsed
209- * as the copy route parses it, under the same body cap : an oversized or invalid body stays on the
210- * workspace route, which returns its own 413 or 400.
238+ * as the copy route parses it: an invalid body stays on the workspace route, which returns its own
239+ * 400.
211240 */
212241async function copiesBeyondWorkspaces ( request : Request ) : Promise < boolean > {
213242 const parsed = await parseRequest (
0 commit comments