feat(ai-sandbox): add Blaxel provider - #1065
Conversation
📝 WalkthroughWalkthroughThis pull request adds the ChangesBlaxel sandbox provider
Estimated code review effort: 5 (Critical) | ~90 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant BlaxelProvider
participant BlaxelSandbox
participant BlaxelHandle
participant ChunkStream
Client->>BlaxelProvider: create sandbox
BlaxelProvider->>BlaxelSandbox: create and prepare workdir
BlaxelProvider-->>Client: return BlaxelHandle
Client->>BlaxelHandle: spawn command
BlaxelHandle->>BlaxelSandbox: start process and stream logs
BlaxelSandbox-->>ChunkStream: stdout and stderr frames
ChunkStream-->>Client: bounded async output
BlaxelHandle-->>Client: process result and exit status
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. 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. Comment |
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
packages/ai-sandbox-blaxel/tests/blaxel.test.ts (1)
1-3: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePlace these tests alongside their source modules.
Both files use Vitest correctly, but both are outside the required colocated test layout.
packages/ai-sandbox-blaxel/tests/blaxel.test.ts#L1-L3: Move this test beside its relevant module underpackages/ai-sandbox-blaxel/src/and update its relative import.packages/ai-sandbox-blaxel/tests/journal.conformance.test.ts#L1-L6: Move this test beside its relevant module underpackages/ai-sandbox-blaxel/src/and update its relative import.As per coding guidelines, “Test files should be placed alongside source code as *.test.ts files using Vitest with happy-dom for DOM testing.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-sandbox-blaxel/tests/blaxel.test.ts` around lines 1 - 3, Move packages/ai-sandbox-blaxel/tests/blaxel.test.ts into the relevant source directory under packages/ai-sandbox-blaxel/src/ and update its relative import of blaxelSandbox. Move packages/ai-sandbox-blaxel/tests/journal.conformance.test.ts alongside its relevant source module under the same src directory and update its relative import; retain the existing Vitest test behavior and naming.Source: Coding guidelines
packages/ai-sandbox-blaxel/src/index.ts (1)
3-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExport
BlaxelSandboxLikewithBlaxelHandleDeps.
BlaxelHandleDeps.sandboxhas typeBlaxelSandboxLike, but that type is not re-exported. A consumer who constructsBlaxelHandledirectly cannot name or implement the dependency type without deep-importing./handle.♻️ Proposed export addition
-export type { BlaxelHandleDeps } from './handle' +export type { + BlaxelHandleDeps, + BlaxelSandboxLike, + BlaxelDirectoryLike, + BlaxelProcessLike, + BlaxelProcessRequestLike, + BlaxelPreviewLike, + BlaxelWatchEventLike, +} from './handle'🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-sandbox-blaxel/src/index.ts` around lines 3 - 4, Update the package exports alongside BlaxelHandleDeps to re-export the BlaxelSandboxLike type from the handle module, allowing consumers to name or implement the sandbox dependency without deep imports.packages/ai-sandbox-blaxel/src/handle.ts (1)
383-399: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueEscape
\nconsistently inside the generated shell script.Lines 385, 398, and 399 are template literals, so
'\n'becomes a real newline character in the emitted script rather than the two characters\andn. The resultingprintfstill prints a newline, because a literal newline inside single quotes is valid shell. Line 442 uses'%s\\n'and emits the escape sequence instead. The two forms are equivalent today, but the embedded raw newlines split one logical script line into two and break if the script text is ever normalized or re-indented.Use
\\nin all three places for consistency with Line 442.♻️ Proposed change
- ` printf '%s' ${q(recordPrefix)}`, + ` printf '%s' ${q(recordPrefix)}`, ` base64 < ${chunkFile} | tr -d '\r\n'`, - ` printf '\n'`, + ` printf '\\n'`, @@ - ` printf '%s\n' ${q(label)} >> ${limitsFile}`, - ` printf '%s\n' ${q(overflowMarker)}`, + ` printf '%s\\n' ${q(label)} >> ${limitsFile}`, + ` printf '%s\\n' ${q(overflowMarker)}`,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-sandbox-blaxel/src/handle.ts` around lines 383 - 399, Update the generated shell-script strings in the surrounding chunk-processing logic to use escaped backslash-n sequences consistently in all three affected printf calls, matching the existing format used by the limits-file output. Ensure the emitted script contains the two characters \ and n rather than embedding literal newlines.packages/ai-sandbox-blaxel/tests/provider.test.ts (1)
347-370: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winCreate the rejected
deleteGatelazily to avoid an unhandled rejection.
deleteGateis assigned an already-rejected promise, but no handler attaches untilSandboxInstance.deleteruns, which happens after several awaited steps. Node can reportunhandledRejectionin that window and make the test flaky. Make the fake produce the rejection whendeleteis called.♻️ Proposed change: reject inside the fake
-let deleteGate: Promise<Record<string, never>> | undefined +let deleteGate: (() => Promise<Record<string, never>>) | undefined @@ delete: async (name: string) => { calls.deleted.push(name) - return deleteGate ?? {} + return deleteGate ? await deleteGate() : {} },Then set
deleteGate = () => Promise.reject({ status: 500, message: 'delete failed' })in the affected tests. Apply the same pattern to the eagerly rejectedcreateGatevalues at Lines 331 and 348.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-sandbox-blaxel/tests/provider.test.ts` around lines 347 - 370, Update the fake gate used by SandboxInstance.delete to create its rejected promise only when delete is invoked, then change the affected tests to assign a rejection-producing function instead of an already-rejected promise. Apply the same lazy-rejection pattern to createGate in both affected create-error tests, preserving their existing error assertions and call tracking.packages/ai-sandbox-blaxel/tests/handle.test.ts (1)
703-725: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake the live child-process cleanup failure-safe.
rmSync(outputDir!)and the child termination run only on the success path. Ifvi.waitForat Line 707 or Line 718 fails, the test leaves a real/tmp/tanstack-ai-output-*directory and possibly livesleep 30process groups on the machine. Move the cleanup into atry/finally.The test also depends on
bashbeing installed, because the generated wrapper ends withexec bash <supervisor>. On a runner withoutbash, thepidsfile never appears and the failure mode is an opaquewaitFortimeout.♻️ Proposed change
const child = spawnChild('/bin/sh', ['-c', script], { stdio: 'ignore', }) - const pidsPath = `${outputDir!}/pids` - await vi.waitFor(() => expect(existsSync(pidsPath)).toBe(true)) - const pids = readFileSync(pidsPath, 'utf8') - .trim() - .split(/\s+/) - .map(Number) - const exited = new Promise<void>((resolve, reject) => { - child.once('error', reject) - child.once('exit', () => resolve()) - }) - child.kill('SIGTERM') - await exited - await vi.waitFor(() => { - for (const pid of pids) { - expect(() => process.kill(-pid, 0)).toThrow() - } - }) - rmSync(outputDir!, { recursive: true, force: true }) + try { + const pidsPath = `${outputDir!}/pids` + await vi.waitFor(() => expect(existsSync(pidsPath)).toBe(true)) + const pids = readFileSync(pidsPath, 'utf8') + .trim() + .split(/\s+/) + .map(Number) + const exited = new Promise<void>((resolve, reject) => { + child.once('error', reject) + child.once('exit', () => resolve()) + }) + child.kill('SIGTERM') + await exited + await vi.waitFor(() => { + for (const pid of pids) { + expect(() => process.kill(-pid, 0)).toThrow() + } + }) + } finally { + child.kill('SIGKILL') + rmSync(outputDir!, { recursive: true, force: true }) + } resolveWait({ exitCode: 0, stdout: '', stderr: '' })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-sandbox-blaxel/tests/handle.test.ts` around lines 703 - 725, Make the child-process test cleanup failure-safe by wrapping the spawn, PID-file wait, termination, and process-group assertions in a try/finally, ensuring the child is terminated and outputDir is removed even when either vi.waitFor fails. Also make the test explicitly skip or guard execution when bash is unavailable, since the generated wrapper invokes bash and otherwise produces a timeout.packages/ai-sandbox-blaxel/src/provider.ts (1)
255-260: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winReconciliation can delay the rejection by about 29 seconds and ignores
input.signal.
findOwnedSandboxpolls up toCREATE_RECONCILE_ATTEMPTS(30) times with a 1000 ms sleep. ThemayHaveCreatedSandboxbranch at Line 259 awaits that reconciliation beforecreate()rejects. A caller that receives a 504 therefore waits about 29 seconds, and an abort raised during that window has no effect becauseinput.signalis not passed intofindOwnedSandbox.Consider passing
input.signalinto the reconciliation loop and detaching the wait from the caller promise, as the abort path at Line 245 already does.♻️ Sketch: stop polling once the caller aborts
private async findOwnedSandbox( name: string, attemptId: string, + signal?: AbortSignal, ): Promise<BlaxelSandboxLike | undefined> { for (let attempt = 0; attempt < CREATE_RECONCILE_ATTEMPTS; attempt += 1) { + if (signal?.aborted) return undefined try {Also applies to: 290-306
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-sandbox-blaxel/src/provider.ts` around lines 255 - 260, Update the mayHaveCreatedSandbox reconciliation path around cleanupOwnedSandbox so create() rejects immediately instead of awaiting polling. Run cleanupOwnedSandbox in a detached, safely handled promise as in the existing abort path, and propagate input.signal through cleanupOwnedSandbox to findOwnedSandbox so reconciliation stops when the caller aborts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ai-sandbox-blaxel/src/handle.ts`:
- Around line 956-958: Update the token creation flow around
preview.tokens.create so the expiry is derived from the configured
BlaxelSandboxConfig.previewTtl instead of the fixed PREVIEW_TOKEN_TTL_MS value.
Preserve the existing default behavior when previewTtl is unset, and ensure
longer configured preview lifetimes produce tokens that remain valid for the
full preview duration.
In `@packages/ai-sandbox-blaxel/tests/blaxel.test.ts`:
- Line 156: Update the test around provider.create({}) to register the returned
sandbox handle with the suite cleanup tracker immediately after creation,
ensuring afterAll can retry destroy even if later assertions or cleanup fail.
In `@packages/ai-sandbox/README.md`:
- Line 56: Update the provider installation documentation around the
`@tanstack/ai-sandbox-blaxel` entry to explicitly show installing the selected
provider package separately, or clearly state that provider packages must be
installed independently of the base package. Ensure the example uses the
provider package name shown in the table.
---
Nitpick comments:
In `@packages/ai-sandbox-blaxel/src/handle.ts`:
- Around line 383-399: Update the generated shell-script strings in the
surrounding chunk-processing logic to use escaped backslash-n sequences
consistently in all three affected printf calls, matching the existing format
used by the limits-file output. Ensure the emitted script contains the two
characters \ and n rather than embedding literal newlines.
In `@packages/ai-sandbox-blaxel/src/index.ts`:
- Around line 3-4: Update the package exports alongside BlaxelHandleDeps to
re-export the BlaxelSandboxLike type from the handle module, allowing consumers
to name or implement the sandbox dependency without deep imports.
In `@packages/ai-sandbox-blaxel/src/provider.ts`:
- Around line 255-260: Update the mayHaveCreatedSandbox reconciliation path
around cleanupOwnedSandbox so create() rejects immediately instead of awaiting
polling. Run cleanupOwnedSandbox in a detached, safely handled promise as in the
existing abort path, and propagate input.signal through cleanupOwnedSandbox to
findOwnedSandbox so reconciliation stops when the caller aborts.
In `@packages/ai-sandbox-blaxel/tests/blaxel.test.ts`:
- Around line 1-3: Move packages/ai-sandbox-blaxel/tests/blaxel.test.ts into the
relevant source directory under packages/ai-sandbox-blaxel/src/ and update its
relative import of blaxelSandbox. Move
packages/ai-sandbox-blaxel/tests/journal.conformance.test.ts alongside its
relevant source module under the same src directory and update its relative
import; retain the existing Vitest test behavior and naming.
In `@packages/ai-sandbox-blaxel/tests/handle.test.ts`:
- Around line 703-725: Make the child-process test cleanup failure-safe by
wrapping the spawn, PID-file wait, termination, and process-group assertions in
a try/finally, ensuring the child is terminated and outputDir is removed even
when either vi.waitFor fails. Also make the test explicitly skip or guard
execution when bash is unavailable, since the generated wrapper invokes bash and
otherwise produces a timeout.
In `@packages/ai-sandbox-blaxel/tests/provider.test.ts`:
- Around line 347-370: Update the fake gate used by SandboxInstance.delete to
create its rejected promise only when delete is invoked, then change the
affected tests to assign a rejection-producing function instead of an
already-rejected promise. Apply the same lazy-rejection pattern to createGate in
both affected create-error tests, preserving their existing error assertions and
call tracking.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a619c30d-f13a-4915-9c71-75bf2d5c4d53
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (16)
.changeset/add-ai-sandbox-blaxel.mddocs/config.jsondocs/sandbox/providers.mdpackages/ai-sandbox-blaxel/CHANGELOG.mdpackages/ai-sandbox-blaxel/package.jsonpackages/ai-sandbox-blaxel/src/handle.tspackages/ai-sandbox-blaxel/src/index.tspackages/ai-sandbox-blaxel/src/provider.tspackages/ai-sandbox-blaxel/src/utils.tspackages/ai-sandbox-blaxel/tests/blaxel.test.tspackages/ai-sandbox-blaxel/tests/handle.test.tspackages/ai-sandbox-blaxel/tests/journal.conformance.test.tspackages/ai-sandbox-blaxel/tests/provider.test.tspackages/ai-sandbox-blaxel/tsconfig.jsonpackages/ai-sandbox-blaxel/vite.config.tspackages/ai-sandbox/README.md
|
Thanks, these were useful catches. I fixed the preview-token lifetime, made the live cleanup tests safer, clarified the provider install step, and cleaned up the two smaller test and shell-script issues. I kept the tests in Everything passes again: the full uncached PR gate across all 74 projects, plus 93 live Blaxel tests with 3 expected capability skips. No test sandboxes were left behind. |
6aab0b1 to
502fa6c
Compare
|
Maintainer sweep: rebased onto |
|
View your CI Pipeline Execution ↗ for commit 502fa6c
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-byteplus
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-codex
@tanstack/ai-devtools-core
@tanstack/ai-durable-stream
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-daytona
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-isolate-quickjs-bun
@tanstack/ai-mcp
@tanstack/ai-memory
@tanstack/ai-mistral
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-persistence
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-sandbox
@tanstack/ai-sandbox-blaxel
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-vercel
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
Signed-off-by: Michael Stolarz <146425971+SystemSculpt@users.noreply.github.com>
502fa6c to
5e58afc
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ai-sandbox-blaxel/src/handle.ts`:
- Around line 547-566: Update the reaper exec call in the surrounding reap
method to use non-blocking execution instead of waitForCompletion: true, then
poll the returned process until it completes while honoring the existing
30-second timeout. Preserve the current cleanup command and working directory,
and ensure the polling path handles completion and timeout explicitly.
- Around line 972-977: Update the port validation in connectPort around
actualPort and the existing preview check so it throws only when
preview.spec.port is defined and differs from the requested port. Allow omitted
port values to proceed without changing the mismatch error for explicitly
returned ports.
In `@packages/ai-sandbox-blaxel/tests/handle.test.ts`:
- Around line 722-746: Update the PID-reading logic in the test’s vi.waitFor
block to parse the file and require exactly three valid, nonzero PIDs before
assigning them to pids or signaling any process groups. Keep waiting while the
file is empty, incomplete, or contains invalid values, and ensure the cleanup
loop in finally only receives validated PIDs.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 31a93b5f-f1f3-42da-a55d-203bd35ca8dd
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (15)
.changeset/add-ai-sandbox-blaxel.mddocs/sandbox/providers.mdpackages/ai-sandbox-blaxel/CHANGELOG.mdpackages/ai-sandbox-blaxel/package.jsonpackages/ai-sandbox-blaxel/src/handle.tspackages/ai-sandbox-blaxel/src/index.tspackages/ai-sandbox-blaxel/src/provider.tspackages/ai-sandbox-blaxel/src/utils.tspackages/ai-sandbox-blaxel/tests/blaxel.test.tspackages/ai-sandbox-blaxel/tests/handle.test.tspackages/ai-sandbox-blaxel/tests/journal.conformance.test.tspackages/ai-sandbox-blaxel/tests/provider.test.tspackages/ai-sandbox-blaxel/tsconfig.jsonpackages/ai-sandbox-blaxel/vite.config.tspackages/ai-sandbox/README.md
🚧 Files skipped from review as they are similar to previous changes (13)
- packages/ai-sandbox-blaxel/CHANGELOG.md
- packages/ai-sandbox-blaxel/tsconfig.json
- .changeset/add-ai-sandbox-blaxel.md
- packages/ai-sandbox-blaxel/vite.config.ts
- packages/ai-sandbox-blaxel/src/index.ts
- packages/ai-sandbox-blaxel/src/utils.ts
- packages/ai-sandbox/README.md
- packages/ai-sandbox-blaxel/tests/blaxel.test.ts
- packages/ai-sandbox-blaxel/tests/journal.conformance.test.ts
- packages/ai-sandbox-blaxel/src/provider.ts
- packages/ai-sandbox-blaxel/tests/provider.test.ts
- packages/ai-sandbox-blaxel/package.json
- docs/sandbox/providers.md
| const result = await this.sandbox.process.exec({ | ||
| name: this.processName('reap'), | ||
| command: [ | ||
| `if [ -r ${pidsFile} ]; then`, | ||
| ` __tanstack_pids=$(cat ${pidsFile})`, | ||
| ' for __tanstack_pid in $__tanstack_pids; do', | ||
| ' case "$__tanstack_pid" in (""|*[!0-9]*) continue ;; esac', | ||
| ' kill -TERM -- "-$__tanstack_pid" 2>/dev/null || true', | ||
| ' done', | ||
| ' sleep 0.1', | ||
| ' for __tanstack_pid in $__tanstack_pids; do', | ||
| ' case "$__tanstack_pid" in (""|*[!0-9]*) continue ;; esac', | ||
| ' kill -KILL -- "-$__tanstack_pid" 2>/dev/null || true', | ||
| ' done', | ||
| 'fi', | ||
| ].join('\n'), | ||
| workingDir: this.workdir, | ||
| waitForCompletion: true, | ||
| timeout: 30, | ||
| }) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
@blaxel/core sandbox process exec ProcessRequest timeout unit seconds default
💡 Result:
In the @blaxel/core library, the unit for the timeout parameter in ProcessRequest is seconds [1][2][3]. For processes started with keepAlive: true, the default timeout is 600 seconds (10 minutes) [1][3]. If no timeout is specified for a keepAlive process, it will automatically terminate after this default duration [3]. To disable this auto-kill behavior and run a process indefinitely, you can set the timeout to 0 [1][3]. It is important to note that the behavior of the timeout parameter depends on whether keepAlive is enabled [2][3]: 1. With keepAlive: true: The timeout acts as an auto-kill duration for the process itself [2][3]. 2. Without keepAlive: true: The timeout does not kill the process; it only limits how long the API call waits when using options like waitForCompletion or waitForPorts [2][3]. In this mode, if the timeout elapses, the API returns an error, but the process continues to run in the background [2][3]. Additionally, when using waitForCompletion, Blaxel enforces a separate timeout limit of 60 seconds for the API call itself [2][3]. Users are advised not to set the timeout parameter longer than this limit when waiting for completion [2][3].
Citations:
- 1: https://docs.blaxel.ai/api-reference/process/execute-a-command
- 2: https://docs.blaxel.ai/Sandboxes/Processes
- 3: https://blaxel.mintlify.app/Sandboxes/Processes
🏁 Script executed:
rg -n --glob '!*node_modules*' 'BlaxelProcessRequestLike|waitForCompletion|keepAlive|timeout' packages/ai-sandbox-blaxel packages | head -200Repository: TanStack/ai
Length of output: 23431
🌐 Web query:
site:docs.blaxel.ai site:blaxel.mintlify.app waitForCompletion timeout 60 seconds process.exec
💡 Result:
The usage of waitForCompletion: true with a timeout parameter in process.exec refers to the Blaxel sandbox environment API [1][2]. In the Blaxel ecosystem, when you execute a command using sandbox.process.exec, you can use the waitForCompletion flag to make the call block until the process finishes [1][3]. The timeout parameter specifies how long the API call will wait for that completion in seconds [1][2]. Key behaviors regarding this timeout: - Enforcement: When waitForCompletion is used, Blaxel enforces a timeout limit of 60 seconds [1][2]. It is advised not to set the timeout value higher than this for this specific mode [1][2]. - Process Lifecycle: For processes without keepAlive: true, the timeout parameter does not kill the underlying process [1][2]. Instead, it only bounds the duration of the API call [1][2]. If the timeout is reached before the process completes, the API returns an error, but the process continues to run in the background [1][2]. - Longer Tasks: For tasks expected to exceed the 60-second limit, you should avoid relying on the API-level waitForCompletion timeout [1][2]. Instead, start the process normally (without waitForCompletion) and use a separate polling or watching method, such as sandbox.process.wait, which allows for longer wait times (e.g., up to 10 minutes) [1][2]. Example usage (TypeScript): const process = await sandbox.process.exec({ name: "build-process", command: "npm run build", waitForCompletion: true, timeout: 60 // 60 seconds });
Citations:
- 1: https://docs.blaxel.ai/Sandboxes/Processes
- 2: https://blaxel.mintlify.app/Sandboxes/Processes
- 3: https://docs.blaxel.ai/Sandboxes/Log-streaming
🏁 Script executed:
sed -n '118,140p;690,715p' packages/ai-sandbox-blaxel/src/handle.tsRepository: TanStack/ai
Length of output: 1497
Use non-blocking execution for long-running commands
timeout is measured in seconds, so the reaper's timeout: 30 covers its sleep 0.1. However, exec uses waitForCompletion: true without a timeout, and Blaxel caps this API wait at 60 seconds. Use non-blocking execution with process polling for longer commands.
🧰 Tools
🪛 OpenGrep (1.26.0)
[ERROR] 547-566: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-sandbox-blaxel/src/handle.ts` around lines 547 - 566, Update the
reaper exec call in the surrounding reap method to use non-blocking execution
instead of waitForCompletion: true, then poll the returned process until it
completes while honoring the existing 30-second timeout. Preserve the current
cleanup command and working directory, and ensure the polling path handles
completion and timeout explicitly.
| const actualPort = preview.spec?.port | ||
| if (actualPort !== port) { | ||
| throw new Error( | ||
| `blaxel: existing preview ${previewName(port)} targets port=${String(actualPort)}, but this provider requested port=${port}. Delete the stale preview before reconnecting.`, | ||
| ) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Blaxel sandbox previews createIfNotExists response spec port field
💡 Result:
In the Blaxel sandbox preview specification, the port field is an integer that defines the internal port of your sandbox to which the preview URL will route traffic [1][2][3]. When using the createIfNotExists (or create_if_not_exists in Python) method for sandbox previews, the port is passed within the spec object [1][4][3]. It is a mandatory configuration that ensures external requests are correctly directed to the application or service running on that specific port inside your sandbox environment [5][3]. Example usage in TypeScript: const preview = await sandbox.previews.createIfNotExists({ metadata: { name: "app-preview" }, spec: { port: 3000, // The internal port to expose public: true } }); For applications requiring access via a preview URL, you do not need to manually expose ports at the sandbox creation time; these ports are dynamically opened by the preview configuration as needed [5].
Citations:
- 1: https://docs.blaxel.ai/Sandboxes/Preview-url
- 2: https://blaxel.mintlify.app/Sandboxes/Preview-url
- 3: https://registry.npmjs.org/%40blaxel%2Fcore
- 4: https://docs.blaxel.ai/Tutorials/Claude-Code
- 5: https://docs.blaxel.ai/Sandboxes/Ports
🏁 Script executed:
#!/bin/bash
set -eu
file=$(git ls-files 'packages/ai-sandbox-blaxel/src/handle.ts' | head -n 1)
test -n "$file"
printf '%s\n' "$file"
sed -n '940,1010p' "$file"
printf '\n-- Blaxel preview API references --\n'
rg -n --glob '!node_modules' --glob '!dist' 'createIfNotExists|previews\.get|spec\??\.port|interface.*Preview|type.*Preview' packages/ai-sandbox-blaxel packages | head -n 120
printf '\n-- Package metadata and lock references --\n'
rg -n '(`@blaxel`|blaxel)' packages/ai-sandbox-blaxel/package.json package.json pnpm-lock.yaml 2>/dev/null | head -n 100Repository: TanStack/ai
Length of output: 7777
🏁 Script executed:
#!/bin/bash
set -eu
file=packages/ai-sandbox-blaxel/src/handle.ts
printf '%s\n' '-- Local preview types and adapter contract --'
sed -n '96,170p' "$file"
printf '%s\n' '-- Handle tests around preview responses --'
sed -n '130,220p' packages/ai-sandbox-blaxel/tests/handle.test.ts
printf '%s\n' '-- Provider tests around preview responses --'
sed -n '35,95p' packages/ai-sandbox-blaxel/tests/provider.test.ts
printf '%s\n' '-- Package dependency declaration --'
sed -n '35,55p' packages/ai-sandbox-blaxel/package.jsonRepository: TanStack/ai
Length of output: 7085
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import io
import json
import re
import tarfile
import urllib.request
package = "`@blaxel/core`"
version = "0.3.10"
url = f"https://registry.npmjs.org/{package.replace('/', '%2F')}/{version}"
with urllib.request.urlopen(url, timeout=20) as response:
metadata = json.load(response)
tarball = metadata["dist"]["tarball"]
with urllib.request.urlopen(tarball, timeout=20) as response:
archive = tarfile.open(fileobj=io.BytesIO(response.read()), mode="r:gz")
matches = []
for member in archive.getmembers():
if not member.isfile() or not member.name.endswith((".d.ts", ".ts", ".js")):
continue
data = archive.extractfile(member).read().decode("utf-8", "replace")
if re.search(r"createIfNotExists|previews|Preview|spec", data, re.I):
matches.append((member.name, data))
print(f"package={package}@{version}")
print(f"files_with_preview_symbols={len(matches)}")
for name, data in matches:
lines = data.splitlines()
selected = [
f"{i + 1}: {line}"
for i, line in enumerate(lines)
if re.search(r"createIfNotExists|previews|Preview|spec", line, re.I)
]
if selected:
print(f"\n-- {name} --")
print("\n".join(selected[:160]))
PYRepository: TanStack/ai
Length of output: 2436
🏁 Script executed:
#!/bin/bash
set -eu
meta=$(mktemp)
trap 'rm -f "$meta"' EXIT
curl -fsSLk 'https://registry.npmjs.org/%40blaxel%2Fcore/0.3.10' -o "$meta"
tarball=$(jq -r '.dist.tarball' "$meta")
curl -fsSLk "$tarball" | tar -tzf - | grep -E '(^|/)(.*\.d\.ts|.*\.ts)$' | grep -Ei 'preview|sandbox|index' | head -n 120Repository: TanStack/ai
Length of output: 4077
🏁 Script executed:
#!/bin/bash
set -eu
meta=$(mktemp)
trap 'rm -f "$meta"' EXIT
curl -fsSLk 'https://registry.npmjs.org/%40blaxel%2Fcore/0.3.10' -o "$meta"
tarball=$(jq -r '.dist.tarball' "$meta")
for file in \
package/dist/cjs/types/sandbox/preview.d.ts \
package/dist/cjs/types/sandbox/types.d.ts \
package/dist/cjs/types/sandbox/client/types.gen.d.ts
do
printf '\n-- %s --\n' "$file"
curl -fsSLk "$tarball" | tar -xOf - "$file" 2>/dev/null \
| grep -n -E -C 5 'Preview|preview|port|createIfNotExists|Create.*Preview' \
| head -n 220 || true
doneRepository: TanStack/ai
Length of output: 535
🏁 Script executed:
#!/bin/bash
set -eu
meta=$(mktemp)
archive=$(mktemp)
trap 'rm -f "$meta" "$archive"' EXIT
curl -fsSLk 'https://registry.npmjs.org/%40blaxel%2Fcore/0.3.10' -o "$meta"
curl -fsSLk "$(jq -r '.dist.tarball' "$meta")" -o "$archive"
for file in \
package/dist/cjs/types/sandbox/preview.d.ts \
package/dist/cjs/types/sandbox/types.d.ts \
package/dist/cjs/types/sandbox/client/types.gen.d.ts
do
printf '\n-- %s --\n' "$file"
tar -xOf "$archive" "$file" 2>/dev/null \
| grep -n -E -C 5 'Preview|preview|port|createIfNotExists|Create.*Preview' \
| head -n 220 || true
doneRepository: TanStack/ai
Length of output: 9348
🏁 Script executed:
#!/bin/bash
set -eu
meta=$(mktemp)
archive=$(mktemp)
types=$(mktemp)
preview=$(mktemp)
trap 'rm -f "$meta" "$archive" "$types" "$preview"' EXIT
curl -fsSLk 'https://registry.npmjs.org/%40blaxel%2Fcore/0.3.10' -o "$meta"
curl -fsSLk "$(jq -r '.dist.tarball' "$meta")" -o "$archive"
tar -xOf "$archive" package/dist/cjs/types/sandbox/client/types.gen.d.ts > "$types"
tar -xOf "$archive" package/dist/cjs/types/sandbox/preview.d.ts > "$preview"
printf '%s\n' '-- PreviewSpec and Preview declarations --'
grep -n -E -A18 -B4 '^export type (Preview|PreviewSpec|PreviewMetadata|PreviewToken)' "$types" || true
printf '%s\n' '-- Preview API implementation declarations --'
cat "$preview"Repository: TanStack/ai
Length of output: 1598
🏁 Script executed:
#!/bin/bash
set -eu
meta=$(mktemp)
archive=$(mktemp)
dir=$(mktemp -d)
trap 'rm -f "$meta" "$archive"; rm -rf "$dir"' EXIT
curl -fsSLk 'https://registry.npmjs.org/%40blaxel%2Fcore/0.3.10' -o "$meta"
curl -fsSLk "$(jq -r '.dist.tarball' "$meta")" -o "$archive"
tar -xzf "$archive" -C "$dir"
printf '%s\n' '-- Preview model declarations --'
rg -n -A20 -B4 'PreviewSpec|export type Preview\b|interface Preview\b|type Preview =' "$dir/package/dist/cjs/types" | head -n 260
printf '%s\n' '-- Preview runtime implementation --'
rg -n -A18 -B8 'createIfNotExists|class SandboxPreview|new SandboxPreview' "$dir/package/dist/cjs" --glob '*.js' | head -n 260Repository: TanStack/ai
Length of output: 33567
Do not fail connectPort when the preview response omits port.
PreviewSpec.port is optional, and createIfNotExists can return an existing preview without echoing the requested port. Compare ports only when the response includes one.
🛡️ Proposed guard
const actualPort = preview.spec?.port
- if (actualPort !== port) {
+ if (actualPort !== undefined && actualPort !== port) {
throw new Error(
`blaxel: existing preview ${previewName(port)} targets port=${String(actualPort)}, but this provider requested port=${port}. Delete the stale preview before reconnecting.`,
)📝 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.
| const actualPort = preview.spec?.port | |
| if (actualPort !== port) { | |
| throw new Error( | |
| `blaxel: existing preview ${previewName(port)} targets port=${String(actualPort)}, but this provider requested port=${port}. Delete the stale preview before reconnecting.`, | |
| ) | |
| } | |
| const actualPort = preview.spec?.port | |
| if (actualPort !== undefined && actualPort !== port) { | |
| throw new Error( | |
| `blaxel: existing preview ${previewName(port)} targets port=${String(actualPort)}, but this provider requested port=${port}. Delete the stale preview before reconnecting.`, | |
| ) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-sandbox-blaxel/src/handle.ts` around lines 972 - 977, Update the
port validation in connectPort around actualPort and the existing preview check
so it throws only when preview.spec.port is defined and differs from the
requested port. Allow omitted port values to proceed without changing the
mismatch error for explicitly returned ports.
| let pids: Array<number> = [] | ||
| try { | ||
| const pidsPath = `${outputDir!}/pids` | ||
| await vi.waitFor(() => expect(existsSync(pidsPath)).toBe(true)) | ||
| pids = readFileSync(pidsPath, 'utf8').trim().split(/\s+/).map(Number) | ||
| child.kill('SIGTERM') | ||
| await exited | ||
| await vi.waitFor(() => { | ||
| for (const pid of pids) { | ||
| expect(() => process.kill(-pid, 0)).toThrow() | ||
| } | ||
| }) | ||
| } finally { | ||
| child.kill('SIGKILL') | ||
| for (const pid of pids) { | ||
| try { | ||
| process.kill(-pid, 'SIGKILL') | ||
| } catch { | ||
| // The process group already exited. | ||
| } | ||
| } | ||
| await exited.catch(() => undefined) | ||
| rmSync(outputDir!, { recursive: true, force: true }) | ||
| resolveWait({ exitCode: 0, stdout: '', stderr: '' }) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Validate the parsed PIDs before you signal a process group.
vi.waitFor only checks that pids exists. The supervisor creates that file with printf ... > pidsFile, so the file can exist while it is still empty. An empty read produces [''].map(Number), that is [0].
With pids = [0], process.kill(-0, 0) in the try block does not throw, so the vi.waitFor assertion times out. The finally block then calls process.kill(-0, 'SIGKILL'), which signals the current process group and kills the Vitest runner.
Parse and validate the PIDs inside waitFor, and require all three.
🐛 Proposed fix
let pids: Array<number> = []
try {
const pidsPath = `${outputDir!}/pids`
- await vi.waitFor(() => expect(existsSync(pidsPath)).toBe(true))
- pids = readFileSync(pidsPath, 'utf8').trim().split(/\s+/).map(Number)
+ await vi.waitFor(() => {
+ expect(existsSync(pidsPath)).toBe(true)
+ const parsed = readFileSync(pidsPath, 'utf8')
+ .trim()
+ .split(/\s+/)
+ .map(Number)
+ .filter((pid) => Number.isInteger(pid) && pid > 1)
+ expect(parsed).toHaveLength(3)
+ pids = parsed
+ })
child.kill('SIGTERM')📝 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.
| let pids: Array<number> = [] | |
| try { | |
| const pidsPath = `${outputDir!}/pids` | |
| await vi.waitFor(() => expect(existsSync(pidsPath)).toBe(true)) | |
| pids = readFileSync(pidsPath, 'utf8').trim().split(/\s+/).map(Number) | |
| child.kill('SIGTERM') | |
| await exited | |
| await vi.waitFor(() => { | |
| for (const pid of pids) { | |
| expect(() => process.kill(-pid, 0)).toThrow() | |
| } | |
| }) | |
| } finally { | |
| child.kill('SIGKILL') | |
| for (const pid of pids) { | |
| try { | |
| process.kill(-pid, 'SIGKILL') | |
| } catch { | |
| // The process group already exited. | |
| } | |
| } | |
| await exited.catch(() => undefined) | |
| rmSync(outputDir!, { recursive: true, force: true }) | |
| resolveWait({ exitCode: 0, stdout: '', stderr: '' }) | |
| } | |
| let pids: Array<number> = [] | |
| try { | |
| const pidsPath = `${outputDir!}/pids` | |
| await vi.waitFor(() => { | |
| expect(existsSync(pidsPath)).toBe(true) | |
| const parsed = readFileSync(pidsPath, 'utf8') | |
| .trim() | |
| .split(/\s+/) | |
| .map(Number) | |
| .filter((pid) => Number.isInteger(pid) && pid > 1) | |
| expect(parsed).toHaveLength(3) | |
| pids = parsed | |
| }) | |
| child.kill('SIGTERM') | |
| await exited | |
| await vi.waitFor(() => { | |
| for (const pid of pids) { | |
| expect(() => process.kill(-pid, 0)).toThrow() | |
| } | |
| }) | |
| } finally { | |
| child.kill('SIGKILL') | |
| for (const pid of pids) { | |
| try { | |
| process.kill(-pid, 'SIGKILL') | |
| } catch { | |
| // The process group already exited. | |
| } | |
| } | |
| await exited.catch(() => undefined) | |
| rmSync(outputDir!, { recursive: true, force: true }) | |
| resolveWait({ exitCode: 0, stdout: '', stderr: '' }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-sandbox-blaxel/tests/handle.test.ts` around lines 722 - 746,
Update the PID-reading logic in the test’s vi.waitFor block to parse the file
and require exactly three valid, nonzero PIDs before assigning them to pids or
signaling any process groups. Keep waiting while the file is empty, incomplete,
or contains invalid values, and ensure the cleanup loop in finally only receives
validated PIDs.
|
Thanks for the PR, @SystemSculpt! 🙌 @tombeckenham will take a look. Automated pre-review checks
Automated triage — a human review follows. |
🎯 Changes
@tanstack/ai-sandbox-blaxelas a managed cloud sandbox provider.✅ Checklist
pnpm run test:pr.🚀 Release Impact
Safety and scope
killableProcessesfalse until child process-group termination has direct proof.Test plan
pnpm test:pracross 74 projectsexec, andspawntanstack-ai-*sandbox remained after the credentialed suiteorigin/maindurability failureBrowser baseline note
The only browser failure is
durable runs - takeover - a real disconnect, then an attach, continues the stream. It expectsdetachedSinceto be cleared after completion.The same assertion fails on every retry in a clean detached
origin/mainworktree after all 53 upstream packages are rebuilt. This PR does not change the test, route, persistence code, sandbox core, or durability code. That fix should stay outside this provider PR.Review notes
@blaxel/coreuses^0.3.10, the current stable release at validation time.Summary by CodeRabbit
New Features
Documentation