11import { createLogger } from '@sim/logger'
22import { type NextRequest , NextResponse } from 'next/server'
33import { checkInternalAuth } from '@/lib/auth/hybrid'
4+ import {
5+ FORMAT_TO_CONTENT_TYPE ,
6+ normalizeOutputWorkspaceFileName ,
7+ resolveOutputFormat ,
8+ } from '@/lib/copilot/request/tools/files'
49import { isE2bEnabled } from '@/lib/core/config/feature-flags'
510import { generateRequestId } from '@/lib/core/utils/request'
6- import { executeInE2B } from '@/lib/execution/e2b'
11+ import { executeInE2B , executeShellInE2B } from '@/lib/execution/e2b'
712import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
813import { CodeLanguage , DEFAULT_CODE_LANGUAGE , isValidCodeLanguage } from '@/lib/execution/languages'
14+ import { uploadWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
15+ import { getWorkflowById } from '@/lib/workflows/utils'
916import { escapeRegExp , normalizeName , REFERENCE } from '@/executor/constants'
1017import { type OutputSchema , resolveBlockReference } from '@/executor/utils/block-reference'
1118import { formatLiteralForCode } from '@/executor/utils/code-formatting'
@@ -580,6 +587,96 @@ function cleanStdout(stdout: string): string {
580587 return stdout
581588}
582589
590+ async function maybeExportSandboxFileToWorkspace ( args : {
591+ authUserId : string
592+ workflowId ?: string
593+ workspaceId ?: string
594+ outputPath ?: string
595+ outputFormat ?: string
596+ outputSandboxPath ?: string
597+ exportedFileContent ?: string
598+ stdout : string
599+ executionTime : number
600+ } ) {
601+ const {
602+ authUserId,
603+ workflowId,
604+ workspaceId,
605+ outputPath,
606+ outputFormat,
607+ outputSandboxPath,
608+ exportedFileContent,
609+ stdout,
610+ executionTime,
611+ } = args
612+
613+ if ( ! outputSandboxPath ) return null
614+
615+ if ( ! outputPath ) {
616+ return NextResponse . json (
617+ {
618+ success : false ,
619+ error :
620+ 'outputSandboxPath requires outputPath. Set outputPath to the destination workspace file, e.g. "files/result.csv".' ,
621+ output : { result : null , stdout : cleanStdout ( stdout ) , executionTime } ,
622+ } ,
623+ { status : 400 }
624+ )
625+ }
626+
627+ const resolvedWorkspaceId =
628+ workspaceId || ( workflowId ? ( await getWorkflowById ( workflowId ) ) ?. workspaceId : undefined )
629+
630+ if ( ! resolvedWorkspaceId ) {
631+ return NextResponse . json (
632+ {
633+ success : false ,
634+ error : 'Workspace context required to save sandbox file to workspace' ,
635+ output : { result : null , stdout : cleanStdout ( stdout ) , executionTime } ,
636+ } ,
637+ { status : 400 }
638+ )
639+ }
640+
641+ if ( exportedFileContent === undefined ) {
642+ return NextResponse . json (
643+ {
644+ success : false ,
645+ error : `Sandbox file "${ outputSandboxPath } " was not found or could not be read` ,
646+ output : { result : null , stdout : cleanStdout ( stdout ) , executionTime } ,
647+ } ,
648+ { status : 500 }
649+ )
650+ }
651+
652+ const fileName = normalizeOutputWorkspaceFileName ( outputPath )
653+ const format = resolveOutputFormat ( fileName , outputFormat )
654+ const contentType = FORMAT_TO_CONTENT_TYPE [ format ]
655+ const uploaded = await uploadWorkspaceFile (
656+ resolvedWorkspaceId ,
657+ authUserId ,
658+ Buffer . from ( exportedFileContent , 'utf-8' ) ,
659+ fileName ,
660+ contentType
661+ )
662+
663+ return NextResponse . json ( {
664+ success : true ,
665+ output : {
666+ result : {
667+ message : `Sandbox file exported to files/${ fileName } ` ,
668+ fileId : uploaded . id ,
669+ fileName,
670+ downloadUrl : uploaded . url ,
671+ sandboxPath : outputSandboxPath ,
672+ } ,
673+ stdout : cleanStdout ( stdout ) ,
674+ executionTime,
675+ } ,
676+ resources : [ { type : 'file' , id : uploaded . id , title : fileName } ] ,
677+ } )
678+ }
679+
583680export async function POST ( req : NextRequest ) {
584681 const requestId = generateRequestId ( )
585682 const startTime = Date . now ( )
@@ -603,12 +700,16 @@ export async function POST(req: NextRequest) {
603700 params = { } ,
604701 timeout = DEFAULT_EXECUTION_TIMEOUT_MS ,
605702 language = DEFAULT_CODE_LANGUAGE ,
703+ outputPath,
704+ outputFormat,
705+ outputSandboxPath,
606706 envVars = { } ,
607707 blockData = { } ,
608708 blockNameMapping = { } ,
609709 blockOutputSchemas = { } ,
610710 workflowVariables = { } ,
611711 workflowId,
712+ workspaceId,
612713 isCustomTool = false ,
613714 _sandboxFiles,
614715 } = body
@@ -652,6 +753,82 @@ export async function POST(req: NextRequest) {
652753 hasImports = jsImports . trim ( ) . length > 0 || hasRequireStatements
653754 }
654755
756+ if ( lang === CodeLanguage . Shell ) {
757+ if ( ! isE2bEnabled ) {
758+ throw new Error (
759+ 'Shell execution requires E2B to be enabled. Please contact your administrator to enable E2B.'
760+ )
761+ }
762+
763+ const shellEnvs : Record < string , string > = { }
764+ for ( const [ k , v ] of Object . entries ( envVars ) ) {
765+ shellEnvs [ k ] = String ( v )
766+ }
767+ for ( const [ k , v ] of Object . entries ( contextVariables ) ) {
768+ shellEnvs [ k ] = String ( v )
769+ }
770+
771+ logger . info ( `[${ requestId } ] E2B shell execution` , {
772+ enabled : isE2bEnabled ,
773+ hasApiKey : Boolean ( process . env . E2B_API_KEY ) ,
774+ envVarCount : Object . keys ( shellEnvs ) . length ,
775+ } )
776+
777+ const execStart = Date . now ( )
778+ const {
779+ result : shellResult ,
780+ stdout : shellStdout ,
781+ sandboxId,
782+ error : shellError ,
783+ exportedFileContent,
784+ } = await executeShellInE2B ( {
785+ code : resolvedCode ,
786+ envs : shellEnvs ,
787+ timeoutMs : timeout ,
788+ sandboxFiles : _sandboxFiles ,
789+ outputSandboxPath,
790+ } )
791+ const executionTime = Date . now ( ) - execStart
792+
793+ logger . info ( `[${ requestId } ] E2B shell sandbox` , {
794+ sandboxId,
795+ stdoutPreview : shellStdout ?. slice ( 0 , 200 ) ,
796+ error : shellError ,
797+ executionTime,
798+ } )
799+
800+ if ( shellError ) {
801+ return NextResponse . json (
802+ {
803+ success : false ,
804+ error : shellError ,
805+ output : { result : null , stdout : cleanStdout ( shellStdout ) , executionTime } ,
806+ } ,
807+ { status : 500 }
808+ )
809+ }
810+
811+ if ( outputSandboxPath ) {
812+ const fileExportResponse = await maybeExportSandboxFileToWorkspace ( {
813+ authUserId : auth . userId ,
814+ workflowId,
815+ workspaceId,
816+ outputPath,
817+ outputFormat,
818+ outputSandboxPath,
819+ exportedFileContent,
820+ stdout : shellStdout ,
821+ executionTime,
822+ } )
823+ if ( fileExportResponse ) return fileExportResponse
824+ }
825+
826+ return NextResponse . json ( {
827+ success : true ,
828+ output : { result : shellResult ?? null , stdout : cleanStdout ( shellStdout ) , executionTime } ,
829+ } )
830+ }
831+
655832 if ( lang === CodeLanguage . Python && ! isE2bEnabled ) {
656833 throw new Error (
657834 'Python execution requires E2B to be enabled. Please contact your administrator to enable E2B, or use JavaScript instead.'
@@ -719,11 +896,13 @@ export async function POST(req: NextRequest) {
719896 stdout : e2bStdout ,
720897 sandboxId,
721898 error : e2bError ,
899+ exportedFileContent,
722900 } = await executeInE2B ( {
723901 code : codeForE2B ,
724902 language : CodeLanguage . JavaScript ,
725903 timeoutMs : timeout ,
726904 sandboxFiles : _sandboxFiles ,
905+ outputSandboxPath,
727906 } )
728907 const executionTime = Date . now ( ) - execStart
729908 stdout += e2bStdout
@@ -752,6 +931,21 @@ export async function POST(req: NextRequest) {
752931 )
753932 }
754933
934+ if ( outputSandboxPath ) {
935+ const fileExportResponse = await maybeExportSandboxFileToWorkspace ( {
936+ authUserId : auth . userId ,
937+ workflowId,
938+ workspaceId,
939+ outputPath,
940+ outputFormat,
941+ outputSandboxPath,
942+ exportedFileContent,
943+ stdout,
944+ executionTime,
945+ } )
946+ if ( fileExportResponse ) return fileExportResponse
947+ }
948+
755949 return NextResponse . json ( {
756950 success : true ,
757951 output : { result : e2bResult ?? null , stdout : cleanStdout ( stdout ) , executionTime } ,
@@ -783,11 +977,13 @@ export async function POST(req: NextRequest) {
783977 stdout : e2bStdout ,
784978 sandboxId,
785979 error : e2bError ,
980+ exportedFileContent,
786981 } = await executeInE2B ( {
787982 code : codeForE2B ,
788983 language : CodeLanguage . Python ,
789984 timeoutMs : timeout ,
790985 sandboxFiles : _sandboxFiles ,
986+ outputSandboxPath,
791987 } )
792988 const executionTime = Date . now ( ) - execStart
793989 stdout += e2bStdout
@@ -816,6 +1012,21 @@ export async function POST(req: NextRequest) {
8161012 )
8171013 }
8181014
1015+ if ( outputSandboxPath ) {
1016+ const fileExportResponse = await maybeExportSandboxFileToWorkspace ( {
1017+ authUserId : auth . userId ,
1018+ workflowId,
1019+ workspaceId,
1020+ outputPath,
1021+ outputFormat,
1022+ outputSandboxPath,
1023+ exportedFileContent,
1024+ stdout,
1025+ executionTime,
1026+ } )
1027+ if ( fileExportResponse ) return fileExportResponse
1028+ }
1029+
8191030 return NextResponse . json ( {
8201031 success : true ,
8211032 output : { result : e2bResult ?? null , stdout : cleanStdout ( stdout ) , executionTime } ,
0 commit comments