tpmjs/templates/vercel-executor/app/api/execute-tool/route.ts
Ajax Davis 32c6e097ed feat(executor): formalize Executor Protocol v1.0 with compliance testing
- Add EXECUTOR_SPECIFICATION.md with formal v1.0 protocol spec
- Add executor-openapi.yaml (OpenAPI 3.0 specification)
- Create @tpmjs/executor-test compliance test package (15 tests)
- Update Railway executor to v1.0 compliance (15/15 tests pass)
- Update Unsandbox executor to v1.0 compliance (15/15 tests pass)
- Update Vercel executor to v1.0 compliance
- Add /info endpoint with capability advertisement to all executors
- Add structured error codes (PACKAGE_NOT_FOUND, TOOL_NOT_FOUND, etc.)
- Add protocolVersion and implementationVersion to /health responses
- Add X-TPMJS-Protocol-Version header support
- Add EXECUTOR_COMPLIANCE.md with test results documentation
2026-02-04 02:09:08 +10:00

309 lines
8.6 KiB
TypeScript

/**
* Tool Execution Endpoint using Vercel Sandbox
*
* POST /api/execute-tool
* Execute a TPMJS tool in an isolated Vercel Sandbox VM
*/
import { Sandbox } from '@vercel/sandbox';
import { type NextRequest, NextResponse } from 'next/server';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 300; // 5 minutes max
interface ExecuteToolRequest {
packageName: string;
name: string;
version?: string;
params: Record<string, unknown>;
env?: Record<string, string>;
}
interface ExecuteToolSuccessResponse {
success: true;
output: unknown;
executionTimeMs: number;
}
interface ExecuteToolErrorResponse {
success: false;
error: {
code: string;
message: string;
};
executionTimeMs: number;
}
type ExecuteToolResponse = ExecuteToolSuccessResponse | ExecuteToolErrorResponse;
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-TPMJS-Protocol-Version',
};
export async function POST(req: NextRequest): Promise<NextResponse<ExecuteToolResponse>> {
const startTime = Date.now();
// Check authorization if EXECUTOR_API_KEY is set
const apiKey = process.env.EXECUTOR_API_KEY;
if (apiKey) {
const authHeader = req.headers.get('Authorization');
if (!authHeader || authHeader !== `Bearer ${apiKey}`) {
return NextResponse.json(
{
success: false,
error: {
code: 'UNAUTHORIZED',
message: 'Invalid or missing API key',
},
executionTimeMs: Date.now() - startTime,
} as ExecuteToolErrorResponse,
{ status: 401, headers: corsHeaders }
);
}
}
let sandbox: Sandbox | null = null;
try {
const body: ExecuteToolRequest = await req.json();
const { packageName, name, version = 'latest', params, env } = body;
if (!packageName || !name) {
return NextResponse.json(
{
success: false,
error: {
code: 'INVALID_REQUEST',
message: 'Missing required fields: packageName, name',
},
executionTimeMs: Date.now() - startTime,
} as ExecuteToolErrorResponse,
{ status: 400, headers: corsHeaders }
);
}
const packageSpec = `${packageName}@${version}`;
const cwd = '/vercel/sandbox';
// Create sandbox with node22 runtime
sandbox = await Sandbox.create({
runtime: 'node22',
timeout: 2 * 60 * 1000, // 2 minute timeout
resources: { vcpus: 2 },
});
// 1) Install the npm package
console.log(`[executor] Installing ${packageSpec}...`);
const installStart = Date.now();
const install = await sandbox.runCommand({
cmd: 'npm',
args: ['install', '--no-save', '--omit=dev', '--no-audit', '--no-fund', packageSpec],
cwd,
});
const installStderr = await install.stderr();
const installStdout = await install.stdout();
const installTime = Date.now() - installStart;
console.log(`[executor] npm install completed in ${installTime}ms (exit: ${install.exitCode})`);
if (install.exitCode !== 0) {
console.error(`[executor] npm install failed:`, {
exitCode: install.exitCode,
stdout: installStdout?.slice(0, 500),
stderr: installStderr?.slice(0, 500),
});
return NextResponse.json(
{
success: false,
error: {
code: 'PACKAGE_NOT_FOUND',
message: `npm install failed for ${packageSpec}: ${installStderr || installStdout}`,
},
executionTimeMs: Date.now() - startTime,
} as ExecuteToolErrorResponse,
{ headers: corsHeaders }
);
}
// 2) Build environment setup for the script
const envSetup = env
? Object.entries(env)
.map(([key, value]) => `process.env[${JSON.stringify(key)}] = ${JSON.stringify(value)};`)
.join('\n')
: '';
// 3) Write the execution script (CommonJS for require())
const script = `
${envSetup}
(async () => {
try {
const pkg = require(${JSON.stringify(packageName)});
// Get the tool export
let tool = pkg[${JSON.stringify(name)}] || pkg.default?.[${JSON.stringify(name)}] || pkg.default;
if (!tool) {
throw new Error('Tool "${name}" not found in package "${packageName}"');
}
// Handle factory functions
if (typeof tool === 'function' && !tool.execute) {
const envVars = ${env ? JSON.stringify(env) : 'null'};
// 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) {
process.stderr.write(JSON.stringify({ __tpmjs_error__: err.message || String(err) }));
process.exitCode = 1;
}
})();
`.trim();
await sandbox.writeFiles([{ path: 'execute.cjs', content: Buffer.from(script, 'utf8') }]);
// 4) Run the script
console.log(`[executor] Running tool ${packageName}/${name}...`);
const runStart = Date.now();
const run = await sandbox.runCommand({
cmd: 'node',
args: ['execute.cjs'],
cwd,
env: env || {},
});
const stdout = await run.stdout();
const stderr = await run.stderr();
const runTime = Date.now() - runStart;
console.log(`[executor] Tool execution completed in ${runTime}ms (exit: ${run.exitCode})`);
if (run.exitCode !== 0) {
console.error(`[executor] Tool execution failed:`, {
exitCode: run.exitCode,
stdout: stdout?.slice(0, 500),
stderr: stderr?.slice(0, 500),
});
// Try to parse error from stderr
try {
const errorObj = JSON.parse(stderr);
if (errorObj.__tpmjs_error__) {
const errorMessage = errorObj.__tpmjs_error__;
// Determine error code based on message
let code = 'TOOL_EXECUTION_ERROR';
if (errorMessage.includes('not found in package')) {
code = 'TOOL_NOT_FOUND';
} else if (errorMessage.includes('does not have an execute()')) {
code = 'TOOL_INVALID';
}
return NextResponse.json(
{
success: false,
error: {
code,
message: errorMessage,
},
executionTimeMs: Date.now() - startTime,
} as ExecuteToolErrorResponse,
{ headers: corsHeaders }
);
}
} catch {}
return NextResponse.json(
{
success: false,
error: {
code: 'TOOL_EXECUTION_ERROR',
message: stderr || `Script exited with code ${run.exitCode}`,
},
executionTimeMs: Date.now() - startTime,
} as ExecuteToolErrorResponse,
{ headers: corsHeaders }
);
}
// 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,
} as ExecuteToolSuccessResponse,
{ headers: corsHeaders }
);
}
} catch {}
// If we couldn't parse structured output, return raw
return NextResponse.json(
{
success: true,
output: stdout || null,
executionTimeMs: Date.now() - startTime,
} as ExecuteToolSuccessResponse,
{ headers: corsHeaders }
);
} catch (error) {
return NextResponse.json(
{
success: false,
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : String(error),
},
executionTimeMs: Date.now() - startTime,
} as ExecuteToolErrorResponse,
{ headers: corsHeaders }
);
} finally {
if (sandbox) {
try {
await sandbox.stop();
} catch {
// Ignore cleanup errors
}
}
}
}
// Handle OPTIONS for CORS preflight
export async function OPTIONS(): Promise<NextResponse> {
return new NextResponse(null, {
status: 200,
headers: corsHeaders,
});
}