Skip to content

Commit 37b91d7

Browse files
committed
Add sandbox network setup and upload fallback
1 parent 93a32a6 commit 37b91d7

1 file changed

Lines changed: 101 additions & 3 deletions

File tree

packages/mcp/src/tools.ts

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,46 @@ import { basename, dirname, join, resolve, sep } from 'node:path'
1616
import { gunzipSync } from 'node:zlib'
1717
import { z } from 'zod'
1818
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
19-
import type { Sandchest } from '@sandchest/sdk'
19+
import type { Sandchest, Sandbox } from '@sandchest/sdk'
2020
import { ExecFailedError, SandchestError, Session } from '@sandchest/sdk'
2121

2222
function 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(/^sandchest\.(ip|gw|dns)=(.+)$/)
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+
2659
const EXEC_OUTPUT_CAP_BYTES = 1_048_576
2760
const ALLOWED_PATHS_ENV = 'SANDCHEST_MCP_ALLOWED_PATHS'
2861
const 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

Comments
 (0)