Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Claude Code Review

on:
pull_request_target:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
Expand All @@ -13,12 +13,9 @@ on:
jobs:
claude-review:
# Skip review for automated "Version Packages" PRs created by changesets
# For external PRs: requires manual approval via 'external-pr' environment
# For internal PRs: runs automatically without approval
if: github.event.pull_request.title != 'Version Packages'

runs-on: ubuntu-latest
environment: ${{ github.event.pull_request.head.repo.full_name != github.repository && 'external-pr' || null }}
permissions:
contents: read
pull-requests: read
Expand All @@ -29,7 +26,6 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 1

- name: Run Claude Code Review
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/pkg-pr-new.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ permissions:
pull-requests: write # Required for pkg.pr.new to comment on PRs

on:
pull_request_target:
pull_request:
types: [opened, synchronize, reopened]
paths:
- '**'
Expand All @@ -14,17 +14,13 @@ on:

jobs:
publish-preview:
# For external PRs: requires manual approval via 'external-pr' environment
# For internal PRs: runs automatically without approval
runs-on: ubuntu-latest
timeout-minutes: 15
environment: ${{ github.event.pull_request.head.repo.full_name != github.repository && 'external-pr' || null }}

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

- name: Setup Node.js
Expand Down
14 changes: 3 additions & 11 deletions .github/workflows/pullrequest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ permissions:
contents: read

on:
pull_request_target:
types: [opened, synchronize, reopened]
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand All @@ -20,8 +19,6 @@ jobs:
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}

- uses: actions/setup-node@v4
with:
Expand Down Expand Up @@ -74,17 +71,12 @@ jobs:
run: npm run test -w @repo/sandbox-container

# E2E tests against deployed worker
# For external PRs: requires manual approval via 'external-pr' environment
# For internal PRs: runs automatically without approval
e2e-tests:
needs: unit-tests
timeout-minutes: 30
runs-on: ubuntu-latest
environment: ${{ github.event.pull_request.head.repo.full_name != github.repository && 'external-pr' || null }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}

- uses: actions/setup-node@v4
with:
Expand All @@ -105,7 +97,7 @@ jobs:
- name: Set environment name
id: env-name
run: |
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "env_name=pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
echo "worker_name=sandbox-e2e-test-worker-pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
else
Expand Down Expand Up @@ -163,7 +155,7 @@ jobs:

# Cleanup: Delete test worker and container (only for PR environments)
- name: Cleanup test deployment
if: always() && github.event_name == 'pull_request_target'
if: always() && github.event_name == 'pull_request'
continue-on-error: true
run: |
cd tests/e2e/test-worker
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ different users share the same sandbox instance.
The container runtime uses Ubuntu 22.04 with:

- Python 3.11 (with matplotlib, numpy, pandas, ipython)
- Node.js 20 LTS
- Node.js 24 LTS
- Bun 1.x runtime (powers the container HTTP server)
- Git, curl, wget, jq, and other common utilities

Expand Down
20 changes: 10 additions & 10 deletions docs/SESSION_EXECUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,20 @@ This document explains how the container session executes commands reliably whil
### Foreground (`exec`)

- Runs in the main bash shell so state persists across commands.
- Uses bash process substitution to prefix stdout/stderr inline and append to the per-command log file.
- After the command returns, we `wait` to ensure process-substitution consumers finish writing to the log before we write the exit code file.
- Why process substitution (not FIFOs):
- Foreground previously used FIFOs + background labelers, which can race on silent commands because FIFO open/close ordering depends on cross-process scheduling.
- Process substitution keeps execution local to the main shell and avoids FIFO semantics entirely.
- Writes stdout/stderr to temporary files, then prefixes and merges them into the log.
- Bash waits for file redirects to complete before continuing, ensuring the log is fully written before the exit code is published.
- This avoids race conditions from process substitution buffering where log reads could happen before writes complete.

Pseudo:

```
# Foreground
{ command; } \
> >(while read; printf "\x01\x01\x01%s\n" "$REPLY" >> "$log") \
2> >(while read; printf "\x02\x02\x02%s\n" "$REPLY" >> "$log")
{ command; } > "$log.stdout" 2> "$log.stderr"
EXIT_CODE=$?
# Ensure consumers have drained
wait 2>/dev/null
# Prefix and merge into main log
(while read line; do printf "\x01\x01\x01%s\n" "$line"; done < "$log.stdout") >> "$log"
(while read line; do printf "\x02\x02\x02%s\n" "$line"; done < "$log.stderr") >> "$log"
rm -f "$log.stdout" "$log.stderr"
# Atomically publish exit code
echo "$EXIT_CODE" > "$exit.tmp" && mv "$exit.tmp" "$exit"
```
Expand Down Expand Up @@ -97,3 +95,5 @@ mkfifo "$sp" "$ep"
- Why not tee? Tee doesn’t split stdout/stderr into separate channels with stable ordering without extra plumbing; our prefixes are simple and explicit.
- Is process substitution portable?
- It is supported by bash (we spawn bash with `--norc`). The container environment supports it; if portability constraints change, we can revisit.
- Why use temp files instead of process substitution for foreground?
Process substitutions run asynchronously - bash returns when the substitution processes close, but their writes to the log file may still be buffered. With large output (e.g., base64-encoded files), the log file can be incomplete when we try to read it. Using direct file redirects ensures bash waits for all writes to complete before continuing, eliminating this race condition.
4 changes: 2 additions & 2 deletions examples/claude-code/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM docker.io/cloudflare/sandbox:0.4.14
FROM docker.io/cloudflare/sandbox:0.4.15
RUN npm install -g @anthropic-ai/claude-code
ENV COMMAND_TIMEOUT_MS=300000
EXPOSE 3000

# On a Mac with Apple Silicon, you might need to specify the platform:
# FROM --platform=linux/arm64 docker.io/cloudflare/sandbox:0.4.14
# FROM --platform=linux/arm64 docker.io/cloudflare/sandbox:0.4.15
6 changes: 3 additions & 3 deletions examples/code-interpreter/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# This image is unique to this repo, and you'll never need it.
# Whenever you're integrating with sandbox SDK in your own project,
# you should use the official image instead:
# FROM docker.io/cloudflare/sandbox:0.4.14
FROM cloudflare/sandbox-test:0.4.14
# FROM docker.io/cloudflare/sandbox:0.4.15
FROM cloudflare/sandbox-test:0.4.15

# On a mac, you might need to actively pick up the
# arm64 build of the image.
# FROM --platform=linux/arm64 cloudflare/sandbox-test:0.4.14
# FROM --platform=linux/arm64 cloudflare/sandbox-test:0.4.15
4 changes: 2 additions & 2 deletions examples/minimal/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM docker.io/cloudflare/sandbox:0.4.14
FROM docker.io/cloudflare/sandbox:0.4.15

# On a Mac with Apple Silicon, you might need to specify the platform:
# FROM --platform=linux/arm64 docker.io/cloudflare/sandbox:0.4.14
# FROM --platform=linux/arm64 docker.io/cloudflare/sandbox:0.4.15

# Required during local development to access exposed ports
EXPOSE 8080
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 12 additions & 9 deletions packages/sandbox-container/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,17 +722,17 @@ export class Session {
// FOREGROUND PATTERN (for exec)
// Command runs in main shell, state persists!

// FOREGROUND: Avoid FIFOs to eliminate race conditions.
// Use bash process substitution to prefix stdout/stderr while keeping
// execution in the main shell so session state persists across commands.
// FOREGROUND: Write stdout/stderr to temp files, then prefix and merge.
// This ensures bash waits for all writes to complete before continuing,
// avoiding race conditions when reading the log file.

if (cwd) {
const safeCwd = this.escapeShellPath(cwd);
script += ` # Save and change directory\n`;
script += ` PREV_DIR=$(pwd)\n`;
script += ` if cd ${safeCwd}; then\n`;
script += ` # Execute command with prefixed streaming via process substitution\n`;
script += ` { ${command}; } < /dev/null > >(while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x01\\x01\\x01%s\\n' "$line"; done >> "$log") 2> >(while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x02\\x02\\x02%s\\n' "$line"; done >> "$log")\n`;
script += ` # Execute command, redirect to temp files\n`;
script += ` { ${command}; } < /dev/null > "$log.stdout" 2> "$log.stderr"\n`;
script += ` EXIT_CODE=$?\n`;
script += ` # Restore directory\n`;
script += ` cd "$PREV_DIR"\n`;
Expand All @@ -741,13 +741,16 @@ export class Session {
script += ` EXIT_CODE=1\n`;
script += ` fi\n`;
} else {
script += ` # Execute command with prefixed streaming via process substitution\n`;
script += ` { ${command}; } < /dev/null > >(while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x01\\x01\\x01%s\\n' "$line"; done >> "$log") 2> >(while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x02\\x02\\x02%s\\n' "$line"; done >> "$log")\n`;
script += ` # Execute command, redirect to temp files\n`;
script += ` { ${command}; } < /dev/null > "$log.stdout" 2> "$log.stderr"\n`;
script += ` EXIT_CODE=$?\n`;
}

// Ensure process-substitution consumers complete before writing exit code
script += ` wait 2>/dev/null\n`;
script += ` \n`;
script += ` # Prefix and merge stdout/stderr into main log\n`;
script += ` (while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x01\\x01\\x01%s\\n' "$line"; done < "$log.stdout" >> "$log") 2>/dev/null\n`;
script += ` (while IFS= read -r line || [[ -n "$line" ]]; do printf '\\x02\\x02\\x02%s\\n' "$line"; done < "$log.stderr" >> "$log") 2>/dev/null\n`;
script += ` rm -f "$log.stdout" "$log.stderr"\n`;
script += ` \n`;
script += ` # Write exit code\n`;
script += ` echo "$EXIT_CODE" > ${safeExitCodeFile}.tmp\n`;
Expand Down
8 changes: 8 additions & 0 deletions packages/sandbox/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @cloudflare/sandbox

## 0.4.15

### Patch Changes

- [#185](https://github.com/cloudflare/sandbox-sdk/pull/185) [`7897cdd`](https://github.com/cloudflare/sandbox-sdk/commit/7897cddefc366bbd640ea138b34a520a0b2ddf8c) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Fix foreground commands blocking on background processes

- [#183](https://github.com/cloudflare/sandbox-sdk/pull/183) [`ff2fa91`](https://github.com/cloudflare/sandbox-sdk/commit/ff2fa91479357ef88cfb22418f88acb257462faa) Thanks [@whoiskatrin](https://github.com/whoiskatrin)! - update python to 3.11.14

## 0.4.14

### Patch Changes
Expand Down
Loading