Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
62032da
Establish runtime operation admission
ghostwriternr Jul 20, 2026
0ebe1e5
Fence preview and tunnel operations
ghostwriternr Jul 20, 2026
cfc3bc3
Fence backup operations to one runtime
ghostwriternr Jul 20, 2026
80adf03
Thread runtime leases through mounts
ghostwriternr Jul 20, 2026
a01af9c
Scope extension workflows to runtimes
ghostwriternr Jul 20, 2026
32733f5
Remove implicit runtime ownership
ghostwriternr Jul 20, 2026
b75479c
Add runtime lifecycle regressions
ghostwriternr Jul 21, 2026
36c95b2
Test control server exit recovery
ghostwriternr Jul 21, 2026
5f7b35a
Fix runtime execution regressions
ghostwriternr Jul 21, 2026
8ba9b8c
Upgrade container lifecycle handling
ghostwriternr Jul 21, 2026
2c7c0bc
Stabilize sandbox proxy transports
ghostwriternr Jul 22, 2026
38a9ed6
Stabilize lifecycle regression workflows
ghostwriternr Jul 22, 2026
5bddd50
Add Sandbox reconstruction harness
ghostwriternr Jul 22, 2026
1daff9a
Reconcile delayed runtime stops
ghostwriternr Jul 22, 2026
4f60cc7
Wire pending stop reconciliation
ghostwriternr Jul 22, 2026
3e3f56f
Accept both terminal container stop statuses
ghostwriternr Jul 22, 2026
3377877
Bump E2E deploy hash for CI rerun
ghostwriternr Jul 22, 2026
6e3dc63
Bump E2E deploy hash again
ghostwriternr Jul 22, 2026
8d63ecf
Add isolated browser test containers
ghostwriternr Jul 22, 2026
fa5c585
Parallelize isolated browser tests
ghostwriternr Jul 22, 2026
3722576
Integrate browser container readiness
ghostwriternr Jul 22, 2026
422dc78
Fix browser test import order
ghostwriternr Jul 22, 2026
5a424ef
Bound browser test concurrency
ghostwriternr Jul 22, 2026
ead2fad
Bootstrap Wrangler in rollout checks
ghostwriternr Jul 22, 2026
c9c2fcb
Reduce browser cold-start pressure
ghostwriternr Jul 22, 2026
12b7d49
Limit browser cold starts to one
ghostwriternr Jul 22, 2026
052b89a
Avoid orphaned browser configuration
ghostwriternr Jul 22, 2026
7c28964
Restore parallel browser coverage
ghostwriternr Jul 22, 2026
3e1fbef
Clarify release notes for callers
ghostwriternr Jul 22, 2026
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
20 changes: 19 additions & 1 deletion .changeset/add-sandbox-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,22 @@
'@cloudflare/sandbox': minor
---

Add the experimental `@cloudflare/sandbox/extensions` framework for attaching opt-in SDK extensions and lazily started container sidecars to a Sandbox subclass. Sidecars are distributed as npm-style `.tgz` packages: the SDK ships the bytes, the container provisions by content hash, derives identity from the embedded `package.json`, and `bun add`s the package. Host ↔ sidecar IPC runs over capnweb on a unix socket, so sidecar methods are a typed remote stub via `await this.sidecar<T>()` — streaming is just a typed callback parameter. Sidecar authors get a `@cloudflare/sandbox/sidecar` helper (`SandboxSidecar` + `serveSandboxSidecar`). npm distribution of third-party extensions is not yet wired up; the wire shape is the one a future authoring story will use.
Add the experimental `@cloudflare/sandbox/extensions` helpers for optional higher-level APIs on a Sandbox subclass. An extension can ship its own helper program as a `.tgz` sidecar that starts on first use, so features like the code interpreter stay out of the core SDK.

Attach a shipped extension and call it directly:

```ts
import { Sandbox as BaseSandbox } from '@cloudflare/sandbox';
import { withInterpreter } from '@cloudflare/sandbox/interpreter';

export class Sandbox extends BaseSandbox<Env> {
interpreter = withInterpreter(this);
}

const context = await sandbox.interpreter.createCodeContext({
language: 'python'
});
const result = await sandbox.interpreter.runCode('print("hello")', { context });
```

To write your own, extend `SandboxExtension` and export a `withYourExtension(sandbox)` helper. Sidecar-backed extensions call their methods through `this.withSidecar(...)`. This is experimental; publishing third-party extensions on npm is not set up yet.
5 changes: 5 additions & 0 deletions .changeset/establish-runtime-incarnations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cloudflare/sandbox': minor
---

Keep each sandbox operation on the container that started it. If that container is replaced after sleep, eviction, or a crash, in-flight work and old process or terminal handles fail instead of continuing against the new container. Create or look up handles again after the new container is running. Extension authors should run runtime work inside `withRuntime()` and `withSidecar()`.
5 changes: 5 additions & 0 deletions .changeset/fix-execution-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cloudflare/sandbox': patch
---

Apply sandbox environment variables and the default `/workspace` directory on every process launch again. Stopping a warm-pool sandbox now finishes container teardown before the slot is reused.
5 changes: 5 additions & 0 deletions .changeset/stabilize-sandbox-proxies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cloudflare/sandbox': patch
---

Keep filesystem watch and terminal output streams open across Worker to Durable Object calls. HTTPS preview and proxy requests now reach the container over HTTP as expected. Sandbox setup finishes before the first forwarded request runs, repeated restores reapply the chosen backup, and idle expiry stops the container cleanly.
24 changes: 20 additions & 4 deletions .github/scripts/wait-for-container-rollout.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ expected_image() {
local worker=$1 image_tag=$2 app_name=$3 image=sandbox
case "$app_name" in
"$worker") ;;
"$worker-browser") ;;
"$worker-python") image=sandbox-python ;;
"$worker-opencode") image=sandbox-opencode ;;
"$worker-standalone") image=sandbox-standalone ;;
Expand Down Expand Up @@ -66,18 +67,33 @@ if [[ ${1:-} == --evaluate ]]; then
exit 0
fi

if [[ ${1:-} == --expected-image ]]; then
expected_image "$2" "$3" "$4"
exit 0
fi

worker=${1:?worker name required}
image_tag=${2:?image tag required}
timeout_seconds=${ROLLOUT_TIMEOUT_SECONDS:-600}
drain_grace_seconds=${ROLLOUT_DRAIN_GRACE_SECONDS:-180}
poll_seconds=${ROLLOUT_POLL_SECONDS:-10}
deadline=$((SECONDS + timeout_seconds))
drain_deadline=$((SECONDS + drain_grace_seconds))
app_names=("$worker" "$worker-python" "$worker-opencode" "$worker-standalone" "$worker-musl")
app_names=("$worker" "$worker-browser" "$worker-python" "$worker-opencode" "$worker-standalone" "$worker-musl")

if command -v wrangler >/dev/null 2>&1; then
wrangler_command=(wrangler)
elif command -v npx >/dev/null 2>&1; then
echo 'Wrangler is not installed globally; running it through npx'
wrangler_command=(npx --yes wrangler@latest)
else
echo '::error::Neither wrangler nor npx is available' >&2
exit 1
fi

echo "Waiting for container applications to serve image tag $image_tag"
while ((SECONDS < deadline)); do
apps=$(wrangler containers list --json)
apps=$("${wrangler_command[@]}" containers list --json)
all_ready=true

for app_name in "${app_names[@]}"; do
Expand All @@ -89,9 +105,9 @@ while ((SECONDS < deadline)); do
fi

app_id=$(jq -r '.id' <<<"$app")
app=$(wrangler containers info "$app_id")
app=$("${wrangler_command[@]}" containers info "$app_id")
expected=$(expected_image "$worker" "$image_tag" "$app_name")
instances=$(wrangler containers instances "$app_id" --json)
instances=$("${wrangler_command[@]}" containers instances "$app_id" --json)
reasons=$(readiness_reasons "$app" "$instances" "$expected")

if [[ -z $reasons ]]; then
Expand Down
64 changes: 64 additions & 0 deletions .github/scripts/wait-for-container-rollout.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,70 @@ assert_output() {
echo "PASS $name"
}

assert_expected_image() {
local name=$1 worker=$2 image_tag=$3 app_name=$4 expected_image=$5 actual
actual=$(CLOUDFLARE_ACCOUNT_ID=account \
"$script" --expected-image "$worker" "$image_tag" "$app_name")
if [[ $actual != "$expected_image" ]]; then
printf 'FAIL %s\nexpected: %q\nactual: %q\n' \
"$name" "$expected_image" "$actual" >&2
exit 1
fi
echo "PASS $name"
}

assert_expected_image browser-image worker ci-expected worker-browser \
'registry.cloudflare.com/account/sandbox:ci-expected'

mkdir -p "$tmp/bin"
cat >"$tmp/bin/npx" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "$*" >>"$WRANGLER_INVOCATIONS"
shift 2
case "$1 $2" in
'containers list')
cat <<'JSON'
[
{"id":"default","name":"worker"},
{"id":"browser","name":"worker-browser"},
{"id":"python","name":"worker-python"},
{"id":"opencode","name":"worker-opencode"},
{"id":"standalone","name":"worker-standalone"},
{"id":"musl","name":"worker-musl"}
]
JSON
;;
'containers info')
case "$3" in
default|browser) image=sandbox ;;
*) image="sandbox-$3" ;;
esac
printf '{"version":1,"configuration":{"image":"registry.cloudflare.com/account/%s:ci-expected"},"health":{"errors":[],"instances":{}}}\n' "$image"
;;
'containers instances') printf '[]\n' ;;
*) exit 1 ;;
esac
SH
chmod +x "$tmp/bin/npx"

invocations="$tmp/wrangler-invocations"
fallback_output=$(PATH="$tmp/bin:/usr/bin:/bin" \
WRANGLER_INVOCATIONS="$invocations" \
CLOUDFLARE_ACCOUNT_ID=account \
CLOUDFLARE_API_TOKEN=token \
ROLLOUT_TIMEOUT_SECONDS=5 \
"$script" worker ci-expected)
if [[ $fallback_output != *'All container applications are ready'* ]]; then
printf 'FAIL npx-fallback\n%s\n' "$fallback_output" >&2
exit 1
fi
if ! grep -qx -- '--yes wrangler@latest containers list --json' "$invocations"; then
printf 'FAIL npx-fallback invocation\n' >&2
cat "$invocations" >&2
exit 1
fi
echo 'PASS npx-fallback'

cat >"$tmp/ready.json" <<'JSON'
{"version":2,"configuration":{"image":"registry.cloudflare.com/account/sandbox:ci-expected"},"health":{"errors":[],"instances":{"starting":0,"scheduling":0,"failed":0}}}
JSON
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/reusable-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:
if: ${{ steps.deploy-check.outputs.skip != 'true' }}
run: |
SAFE_BUDGET=84
EXPECTED_PR_APPS=5
EXPECTED_PR_APPS=6

CONTAINERS=$(wrangler containers list --json 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$CONTAINERS" ]; then
Expand Down Expand Up @@ -326,7 +326,7 @@ jobs:
}

pids=()
for type in default python opencode standalone musl; do
for type in default browser python opencode standalone musl; do
smoke_type "$type" &
pids+=("$!")
done
Expand Down
4 changes: 2 additions & 2 deletions bridge/worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"devDependencies": {
"@biomejs/biome": "2.3.7",
"@cloudflare/sandbox": "*",
"@cloudflare/vitest-pool-workers": "^0.16.20",
"@cloudflare/workers-types": "^4.20251126.0",
"@cloudflare/vitest-pool-workers": "^0.18.7",
"@cloudflare/workers-types": "^5.20260721.1",
"@types/node": "^24.10.1",
"hono": "^4.12.26",
"typescript": "^5.9.3",
Expand Down
4 changes: 4 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ See [PROCESS_EXECUTION.md](./PROCESS_EXECUTION.md).

`createTerminal()` is the single PTY primitive for interactive shells. A terminal has its own ID, cursor-retained output, input, resize, interrupt, terminate, and reconnect path via `getTerminal(id)`. These terminal controls are intentionally separate: use terminals for interactive PTY state and `exec()` for supervised argv processes and numeric signals.

## Runtime authority

The Durable Object owns sandbox-lifetime state, while each live container runtime owns runtime-local truth. Before runtime RPCs, the SDK establishes or observes the control process, validates its runtime incarnation, activates one scoped control session, and admits the semantic operation through a runtime lease. Waking operations choose to start at the operation boundary; non-waking discovery and cleanup observe only an existing exact runtime and never create a replacement. Runtime authority, control domains, extension sidecars, streams, and WebSockets are callback- or transport-scoped and are not replayed after interruption.

## Active resources

The current runtime owns active process and terminal leases. Active resources pin the live Sandbox independent of the Worker request that launched them. Durable Object storage records durable sandbox configuration such as preview ports and mounts, not process or terminal truth.
Expand Down
Loading
Loading