Skip to content

feat(ai-sandbox): add Blaxel provider - #1065

Open
SystemSculpt wants to merge 3 commits into
TanStack:mainfrom
SystemSculpt:ready/ai-sandbox-blaxel
Open

feat(ai-sandbox): add Blaxel provider#1065
SystemSculpt wants to merge 3 commits into
TanStack:mainfrom
SystemSculpt:ready/ai-sandbox-blaxel

Conversation

@SystemSculpt

@SystemSculpt SystemSculpt commented Aug 7, 2026

Copy link
Copy Markdown

🎯 Changes

  • Add @tanstack/ai-sandbox-blaxel as a managed cloud sandbox provider.
  • Implement filesystem access, native file watch, environment values, bounded process streaming, token-gated previews, and resume by id.
  • Reconcile ambiguous remote creates and process starts so cancellation cannot strand billed sandboxes or persistent processes, even when the SDK promise never settles.
  • Document setup, runtime requirements, capability limits, and cleanup behavior.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Safety and scope

  • Give each sandbox a one-hour TTL by default.
  • Gate preview URLs with a token by default.
  • Bound stdout and stderr to 8 MiB per stream, with remote process cleanup on overflow or cancellation.
  • Delete only the sandbox create attempt that owns the unique attempt label.
  • Keep killableProcesses false until child process-group termination has direct proof.
  • Keep snapshot, restore, and fork support disabled until Blaxel exposes reconstruct-after-delete semantics.
  • Reject conflicting process-global Blaxel credentials to prevent cross-workspace requests.
  • Keep all provider-specific behavior inside the new package. This PR makes no core sandbox contract changes.

Test plan

  • Complete uncached pnpm test:pr across 74 projects
  • Declaration scan: 780 files clean across 54 package build directories
  • Blaxel package: TypeScript, type-aware lint, Vite build, and strict publint
  • Credentialed Blaxel suite: 91 passed, 3 documented capability skips
  • Never-settling abort coverage for sandbox creation, exec, and spawn
  • Live create, exec, failure exit, filesystem, stream, preview auth, HTTP 401 denial, resume, destroy, and journal proof
  • No test-owned tanstack-ai-* sandbox remained after the credentialed suite
  • Live Terminal playground opened from the final worktree with a private one-hour sandbox
  • Full browser E2E: 444 passed, 1 skipped, 1 existing origin/main durability failure

Browser baseline note

The only browser failure is durable runs - takeover - a real disconnect, then an attach, continues the stream. It expects detachedSince to be cleared after completion.

The same assertion fails on every retry in a clean detached origin/main worktree 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/core uses ^0.3.10, the current stable release at validation time.
  • Node 22.22.3 and GNU Coreutils were used for the complete local gate.
  • The effective diff is 17 files: one provider package, its tests, docs, one changeset, the provider list, and the lockfile.

Summary by CodeRabbit

  • New Features

    • Added Blaxel as a supported cloud sandbox provider.
    • Supports filesystem operations, Git commands, environment variables, process execution and streaming output.
    • Added sandbox resume, configurable images, regions, memory, working directories, and one-hour default TTLs.
    • Added token-protected preview URLs with configurable expiration.
    • Snapshot, fork, and restore remain unavailable.
  • Documentation

    • Added installation, configuration, authentication, capability, and usage guidance for Blaxel sandboxes.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request adds the @tanstack/ai-sandbox-blaxel package. It implements Blaxel sandbox lifecycle, filesystem, process, preview, environment, and watch operations. It adds tests, package configuration, release metadata, and documentation.

Changes

Blaxel sandbox provider

Layer / File(s) Summary
Provider lifecycle and package wiring
packages/ai-sandbox-blaxel/package.json, packages/ai-sandbox-blaxel/src/provider.ts, packages/ai-sandbox-blaxel/src/utils.ts, packages/ai-sandbox-blaxel/src/index.ts, packages/ai-sandbox-blaxel/tsconfig.json, packages/ai-sandbox-blaxel/vite.config.ts, packages/ai-sandbox-blaxel/tests/provider.test.ts
Adds provider configuration, credential validation, sandbox creation, preparation, cleanup reconciliation, resume, destruction, exports, and lifecycle tests.
Handle operations and process supervision
packages/ai-sandbox-blaxel/src/handle.ts, packages/ai-sandbox-blaxel/tests/handle.test.ts
Adds filesystem, Git, watch, environment, command, process streaming, preview, cancellation, bounded output, cleanup, and destruction behavior with unit coverage.
Integration and conformance validation
packages/ai-sandbox-blaxel/tests/blaxel.test.ts, packages/ai-sandbox-blaxel/tests/journal.conformance.test.ts
Adds credential-gated integration coverage for sandbox lifecycle, filesystem operations, process output, previews, resume, and conformance registration.
Documentation and release metadata
.changeset/add-ai-sandbox-blaxel.md, packages/ai-sandbox-blaxel/CHANGELOG.md, docs/sandbox/providers.md, packages/ai-sandbox/README.md
Documents Blaxel capabilities, configuration, limitations, previews, process behavior, installation, and the package minor release.

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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the addition of the Blaxel sandbox provider.
Description check ✅ Passed The description covers the required changes, checklist, release impact, testing, scope, and known browser test limitation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

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

@socket-security

socket-security Bot commented Aug 7, 2026

Copy link
Copy Markdown

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.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm js-yaml is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/@blaxel/core@0.3.10npm/js-yaml@4.2.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/js-yaml@4.2.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Deprecated by its maintainer: npm @hey-api/client-fetch

Reason: Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts.

From: pnpm-lock.yamlnpm/@blaxel/core@0.3.10npm/@hey-api/client-fetch@0.10.2

ℹ Read more on: This package | This alert | What is a deprecated package?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Research the state of the package and determine if there are non-deprecated versions that can be used, or if it should be replaced with a new, supported solution.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@hey-api/client-fetch@0.10.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
packages/ai-sandbox-blaxel/tests/blaxel.test.ts (1)

1-3: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Place 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 under packages/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 under packages/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 win

Export BlaxelSandboxLike with BlaxelHandleDeps.

BlaxelHandleDeps.sandbox has type BlaxelSandboxLike, but that type is not re-exported. A consumer who constructs BlaxelHandle directly 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 value

Escape \n consistently 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 \ and n. The resulting printf still 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 \\n in 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 win

Create the rejected deleteGate lazily to avoid an unhandled rejection.

deleteGate is assigned an already-rejected promise, but no handler attaches until SandboxInstance.delete runs, which happens after several awaited steps. Node can report unhandledRejection in that window and make the test flaky. Make the fake produce the rejection when delete is 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 rejected createGate values 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 win

Make the live child-process cleanup failure-safe.

rmSync(outputDir!) and the child termination run only on the success path. If vi.waitFor at Line 707 or Line 718 fails, the test leaves a real /tmp/tanstack-ai-output-* directory and possibly live sleep 30 process groups on the machine. Move the cleanup into a try/finally.

The test also depends on bash being installed, because the generated wrapper ends with exec bash <supervisor>. On a runner without bash, the pids file never appears and the failure mode is an opaque waitFor timeout.

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

Reconciliation can delay the rejection by about 29 seconds and ignores input.signal.

findOwnedSandbox polls up to CREATE_RECONCILE_ATTEMPTS (30) times with a 1000 ms sleep. The mayHaveCreatedSandbox branch at Line 259 awaits that reconciliation before create() rejects. A caller that receives a 504 therefore waits about 29 seconds, and an abort raised during that window has no effect because input.signal is not passed into findOwnedSandbox.

Consider passing input.signal into 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7d92296 and e58d7bf.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • .changeset/add-ai-sandbox-blaxel.md
  • docs/config.json
  • docs/sandbox/providers.md
  • packages/ai-sandbox-blaxel/CHANGELOG.md
  • packages/ai-sandbox-blaxel/package.json
  • packages/ai-sandbox-blaxel/src/handle.ts
  • packages/ai-sandbox-blaxel/src/index.ts
  • packages/ai-sandbox-blaxel/src/provider.ts
  • packages/ai-sandbox-blaxel/src/utils.ts
  • packages/ai-sandbox-blaxel/tests/blaxel.test.ts
  • packages/ai-sandbox-blaxel/tests/handle.test.ts
  • packages/ai-sandbox-blaxel/tests/journal.conformance.test.ts
  • packages/ai-sandbox-blaxel/tests/provider.test.ts
  • packages/ai-sandbox-blaxel/tsconfig.json
  • packages/ai-sandbox-blaxel/vite.config.ts
  • packages/ai-sandbox/README.md

Comment thread packages/ai-sandbox-blaxel/src/handle.ts
Comment thread packages/ai-sandbox-blaxel/tests/blaxel.test.ts Outdated
Comment thread packages/ai-sandbox/README.md
@SystemSculpt

Copy link
Copy Markdown
Author

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 tests/ because that matches every other sandbox provider. I also kept ambiguous-create cleanup blocking and independent of caller aborts. Making that cleanup detached or abortable can leave a paid sandbox behind, especially with ttl: null.

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.

@tombeckenham
tombeckenham force-pushed the ready/ai-sandbox-blaxel branch from 6aab0b1 to 502fa6c Compare August 10, 2026 09:06
@tombeckenham

Copy link
Copy Markdown
Contributor

Maintainer sweep: rebased onto main (force-with-lease). CI approval handled if it was waiting.

@nx-cloud

nx-cloud Bot commented Aug 10, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 502fa6c

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 57s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-10 09:09:53 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@1065

@tanstack/ai-acp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-acp@1065

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@1065

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@1065

@tanstack/ai-bedrock

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-bedrock@1065

@tanstack/ai-byteplus

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-byteplus@1065

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@1065

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@1065

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@1065

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-skills@1065

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@1065

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@1065

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-durable-stream@1065

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@1065

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@1065

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@1065

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@1065

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@1065

@tanstack/ai-grok-build

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok-build@1065

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@1065

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@1065

@tanstack/ai-isolate-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-daytona@1065

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@1065

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@1065

@tanstack/ai-isolate-quickjs-bun

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs-bun@1065

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@1065

@tanstack/ai-memory

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-memory@1065

@tanstack/ai-mistral

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mistral@1065

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@1065

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@1065

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@1065

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@1065

@tanstack/ai-persistence

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence@1065

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@1065

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@1065

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@1065

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@1065

@tanstack/ai-sandbox-blaxel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-blaxel@1065

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@1065

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-daytona@1065

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@1065

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@1065

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-sprites@1065

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-vercel@1065

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@1065

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@1065

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@1065

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@1065

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@1065

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@1065

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@1065

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@1065

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@1065

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@1065

commit: 502fa6c

@SystemSculpt
SystemSculpt force-pushed the ready/ai-sandbox-blaxel branch from 502fa6c to 5e58afc Compare August 12, 2026 18:41
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

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.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​blaxel/​core@​0.3.106610010099100

View full report

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 053333d and 5e58afc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • .changeset/add-ai-sandbox-blaxel.md
  • docs/sandbox/providers.md
  • packages/ai-sandbox-blaxel/CHANGELOG.md
  • packages/ai-sandbox-blaxel/package.json
  • packages/ai-sandbox-blaxel/src/handle.ts
  • packages/ai-sandbox-blaxel/src/index.ts
  • packages/ai-sandbox-blaxel/src/provider.ts
  • packages/ai-sandbox-blaxel/src/utils.ts
  • packages/ai-sandbox-blaxel/tests/blaxel.test.ts
  • packages/ai-sandbox-blaxel/tests/handle.test.ts
  • packages/ai-sandbox-blaxel/tests/journal.conformance.test.ts
  • packages/ai-sandbox-blaxel/tests/provider.test.ts
  • packages/ai-sandbox-blaxel/tsconfig.json
  • packages/ai-sandbox-blaxel/vite.config.ts
  • packages/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

Comment on lines +547 to +566
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,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 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:


🏁 Script executed:

rg -n --glob '!*node_modules*' 'BlaxelProcessRequestLike|waitForCompletion|keepAlive|timeout' packages/ai-sandbox-blaxel packages | head -200

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


🏁 Script executed:

sed -n '118,140p;690,715p' packages/ai-sandbox-blaxel/src/handle.ts

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

Comment on lines +972 to +977
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.`,
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 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:


🏁 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 100

Repository: 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.json

Repository: 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]))
PY

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

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

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

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

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

Suggested change
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.

Comment on lines +722 to +746
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: '' })
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Suggested change
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.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the PR, @SystemSculpt! 🙌 @tombeckenham will take a look.

Automated pre-review checks

  • ✅ CI passing
  • ✅ No merge conflicts
  • ✅ Changeset present
  • ⚠️ No E2E test changes detected — behavior changes need coverage under testing/e2e/ (see CONTRIBUTING)

Automated triage — a human review follows.

@github-actions github-actions Bot added the waiting-on: maintainer The ball is in the maintainers’ court label Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on: maintainer The ball is in the maintainers’ court

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants