Skip to content

Commit fe2bc50

Browse files
committed
Sandbox warning
1 parent 7049cec commit fe2bc50

4 files changed

Lines changed: 70 additions & 12 deletions

File tree

apps/sim/app/api/function/execute/route.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,37 @@ function resolveCodeVariables(
679679
* This handles the common case where print() or console.log() adds a trailing \n
680680
* that users don't expect to see in the output
681681
*/
682+
/**
683+
* Heuristic: did the sandbox die from an infrastructure failure (OOM kill,
684+
* timeout, lost connection) rather than a normal code error? Python/JS code
685+
* exceptions surface via execution.error; an OOM kill instead makes runCode
686+
* throw, often with an empty or cryptic message.
687+
*/
688+
function isLikelySandboxKill(error: any): boolean {
689+
const msg = `${error?.name ?? ''} ${error?.message ?? ''} ${error?.code ?? ''}`
690+
.toLowerCase()
691+
.trim()
692+
if (!msg) return true
693+
return [
694+
'out of memory',
695+
'oom',
696+
'killed',
697+
'sigkill',
698+
'code 137',
699+
'signal 9',
700+
'terminated',
701+
'econnreset',
702+
'epipe',
703+
'socket hang up',
704+
'connection closed',
705+
'connection reset',
706+
'websocket',
707+
'timed out',
708+
'timeout',
709+
'deadline',
710+
].some((s) => msg.includes(s))
711+
}
712+
682713
function cleanStdout(stdout: string): string {
683714
if (stdout.endsWith('\n')) {
684715
return stdout.slice(0, -1)
@@ -1833,6 +1864,24 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
18331864
)
18341865
}
18351866

1867+
if (isLikelySandboxKill(error)) {
1868+
const underlying = (error?.message || String(error)).slice(0, 300)
1869+
logger.warn(`[${requestId}] Sandbox terminated before completion (likely OOM or timeout)`, {
1870+
executionTime,
1871+
underlying,
1872+
})
1873+
const killResponse = {
1874+
success: false,
1875+
error:
1876+
'The sandbox was terminated before finishing — most likely it ran out of memory or hit the time limit while processing large or combined inputs. Mount and process fewer/smaller files at once (e.g. one file at a time), or stream and aggregate incrementally instead of loading everything into memory. ' +
1877+
`(underlying: ${underlying || 'no detail; sandbox died'})`,
1878+
output: { result: null, stdout: cleanStdout(stdout), executionTime },
1879+
}
1880+
return routeContext
1881+
? functionJsonResponse(killResponse, routeContext, { status: 500 })
1882+
: NextResponse.json(killResponse, { status: 500 })
1883+
}
1884+
18361885
logger.error(`[${requestId}] Function execution failed`, {
18371886
error: error.message || 'Unknown error',
18381887
stack: error.stack,

apps/sim/lib/copilot/generated/tool-catalog-v1.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,7 @@ export const FunctionExecute: ToolCatalogEntry = {
11261126
sandboxPath: {
11271127
type: 'string',
11281128
description:
1129-
'Optional full sandbox path override. Omit to mount at /home/user/{path}.',
1129+
'Full sandbox path to mount at, e.g. /home/user/inputs/data.csv. STRONGLY RECOMMENDED whenever the file name has spaces or special characters: the default mount path is the percent-ENCODED canonical path (e.g. /home/user/files/Q4%20Sales%20(Final).csv), which code using the human-readable name will not find. Set a simple sandboxPath and read exactly that.',
11301130
},
11311131
},
11321132
required: ['path'],
@@ -1288,7 +1288,7 @@ export const GenerateImage: ToolCatalogEntry = {
12881288
sandboxPath: {
12891289
type: 'string',
12901290
description:
1291-
'Optional full sandbox path override. Omit to mount at /home/user/{path}.',
1291+
'Full sandbox path to mount at, e.g. /home/user/inputs/data.csv. STRONGLY RECOMMENDED whenever the file name has spaces or special characters: the default mount path is the percent-ENCODED canonical path (e.g. /home/user/files/Q4%20Sales%20(Final).csv), which code using the human-readable name will not find. Set a simple sandboxPath and read exactly that.',
12921292
},
12931293
},
12941294
required: ['path'],

apps/sim/lib/copilot/generated/tool-schemas-v1.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -921,7 +921,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
921921
sandboxPath: {
922922
type: 'string',
923923
description:
924-
'Optional full sandbox path override. Omit to mount at /home/user/{path}.',
924+
'Full sandbox path to mount at, e.g. /home/user/inputs/data.csv. STRONGLY RECOMMENDED whenever the file name has spaces or special characters: the default mount path is the percent-ENCODED canonical path (e.g. /home/user/files/Q4%20Sales%20(Final).csv), which code using the human-readable name will not find. Set a simple sandboxPath and read exactly that.',
925925
},
926926
},
927927
required: ['path'],
@@ -1078,7 +1078,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
10781078
sandboxPath: {
10791079
type: 'string',
10801080
description:
1081-
'Optional full sandbox path override. Omit to mount at /home/user/{path}.',
1081+
'Full sandbox path to mount at, e.g. /home/user/inputs/data.csv. STRONGLY RECOMMENDED whenever the file name has spaces or special characters: the default mount path is the percent-ENCODED canonical path (e.g. /home/user/files/Q4%20Sales%20(Final).csv), which code using the human-readable name will not find. Set a simple sandboxPath and read exactly that.',
10821082
},
10831083
},
10841084
required: ['path'],

apps/sim/lib/copilot/tools/handlers/function-execute.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,24 @@ async function resolveInputFiles(
9494
}
9595
const record = findWorkspaceFileRecord(allFiles, alias?.backingPath ?? filePath)
9696
if (!record) {
97-
logger.warn('Input file not found', { fileRef })
98-
continue
97+
if (filePath.startsWith('uploads/')) {
98+
throw new Error(
99+
`Cannot mount "${filePath}": uploads/ files are not mountable into the sandbox. Use materialize_file to save it to a files/... path first, then mount that canonical path.`
100+
)
101+
}
102+
throw new Error(
103+
`Input file not found: "${filePath}". Pass the exact canonical VFS path copied from glob/read (e.g. "files/Reports/data.csv").`
104+
)
99105
}
100106
if (record.size > MAX_FILE_SIZE) {
101-
logger.warn('Input file exceeds size limit', { fileId: record.id, size: record.size })
102-
continue
107+
throw new Error(
108+
`Input file "${filePath}" is ${Math.round(record.size / 1024 / 1024)}MB, over the ${MAX_FILE_SIZE / 1024 / 1024}MB per-file mount limit.`
109+
)
103110
}
104111
if (totalSize + record.size > MAX_TOTAL_SIZE) {
105-
logger.warn('Total input size limit reached')
106-
break
112+
throw new Error(
113+
`Mounting "${filePath}" would exceed the ${MAX_TOTAL_SIZE / 1024 / 1024}MB total mount limit. Mount fewer or smaller files.`
114+
)
107115
}
108116
const buffer = await fetchWorkspaceFileBuffer(record)
109117
totalSize += buffer.length
@@ -249,8 +257,9 @@ async function resolveInputFiles(
249257
if (!tableId) continue
250258
const table = await resolveTableRef(tableId, tablePathLookup)
251259
if (!table || table.workspaceId !== workspaceId) {
252-
logger.warn('Input table not found', { tableId })
253-
continue
260+
throw new Error(
261+
`Input table not found: "${tableId}". Pass the table id (tbl_...) from tables/{name}/meta.json, or a tables/{name}/meta.json path.`
262+
)
254263
}
255264
const rows = await queryRows(table, {}, 'copilot-fn-exec')
256265

0 commit comments

Comments
 (0)