withruntime 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/BLAXEL.md +4 -4
  2. package/DAYTONA.md +52 -40
  3. package/E2B.md +65 -62
  4. package/README.md +15 -7
  5. package/README.src.md +15 -7
  6. package/VERCEL.md +34 -22
  7. package/compatibility-lock.json +16 -16
  8. package/dist/blaxel/errors.js +1 -1
  9. package/dist/blaxel/lookup.d.ts +3 -2
  10. package/dist/blaxel/lookup.js +3 -2
  11. package/dist/blaxel/pagination.d.ts +2 -2
  12. package/dist/blaxel/pagination.js +45 -17
  13. package/dist/blaxel/preview.d.ts +2 -1
  14. package/dist/blaxel/preview.js +15 -13
  15. package/dist/blaxel/process.js +56 -10
  16. package/dist/blaxel/sandbox.d.ts +2 -0
  17. package/dist/blaxel/sandbox.js +2 -0
  18. package/dist/blaxel/snapshot.js +3 -5
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +262 -56
  21. package/dist/client.d.ts +41 -3
  22. package/dist/client.js +67 -11
  23. package/dist/cloudflare/identity.d.ts +7 -0
  24. package/dist/cloudflare/identity.js +13 -0
  25. package/dist/cloudflare/index.d.ts +9 -0
  26. package/dist/cloudflare/index.js +47 -9
  27. package/dist/cloudflare/worker.d.ts +4 -0
  28. package/dist/cloudflare/worker.js +166 -16
  29. package/dist/codesandbox/guest-service.js +57 -20
  30. package/dist/codesandbox/index.js +42 -8
  31. package/dist/compat/core.d.ts +9 -8
  32. package/dist/compat/core.js +60 -21
  33. package/dist/compat/files.js +22 -4
  34. package/dist/compat/lease.d.ts +36 -0
  35. package/dist/compat/lease.js +87 -0
  36. package/dist/credentials.d.ts +6 -1
  37. package/dist/credentials.js +107 -38
  38. package/dist/daytona/computer-use.d.ts +162 -0
  39. package/dist/daytona/computer-use.js +324 -0
  40. package/dist/daytona/context.d.ts +10 -0
  41. package/dist/daytona/context.js +24 -0
  42. package/dist/daytona/daytona.d.ts +20 -0
  43. package/dist/daytona/daytona.js +134 -20
  44. package/dist/daytona/filesystem.d.ts +17 -4
  45. package/dist/daytona/filesystem.js +129 -38
  46. package/dist/daytona/index.d.ts +3 -27
  47. package/dist/daytona/index.js +2 -7
  48. package/dist/daytona/process.d.ts +63 -6
  49. package/dist/daytona/process.js +224 -23
  50. package/dist/daytona/sandbox.d.ts +20 -6
  51. package/dist/daytona/sandbox.js +55 -32
  52. package/dist/e2b/client.d.ts +5 -0
  53. package/dist/e2b/client.js +6 -2
  54. package/dist/e2b/commands.d.ts +43 -12
  55. package/dist/e2b/commands.js +217 -64
  56. package/dist/e2b/errors.d.ts +7 -0
  57. package/dist/e2b/errors.js +20 -0
  58. package/dist/e2b/filesystem.d.ts +2 -1
  59. package/dist/e2b/filesystem.js +43 -6
  60. package/dist/e2b/index.d.ts +3 -2
  61. package/dist/e2b/index.js +2 -1
  62. package/dist/e2b/pty.d.ts +24 -0
  63. package/dist/e2b/pty.js +83 -0
  64. package/dist/e2b/sandbox.d.ts +17 -6
  65. package/dist/e2b/sandbox.js +239 -43
  66. package/dist/e2b/users.d.ts +30 -0
  67. package/dist/e2b/users.js +185 -0
  68. package/dist/file-source.d.ts +16 -0
  69. package/dist/file-source.js +162 -0
  70. package/dist/freestyle/index.d.ts +9 -1
  71. package/dist/freestyle/index.js +38 -5
  72. package/dist/index.d.ts +3 -1
  73. package/dist/login.js +16 -10
  74. package/dist/mcp.d.ts +2 -1
  75. package/dist/mcp.js +6 -1
  76. package/dist/modal/index.js +2 -1
  77. package/dist/network-cli.js +139 -43
  78. package/dist/openai-agents/index.js +9 -45
  79. package/dist/page.d.ts +1 -1
  80. package/dist/page.js +2 -2
  81. package/dist/products/browser.d.ts +60 -0
  82. package/dist/products/browser.js +37 -0
  83. package/dist/products/desktop.d.ts +2 -2
  84. package/dist/products/desktop.js +2 -2
  85. package/dist/products/images.d.ts +3 -1
  86. package/dist/products/images.js +14 -2
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/network-products.d.ts +7 -0
  90. package/dist/products/network-products.js +3 -0
  91. package/dist/products/observability.d.ts +44 -0
  92. package/dist/products/observability.js +6 -0
  93. package/dist/products/previews.d.ts +8 -2
  94. package/dist/products/previews.js +11 -3
  95. package/dist/products/private-network.d.ts +22 -0
  96. package/dist/products/private-network.js +10 -0
  97. package/dist/products/volumes.d.ts +42 -0
  98. package/dist/products/volumes.js +36 -0
  99. package/dist/products/watch.js +61 -8
  100. package/dist/proxy.js +3 -1
  101. package/dist/runloop/index.d.ts +13 -27
  102. package/dist/runloop/index.js +149 -52
  103. package/dist/sandbox.d.ts +59 -8
  104. package/dist/sandbox.js +479 -68
  105. package/dist/snapshots.d.ts +9 -4
  106. package/dist/sprites/index.js +69 -33
  107. package/dist/tar.d.ts +3 -1
  108. package/dist/tar.js +140 -14
  109. package/dist/transport.d.ts +4 -7
  110. package/dist/transport.js +205 -122
  111. package/dist/types.d.ts +5 -2
  112. package/dist/vercel/command.d.ts +1 -1
  113. package/dist/vercel/command.js +3 -2
  114. package/dist/vercel/errors.d.ts +1 -1
  115. package/dist/vercel/errors.js +7 -1
  116. package/dist/vercel/index.d.ts +1 -7
  117. package/dist/vercel/index.js +1 -3
  118. package/dist/vercel/sandbox-user.d.ts +70 -0
  119. package/dist/vercel/sandbox-user.js +206 -0
  120. package/dist/vercel/sandbox.d.ts +55 -20
  121. package/dist/vercel/sandbox.js +353 -99
  122. package/dist/vercel/snapshot.d.ts +3 -2
  123. package/dist/vercel/snapshot.js +9 -5
  124. package/package.json +2 -1
package/README.src.md CHANGED
@@ -14,7 +14,7 @@ The client uses `RUNTIME_API_KEY` when it is set, and otherwise the connection
14
14
  this machine saved when the CLI connected it (one browser approval, no key to
15
15
  copy). On a server or a CI runner, set `RUNTIME_API_KEY` from your secret
16
16
  manager: `npx withruntime keys create` prints a new key once after an account
17
- owner approves it in the browser, or create one at
17
+ owner, admin or developer approves it in the browser, or create one at
18
18
  https://withruntime.com/account/keys. Never put a key in browser code, a URL or
19
19
  a command-line argument.
20
20
 
@@ -22,7 +22,8 @@ a command-line argument.
22
22
  import { Sandbox } from "withruntime";
23
23
 
24
24
  await using sbx = await Sandbox.create();
25
- const result = await sbx.exec("python3 -c 'print(6 * 7)'");
25
+ await sbx.files.write("/workspace/invoice.py", "print(sum([125, 250, 375]))\n");
26
+ const result = await sbx.exec("python3 /workspace/invoice.py");
26
27
  console.log(result.exitCode, result.stdout);
27
28
  ```
28
29
 
@@ -32,6 +33,8 @@ Node 22 call `await sbx.stop()`). With no arguments you get the free
32
33
  trial while it lasts ({{trial-hours}} free hours, up to {{trial-sandboxes}} sandboxes running at once), 2 vCPU, 4 GiB of
33
34
  memory and a 4 GiB disk. The current default image includes NumPy, pandas and
34
35
  matplotlib; see the [sandbox environment](https://withruntime.com/docs/sandbox-environment).
36
+ Omitting `funding` can use prepaid credit after the trial is exhausted. Use
37
+ `Sandbox.create({ funding: "trial" })` for free use only; it never falls back to paid.
35
38
 
36
39
  The sandbox object does the rest:
37
40
 
@@ -59,7 +62,9 @@ no compute; `idlePauseSeconds` sets {{idle-pause-min}} to {{idle-pause-max}}, or
59
62
  `Sandbox.getOrCreate(name)` returns the sandbox with that name, woken if it is
60
63
  paused, or creates it.
61
64
 
62
- The client has `sandboxes`, `images`, `volumes`, `snapshots`, `jobs`, `secrets`,
65
+ `runtime.sandbox`, `snapshot`, `image`, `volume`, `job`, `domain`, `port` and
66
+ `address` name the products as the CLI does. Their existing plural names
67
+ remain aliases of the same product clients. The client has `sandboxes`, `images`, `volumes`, `snapshots`, `jobs`, `secrets`,
63
68
  `limits`, `feedback` and `support`; `webhooks`, `events`, `otel` and `audit`
64
69
  to watch the account; `domains`, `ports`, `addresses`, `tunnel` and `network`
65
70
  to connect sandboxes to your own world (paid accounts); and `mcp`, `sso`,
@@ -67,6 +72,8 @@ to connect sandboxes to your own world (paid accounts); and `mcp`, `sso`,
67
72
  the key is read-only and what its agent may still spend today. Every write carries an idempotency key, made for you, so the SDK's
68
73
  own retries (timeouts, 429, 503) never do anything twice. Errors are typed and
69
74
  carry a `code`, a `hint` and a `requestId`.
75
+ `runtime.usageRequests("7d")` reads the account's API call counts and error rates;
76
+ counts are exact decimal strings, and an empty window has a null `errorPercent`.
70
77
 
71
78
  ## Behind a proxy
72
79
 
@@ -89,7 +96,7 @@ Runtime improves fast because agents tell us what they run into: reports go stra
89
96
 
90
97
  ## Code written for E2B
91
98
 
92
- `withruntime/e2b` runs code written for E2B's SDK on Runtime. Change the
99
+ `withruntime/e2b` translates supported E2B sandbox calls to Runtime. Change the
93
100
  import and set `RUNTIME_API_KEY`. The
94
101
  [switch guide](https://withruntime.com/docs/migrate) covers the other providers
95
102
  and how to work out what you save:
@@ -123,7 +130,7 @@ package list every mapping and gap.
123
130
 
124
131
  ## Code written for Blaxel
125
132
 
126
- `withruntime/blaxel` runs code written for Blaxel's sandbox SDK. Change the
133
+ `withruntime/blaxel` translates supported Blaxel sandbox calls. Change the
127
134
  import:
128
135
 
129
136
  ```ts no-run
@@ -134,7 +141,7 @@ Sandboxes get Blaxel's default of 4096 MB, with one vCPU for every 2048 MB.
134
141
  Standby becomes Runtime's pause: after 60 seconds without a call a sandbox
135
142
  pauses with its memory and processes, and the next call wakes it. Envs,
136
143
  processes by name, files, previews, snapshots, forks and the code interpreter
137
- work as they do on Blaxel, and so do sessions: `sandbox.sessions` makes a
144
+ are supported by the adapter, along with sessions: `sandbox.sessions` makes a
138
145
  Runtime sandbox session and `SandboxInstance.fromSession` drives the sandbox
139
146
  with its token. A Blaxel key is never sent anywhere. Drives, codegen, schedules and Blaxel's agent, model and MCP hosting throw
140
147
  `NotSupportedError` before anything happens, naming what to use instead.
@@ -154,7 +161,8 @@ https://withruntime.com/docs/frameworks.
154
161
  ## CLI
155
162
 
156
163
  ```bash no-run
157
- npx withruntime sandbox run -- python3 -c 'print(6 * 7)' # connects on first use
164
+ printf '%s\n' 'print(sum([125, 250, 375]))' > invoice.py
165
+ npx withruntime sandbox run -- python3 - < invoice.py # connects on first use
158
166
  npx withruntime sandbox create
159
167
  npx withruntime help
160
168
  ```
package/VERCEL.md CHANGED
@@ -16,9 +16,9 @@ from withruntime.vercel import sandbox # was: from vercel import sandbox
16
16
  from withruntime.vercel.sandbox import sync as sandbox # was: from vercel.sandbox import sync as sandbox
17
17
  ```
18
18
 
19
- The mapping was written against `@vercel/sandbox` 3.5.0 and `vercel-sandbox`
19
+ The mapping was written against `@vercel/sandbox` 3.5.1 and `vercel-sandbox`
20
20
  0.7.0 on PyPI (the `vercel.sandbox` module of `vercel` 0.11.4), checked
21
- 23 September 2026.
21
+ 30 September 2026.
22
22
 
23
23
  ## Keys
24
24
 
@@ -34,7 +34,10 @@ are ignored.
34
34
  - **Machine:** Vercel's default, 2 vCPUs with 2048 MiB each.
35
35
  `resources: { vcpus }` sets another, with 2048 MiB per vCPU.
36
36
  - **Timeout:** Vercel's default of 5 minutes. Runtime's leases run 60 seconds to
37
- an hour; `extendTimeout` moves the end later, as often as needed.
37
+ an hour. A longer `timeout` (Python `execution_time_limit`) starts with an
38
+ hour's lease, and the sandbox object renews it toward the timeout while it
39
+ lives, never past it; once the program ends, the sandbox pauses within an
40
+ hour. `extendTimeout` moves the end later, as often as needed.
38
41
  - **Persistence:** persistent by default, as in Vercel. `stop()` and the end of
39
42
  the timeout pause the sandbox, keeping its files and also its memory and
40
43
  processes. `Sandbox.get({ name })`, or the next command or file call, wakes
@@ -63,6 +66,8 @@ are ignored.
63
66
  | `source: { type: "tarball", url }`, `{ type: "snapshot" }` | Downloaded and unpacked into the working directory; a Runtime snapshot, with its own shape. |
64
67
  | `networkPolicy`, `updateNetworkPolicy` | Runtime network rules: `allow-all`, `deny-all`, or a list of domains and subnets to allow and deny. |
65
68
  | `Sandbox.get({ name })`, `getOrCreate`, `list({ tags })` | The newest live sandbox with that name (or a Runtime id), woken; create when missing; `sandboxes.list` with Vercel's paginator. |
69
+ | `list({ namePrefix, since, until, sortBy, sortOrder, cursor })` | Applied here over every sandbox the key sees; the cursor continues the same order. Python's `query_sandboxes` does the same. |
70
+ | `createUser`, `asUser`, `createGroup`, `addUserToGroup` | `useradd`, `groupadd` and `usermod` with `sudo`, as Vercel does; a user's commands run under `sudo -u` in its home, its files given to it. |
66
71
  | `Sandbox.fork({ sourceSandbox, name })` | A Runtime fork, which copies memory and processes too. |
67
72
  | `runCommand(cmd, args)`, `runCommand({ cmd, args, cwd, env, sudo })` | `exec` of the argv without a shell. `sudo` runs it under `sudo --preserve-env`. A non-zero exit is a result. Past `timeoutMs` it exits 137. |
68
73
  | `stdout`, `stderr` writers, `detached: true`, `logs()`, `wait()` | Output streams to the writers; a detached command is a Runtime process, its logs followed from the start. |
@@ -72,8 +77,8 @@ are ignored.
72
77
  | Paths | Relative paths resolve from `/vercel/sandbox`, which is `/workspace`. A command that names `/vercel/sandbox` finds a link to it. |
73
78
  | `domain(port)`, `update({ ports })` | The public preview of that port; ports added or removed. |
74
79
  | `stop()`, `delete()`, `extendTimeout(ms)` | `pause` (persistent) or `stop`; `stop`; `extend`. |
75
- | `update({ timeout, networkPolicy, snapshotExpiration })` | `extend` (later only), network rules, retention. |
76
- | `snapshot()`, `Snapshot.get`, `Snapshot.list`, `snapshot.delete()` | A Runtime snapshot (files, memory and processes), then the sandbox stops, as in Vercel; `snapshots.get`, `list`, `delete`. |
80
+ | `update({ timeout, networkPolicy, snapshotExpiration })` | A new timeout (earlier only while the lease has not reached it), network rules, retention. |
81
+ | `snapshot()`, `Snapshot.get`, `Snapshot.list`, `snapshot.delete()` | A verified disk-only capture, then the source stops; restored snapshots start fresh processes; `snapshots.get`, `list`, `delete`. |
77
82
  | Error classes | `APIError` with a `response` whose status is Runtime's and `json` of `{ error: { code, message } }`; also `code`, `hint` and `requestId`. |
78
83
  | Python: `create_sandbox`, `run_process`, `create_process`, `box.fs` | The same mapping; operations await or work as context managers, which stop and by default destroy the sandbox; readers iterate by line. |
79
84
 
@@ -82,21 +87,30 @@ are ignored.
82
87
  Each of these throws `NotSupportedError` before anything happens. Its
83
88
  `feature` names the gap and its `alternative` says what to use.
84
89
 
85
- | Vercel Sandbox | Use instead |
86
- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
87
- | An `image` with no Runtime image of that name | Build it: `npx withruntime image build --dockerfile Dockerfile --name <image>`. |
88
- | `timeout` over one hour | Create with up to an hour, then `extendTimeout` before it ends. |
89
- | Drives (`mounts`), `networkId` | Runtime volumes through `withruntime: { create: { volumes } }`; network rules. |
90
- | A region outside the US, `failoverRegions` | Runtime runs in one US region. |
91
- | Network rules that transform or forward requests | Allow the domain and store the credential as a Runtime secret for it: the sandbox sees a placeholder. |
92
- | `openInteractive` | `sandbox.withruntime.terminal(...)` or `npx withruntime sandbox shell <id>`. |
93
- | Extra users and groups (`createUser`, `asUser`) | `sudo useradd` and `sudo -u` through `runCommand`. |
94
- | Sessions (`currentSession`, `listSessions`) | A Runtime sandbox is its own session. |
95
- | `update` of tags, resources, persistence or region | Create a new sandbox with them. |
96
- | Overrides on `Sandbox.fork` | Fork, then change what Runtime can change. |
97
- | `delete({ deleteOrphanSnapshots: true })`, `Snapshot.tree` | Delete snapshots one by one; `Snapshot.list`. |
98
- | `Drive`, `SandboxUser`, `defineSandboxProxy` | Runtime volumes; `sudo`; `sandbox.withruntime.previews`. |
99
- | Listing by time, prefix or cursor (JavaScript) | List them all and filter the result yourself. (Python applies a name prefix and newest-first order itself.) |
90
+ | Vercel Sandbox | Use instead |
91
+ | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
92
+ | An `image` with no Runtime image of that name | Build it: `npx withruntime image build --dockerfile Dockerfile --name <image>`. |
93
+ | Drives (`mounts`), `networkId` | Runtime volumes through `withruntime: { create: { volumes } }`; network rules. |
94
+ | A region outside the US, `failoverRegions` | Runtime runs in one US region. |
95
+ | Network rules that transform or forward requests | Allow the domain and store the credential as a Runtime secret for it: the sandbox sees a placeholder. |
96
+ | A `timeout` earlier than the current lease's end | Stop the sandbox when you are done with it. |
97
+ | Sessions (`currentSession`, `listSessions`) | A Runtime sandbox is its own session. |
98
+ | `openInteractive` | `sandbox.withruntime.terminal(...)` or `npx withruntime sandbox shell <id>`. |
99
+ | `update` of tags, resources, persistence or region | Create a new sandbox with them. |
100
+ | Overrides on `Sandbox.fork` | Fork, then change what Runtime can change. |
101
+ | `delete({ deleteOrphanSnapshots: true })`, `Snapshot.tree` | Delete snapshots one by one; `Snapshot.list`. |
102
+ | `Drive`, `defineSandboxProxy` | Runtime volumes; `sandbox.withruntime.previews`. |
103
+ | `keepLastSnapshots` | Manage saved snapshots with `Snapshot.list` and `snapshot.delete`. |
104
+
105
+ Explicit snapshots require the released native disk-capture capability; a server
106
+ that does not confirm disk-only capture is refused. If capture succeeds but the
107
+ source cannot finish stopping, the error retains `snapshotId` and
108
+ `sourceSandboxId` so the saved filesystem remains recoverable. JavaScript exposes
109
+ them in `error.json.error.details` for API errors and directly on other errors;
110
+ Python exposes them in `error.data` for API errors and directly on other errors.
111
+ `snapshot_source_stop_timeout` means the source is still stopping after its wait;
112
+ a failed stop request keeps its original error code. These contracts remain
113
+ partial; controlled package comparisons do not prove hosted restoration.
100
114
 
101
115
  Some differences are not refusals:
102
116
 
@@ -106,8 +120,6 @@ Some differences are not refusals:
106
120
  `pwd` prints `/workspace`.
107
121
  - Runtime's stock image is Ubuntu 24.04 with Python 3.12, Node.js 24 and Bun;
108
122
  Vercel's `universal` has Python 3.14.
109
- - `keepLastSnapshots` is accepted: a paused Runtime sandbox keeps exactly its
110
- latest state.
111
123
  - A git source is cloned into the working directory itself.
112
124
 
113
125
  ## Tests
@@ -9,9 +9,9 @@
9
9
  {
10
10
  "registry": "npm",
11
11
  "name": "e2b",
12
- "version": "2.51.0",
12
+ "version": "2.52.0",
13
13
  "artifacts": {
14
- "2.51.0": "sha512-P+50lx1nOYpgCFyE1xuz6JVN/TqJivMUiH7v+RW0CUSfb4+NCu8IkC5KruT2sNPgp6aexddQQ77+OZKGpZZJ+Q=="
14
+ "2.52.0": "sha512-TmUZlAvQq8BbguodpA+BBdawR/x5uILMQtHcLkdaLUqevsDjDdh46IYYU8ZRYKsQ4OhWr4xhsFIPSg1kgf+O0w=="
15
15
  }
16
16
  },
17
17
  {
@@ -25,19 +25,19 @@
25
25
  {
26
26
  "registry": "pypi",
27
27
  "name": "e2b",
28
- "version": "2.51.0",
28
+ "version": "2.52.0",
29
29
  "artifacts": {
30
- "e2b-2.51.0-py3-none-any.whl": "sha256-951d0fd069f017e760b338a435bdb83589b4bfb9574fc5203a7f777e9cb994c2",
31
- "e2b-2.51.0.tar.gz": "sha256-902ee861b30c8ec3e46e7f84db8c993f30695872c7b4490efaad1627ec6e08ba"
30
+ "e2b-2.52.0-py3-none-any.whl": "sha256-6f4b5084b09986db43e99c7405bae6b14dc56984a69bbb1dc0687f1c71515cfb",
31
+ "e2b-2.52.0.tar.gz": "sha256-58c0042609513799341d4c1837e2581a4a5e73d3d62c9f1c57495c5e23eedee2"
32
32
  }
33
33
  },
34
34
  {
35
35
  "registry": "pypi",
36
36
  "name": "e2b-code-interpreter",
37
- "version": "2.10.0",
37
+ "version": "2.10.1",
38
38
  "artifacts": {
39
- "e2b_code_interpreter-2.10.0-py3-none-any.whl": "sha256-6ba776122086261875bc6033ef2dc42ac6b6e22cefdffdfd19d30e93bb09786f",
40
- "e2b_code_interpreter-2.10.0.tar.gz": "sha256-57fb7f9b9ad99ebbdf407619786fdb84e31537c2fbc8358c394aa0f0cbc1af28"
39
+ "e2b_code_interpreter-2.10.1-py3-none-any.whl": "sha256-f4c9732699ee4b1c289acedac8bff66fccecc6ab374bf0b4ef88d5ad063e333d",
40
+ "e2b_code_interpreter-2.10.1.tar.gz": "sha256-94e504667788c6cbb175e5deaf1f68e8df474b670343fe1d72e2496b49d5a666"
41
41
  }
42
42
  }
43
43
  ]
@@ -128,18 +128,18 @@
128
128
  {
129
129
  "registry": "npm",
130
130
  "name": "@blaxel/core",
131
- "version": "0.3.24",
131
+ "version": "0.3.25",
132
132
  "artifacts": {
133
- "0.3.24": "sha512-zF1S7vx+mYHDbK9F4FQbdobz3N+YIlNqHCyOC7R7QpbQ4nP9laeCuQRaN1OTfJVZcRwFvFQ5Nx1lh0su+esd7g=="
133
+ "0.3.25": "sha512-K6aGUyIpgrPnkpwMOI/U/gbZwveOMu1Am04pkawZQjkp7NUa/roG0HC6NJPBp3AVVJY3CQtkGSWpAJ2DV4ZKpw=="
134
134
  }
135
135
  },
136
136
  {
137
137
  "registry": "pypi",
138
138
  "name": "blaxel",
139
- "version": "0.4.12",
139
+ "version": "0.4.13",
140
140
  "artifacts": {
141
- "blaxel-0.4.12-py3-none-any.whl": "sha256-9ca08aa0c1c5aaac87055f684eae002365c55010eaa008d5dc36c552e153210a",
142
- "blaxel-0.4.12.tar.gz": "sha256-58dc5c9aea5b21aa65538f811c7de0b24e8e7a411bdf64e374a295eb1b9cb15e"
141
+ "blaxel-0.4.13-py3-none-any.whl": "sha256-b8817718ca63a2b31a5026f98c7191d5ed8f2f4c1aafc2d4aa57491a560a2146",
142
+ "blaxel-0.4.13.tar.gz": "sha256-a036256792e85238724b8bce38fb7c0b4f661fe4c3ac576258a8be1736ac8483"
143
143
  }
144
144
  }
145
145
  ]
@@ -225,10 +225,10 @@
225
225
  {
226
226
  "registry": "pypi",
227
227
  "name": "prime-sandboxes",
228
- "version": "0.4.0",
228
+ "version": "0.4.1",
229
229
  "artifacts": {
230
- "prime_sandboxes-0.4.0-py3-none-any.whl": "sha256-2a15263a30f3a3a27307c83701b92317af83ec8df0968e910100ccdb557cbb6b",
231
- "prime_sandboxes-0.4.0.tar.gz": "sha256-a6934a5f5fce8342dbe9ade9816242e893ebf23b83a4fa5fda5b0ddbce651f27"
230
+ "prime_sandboxes-0.4.1-py3-none-any.whl": "sha256-1a1bb96afacc66d690815be82c0b7af180ea141421b7d180b4a0adbada4f38f0",
231
+ "prime_sandboxes-0.4.1.tar.gz": "sha256-53f7577dc9070e3b8e30db360f9e3cce3bcc35f8765b26f24f64dbc3c7763b06"
232
232
  }
233
233
  }
234
234
  ]
@@ -95,7 +95,7 @@ const BLAXEL_HINTS = {
95
95
  sandbox_paused: "Call sandbox.unarchive(), then try again.",
96
96
  not_running: "The sandbox is not running: call sandbox.unarchive() if it was archived, or make a new one with SandboxInstance.create if it was deleted.",
97
97
  trial_busy: "The trial's sandboxes are all in use: delete one you no longer need (sandbox.delete()) or archive it (sandbox.archive()), then try again. Moving to paid credit is the account owner's decision.",
98
- public_preview_not_allowed: "On the trial, share the port privately: sandbox.previews.create({ metadata: { name }, spec: { port, public: false } }) and a token from preview.tokens.create(expiresAt). A public preview needs a paid sandbox, which is the account owner's decision.",
98
+ public_preview_not_allowed: "On the trial, share the port privately: sandbox.previews.create({ metadata: { name }, spec: { port, public: false } }) and a token from preview.tokens.create(expiresAt) on a server supporting absolute preview deadlines. A public preview needs a paid sandbox, which is the account owner's decision.",
99
99
  busy: AGAIN,
100
100
  guest_busy: AGAIN,
101
101
  rate_limited: AGAIN,
@@ -32,6 +32,7 @@ export declare function findSandbox(client: Runtime, name: string): Promise<Runt
32
32
  export declare function findByExternalId(client: Runtime, externalId: string): Promise<RuntimeSandbox>;
33
33
  /** Runs a create or fork that names a sandbox. When the name is still held
34
34
  * by a sandbox that is stopping (deleted a moment ago, by this client or
35
- * another), waits for it to stop, up to 30 seconds, and runs it once more, as
36
- * Blaxel's createIfNotExists waits out a sandbox being deleted. */
35
+ * another), waits for it to stop, up to 30 seconds, and runs it once more.
36
+ * This reconciles Runtime's stopping-name conflicts. Blaxel 0.3.25 delegates
37
+ * createIfNotExists reconciliation to its control plane without client polling. */
37
38
  export declare function whenNameFree<T>(client: Runtime, work: () => Promise<T>): Promise<T>;
@@ -54,8 +54,9 @@ export async function findByExternalId(client, externalId) {
54
54
  }
55
55
  /** Runs a create or fork that names a sandbox. When the name is still held
56
56
  * by a sandbox that is stopping (deleted a moment ago, by this client or
57
- * another), waits for it to stop, up to 30 seconds, and runs it once more, as
58
- * Blaxel's createIfNotExists waits out a sandbox being deleted. */
57
+ * another), waits for it to stop, up to 30 seconds, and runs it once more.
58
+ * This reconciles Runtime's stopping-name conflicts. Blaxel 0.3.25 delegates
59
+ * createIfNotExists reconciliation to its control plane without client polling. */
59
60
  export async function whenNameFree(client, work) {
60
61
  try {
61
62
  return await work();
@@ -28,7 +28,7 @@ export type PaginatedList<T, TQuery extends CursorPaginationQuery = CursorPagina
28
28
  /** Blaxel's own helper: the items of a list answer in any of its shapes. */
29
29
  export declare function unwrapListData<T>(response: ListResponse<T>): T[];
30
30
  /** Blaxel's own helper, for a list fetched by `fetchPage`. */
31
- export declare function createPaginatedList<TRaw, TItem, TQuery extends CursorPaginationQuery>({ response, fetchPage, mapItem, query, }: {
31
+ export declare function createPaginatedList<TRaw, TItem, TQuery extends CursorPaginationQuery>({ response, fetchPage, mapItem, query, seenCursors, }: {
32
32
  response: ListResponse<TRaw>;
33
33
  fetchPage: (query?: TQuery) => Promise<ListResponse<TRaw>>;
34
34
  mapItem: (item: TRaw) => TItem | Promise<TItem>;
@@ -36,4 +36,4 @@ export declare function createPaginatedList<TRaw, TItem, TQuery extends CursorPa
36
36
  seenCursors?: Set<string>;
37
37
  }): Promise<PaginatedList<TItem, TQuery>>;
38
38
  /** A Runtime page, each item mapped, as Blaxel's paginated list. */
39
- export declare function paginate<TRaw, TItem>(page: Page<TRaw>, map: (item: TRaw) => TItem): PaginatedList<TItem>;
39
+ export declare function paginate<TRaw, TItem>(page: Page<TRaw>, map: (item: TRaw) => TItem, seenCursors?: Set<string>): PaginatedList<TItem>;
@@ -8,30 +8,58 @@ export function unwrapListData(response) {
8
8
  return response.data ?? [];
9
9
  }
10
10
  /** Blaxel's own helper, for a list fetched by `fetchPage`. */
11
- export async function createPaginatedList({ response, fetchPage, mapItem, query, }) {
12
- const meta = !response || Array.isArray(response) ? {} : (response.meta ?? {});
11
+ export async function createPaginatedList({ response, fetchPage, mapItem, query, seenCursors, }) {
12
+ const meta = !response || Array.isArray(response)
13
+ ? { hasMore: false }
14
+ : (response.meta ?? { hasMore: false });
13
15
  const data = await Promise.all(unwrapListData(response).map(mapItem));
14
- return build(data, meta.nextCursor || undefined, async (cursor) => createPaginatedList({
15
- response: await fetchPage({ ...(query ?? {}), cursor }),
16
- fetchPage,
17
- mapItem,
18
- ...(query ? { query: { ...query, cursor } } : {}),
19
- }));
16
+ const cursors = new Set(seenCursors);
17
+ if (query?.cursor)
18
+ cursors.add(query.cursor);
19
+ return build(data, meta.nextCursor || undefined, async (cursor) => {
20
+ if (cursors.has(cursor))
21
+ throw new Error("Pagination returned a repeated cursor");
22
+ const nextQuery = { ...(query ?? {}), cursor };
23
+ const nextSeenCursors = new Set(cursors);
24
+ nextSeenCursors.add(cursor);
25
+ return createPaginatedList({
26
+ response: await fetchPage(nextQuery),
27
+ fetchPage,
28
+ mapItem,
29
+ query: nextQuery,
30
+ seenCursors: nextSeenCursors,
31
+ });
32
+ }, meta);
20
33
  }
21
34
  /** A Runtime page, each item mapped, as Blaxel's paginated list. */
22
- export function paginate(page, map) {
23
- return build(page.data.map(map), page.nextCursor ?? undefined, async () => {
24
- const next = await guard(() => page.next());
25
- return next ? paginate(next, map) : null;
35
+ export function paginate(page, map, seenCursors = new Set()) {
36
+ return build(page.data.map(map), page.nextCursor ?? undefined, async (cursor) => {
37
+ if (cursor && seenCursors.has(cursor))
38
+ throw new Error("Pagination returned a repeated cursor");
39
+ const nextSeenCursors = new Set(seenCursors);
40
+ if (cursor)
41
+ nextSeenCursors.add(cursor);
42
+ const next = await guard(() => page.next(cursor));
43
+ return next ? paginate(next, map, nextSeenCursors) : null;
26
44
  });
27
45
  }
28
- function build(data, nextCursor, more) {
46
+ function build(data, nextCursor, more, meta = {
47
+ hasMore: nextCursor !== undefined,
48
+ ...(nextCursor ? { nextCursor } : {}),
49
+ }) {
29
50
  const list = {
30
51
  data,
31
- meta: { hasMore: nextCursor !== undefined, ...(nextCursor ? { nextCursor } : {}) },
32
- hasMore: nextCursor !== undefined,
33
- ...(nextCursor ? { nextCursor } : {}),
34
- nextPage: async () => (nextCursor ? more(nextCursor) : null),
52
+ meta,
53
+ get hasMore() {
54
+ return Boolean(list.nextCursor);
55
+ },
56
+ get nextCursor() {
57
+ return meta.nextCursor || undefined;
58
+ },
59
+ nextPage: async () => {
60
+ const cursor = list.nextCursor;
61
+ return cursor ? more(cursor) : null;
62
+ },
35
63
  async autoPagingEach(onItem) {
36
64
  for await (const item of list)
37
65
  if ((await onItem(item)) === false)
@@ -67,7 +67,8 @@ export declare class SandboxPreviewTokens {
67
67
  constructor(preview: SandboxPreview, ctx: PreviewContext);
68
68
  get previewName(): string;
69
69
  get resourceName(): string;
70
- /** A token that lasts until `expiresAt` (at least a minute, at most a week). */
70
+ /** A token bounded by `expiresAt`; native signing floors milliseconds and
71
+ * caps it to the caller's session. Older servers refuse without a TTL fallback. */
71
72
  create(expiresAt: Date): Promise<SandboxPreviewToken>;
72
73
  list(): Promise<never>;
73
74
  /** Revokes the preview's tokens. Runtime cannot revoke one token alone, so
@@ -1,8 +1,5 @@
1
1
  import { guard, isNotFound, NotSupportedError, responseError } from "./errors.js";
2
2
  import { PREVIEW } from "./lookup.js";
3
- /** Runtime's shortest and longest token, in seconds. */
4
- const TOKEN_MIN = 60;
5
- const TOKEN_MAX = 7 * 86_400;
6
3
  function checkName(name) {
7
4
  if (!/^[A-Za-z0-9._-]{1,48}$/.test(name))
8
5
  throw new NotSupportedError(`The preview name "${name}"`, "Use 1 to 48 letters, digits, dots, dashes or underscores.");
@@ -56,19 +53,24 @@ export class SandboxPreviewTokens {
56
53
  get resourceName() {
57
54
  return this.#ctx.sandboxName;
58
55
  }
59
- /** A token that lasts until `expiresAt` (at least a minute, at most a week). */
56
+ /** A token bounded by `expiresAt`; native signing floors milliseconds and
57
+ * caps it to the caller's session. Older servers refuse without a TTL fallback. */
60
58
  async create(expiresAt) {
61
- const seconds = Math.ceil((expiresAt.getTime() - Date.now()) / 1000);
62
- if (seconds > TOKEN_MAX)
63
- throw new NotSupportedError("A preview token lasting more than a week", "Create one for at most seven days, and a new one when it runs out.");
64
- const port = this.#preview.spec.port;
65
- const got = await this.#ctx.run((runtime) => runtime.previews.get(port, Math.max(TOKEN_MIN, seconds)));
59
+ const requested = expiresAt.getTime();
60
+ if (!Number.isFinite(requested))
61
+ throw responseError(400, "A preview token needs a valid expiration date.");
62
+ const remaining = requested - Date.now();
63
+ if (remaining < 60_000 || remaining > 7 * 86_400_000)
64
+ throw new NotSupportedError("A preview token lasting less than a minute or more than a week", "Request an expiration at least one minute and at most seven days away.");
65
+ const got = await this.#ctx.run((runtime) => runtime.previews.get(this.#preview.spec.port, undefined, {
66
+ expiresAt: expiresAt.toISOString(),
67
+ }));
68
+ const actual = got.tokenExpiresAt === null ? Number.NaN : Date.parse(got.tokenExpiresAt);
69
+ if (!got.token || !Number.isFinite(actual) || actual > requested)
70
+ throw new NotSupportedError("A preview token with the requested expiration", "The server must issue a token that never outlives the requested expiration; no duration-based fallback was requested.");
66
71
  return new SandboxPreviewToken({
67
72
  metadata: { name: `token-${Date.now()}`, previewName: this.previewName },
68
- spec: {
69
- ...(got.token ? { token: got.token } : {}),
70
- ...(got.tokenExpiresAt ? { expiresAt: got.tokenExpiresAt } : {}),
71
- },
73
+ spec: { token: got.token, expiresAt: got.tokenExpiresAt },
72
74
  });
73
75
  }
74
76
  list() {
@@ -152,14 +152,17 @@ export class SandboxProcess {
152
152
  }
153
153
  /** A call that found the sandbox paused (autoWake off) is made again once,
154
154
  * after a wake: it never started, so it cannot run twice. */
155
- async #retryPaused(work) {
155
+ async #retryPaused(work, signal) {
156
+ signal?.throwIfAborted();
156
157
  try {
157
158
  return await work();
158
159
  }
159
160
  catch (error) {
161
+ signal?.throwIfAborted();
160
162
  if (codeOf(error) !== "sandbox_paused")
161
163
  throw translate(error);
162
164
  await this.#ctx.wake();
165
+ signal?.throwIfAborted();
163
166
  return guard(work);
164
167
  }
165
168
  }
@@ -281,16 +284,20 @@ export class SandboxProcess {
281
284
  };
282
285
  }
283
286
  /** A process by pid or name: the registry first, then Runtime's list. */
284
- async #find(identifier) {
287
+ async #find(identifier, signal) {
288
+ signal?.throwIfAborted();
285
289
  const runtime = await this.#ctx.live();
290
+ signal?.throwIfAborted();
286
291
  const registry = this.#registry;
287
292
  const known = registry.byName.get(identifier) ?? (registry.meta.has(identifier) ? identifier : undefined);
288
293
  if (known !== undefined) {
289
- const process = await this.#retryPaused(() => runtime.processes.get(known));
294
+ const process = await this.#retryPaused(() => runtime.processes.get(known, { signal }), signal);
295
+ signal?.throwIfAborted();
290
296
  registry.processes.set(known, process);
291
297
  return { id: known, info: process.info, process };
292
298
  }
293
- const all = await this.#retryPaused(() => runtime.processes.list());
299
+ const all = await this.#retryPaused(() => runtime.processes.list({ signal }), signal);
300
+ signal?.throwIfAborted();
294
301
  let found;
295
302
  for (const info of all) {
296
303
  const parsed = parseProcessLine(info.command);
@@ -323,7 +330,9 @@ export class SandboxProcess {
323
330
  const running = found.info.state === "running";
324
331
  if (running && !untilExit && found.info.outputBytes <= found.info.firstOffset)
325
332
  return out;
333
+ signal?.throwIfAborted();
326
334
  const runtime = await this.#ctx.live();
335
+ signal?.throwIfAborted();
327
336
  const abort = new AbortController();
328
337
  const forward = () => abort.abort();
329
338
  signal?.throwIfAborted();
@@ -369,9 +378,30 @@ export class SandboxProcess {
369
378
  /** The process, with the output it has written so far. */
370
379
  async get(identifier, options = {}) {
371
380
  options.signal?.throwIfAborted();
372
- const found = await this.#find(identifier);
373
- const out = await this.#output(found, false, options.signal);
374
- return this.#settled(this.#response(found.info, this.#registry.meta.get(found.id), out));
381
+ const signal = options.signal;
382
+ let onAbort;
383
+ const interrupted = new Promise((_, reject) => {
384
+ onAbort = () => reject(new Error("Process read was interrupted.", { cause: signal?.reason }));
385
+ signal?.addEventListener("abort", onAbort, { once: true });
386
+ });
387
+ try {
388
+ const reading = (async () => {
389
+ const found = await this.#find(identifier, signal);
390
+ const out = await this.#output(found, false, signal);
391
+ signal?.throwIfAborted();
392
+ return this.#settled(this.#response(found.info, this.#registry.meta.get(found.id), out));
393
+ })();
394
+ return await Promise.race([reading, interrupted]);
395
+ }
396
+ catch (error) {
397
+ if (signal?.aborted)
398
+ throw signal.reason ?? error;
399
+ throw error;
400
+ }
401
+ finally {
402
+ if (onAbort)
403
+ signal?.removeEventListener("abort", onAbort);
404
+ }
375
405
  }
376
406
  /** Waits for the process to end (at most `maxWait` ms, -1 for no limit) and
377
407
  * answers it with its output. The process is never stopped by the wait. */
@@ -390,10 +420,24 @@ export class SandboxProcess {
390
420
  signal?.throwIfAborted();
391
421
  signal?.addEventListener("abort", forward, { once: true });
392
422
  const timer = maxWait === -1 ? undefined : setTimeout(() => abort.abort(late()), maxWait);
423
+ let onAbort;
424
+ const interrupted = new Promise((_, reject) => {
425
+ onAbort = () => {
426
+ const reason = abort.signal.reason;
427
+ reject(reason instanceof Error
428
+ ? reason
429
+ : new Error("Process wait was interrupted.", { cause: reason }));
430
+ };
431
+ abort.signal.addEventListener("abort", onAbort, { once: true });
432
+ });
393
433
  try {
394
- const found = await this.#find(identifier);
395
- const out = await this.#output(found, true, abort.signal);
396
- return await this.#settled(this.#response(found.info, this.#registry.meta.get(found.id), out));
434
+ const result = (async () => {
435
+ const found = await this.#find(identifier, abort.signal);
436
+ const out = await this.#output(found, true, abort.signal);
437
+ abort.signal.throwIfAborted();
438
+ return await this.#settled(this.#response(found.info, this.#registry.meta.get(found.id), out));
439
+ })();
440
+ return await Promise.race([result, interrupted]);
397
441
  }
398
442
  catch (error) {
399
443
  if (abort.signal.aborted)
@@ -402,6 +446,8 @@ export class SandboxProcess {
402
446
  }
403
447
  finally {
404
448
  clearTimeout(timer);
449
+ if (onAbort)
450
+ abort.signal.removeEventListener("abort", onAbort);
405
451
  signal?.removeEventListener("abort", forward);
406
452
  }
407
453
  }
@@ -32,6 +32,8 @@ export type SandboxForkOptions = {
32
32
  /** A snapshot's id or, of this sandbox's snapshots, its name. */
33
33
  snapshotId?: string;
34
34
  envs?: Env[];
35
+ /** Blaxel's fork override; Runtime cannot enforce this override during a fork. */
36
+ lifecycle?: SandboxLifecycle;
35
37
  };
36
38
  export type SandboxArchiveOptions = {
37
39
  wait?: boolean;
@@ -750,6 +750,8 @@ export class SandboxInstance {
750
750
  * (files, memory and running processes), or started from a snapshot with
751
751
  * `snapshotId`. `envs` are added over this sandbox's. */
752
752
  async fork(targetName, options = {}) {
753
+ if (options.lifecycle !== undefined)
754
+ throw new NotSupportedError("Overriding lifecycle during a fork", "Omit lifecycle to use the existing Runtime fork behavior.");
753
755
  refuseApplication(options);
754
756
  const envs = envRecord(options.envs);
755
757
  checkEnvNames(Object.keys(envs));
@@ -22,13 +22,11 @@ export function snapshotModel(snapshot, sandboxName) {
22
22
  }
23
23
  /** Envs as a record; a nameless one is refused. */
24
24
  export function envRecord(envs) {
25
- const out = {};
26
- for (const env of envs ?? []) {
25
+ return Object.fromEntries((envs ?? []).map((env) => {
27
26
  if (!env.name)
28
27
  throw responseError(400, "every env needs a name");
29
- out[env.name] = env.value ?? "";
30
- }
31
- return out;
28
+ return [env.name, env.value ?? ""];
29
+ }));
32
30
  }
33
31
  /** Appends envs to a sandbox's env file: they win over the ones it has. */
34
32
  export async function appendEnvs(runtime, envs) {
package/dist/cli.d.ts CHANGED
@@ -7,6 +7,8 @@ type Out = {
7
7
  json: boolean;
8
8
  write: (text: string) => void;
9
9
  error: (text: string) => void;
10
+ /** Wait when an output pipe cannot accept another page yet. */
11
+ drain?: () => Promise<void>;
10
12
  };
11
13
  type Args = {
12
14
  positional: string[];