@@ -18,6 +18,10 @@ import {
1818 mothershipGoFetchMock ,
1919 mothershipGoFetchMockFns ,
2020} from '@sim/testing/mocks/mothership-go-fetch.mock'
21+ import {
22+ mothershipWorkspaceTargetMock ,
23+ mothershipWorkspaceTargetMockFns ,
24+ } from '@sim/testing/mocks/mothership-workspace-target.mock'
2125import { redisConfigMockFns } from '@sim/testing/mocks/redis-config.mock'
2226import {
2327 remoteSandboxProviderMock ,
@@ -30,6 +34,7 @@ import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
3034
3135const io = vi . hoisted ( ( ) => ( { mount : vi . fn ( ) , find : vi . fn ( ) , write : vi . fn ( ) } ) )
3236vi . mock ( '@/tools' , ( ) => toolsMock )
37+ vi . mock ( '@/lib/mothership/application/workspace-target' , ( ) => mothershipWorkspaceTargetMock )
3338vi . mock ( '@/lib/mothership/tools/secret-mount-materializer.server' , ( ) => ( {
3439 materializeCopilotCodeSecrets : io . mount ,
3540 CopilotCodeSecretAccessError : class extends Error { } ,
@@ -54,6 +59,7 @@ vi.mock('@/lib/mothership/vfs/resource-writer', () => ({
5459} ) )
5560
5661import { functionExecuteBodySchema } from '@/lib/api/contracts'
62+ import * as inProcessTransport from '@/lib/api/server/routes/in-process-transport'
5763import { encryptSecret } from '@/lib/core/security/encryption'
5864import {
5965 PRIVATE_TOOL_METADATA_REQUEST_HEADER ,
@@ -73,12 +79,15 @@ import { inspectToolResultForCopilot } from '@/lib/mothership/request/tools/reso
7379import type { ToolExecutionContext } from '@/lib/mothership/tool-executor/types'
7480import { executeFunctionExecute } from '@/lib/mothership/tools/handlers/function-execute'
7581import { executeRunCode } from '@/lib/mothership/tools/handlers/run-code'
82+ import { proxySandboxResourceRequest } from '@/lib/mothership/tools/sandbox-resource-transport'
7683import {
7784 readSandboxResourceScope ,
7885 withSandboxResourceScope ,
7986} from '@/lib/mothership/tools/sandbox-resources'
8087import { buildMothershipSandboxSession } from '@/lib/mothership/tools/sandbox-session'
8188import { chatSandboxSessionKey } from '@/lib/mothership/tools/sandbox-session-key'
89+ import { reportTableRowDelivery } from '@/lib/table/application/row-delivery-observer'
90+ import { reportWorkspaceFileDelivery } from '@/lib/workspace-files/application/file-delivery-observer'
8291import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
8392import { buildFunctionExecuteBody , functionExecuteTool } from '@/tools/function/execute'
8493import type { CodeExecutionInput } from '@/tools/function/types'
@@ -320,6 +329,123 @@ function inResourceScope<T>(action: () => Promise<T>) {
320329 )
321330}
322331
332+ async function sandboxApi ( path : string , handler : ( ) => Promise < Response > , method = 'GET' ) {
333+ mothershipWorkspaceTargetMockFns . mockResolveInvocationWorkspace . mockResolvedValue ( scope )
334+ vi . spyOn ( inProcessTransport , 'matchV2Route' ) . mockReturnValue ( {
335+ pattern : path ,
336+ params : { fileId : 'fixture' , tableId : 'fixture' } ,
337+ literals : 3 ,
338+ load : async ( ) => ( { GET : handler , POST : handler } ) ,
339+ } )
340+ return inResourceScope ( async ( ) => {
341+ const session = await buildMothershipSandboxSession ( {
342+ ...scope ,
343+ sessionKey : chatSandboxSessionKey ( chatId ) ,
344+ } )
345+ const endpoint = session . envs ! . SIM_ENDPOINT
346+ return proxySandboxResourceRequest (
347+ new Request ( `${ endpoint } ${ path } ` , {
348+ method,
349+ headers : { 'x-api-key' : session . envs ! . SIM_API_KEY } ,
350+ } ) ,
351+ endpoint . split ( '/' ) . at ( - 1 ) !
352+ )
353+ } )
354+ }
355+
356+ describe ( 'sandbox API provenance admission' , ( ) => {
357+ it ( 'keeps ordinary API mutations usable for later code output and generated CLI input' , async ( ) => {
358+ const response = await sandboxApi (
359+ '/api/v2/custom-tools' ,
360+ async ( ) => Response . json ( { data : { id : 'fixture-tool' , title : 'fixture' } } ) ,
361+ 'POST'
362+ )
363+ expect ( response . status ) . toBe ( 200 )
364+ const result = await run ( 'printf "[]" > operations.json; printf "ready"' )
365+ expect ( result . projected . safe ) . toBe ( true )
366+ expect ( result . projected . result ) . toMatchObject ( { success : true , output : { stdout : 'ready' } } )
367+ expect (
368+ ( await readCliInputFile ( chatSandboxSessionKey ( chatId ) , 'operations.json' ) ) . toString ( )
369+ ) . toBe ( '[]' )
370+ } )
371+
372+ it ( 'retains earlier secret protection after an API response without provenance' , async ( ) => {
373+ await run ( 'printf "%s" "$TOKEN" > saved.txt' , [ 'TOKEN' ] )
374+ await sandboxApi ( '/api/v2/custom-tools' , async ( ) => Response . json ( { data : [ ] } ) )
375+ const result = await run ( 'cat saved.txt' )
376+ expect ( result . projected . safe ) . toBe ( true )
377+ expect ( result . projected . result ) . toMatchObject ( {
378+ success : true ,
379+ output : { stdout : '{{TOKEN}}' } ,
380+ } )
381+ await expect ( readCliInputFile ( chatSandboxSessionKey ( chatId ) , 'saved.txt' ) ) . rejects . toThrow (
382+ 'protected workbench values'
383+ )
384+ } )
385+
386+ it . each ( [ 'file' , 'table' ] as const ) (
387+ 'imports explicit %s delivery evidence before later output' ,
388+ async ( source ) => {
389+ const response = await sandboxApi (
390+ `/api/v2/${ source === 'file' ? 'files/fixture' : 'tables/fixture/rows' } ` ,
391+ async ( ) => {
392+ if ( source === 'file' ) {
393+ await reportWorkspaceFileDelivery ( {
394+ status : 'exact' ,
395+ entries : [
396+ {
397+ name : 'TOKEN' ,
398+ encryptedValue : catalog [ 0 ] . encryptedValue ,
399+ sourceUserId : scope . userId ,
400+ sourceWorkspaceId : scope . workspaceId ,
401+ } ,
402+ ] ,
403+ } )
404+ } else {
405+ await reportTableRowDelivery (
406+ {
407+ version : 1 ,
408+ complete : true ,
409+ scope,
410+ entries : [ { name : 'TOKEN' , encryptedValue : catalog [ 0 ] . encryptedValue } ] ,
411+ } ,
412+ [ { value : canary } ]
413+ )
414+ }
415+ return new Response ( canary )
416+ }
417+ )
418+ await machine . writeFile ( '/home/user/delivered.txt' , await response . text ( ) )
419+ const result = await run ( 'cat delivered.txt' )
420+ expect ( result . projected . safe ) . toBe ( true )
421+ expect ( result . projected . result ) . toMatchObject ( {
422+ success : true ,
423+ output : { stdout : '{{TOKEN}}' } ,
424+ } )
425+ }
426+ )
427+
428+ it . each ( [ 'file' , 'table' ] as const ) (
429+ 'preserves an explicit unknown %s delivery as unknown' ,
430+ async ( source ) => {
431+ await sandboxApi (
432+ `/api/v2/${ source === 'file' ? 'files/fixture' : 'tables/fixture/rows' } ` ,
433+ async ( ) => {
434+ if ( source === 'file' ) await reportWorkspaceFileDelivery ( { status : 'unknown' } )
435+ else
436+ await reportTableRowDelivery ( { version : 1 , complete : false , entries : [ ] } , [
437+ { value : 'unknown' } ,
438+ ] )
439+ return new Response ( 'unknown' )
440+ }
441+ )
442+ const result = await run ( 'printf "ready"' )
443+ expect ( result . projected . safe ) . toBe ( false )
444+ expect ( JSON . stringify ( result . projected . result ) ) . not . toContain ( 'ready' )
445+ }
446+ )
447+ } )
448+
323449describe ( 'persistent workbench output confidentiality' , ( ) => {
324450 it . each ( [ 'javascript' , 'shell' ] as const ) (
325451 'redacts session credentials in %s output while preserving routing metadata' ,
0 commit comments