Skip to content

feat: check sandbox snapshot on login, trigger setup if missing#8

Closed
sweetmantech wants to merge 7 commits intomainfrom
sweetmantech/myc-4217-bash-onlogin-if-missing-github-snapshot-call-post
Closed

feat: check sandbox snapshot on login, trigger setup if missing#8
sweetmantech wants to merge 7 commits intomainfrom
sweetmantech/myc-4217-bash-onlogin-if-missing-github-snapshot-call-post

Conversation

@sweetmantech
Copy link
Copy Markdown

@sweetmantech sweetmantech commented Feb 12, 2026

Summary

  • Adds useSetupSandbox hook that runs once after Privy authentication
  • Checks GET /api/sandboxes for existing snapshots; if none exist, fires POST /api/sandboxes/setup in the background
  • Ensures every user has a GitHub repo + snapshot provisioned before they start using the terminal

Test plan

  • Log in with a new account → verify POST /api/sandboxes/setup is called
  • Log in with an existing account that has a sandbox → verify setup is NOT called
  • Verify no UI changes or error messages appear (silent background provisioning)
  • Verify the hook only runs once per login session (refresh should not re-trigger)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added server-side sandbox environment for executing shell commands with persistence across sessions.
    • Introduced automatic sandbox provisioning and setup on app initialization when authenticated.
  • Refactor

    • Unified terminal command execution workflow to use consistent sandbox-based approach.
    • Integrated agent commands with new sandbox execution model.

sweetmantech and others added 4 commits February 11, 2026 16:13
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Adds useSetupSandbox hook that runs once after Privy authentication.
Checks GET /api/sandboxes for existing snapshots, and if none exist,
fires POST /api/sandboxes/setup in the background to provision a
GitHub repo + snapshot before the user starts using the terminal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
bash Ready Ready Preview Feb 12, 2026 2:27pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 12, 2026

Warning

Rate limit exceeded

@sweetmantech has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 27 minutes and 28 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR introduces a sandbox execution infrastructure by adding new helper modules for sandbox creation and file reading, establishing a new /api/exec endpoint for server-side command execution, and refactoring the Terminal component to route commands through a unified executor instead of client-side operations. A new hook provisions sandboxes on authentication. The agent command handler is refactored to return an async callable instead of a command object.

Changes

Cohort / File(s) Summary
Sandbox Infrastructure
app/api/_lib/createSandbox.ts, app/api/_lib/readSourceFiles.ts, app/hooks/useSetupSandbox.ts
New modules for creating sandboxes with optional file seeding, recursively reading source directories excluding node_modules/.git, and provisioning sandboxes via background API calls on authentication.
API Routes
app/api/agent/route.ts, app/api/exec/route.ts
Agent route refactored to use createSandbox helper with pre-collected files. New exec route handles POST requests for shell command execution with authorization, sandbox lifecycle management (create/reuse), file fetching from /api/fs, and result streaming.
Terminal Component & Handlers
app/components/Terminal.tsx, app/components/terminal-parts/agent-command.ts, app/components/terminal-parts/input-handler.ts, app/components/terminal-parts/index.ts
Terminal refactored from client-side Bash-based execution to unified exec function routing to static, agent, or sandbox endpoints. Agent command handler signature changed from command constructor to async handler returning ExecResult. Input handler signature updated to accept generic ExecFn instead of Bash. Exports updated to reflect API changes.
Application Entry
app/page.tsx
Invoked useSetupSandbox hook during Home component render to trigger background sandbox provisioning.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client<br/>(Terminal)
    participant Server as /api/exec
    participant FSApi as /api/fs
    participant Sandbox as Sandbox<br/>Environment
    
    Client->>Server: POST /api/exec<br/>(command, sandboxId?)
    activate Server
    
    alt Reusing existing sandbox
        Server->>Sandbox: Get existing sandbox
        alt Sandbox unavailable
            Server->>FSApi: GET source files
            FSApi-->>Server: File contents
            Server->>Sandbox: Create & seed<br/>new sandbox
        end
    else Creating new sandbox
        Server->>FSApi: GET source files
        FSApi-->>Server: File contents
        Server->>Sandbox: Create & seed<br/>new sandbox
    end
    
    Server->>Sandbox: Execute command<br/>(bash -c)
    Sandbox-->>Server: stdout, stderr,<br/>exitCode
    
    deactivate Server
    Server-->>Client: {stdout, stderr,<br/>exitCode, sandboxId}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A sandbox tale unfolds anew,
Commands execute, and shells ring true!
Files hop in, results leap out,
The exec route handles all the clout.
No Bash left on the client side,
Just streaming results, with joy and pride! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely describes the main objective: checking for sandbox snapshots on login and triggering setup if missing, which aligns with the primary addition of useSetupSandbox hook and the setup provisioning workflow.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sweetmantech/myc-4217-bash-onlogin-if-missing-github-snapshot-call-post

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/components/terminal-parts/agent-command.ts (1)

334-341: ⚠️ Potential issue | 🟡 Minor

thinkingTimeout is not cleared on the error path.

If the catch block on Line 334 is reached, the setTimeout from resetThinkingTimer() (Line 137) may still be pending. It will fire after the handler returns, writing "Thinking..." to the terminal at an unexpected time.

🐛 Proposed fix — clear timer in catch
     } catch (error) {
+      clearThinking(false);
       agentMessages.pop();
       return {
🤖 Fix all issues with AI agents
In `@app/api/_lib/createSandbox.ts`:
- Around line 6-14: The createSandbox function can leak a created Sandbox when
Sandbox.create() succeeds but sandbox.writeFiles(files) throws; update
createSandbox to ensure the sandbox is cleaned up on error by wrapping the
writeFiles call in a try/catch (or try/finally) and calling the sandbox cleanup
method (e.g., sandbox.stop() or sandbox.dispose()) if writeFiles throws, then
rethrow the original error; only return the sandbox instance after successful
writeFiles. Ensure you reference Sandbox.create(), sandbox.writeFiles(...), and
the sandbox cleanup method (sandbox.stop()/sandbox.dispose()) when implementing
the fix.

In `@app/api/agent/route.ts`:
- Line 52: Remove the unconditional console.log that prints user-submitted
content (console.log("Prompt:", lastUserMessage?.parts?.[0]?.text)); either
delete it or gate it behind a debug/config flag (e.g., process.env.DEBUG) or use
a logger at debug level (e.g., processLogger.debug) so prompts are not written
to production logs; locate the console.log call and update it to only run when
debugging is enabled, referencing the console.log call and lastUserMessage
variable to find the exact spot.
- Around line 54-55: readSourceFiles is doing synchronous disk I/O on every POST
which blocks the event loop; change the code so AGENT_DATA_DIR is read once and
cached at module load instead of per-request: move the call to
readSourceFiles(AGENT_DATA_DIR, SANDBOX_CWD) out of the request handler in
route.ts into top-level initialization, store the result in a cachedFiles
constant (or lazy-initialize it on first access), and then pass cachedFiles into
createSandbox(...) inside the handler (or reuse a cached sandbox if
appropriate), ensuring readSourceFiles, AGENT_DATA_DIR, SANDBOX_CWD and
createSandbox are the referenced symbols to modify.

In `@app/api/exec/route.ts`:
- Around line 67-81: The code creates new sandboxes via createAndSeedSandbox()
when sandboxId is missing or not found but never stops them, causing resource
leaks; update the exec route to (1) ensure any sandbox you create is stopped
after the command completes (e.g., call sandbox.stop() or a provided
Sandbox.stop(activeSandboxId) in the finally/cleanup path), (2) return the
activeSandboxId in the response so clients can reuse an existing sandbox, and
(3) add or trigger a TTL/cleanup mechanism (e.g., enqueue the sandboxId for
background cleanup or set an expiration) for sandboxes created here to avoid
long-lived resources. Ensure changes reference the Sandbox instance,
activeSandboxId and createAndSeedSandbox() locations so the cleanup runs
regardless of success/failure.
- Around line 83-98: The sandbox.runCommand calls can hang on long commands;
update sandbox creation in createSandbox.ts to include a lifetime timeout (e.g.,
Sandbox.create({ timeout: 30000 })) and add per-command cancellation by creating
an AbortController around sandbox.runCommand (use a timeout to call
controller.abort() and pass signal: controller.signal to sandbox.runCommand) so
runCommand is aborted if it exceeds the intended duration; modify the code paths
that call sandbox.runCommand (the runCommand invocation in route.ts) to use this
AbortController and ensure any controller cleanup is handled.

In `@app/hooks/useSetupSandbox.ts`:
- Around line 32-35: The un-awaited fetch in useSetupSandbox.ts (the POST to
`${RECOUP_API_URL}/api/sandboxes/setup`) can reject outside the surrounding
try/catch and cause unhandled promise rejections; fix it by either awaiting the
fetch inside the try block (so rejections are caught) or explicitly handling
errors on the returned promise (e.g., append .catch(() => {}) to swallow/handle
failures) depending on whether you want the call truly fire-and-forget or to
surface errors; update the fetch invocation in useSetupSandbox.ts accordingly so
the promise rejection is handled.
🧹 Nitpick comments (5)
app/api/_lib/readSourceFiles.ts (1)

16-27: Consider guarding against symlink cycles and expanding the exclusion list.

The recursive traversal has no depth limit and doesn't check for symbolic links. A circular symlink would cause infinite recursion and a stack overflow. Additionally, directories like .next, dist, or build could bloat the returned file list unnecessarily.

A lstatSync check and/or a max-depth guard would make this safer. Expanding the exclusion set (or making it configurable) would avoid reading build artifacts.

app/components/terminal-parts/input-handler.ts (1)

12-18: ExecResult type is duplicated across modules.

The identical ExecResult type is defined locally in both input-handler.ts (Line 12) and agent-command.ts (Line 4). Consider extracting it into a shared types file (e.g., terminal-parts/types.ts) and importing it in both places.

app/hooks/useSetupSandbox.ts (1)

15-40: No cleanup on unmount — in-flight requests continue after the component is removed.

If the component unmounts while the async IIFE is executing (e.g., during navigation), the fetch calls and their callbacks continue running against a stale closure. Consider using an AbortController signal passed to the fetch calls and aborting in the effect cleanup.

app/api/exec/route.ts (1)

6-19: Self-fetch to /api/fs adds unnecessary latency and fragility.

fetchSourceFiles makes an HTTP call back to the same deployment to read files. This adds a network round-trip, risks timeout/failure during cold starts, and means the /api/fs route must be publicly accessible. Consider importing the file-reading logic directly (similar to how app/api/agent/route.ts uses readSourceFiles synchronously) instead of going through HTTP.

app/components/Terminal.tsx (1)

62-63: sandboxId is lost if the effect re-runs due to getAccessToken identity change.

sandboxId lives as a let inside the effect closure. If getAccessToken is not referentially stable across renders (common with Privy), the effect re-runs, the terminal is disposed and recreated, and the sandbox session is lost — causing a new sandbox to be provisioned on each re-render cycle.

Consider storing getAccessToken in a ref so the effect doesn't depend on its identity, or store sandboxId in a ref that survives effect re-runs.

Option: stabilize getAccessToken via ref
+  const getAccessTokenRef = useRef(getAccessToken);
+  useEffect(() => {
+    getAccessTokenRef.current = getAccessToken;
+  }, [getAccessToken]);
+
   useEffect(() => {
     const container = terminalRef.current;
     if (!container) return;
     ...
-    const token = await getAccessToken();
+    const token = await getAccessTokenRef.current();
     ...
-  }, [getAccessToken]);
+  }, []);

Also applies to: 176-176

Comment thread lib/createSandbox.ts
Comment on lines +6 to +14
export async function createSandbox(
files: Array<{ path: string; content: Buffer }>
): Promise<Sandbox> {
const sandbox = await Sandbox.create();
if (files.length > 0) {
await sandbox.writeFiles(files);
}
return sandbox;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sandbox leaks if writeFiles throws.

If Sandbox.create() succeeds but sandbox.writeFiles(files) throws, the created sandbox is never stopped/cleaned up by the caller (since createSandbox rejects before returning the instance).

🛡️ Proposed fix
 export async function createSandbox(
   files: Array<{ path: string; content: Buffer }>
 ): Promise<Sandbox> {
   const sandbox = await Sandbox.create();
-  if (files.length > 0) {
-    await sandbox.writeFiles(files);
+  try {
+    if (files.length > 0) {
+      await sandbox.writeFiles(files);
+    }
+  } catch (err) {
+    await sandbox.stop().catch(() => {});
+    throw err;
   }
   return sandbox;
 }
🤖 Prompt for AI Agents
In `@app/api/_lib/createSandbox.ts` around lines 6 - 14, The createSandbox
function can leak a created Sandbox when Sandbox.create() succeeds but
sandbox.writeFiles(files) throws; update createSandbox to ensure the sandbox is
cleaned up on error by wrapping the writeFiles call in a try/catch (or
try/finally) and calling the sandbox cleanup method (e.g., sandbox.stop() or
sandbox.dispose()) if writeFiles throws, then rethrow the original error; only
return the sandbox instance after successful writeFiles. Ensure you reference
Sandbox.create(), sandbox.writeFiles(...), and the sandbox cleanup method
(sandbox.stop()/sandbox.dispose()) when implementing the fix.

Comment thread app/api/agent/route.ts
@@ -80,14 +51,10 @@ export async function POST(req: Request) {
.pop();
console.log("Prompt:", lastUserMessage?.parts?.[0]?.text);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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
In `@app/api/agent/route.ts` at line 52, Remove the unconditional console.log that
prints user-submitted content (console.log("Prompt:",
lastUserMessage?.parts?.[0]?.text)); either delete it or gate it behind a
debug/config flag (e.g., process.env.DEBUG) or use a logger at debug level
(e.g., processLogger.debug) so prompts are not written to production logs;
locate the console.log call and update it to only run when debugging is enabled,
referencing the console.log call and lastUserMessage variable to find the exact
spot.

Comment thread app/api/agent/route.ts
Comment on lines +54 to +55
const files = readSourceFiles(AGENT_DATA_DIR, SANDBOX_CWD);
const sandbox = await createSandbox(files);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

readSourceFiles uses readdirSync/readFileSync and is invoked on every POST. For a non-trivial source tree this blocks the Node.js event loop, increasing latency for all concurrent requests. Since AGENT_DATA_DIR is static, consider reading the files once at module load time and caching the result:

♻️ 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const files = readSourceFiles(AGENT_DATA_DIR, SANDBOX_CWD);
const sandbox = await createSandbox(files);
const sandbox = await createSandbox(AGENT_FILES);
🤖 Prompt for AI Agents
In `@app/api/agent/route.ts` around lines 54 - 55, readSourceFiles is doing
synchronous disk I/O on every POST which blocks the event loop; change the code
so AGENT_DATA_DIR is read once and cached at module load instead of per-request:
move the call to readSourceFiles(AGENT_DATA_DIR, SANDBOX_CWD) out of the request
handler in route.ts into top-level initialization, store the result in a
cachedFiles constant (or lazy-initialize it on first access), and then pass
cachedFiles into createSandbox(...) inside the handler (or reuse a cached
sandbox if appropriate), ensuring readSourceFiles, AGENT_DATA_DIR, SANDBOX_CWD
and createSandbox are the referenced symbols to modify.

Comment thread app/api/exec/route.ts
Comment on lines +67 to +81
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sandboxes are created but never stopped — potential resource leak.

When sandboxId is absent (or the existing one can't be found), createAndSeedSandbox() provisions a new sandbox. However, the sandbox is never explicitly stopped after the command completes. Unlike the agent route (which stops the sandbox after the stream ends), this endpoint leaves every sandbox running until the platform garbage-collects it. Under sustained traffic this could exhaust your sandbox quota.

Consider stopping the sandbox after the command finishes, or at minimum returning the sandboxId so the client can reuse it (which it does), and adding a TTL/cleanup mechanism.

🤖 Prompt for AI Agents
In `@app/api/exec/route.ts` around lines 67 - 81, The code creates new sandboxes
via createAndSeedSandbox() when sandboxId is missing or not found but never
stops them, causing resource leaks; update the exec route to (1) ensure any
sandbox you create is stopped after the command completes (e.g., call
sandbox.stop() or a provided Sandbox.stop(activeSandboxId) in the
finally/cleanup path), (2) return the activeSandboxId in the response so clients
can reuse an existing sandbox, and (3) add or trigger a TTL/cleanup mechanism
(e.g., enqueue the sandboxId for background cleanup or set an expiration) for
sandboxes created here to avoid long-lived resources. Ensure changes reference
the Sandbox instance, activeSandboxId and createAndSeedSandbox() locations so
the cleanup runs regardless of success/failure.

Comment thread app/api/exec/route.ts
Comment on lines +83 to +98
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,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@vercel/sandbox runCommand timeout option documentation

💡 Result:

@vercel/sandbox does not expose a timeout option on sandbox.runCommand(). The runCommand() options include things like cwd, env, sudo, detached, stdout/stderr, and an AbortSignal (signal) to cancel the command, but no per-command timeout field. [1]

If you’re trying to avoid long-running work, you have two supported approaches:

  • Set the sandbox lifetime timeout when creating the sandbox: Sandbox.create({ timeout: <ms> }) (default is 5 minutes; plan maximums apply). [2][3]
  • Extend the sandbox lifetime while it’s running: sandbox.extendTimeout(<ms>). [2][3]

Source(s):

  • sandbox.runCommand() API/options (no timeout parameter): [1]
  • Sandbox lifetime timeout is configured on Sandbox.create() / extendTimeout(): [2][3]

🏁 Script executed:

# Find and examine the route.ts file mentioned in the review
find . -name "route.ts" -path "*api/exec*" | head -20

Repository: 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 -5

Repository: 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 -30

Repository: recoupable/bash

Length of output: 1837


🏁 Script executed:

cat -n ./app/api/exec/route.ts

Repository: recoupable/bash

Length of output: 4130


🏁 Script executed:

# Search for Sandbox.create and timeout configuration in the codebase
rg "Sandbox.create" -A 5 -B 2

Repository: recoupable/bash

Length of output: 1018


🏁 Script executed:

# Look for AbortSignal usage in the same file
rg "AbortSignal|AbortController" -A 3 -B 2

Repository: recoupable/bash

Length of output: 41


Add timeout protection to prevent long-running commands from blocking the serverless function.

sandbox.runCommand() lacks timeout protection. A user could submit a command like sleep 9999 or an infinite loop, consuming the entire serverless function execution time. While @vercel/sandbox does not expose a per-command timeout option, you can mitigate this in two ways:

  1. Set sandbox lifetime timeout when creating the sandbox in createSandbox.ts (line 29): Sandbox.create({ timeout: 30000 }) (adjust milliseconds as needed).
  2. Use AbortSignal to cancel long-running commands in the runCommand call (lines 84–88): implement an AbortController with a timeout and pass signal: controller.signal to runCommand.

Either approach prevents indefinite blocking. The first is simpler; the second offers per-command granularity.

🤖 Prompt for AI Agents
In `@app/api/exec/route.ts` around lines 83 - 98, The sandbox.runCommand calls can
hang on long commands; update sandbox creation in createSandbox.ts to include a
lifetime timeout (e.g., Sandbox.create({ timeout: 30000 })) and add per-command
cancellation by creating an AbortController around sandbox.runCommand (use a
timeout to call controller.abort() and pass signal: controller.signal to
sandbox.runCommand) so runCommand is aborted if it exceeds the intended
duration; modify the code paths that call sandbox.runCommand (the runCommand
invocation in route.ts) to use this AbortController and ensure any controller
cleanup is handled.

Comment on lines +32 to +35
fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, {
method: "POST",
headers,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Unawaited fetch escapes the try/catch, causing unhandled promise rejections.

The POST to /api/sandboxes/setup is fire-and-forget (no await). If this fetch rejects (e.g., network error), the rejection is not caught by the surrounding try/catch, which defeats the "silent background provisioning" intent and will trigger an unhandledrejection event in the browser.

Either await it or explicitly .catch(() => {}) the promise:

🐛 Proposed fix
-        fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, {
+        await fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, {
           method: "POST",
           headers,
         });

Or if truly fire-and-forget:

         fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, {
           method: "POST",
           headers,
-        });
+        }).catch(() => {});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, {
method: "POST",
headers,
});
await fetch(`${RECOUP_API_URL}/api/sandboxes/setup`, {
method: "POST",
headers,
});
🤖 Prompt for AI Agents
In `@app/hooks/useSetupSandbox.ts` around lines 32 - 35, The un-awaited fetch in
useSetupSandbox.ts (the POST to `${RECOUP_API_URL}/api/sandboxes/setup`) can
reject outside the surrounding try/catch and cause unhandled promise rejections;
fix it by either awaiting the fetch inside the try block (so rejections are
caught) or explicitly handling errors on the returned promise (e.g., append
.catch(() => {}) to swallow/handle failures) depending on whether you want the
call truly fire-and-forget or to surface errors; update the fetch invocation in
useSetupSandbox.ts accordingly so the promise rejection is handled.

Moves createSandbox.ts and readSourceFiles.ts out of the API-specific
_lib directory into app/lib so they are accessible project-wide.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sweetmantech and others added 2 commits February 12, 2026 09:25
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sweetmantech
Copy link
Copy Markdown
Author

Replaced by clean PR from a branch based on main

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant