fix(executor): use correct Vercel Sandbox SDK API
Key fixes:
- Use await result.stdout() / stderr() instead of result.stdout property
- Use sandbox.writeFiles([{ path, content: Buffer }]) correctly
- Use object form of runCommand with cmd, args, cwd, env
- Proper error handling and result parsing
This commit is contained in:
parent
e2153120f9
commit
9bcc1a8c15
1 changed files with 89 additions and 117 deletions
|
|
@ -24,6 +24,7 @@ interface ExecuteToolResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
output?: unknown;
|
output?: unknown;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
stderr?: string;
|
||||||
executionTimeMs: number;
|
executionTimeMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,169 +61,141 @@ export async function POST(req: NextRequest): Promise<NextResponse<ExecuteToolRe
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageSpec = `${packageName}@${version}`;
|
const packageSpec = `${packageName}@${version}`;
|
||||||
|
const cwd = '/vercel/sandbox';
|
||||||
|
|
||||||
// Create sandbox with node22 runtime
|
// Create sandbox with node22 runtime
|
||||||
sandbox = await Sandbox.create({
|
sandbox = await Sandbox.create({
|
||||||
runtime: 'node22',
|
runtime: 'node22',
|
||||||
timeout: 2 * 60 * 1000, // 2 minute timeout for the sandbox
|
timeout: 2 * 60 * 1000, // 2 minute timeout
|
||||||
|
resources: { vcpus: 2 },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Install the npm package
|
// 1) Install the npm package
|
||||||
await sandbox.runCommand({
|
const install = await sandbox.runCommand({
|
||||||
cmd: 'npm',
|
cmd: 'npm',
|
||||||
args: ['install', '--no-save', packageSpec],
|
args: ['install', '--no-save', '--omit=dev', '--no-audit', '--no-fund', packageSpec],
|
||||||
cwd: '/vercel/sandbox',
|
cwd,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build environment setup code
|
const installStderr = await install.stderr();
|
||||||
|
if (install.exitCode !== 0) {
|
||||||
|
const installStdout = await install.stdout();
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `npm install failed with exit code ${install.exitCode}`,
|
||||||
|
stderr: installStderr || installStdout,
|
||||||
|
executionTimeMs: Date.now() - startTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Build environment setup for the script
|
||||||
const envSetup = env
|
const envSetup = env
|
||||||
? Object.entries(env)
|
? Object.entries(env)
|
||||||
.map(([key, value]) => `process.env[${JSON.stringify(key)}] = ${JSON.stringify(value)};`)
|
.map(([key, value]) => `process.env[${JSON.stringify(key)}] = ${JSON.stringify(value)};`)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// Create the execution script
|
// 3) Write the execution script (CommonJS for require())
|
||||||
const script = `
|
const script = `
|
||||||
${envSetup}
|
${envSetup}
|
||||||
|
|
||||||
const pkg = require('${packageName}');
|
|
||||||
|
|
||||||
// Get the tool export
|
|
||||||
let tool = pkg['${name}'] || pkg.default?.['${name}'] || pkg.default;
|
|
||||||
|
|
||||||
if (!tool) {
|
|
||||||
console.log(JSON.stringify({ __tpmjs_error__: 'Tool "${name}" not found in package "${packageName}"' }));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle factory functions (tools that need to be called to create the tool instance)
|
|
||||||
async function resolveFactory(rawTool, envVars) {
|
|
||||||
if (typeof rawTool !== 'function' || rawTool.execute) {
|
|
||||||
return rawTool;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 1: Try calling with no arguments
|
|
||||||
try {
|
|
||||||
const result = rawTool();
|
|
||||||
if (result && typeof result.execute === 'function') {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
// Strategy 2: Try with env config object
|
|
||||||
if (envVars) {
|
|
||||||
const configVariations = [
|
|
||||||
envVars,
|
|
||||||
// Extract API key if present
|
|
||||||
(() => {
|
|
||||||
const entry = Object.entries(envVars).find(([k]) => k.toUpperCase().includes('API_KEY'));
|
|
||||||
return entry ? { apiKey: entry[1] } : null;
|
|
||||||
})(),
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
for (const config of configVariations) {
|
|
||||||
try {
|
|
||||||
const result = rawTool(config);
|
|
||||||
if (result && typeof result.execute === 'function') {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawTool;
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const envVars = ${env ? JSON.stringify(env) : 'null'};
|
const pkg = require(${JSON.stringify(packageName)});
|
||||||
const resolvedTool = await resolveFactory(tool, envVars);
|
|
||||||
|
|
||||||
if (!resolvedTool || typeof resolvedTool.execute !== 'function') {
|
// Get the tool export
|
||||||
console.log(JSON.stringify({ __tpmjs_error__: 'Tool "${name}" does not have an execute() function' }));
|
let tool = pkg[${JSON.stringify(name)}] || pkg.default?.[${JSON.stringify(name)}] || pkg.default;
|
||||||
process.exit(1);
|
|
||||||
|
if (!tool) {
|
||||||
|
throw new Error('Tool "${name}" not found in package "${packageName}"');
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = ${JSON.stringify(params)};
|
// Handle factory functions
|
||||||
const result = await resolvedTool.execute(params);
|
if (typeof tool === 'function' && !tool.execute) {
|
||||||
|
const envVars = ${env ? JSON.stringify(env) : 'null'};
|
||||||
|
|
||||||
console.log(JSON.stringify({ __tpmjs_result__: result }));
|
// Try no-arg call first
|
||||||
|
try {
|
||||||
|
const result = tool();
|
||||||
|
if (result && typeof result.execute === 'function') {
|
||||||
|
tool = result;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Try with env config
|
||||||
|
if (typeof tool === 'function' && envVars) {
|
||||||
|
try {
|
||||||
|
const result = tool(envVars);
|
||||||
|
if (result && typeof result.execute === 'function') {
|
||||||
|
tool = result;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tool || typeof tool.execute !== 'function') {
|
||||||
|
throw new Error('Tool "${name}" does not have an execute() function');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await tool.execute(${JSON.stringify(params)});
|
||||||
|
process.stdout.write(JSON.stringify({ __tpmjs_result__: result }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(JSON.stringify({ __tpmjs_error__: err.message || String(err) }));
|
process.stderr.write(JSON.stringify({ __tpmjs_error__: err.message || String(err) }));
|
||||||
process.exit(1);
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
`;
|
`.trim();
|
||||||
|
|
||||||
// Write the script to a file
|
await sandbox.writeFiles([{ path: 'execute.cjs', content: Buffer.from(script, 'utf8') }]);
|
||||||
await sandbox.writeFiles([
|
|
||||||
{ path: '/vercel/sandbox/execute.cjs', content: Buffer.from(script) },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Run the script and capture output
|
// 4) Run the script
|
||||||
let stdout = '';
|
const run = await sandbox.runCommand({
|
||||||
let stderr = '';
|
|
||||||
|
|
||||||
await sandbox.runCommand({
|
|
||||||
cmd: 'node',
|
cmd: 'node',
|
||||||
args: ['execute.cjs'],
|
args: ['execute.cjs'],
|
||||||
cwd: '/vercel/sandbox',
|
cwd,
|
||||||
stdout: {
|
env: env || {},
|
||||||
write(chunk: Buffer | string) {
|
|
||||||
stdout += chunk.toString();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
} as NodeJS.WritableStream,
|
|
||||||
stderr: {
|
|
||||||
write(chunk: Buffer | string) {
|
|
||||||
stderr += chunk.toString();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
} as NodeJS.WritableStream,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Parse the output to extract the result
|
const stdout = await run.stdout();
|
||||||
const lines = stdout.split('\n');
|
const stderr = await run.stderr();
|
||||||
for (const line of lines) {
|
|
||||||
if (line.includes('__tpmjs_result__')) {
|
if (run.exitCode !== 0) {
|
||||||
try {
|
// Try to parse error from stderr
|
||||||
const parsed = JSON.parse(line);
|
try {
|
||||||
return NextResponse.json({
|
const errorObj = JSON.parse(stderr);
|
||||||
success: true,
|
if (errorObj.__tpmjs_error__) {
|
||||||
output: parsed.__tpmjs_result__,
|
|
||||||
executionTimeMs: Date.now() - startTime,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Continue searching
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (line.includes('__tpmjs_error__')) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(line);
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: parsed.__tpmjs_error__,
|
error: errorObj.__tpmjs_error__,
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
} catch {
|
|
||||||
// Continue searching
|
|
||||||
}
|
}
|
||||||
}
|
} catch {}
|
||||||
}
|
|
||||||
|
|
||||||
// If we couldn't find structured output, check stderr
|
|
||||||
if (stderr) {
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: stderr.trim(),
|
error: stderr || `Script exited with code ${run.exitCode}`,
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return raw stdout if no structured result
|
// 5) Parse the result
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stdout);
|
||||||
|
if (parsed.__tpmjs_result__ !== undefined) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
output: parsed.__tpmjs_result__,
|
||||||
|
executionTimeMs: Date.now() - startTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// If we couldn't parse structured output, return raw
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
output: stdout.trim() || null,
|
output: stdout || null,
|
||||||
|
stderr: stderr || undefined,
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -232,7 +205,6 @@ async function resolveFactory(rawTool, envVars) {
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// Always stop the sandbox
|
|
||||||
if (sandbox) {
|
if (sandbox) {
|
||||||
try {
|
try {
|
||||||
await sandbox.stop();
|
await sandbox.stop();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue