Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions app/api/sandboxes/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,22 @@ export async function OPTIONS() {
/**
* POST /api/sandboxes
*
* Creates a new ephemeral sandbox environment and executes a command.
* Creates a new ephemeral sandbox environment. Optionally executes a command.
* Sandboxes are isolated Linux microVMs that can be used to evaluate
* account-generated code, run AI agent output safely, or execute reproducible tasks.
* The sandbox will automatically stop after the timeout period.
*
* Authentication: x-api-key header or Authorization Bearer token required.
*
* Request body:
* - command: string (required) - The command to execute in the sandbox
* - command: string (optional) - The command to execute in the sandbox. If omitted, sandbox is created without running any command.
* - args: string[] (optional) - Arguments to pass to the command
* - cwd: string (optional) - Working directory for command execution
*
* Response (200):
* - status: "success"
* - sandboxes: [{ sandboxId, sandboxStatus, timeout, createdAt, runId }]
* - sandboxes: [{ sandboxId, sandboxStatus, timeout, createdAt, runId? }]
* - runId is only included when a command was provided
*
* Error (400/401):
* - status: "error"
Expand Down
33 changes: 18 additions & 15 deletions lib/sandbox/createSandboxPostHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import { selectAccountSnapshots } from "@/lib/supabase/account_snapshots/selectA
/**
* Handler for POST /api/sandboxes.
*
* Creates a Vercel Sandbox (from account's snapshot if available, otherwise fresh)
* and triggers the run-sandbox-command task to execute the command.
* Creates a Vercel Sandbox (from account's snapshot if available, otherwise fresh).
* If a command is provided, triggers the run-sandbox-command task to execute it.
* If no command is provided, simply creates the sandbox without running any command.
* Requires authentication via x-api-key header or Authorization Bearer token.
* Saves sandbox info to the account_sandboxes table.
*
* @param request - The request object
* @returns A NextResponse with sandbox creation result including runId
* @returns A NextResponse with sandbox creation result (includes runId only if command was provided)
*/
export async function createSandboxPostHandler(request: NextRequest): Promise<NextResponse> {
const validated = await validateSandboxBody(request);
Expand All @@ -39,19 +40,21 @@ export async function createSandboxPostHandler(request: NextRequest): Promise<Ne
sandbox_id: result.sandboxId,
});

// Trigger the command execution task
// Trigger the command execution task only if a command was provided
let runId: string | undefined;
try {
const handle = await triggerRunSandboxCommand({
command: validated.command,
args: validated.args,
cwd: validated.cwd,
sandboxId: result.sandboxId,
accountId: validated.accountId,
});
runId = handle.id;
} catch (triggerError) {
console.error("Failed to trigger run-sandbox-command task:", triggerError);
if (validated.command) {
try {
const handle = await triggerRunSandboxCommand({
command: validated.command,
args: validated.args,
cwd: validated.cwd,
sandboxId: result.sandboxId,
accountId: validated.accountId,
});
runId = handle.id;
} catch (triggerError) {
console.error("Failed to trigger run-sandbox-command task:", triggerError);
}
Comment on lines +43 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Return an error when a requested command fails to trigger.

Right now a client can send command and still get a success response without runId if the trigger fails. That’s indistinguishable from “no command provided” and hides execution failures.

🐛 Suggested change
     if (validated.command) {
       try {
         const handle = await triggerRunSandboxCommand({
           command: validated.command,
           args: validated.args,
           cwd: validated.cwd,
           sandboxId: result.sandboxId,
           accountId: validated.accountId,
         });
         runId = handle.id;
       } catch (triggerError) {
         console.error("Failed to trigger run-sandbox-command task:", triggerError);
+        return NextResponse.json(
+          { status: "error", error: "Failed to run command in sandbox" },
+          { status: 502, headers: getCorsHeaders() },
+        );
       }
     }
🤖 Prompt for AI Agents
In `@lib/sandbox/createSandboxPostHandler.ts` around lines 43 - 57, The current
catch in createSandboxPostHandler swallows triggerRunSandboxCommand failures,
making a missing runId indistinguishable from "no command provided"; change the
catch in the block that calls triggerRunSandboxCommand to propagate a proper
error response instead of just logging: when triggerRunSandboxCommand({ command:
validated.command, args: validated.args, cwd: validated.cwd, sandboxId:
result.sandboxId, accountId: validated.accountId }) throws, rethrow or return a
4xx/5xx error (include the original error message) so the handler returns
failure to the client rather than a success without runId; update any
callers/response flow that rely on runId accordingly.

}

return NextResponse.json(
Expand Down
2 changes: 1 addition & 1 deletion lib/sandbox/validateSandboxBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { safeParseJson } from "@/lib/networking/safeParseJson";
import { z } from "zod";

export const sandboxBodySchema = z.object({
command: z.string({ message: "command is required" }).min(1, "command cannot be empty"),
command: z.string().min(1, "command cannot be empty").optional(),
args: z.array(z.string()).optional(),
cwd: z.string().optional(),
});
Expand Down
Loading