From 88ccb8ca631e2e31e4719815468493a18203889a Mon Sep 17 00:00:00 2001 From: scuffi Date: Fri, 3 Jul 2026 14:37:59 +0100 Subject: [PATCH 1/4] Add documentation for extensions --- .../docs/sandbox/concepts/extensions.mdx | 302 ++++++++++++++++++ src/content/docs/sandbox/concepts/index.mdx | 1 + 2 files changed, 303 insertions(+) create mode 100644 src/content/docs/sandbox/concepts/extensions.mdx diff --git a/src/content/docs/sandbox/concepts/extensions.mdx b/src/content/docs/sandbox/concepts/extensions.mdx new file mode 100644 index 00000000000..b5f97f65d03 --- /dev/null +++ b/src/content/docs/sandbox/concepts/extensions.mdx @@ -0,0 +1,302 @@ +--- +title: Extensions +description: Add opt-in capabilities to a Sandbox subclass with the extension framework. +pcx_content_type: concept +sidebar: + order: 7 +products: + - sandbox +--- + +import { TypeScriptExample, PackageManagers } from "~/components"; + +The extension framework lets you attach opt-in capabilities to your `Sandbox` subclass instead of relying on features baked into the core SDK or container image. Extensions are how larger, optional features (such as the code interpreter and Git client) now ship in the Sandbox SDK. + +:::caution[Available in the `next` release only] +The extension framework, the `withInterpreter()` extension, and the `withGit()` extension are only available in the [`@cloudflare/sandbox@next`](https://www.npmjs.com/package/@cloudflare/sandbox/v/next) prerelease of the SDK. Install it explicitly: + + + +The `next` channel will become the default `latest` release in a future version. Until then, code samples on this page will not work against the current stable release. +::: + +## What extensions are for + +The base `Sandbox` class exposes a small, general-purpose surface: commands, files, processes, ports, and sessions. Extensions layer higher-level capabilities on top of that surface without bloating it: + +- **Keep the core lean.** You only pay for what you opt into. Extensions that are not attached are not shipped, provisioned, or executed. +- **Isolate feature runtimes.** An extension can ship its own long-running process inside the container (a sidecar) without polluting the base container image, and without exposing extra ports. +- **Encapsulate cross-layer logic.** An extension can drive existing container APIs from the Durable Object side, own its own container-side runtime, or do both. +- **Provide a stable authoring pattern.** Every extension follows the same `SandboxExtension` + `withX(this)` shape, so features written by Cloudflare and features you write yourself look identical to a caller. + +The code interpreter and Git client are the first two Cloudflare-authored extensions. Both were previously methods on the base `Sandbox` class and are now opt-in extensions instead. + +## How extensions work + +Every extension is a subclass of `SandboxExtension` from `@cloudflare/sandbox/extensions`, attached to a `Sandbox` subclass via a `withX(this)` factory: + + +```ts +import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; +import { withInterpreter } from "@cloudflare/sandbox/interpreter"; +import { withGit } from "@cloudflare/sandbox/git"; + +export class Sandbox extends BaseSandbox { + interpreter = withInterpreter(this); + git = withGit(this); +} +``` + + +From a Worker, you call extension methods as a nested namespace on the sandbox stub: + + +```ts +import { getSandbox } from "@cloudflare/sandbox"; + +const sandbox = getSandbox(env.Sandbox, "my-sandbox"); + +const ctx = await sandbox.interpreter.createCodeContext({ language: "python" }); +const result = await sandbox.interpreter.runCode('print("hello")', { context: ctx }); + +await sandbox.git.checkout("https://github.com/owner/repo.git", { depth: 1 }); + +```` + + +Under the hood, the SDK routes `sandbox..(...)` calls across the Worker → Durable Object boundary and dispatches them to the matching `SandboxExtension` instance. You can still add flatter delegate methods on your `Sandbox` subclass if you want to keep an existing API shape (`sandbox.runCode(...)`), but they are optional. + +Extension methods called from a Worker must return serializable data. You cannot hand back class instances that only make sense inside the Durable Object. + +### Two flavors: SDK-only and sidecar + +Extensions come in two flavors, depending on whether they need their own container-side runtime: + +- **SDK-only extensions** orchestrate existing container APIs (`commands`, `files`, `processes`, and so on) from the Durable Object side. Nothing extra runs inside the container. The Git extension works this way, because `git` is just shell commands: the extension calls `exec()` with the right arguments and parses the output. +- **Sidecar extensions** ship a bundled runtime that runs as a supervised process inside the container. The SDK talks to it over a private channel, with no extra network ports and no HTTP. The code interpreter works this way, because its Python and JavaScript executors need a long-running process pool inside the container. + +Sidecars are provisioned lazily the first time you call the extension, cached across sandboxes, and restarted automatically if they crash. You never manage the lifecycle yourself. + +As a rule of thumb: if existing container APIs are enough to implement your feature, use an SDK-only extension. Reach for a sidecar only when you need a long-running process inside the container. + +## Authoring an extension + +Every extension follows the same shape: a class extending `SandboxExtension`, and a `withX(sandbox)` factory. The base class captures the sandbox and exposes `this.client` (the container API surface) and, for sidecar extensions, `this.sidecar()` (a typed remote handle to the sidecar process). + +### SDK-only extension + +An SDK-only extension calls existing container APIs. Here is a minimal example that shells out via `commands.execute`: + + +```ts +import { + SandboxExtension, + type SandboxLike, +} from "@cloudflare/sandbox/extensions"; + +class Greeter extends SandboxExtension { + constructor(sandbox: SandboxLike) { + super(sandbox); + } + + async greet(name: string, sessionId: string) { + const { stdout } = await this.client.commands.execute( + `echo "hi ${name}"`, + sessionId, + ); + return stdout.trim(); + } +} + +export const withGreeter = (s: SandboxLike) => new Greeter(s); +```` + + + +Wire it onto a `Sandbox` subclass: + + +```ts +import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; +import { withGreeter } from "./greeter"; + +export class Sandbox extends BaseSandbox { + greeter = withGreeter(this); +} +``` + + +Callers use `sandbox.greeter.greet(name, sessionId)` from a Worker. + +### Sidecar extension + +A sidecar extension additionally passes a tarball to `super()` and reaches its container-side runtime through `this.sidecar()`: + + +```ts +import sidecarTarballBytes from "./sidecar-package.tgz"; +import { + SandboxExtension, + type SandboxLike, +} from "@cloudflare/sandbox/extensions"; +import type { MySidecarAPI } from "./shared"; + +class MyExtension extends SandboxExtension { +constructor(sandbox: SandboxLike) { +super(sandbox, { tarball: new Uint8Array(sidecarTarballBytes) }); +} + + async run(input: string) { + const api = await this.sidecar(); + return api.run(input); + } + +} + +export const withMyExtension = (s: SandboxLike) => new MyExtension(s); +``` + + +The sidecar itself is a small npm package that extends `SandboxSidecar` and calls `serveSandboxSidecar(...)`. It listens on the `EXT_SOCKET` Unix socket the container injects and serves a capnweb session per connection: + + +```ts +import { + SandboxSidecar, + serveSandboxSidecar, +} from "@cloudflare/sandbox/sidecar"; + +class MySidecar extends SandboxSidecar { +async run(input: string) { +return { result: input.toUpperCase() }; +} +} + +serveSandboxSidecar(new MySidecar()); + +```` + + +Streaming is expressed as a typed callback parameter on a sidecar method. The callback is stubbed across both the Worker to Durable Object and Durable Object to sidecar hops, so there is no separate streaming protocol to learn: + + +```ts +import { + SandboxExtension, + type SandboxLike, +} from "@cloudflare/sandbox/extensions"; +import type { InterpreterSidecarAPI } from "./shared"; + +class Interpreter extends SandboxExtension { + constructor(sandbox: SandboxLike) { + super(sandbox, { tarball: new Uint8Array() }); + } + + async runCode( + code: string, + onEvent: (event: { kind: string; data: unknown }) => void, + ) { + const api = await this.sidecar(); + return api.runCode(code, onEvent); + } +} +``` + + + +## Practical examples + +### Code interpreter (sidecar extension) + +The Python/JavaScript interpreter ships as `@cloudflare/sandbox/interpreter`. Its container-side runtime (the process pool and language executors) runs as a sidecar that is provisioned the first time you use it, so you only pay for it when you opt in. + + +```ts +import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; +import { withInterpreter } from "@cloudflare/sandbox/interpreter"; + +export class Sandbox extends BaseSandbox { + interpreter = withInterpreter(this); +} +``` + + +From a Worker: + + +```ts +import { getSandbox } from "@cloudflare/sandbox"; + +const sandbox = getSandbox(env.Sandbox, "my-sandbox"); + +const context = await sandbox.interpreter.createCodeContext({ +language: "python", +}); + +const result = await sandbox.interpreter.runCode('print("hello")', { +context, +}); + +```` + + + +Python execution still requires the `-python` container image variant. `runCode()` resolves to an `ExecutionResult` (a plain, serializable object) so it can safely cross the Worker → Durable Object boundary. + +### Git client (SDK-only extension) + +Git ships as `@cloudflare/sandbox/git`. Because `git` is just shell commands, no sidecar is needed: the extension calls `exec()` with the right arguments and parses the results. + + +```ts +import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; +import { withGit } from "@cloudflare/sandbox/git"; + +export class Sandbox extends BaseSandbox { + git = withGit(this); +} +```` + + + +From a Worker: + + +```ts +const sandbox = getSandbox(env.Sandbox, "my-sandbox"); + +await sandbox.git.checkout("https://github.com/owner/repo.git", { depth: 1 }); +await sandbox.git.checkoutBranch("feature/my-work"); + +const branch = await sandbox.git.getCurrentBranch(); +const branches = await sandbox.git.listBranches(); + +```` + + +### Migrating from the base class + +In the `next` release, `sandbox.gitCheckout()` and the interpreter methods (`createCodeContext`, `runCode`, `runCodeStream`, `listCodeContexts`, `deleteCodeContext`) no longer exist on the base `Sandbox` class. To keep existing call sites working, attach the extension and add a delegate method: + + +```ts +import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; +import { withGit } from "@cloudflare/sandbox/git"; + +export class Sandbox extends BaseSandbox { + git = withGit(this); + + // Optional: preserve the previous flat API. + gitCheckout(url: string, options?: { depth?: number }) { + return this.git.checkout(url, options); + } +} +```` + + + +## Related resources + +- [Architecture](/sandbox/concepts/architecture/): how Workers, Durable Objects, and containers fit together +- [Use code interpreter](/sandbox/guides/code-execution/): running Python and JavaScript via the interpreter extension +- [Git workflows](/sandbox/guides/git-workflows/): cloning and managing repositories via the Git extension diff --git a/src/content/docs/sandbox/concepts/index.mdx b/src/content/docs/sandbox/concepts/index.mdx index 586ad13822d..570fb26b18b 100644 --- a/src/content/docs/sandbox/concepts/index.mdx +++ b/src/content/docs/sandbox/concepts/index.mdx @@ -17,6 +17,7 @@ These pages explain how the Sandbox SDK works, why it's designed the way it is, - [Preview URLs](/sandbox/concepts/preview-urls/) - How to expose sandboxed services on the public internet. - [Security model](/sandbox/concepts/security/) - Isolation, validation, and safety mechanisms - [Terminal connections](/sandbox/concepts/terminal/) - How browser terminal connections work +- [Extensions](/sandbox/concepts/extensions/) - Add opt-in capabilities with the extension framework ## Related resources From 9d4dd78dc14095dc3686a2ee3cad6067623de918 Mon Sep 17 00:00:00 2001 From: scuffi Date: Fri, 3 Jul 2026 14:58:28 +0100 Subject: [PATCH 2/4] [Sandbox] Address review feedback on extensions page - Restore lost indentation in MyExtension/MySidecar class bodies and interpreter runCode example - Add missing getSandbox import to the Git 'From a Worker' snippet - Standardize all code fences to three backticks - Removes blank lines inside code blocks to prevent the MDX prettier plugin from stripping indentation --- .../docs/sandbox/concepts/extensions.mdx | 166 ++++++++---------- 1 file changed, 76 insertions(+), 90 deletions(-) diff --git a/src/content/docs/sandbox/concepts/extensions.mdx b/src/content/docs/sandbox/concepts/extensions.mdx index b5f97f65d03..e3f3e467db1 100644 --- a/src/content/docs/sandbox/concepts/extensions.mdx +++ b/src/content/docs/sandbox/concepts/extensions.mdx @@ -36,14 +36,14 @@ The code interpreter and Git client are the first two Cloudflare-authored extens Every extension is a subclass of `SandboxExtension` from `@cloudflare/sandbox/extensions`, attached to a `Sandbox` subclass via a `withX(this)` factory: -```ts +``` import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; import { withInterpreter } from "@cloudflare/sandbox/interpreter"; import { withGit } from "@cloudflare/sandbox/git"; export class Sandbox extends BaseSandbox { - interpreter = withInterpreter(this); - git = withGit(this); + interpreter = withInterpreter(this); + git = withGit(this); } ``` @@ -51,20 +51,21 @@ export class Sandbox extends BaseSandbox { From a Worker, you call extension methods as a nested namespace on the sandbox stub: -```ts +``` import { getSandbox } from "@cloudflare/sandbox"; const sandbox = getSandbox(env.Sandbox, "my-sandbox"); const ctx = await sandbox.interpreter.createCodeContext({ language: "python" }); -const result = await sandbox.interpreter.runCode('print("hello")', { context: ctx }); +const result = await sandbox.interpreter.runCode('print("hello")', { + context: ctx, +}); await sandbox.git.checkout("https://github.com/owner/repo.git", { depth: 1 }); - -```` +``` -Under the hood, the SDK routes `sandbox..(...)` calls across the Worker → Durable Object boundary and dispatches them to the matching `SandboxExtension` instance. You can still add flatter delegate methods on your `Sandbox` subclass if you want to keep an existing API shape (`sandbox.runCode(...)`), but they are optional. +Under the hood, the SDK routes `sandbox..(...)` calls across the Worker to Durable Object boundary and dispatches them to the matching `SandboxExtension` instance. You can still add flatter delegate methods on your `Sandbox` subclass if you want to keep an existing API shape (`sandbox.runCode(...)`), but they are optional. Extension methods called from a Worker must return serializable data. You cannot hand back class instances that only make sense inside the Durable Object. @@ -88,40 +89,37 @@ Every extension follows the same shape: a class extending `SandboxExtension`, an An SDK-only extension calls existing container APIs. Here is a minimal example that shells out via `commands.execute`: -```ts +``` import { - SandboxExtension, - type SandboxLike, + SandboxExtension, + type SandboxLike, } from "@cloudflare/sandbox/extensions"; class Greeter extends SandboxExtension { - constructor(sandbox: SandboxLike) { - super(sandbox); - } - - async greet(name: string, sessionId: string) { - const { stdout } = await this.client.commands.execute( - `echo "hi ${name}"`, - sessionId, - ); - return stdout.trim(); - } + constructor(sandbox: SandboxLike) { + super(sandbox); + } + async greet(name: string, sessionId: string) { + const { stdout } = await this.client.commands.execute( + `echo "hi ${name}"`, + sessionId, + ); + return stdout.trim(); + } } - export const withGreeter = (s: SandboxLike) => new Greeter(s); -```` - +``` Wire it onto a `Sandbox` subclass: -```ts +``` import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; import { withGreeter } from "./greeter"; export class Sandbox extends BaseSandbox { - greeter = withGreeter(this); + greeter = withGreeter(this); } ``` @@ -133,26 +131,23 @@ Callers use `sandbox.greeter.greet(name, sessionId)` from a Worker. A sidecar extension additionally passes a tarball to `super()` and reaches its container-side runtime through `this.sidecar()`: -```ts +``` import sidecarTarballBytes from "./sidecar-package.tgz"; import { - SandboxExtension, - type SandboxLike, + SandboxExtension, + type SandboxLike, } from "@cloudflare/sandbox/extensions"; import type { MySidecarAPI } from "./shared"; class MyExtension extends SandboxExtension { -constructor(sandbox: SandboxLike) { -super(sandbox, { tarball: new Uint8Array(sidecarTarballBytes) }); + constructor(sandbox: SandboxLike) { + super(sandbox, { tarball: new Uint8Array(sidecarTarballBytes) }); + } + async run(input: string) { + const api = await this.sidecar(); + return api.run(input); + } } - - async run(input: string) { - const api = await this.sidecar(); - return api.run(input); - } - -} - export const withMyExtension = (s: SandboxLike) => new MyExtension(s); ``` @@ -160,48 +155,44 @@ export const withMyExtension = (s: SandboxLike) => new MyExtension(s); The sidecar itself is a small npm package that extends `SandboxSidecar` and calls `serveSandboxSidecar(...)`. It listens on the `EXT_SOCKET` Unix socket the container injects and serves a capnweb session per connection: -```ts +``` import { - SandboxSidecar, - serveSandboxSidecar, + SandboxSidecar, + serveSandboxSidecar, } from "@cloudflare/sandbox/sidecar"; class MySidecar extends SandboxSidecar { -async run(input: string) { -return { result: input.toUpperCase() }; -} + async run(input: string) { + return { result: input.toUpperCase() }; + } } - serveSandboxSidecar(new MySidecar()); - -```` +``` Streaming is expressed as a typed callback parameter on a sidecar method. The callback is stubbed across both the Worker to Durable Object and Durable Object to sidecar hops, so there is no separate streaming protocol to learn: -```ts +``` import { - SandboxExtension, - type SandboxLike, + SandboxExtension, + type SandboxLike, } from "@cloudflare/sandbox/extensions"; import type { InterpreterSidecarAPI } from "./shared"; class Interpreter extends SandboxExtension { - constructor(sandbox: SandboxLike) { - super(sandbox, { tarball: new Uint8Array() }); - } - - async runCode( - code: string, - onEvent: (event: { kind: string; data: unknown }) => void, - ) { - const api = await this.sidecar(); - return api.runCode(code, onEvent); - } + constructor(sandbox: SandboxLike) { + super(sandbox, { tarball: new Uint8Array() }); + } + async runCode( + code: string, + onEvent: (event: { kind: string; data: unknown }) => void, + ) { + const api = await this.sidecar(); + return api.runCode(code, onEvent); + } } ``` - ## Practical examples @@ -211,12 +202,12 @@ class Interpreter extends SandboxExtension { The Python/JavaScript interpreter ships as `@cloudflare/sandbox/interpreter`. Its container-side runtime (the process pool and language executors) runs as a sidecar that is provisioned the first time you use it, so you only pay for it when you opt in. -```ts +``` import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; import { withInterpreter } from "@cloudflare/sandbox/interpreter"; export class Sandbox extends BaseSandbox { - interpreter = withInterpreter(this); + interpreter = withInterpreter(this); } ``` @@ -224,45 +215,43 @@ export class Sandbox extends BaseSandbox { From a Worker: -```ts +``` import { getSandbox } from "@cloudflare/sandbox"; const sandbox = getSandbox(env.Sandbox, "my-sandbox"); const context = await sandbox.interpreter.createCodeContext({ -language: "python", + language: "python", }); - const result = await sandbox.interpreter.runCode('print("hello")', { -context, + context, }); - -```` - +``` -Python execution still requires the `-python` container image variant. `runCode()` resolves to an `ExecutionResult` (a plain, serializable object) so it can safely cross the Worker → Durable Object boundary. +Python execution still requires the `-python` container image variant. `runCode()` resolves to an `ExecutionResult` (a plain, serializable object) so it can safely cross the Worker to Durable Object boundary. ### Git client (SDK-only extension) Git ships as `@cloudflare/sandbox/git`. Because `git` is just shell commands, no sidecar is needed: the extension calls `exec()` with the right arguments and parses the results. -```ts +``` import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; import { withGit } from "@cloudflare/sandbox/git"; export class Sandbox extends BaseSandbox { - git = withGit(this); + git = withGit(this); } -```` - +``` From a Worker: -```ts +``` +import { getSandbox } from "@cloudflare/sandbox"; + const sandbox = getSandbox(env.Sandbox, "my-sandbox"); await sandbox.git.checkout("https://github.com/owner/repo.git", { depth: 1 }); @@ -270,8 +259,7 @@ await sandbox.git.checkoutBranch("feature/my-work"); const branch = await sandbox.git.getCurrentBranch(); const branches = await sandbox.git.listBranches(); - -```` +``` ### Migrating from the base class @@ -279,20 +267,18 @@ const branches = await sandbox.git.listBranches(); In the `next` release, `sandbox.gitCheckout()` and the interpreter methods (`createCodeContext`, `runCode`, `runCodeStream`, `listCodeContexts`, `deleteCodeContext`) no longer exist on the base `Sandbox` class. To keep existing call sites working, attach the extension and add a delegate method: -```ts +``` import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; import { withGit } from "@cloudflare/sandbox/git"; export class Sandbox extends BaseSandbox { - git = withGit(this); - - // Optional: preserve the previous flat API. - gitCheckout(url: string, options?: { depth?: number }) { - return this.git.checkout(url, options); - } + git = withGit(this); + // Optional: preserve the previous flat API. + gitCheckout(url: string, options?: { depth?: number }) { + return this.git.checkout(url, options); + } } -```` - +``` ## Related resources From 9c4fb02f5405fdca60e8a04e8a4691c6040f8581 Mon Sep 17 00:00:00 2001 From: scuffi Date: Fri, 3 Jul 2026 16:26:04 +0100 Subject: [PATCH 3/4] [Sandbox] Address style-guide and code-safety review feedback - Replace Greeter example with SystemInfo (uname -a) to avoid demoing shell-string interpolation of user input; add a short safety note for callers that do need to pass values into shell commands - Rename headings to imperative form: 'Author an extension' and 'Migrate from the base class' --- .../docs/sandbox/concepts/extensions.mdx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/content/docs/sandbox/concepts/extensions.mdx b/src/content/docs/sandbox/concepts/extensions.mdx index e3f3e467db1..7d2e3b6d89f 100644 --- a/src/content/docs/sandbox/concepts/extensions.mdx +++ b/src/content/docs/sandbox/concepts/extensions.mdx @@ -80,13 +80,13 @@ Sidecars are provisioned lazily the first time you call the extension, cached ac As a rule of thumb: if existing container APIs are enough to implement your feature, use an SDK-only extension. Reach for a sidecar only when you need a long-running process inside the container. -## Authoring an extension +## Author an extension Every extension follows the same shape: a class extending `SandboxExtension`, and a `withX(sandbox)` factory. The base class captures the sandbox and exposes `this.client` (the container API surface) and, for sidecar extensions, `this.sidecar()` (a typed remote handle to the sidecar process). ### SDK-only extension -An SDK-only extension calls existing container APIs. Here is a minimal example that shells out via `commands.execute`: +An SDK-only extension calls existing container APIs. Here is a minimal example that reports basic system information by shelling out via `commands.execute`: ``` @@ -95,19 +95,19 @@ import { type SandboxLike, } from "@cloudflare/sandbox/extensions"; -class Greeter extends SandboxExtension { +class SystemInfo extends SandboxExtension { constructor(sandbox: SandboxLike) { super(sandbox); } - async greet(name: string, sessionId: string) { + async kernelVersion(sessionId: string) { const { stdout } = await this.client.commands.execute( - `echo "hi ${name}"`, + "uname -a", sessionId, ); return stdout.trim(); } } -export const withGreeter = (s: SandboxLike) => new Greeter(s); +export const withSystemInfo = (s: SandboxLike) => new SystemInfo(s); ``` @@ -116,15 +116,17 @@ Wire it onto a `Sandbox` subclass: ``` import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; -import { withGreeter } from "./greeter"; +import { withSystemInfo } from "./system-info"; export class Sandbox extends BaseSandbox { - greeter = withGreeter(this); + systemInfo = withSystemInfo(this); } ``` -Callers use `sandbox.greeter.greet(name, sessionId)` from a Worker. +Callers use `sandbox.systemInfo.kernelVersion(sessionId)` from a Worker. + +If your extension needs to pass caller-supplied values into a shell command, do not string-interpolate them into the command. Untrusted input can contain shell metacharacters and alter the command that runs. Validate the input against an allowlist, or escape it before use. ### Sidecar extension @@ -262,7 +264,7 @@ const branches = await sandbox.git.listBranches(); ``` -### Migrating from the base class +### Migrate from the base class In the `next` release, `sandbox.gitCheckout()` and the interpreter methods (`createCodeContext`, `runCode`, `runCodeStream`, `listCodeContexts`, `deleteCodeContext`) no longer exist on the base `Sandbox` class. To keep existing call sites working, attach the extension and add a delegate method: From d1eacc17616217cec30d7db3a9a5b411ee1ef8f8 Mon Sep 17 00:00:00 2001 From: scuffi Date: Fri, 3 Jul 2026 16:54:40 +0100 Subject: [PATCH 4/4] [Sandbox] Add changelog entry for extensions framework Announces the new extension framework and the interpreter and Git extensions available in @cloudflare/sandbox@next. Links back to the concepts page for full authoring and migration guidance. --- .../2026-07-03-extensions-framework.mdx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/content/changelog/sandbox/2026-07-03-extensions-framework.mdx diff --git a/src/content/changelog/sandbox/2026-07-03-extensions-framework.mdx b/src/content/changelog/sandbox/2026-07-03-extensions-framework.mdx new file mode 100644 index 00000000000..2d0199bc7d1 --- /dev/null +++ b/src/content/changelog/sandbox/2026-07-03-extensions-framework.mdx @@ -0,0 +1,44 @@ +--- +title: Sandbox SDK extensions framework +description: Attach opt-in capabilities to your Sandbox subclass with the new extension framework. The code interpreter and Git client are now shipped as extensions. +products: + - sandbox +date: 2026-07-03 +--- + +The Sandbox SDK now has an **extension framework** that lets you attach opt-in capabilities to your `Sandbox` subclass instead of relying on features baked into the core SDK or container image. Extensions come in two flavors: SDK-only extensions that orchestrate existing container APIs from the Durable Object side, and sidecar extensions that ship a bundled runtime provisioned inside the container on first use. + +The framework also delivers on our previously announced [plan to move non-core features into helpers](/changelog/2026-06-09-deprecating-sandbox-sdk-features/). Two Cloudflare-authored extensions ship alongside the framework: + +- **`@cloudflare/sandbox/interpreter`** — the Python and JavaScript code interpreter, now provisioned as a sidecar the first time you use it. +- **`@cloudflare/sandbox/git`** — the Git client, driving `exec()` from the Durable Object side. + +Attach them to your `Sandbox` subclass: + +```ts +import { Sandbox as BaseSandbox } from "@cloudflare/sandbox"; +import { withInterpreter } from "@cloudflare/sandbox/interpreter"; +import { withGit } from "@cloudflare/sandbox/git"; + +export class Sandbox extends BaseSandbox { + interpreter = withInterpreter(this); + git = withGit(this); +} +``` + +And call them from a Worker as a nested namespace on the sandbox stub: + +```ts +await sandbox.interpreter.runCode('print("hello")', { context }); +await sandbox.git.checkout("https://github.com/owner/repo.git", { depth: 1 }); +``` + +The extension framework, `withInterpreter()`, and `withGit()` are currently available in the [`@cloudflare/sandbox@next`](https://www.npmjs.com/package/@cloudflare/sandbox/v/next) prerelease of the SDK. Install it explicitly to try it today: + +```sh +npm install @cloudflare/sandbox@next +``` + +The `next` channel will become the default `latest` release in a future version. Note that on `next`, `sandbox.gitCheckout()` and the interpreter methods (`createCodeContext`, `runCode`, `runCodeStream`, `listCodeContexts`, `deleteCodeContext`) no longer exist on the base `Sandbox` class — attach the extension and, optionally, add a delegate method to preserve the previous flat API. + +For the full authoring guide, examples, and migration notes, refer to the new [Extensions concept page](/sandbox/concepts/extensions/).