-
Notifications
You must be signed in to change notification settings - Fork 0
feat: check sandbox snapshot on login, trigger setup if missing #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
06d6a95
6806ee7
470392f
14603dd
f67e084
69ec58a
716022f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -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); | ||||||||
|
Comment on lines
+54
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Synchronous filesystem traversal runs on every request, blocking the event loop.
♻️ Suggested caching approach const AGENT_DATA_DIR = join(__dirname, "./_agent-data");
const SANDBOX_CWD = "/vercel/sandbox";
+
+// Read once at module load — AGENT_DATA_DIR is static
+const AGENT_FILES = readSourceFiles(AGENT_DATA_DIR, SANDBOX_CWD);
// ... inside POST handler:
- const files = readSourceFiles(AGENT_DATA_DIR, SANDBOX_CWD);
- const sandbox = await createSandbox(files);
+ const sandbox = await createSandbox(AGENT_FILES);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| 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, | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| import { Sandbox } from "@vercel/sandbox"; | ||
| import { createSandbox } from "@/lib/createSandbox"; | ||
|
|
||
| const SANDBOX_CWD = "/home/user"; | ||
|
|
||
| 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<string, string> = await res.json(); | ||
| return Object.entries(filesMap).map(([path, content]) => ({ | ||
| path: `${SANDBOX_CWD}/${path}`, | ||
| content: Buffer.from(content), | ||
| })); | ||
| } | ||
|
|
||
| async function createAndSeedSandbox(): Promise<Sandbox> { | ||
| let files: Array<{ path: string; content: Buffer }> = []; | ||
| try { | ||
| files = await fetchSourceFiles(); | ||
| } catch { | ||
| // File seeding is best-effort | ||
| } | ||
|
|
||
| const sandbox = await createSandbox(files); | ||
|
|
||
| // Create convenience copies of top-level demo files | ||
| 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) { | ||
| try { | ||
| 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; | ||
| } | ||
|
Comment on lines
+67
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sandboxes are created but never stopped — potential resource leak. When Consider stopping the sandbox after the command finishes, or at minimum returning the 🤖 Prompt for AI Agents |
||
|
|
||
| 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, | ||
| }); | ||
|
Comment on lines
+83
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result:
If you’re trying to avoid long-running work, you have two supported approaches:
Source(s):
🏁 Script executed: # Find and examine the route.ts file mentioned in the review
find . -name "route.ts" -path "*api/exec*" | head -20Repository: recoupable/bash Length of output: 82 🏁 Script executed: # Search for package.json to check the `@vercel/sandbox` version
find . -name "package.json" -type f | head -5Repository: recoupable/bash Length of output: 381 🏁 Script executed: # Look for any timeout-related code in the codebase
rg "timeout" -i --type ts --type js | head -30Repository: recoupable/bash Length of output: 1837 🏁 Script executed: cat -n ./app/api/exec/route.tsRepository: recoupable/bash Length of output: 4130 🏁 Script executed: # Search for Sandbox.create and timeout configuration in the codebase
rg "Sandbox.create" -A 5 -B 2Repository: recoupable/bash Length of output: 1018 🏁 Script executed: # Look for AbortSignal usage in the same file
rg "AbortSignal|AbortController" -A 3 -B 2Repository: recoupable/bash Length of output: 41 Add timeout protection to prevent long-running commands from blocking the serverless function.
Either approach prevents indefinite blocking. The first is simpler; the second offers per-command granularity. 🤖 Prompt for AI Agents |
||
| } catch (error) { | ||
| return Response.json({ | ||
| stdout: "", | ||
| stderr: error instanceof Error ? error.message : "Execution failed", | ||
| exitCode: 1, | ||
| sandboxId: activeSandboxId, | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| console.error("[/api/exec] Error:", error); | ||
| return Response.json( | ||
| { | ||
| error: error instanceof Error ? error.message : "Internal server error", | ||
| }, | ||
| { status: 500 }, | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
User prompt is logged to the console.
console.log("Prompt:", lastUserMessage?.parts?.[0]?.text)logs user-submitted content to server logs. Depending on usage, prompts may contain sensitive or personally identifiable information. Consider removing this or gating it behind a debug flag.🤖 Prompt for AI Agents