From ab3dbaee6275a6ab16c6712f3c8b5b02a4cf52b5 Mon Sep 17 00:00:00 2001 From: Naresh Date: Thu, 8 Oct 2026 13:58:51 +0100 Subject: [PATCH 1/2] [Sandbox] Clarify the first half of the migration The first migration pages used container application, instance type, interception, snapshot, outbound handler, and restart without saying what they are. The inventory search missed exec(), the move-in step for background processes left readers to work out which parts of two pages to copy, process IDs reached rm -rf unchecked, and several pages restated limits and outcomes that the Containers API owns. Define each term at first use, add exec() and execStream() to the search with its real output, name exactly what to copy, check IDs against one pattern before any process method or rm -rf, and link the owning pages instead of restating them. --- .../sandbox/sdk/migrate/background-processes.mdx | 10 ++++------ .../docs/sandbox/sdk/migrate/changes-in-1-0.mdx | 8 ++++---- src/content/docs/sandbox/sdk/migrate/commands.mdx | 9 +++++++-- src/content/docs/sandbox/sdk/migrate/files.mdx | 6 +++--- src/content/docs/sandbox/sdk/migrate/index.mdx | 12 +++++++----- .../docs/sandbox/sdk/migrate/move-side-by-side.mdx | 2 +- .../docs/sandbox/sdk/migrate/plan-the-move.mdx | 6 +++--- .../sdk/migrate/replace-the-sandbox-class.mdx | 12 ++++++------ 8 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/content/docs/sandbox/sdk/migrate/background-processes.mdx b/src/content/docs/sandbox/sdk/migrate/background-processes.mdx index 0f4c263adb5..d5dc957ee9a 100644 --- a/src/content/docs/sandbox/sdk/migrate/background-processes.mdx +++ b/src/content/docs/sandbox/sdk/migrate/background-processes.mdx @@ -24,11 +24,11 @@ This page replaces `startProcess()`, the `Process` methods, `listProcesses()`, ` -1. Add the scripts and methods from [Run background processes](/sandbox/commands/run-background-processes/) to your class, including the alarm handler, and `follow()` and `waitForLog()` from [Follow a background process](/sandbox/commands/follow-a-background-process/). +1. From [Run background processes](/sandbox/commands/run-background-processes/), add the `ROOT`, `RUN`, `CURRENT`, and `STATUS` constants, the `Status` type, and the `run()` function to your class file. Add its `status()`, `logs()`, `stop()`, and `alarm()` methods to your class. Leave out its `INACTIVITY_TIMEOUT_MS` constant and its constructor, which your class file already has. From [Follow a background process](/sandbox/commands/follow-a-background-process/), add the `WAIT` constant and the `follow()` and `waitForLog()` methods. A Durable Object has one alarm. If your class already has an `alarm()` handler, such as the one from [Move every sandbox in one deploy](/sandbox/sdk/migrate/plan-the-move/#move-every-sandbox-in-one-deploy), merge the process checks into it rather than adding a second. -2. Change `startProcess()` to start the container with `ensureRunning()`, and to run each process with the working directory and environment that 0.12 gave it: +2. Add `startProcess()`. Unlike the version in Run background processes, it starts the container with `ensureRunning()`, and runs each process with the working directory and environment that 0.12 gave it: ```ts title="src/index.ts" ins={5,19,20} export class MySandbox extends DurableObject { @@ -71,8 +71,6 @@ This page replaces `startProcess()`, the `Process` methods, `listProcesses()`, ` 0.12 ran each process in `/workspace`, with the variables from `envVars`. `exec()` starts in `/`, even when the image sets `WORKDIR`. It does not inherit the variables that `start()` sets, except `PATH`. -3. Remove `INACTIVITY_TIMEOUT_MS` from the scripts that you added in step 1. Your class file already defines it, so the constructor and the alarm handler keep the inactivity timeout of your class. - On this page, `sandbox` is the stub that `getByName()` returns. @@ -95,7 +93,7 @@ const started = await sandbox.startProcess("dev", [ ]); ``` -Each process needs an ID. Where 0.12 generated one, such as `proc_1790379992394_6snsuw`, use `crypto.randomUUID()`. The ID becomes a directory name, so use only characters that are safe in a path. +Each process needs an ID. Where 0.12 generated one, such as `proc_1790379992394_6snsuw`, use `crypto.randomUUID()`. The ID becomes a directory name, so reject an ID unless it matches `^[A-Za-z0-9_-]+$` before you pass it to any process method. An ID such as `../app` would otherwise reach a directory outside `ROOT`. `startProcess()` returns `false` when a process already uses the ID. 0.12 replaced the record instead, and the first process kept running without one. An ended process keeps its directory, so `startProcess()` also returns `false` for its ID. To start that ID again, delete the directory first, as [Stop processes](#stop-processes) describes. @@ -239,7 +237,7 @@ await sandbox.stop("dev"); `stop()` sends `SIGTERM` to the process group, which reaches the processes that the command started. 0.12 sent `SIGKILL` 5 seconds later to any process still running, and ignored the `signal` argument. To stop a process that ignores `SIGTERM`, change `TERM` to `KILL` in `stop()`. -Replace `killAllProcesses()` with a `stop()` call for each running process that `listProcesses()` returns. Remove `cleanupCompletedProcesses()` and the `autoCleanup` option, which did nothing in 0.12. To delete the directory of an ended process, run `rm -rf` on it. +Replace `killAllProcesses()` with a `stop()` call for each running process that `listProcesses()` returns. Remove `cleanupCompletedProcesses()` and the `autoCleanup` option, which did nothing in 0.12. To delete the directory of an ended process, check its ID against the same pattern, then run `rm -rf` on `${ROOT}/${id}`. ## Replace callbacks diff --git a/src/content/docs/sandbox/sdk/migrate/changes-in-1-0.mdx b/src/content/docs/sandbox/sdk/migrate/changes-in-1-0.mdx index 8667bf812a6..fd6f225e575 100644 --- a/src/content/docs/sandbox/sdk/migrate/changes-in-1-0.mdx +++ b/src/content/docs/sandbox/sdk/migrate/changes-in-1-0.mdx @@ -21,7 +21,7 @@ In 0.x, the `Sandbox` class from the package starts the container, keeps it runn -`this.ctx.container` does what the 0.x SDK server did. It runs commands, connects to ports, intercepts outbound requests, and saves snapshots. The package keeps only small helpers, such as the `Files` class. +`this.ctx.container` does what the 0.x SDK server did. It runs commands, connects to ports, routes outbound requests from the container to code in your Worker, and saves the root filesystem of a container as a snapshot that a later container can start from. The package keeps only small helpers, such as the `Files` class. Decisions that depend on your application stay in your code: how long a sandbox runs, what it can reach, and what happens when a call fails. For example, a cancelled command may already have changed files, and only your application knows whether running it again is safe. @@ -37,17 +37,17 @@ Some 0.x features have no equivalent. For each 0.x API and its replacement, refe ## Decisions your code makes -A container has no Internet access unless `start()` passes `enableInternet: true`. To allow one destination, your class registers an outbound handler for its hostname. The handler can add a credential that the container never sees. For more information, refer to [Sandbox security](/sandbox/concepts/security/). +A container has no Internet access unless `start()` passes `enableInternet: true`. To allow one destination, your class registers an outbound handler for its hostname. An outbound handler is Worker code that receives each request that the container sends to that hostname. The handler can add a credential that the container never sees. For more information, refer to [Sandbox security](/sandbox/concepts/security/). `exec()` takes an array of arguments and starts that process without a shell. Text passed as an argument cannot start a second command. A shell runs only when you start one, such as `["sh", "-c", "npm ci && npm test"]`. Each call also sets its own `cwd` and `env`. No session carries them from one call to the next. -Your class decides how long a sandbox runs by setting the inactivity timeout after `start()`, and again in the constructor when a restarted Durable Object finds the container running. A deploy does not replace running containers. A new image applies only to containers that start after the deploy. The same holds for code that your class runs only when it starts a container, such as the calls that register outbound handlers. A handler that is already registered runs the code of the new version, with the props it was registered with. For more information, refer to [Sandbox lifetime](/sandbox/concepts/lifetime/). +Your class decides how long a sandbox runs, through the inactivity timeout that it sets on the container. A deploy does not replace running containers. A new image applies only to containers that start after the deploy. The same holds for code that your class runs only when it starts a container, such as the calls that register outbound handlers. A handler that is already registered runs the code of the new version, with the props it was registered with. For more information, refer to [Sandbox lifetime](/sandbox/concepts/lifetime/). Your image provides every tool that your commands use. The 0.x image includes Python, Git, `curl`, `wget`, `jq`, `unzip`, and `ps`. `cloudflare/debian-trixie` and `node:24-trixie-slim`, which these pages use, include none of them. `Files`, `S3Mount`, and `DirectoryBackup` also need the `sandbox-shim` helper in the image. For the `COPY` line, refer to [`@cloudflare/sandbox` requirements](/sandbox/reference/#requirements). The package writes nothing to Durable Object storage. A preview token, a process record, or a snapshot ID exists only if your class stores it. The keys that 0.x wrote stay in storage. The package ignores them, and your class can read them, as the pages that move preview URLs, tunnels, and outbound rules do. -The package also retries nothing and sets no time limits. To stop a command that runs too long, run it under GNU coreutils `timeout`, as [Replace timeouts](/sandbox/sdk/migrate/commands/#replace-timeouts) describes. +The package also retries nothing and sets no time limits. To limit how long a command runs, refer to [Replace timeouts](/sandbox/sdk/migrate/commands/#replace-timeouts). ## Related resources diff --git a/src/content/docs/sandbox/sdk/migrate/commands.mdx b/src/content/docs/sandbox/sdk/migrate/commands.mdx index 549ed9a3f7d..4039747aeca 100644 --- a/src/content/docs/sandbox/sdk/migrate/commands.mdx +++ b/src/content/docs/sandbox/sdk/migrate/commands.mdx @@ -131,7 +131,7 @@ const result = await process.output(); const timedOut = result.exitCode === 124 || result.exitCode === 137; ``` -0.12 threw `CommandError` with the message `Command timeout after 60000ms`, and the command kept running. In 1.0, a command that times out stops, so it cannot change files after the error. +0.12 threw `CommandError` with the message `Command timeout after 60000ms`, and the command kept running. In 1.0, a command that times out stops with the processes still in its process group, so they cannot change files after the error. To cancel a command from your code instead, pass an `AbortSignal`. For how to clear its timer, refer to [`exec()`](/containers/api/durable-object-container/#exec). @@ -151,9 +151,14 @@ const withOptions = await this.container.exec(command, { cwd: "/workspace", env: ENV, }); + +for (const result of [bare, withOptions]) { + const { stdout } = await result.output(); + console.log(new TextDecoder().decode(stdout).trim()); +} ``` -The output of `bare` shows the defaults, and the output of `withOptions` shows what 0.12 gave each command: +The first line shows the defaults, and the second line shows what 0.12 gave each command: ```txt output pwd=/ NODE_ENV=unset diff --git a/src/content/docs/sandbox/sdk/migrate/files.mdx b/src/content/docs/sandbox/sdk/migrate/files.mdx index 34f2481771c..86bfad796e5 100644 --- a/src/content/docs/sandbox/sdk/migrate/files.mdx +++ b/src/content/docs/sandbox/sdk/migrate/files.mdx @@ -10,7 +10,7 @@ products: - durable-objects --- -In Sandbox SDK 0.12, the SDK server in the container handled file calls. It created missing directories, returned binary files as base64, and hid dotfiles. In 1.0, [`Files`](/sandbox/reference/files/) runs a small helper in the container for each call, and does only what the call asks. +In Sandbox SDK 0.12, a server that the SDK ran in the container handled file calls. It created missing directories, returned binary files as base64, and left dotfiles, whose names begin with `.`, out of listings. In 1.0, [`Files`](/sandbox/reference/files/) runs a small helper in the container for each call, and does only what the call asks. ## Before you start @@ -31,7 +31,7 @@ This page replaces `readFile()`, `readFileStream()`, `writeFile()`, `exists()`, ## Change each file call -These calls keep their meaning, under a new name or with a different result: +Each 0.12 call maps to one 1.0 call, under a new name or with a different result: | 0.12 | 1.0 | | --- | --- | @@ -137,7 +137,7 @@ try { } ``` -Two calls behave differently from 0.12: +Removing and renaming behave differently from 0.12: - `deleteFile()` refused a directory. `remove()` removes one with `recursive: true`, and rejects with `EISDIR` without it. - `renameFile()` and `moveFile()` ran `mv`. When the destination was an existing directory, `mv` moved the source into it. `rename()` follows Linux `rename` rules instead: it replaces the destination, and rejects with `ENOTEMPTY` when the destination is a directory that holds files. Pass the full new path, such as `/workspace/archive/report.txt`. diff --git a/src/content/docs/sandbox/sdk/migrate/index.mdx b/src/content/docs/sandbox/sdk/migrate/index.mdx index a04ca2e7ca2..34fd96f3789 100644 --- a/src/content/docs/sandbox/sdk/migrate/index.mdx +++ b/src/content/docs/sandbox/sdk/migrate/index.mdx @@ -13,9 +13,9 @@ products: - durable-objects --- -To move an application from Sandbox SDK 0.x to 1.0, replace the `Sandbox` class with a Durable Object class that you write. Then replace each 0.x feature that your application uses. Your class starts a container that uses the [Durable Object scheduling policy](/containers/configuration/scheduling-policy/#use-the-durable-object-scheduling-policy), which is in public beta. For the reasons behind these changes, refer to [Changes in Sandbox SDK 1.0](/sandbox/sdk/migrate/changes-in-1-0/). +To move an application from Sandbox SDK 0.x to 1.0, replace the `Sandbox` class with a Durable Object class that you write. Then replace each 0.x feature that your application uses. Your class starts a container that uses the [Durable Object scheduling policy](/containers/configuration/scheduling-policy/#use-the-durable-object-scheduling-policy), which is in public beta. With this policy, your Durable Object chooses the image and instance size each time it starts a container. For the reasons behind these changes, refer to [Changes in Sandbox SDK 1.0](/sandbox/sdk/migrate/changes-in-1-0/). -Containers that use the Durable Object scheduling policy start faster. In an independent benchmark that starts 100 sandboxes at once, the median time from creating a sandbox to running a command in it fell from 4.0 seconds to 648 milliseconds. For the measurements, refer to [Cloudflare Containers, rebuilt to scale agent sandboxes](https://blog.cloudflare.com/faster-agent-sandboxes/). Each `start()` call also chooses its own image, and a deploy does not restart running sandboxes. +Containers that use the Durable Object scheduling policy start faster. For the measurements, refer to [Cloudflare Containers, rebuilt to scale agent sandboxes](https://blog.cloudflare.com/faster-agent-sandboxes/). A deploy does not restart running sandboxes. ## Support for Sandbox SDK 0.x @@ -29,7 +29,7 @@ The migration pages compare 1.0 with 0.12, the last 0.x release. If your applica You cannot undo the deploy that moves your class to `scheduling_policy: "durable_object"`. After that deploy, 0.x code cannot start containers for the class, even after `wrangler rollback`. ::: -You can move every sandbox in one deploy, or run a 1.0 class next to the 0.x class and move sandboxes one at a time. Rehearse either way on a staging Worker first. To choose a way and prepare for the deploy, refer to [Plan the move to Sandbox SDK 1.0](/sandbox/sdk/migrate/plan-the-move/). +You can move every sandbox in one deploy, or run a 1.0 class next to the 0.x class and move sandboxes one at a time. To choose a way, rehearse it, and prepare for the deploy, refer to [Plan the move to Sandbox SDK 1.0](/sandbox/sdk/migrate/plan-the-move/). ## Find the pages your application needs @@ -39,7 +39,8 @@ Every application needs the first two pages in the following table. To find the grep -rhow \ -e getSandbox -e sleepAfter -e keepAlive \ -e envVars -e onStart -e onStop \ - -e onActivityExpired -e createSession \ + -e onActivityExpired \ + -e exec -e execStream -e createSession \ -e readFile -e writeFile -e listFiles -e watch \ -e startProcess -e streamProcessLogs \ -e exposePort -e proxyToSandbox -e wsConnect \ @@ -54,10 +55,11 @@ grep -rhow \ src | sort | uniq -c ``` -The command prints each name it finds and how often it appears. Some names, such as `watch` and `terminal`, can also match code that does not use the SDK. The 0.12 `codex-app-server` template prints: +The command prints each name it finds and how often it appears. Some names, such as `exec`, `watch`, and `terminal`, can also match code that does not use the SDK. The 0.12 `codex-app-server` template prints: ```txt output 1 enableInternet + 8 exec 6 getSandbox 1 gitCheckout 2 interceptHttps diff --git a/src/content/docs/sandbox/sdk/migrate/move-side-by-side.mdx b/src/content/docs/sandbox/sdk/migrate/move-side-by-side.mdx index 65712fcf395..e678f482fa0 100644 --- a/src/content/docs/sandbox/sdk/migrate/move-side-by-side.mdx +++ b/src/content/docs/sandbox/sdk/migrate/move-side-by-side.mdx @@ -20,7 +20,7 @@ Run a Sandbox SDK 1.0 class next to your 0.12 class in the same Worker, and move - Your 1.0 class is written under a new name, such as `SandboxV1`. Choose a name you want to keep, because renaming a class later is another class migration. For the class, refer to [Replace the Sandbox class](/sandbox/sdk/migrate/replace-the-sandbox-class/). - You have a staging copy of the Worker with its own Wrangler configuration file, a different Worker `name`, and a different `name` for each container. -The code on this page copies `/workspace` from the 0.12 container into the 1.0 container as one archive. A stopped 0.12 container has no files to copy. The archive travels base64-encoded in one RPC message, which is limited to 32 MiB. For larger workspaces, save a 0.12 backup and convert it as [Move backups](/sandbox/sdk/migrate/backups/#convert-backups-that-012-created) describes. +The code on this page copies `/workspace` from the 0.12 container into the 1.0 container as one archive. A stopped 0.12 container has no files to copy. The 0.12 Durable Object returns the archive as base64 text in one remote procedure call (RPC), so the encoded archive must fit the [RPC size limit](/workers/runtime-apis/rpc/#limitations). For larger workspaces, save a 0.12 backup and convert it as [Move backups](/sandbox/sdk/migrate/backups/#convert-backups-that-012-created) describes. Keep the 0.12 exports, variables, and secrets that other migration pages tell you to remove, such as `ContainerProxy`, `SANDBOX_TRANSPORT`, and `R2_ACCESS_KEY_ID`, until you delete the 0.12 class. The 0.12 class still reads them. diff --git a/src/content/docs/sandbox/sdk/migrate/plan-the-move.mdx b/src/content/docs/sandbox/sdk/migrate/plan-the-move.mdx index 1f1d7a6b3c4..29cef4270ce 100644 --- a/src/content/docs/sandbox/sdk/migrate/plan-the-move.mdx +++ b/src/content/docs/sandbox/sdk/migrate/plan-the-move.mdx @@ -19,7 +19,7 @@ This page compares the two ways to move sandboxes from Sandbox SDK 0.12 to 1.0, - Your application runs `@cloudflare/sandbox` 0.12.14. - Your 1.0 class is written. For the class, refer to [Replace the Sandbox class](/sandbox/sdk/migrate/replace-the-sandbox-class/). -- You have a staging copy of the Worker with its own Wrangler configuration file. Set a different Worker `name` and a different `name` for each container in that file, because the `--name` option of `wrangler deploy` does not rename container applications. +- You have a staging copy of the Worker with its own Wrangler configuration file. Wrangler deploys each `containers` entry in that file as a container application, which runs the containers of its class. Set a different Worker `name` and a different `name` for each container application in that file, because the `--name` option of `wrangler deploy` does not rename them. ## Choose a path @@ -40,7 +40,7 @@ Rehearse your path on the staging Worker, with sandboxes running in it, before y ## What the switch does to live sandboxes -The switch happens when the deploy that moves your class to `scheduling_policy: "durable_object"` goes live. Each part of a 0.12 sandbox changes as follows: +The switch happens when the deploy that moves your class to `scheduling_policy: "durable_object"` goes live. Your 1.0 class runs its containers in a new container application, and the 0.12 container application stays. The containers of each application hold their own files and processes. Durable Object storage is separate from both applications. Each part of a 0.12 sandbox changes as follows: | Part | At the switch | | --- | --- | @@ -119,7 +119,7 @@ Delete the old container application. Replace `` with its ID The old application runs its containers, and bills for them, until you delete it. Deleting it stops the containers that 0.12 started and deletes their files. Durable Objects and their storage stay. -The keys that 0.12 wrote stay in the storage of each Durable Object. Keep `portTokens`, `tunnels`, `tunnels:meta`, and `OUTBOUND_CONFIGURATION` while your class reads them, as the pages that move preview URLs, tunnels, and outbound rules do. If you delete other keys, name each key in a `delete()` call. Do not call `deleteAll()`, which also deletes your own keys and any 0.12 backup handles that you still need to convert. +Keep the 0.12 keys `portTokens`, `tunnels`, `tunnels:meta`, and `OUTBOUND_CONFIGURATION` in Durable Object storage while your class reads them, as the pages that move preview URLs, tunnels, and outbound rules do. If you delete other 0.12 keys, name each key in a `delete()` call. Do not call `deleteAll()`, which also deletes your own keys and any 0.12 backup handles that you still need to convert. ## Related resources diff --git a/src/content/docs/sandbox/sdk/migrate/replace-the-sandbox-class.mdx b/src/content/docs/sandbox/sdk/migrate/replace-the-sandbox-class.mdx index 386198106bc..919dd2bab5a 100644 --- a/src/content/docs/sandbox/sdk/migrate/replace-the-sandbox-class.mdx +++ b/src/content/docs/sandbox/sdk/migrate/replace-the-sandbox-class.mdx @@ -73,8 +73,8 @@ The steps keep your class name, which moves every sandbox in place. To move sand The new entry makes these changes: - - `scheduling_policy: "durable_object"` lets each Durable Object start its own container. This policy does not accept `image`, `instance_type`, or `max_instances`, so remove them. The `Dockerfile` moves to `images`, and the instance type moves to `start()`. - - The container application gets a new `name`. Without one, the new application takes the name that Wrangler generated for the old application, and the deploy fails after the new Worker version is already live. + - `scheduling_policy: "durable_object"` lets each Durable Object start its own container. This policy does not accept `image`, `instance_type`, or `max_instances`, so remove them. The `Dockerfile` moves to `images`, and the [instance type](/containers/platform/limits/#instance-types), which sets the CPU, memory, and disk of each container, moves to the `instance` option of `start()`. + - Wrangler deploys this entry as a container application, which runs the containers of your class. The application gets a new `name`. Without one, the new application takes the name that Wrangler generated for the old application, and the deploy fails after the new Worker version is already live. - Leave `durable_objects` and your `migrations` or `exports` as they are. The class keeps its name, so it needs no class migration. Also remove `SANDBOX_TRANSPORT`, `SANDBOX_INSTANCE_TIMEOUT_MS`, `SANDBOX_PORT_TIMEOUT_MS`, and `SANDBOX_POLL_INTERVAL_MS` from `vars`, and keep `nodejs_compat`, which 1.0 requires. @@ -181,9 +181,9 @@ The steps keep your class name, which moves every sandbox in place. To move sand } ``` - Call `ensureRunning()` before each use of the container. It runs `startContainer()` once for each new container, and once more when a restarted Durable Object finds the container running. A deploy restarts the Durable Object and can stop setup partway, so every step after `start()` must be safe to run again. `running` is `true` as soon as `start()` returns, so requests that arrive during setup wait for the same `startContainer()` call. If a setup step throws, `startContainer()` stops the container, so no request runs commands in a container that is not set up. + Call `ensureRunning()` before each use of the container. It runs `startContainer()` once for each new container, and once more when a restarted Durable Object finds the container running. A Durable Object [restarts](/sandbox/concepts/lifetime/#the-timeout-does-not-survive-a-restart) when it leaves memory and later runs its constructor again, and its container can keep running. A deploy restarts the Durable Object and can stop setup partway, so every step after `start()` must be safe to run again. `running` is `true` as soon as `start()` returns, so requests that arrive during setup wait for the same `startContainer()` call. If a setup step throws, `startContainer()` stops the container, so no request runs commands in a container that is not set up. - `start()` returns before the container is ready, and the first `exec()` waits for it. The inactivity timeout does not survive a Durable Object restart. The constructor sets it again when a restarted Durable Object finds the container running. The timeout accepts at most 6 hours. If your commands do not need the Internet, set `enableInternet` to `false`. + `start()` returns before the container is ready, and the first `exec()` waits for it. The inactivity timeout does not survive a restart, so the constructor sets it again when the container is running. For the values that the timeout accepts, refer to [`setInactivityTimeout()`](/containers/api/durable-object-container/#setinactivitytimeout). If your commands do not need the Internet, set `enableInternet` to `false`. 7. Replace each class field and `getSandbox()` option that your code sets: @@ -193,7 +193,7 @@ The steps keep your class name, which moves every sandbox in place. To move sand | `keepAlive` | An [alarm](/durable-objects/api/alarms/) that runs more often than the inactivity timeout while work remains. | | `envVars`, `setEnvVars()` | `env` on `start()` for the main process, and `env` on each `exec()` call. Commands do not inherit the `start()` variables, except `PATH`. | | `entrypoint` | `entrypoint` on `start()`, or `CMD` in the image. | - | `enableInternet` | `enableInternet` on `start()`, which every call must pass. 0.12 defaulted to `true`. | + | `enableInternet` | `enableInternet` on `start()`, which every call must pass. | | `labels`, `setLabels()` | `labels` on `start()`. | | `defaultPort`, `requiredPorts` | `this.container.getTcpPort(port)`. Refer to [Move preview URLs](/sandbox/sdk/migrate/preview-urls/). | | `allowedHosts`, `deniedHosts`, `outboundByHost`, `outbound`, `interceptHttps` | Refer to [Move outbound rules](/sandbox/sdk/migrate/outbound-traffic/). | @@ -223,7 +223,7 @@ The steps keep your class name, which moves every sandbox in place. To move sand } ``` - `monitor()` resolves when the main process exits with code `0` or `destroy()` stops the container without a reason. It rejects when the process fails or `destroy()` passes a reason. The call ends when the Durable Object leaves memory, so it misses a stop that happens before the next request. While the call keeps the Durable Object in memory, the inactivity timeout does not start, and the container keeps running past the timeout. For more information, refer to [Sandbox lifetime](/sandbox/concepts/lifetime/#what-stops-an-instance). For a record of every stop, refer to [Run code when a sandbox stops](/sandbox/manage/run-code-when-a-sandbox-stops/). + For when the call resolves and when it rejects, refer to [`monitor()`](/containers/api/durable-object-container/#monitor). The call ends when the Durable Object leaves memory, so it misses a stop that happens before the next request. While the call keeps the Durable Object in memory, the inactivity timeout does not start, and the container keeps running past the timeout. For more information, refer to [Sandbox lifetime](/sandbox/concepts/lifetime/#what-stops-an-instance). For a record of every stop, refer to [Run code when a sandbox stops](/sandbox/manage/run-code-when-a-sandbox-stops/). 9. Move the calls your Worker makes into methods on the class. In 0.12, the Worker calls package methods on the stub that `getSandbox()` returns: From 3a8d72725510d39fc35ff24c73d98cabf0687377 Mon Sep 17 00:00:00 2001 From: Naresh Date: Thu, 8 Oct 2026 14:08:09 +0100 Subject: [PATCH 2/2] [Sandbox] Clarify the second half of the migration The later migration pages used cloudflared, WorkerEntrypoint, props, CA, FUSE, and SquashFS without saying what they are, stated a fixed number of intercept targets and a benchmark figure, and left the API map saying enableInternet has a default. The tunnel cleanup used undeclared shell variables and read only the first page of results, and the OpenCode step clashed with an existing fetch() handler. Define each term at first use, link the intercept limit and the benchmark post instead of numbers, say that every start() passes enableInternet, declare the cleanup variables and page through the results, and route OpenCode from the existing handler. --- .../docs/sandbox/sdk/migrate/api-map.mdx | 6 ++--- .../docs/sandbox/sdk/migrate/backups.mdx | 2 +- .../sandbox/sdk/migrate/bucket-mounts.mdx | 4 ++- .../sandbox/sdk/migrate/code-interpreter.mdx | 4 +-- .../sandbox/sdk/migrate/other-features.mdx | 6 +++-- .../sandbox/sdk/migrate/outbound-traffic.mdx | 6 ++--- .../docs/sandbox/sdk/migrate/preview-urls.mdx | 2 +- .../docs/sandbox/sdk/migrate/tunnels.mdx | 27 +++++++++++++------ 8 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/content/docs/sandbox/sdk/migrate/api-map.mdx b/src/content/docs/sandbox/sdk/migrate/api-map.mdx index 915c5ed508c..9599d5b4abd 100644 --- a/src/content/docs/sandbox/sdk/migrate/api-map.mdx +++ b/src/content/docs/sandbox/sdk/migrate/api-map.mdx @@ -31,7 +31,7 @@ For the steps, refer to [Replace the Sandbox class](/sandbox/sdk/migrate/replace | -------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | `class X extends Sandbox`, `export { Sandbox }` | Removed | `class X extends DurableObject` | Construct the classes of the package from `container`. | | `getSandbox(ns, id, options)` | Native | [`env.NS.getByName(id)`](/durable-objects/api/namespace/#getbyname) | Validate names yourself. | -| `normalizeId`, `setSandboxName()` | Removed | Lowercase names yourself | Needed only when names become hostnames. | +| `normalizeId`, `setSandboxName()` | Removed | Lowercase names yourself | If you passed `normalizeId: true`, lowercase each name to reach the same Durable Object. | | `sleepAfter`, `setSleepAfter()` | Native | [`container.setInactivityTimeout(ms)`](/containers/api/durable-object-container/#setinactivitytimeout) | At most 6 hours. Call it after `start()`, and again in the constructor. | | `keepAlive`, `setKeepAlive()` | Pattern | An [alarm](/durable-objects/api/alarms/) that sends the container a request while work remains | Refer to [Sandbox lifetime](/sandbox/concepts/lifetime/#activity-keeps-an-instance-running). | | `renewActivityTimeout()` | Native | Any request to the Durable Object | The timeout counts from when the Durable Object becomes inactive. | @@ -40,11 +40,11 @@ For the steps, refer to [Replace the Sandbox class](/sandbox/sdk/migrate/replace | `labels`, `setLabels()` | Native | [`container.start({ image, enableInternet, labels })`](/containers/api/durable-object-container/#start) | | | `configure()` | Removed | Options on each call | | | `setEnvVars()`, class `envVars` | Native | `container.start({ env })` or `container.exec(argv, { env })` | `exec()` does not inherit `start()` variables, except `PATH`. | -| Class `entrypoint`, `enableInternet` | Native | `container.start({ entrypoint, enableInternet })` | `enableInternet` defaults to `false`. It defaulted to `true` in 0.12. | +| Class `entrypoint`, `enableInternet` | Native | `container.start({ image, entrypoint, enableInternet })` | Pass `enableInternet` on every call. It defaulted to `true` in 0.12. | | Class `defaultPort`, `requiredPorts` | Native | [`container.getTcpPort(port)`](/containers/api/durable-object-container/#gettcpport) | | | `start()` | Native | [`container.start({ image, enableInternet })`](/containers/api/durable-object-container/#start) | Does not wait for the container. | | `startAndWaitForPorts()`, `waitForPort()` | Pattern | Retry `getTcpPort(port).fetch()` | [Preview a web application](/sandbox/previews/) shows a readiness check. | -| `stop()`, `destroy()` | Native | [`container.signal()`](/containers/api/durable-object-container/#signal), [`container.destroy()`](/containers/api/durable-object-container/#destroy) | | +| `stop()`, `destroy()` | Native | [`container.signal()`](/containers/api/durable-object-container/#signal), [`container.destroy()`](/containers/api/durable-object-container/#destroy) | 0.12 `destroy()` also deleted [tokens and tunnels](#ports-preview-urls-and-tunnels). | | `getState()` | Native | [`container.running`](/containers/api/durable-object-container/#running), [`container.monitor()`](/containers/api/durable-object-container/#monitor) | | | `getContainerPlacementId()` | Pattern | Read `CLOUDFLARE_PLACEMENT_ID` from `/proc/1/environ` with `container.exec()` | The main process of the container has the variable. Commands do not. | | `onStart`, `onStop`, `onError`, `onActivityExpired` | Pattern | Your code around `start()` and `monitor()` | | diff --git a/src/content/docs/sandbox/sdk/migrate/backups.mdx b/src/content/docs/sandbox/sdk/migrate/backups.mdx index 755a994a9e0..5c95a2d06b6 100644 --- a/src/content/docs/sandbox/sdk/migrate/backups.mdx +++ b/src/content/docs/sandbox/sdk/migrate/backups.mdx @@ -13,7 +13,7 @@ products: import { PackageManagers, Steps, WranglerConfig } from "~/components"; -Replace 0.12 `createBackup()` and `restoreBackup()` with Durable Object methods of the same names, built on `DirectoryBackup`. Your Worker calls them with the same options, and backups keep the 0.12 time to live. `DirectoryBackup` saves the directory to the same `BACKUP_BUCKET` binding as a compressed archive, and restores it as ordinary files. 0.12 saved a SquashFS image and mounted it over the directory. Backups that 0.12 created convert to 1.0 backups the first time you restore them. +Replace 0.12 `createBackup()` and `restoreBackup()` with Durable Object methods of the same names, built on `DirectoryBackup`. Your Worker calls them with the same options, and backups keep the 0.12 time to live. `DirectoryBackup` saves the directory to the same `BACKUP_BUCKET` binding as a compressed archive, and restores it as ordinary files. 0.12 saved the directory as a SquashFS image, a compressed read-only filesystem, and mounted the image over the directory. Backups that 0.12 created convert to 1.0 backups the first time you restore them. To save the whole filesystem instead of one directory, use a Container snapshot. For more information, refer to [Save and restore a sandbox with snapshots](/sandbox/files/save-and-restore-a-workspace/). diff --git a/src/content/docs/sandbox/sdk/migrate/bucket-mounts.mdx b/src/content/docs/sandbox/sdk/migrate/bucket-mounts.mdx index e94a42a40e9..8a57d83f0ac 100644 --- a/src/content/docs/sandbox/sdk/migrate/bucket-mounts.mdx +++ b/src/content/docs/sandbox/sdk/migrate/bucket-mounts.mdx @@ -34,6 +34,8 @@ This page replaces `mountBucket()`, `unmountBucket()`, their options, and their && rm -rf /var/lib/apt/lists/* ``` + FUSE lets a process provide a filesystem, and `s3fs` uses it to present the objects in a bucket as files. For what else `S3Mount` needs, refer to [Requirements](/sandbox/reference/s3-mounts/#requirements). + 2. Export `S3Gateway` from the main module of your Worker: ```ts title="src/index.ts" @@ -212,7 +214,7 @@ export class MySandbox extends DurableObject { Remove the `mountData()` calls from the other methods. A path that you mount after `intercept()` does not work, so mount every path that the sandbox needs here. To mount a path later, open a new sandbox. A restarted Durable Object runs `startContainer()` again, and `mount()` reuses a mount that already matches. -Each mount uses one of the [64 intercept targets](/containers/api/durable-object-container/#interceptoutboundhttp) of the container, and `unmount()` does not free it. Move outbound rules uses two. Give each job its own sandbox name instead of mounting a new prefix for each job in one container. +Mounts and the rules from Move outbound rules share the intercept entries of the container, and `unmount()` does not free them. Refer to [Intercept limit](/containers/api/durable-object-container/#intercept-limit). Give each job its own sandbox name instead of mounting a new prefix for each job in one container. ## Replace localBucket diff --git a/src/content/docs/sandbox/sdk/migrate/code-interpreter.mdx b/src/content/docs/sandbox/sdk/migrate/code-interpreter.mdx index b12e2360290..dc2031149ea 100644 --- a/src/content/docs/sandbox/sdk/migrate/code-interpreter.mdx +++ b/src/content/docs/sandbox/sdk/migrate/code-interpreter.mdx @@ -34,6 +34,8 @@ If your code relies on this, or on contexts that you create, follow [Keep Python | `runCode()` with `language: "javascript"` or `"typescript"` | [Run JavaScript in a Dynamic Worker](#run-javascript-in-a-dynamic-worker) | | `runCodeStream()`, `onStdout`, `onStderr`, `onResult` | [Replace streaming output](#replace-streaming-output) | +Variables that 0.12 contexts held stay in the 0.12 container, and 0.12 context IDs are not in the storage of your Durable Object. After the switch, run your setup code again, and create your contexts again before the first `runCode()` call. + ## Keep Python variables between calls A kernel is a Python process that runs the code of each call with IPython, and keeps its variables until it exits. Your Durable Object starts one kernel for each context, on its own port. @@ -473,8 +475,6 @@ A kernel is a Python process that runs the code of each call with IPython, and k -Variables that 0.12 contexts held stay in the 0.12 container. After the switch, run your setup code again, and create your contexts again, because 0.12 context IDs are not in the storage of your Durable Object. - ## Run JavaScript in a Dynamic Worker Run JavaScript in a [Dynamic Worker](/dynamic-workers/) that your Worker loads, so JavaScript needs no container. diff --git a/src/content/docs/sandbox/sdk/migrate/other-features.mdx b/src/content/docs/sandbox/sdk/migrate/other-features.mdx index 85b90427986..13f17f07786 100644 --- a/src/content/docs/sandbox/sdk/migrate/other-features.mdx +++ b/src/content/docs/sandbox/sdk/migrate/other-features.mdx @@ -342,6 +342,8 @@ In 0.12, `createOpencode()` started `opencode serve` in the sandbox, and returne This replaces `proxyToOpencode()`, which made the same redirect. The UI loads its files from `/`, so serve it at the root of a hostname and forward every path to your Durable Object, for example with `env.Sandbox.getByName("my-agent").fetch(request)`. The UI can run commands in the sandbox, so authenticate these requests in your Worker. + If your class already has a `fetch()` handler, for example from [Move terminals](/sandbox/sdk/migrate/terminals/), rename this handler to `serveOpenCode()`, make it `private`, and call it from your `fetch()` handler for the requests that your Worker routes to OpenCode, for example by the hostname of the request. If your class also forwards preview URLs, remove the `X-Sandbox-Port` header in the Worker route that forwards OpenCode requests, as [Forward the WebSocket](/sandbox/sdk/migrate/terminals/#forward-the-websocket) shows. + 5. Call `createSession("Fix tests")` from your Worker. It returns the new session ID, such as `ses_f1822cedcffe1CpcMlwM9eQSGZ`. Open your Worker URL in a browser. It redirects to `/?url=` and loads OpenCode. @@ -560,7 +562,7 @@ Clients written for the bridge API, such as `CloudflareSandboxClient` in the Ope ## Remove WarmPool -Start each sandbox when a request needs it, and remove `WarmPool`. The pool kept 0.x sandboxes started ahead of time, because a 0.x start took seconds. With the Durable Object scheduling policy, the median time from creating a sandbox to running a command in it was 648 milliseconds in an independent benchmark, so a pool saves little time. +Start each sandbox when a request needs it, and remove `WarmPool`. The pool kept 0.x sandboxes started ahead of time, because a 0.x start took seconds. Containers that use the Durable Object scheduling policy start faster, so a pool saves little time. For the measurements, refer to [Cloudflare Containers, rebuilt to scale agent sandboxes](https://blog.cloudflare.com/faster-agent-sandboxes/). @@ -582,7 +584,7 @@ Start each sandbox when a request needs it, and remove `WarmPool`. The pool kept - If your Worker uses `exports`, mark the class as deleted instead: + If your Worker declares its Durable Object classes in the `exports` field of the Wrangler configuration, mark the class as deleted instead: diff --git a/src/content/docs/sandbox/sdk/migrate/outbound-traffic.mdx b/src/content/docs/sandbox/sdk/migrate/outbound-traffic.mdx index 3bea5209905..55c167e65cc 100644 --- a/src/content/docs/sandbox/sdk/migrate/outbound-traffic.mdx +++ b/src/content/docs/sandbox/sdk/migrate/outbound-traffic.mdx @@ -12,7 +12,7 @@ products: import { Steps } from "~/components"; -In Sandbox SDK 0.12, `ContainerProxy` applies the outbound fields of your `Sandbox` class to each request that the container sends. In 1.0, your Durable Object registers your own `WorkerEntrypoint` for HTTP and HTTPS requests from the container, and the entrypoint applies the same rules in the same order. +In Sandbox SDK 0.12, `ContainerProxy` applies the outbound fields of your `Sandbox` class to each request that the container sends. In 1.0, you write a `WorkerEntrypoint`, an exported class in your Worker that handles requests. Your Durable Object registers it as an outbound intercept, which sends HTTP and HTTPS requests from the container to the entrypoint. The entrypoint applies the same rules in the same order. ## Before you start @@ -139,7 +139,7 @@ This page replaces `enableInternet`, `interceptHttps`, `allowedHosts`, `deniedHo The entrypoint blocks denied hostnames, then hostnames outside the allowed list. Next, it runs the first handler that matches, in this order: a hostname handler that the sandbox set at runtime, a hostname handler from your class, the catch-all handler set at runtime, and the catch-all handler of your class. With no handler, it sends the request to the Internet if you set an allowed list or allow the Internet. A blocked request gets status `520` with the body `Origin is disallowed`, as in 0.12. - The props carry the rules that the sandbox set at runtime, in the shape 0.12 stored them. Like 0.12, the entrypoint skips a handler whose name is not in `HANDLERS`, such as the handlers that 0.12 stored for bucket mounts. + Props are values that your Durable Object passes to the entrypoint when it registers the entrypoint. Here, the props carry the rules that the sandbox set at runtime, in the shape 0.12 stored them. Like 0.12, the entrypoint skips a handler whose name is not in `HANDLERS`, such as the handlers that 0.12 stored for bucket mounts. 3. Add the certificate variables to `ENV`, and register the entrypoint in `startContainer()`: @@ -225,7 +225,7 @@ This page replaces `enableInternet`, `interceptHttps`, `allowedHosts`, `deniedHo If your class uses `DirectoryBackup` from [Move backups](/sandbox/sdk/migrate/backups/), call [`this.backups.intercept()`](/sandbox/reference/directory-backups/#intercept) in the `try` block, before your `intercept()`. Otherwise every backup and restore fails with `BACKUP_TRANSFER`, because your `Outbound` entrypoint receives backup requests from the container. Call it only in `startContainer()`, not on every request. - The HTTPS intercept signs responses with a certificate that the container does not trust by default. As in 0.12, the `TRUST` command adds the certificate to the system bundle, which curl and Git read. `ENV` sets `NODE_EXTRA_CA_CERTS` and `REQUESTS_CA_BUNDLE` for Node.js and Python `requests`, which do not read the bundle by default. Keep your other `ENV` values, and pass `ENV` to each `exec()` call. The image needs the `ca-certificates` package, which provides the bundle. + The HTTPS intercept signs responses with a certificate from a certificate authority (CA) that the container does not trust by default. As in 0.12, the `TRUST` command adds the CA certificate to the system bundle, so curl and Git, which read the bundle, trust intercepted HTTPS connections. `ENV` sets `NODE_EXTRA_CA_CERTS` and `REQUESTS_CA_BUNDLE` for Node.js and Python `requests`, which do not read the bundle by default. Keep your other `ENV` values, and pass `ENV` to each `exec()` call. The image needs the `ca-certificates` package, which provides the bundle. The certificate appears once the HTTPS intercept is registered. `TRUST` waits up to 10 seconds for it. 0.12 waited up to 5 seconds. If the certificate does not appear, or the append fails, `trust()` throws, and `startContainer()` stops the container. Each container gets a new certificate. A [snapshot](/containers/api/durable-object-container/#snapshotcontainer) saves the bundle with the certificate of the container it came from, so appending to the bundle would keep certificates from earlier containers. `TRUST` therefore keeps a copy of the system bundle as it was before any certificate was added, and writes the bundle again from that copy and the current certificate. Running it again for the same container writes the same bundle. diff --git a/src/content/docs/sandbox/sdk/migrate/preview-urls.mdx b/src/content/docs/sandbox/sdk/migrate/preview-urls.mdx index e7745dbf07b..be77c5783a2 100644 --- a/src/content/docs/sandbox/sdk/migrate/preview-urls.mdx +++ b/src/content/docs/sandbox/sdk/migrate/preview-urls.mdx @@ -12,7 +12,7 @@ products: import { Steps } from "~/components"; -In Sandbox SDK 0.12, `exposePort()` stores a token for each port in Durable Object storage. `proxyToSandbox()` routes hostnames such as `8080-ada-.example.com` to the server on that port. In 1.0, your Worker routes those hostnames, and your Durable Object checks the token and forwards the request with `getTcpPort()`. The code on this page reads the tokens where 0.12 stored them, so the preview URLs that your users already have keep working. +In Sandbox SDK 0.12, `exposePort()` stores a token for each port in Durable Object storage. The token is a secret in the preview URL, so anyone with the URL can reach the port. `proxyToSandbox()` routes hostnames such as `8080-ada-.example.com` to the server on that port. In 1.0, your Worker routes those hostnames, and your Durable Object checks the token and forwards the request with `getTcpPort()`. The code on this page reads the tokens where 0.12 stored them, so the preview URLs that your users already have keep working. ## Before you start diff --git a/src/content/docs/sandbox/sdk/migrate/tunnels.mdx b/src/content/docs/sandbox/sdk/migrate/tunnels.mdx index e18b90ac75b..097d27b960c 100644 --- a/src/content/docs/sandbox/sdk/migrate/tunnels.mdx +++ b/src/content/docs/sandbox/sdk/migrate/tunnels.mdx @@ -12,7 +12,7 @@ products: import { Steps } from "~/components"; -In Sandbox SDK 0.12, `tunnels.get()` runs `cloudflared` in the container and returns a public URL. A quick tunnel gets a random `trycloudflare.com` URL, and a named tunnel gets a hostname in your account, such as `ada.example.com`. In 1.0, your Durable Object starts `cloudflared` with `exec()`. Named tunnels that 0.12 created stay in your account, and keep their hostnames once `cloudflared` runs again. +In Sandbox SDK 0.12, `tunnels.get()` runs `cloudflared` in the container and returns a public URL. `cloudflared` is the connector for [Cloudflare Tunnel](/cloudflare-one/networks/connectors/cloudflare-tunnel/), and sends requests for that URL to a server in the container. A quick tunnel gets a random `trycloudflare.com` URL, and a named tunnel gets a hostname in your account, such as `ada.example.com`. In 1.0, your Durable Object starts `cloudflared` with `exec()`. Named tunnels that 0.12 created stay in your account, and keep their hostnames once `cloudflared` runs again. Your Worker already routes requests to the container, so most tunnels can become preview URLs instead. For those, refer to [Move preview URLs](/sandbox/sdk/migrate/preview-urls/). @@ -303,14 +303,26 @@ Code in the sandbox can read the run token from the environment of the `cloudfla ## Find tunnels that 0.12 left behind -After an in-place switch, no 0.12 `destroy()` runs, so tunnels and DNS records for sandboxes that no longer exist stay in your account. 0.12 names each tunnel `sandbox--`, and tags it with `createdBy: "sandbox-sdk"` metadata. List those tunnels: +After an in-place switch, no 0.12 `destroy()` runs, so tunnels and DNS records for sandboxes that no longer exist stay in your account. 0.12 names each tunnel `sandbox--`, and tags it with `createdBy: "sandbox-sdk"` metadata. + +The commands in this section run in your shell. Set `ACCOUNT_ID` to the account that holds the 0.12 tunnels, `ZONE_ID` to the zone that holds their DNS records, and `CLOUDFLARE_API_TOKEN` to a token with the Cloudflare Tunnel Edit and DNS Edit permissions. These shell variables are for this cleanup only, and your Worker still does not need an account or zone ID. The `list_pages` function requests each [page of results](/fundamentals/api/how-to/make-api-calls/#pagination) until a page is empty. List those tunnels: ```sh API=https://api.cloudflare.com/client/v4 -curl --get "$API/accounts/$ACCOUNT_ID/cfd_tunnel" \ - --data is_deleted=false --data per_page=100 \ - --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | +list_pages() { + page=1 + while body=$(curl --fail --silent --show-error --get "$@" \ + --data page="$page" \ + --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN") && + [ "$(printf '%s' "$body" | jq '.result | length')" -gt 0 ]; do + printf '%s\n' "$body" + page=$((page + 1)) + done +} + +list_pages "$API/accounts/$ACCOUNT_ID/cfd_tunnel" \ + --data is_deleted=false --data per_page=1000 | jq -r '.result[] | select(.metadata.createdBy == "sandbox-sdk") | "\(.id) \(.name) \(.status)"' @@ -319,9 +331,8 @@ curl --get "$API/accounts/$ACCOUNT_ID/cfd_tunnel" \ 0.12 gives each DNS record the comment `sandbox-`. List those records: ```sh -curl --get "$API/zones/$ZONE_ID/dns_records" \ - --data comment.startswith=sandbox- \ - --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | +list_pages "$API/zones/$ZONE_ID/dns_records" \ + --data comment.startswith=sandbox- --data per_page=1000 | jq -r '.result[] | "\(.id) \(.name) \(.comment)"' ```