From 06d6a95704871e9bc58b2bc2b6f22d7d1173280c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 11 Feb 2026 16:13:47 -0500 Subject: [PATCH 1/3] feat: route all commands through Vercel Sandbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove client-side just-bash/browser dependency for command execution. All terminal commands now go through /api/exec which creates and reuses a Vercel Sandbox VM, eliminating split logic between local and remote command execution. - Add /api/exec endpoint that creates/reuses sandbox sessions - Refactor input-handler to accept generic exec function - Refactor agent-command to standalone handler (no defineCommand) - Update Terminal.tsx to route commands: static (local) → agent (API) → sandbox (API) Co-Authored-By: Claude Opus 4.6 --- app/api/exec/route.ts | 113 ++++++++++++++++ app/components/Terminal.tsx | 127 +++++++++++++----- .../terminal-parts/agent-command.ts | 18 +-- app/components/terminal-parts/index.ts | 3 +- .../terminal-parts/input-handler.ts | 15 ++- 5 files changed, 228 insertions(+), 48 deletions(-) create mode 100644 app/api/exec/route.ts diff --git a/app/api/exec/route.ts b/app/api/exec/route.ts new file mode 100644 index 00000000..e9272569 --- /dev/null +++ b/app/api/exec/route.ts @@ -0,0 +1,113 @@ +import { Sandbox } from "@vercel/sandbox"; +import { readdirSync, readFileSync } from "fs"; +import { dirname, join, relative } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const AGENT_DATA_DIR = join(__dirname, "../agent/_agent-data"); +const SANDBOX_CWD = "/home/user"; + +function readSourceFiles( + dir: string, + baseDir?: string +): Array<{ path: string; content: Buffer }> { + const base = baseDir ?? dir; + const files: Array<{ path: string; content: Buffer }> = []; + + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === ".git") continue; + files.push(...readSourceFiles(fullPath, base)); + } else { + const relPath = relative(base, fullPath); + files.push({ + path: join(SANDBOX_CWD, relPath), + content: readFileSync(fullPath), + }); + } + } + + return files; +} + +async function createAndSeedSandbox(): Promise { + const sandbox = await Sandbox.create(); + + const files = readSourceFiles(AGENT_DATA_DIR); + if (files.length > 0) { + await sandbox.writeFiles(files); + } + + // Create convenience copies of top-level demo files + await sandbox.runCommand({ + cmd: "bash", + args: [ + "-c", + [ + `mkdir -p ${SANDBOX_CWD}/dirs/are/fun/author`, + `cp ${SANDBOX_CWD}/just-bash/README.md ${SANDBOX_CWD}/README.md 2>/dev/null || true`, + `cp ${SANDBOX_CWD}/just-bash/LICENSE ${SANDBOX_CWD}/LICENSE 2>/dev/null || true`, + `cp ${SANDBOX_CWD}/just-bash/package.json ${SANDBOX_CWD}/package.json 2>/dev/null || true`, + `echo 'https://x.com/cramforce' > ${SANDBOX_CWD}/dirs/are/fun/author/info.txt`, + ].join(" && "), + ], + cwd: SANDBOX_CWD, + }); + + return sandbox; +} + +export async function POST(req: Request) { + const authHeader = req.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { command, sandboxId } = await req.json(); + + if (!command || typeof command !== "string") { + return Response.json({ error: "Command is required" }, { status: 400 }); + } + + let sandbox: Sandbox; + let activeSandboxId: string; + + if (sandboxId) { + try { + sandbox = await Sandbox.get({ sandboxId }); + activeSandboxId = sandboxId; + } catch { + sandbox = await createAndSeedSandbox(); + activeSandboxId = sandbox.sandboxId; + } + } else { + sandbox = await createAndSeedSandbox(); + activeSandboxId = sandbox.sandboxId; + } + + try { + const result = await sandbox.runCommand({ + cmd: "bash", + args: ["-c", command], + cwd: SANDBOX_CWD, + }); + + const stdout = await result.stdout(); + const stderr = await result.stderr(); + + return Response.json({ + stdout, + stderr, + exitCode: result.exitCode, + sandboxId: activeSandboxId, + }); + } catch (error) { + return Response.json({ + stdout: "", + stderr: error instanceof Error ? error.message : "Execution failed", + exitCode: 1, + sandboxId: activeSandboxId, + }); + } +} diff --git a/app/components/Terminal.tsx b/app/components/Terminal.tsx index 0eba43fb..b8343815 100644 --- a/app/components/Terminal.tsx +++ b/app/components/Terminal.tsx @@ -1,24 +1,18 @@ "use client"; import { useEffect, useRef } from "react"; -import { Bash } from "just-bash/browser"; -import { getTerminalData } from "./TerminalData"; import { - createStaticCommands, - createAgentCommand, + CMD_ABOUT, + CMD_INSTALL, + CMD_GITHUB, +} from "./terminal-content"; +import { + createAgentHandler, createInputHandler, showWelcome, } from "./terminal-parts"; import { LiteTerminal } from "./lite-terminal"; -async function fetchFiles(bash: Bash) { - const response = await fetch("/api/fs"); - const files: Record = await response.json(); - for (const [path, content] of Object.entries(files)) { - bash.writeFile(path, content); - } -} - function getTheme(isDark: boolean) { return { background: isDark ? "#000" : "#fff", @@ -30,6 +24,19 @@ function getTheme(isDark: boolean) { }; } +type ExecResult = { + stdout: string; + stderr: string; + exitCode: number; +}; + +// Static commands handled client-side (no sandbox needed) +const staticCommands: Record ExecResult> = { + about: () => ({ stdout: CMD_ABOUT, stderr: "", exitCode: 0 }), + install: () => ({ stdout: CMD_INSTALL, stderr: "", exitCode: 0 }), + github: () => ({ stdout: CMD_GITHUB, stderr: "", exitCode: 0 }), +}; + export default function TerminalComponent({ getAccessToken, }: { @@ -49,31 +56,83 @@ export default function TerminalComponent({ }); term.open(container); - // Create commands - const { aboutCmd, installCmd, githubCmd } = createStaticCommands(); - const agentCmd = createAgentCommand(term, getAccessToken); - - // Files from DOM - const files = { - "/home/user/README.md": getTerminalData("file-readme"), - "/home/user/LICENSE": getTerminalData("file-license"), - "/home/user/package.json": getTerminalData("file-package-json"), - "/home/user/AGENTS.md": getTerminalData("file-agents-md"), - "/home/user/wtf-is-this.md": getTerminalData("file-wtf-is-this"), - "/home/user/dirs/are/fun/author/info.txt": "https://x.com/cramforce\n", - }; + // Agent handler + const agentHandler = createAgentHandler(term, getAccessToken); - const bash = new Bash({ - customCommands: [aboutCmd, installCmd, githubCmd, agentCmd], - files, - cwd: "/home/user", - }); + // Sandbox session ID (persisted across commands) + let sandboxId: string | null = null; + + // Unified exec function - all commands go through sandbox + const exec = async (command: string): Promise => { + const trimmed = command.trim(); + const firstWord = trimmed.split(/\s+/)[0]; - // Set up input handling - const inputHandler = createInputHandler(term, bash); + // Static commands (about, install, github) - no sandbox needed + if (firstWord in staticCommands) { + return staticCommands[firstWord](); + } + + // Agent command - uses its own API endpoint + if (firstWord === "agent") { + let prompt = trimmed.slice(5).trim(); + // Strip surrounding quotes + if ( + (prompt.startsWith('"') && prompt.endsWith('"')) || + (prompt.startsWith("'") && prompt.endsWith("'")) + ) { + prompt = prompt.slice(1, -1); + } + return agentHandler(prompt); + } + + // All other commands → sandbox + const token = await getAccessToken(); + if (!token) { + return { + stdout: "", + stderr: "Error: Not authenticated. Please log in and try again.\n", + exitCode: 1, + }; + } + + try { + const res = await fetch("/api/exec", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ command: trimmed, sandboxId }), + }); + + if (!res.ok) { + return { + stdout: "", + stderr: `Error: ${res.status} ${res.statusText}\n`, + exitCode: 1, + }; + } + + const result = await res.json(); + if (result.sandboxId) { + sandboxId = result.sandboxId; + } + return { + stdout: result.stdout || "", + stderr: result.stderr || "", + exitCode: result.exitCode ?? 0, + }; + } catch (error) { + return { + stdout: "", + stderr: `Error: ${error instanceof Error ? error.message : "Unknown error"}\n`, + exitCode: 1, + }; + } + }; - // Load additional files from API into bash filesystem - void fetchFiles(bash); + // Set up input handling with unified exec + const inputHandler = createInputHandler(term, exec); // Track cleanup state let disposed = false; diff --git a/app/components/terminal-parts/agent-command.ts b/app/components/terminal-parts/agent-command.ts index b19aadcc..2c9d58b5 100644 --- a/app/components/terminal-parts/agent-command.ts +++ b/app/components/terminal-parts/agent-command.ts @@ -1,7 +1,12 @@ -import { defineCommand } from "just-bash/browser"; import { MAX_TOOL_OUTPUT_LINES } from "./constants"; import { formatMarkdown } from "./markdown"; +type ExecResult = { + stdout: string; + stderr: string; + exitCode: number; +}; + type UIMessage = { id: string; role: "user" | "assistant"; @@ -17,15 +22,14 @@ function formatForTerminal(text: string): string { return text.replace(/\t/g, " ").replace(/\r?\n/g, "\r\n"); } -export function createAgentCommand( +export function createAgentHandler( term: TerminalWriter, getAccessToken: () => Promise, -) { +): (prompt: string) => Promise { const agentMessages: UIMessage[] = []; let messageIdCounter = 0; - const agentCmd = defineCommand("agent", async (args) => { - const prompt = args.join(" "); + return async (prompt: string): Promise => { if (!prompt) { return { stdout: "", @@ -335,7 +339,5 @@ export function createAgentCommand( exitCode: 1, }; } - }); - - return agentCmd; + }; } diff --git a/app/components/terminal-parts/index.ts b/app/components/terminal-parts/index.ts index 331e2f31..5786cc49 100644 --- a/app/components/terminal-parts/index.ts +++ b/app/components/terminal-parts/index.ts @@ -1,6 +1,5 @@ export { ASCII_ART, HISTORY_KEY, MAX_HISTORY, MAX_TOOL_OUTPUT_LINES } from "./constants"; -export { createStaticCommands } from "./commands"; -export { createAgentCommand } from "./agent-command"; +export { createAgentHandler } from "./agent-command"; export { createInputHandler } from "./input-handler"; export { showWelcome } from "./welcome"; export { formatMarkdown } from "./markdown"; diff --git a/app/components/terminal-parts/input-handler.ts b/app/components/terminal-parts/input-handler.ts index b3e97bb7..9de3ba8e 100644 --- a/app/components/terminal-parts/input-handler.ts +++ b/app/components/terminal-parts/input-handler.ts @@ -1,4 +1,3 @@ -import type { Bash } from "just-bash/browser"; import { track } from "@vercel/analytics"; import { HISTORY_KEY, MAX_HISTORY } from "./constants"; import { formatMarkdown } from "./markdown"; @@ -10,6 +9,14 @@ type Terminal = { onData: (callback: (data: string) => void) => void; }; +type ExecResult = { + stdout: string; + stderr: string; + exitCode: number; +}; + +type ExecFn = (command: string) => Promise; + // Find the start of the previous word function findPrevWordBoundary(str: string, pos: number): number { @@ -47,7 +54,7 @@ function getCompletionContext(cmd: string, cursorPos: number): { prefix: string; }; } -export function createInputHandler(term: Terminal, bash: Bash) { +export function createInputHandler(term: Terminal, exec: ExecFn) { const history: string[] = JSON.parse( sessionStorage.getItem(HISTORY_KEY) || "[]" ); @@ -198,7 +205,7 @@ export function createInputHandler(term: Terminal, bash: Bash) { candidates = commands; } else { // Complete files from current directory - const lsResult = await bash.exec("ls -1"); + const lsResult = await exec("ls -1"); candidates = lsResult.stdout .split("\n") .map((f) => f.trim()) @@ -272,7 +279,7 @@ export function createInputHandler(term: Terminal, bash: Bash) { if (trimmed === "clear") { term.write("\x1b[2J\x1b[3J\x1b[H"); } else { - const result = await bash.exec(trimmed); + const result = await exec(trimmed); if (result.stdout) term.write( formatMarkdown(colorizeUrls(result.stdout)).replace(/\n/g, "\r\n") From 6806ee7b549b99ea24478c051d9be09d60fd7428 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 11 Feb 2026 16:20:02 -0500 Subject: [PATCH 2/3] fix: use /api/fs for sandbox file seeding, add error handling The previous approach read _agent-data from disk via __dirname, which doesn't work in serverless since each route is bundled independently. Now fetches files via the existing /api/fs endpoint. Added top-level try/catch so errors return useful messages instead of raw 500s. Co-Authored-By: Claude Opus 4.6 --- app/api/exec/route.ts | 172 +++++++++++++++++++++--------------------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/app/api/exec/route.ts b/app/api/exec/route.ts index e9272569..1ed4245a 100644 --- a/app/api/exec/route.ts +++ b/app/api/exec/route.ts @@ -1,113 +1,113 @@ import { Sandbox } from "@vercel/sandbox"; -import { readdirSync, readFileSync } from "fs"; -import { dirname, join, relative } from "path"; -import { fileURLToPath } from "url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const AGENT_DATA_DIR = join(__dirname, "../agent/_agent-data"); const SANDBOX_CWD = "/home/user"; -function readSourceFiles( - dir: string, - baseDir?: string -): Array<{ path: string; content: Buffer }> { - const base = baseDir ?? dir; - const files: Array<{ path: string; content: Buffer }> = []; - - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { - if (entry.name === "node_modules" || entry.name === ".git") continue; - files.push(...readSourceFiles(fullPath, base)); - } else { - const relPath = relative(base, fullPath); - files.push({ - path: join(SANDBOX_CWD, relPath), - content: readFileSync(fullPath), - }); - } - } - - return files; -} - async function createAndSeedSandbox(): Promise { const sandbox = await Sandbox.create(); - const files = readSourceFiles(AGENT_DATA_DIR); - if (files.length > 0) { - await sandbox.writeFiles(files); + // Seed sandbox with source files from the fs API + try { + const baseUrl = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000"; + const res = await fetch(`${baseUrl}/api/fs`); + if (res.ok) { + const filesMap: Record = await res.json(); + const files = Object.entries(filesMap).map(([path, content]) => ({ + path: `${SANDBOX_CWD}/${path}`, + content: Buffer.from(content), + })); + if (files.length > 0) { + await sandbox.writeFiles(files); + } + } + } catch { + // File seeding is best-effort — sandbox still works without files } // Create convenience copies of top-level demo files - await sandbox.runCommand({ - cmd: "bash", - args: [ - "-c", - [ - `mkdir -p ${SANDBOX_CWD}/dirs/are/fun/author`, - `cp ${SANDBOX_CWD}/just-bash/README.md ${SANDBOX_CWD}/README.md 2>/dev/null || true`, - `cp ${SANDBOX_CWD}/just-bash/LICENSE ${SANDBOX_CWD}/LICENSE 2>/dev/null || true`, - `cp ${SANDBOX_CWD}/just-bash/package.json ${SANDBOX_CWD}/package.json 2>/dev/null || true`, - `echo 'https://x.com/cramforce' > ${SANDBOX_CWD}/dirs/are/fun/author/info.txt`, - ].join(" && "), - ], - cwd: SANDBOX_CWD, - }); + try { + await sandbox.runCommand({ + cmd: "bash", + args: [ + "-c", + [ + `mkdir -p ${SANDBOX_CWD}/dirs/are/fun/author`, + `cp ${SANDBOX_CWD}/just-bash/README.md ${SANDBOX_CWD}/README.md 2>/dev/null || true`, + `cp ${SANDBOX_CWD}/just-bash/LICENSE ${SANDBOX_CWD}/LICENSE 2>/dev/null || true`, + `cp ${SANDBOX_CWD}/just-bash/package.json ${SANDBOX_CWD}/package.json 2>/dev/null || true`, + `echo 'https://x.com/cramforce' > ${SANDBOX_CWD}/dirs/are/fun/author/info.txt`, + ].join(" && "), + ], + cwd: SANDBOX_CWD, + }); + } catch { + // Best-effort file setup + } return sandbox; } export async function POST(req: Request) { - const authHeader = req.headers.get("Authorization"); - if (!authHeader?.startsWith("Bearer ")) { - return Response.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { command, sandboxId } = await req.json(); + try { + const authHeader = req.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } - if (!command || typeof command !== "string") { - return Response.json({ error: "Command is required" }, { status: 400 }); - } + const { command, sandboxId } = await req.json(); - let sandbox: Sandbox; - let activeSandboxId: string; + if (!command || typeof command !== "string") { + return Response.json({ error: "Command is required" }, { status: 400 }); + } - if (sandboxId) { - try { - sandbox = await Sandbox.get({ sandboxId }); - activeSandboxId = sandboxId; - } catch { + let sandbox: Sandbox; + let activeSandboxId: string; + + if (sandboxId) { + try { + sandbox = await Sandbox.get({ sandboxId }); + activeSandboxId = sandboxId; + } catch { + sandbox = await createAndSeedSandbox(); + activeSandboxId = sandbox.sandboxId; + } + } else { sandbox = await createAndSeedSandbox(); activeSandboxId = sandbox.sandboxId; } - } else { - sandbox = await createAndSeedSandbox(); - activeSandboxId = sandbox.sandboxId; - } - try { - const result = await sandbox.runCommand({ - cmd: "bash", - args: ["-c", command], - cwd: SANDBOX_CWD, - }); + try { + const result = await sandbox.runCommand({ + cmd: "bash", + args: ["-c", command], + cwd: SANDBOX_CWD, + }); - const stdout = await result.stdout(); - const stderr = await result.stderr(); + const stdout = await result.stdout(); + const stderr = await result.stderr(); - return Response.json({ - stdout, - stderr, - exitCode: result.exitCode, - sandboxId: activeSandboxId, - }); + return Response.json({ + stdout, + stderr, + exitCode: result.exitCode, + sandboxId: activeSandboxId, + }); + } catch (error) { + return Response.json({ + stdout: "", + stderr: error instanceof Error ? error.message : "Execution failed", + exitCode: 1, + sandboxId: activeSandboxId, + }); + } } catch (error) { - return Response.json({ - stdout: "", - stderr: error instanceof Error ? error.message : "Execution failed", - exitCode: 1, - sandboxId: activeSandboxId, - }); + console.error("[/api/exec] Error:", error); + return Response.json( + { + error: error instanceof Error ? error.message : "Internal server error", + }, + { status: 500 }, + ); } } From 470392f03ac25b16c0a354c25e05ecc4fcac0f8c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 11 Feb 2026 16:33:22 -0500 Subject: [PATCH 3/3] refactor: extract shared sandbox logic into _lib DRY: Move readSourceFiles and createSandbox into app/api/_lib/ so both /api/agent and /api/exec reuse the same sandbox creation logic. Co-Authored-By: Claude Opus 4.6 --- app/api/_lib/createSandbox.ts | 14 +++++++++++ app/api/_lib/readSourceFiles.ts | 31 ++++++++++++++++++++++++ app/api/agent/route.ts | 43 ++++----------------------------- app/api/exec/route.ts | 39 ++++++++++++++++-------------- 4 files changed, 71 insertions(+), 56 deletions(-) create mode 100644 app/api/_lib/createSandbox.ts create mode 100644 app/api/_lib/readSourceFiles.ts diff --git a/app/api/_lib/createSandbox.ts b/app/api/_lib/createSandbox.ts new file mode 100644 index 00000000..981b0675 --- /dev/null +++ b/app/api/_lib/createSandbox.ts @@ -0,0 +1,14 @@ +import { Sandbox } from "@vercel/sandbox"; + +/** + * Create a Vercel Sandbox and seed it with files. + */ +export async function createSandbox( + files: Array<{ path: string; content: Buffer }> +): Promise { + const sandbox = await Sandbox.create(); + if (files.length > 0) { + await sandbox.writeFiles(files); + } + return sandbox; +} diff --git a/app/api/_lib/readSourceFiles.ts b/app/api/_lib/readSourceFiles.ts new file mode 100644 index 00000000..cde9edc0 --- /dev/null +++ b/app/api/_lib/readSourceFiles.ts @@ -0,0 +1,31 @@ +import { readdirSync, readFileSync } from "fs"; +import { join, relative } from "path"; + +/** + * Recursively read all files from a directory, returning them in the format + * expected by Sandbox.writeFiles(). + */ +export function readSourceFiles( + dir: string, + destDir: string, + baseDir?: string +): Array<{ path: string; content: Buffer }> { + const base = baseDir ?? dir; + const files: Array<{ path: string; content: Buffer }> = []; + + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules" || entry.name === ".git") continue; + files.push(...readSourceFiles(fullPath, destDir, base)); + } else { + const relPath = relative(base, fullPath); + files.push({ + path: join(destDir, relPath), + content: readFileSync(fullPath), + }); + } + } + + return files; +} diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index 022fa314..df8d3410 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -1,9 +1,9 @@ import { ToolLoopAgent, createAgentUIStreamResponse, stepCountIs } from "ai"; import { createBashTool } from "bash-tool"; -import { Sandbox } from "@vercel/sandbox"; -import { readdirSync, readFileSync } from "fs"; -import { dirname, join, relative } from "path"; +import { dirname, join } from "path"; import { fileURLToPath } from "url"; +import { createSandbox } from "../_lib/createSandbox"; +import { readSourceFiles } from "../_lib/readSourceFiles"; const __dirname = dirname(fileURLToPath(import.meta.url)); const AGENT_DATA_DIR = join(__dirname, "./_agent-data"); @@ -36,35 +36,6 @@ Use cat to read files. Use head, tail to read parts of large files. Keep responses concise. You have access to a full Linux environment with standard tools.`; -/** - * Recursively read all files from a directory, returning them in the format - * expected by Sandbox.writeFiles(). - */ -function readSourceFiles( - dir: string, - baseDir?: string -): Array<{ path: string; content: Buffer }> { - const base = baseDir ?? dir; - const files: Array<{ path: string; content: Buffer }> = []; - - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { - // Skip node_modules and other large/irrelevant dirs - if (entry.name === "node_modules" || entry.name === ".git") continue; - files.push(...readSourceFiles(fullPath, base)); - } else { - const relPath = relative(base, fullPath); - files.push({ - path: join(SANDBOX_CWD, relPath), - content: readFileSync(fullPath), - }); - } - } - - return files; -} - export async function POST(req: Request) { const authHeader = req.headers.get("Authorization"); if (!authHeader?.startsWith("Bearer ")) { @@ -80,14 +51,10 @@ export async function POST(req: Request) { .pop(); console.log("Prompt:", lastUserMessage?.parts?.[0]?.text); - const sandbox = await Sandbox.create(); + const files = readSourceFiles(AGENT_DATA_DIR, SANDBOX_CWD); + const sandbox = await createSandbox(files); try { - // Upload source files so the agent can explore them - const files = readSourceFiles(AGENT_DATA_DIR); - if (files.length > 0) { - await sandbox.writeFiles(files); - } const bashToolkit = await createBashTool({ sandbox, diff --git a/app/api/exec/route.ts b/app/api/exec/route.ts index 1ed4245a..d1f7f7f1 100644 --- a/app/api/exec/route.ts +++ b/app/api/exec/route.ts @@ -1,30 +1,33 @@ import { Sandbox } from "@vercel/sandbox"; +import { createSandbox } from "../_lib/createSandbox"; const SANDBOX_CWD = "/home/user"; -async function createAndSeedSandbox(): Promise { - const sandbox = await Sandbox.create(); +async function fetchSourceFiles(): Promise< + Array<{ path: string; content: Buffer }> +> { + const baseUrl = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000"; + const res = await fetch(`${baseUrl}/api/fs`); + if (!res.ok) return []; + const filesMap: Record = await res.json(); + return Object.entries(filesMap).map(([path, content]) => ({ + path: `${SANDBOX_CWD}/${path}`, + content: Buffer.from(content), + })); +} - // Seed sandbox with source files from the fs API +async function createAndSeedSandbox(): Promise { + let files: Array<{ path: string; content: Buffer }> = []; try { - const baseUrl = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : "http://localhost:3000"; - const res = await fetch(`${baseUrl}/api/fs`); - if (res.ok) { - const filesMap: Record = await res.json(); - const files = Object.entries(filesMap).map(([path, content]) => ({ - path: `${SANDBOX_CWD}/${path}`, - content: Buffer.from(content), - })); - if (files.length > 0) { - await sandbox.writeFiles(files); - } - } + files = await fetchSourceFiles(); } catch { - // File seeding is best-effort — sandbox still works without files + // File seeding is best-effort } + const sandbox = await createSandbox(files); + // Create convenience copies of top-level demo files try { await sandbox.runCommand({