@@ -16,13 +16,46 @@ import { basename, dirname, join, resolve, sep } from 'node:path'
1616import { gunzipSync } from 'node:zlib'
1717import { z } from 'zod'
1818import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
19- import type { Sandchest } from '@sandchest/sdk'
19+ import type { Sandchest , Sandbox } from '@sandchest/sdk'
2020import { ExecFailedError , SandchestError , Session } from '@sandchest/sdk'
2121
2222function jsonContent ( data : unknown ) : { content : Array < { type : 'text' ; text : string } > } {
2323 return { content : [ { type : 'text' , text : JSON . stringify ( data , null , 2 ) } ] }
2424}
2525
26+ /**
27+ * Ensures the sandbox has working network connectivity.
28+ * Reads network params from kernel cmdline and configures eth0 if needed.
29+ */
30+ async function ensureSandboxNetwork ( sb : Sandbox ) : Promise < void > {
31+ const check = await sb . exec (
32+ [ 'sh' , '-c' , 'ip addr show eth0 2>/dev/null | grep -q "state UP" && ip route show | grep -q default' ] ,
33+ { timeout : 5 } ,
34+ )
35+ if ( check . exitCode === 0 ) return
36+
37+ const cmdline = await sb . exec ( [ 'cat' , '/proc/cmdline' ] , { timeout : 5 } )
38+ if ( cmdline . exitCode !== 0 ) return
39+
40+ const params : Record < string , string > = { }
41+ for ( const part of cmdline . stdout . split ( / \s + / ) ) {
42+ const match = part . match ( / ^ s a n d c h e s t \. ( i p | g w | d n s ) = ( .+ ) $ / )
43+ if ( match ) params [ match [ 1 ] ] = match [ 2 ]
44+ }
45+
46+ if ( ! params . ip || ! params . gw ) return
47+
48+ const dns = params . dns || '1.1.1.1'
49+
50+ await sb . exec ( [ 'sh' , '-c' , [
51+ 'ip link set eth0 up' ,
52+ `ip addr add ${ params . ip } dev eth0 2>/dev/null || true` ,
53+ `ip route add default via ${ params . gw } 2>/dev/null || true` ,
54+ 'mount -o remount,rw / 2>/dev/null || true' ,
55+ `rm -f /etc/resolv.conf && printf "nameserver ${ dns } \\n" > /etc/resolv.conf` ,
56+ ] . join ( ' && ' ) ] , { timeout : 10 } )
57+ }
58+
2659const EXEC_OUTPUT_CAP_BYTES = 1_048_576
2760const ALLOWED_PATHS_ENV = 'SANDCHEST_MCP_ALLOWED_PATHS'
2861const TEXT_ENCODER = new TextEncoder ( )
@@ -971,7 +1004,71 @@ export function registerTools(server: McpServer, sandchest: Sandchest): void {
9711004 } )
9721005 }
9731006
974- await sb . fs . uploadDir ( remotePath , tarball )
1007+ let uploadMethod : string = method
1008+ try {
1009+ await sb . fs . uploadDir ( remotePath , tarball )
1010+ } catch {
1011+ // Fallback: upload via exec (base64 chunks through shell commands)
1012+ uploadMethod = `${ method } +exec-fallback`
1013+ const b64 = Buffer . from ( tarball ) . toString ( 'base64' )
1014+ const tmpPath = `/tmp/.sandchest-upload-${ crypto . randomUUID ( ) } .tar.gz`
1015+ const CHUNK_SIZE = 200_000 // ~200KB base64 per exec call
1016+ const totalChunks = Math . ceil ( b64 . length / CHUNK_SIZE )
1017+
1018+ for ( let i = 0 ; i < totalChunks ; i ++ ) {
1019+ const chunk = b64 . slice ( i * CHUNK_SIZE , ( i + 1 ) * CHUNK_SIZE )
1020+ const op = i === 0 ? '>' : '>>'
1021+ const result = await sb . exec (
1022+ [ 'sh' , '-c' , `printf '%s' '${ chunk } ' ${ op } ${ tmpPath } .b64` ] ,
1023+ { timeout : 30 } ,
1024+ )
1025+ if ( result . exitCode !== 0 ) {
1026+ throw new ExecFailedError ( {
1027+ operation : 'uploadDir:exec-write' ,
1028+ exitCode : result . exitCode ,
1029+ stderr : result . stderr ,
1030+ } )
1031+ }
1032+ }
1033+
1034+ // Decode base64 and extract
1035+ const decodeResult = await sb . exec (
1036+ [ 'sh' , '-c' , `base64 -d ${ tmpPath } .b64 > ${ tmpPath } && rm -f ${ tmpPath } .b64` ] ,
1037+ { timeout : 30 } ,
1038+ )
1039+ if ( decodeResult . exitCode !== 0 ) {
1040+ throw new ExecFailedError ( {
1041+ operation : 'uploadDir:exec-decode' ,
1042+ exitCode : decodeResult . exitCode ,
1043+ stderr : decodeResult . stderr ,
1044+ } )
1045+ }
1046+
1047+ // mkdir, validate, extract (same as SDK uploadDir)
1048+ const mkdirResult = await sb . exec ( [ 'mkdir' , '-p' , remotePath ] , { timeout : 10 } )
1049+ if ( mkdirResult . exitCode !== 0 ) {
1050+ throw new ExecFailedError ( {
1051+ operation : 'uploadDir:mkdir' ,
1052+ exitCode : mkdirResult . exitCode ,
1053+ stderr : mkdirResult . stderr ,
1054+ } )
1055+ }
1056+
1057+ const extractResult = await sb . exec (
1058+ [ 'tar' , 'xzf' , tmpPath , '--no-same-owner' , '-C' , remotePath ] ,
1059+ { timeout : 60 } ,
1060+ )
1061+ if ( extractResult . exitCode !== 0 ) {
1062+ throw new ExecFailedError ( {
1063+ operation : 'uploadDir:extract' ,
1064+ exitCode : extractResult . exitCode ,
1065+ stderr : extractResult . stderr ,
1066+ } )
1067+ }
1068+
1069+ // Cleanup
1070+ await sb . exec ( [ 'rm' , '-f' , tmpPath ] , { timeout : 5 } ) . catch ( ( ) => { } )
1071+ }
9751072
9761073 let baselineCreated : boolean | undefined
9771074 if ( args . baseline ) {
@@ -1005,7 +1102,7 @@ export function registerTools(server: McpServer, sandchest: Sandchest): void {
10051102 ok : true ,
10061103 local_path : localPath ,
10071104 remote_path : remotePath ,
1008- method,
1105+ method : uploadMethod ,
10091106 bytes : tarball . byteLength ,
10101107 baseline_created : baselineCreated ,
10111108 } )
@@ -1169,6 +1266,7 @@ export function registerTools(server: McpServer, sandchest: Sandchest): void {
11691266
11701267 try {
11711268 const sb = await sandchest . get ( args . sandbox_id )
1269+ await ensureSandboxNetwork ( sb )
11721270 const result = await sb . git . clone ( validated . url , {
11731271 dest : args . path ,
11741272 branch : args . branch ,
0 commit comments