@@ -6,6 +6,18 @@ import { createDelegatedPrincipal } from '@sim/testing/factories/principal.facto
66import { createDeferred } from '@sim/testing/helpers/deferred'
77import { setEnv } from '@sim/testing/mocks/env.mock'
88import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock'
9+ import {
10+ mothershipAgentUrlMock ,
11+ mothershipAgentUrlMockFns ,
12+ } from '@sim/testing/mocks/mothership-agent-url.mock'
13+ import {
14+ mothershipAsyncRunsMock ,
15+ mothershipAsyncRunsMockFns ,
16+ } from '@sim/testing/mocks/mothership-async-runs.mock'
17+ import {
18+ mothershipGoFetchMock ,
19+ mothershipGoFetchMockFns ,
20+ } from '@sim/testing/mocks/mothership-go-fetch.mock'
921import { redisConfigMockFns } from '@sim/testing/mocks/redis-config.mock'
1022import {
1123 remoteSandboxProviderMock ,
@@ -30,9 +42,9 @@ vi.mock('@/lib/execution/remote-sandbox/resolve', () => ({
3042 repairMissingSandboxImage : async ( ) => null ,
3143 RUNTIME_INSTALL_TIMEOUT_MS : 60_000 ,
3244} ) )
33- vi . mock ( '@/lib/mothership/tools/sandbox-session ' , ( ) => ( {
34- buildMothershipSandboxSession : async ( args : { sessionKey : string } ) => ( { key : args . sessionKey } ) ,
35- } ) )
45+ vi . mock ( '@/lib/mothership/async-runs/repository ' , ( ) => mothershipAsyncRunsMock )
46+ vi . mock ( '@/lib/mothership/request/go/fetch' , ( ) => mothershipGoFetchMock )
47+ vi . mock ( '@/lib/mothership/server/agent-url' , ( ) => mothershipAgentUrlMock )
3648vi . mock ( '@/lib/workspace-files/application/delegated-principal' , ( ) => ( {
3749 rebindWorkspaceFileDelegatedPrincipal : ( { principal } : { principal : unknown } ) => principal ,
3850} ) )
@@ -61,6 +73,10 @@ import { inspectToolResultForCopilot } from '@/lib/mothership/request/tools/reso
6173import type { ToolExecutionContext } from '@/lib/mothership/tool-executor/types'
6274import { executeFunctionExecute } from '@/lib/mothership/tools/handlers/function-execute'
6375import { executeRunCode } from '@/lib/mothership/tools/handlers/run-code'
76+ import {
77+ readSandboxResourceScope ,
78+ withSandboxResourceScope ,
79+ } from '@/lib/mothership/tools/sandbox-resources'
6480import { chatSandboxSessionKey } from '@/lib/mothership/tools/sandbox-session-key'
6581import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
6682import { buildFunctionExecuteBody , functionExecuteTool } from '@/tools/function/execute'
@@ -83,34 +99,47 @@ function workerPath(path: string) {
8399 return path . startsWith ( '/' ) ? join ( root , path . slice ( 1 ) ) : join ( root , 'home/user' , path )
84100}
85101
102+ async function runWorkerProcess (
103+ executable : string ,
104+ args : string [ ] ,
105+ options : Parameters < SandboxHandle [ 'runCommand' ] > [ 1 ]
106+ ) {
107+ const envs = Object . fromEntries (
108+ Object . entries ( options . envs ?? { } ) . map ( ( [ key , value ] ) => [
109+ key ,
110+ value
111+ . replaceAll ( '/home/user' , workerPath ( '/home/user' ) )
112+ . replaceAll ( '/tmp/sim/' , `${ workerPath ( '/tmp/sim' ) } /` )
113+ . replaceAll ( '/tmp/.sim-private-input-' , workerPath ( '/tmp/.sim-private-input-' ) ) ,
114+ ] )
115+ )
116+ try {
117+ const output = await execute ( executable , args , {
118+ cwd : workerPath ( '/home/user' ) ,
119+ env : { PATH : '/usr/bin:/bin:/opt/homebrew/bin' , ...envs } ,
120+ timeout : options . timeoutMs ,
121+ maxBuffer : options . maxOutputBytes ,
122+ } )
123+ return { ...output , exitCode : 0 }
124+ } catch ( error ) {
125+ const failure = error as { stdout : string ; stderr : string ; code : number }
126+ return { stdout : failure . stdout , stderr : failure . stderr , exitCode : failure . code }
127+ }
128+ }
129+
86130function localWorker ( ) : SandboxHandle {
87131 return {
88132 sandboxId : `local-${ generateShortId ( 12 ) } ` ,
89- runCode : async ( ) => {
90- throw new Error ( 'This reproduction uses actual shell processes' )
91- } ,
92- async runCommand ( command , options ) {
93- const envs = Object . fromEntries (
94- Object . entries ( options . envs ?? { } ) . map ( ( [ key , value ] ) => [
95- key ,
96- value
97- . replaceAll ( '/home/user' , workerPath ( '/home/user' ) )
98- . replaceAll ( '/tmp/sim/' , `${ workerPath ( '/tmp/sim' ) } /` ) ,
99- ] )
100- )
101- try {
102- const output = await execute ( '/bin/bash' , [ '-c' , command ] , {
103- cwd : workerPath ( '/home/user' ) ,
104- env : { PATH : '/usr/bin:/bin:/opt/homebrew/bin' , ...envs } ,
105- timeout : options . timeoutMs ,
106- maxBuffer : options . maxOutputBytes ,
107- } )
108- return { ...output , exitCode : 0 }
109- } catch ( error ) {
110- const failure = error as { stdout : string ; stderr : string ; code : number }
111- return { stdout : failure . stdout , stderr : failure . stderr , exitCode : failure . code }
133+ async runCode ( code , options ) {
134+ const result = await runWorkerProcess ( process . execPath , [ '-e' , code ] , options )
135+ return {
136+ text : '' ,
137+ stdout : result . stdout ,
138+ stderr : result . stderr ,
139+ ...( result . exitCode ? { error : { name : 'RuntimeError' , value : result . stderr } } : { } ) ,
112140 }
113141 } ,
142+ runCommand : ( command , options ) => runWorkerProcess ( '/bin/bash' , [ '-c' , command ] , options ) ,
114143 extendLifetime : async ( ) => { } ,
115144 getFileSize : async ( path ) => ( await stat ( workerPath ( path ) ) ) . size ,
116145 readFile : async ( path ) => readFile ( workerPath ( path ) , 'utf8' ) ,
@@ -175,7 +204,16 @@ beforeEach(async () => {
175204 throw new Error ( 'Only the existing disposable worker may be used' )
176205 } ,
177206 } )
178- setEnv ( { ENCRYPTION_KEY : 'a' . repeat ( 64 ) } )
207+ setEnv ( {
208+ ENCRYPTION_KEY : 'a' . repeat ( 64 ) ,
209+ MOTHERSHIP_SIM_TRANSPORT : 'direct' ,
210+ MOTHERSHIP_SANDBOX_CLI_ENDPOINT : 'https://callback.test' ,
211+ } )
212+ mothershipAsyncRunsMockFns . mockIsActiveSandboxResourceOwner . mockResolvedValue ( true )
213+ mothershipAgentUrlMockFns . mockGetMothershipBaseURL . mockResolvedValue ( 'https://worker.test' )
214+ mothershipGoFetchMockFns . mockFetchGo . mockImplementation ( async ( ) =>
215+ Response . json ( { version : 1 , entrypoint : 'fixture-bootstrap' } )
216+ )
179217 envFlagsMock . isMothershipSandboxEnabled = true
180218 envFlagsMock . isRemoteSandboxEnabled = true
181219 root = await mkdtemp ( '/private/tmp/sim-workbench-test-' )
@@ -250,9 +288,13 @@ function context(): ToolExecutionContext {
250288 }
251289}
252290
253- async function run ( code : string , secrets : string [ ] = [ ] ) {
291+ async function run (
292+ code : string ,
293+ secrets : string [ ] = [ ] ,
294+ language : 'shell' | 'javascript' = 'shell'
295+ ) {
254296 const current = context ( )
255- const raw = await executeRunCode ( { code, language : 'shell' , secrets } , current )
297+ const raw = await inResourceScope ( ( ) => executeRunCode ( { code, language, secrets } , current ) )
256298 const projected = inspectToolResultForCopilot (
257299 raw ,
258300 current . resolvedSecretTraceRegistry ,
@@ -262,7 +304,82 @@ async function run(code: string, secrets: string[] = []) {
262304 return { raw, projected }
263305}
264306
307+ function inResourceScope < T > ( action : ( ) => Promise < T > ) {
308+ return withSandboxResourceScope (
309+ {
310+ ...scope ,
311+ chatId,
312+ runId : 'fixture-run' ,
313+ toolCallId : 'fixture-call' ,
314+ ownerToken : 'fixture-owner' ,
315+ } ,
316+ AbortSignal . timeout ( 15_000 ) ,
317+ undefined ,
318+ action
319+ )
320+ }
321+
265322describe ( 'persistent workbench output confidentiality' , ( ) => {
323+ it . each ( [ 'javascript' , 'shell' ] as const ) (
324+ 'redacts session credentials in %s output while preserving routing metadata' ,
325+ async ( language ) => {
326+ const code =
327+ language === 'shell'
328+ ? 'printf "%s" "$SIM_API_KEY" > session-key.txt; printf "%s %s" "$SIM_API_KEY" "$SIM_WORKSPACE"'
329+ : '(await import("node:fs")).writeFileSync("session-key.txt", process.env.SIM_API_KEY); process.stdout.write(process.env.SIM_API_KEY + " " + process.env.SIM_WORKSPACE)'
330+ const result = await run ( code , [ ] , language )
331+ expect ( result . raw . success ) . toBe ( true )
332+ const credential = await readFile ( workerPath ( 'session-key.txt' ) , 'utf8' )
333+ expect ( credential ) . toMatch ( / ^ m o t h e r s h i p - s a n d b o x : / )
334+ expect ( result . projected . safe ) . toBe ( true )
335+ expect ( JSON . stringify ( result . projected . result ) ) . not . toContain ( credential )
336+ expect ( JSON . stringify ( result . projected . result ) ) . toContain ( '{{SIM_API_KEY}}' )
337+ expect ( JSON . stringify ( result . projected . result ) ) . toContain ( scope . workspaceId )
338+ expect (
339+ await readSessionSecretProvenance ( chatSandboxSessionKey ( chatId ) , {
340+ providerId : 'e2b' ,
341+ sandboxId : machine . sandboxId ,
342+ } )
343+ ) . toEqual ( { status : 'exact' , entries : [ ] } )
344+ }
345+ )
346+ it ( 'redacts session credentials when the provider falls back to a one-shot machine' , async ( ) => {
347+ remoteSandboxProviderMockFns . mockResolveProvider . mockReturnValue ( {
348+ id : 'e2b' ,
349+ dependencyStrategy : 'prebuilt' ,
350+ resolveLifetimeMs : ( ms : number ) => ms ,
351+ create : async ( ) => machine ,
352+ } )
353+ const result = await run ( 'printf "%s" "$SIM_API_KEY" > session-key.txt; cat session-key.txt' )
354+ const credential = await readFile ( workerPath ( 'session-key.txt' ) , 'utf8' )
355+ expect ( result . raw . success ) . toBe ( true )
356+ expect ( result . projected . safe ) . toBe ( true )
357+ expect ( JSON . stringify ( result . projected . result ) ) . not . toContain ( credential )
358+ expect ( JSON . stringify ( result . projected . result ) ) . toContain ( '{{SIM_API_KEY}}' )
359+ } )
360+ it ( 'omits session authentication if its encrypted receipt cannot be created' , async ( ) => {
361+ setEnv ( { ENCRYPTION_KEY : '' } )
362+ const result = await run ( 'test -z "$SIM_API_KEY" && printf allowed' )
363+ expect ( result . raw . success ) . toBe ( true )
364+ expect ( JSON . stringify ( result . projected . result ) ) . toContain ( 'allowed' )
365+ } )
366+ it ( 'keeps large ordinary results readable when callback credentials are absent from them' , async ( ) => {
367+ const result = await run ( 'return Array.from({ length: 100_001 }, () => 0)' , [ ] , 'javascript' )
368+ expect ( result . raw . success ) . toBe ( true )
369+ expect ( result . raw . output ) . toHaveProperty ( 'result.length' , 100_001 )
370+ expect ( result . projected . safe ) . toBe ( true )
371+ } )
372+ it ( 'revokes callback authentication before a result reaches the model' , async ( ) => {
373+ const result = await run (
374+ 'printf "%s" "$SIM_API_KEY" > session-key.txt; printf "%s" "$SIM_ENDPOINT" > session-endpoint.txt; printf done'
375+ )
376+ const credential = await readFile ( workerPath ( 'session-key.txt' ) , 'utf8' )
377+ const endpoint = await readFile ( workerPath ( 'session-endpoint.txt' ) , 'utf8' )
378+ expect ( result . raw . success ) . toBe ( true )
379+ expect ( result . projected . safe ) . toBe ( true )
380+ expect ( await readSandboxResourceScope ( endpoint . split ( '/' ) . at ( - 1 ) ! , credential ) ) . toBeNull ( )
381+ } )
382+
266383 it ( 'allows a mounted empty value without requiring a redaction receipt' , async ( ) => {
267384 const emptyCatalog = [
268385 { name : 'TOKEN' , plaintext : '' , encryptedValue : ( await encryptSecret ( '' ) ) . encrypted } ,
@@ -395,6 +512,22 @@ describe('persistent workbench output confidentiality', () => {
395512 expect ( JSON . stringify ( output . projected . result ) ) . not . toContain ( canary )
396513 expect ( JSON . stringify ( output . projected . result ) ) . toContain ( '{{TOKEN}}' )
397514 } )
515+ it ( 'keeps ordinary binary exports usable when only callback authentication is present' , async ( ) => {
516+ const result = await inResourceScope ( ( ) =>
517+ executeFunctionExecute (
518+ {
519+ code : "printf '\\211PNG\\000\\001' > image.png" ,
520+ language : 'shell' ,
521+ outputs : { files : [ { path : 'files/image.png' , sandboxPath : 'image.png' } ] } ,
522+ } ,
523+ context ( )
524+ )
525+ )
526+ expect ( result . success ) . toBe ( true )
527+ const saved = io . write . mock . calls . at ( - 1 ) ! [ 0 ]
528+ expect ( saved . buffer ) . toEqual ( Buffer . from ( [ 0x89 , 0x50 , 0x4e , 0x47 , 0 , 1 ] ) )
529+ expect ( saved . secretProvenance ) . toEqual ( { status : 'exact' , entries : [ ] } )
530+ } )
398531 it ( 'retains historical secret provenance on a text export' , async ( ) => {
399532 await run ( 'printf "%s" "$TOKEN" > saved.txt' , [ 'TOKEN' ] )
400533 const current = context ( )
0 commit comments