From a35dc1005a4baa5da8cf444a7a98e9717561d6b2 Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 09:34:11 -0500 Subject: [PATCH 01/13] Fix React Server Components CVE vulnerabilities (#194) Updated dependencies to fix Next.js and React CVE vulnerabilities. The fix-react2shell-next tool automatically updated the following packages to their secure versions: - next - react-server-dom-webpack - react-server-dom-parcel - react-server-dom-turbopack All package.json files have been scanned and vulnerable versions have been patched to the correct fixed versions based on the official React advisory. Co-authored-by: Vercel --- package.json | 2 +- pnpm-lock.yaml | 99 ++++++++++++++++++++++++++------------------------ 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index dea03405..c53bb782 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "mcp-handler": "^1.0.4", "ms": "^2.1.3", "multiformats": "^13.4.1", - "next": "^16.0.7", + "next": "16.0.10", "node-telegram-bot-api": "^0.66.0", "react": "^19.2.1", "react-dom": "^19.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff6111b4..f0b0d314 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,7 +79,7 @@ importers: version: 15.0.12 mcp-handler: specifier: ^1.0.4 - version: 1.0.4(@modelcontextprotocol/sdk@1.24.3(zod@4.1.13))(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)) + version: 1.0.4(@modelcontextprotocol/sdk@1.24.3(zod@4.1.13))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)) ms: specifier: ^2.1.3 version: 2.1.3 @@ -87,8 +87,8 @@ importers: specifier: ^13.4.1 version: 13.4.1 next: - specifier: ^16.0.7 - version: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + specifier: 16.0.10 + version: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) node-telegram-bot-api: specifier: ^0.66.0 version: 0.66.0(request@2.88.2) @@ -112,7 +112,7 @@ importers: version: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) x402-next: specifier: ^0.7.3 - version: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: specifier: ^4.1.13 version: 4.1.13 @@ -1056,56 +1056,56 @@ packages: '@next/env@14.2.35': resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} - '@next/env@16.0.7': - resolution: {integrity: sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==} + '@next/env@16.0.10': + resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} '@next/eslint-plugin-next@15.1.7': resolution: {integrity: sha512-kRP7RjSxfTO13NE317ek3mSGzoZlI33nc/i5hs1KaWpK+egs85xg0DJ4p32QEiHnR0mVjuUfhRIun7awqfL7pQ==} - '@next/swc-darwin-arm64@16.0.7': - resolution: {integrity: sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==} + '@next/swc-darwin-arm64@16.0.10': + resolution: {integrity: sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.0.7': - resolution: {integrity: sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==} + '@next/swc-darwin-x64@16.0.10': + resolution: {integrity: sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.0.7': - resolution: {integrity: sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==} + '@next/swc-linux-arm64-gnu@16.0.10': + resolution: {integrity: sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@16.0.7': - resolution: {integrity: sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==} + '@next/swc-linux-arm64-musl@16.0.10': + resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@16.0.7': - resolution: {integrity: sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==} + '@next/swc-linux-x64-gnu@16.0.10': + resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@16.0.7': - resolution: {integrity: sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==} + '@next/swc-linux-x64-musl@16.0.10': + resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@16.0.7': - resolution: {integrity: sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==} + '@next/swc-win32-arm64-msvc@16.0.10': + resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.0.7': - resolution: {integrity: sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==} + '@next/swc-win32-x64-msvc@16.0.10': + resolution: {integrity: sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2434,6 +2434,7 @@ packages: '@walletconnect/ethereum-provider@2.21.1': resolution: {integrity: sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/events@1.0.1': resolution: {integrity: sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==} @@ -2478,9 +2479,11 @@ packages: '@walletconnect/sign-client@2.21.0': resolution: {integrity: sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/sign-client@2.21.1': resolution: {integrity: sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/time@1.0.2': resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} @@ -2493,9 +2496,11 @@ packages: '@walletconnect/universal-provider@2.21.0': resolution: {integrity: sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/universal-provider@2.21.1': resolution: {integrity: sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/utils@2.21.0': resolution: {integrity: sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==} @@ -4520,8 +4525,8 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - next@16.0.7: - resolution: {integrity: sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==} + next@16.0.10: + resolution: {integrity: sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -7150,34 +7155,34 @@ snapshots: '@next/env@14.2.35': {} - '@next/env@16.0.7': {} + '@next/env@16.0.10': {} '@next/eslint-plugin-next@15.1.7': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.0.7': + '@next/swc-darwin-arm64@16.0.10': optional: true - '@next/swc-darwin-x64@16.0.7': + '@next/swc-darwin-x64@16.0.10': optional: true - '@next/swc-linux-arm64-gnu@16.0.7': + '@next/swc-linux-arm64-gnu@16.0.10': optional: true - '@next/swc-linux-arm64-musl@16.0.7': + '@next/swc-linux-arm64-musl@16.0.10': optional: true - '@next/swc-linux-x64-gnu@16.0.7': + '@next/swc-linux-x64-gnu@16.0.10': optional: true - '@next/swc-linux-x64-musl@16.0.7': + '@next/swc-linux-x64-musl@16.0.10': optional: true - '@next/swc-win32-arm64-msvc@16.0.7': + '@next/swc-win32-arm64-msvc@16.0.10': optional: true - '@next/swc-win32-x64-msvc@16.0.7': + '@next/swc-win32-x64-msvc@16.0.10': optional: true '@noble/ciphers@1.2.1': {} @@ -11831,14 +11836,14 @@ snapshots: math-intrinsics@1.1.0: {} - mcp-handler@1.0.4(@modelcontextprotocol/sdk@1.24.3(zod@4.1.13))(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)): + mcp-handler@1.0.4(@modelcontextprotocol/sdk@1.24.3(zod@4.1.13))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)): dependencies: '@modelcontextprotocol/sdk': 1.24.3(zod@4.1.13) chalk: 5.6.2 commander: 11.1.0 redis: 4.7.1 optionalDependencies: - next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) md5@2.3.0: dependencies: @@ -11981,9 +11986,9 @@ snapshots: negotiator@1.0.0: {} - next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: - '@next/env': 16.0.7 + '@next/env': 16.0.10 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001757 postcss: 8.4.31 @@ -11991,14 +11996,14 @@ snapshots: react-dom: 19.2.1(react@19.2.1) styled-jsx: 5.1.6(react@19.2.1) optionalDependencies: - '@next/swc-darwin-arm64': 16.0.7 - '@next/swc-darwin-x64': 16.0.7 - '@next/swc-linux-arm64-gnu': 16.0.7 - '@next/swc-linux-arm64-musl': 16.0.7 - '@next/swc-linux-x64-gnu': 16.0.7 - '@next/swc-linux-x64-musl': 16.0.7 - '@next/swc-win32-arm64-msvc': 16.0.7 - '@next/swc-win32-x64-msvc': 16.0.7 + '@next/swc-darwin-arm64': 16.0.10 + '@next/swc-darwin-x64': 16.0.10 + '@next/swc-linux-arm64-gnu': 16.0.10 + '@next/swc-linux-arm64-musl': 16.0.10 + '@next/swc-linux-x64-gnu': 16.0.10 + '@next/swc-linux-x64-musl': 16.0.10 + '@next/swc-win32-arm64-msvc': 16.0.10 + '@next/swc-win32-x64-msvc': 16.0.10 '@opentelemetry/api': 1.9.0 sharp: 0.34.5 transitivePeerDependencies: @@ -13878,11 +13883,11 @@ snapshots: - utf-8-validate - ws - x402-next@0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402-next@0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@coinbase/cdp-sdk': 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) x402: 0.7.3(@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.2.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(ioredis@5.8.2)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 From 85b66eae315903d7da0af79a59a90d828a2e4154 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 3 Feb 2026 11:38:34 -0500 Subject: [PATCH 02/13] feat: update POST /api/sandboxes to use command execution - Change request body from { prompt } to { command, args?, cwd? } - Add account snapshot support for creating sandboxes from snapshots - Update trigger payload to include command, args, cwd, sandboxId, accountId - Return runId in response from triggered task - Update tests to reflect new API structure Co-Authored-By: Claude Opus 4.5 --- .../createSandboxPostHandler.test.ts | 92 ++++++++++++++++--- .../__tests__/validateSandboxBody.test.ts | 37 ++++++-- lib/sandbox/createSandbox.ts | 27 ++++-- lib/sandbox/createSandboxPostHandler.ts | 31 +++++-- lib/sandbox/validateSandboxBody.ts | 4 +- .../selectAccountSnapshot.ts | 31 +++++++ lib/trigger/triggerRunSandboxCommand.ts | 11 ++- 7 files changed, 198 insertions(+), 35 deletions(-) create mode 100644 lib/supabase/account_snapshots/selectAccountSnapshot.ts diff --git a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts index 28d49dab..4f1c4e84 100644 --- a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts +++ b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts @@ -7,6 +7,7 @@ import { validateSandboxBody } from "@/lib/sandbox/validateSandboxBody"; import { createSandbox } from "@/lib/sandbox/createSandbox"; import { insertAccountSandbox } from "@/lib/supabase/account_sandboxes/insertAccountSandbox"; import { triggerRunSandboxCommand } from "@/lib/trigger/triggerRunSandboxCommand"; +import { selectAccountSnapshot } from "@/lib/supabase/account_snapshots/selectAccountSnapshot"; vi.mock("@/lib/sandbox/validateSandboxBody", () => ({ validateSandboxBody: vi.fn(), @@ -24,6 +25,10 @@ vi.mock("@/lib/trigger/triggerRunSandboxCommand", () => ({ triggerRunSandboxCommand: vi.fn(), })); +vi.mock("@/lib/supabase/account_snapshots/selectAccountSnapshot", () => ({ + selectAccountSnapshot: vi.fn(), +})); + /** * Creates a mock NextRequest for testing. * @@ -51,13 +56,14 @@ describe("createSandboxPostHandler", () => { expect(response.status).toBe(401); }); - it("returns 200 with sandboxes array on success", async () => { + it("returns 200 with sandboxes array including runId on success", async () => { vi.mocked(validateSandboxBody).mockResolvedValue({ accountId: "acc_123", orgId: null, authToken: "token", - prompt: "tell me hello", + command: "ls", }); + vi.mocked(selectAccountSnapshot).mockResolvedValue(null); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_123", sandboxStatus: "running", @@ -73,6 +79,9 @@ describe("createSandboxPostHandler", () => { }, error: null, }); + vi.mocked(triggerRunSandboxCommand).mockResolvedValue({ + id: "run_abc123", + }); const request = createMockRequest(); const response = await createSandboxPostHandler(request); @@ -87,18 +96,58 @@ describe("createSandboxPostHandler", () => { sandboxStatus: "running", timeout: 600000, createdAt: "2024-01-01T00:00:00.000Z", + runId: "run_abc123", }, ], }); }); - it("calls createSandbox without arguments", async () => { + it("calls createSandbox with snapshotId when account has snapshot", async () => { + vi.mocked(validateSandboxBody).mockResolvedValue({ + accountId: "acc_123", + orgId: null, + authToken: "token", + command: "ls", + }); + vi.mocked(selectAccountSnapshot).mockResolvedValue({ + id: "snap_record_123", + account_id: "acc_123", + snapshot_id: "snap_xyz", + created_at: "2024-01-01T00:00:00.000Z", + }); + vi.mocked(createSandbox).mockResolvedValue({ + sandboxId: "sbx_456", + sandboxStatus: "running", + timeout: 600000, + createdAt: "2024-01-01T00:00:00.000Z", + }); + vi.mocked(insertAccountSandbox).mockResolvedValue({ + data: { + id: "record_123", + account_id: "acc_123", + sandbox_id: "sbx_456", + created_at: "2024-01-01T00:00:00.000Z", + }, + error: null, + }); + vi.mocked(triggerRunSandboxCommand).mockResolvedValue({ + id: "run_def456", + }); + + const request = createMockRequest(); + await createSandboxPostHandler(request); + + expect(createSandbox).toHaveBeenCalledWith({ snapshotId: "snap_xyz" }); + }); + + it("calls createSandbox with null snapshotId when account has no snapshot", async () => { vi.mocked(validateSandboxBody).mockResolvedValue({ accountId: "acc_123", orgId: null, authToken: "token", - prompt: "tell me hello", + command: "ls", }); + vi.mocked(selectAccountSnapshot).mockResolvedValue(null); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_456", sandboxStatus: "running", @@ -114,11 +163,14 @@ describe("createSandboxPostHandler", () => { }, error: null, }); + vi.mocked(triggerRunSandboxCommand).mockResolvedValue({ + id: "run_def456", + }); const request = createMockRequest(); await createSandboxPostHandler(request); - expect(createSandbox).toHaveBeenCalledWith(); + expect(createSandbox).toHaveBeenCalledWith({ snapshotId: null }); }); it("calls insertAccountSandbox with correct account_id and sandbox_id", async () => { @@ -126,8 +178,9 @@ describe("createSandboxPostHandler", () => { accountId: "acc_123", orgId: null, authToken: "token", - prompt: "tell me hello", + command: "ls", }); + vi.mocked(selectAccountSnapshot).mockResolvedValue(null); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_456", sandboxStatus: "running", @@ -143,6 +196,9 @@ describe("createSandboxPostHandler", () => { }, error: null, }); + vi.mocked(triggerRunSandboxCommand).mockResolvedValue({ + id: "run_def456", + }); const request = createMockRequest(); await createSandboxPostHandler(request); @@ -153,13 +209,16 @@ describe("createSandboxPostHandler", () => { }); }); - it("calls triggerRunSandboxCommand with prompt and sandboxId", async () => { + it("calls triggerRunSandboxCommand with command, args, cwd, sandboxId, and accountId", async () => { vi.mocked(validateSandboxBody).mockResolvedValue({ accountId: "acc_123", orgId: null, authToken: "token", - prompt: "tell me hello", + command: "ls", + args: ["-la"], + cwd: "/home", }); + vi.mocked(selectAccountSnapshot).mockResolvedValue(null); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_789", sandboxStatus: "running", @@ -175,13 +234,19 @@ describe("createSandboxPostHandler", () => { }, error: null, }); + vi.mocked(triggerRunSandboxCommand).mockResolvedValue({ + id: "run_ghi789", + }); const request = createMockRequest(); await createSandboxPostHandler(request); expect(triggerRunSandboxCommand).toHaveBeenCalledWith({ - prompt: "tell me hello", + command: "ls", + args: ["-la"], + cwd: "/home", sandboxId: "sbx_789", + accountId: "acc_123", }); }); @@ -190,8 +255,9 @@ describe("createSandboxPostHandler", () => { accountId: "acc_123", orgId: null, authToken: "token", - prompt: "tell me hello", + command: "ls", }); + vi.mocked(selectAccountSnapshot).mockResolvedValue(null); vi.mocked(createSandbox).mockRejectedValue(new Error("Sandbox creation failed")); const request = createMockRequest(); @@ -210,8 +276,9 @@ describe("createSandboxPostHandler", () => { accountId: "acc_123", orgId: null, authToken: "token", - prompt: "tell me hello", + command: "ls", }); + vi.mocked(selectAccountSnapshot).mockResolvedValue(null); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_123", sandboxStatus: "running", @@ -236,8 +303,9 @@ describe("createSandboxPostHandler", () => { accountId: "acc_123", orgId: null, authToken: "token", - prompt: "tell me hello", + command: "ls", }); + vi.mocked(selectAccountSnapshot).mockResolvedValue(null); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_123", sandboxStatus: "running", diff --git a/lib/sandbox/__tests__/validateSandboxBody.test.ts b/lib/sandbox/__tests__/validateSandboxBody.test.ts index 3aa37cb1..5ac255ab 100644 --- a/lib/sandbox/__tests__/validateSandboxBody.test.ts +++ b/lib/sandbox/__tests__/validateSandboxBody.test.ts @@ -41,13 +41,13 @@ describe("validateSandboxBody", () => { expect((result as NextResponse).status).toBe(401); }); - it("returns validated body with auth context when prompt is provided", async () => { + it("returns validated body with auth context when command is provided", async () => { vi.mocked(validateAuthContext).mockResolvedValue({ accountId: "acc_123", orgId: "org_456", authToken: "token", }); - vi.mocked(safeParseJson).mockResolvedValue({ prompt: "tell me hello" }); + vi.mocked(safeParseJson).mockResolvedValue({ command: "ls" }); const request = createMockRequest(); const result = await validateSandboxBody(request); @@ -56,11 +56,36 @@ describe("validateSandboxBody", () => { accountId: "acc_123", orgId: "org_456", authToken: "token", - prompt: "tell me hello", + command: "ls", }); }); - it("returns error response when prompt is missing", async () => { + it("returns validated body with optional args and cwd", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: "acc_123", + orgId: "org_456", + authToken: "token", + }); + vi.mocked(safeParseJson).mockResolvedValue({ + command: "ls", + args: ["-la", "/home"], + cwd: "/tmp", + }); + + const request = createMockRequest(); + const result = await validateSandboxBody(request); + + expect(result).toEqual({ + accountId: "acc_123", + orgId: "org_456", + authToken: "token", + command: "ls", + args: ["-la", "/home"], + cwd: "/tmp", + }); + }); + + it("returns error response when command is missing", async () => { vi.mocked(validateAuthContext).mockResolvedValue({ accountId: "acc_123", orgId: null, @@ -75,13 +100,13 @@ describe("validateSandboxBody", () => { expect((result as NextResponse).status).toBe(400); }); - it("returns error response when prompt is empty string", async () => { + it("returns error response when command is empty string", async () => { vi.mocked(validateAuthContext).mockResolvedValue({ accountId: "acc_123", orgId: null, authToken: "token", }); - vi.mocked(safeParseJson).mockResolvedValue({ prompt: "" }); + vi.mocked(safeParseJson).mockResolvedValue({ command: "" }); const request = createMockRequest(); const result = await validateSandboxBody(request); diff --git a/lib/sandbox/createSandbox.ts b/lib/sandbox/createSandbox.ts index 9f3824fc..c415fdba 100644 --- a/lib/sandbox/createSandbox.ts +++ b/lib/sandbox/createSandbox.ts @@ -8,20 +8,35 @@ export interface SandboxCreatedResponse { createdAt: string; } +interface CreateSandboxOptions { + snapshotId?: string | null; +} + /** * Creates a Vercel Sandbox and returns its info. * * The sandbox is left running so that commands can be executed via the runSandboxCommand task. + * If a snapshotId is provided, the sandbox will be created from that snapshot. * + * @param options - Optional configuration including snapshotId * @returns The sandbox creation response * @throws Error if sandbox creation fails */ -export async function createSandbox(): Promise { - const sandbox = await Sandbox.create({ - resources: { vcpus: 4 }, - timeout: ms("10m"), - runtime: "node22", - }); +export async function createSandbox( + options: CreateSandboxOptions = {}, +): Promise { + const { snapshotId } = options; + + const sandbox = snapshotId + ? await Sandbox.create({ + snapshotId, + timeout: ms("10m"), + }) + : await Sandbox.create({ + resources: { vcpus: 4 }, + timeout: ms("10m"), + runtime: "node22", + }); return { sandboxId: sandbox.sandboxId, diff --git a/lib/sandbox/createSandboxPostHandler.ts b/lib/sandbox/createSandboxPostHandler.ts index 9b503e52..ec1a5b65 100644 --- a/lib/sandbox/createSandboxPostHandler.ts +++ b/lib/sandbox/createSandboxPostHandler.ts @@ -5,16 +5,18 @@ import { createSandbox } from "@/lib/sandbox/createSandbox"; import { validateSandboxBody } from "@/lib/sandbox/validateSandboxBody"; import { insertAccountSandbox } from "@/lib/supabase/account_sandboxes/insertAccountSandbox"; import { triggerRunSandboxCommand } from "@/lib/trigger/triggerRunSandboxCommand"; +import { selectAccountSnapshot } from "@/lib/supabase/account_snapshots/selectAccountSnapshot"; /** * Handler for POST /api/sandboxes. * - * Creates a Vercel Sandbox and triggers the run-sandbox-command task to execute the prompt. + * Creates a Vercel Sandbox (from account's snapshot if available, otherwise fresh) + * and triggers the run-sandbox-command task to execute the 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 or error + * @returns A NextResponse with sandbox creation result including runId */ export async function createSandboxPostHandler(request: NextRequest): Promise { const validated = await validateSandboxBody(request); @@ -23,20 +25,37 @@ export async function createSandboxPostHandler(request: NextRequest): Promise & AuthContext; diff --git a/lib/supabase/account_snapshots/selectAccountSnapshot.ts b/lib/supabase/account_snapshots/selectAccountSnapshot.ts new file mode 100644 index 00000000..ca8a077d --- /dev/null +++ b/lib/supabase/account_snapshots/selectAccountSnapshot.ts @@ -0,0 +1,31 @@ +import supabase from "../serverClient"; + +interface AccountSnapshot { + id: string; + account_id: string; + snapshot_id: string; + created_at: string; +} + +/** + * Selects the most recent snapshot for an account. + * + * @param accountId - The account ID to get the snapshot for + * @returns The snapshot record or null if not found + */ +export async function selectAccountSnapshot(accountId: string): Promise { + const { data, error } = await supabase + .from("account_snapshots") + .select("*") + .eq("account_id", accountId) + .order("created_at", { ascending: false }) + .limit(1) + .single(); + + if (error) { + // Table might not exist or no snapshot found - both are ok + return null; + } + + return data; +} diff --git a/lib/trigger/triggerRunSandboxCommand.ts b/lib/trigger/triggerRunSandboxCommand.ts index dd6db6e5..524f68ec 100644 --- a/lib/trigger/triggerRunSandboxCommand.ts +++ b/lib/trigger/triggerRunSandboxCommand.ts @@ -1,15 +1,18 @@ import { tasks } from "@trigger.dev/sdk"; type RunSandboxCommandPayload = { - prompt: string; + command: string; + args?: string[]; + cwd?: string; sandboxId: string; + accountId: string; }; /** - * Triggers the run-sandbox-command task to execute a prompt in a sandbox. + * Triggers the run-sandbox-command task to execute a command in a sandbox. * - * @param payload - The task payload with prompt and sandboxId - * @returns The task handle + * @param payload - The task payload with command, args, cwd, sandboxId, and accountId + * @returns The task handle with runId */ export async function triggerRunSandboxCommand(payload: RunSandboxCommandPayload) { const handle = await tasks.trigger("run-sandbox-command", payload); From 192917b332a50e78f11d943d7341e1393ca66257 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 3 Feb 2026 12:18:00 -0500 Subject: [PATCH 03/13] fix: use correct source parameter for snapshot in Sandbox.create The Vercel Sandbox SDK requires snapshotId to be passed via the source parameter with type: 'snapshot', not as a direct property. Co-Authored-By: Claude Opus 4.5 --- lib/sandbox/createSandbox.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sandbox/createSandbox.ts b/lib/sandbox/createSandbox.ts index c415fdba..a1ef7016 100644 --- a/lib/sandbox/createSandbox.ts +++ b/lib/sandbox/createSandbox.ts @@ -29,7 +29,7 @@ export async function createSandbox( const sandbox = snapshotId ? await Sandbox.create({ - snapshotId, + source: { type: "snapshot", snapshotId }, timeout: ms("10m"), }) : await Sandbox.create({ From 7a0f2a7219069e3865011c2461541d98dbb4f6dd Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 3 Feb 2026 12:28:15 -0500 Subject: [PATCH 04/13] test: update triggerRunSandboxCommand tests to use command payload Update test payloads to match new API: command, args, cwd, sandboxId, accountId Co-Authored-By: Claude Opus 4.5 --- .../__tests__/triggerRunSandboxCommand.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/trigger/__tests__/triggerRunSandboxCommand.test.ts b/lib/trigger/__tests__/triggerRunSandboxCommand.test.ts index e98377ad..64718f39 100644 --- a/lib/trigger/__tests__/triggerRunSandboxCommand.test.ts +++ b/lib/trigger/__tests__/triggerRunSandboxCommand.test.ts @@ -14,12 +14,15 @@ describe("triggerRunSandboxCommand", () => { }); it("triggers run-sandbox-command task with correct payload", async () => { - const mockHandle = { id: "task_123" }; + const mockHandle = { id: "run_123" }; vi.mocked(tasks.trigger).mockResolvedValue(mockHandle); const payload = { - prompt: "tell me hello", + command: "ls", + args: ["-la"], + cwd: "/home", sandboxId: "sbx_456", + accountId: "acc_123", }; const result = await triggerRunSandboxCommand(payload); @@ -29,12 +32,13 @@ describe("triggerRunSandboxCommand", () => { }); it("passes through the task handle from trigger", async () => { - const mockHandle = { id: "task_789", publicAccessToken: "token_abc" }; + const mockHandle = { id: "run_789", publicAccessToken: "token_abc" }; vi.mocked(tasks.trigger).mockResolvedValue(mockHandle); const result = await triggerRunSandboxCommand({ - prompt: "another prompt", + command: "echo", sandboxId: "sbx_999", + accountId: "acc_456", }); expect(result).toBe(mockHandle); From 279d6c47322a3caedd9e6914ea166dcec60e5d08 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 3 Feb 2026 12:57:17 -0500 Subject: [PATCH 05/13] refactor: use CreateSandboxParams from @vercel/sandbox SDK - Export CreateSandboxParams type extracted from Sandbox.create signature - Accept full SDK params instead of custom snapshotId option - Apply sensible defaults for timeout, resources, and runtime - Update handler to pass source object for snapshots - Add tests for new parameter combinations Co-Authored-By: Claude Opus 4.5 --- lib/sandbox/__tests__/createSandbox.test.ts | 31 +++++++++++++++++- .../createSandboxPostHandler.test.ts | 6 ++-- lib/sandbox/createSandbox.ts | 32 ++++++++++--------- lib/sandbox/createSandboxPostHandler.ts | 6 ++-- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/lib/sandbox/__tests__/createSandbox.test.ts b/lib/sandbox/__tests__/createSandbox.test.ts index 6cc3eb06..7cbe4970 100644 --- a/lib/sandbox/__tests__/createSandbox.test.ts +++ b/lib/sandbox/__tests__/createSandbox.test.ts @@ -28,7 +28,7 @@ describe("createSandbox", () => { vi.clearAllMocks(); }); - it("creates sandbox with correct configuration", async () => { + it("creates sandbox with default configuration when no params provided", async () => { await createSandbox(); expect(Sandbox.create).toHaveBeenCalledWith({ @@ -38,6 +38,35 @@ describe("createSandbox", () => { }); }); + it("creates sandbox from snapshot when source is provided", async () => { + await createSandbox({ source: { type: "snapshot", snapshotId: "snap_abc123" } }); + + expect(Sandbox.create).toHaveBeenCalledWith({ + source: { type: "snapshot", snapshotId: "snap_abc123" }, + timeout: 600000, + }); + }); + + it("allows overriding default timeout", async () => { + await createSandbox({ timeout: 300000 }); + + expect(Sandbox.create).toHaveBeenCalledWith({ + resources: { vcpus: 4 }, + timeout: 300000, + runtime: "node22", + }); + }); + + it("allows overriding default resources", async () => { + await createSandbox({ resources: { vcpus: 2 } }); + + expect(Sandbox.create).toHaveBeenCalledWith({ + resources: { vcpus: 2 }, + timeout: 600000, + runtime: "node22", + }); + }); + it("returns sandbox created response with sandboxStatus", async () => { const result = await createSandbox(); diff --git a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts index 4f1c4e84..a6503fbd 100644 --- a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts +++ b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts @@ -137,10 +137,10 @@ describe("createSandboxPostHandler", () => { const request = createMockRequest(); await createSandboxPostHandler(request); - expect(createSandbox).toHaveBeenCalledWith({ snapshotId: "snap_xyz" }); + expect(createSandbox).toHaveBeenCalledWith({ source: { type: "snapshot", snapshotId: "snap_xyz" } }); }); - it("calls createSandbox with null snapshotId when account has no snapshot", async () => { + it("calls createSandbox with empty params when account has no snapshot", async () => { vi.mocked(validateSandboxBody).mockResolvedValue({ accountId: "acc_123", orgId: null, @@ -170,7 +170,7 @@ describe("createSandboxPostHandler", () => { const request = createMockRequest(); await createSandboxPostHandler(request); - expect(createSandbox).toHaveBeenCalledWith({ snapshotId: null }); + expect(createSandbox).toHaveBeenCalledWith({}); }); it("calls insertAccountSandbox with correct account_id and sandbox_id", async () => { diff --git a/lib/sandbox/createSandbox.ts b/lib/sandbox/createSandbox.ts index a1ef7016..28e92e8e 100644 --- a/lib/sandbox/createSandbox.ts +++ b/lib/sandbox/createSandbox.ts @@ -8,34 +8,36 @@ export interface SandboxCreatedResponse { createdAt: string; } -interface CreateSandboxOptions { - snapshotId?: string | null; -} +/** Extract CreateSandboxParams from Sandbox.create method signature */ +export type CreateSandboxParams = NonNullable[0]>; + +const DEFAULT_TIMEOUT = ms("10m"); +const DEFAULT_VCPUS = 4; +const DEFAULT_RUNTIME = "node22"; /** * Creates a Vercel Sandbox and returns its info. * * The sandbox is left running so that commands can be executed via the runSandboxCommand task. - * If a snapshotId is provided, the sandbox will be created from that snapshot. + * Accepts the same parameters as Sandbox.create from @vercel/sandbox. * - * @param options - Optional configuration including snapshotId + * @param params - Sandbox creation parameters (source, timeout, resources, runtime, ports) * @returns The sandbox creation response * @throws Error if sandbox creation fails */ -export async function createSandbox( - options: CreateSandboxOptions = {}, -): Promise { - const { snapshotId } = options; +export async function createSandbox(params: CreateSandboxParams = {}): Promise { + const hasSnapshotSource = params.source && "type" in params.source && params.source.type === "snapshot"; - const sandbox = snapshotId + const sandbox = hasSnapshotSource ? await Sandbox.create({ - source: { type: "snapshot", snapshotId }, - timeout: ms("10m"), + ...params, + timeout: params.timeout ?? DEFAULT_TIMEOUT, }) : await Sandbox.create({ - resources: { vcpus: 4 }, - timeout: ms("10m"), - runtime: "node22", + ...params, + resources: params.resources ?? { vcpus: DEFAULT_VCPUS }, + timeout: params.timeout ?? DEFAULT_TIMEOUT, + runtime: params.runtime ?? DEFAULT_RUNTIME, }); return { diff --git a/lib/sandbox/createSandboxPostHandler.ts b/lib/sandbox/createSandboxPostHandler.ts index ec1a5b65..34e2b59b 100644 --- a/lib/sandbox/createSandboxPostHandler.ts +++ b/lib/sandbox/createSandboxPostHandler.ts @@ -27,10 +27,12 @@ export async function createSandboxPostHandler(request: NextRequest): Promise Date: Tue, 3 Feb 2026 13:07:05 -0500 Subject: [PATCH 06/13] fix: handle union type for CreateSandboxParams Use 'in' operator to check for runtime/resources properties since they don't exist on the snapshot variant of the union type. Co-Authored-By: Claude Opus 4.5 --- lib/sandbox/createSandbox.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/sandbox/createSandbox.ts b/lib/sandbox/createSandbox.ts index 28e92e8e..fb8c8bb3 100644 --- a/lib/sandbox/createSandbox.ts +++ b/lib/sandbox/createSandbox.ts @@ -34,10 +34,12 @@ export async function createSandbox(params: CreateSandboxParams = {}): Promise Date: Tue, 3 Feb 2026 13:11:49 -0500 Subject: [PATCH 07/13] fix: simplify createSandbox to spread defaults before params Spread defaults first, then override with params. This avoids type conflicts between snapshot and base param variants. Co-Authored-By: Claude Opus 4.5 --- lib/sandbox/createSandbox.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/sandbox/createSandbox.ts b/lib/sandbox/createSandbox.ts index fb8c8bb3..570621af 100644 --- a/lib/sandbox/createSandbox.ts +++ b/lib/sandbox/createSandbox.ts @@ -28,19 +28,20 @@ const DEFAULT_RUNTIME = "node22"; export async function createSandbox(params: CreateSandboxParams = {}): Promise { const hasSnapshotSource = params.source && "type" in params.source && params.source.type === "snapshot"; - const sandbox = hasSnapshotSource - ? await Sandbox.create({ - ...params, - timeout: params.timeout ?? DEFAULT_TIMEOUT, - }) - : await Sandbox.create({ - resources: "resources" in params && params.resources ? params.resources : { vcpus: DEFAULT_VCPUS }, - timeout: params.timeout ?? DEFAULT_TIMEOUT, - runtime: "runtime" in params && params.runtime ? params.runtime : DEFAULT_RUNTIME, - ports: params.ports, - source: params.source, - signal: params.signal, - }); + // Pass params directly to SDK - it handles all the type variants + const sandbox = await Sandbox.create( + hasSnapshotSource + ? { + ...params, + timeout: params.timeout ?? DEFAULT_TIMEOUT, + } + : { + resources: { vcpus: DEFAULT_VCPUS }, + timeout: params.timeout ?? DEFAULT_TIMEOUT, + runtime: DEFAULT_RUNTIME, + ...params, + }, + ); return { sandboxId: sandbox.sandboxId, From 056b7fed85f5c576f866d379613992004da8b62f Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 3 Feb 2026 13:24:18 -0500 Subject: [PATCH 08/13] fix: handle trigger failure gracefully and always return response - Wrap triggerRunSandboxCommand in try-catch to prevent hanging - Return 200 with sandbox info even if trigger fails (runId omitted) - Update JSDoc to document new command/args/cwd request format - Update tests to reflect new graceful failure behavior Co-Authored-By: Claude Opus 4.5 --- app/api/sandboxes/route.ts | 8 ++++--- .../createSandboxPostHandler.test.ts | 17 ++++++++++---- lib/sandbox/createSandboxPostHandler.ts | 23 ++++++++++++------- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/app/api/sandboxes/route.ts b/app/api/sandboxes/route.ts index 897848a2..eec0ab82 100644 --- a/app/api/sandboxes/route.ts +++ b/app/api/sandboxes/route.ts @@ -19,7 +19,7 @@ export async function OPTIONS() { /** * POST /api/sandboxes * - * Creates a new ephemeral sandbox environment. + * Creates a new ephemeral sandbox environment and 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. @@ -27,11 +27,13 @@ export async function OPTIONS() { * Authentication: x-api-key header or Authorization Bearer token required. * * Request body: - * - prompt: string (required, min length 1) - The prompt to send to Claude Code + * - command: string (required) - The command to execute in the sandbox + * - 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 }] + * - sandboxes: [{ sandboxId, sandboxStatus, timeout, createdAt, runId }] * * Error (400/401): * - status: "error" diff --git a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts index a6503fbd..888cd707 100644 --- a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts +++ b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts @@ -298,7 +298,7 @@ describe("createSandboxPostHandler", () => { }); }); - it("returns 400 with error status when triggerRunSandboxCommand throws", async () => { + it("returns 200 without runId when triggerRunSandboxCommand throws", async () => { vi.mocked(validateSandboxBody).mockResolvedValue({ accountId: "acc_123", orgId: null, @@ -326,11 +326,20 @@ describe("createSandboxPostHandler", () => { const request = createMockRequest(); const response = await createSandboxPostHandler(request); - expect(response.status).toBe(400); + // Sandbox was created successfully, so return 200 even if trigger fails + expect(response.status).toBe(200); const json = await response.json(); expect(json).toEqual({ - status: "error", - error: "Task trigger failed", + status: "success", + sandboxes: [ + { + sandboxId: "sbx_123", + sandboxStatus: "running", + timeout: 600000, + createdAt: "2024-01-01T00:00:00.000Z", + // Note: runId is not included when trigger fails + }, + ], }); }); }); diff --git a/lib/sandbox/createSandboxPostHandler.ts b/lib/sandbox/createSandboxPostHandler.ts index 34e2b59b..0e49a461 100644 --- a/lib/sandbox/createSandboxPostHandler.ts +++ b/lib/sandbox/createSandboxPostHandler.ts @@ -40,13 +40,20 @@ export async function createSandboxPostHandler(request: NextRequest): Promise Date: Tue, 3 Feb 2026 13:39:42 -0500 Subject: [PATCH 09/13] debug: add extensive logging to trace request flow Add console.log at each step to identify where the request is hanging: - Request received - Validation - Snapshot lookup - Sandbox creation - DB insert - Task trigger - Response building Co-Authored-By: Claude Opus 4.5 --- app/api/sandboxes/route.ts | 13 ++++++++++++- lib/sandbox/createSandboxPostHandler.ts | 25 ++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/app/api/sandboxes/route.ts b/app/api/sandboxes/route.ts index eec0ab82..181b2adc 100644 --- a/app/api/sandboxes/route.ts +++ b/app/api/sandboxes/route.ts @@ -43,7 +43,18 @@ export async function OPTIONS() { * @returns A NextResponse with the sandbox creation result or error */ export async function POST(request: NextRequest): Promise { - return createSandboxPostHandler(request); + console.log("[POST /api/sandboxes] Request received"); + try { + const response = await createSandboxPostHandler(request); + console.log("[POST /api/sandboxes] Handler returned response with status:", response.status); + return response; + } catch (error) { + console.error("[POST /api/sandboxes] Unhandled error:", error); + return NextResponse.json( + { status: "error", error: "Internal server error" }, + { status: 500, headers: getCorsHeaders() }, + ); + } } /** diff --git a/lib/sandbox/createSandboxPostHandler.ts b/lib/sandbox/createSandboxPostHandler.ts index 0e49a461..9e5d0ba7 100644 --- a/lib/sandbox/createSandboxPostHandler.ts +++ b/lib/sandbox/createSandboxPostHandler.ts @@ -19,29 +19,41 @@ import { selectAccountSnapshot } from "@/lib/supabase/account_snapshots/selectAc * @returns A NextResponse with sandbox creation result including runId */ export async function createSandboxPostHandler(request: NextRequest): Promise { + console.log("[createSandboxPostHandler] Starting handler"); + + console.log("[createSandboxPostHandler] Validating request body"); const validated = await validateSandboxBody(request); if (validated instanceof NextResponse) { + console.log("[createSandboxPostHandler] Validation failed, returning error response"); return validated; } + console.log("[createSandboxPostHandler] Validation passed for account:", validated.accountId); try { // Get account's snapshot if available + console.log("[createSandboxPostHandler] Fetching account snapshot"); const accountSnapshot = await selectAccountSnapshot(validated.accountId); const snapshotId = accountSnapshot?.snapshot_id; + console.log("[createSandboxPostHandler] Snapshot ID:", snapshotId ?? "none"); // Create sandbox (from snapshot if valid, otherwise fresh) + console.log("[createSandboxPostHandler] Creating sandbox"); const result = await createSandbox( snapshotId ? { source: { type: "snapshot", snapshotId } } : {}, ); + console.log("[createSandboxPostHandler] Sandbox created:", result.sandboxId); + console.log("[createSandboxPostHandler] Inserting account sandbox record"); await insertAccountSandbox({ account_id: validated.accountId, sandbox_id: result.sandboxId, }); + console.log("[createSandboxPostHandler] Account sandbox record inserted"); // Trigger the command execution task let runId: string | undefined; try { + console.log("[createSandboxPostHandler] Triggering run-sandbox-command task"); const handle = await triggerRunSandboxCommand({ command: validated.command, args: validated.args, @@ -50,12 +62,14 @@ export async function createSandboxPostHandler(request: NextRequest): Promise Date: Tue, 3 Feb 2026 16:01:42 -0500 Subject: [PATCH 10/13] chore: remove debug logging Co-Authored-By: Claude Opus 4.5 --- app/api/sandboxes/route.ts | 13 +------------ lib/sandbox/createSandboxPostHandler.ts | 26 +++---------------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/app/api/sandboxes/route.ts b/app/api/sandboxes/route.ts index 181b2adc..eec0ab82 100644 --- a/app/api/sandboxes/route.ts +++ b/app/api/sandboxes/route.ts @@ -43,18 +43,7 @@ export async function OPTIONS() { * @returns A NextResponse with the sandbox creation result or error */ export async function POST(request: NextRequest): Promise { - console.log("[POST /api/sandboxes] Request received"); - try { - const response = await createSandboxPostHandler(request); - console.log("[POST /api/sandboxes] Handler returned response with status:", response.status); - return response; - } catch (error) { - console.error("[POST /api/sandboxes] Unhandled error:", error); - return NextResponse.json( - { status: "error", error: "Internal server error" }, - { status: 500, headers: getCorsHeaders() }, - ); - } + return createSandboxPostHandler(request); } /** diff --git a/lib/sandbox/createSandboxPostHandler.ts b/lib/sandbox/createSandboxPostHandler.ts index 9e5d0ba7..d5bbe18c 100644 --- a/lib/sandbox/createSandboxPostHandler.ts +++ b/lib/sandbox/createSandboxPostHandler.ts @@ -19,41 +19,29 @@ import { selectAccountSnapshot } from "@/lib/supabase/account_snapshots/selectAc * @returns A NextResponse with sandbox creation result including runId */ export async function createSandboxPostHandler(request: NextRequest): Promise { - console.log("[createSandboxPostHandler] Starting handler"); - - console.log("[createSandboxPostHandler] Validating request body"); const validated = await validateSandboxBody(request); if (validated instanceof NextResponse) { - console.log("[createSandboxPostHandler] Validation failed, returning error response"); return validated; } - console.log("[createSandboxPostHandler] Validation passed for account:", validated.accountId); try { // Get account's snapshot if available - console.log("[createSandboxPostHandler] Fetching account snapshot"); const accountSnapshot = await selectAccountSnapshot(validated.accountId); const snapshotId = accountSnapshot?.snapshot_id; - console.log("[createSandboxPostHandler] Snapshot ID:", snapshotId ?? "none"); // Create sandbox (from snapshot if valid, otherwise fresh) - console.log("[createSandboxPostHandler] Creating sandbox"); const result = await createSandbox( snapshotId ? { source: { type: "snapshot", snapshotId } } : {}, ); - console.log("[createSandboxPostHandler] Sandbox created:", result.sandboxId); - console.log("[createSandboxPostHandler] Inserting account sandbox record"); await insertAccountSandbox({ account_id: validated.accountId, sandbox_id: result.sandboxId, }); - console.log("[createSandboxPostHandler] Account sandbox record inserted"); // Trigger the command execution task let runId: string | undefined; try { - console.log("[createSandboxPostHandler] Triggering run-sandbox-command task"); const handle = await triggerRunSandboxCommand({ command: validated.command, args: validated.args, @@ -62,14 +50,11 @@ export async function createSandboxPostHandler(request: NextRequest): Promise Date: Tue, 3 Feb 2026 16:06:44 -0500 Subject: [PATCH 11/13] refactor: rename selectAccountSnapshot.ts to selectAccountSnapshots.ts Follow the select[TableName].ts naming convention where the table name is account_snapshots (plural). Function name unchanged. Co-Authored-By: Claude Opus 4.5 --- lib/sandbox/__tests__/createSandboxPostHandler.test.ts | 4 ++-- lib/sandbox/createSandboxPostHandler.ts | 2 +- .../{selectAccountSnapshot.ts => selectAccountSnapshots.ts} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename lib/supabase/account_snapshots/{selectAccountSnapshot.ts => selectAccountSnapshots.ts} (100%) diff --git a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts index 888cd707..832b5f30 100644 --- a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts +++ b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts @@ -7,7 +7,7 @@ import { validateSandboxBody } from "@/lib/sandbox/validateSandboxBody"; import { createSandbox } from "@/lib/sandbox/createSandbox"; import { insertAccountSandbox } from "@/lib/supabase/account_sandboxes/insertAccountSandbox"; import { triggerRunSandboxCommand } from "@/lib/trigger/triggerRunSandboxCommand"; -import { selectAccountSnapshot } from "@/lib/supabase/account_snapshots/selectAccountSnapshot"; +import { selectAccountSnapshot } from "@/lib/supabase/account_snapshots/selectAccountSnapshots"; vi.mock("@/lib/sandbox/validateSandboxBody", () => ({ validateSandboxBody: vi.fn(), @@ -25,7 +25,7 @@ vi.mock("@/lib/trigger/triggerRunSandboxCommand", () => ({ triggerRunSandboxCommand: vi.fn(), })); -vi.mock("@/lib/supabase/account_snapshots/selectAccountSnapshot", () => ({ +vi.mock("@/lib/supabase/account_snapshots/selectAccountSnapshots", () => ({ selectAccountSnapshot: vi.fn(), })); diff --git a/lib/sandbox/createSandboxPostHandler.ts b/lib/sandbox/createSandboxPostHandler.ts index d5bbe18c..a468bb9c 100644 --- a/lib/sandbox/createSandboxPostHandler.ts +++ b/lib/sandbox/createSandboxPostHandler.ts @@ -5,7 +5,7 @@ import { createSandbox } from "@/lib/sandbox/createSandbox"; import { validateSandboxBody } from "@/lib/sandbox/validateSandboxBody"; import { insertAccountSandbox } from "@/lib/supabase/account_sandboxes/insertAccountSandbox"; import { triggerRunSandboxCommand } from "@/lib/trigger/triggerRunSandboxCommand"; -import { selectAccountSnapshot } from "@/lib/supabase/account_snapshots/selectAccountSnapshot"; +import { selectAccountSnapshot } from "@/lib/supabase/account_snapshots/selectAccountSnapshots"; /** * Handler for POST /api/sandboxes. diff --git a/lib/supabase/account_snapshots/selectAccountSnapshot.ts b/lib/supabase/account_snapshots/selectAccountSnapshots.ts similarity index 100% rename from lib/supabase/account_snapshots/selectAccountSnapshot.ts rename to lib/supabase/account_snapshots/selectAccountSnapshots.ts From 190bd838c31e4e5e80440b519d90ff6f7c2835ca Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 3 Feb 2026 16:11:31 -0500 Subject: [PATCH 12/13] refactor: rename function to selectAccountSnapshots and return array - Rename function from selectAccountSnapshot to selectAccountSnapshots - Remove .limit(1).single() to return full array - Update handler to get first element from array - Update tests with new function name and array return values Co-Authored-By: Claude Opus 4.5 --- .../createSandboxPostHandler.test.ts | 32 ++++++++++--------- lib/sandbox/createSandboxPostHandler.ts | 8 ++--- .../selectAccountSnapshots.ts | 18 +++++------ 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts index 832b5f30..c975b8eb 100644 --- a/lib/sandbox/__tests__/createSandboxPostHandler.test.ts +++ b/lib/sandbox/__tests__/createSandboxPostHandler.test.ts @@ -7,7 +7,7 @@ import { validateSandboxBody } from "@/lib/sandbox/validateSandboxBody"; import { createSandbox } from "@/lib/sandbox/createSandbox"; import { insertAccountSandbox } from "@/lib/supabase/account_sandboxes/insertAccountSandbox"; import { triggerRunSandboxCommand } from "@/lib/trigger/triggerRunSandboxCommand"; -import { selectAccountSnapshot } from "@/lib/supabase/account_snapshots/selectAccountSnapshots"; +import { selectAccountSnapshots } from "@/lib/supabase/account_snapshots/selectAccountSnapshots"; vi.mock("@/lib/sandbox/validateSandboxBody", () => ({ validateSandboxBody: vi.fn(), @@ -26,7 +26,7 @@ vi.mock("@/lib/trigger/triggerRunSandboxCommand", () => ({ })); vi.mock("@/lib/supabase/account_snapshots/selectAccountSnapshots", () => ({ - selectAccountSnapshot: vi.fn(), + selectAccountSnapshots: vi.fn(), })); /** @@ -63,7 +63,7 @@ describe("createSandboxPostHandler", () => { authToken: "token", command: "ls", }); - vi.mocked(selectAccountSnapshot).mockResolvedValue(null); + vi.mocked(selectAccountSnapshots).mockResolvedValue([]); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_123", sandboxStatus: "running", @@ -109,12 +109,14 @@ describe("createSandboxPostHandler", () => { authToken: "token", command: "ls", }); - vi.mocked(selectAccountSnapshot).mockResolvedValue({ - id: "snap_record_123", - account_id: "acc_123", - snapshot_id: "snap_xyz", - created_at: "2024-01-01T00:00:00.000Z", - }); + vi.mocked(selectAccountSnapshots).mockResolvedValue([ + { + id: "snap_record_123", + account_id: "acc_123", + snapshot_id: "snap_xyz", + created_at: "2024-01-01T00:00:00.000Z", + }, + ]); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_456", sandboxStatus: "running", @@ -147,7 +149,7 @@ describe("createSandboxPostHandler", () => { authToken: "token", command: "ls", }); - vi.mocked(selectAccountSnapshot).mockResolvedValue(null); + vi.mocked(selectAccountSnapshots).mockResolvedValue([]); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_456", sandboxStatus: "running", @@ -180,7 +182,7 @@ describe("createSandboxPostHandler", () => { authToken: "token", command: "ls", }); - vi.mocked(selectAccountSnapshot).mockResolvedValue(null); + vi.mocked(selectAccountSnapshots).mockResolvedValue([]); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_456", sandboxStatus: "running", @@ -218,7 +220,7 @@ describe("createSandboxPostHandler", () => { args: ["-la"], cwd: "/home", }); - vi.mocked(selectAccountSnapshot).mockResolvedValue(null); + vi.mocked(selectAccountSnapshots).mockResolvedValue([]); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_789", sandboxStatus: "running", @@ -257,7 +259,7 @@ describe("createSandboxPostHandler", () => { authToken: "token", command: "ls", }); - vi.mocked(selectAccountSnapshot).mockResolvedValue(null); + vi.mocked(selectAccountSnapshots).mockResolvedValue([]); vi.mocked(createSandbox).mockRejectedValue(new Error("Sandbox creation failed")); const request = createMockRequest(); @@ -278,7 +280,7 @@ describe("createSandboxPostHandler", () => { authToken: "token", command: "ls", }); - vi.mocked(selectAccountSnapshot).mockResolvedValue(null); + vi.mocked(selectAccountSnapshots).mockResolvedValue([]); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_123", sandboxStatus: "running", @@ -305,7 +307,7 @@ describe("createSandboxPostHandler", () => { authToken: "token", command: "ls", }); - vi.mocked(selectAccountSnapshot).mockResolvedValue(null); + vi.mocked(selectAccountSnapshots).mockResolvedValue([]); vi.mocked(createSandbox).mockResolvedValue({ sandboxId: "sbx_123", sandboxStatus: "running", diff --git a/lib/sandbox/createSandboxPostHandler.ts b/lib/sandbox/createSandboxPostHandler.ts index a468bb9c..b01a79c3 100644 --- a/lib/sandbox/createSandboxPostHandler.ts +++ b/lib/sandbox/createSandboxPostHandler.ts @@ -5,7 +5,7 @@ import { createSandbox } from "@/lib/sandbox/createSandbox"; import { validateSandboxBody } from "@/lib/sandbox/validateSandboxBody"; import { insertAccountSandbox } from "@/lib/supabase/account_sandboxes/insertAccountSandbox"; import { triggerRunSandboxCommand } from "@/lib/trigger/triggerRunSandboxCommand"; -import { selectAccountSnapshot } from "@/lib/supabase/account_snapshots/selectAccountSnapshots"; +import { selectAccountSnapshots } from "@/lib/supabase/account_snapshots/selectAccountSnapshots"; /** * Handler for POST /api/sandboxes. @@ -25,9 +25,9 @@ export async function createSandboxPostHandler(request: NextRequest): Promise { +export async function selectAccountSnapshots(accountId: string): Promise { const { data, error } = await supabase .from("account_snapshots") .select("*") .eq("account_id", accountId) - .order("created_at", { ascending: false }) - .limit(1) - .single(); + .order("created_at", { ascending: false }); if (error) { - // Table might not exist or no snapshot found - both are ok - return null; + // Table might not exist or query failed - return empty array + return []; } - return data; + return data ?? []; } From 7f8fa6543c961a2a6bcec966d21cf167958668b3 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Tue, 3 Feb 2026 16:29:39 -0500 Subject: [PATCH 13/13] refactor: use Tables type from Supabase schema for account_snapshots - Regenerate types with supabase gen types to include account_snapshots table - Replace local AccountSnapshot interface with Tables<"account_snapshots"> - DRY: single source of truth for type definitions Co-Authored-By: Claude Opus 4.5 --- .../selectAccountSnapshots.ts | 12 +++---- types/database.types.ts | 35 +++++++++++++++---- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/lib/supabase/account_snapshots/selectAccountSnapshots.ts b/lib/supabase/account_snapshots/selectAccountSnapshots.ts index b269de48..89139725 100644 --- a/lib/supabase/account_snapshots/selectAccountSnapshots.ts +++ b/lib/supabase/account_snapshots/selectAccountSnapshots.ts @@ -1,11 +1,5 @@ import supabase from "../serverClient"; - -interface AccountSnapshot { - id: string; - account_id: string; - snapshot_id: string; - created_at: string; -} +import type { Tables } from "@/types/database.types"; /** * Selects snapshots for an account, ordered by creation date (newest first). @@ -13,7 +7,9 @@ interface AccountSnapshot { * @param accountId - The account ID to get snapshots for * @returns Array of snapshot records, or empty array if none found */ -export async function selectAccountSnapshots(accountId: string): Promise { +export async function selectAccountSnapshots( + accountId: string, +): Promise[]> { const { data, error } = await supabase .from("account_snapshots") .select("*") diff --git a/types/database.types.ts b/types/database.types.ts index 58a12bfa..e22ee933 100644 --- a/types/database.types.ts +++ b/types/database.types.ts @@ -166,8 +166,6 @@ export type Database = { job_title: string | null knowledges: Json | null label: string | null - onboarding_data: Json | null - onboarding_status: Json | null organization: string | null role_type: string | null updated_at: string @@ -181,8 +179,6 @@ export type Database = { job_title?: string | null knowledges?: Json | null label?: string | null - onboarding_data?: Json | null - onboarding_status?: Json | null organization?: string | null role_type?: string | null updated_at?: string @@ -196,8 +192,6 @@ export type Database = { job_title?: string | null knowledges?: Json | null label?: string | null - onboarding_data?: Json | null - onboarding_status?: Json | null organization?: string | null role_type?: string | null updated_at?: string @@ -306,6 +300,35 @@ export type Database = { }, ] } + account_snapshots: { + Row: { + account_id: string + created_at: string | null + expires_at: string + snapshot_id: string + } + Insert: { + account_id: string + created_at?: string | null + expires_at: string + snapshot_id: string + } + Update: { + account_id?: string + created_at?: string | null + expires_at?: string + snapshot_id?: string + } + Relationships: [ + { + foreignKeyName: "account_snapshots_account_id_fkey" + columns: ["account_id"] + isOneToOne: true + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + ] + } account_socials: { Row: { account_id: string | null