withruntime 0.11.0 → 0.11.1

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 (60) hide show
  1. package/BLAXEL.md +7 -8
  2. package/DAYTONA.md +7 -8
  3. package/E2B.md +16 -2
  4. package/README.md +3 -3
  5. package/README.src.md +3 -3
  6. package/VERCEL.md +3 -2
  7. package/compatibility-lock.json +55 -0
  8. package/dist/api-defaults.d.ts +1 -1
  9. package/dist/api-defaults.js +1 -1
  10. package/dist/blaxel/lookup.d.ts +2 -1
  11. package/dist/blaxel/lookup.js +2 -1
  12. package/dist/blaxel/sandbox.d.ts +7 -5
  13. package/dist/blaxel/sandbox.js +15 -10
  14. package/dist/cli.js +39 -15
  15. package/dist/client.d.ts +5 -1
  16. package/dist/client.js +19 -3
  17. package/dist/compat/files.js +5 -1
  18. package/dist/compat/lease.d.ts +1 -0
  19. package/dist/compat/lease.js +14 -8
  20. package/dist/credentials.d.ts +9 -0
  21. package/dist/credentials.js +26 -4
  22. package/dist/daytona/daytona.js +29 -12
  23. package/dist/daytona/sandbox.d.ts +8 -3
  24. package/dist/daytona/sandbox.js +23 -4
  25. package/dist/e2b/sandbox.d.ts +7 -3
  26. package/dist/e2b/sandbox.js +21 -7
  27. package/dist/errors.d.ts +6 -0
  28. package/dist/errors.js +6 -0
  29. package/dist/identity.js +2 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.js +2 -1
  32. package/dist/modal/index.js +6 -2
  33. package/dist/network-cli.js +7 -7
  34. package/dist/products/browser.d.ts +2 -1
  35. package/dist/products/browser.js +9 -19
  36. package/dist/products/desktop.d.ts +2 -1
  37. package/dist/products/desktop.js +5 -30
  38. package/dist/products/jobs.d.ts +7 -5
  39. package/dist/products/jobs.js +7 -3
  40. package/dist/products/mcp.d.ts +5 -1
  41. package/dist/products/mcp.js +29 -8
  42. package/dist/products/network-products.d.ts +1 -1
  43. package/dist/products/network-products.js +1 -1
  44. package/dist/products/observability.d.ts +1 -1
  45. package/dist/products/private-network.d.ts +1 -1
  46. package/dist/products/secrets.d.ts +2 -2
  47. package/dist/runloop/index.js +6 -2
  48. package/dist/sandbox.d.ts +27 -19
  49. package/dist/sandbox.js +121 -54
  50. package/dist/tar.d.ts +2 -0
  51. package/dist/tar.js +74 -16
  52. package/dist/transport.d.ts +33 -7
  53. package/dist/transport.js +171 -36
  54. package/dist/types.d.ts +54 -9
  55. package/dist/vercel/filesystem.js +3 -1
  56. package/dist/vercel/sandbox.d.ts +17 -10
  57. package/dist/vercel/sandbox.js +26 -13
  58. package/dist/wait.d.ts +12 -0
  59. package/dist/wait.js +47 -0
  60. package/package.json +1 -1
package/BLAXEL.md CHANGED
@@ -48,15 +48,14 @@ name, or through `getByExternalId`, use the same Runtime key.
48
48
  resumes on the next call. On Runtime it pauses after 60 seconds without a
49
49
  call (Runtime's shortest idle pause; Blaxel's is about 15 seconds), keeping
50
50
  its memory, files and processes, and wakes by itself on the next command,
51
- file call or preview visit. Its lease is Runtime's longest, an hour, and the
52
- adapter renews it in the background while the sandbox is in use; when a lease
53
- does run out the sandbox pauses rather than ends.
51
+ file call or preview visit. It has no time limit, so a sandbox in use is
52
+ never paused on a clock.
54
53
  - **How long it is kept:** Blaxel keeps a sandbox until it is deleted or its
55
54
  TTL ends. A paused Runtime sandbox is kept 365 days when no TTL, expiry or
56
55
  lifecycle is given. With one, it is never deleted before Blaxel would delete
57
56
  it, and may be kept longer:
58
- - a `ttl`, `expires`, `ttl-max-age` or `date` within the hour is a lease that
59
- ends the sandbox at that time, or later if the sandbox paused and woke;
57
+ - a `ttl`, `expires`, `ttl-max-age` or `date` within the hour is a time limit
58
+ that ends the sandbox at that time, or later if the sandbox paused and woke;
60
59
  - any other deadline, and any `ttl-idle`, keeps a paused sandbox that many
61
60
  days, rounded up, counted from each pause; with several, the shortest wins;
62
61
  - at most 365 days.
@@ -161,9 +160,9 @@ Some differences are not refusals:
161
160
  The old idle pause comes back once no `keepAlive` process runs, when a client
162
161
  sees one end (a waited exec, `wait` or `get`), stops or kills one, or calls
163
162
  `SandboxInstance.get`. With no such call, the sandbox pauses when it has been
164
- idle for that `timeout`. A `keepAlive` process past Runtime's hour-long lease
165
- is paused with the sandbox when the lease ends with no call to renew it, and
166
- carries on at the next call.
163
+ idle for that `timeout`. On a sandbox with a time limit, a `keepAlive`
164
+ process past it is paused with the sandbox when the limit ends with no call
165
+ to renew it, and carries on at the next call.
167
166
  - **A name is not refused while its process runs:** a second process may take a
168
167
  running process's name, and `get(name)` answers the newest.
169
168
  - **`process.list()` leaves each process's output empty** (Blaxel includes a
package/DAYTONA.md CHANGED
@@ -38,14 +38,13 @@ package talks only to Runtime.
38
38
  kept for Runtime's retention: 30 days on credit, 7 on the trial, or
39
39
  `autoDeleteInterval` minutes rounded up to whole days. `delete()` ends it.
40
40
  An `ephemeral` sandbox (or `autoDeleteInterval: 0`) ends on `stop()`.
41
- - **Auto-stop:** Runtime runs a sandbox on a lease of up to an hour. The lease
42
- is `autoStopInterval` minutes (default 15), and every call through the
43
- adapter moves it on once less than half is left, so a sandbox pauses after
44
- that long without calls, as Daytona's does. Past an hour, or `0` (never),
45
- the sandbox object also renews the lease each minute while it lives, toward
46
- `autoStopInterval` after the last call; once the program ends, the sandbox
47
- pauses within an hour. `ttlMinutes` caps the renewals and ends the sandbox at
48
- the deadline.
41
+ - **Auto-stop:** `autoStopInterval` minutes (default 15; `0` never) is the
42
+ sandbox's idle pause: it pauses after that long with nothing happening in
43
+ it, counted by the sandbox itself, so a long command is never paused for
44
+ want of calls. It has no time limit. `ttlMinutes` gives it one, and with
45
+ it Daytona's own rule: the sandbox pauses after `autoStopInterval` minutes
46
+ without a call through the adapter, each call moving the limit on, and ends
47
+ at the deadline.
49
48
  - **Funding:** left to Runtime, as `withruntime`'s own `create()` does: the free
50
49
  trial while the account has trial time, then prepaid credit. Pass
51
50
  `withruntime: { create: { funding: "trial" } }` to `new Daytona(...)` or to
package/E2B.md CHANGED
@@ -41,6 +41,20 @@ machine. An E2B key (`e2b_...`) is never sent anywhere. If one is passed as
41
41
  - **Timeout:** E2B's default of 300 seconds. When it ends, the sandbox is
42
42
  stopped, as E2B kills it. `lifecycle: { onTimeout: "pause" }` pauses it
43
43
  instead.
44
+ - **Idle pause:** Runtime's default, which E2B does not have. After 60
45
+ seconds with nothing happening in it (no request, running command, open
46
+ connection, traffic or CPU use) the sandbox pauses with its memory kept, and
47
+ the next command or file call wakes it. While it is paused, `isRunning()`
48
+ is `false` and `getInfo()` says `paused`, so code that reads either as
49
+ "the sandbox is gone" should not. Pass
50
+ `runtime: { create: { idlePauseSeconds: 0 } }` (Python:
51
+ `runtime_create={"idle_pause_seconds": 0}`) to keep it running, as on E2B.
52
+ - **Connections:** one client per key, holding at most 48 connections to the
53
+ API at once, since one address may hold 64. Each command being run holds
54
+ one while its output is read, and 8 are kept for other calls, so past 40
55
+ commands at once from one process the rest wait their turn. Pass your own
56
+ client with `runtime: { client: new Runtime({ maxConnections }) }` (Python:
57
+ `client=Runtime(max_connections=...)`).
44
58
  - **Funding:** left to Runtime, as `withruntime`'s own `create()` does. The
45
59
  free trial is used while the account has trial time, then prepaid credit.
46
60
  Pass `runtime: { create: { funding: "trial" } }` to use only the trial.
@@ -54,7 +68,7 @@ machine. An E2B key (`e2b_...`) is never sent anywhere. If one is passed as
54
68
  | E2B | On Runtime |
55
69
  | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
70
  | `Sandbox.create()`, `Sandbox.create(template, opts)` | `sandboxes.create`. `base` and `code-interpreter-v1` are Runtime's stock image. Any other name must be a ready Runtime image with that name. A UUID is an image or a snapshot. |
57
- | `timeoutMs` (Python `timeout`, seconds) | `timeoutSeconds`, rounded up. Under 60 s becomes 60 s, with a warning. Up to 24 hours, as on E2B: a Runtime lease reaches at most an hour ahead, so while this process runs the adapter extends it every five minutes, never past the end asked for. If the process ends first, the sandbox ends with its last lease, at most an hour later. Over 24 hours throws `InvalidArgumentError`. |
71
+ | `timeoutMs` (Python `timeout`, seconds) | Left out, the sandbox has no time limit: it runs while it works and pauses itself when idle. Given, it is `timeoutSeconds`, rounded up. Under 60 s becomes 60 s, with a warning. Up to 24 hours, as on E2B: a Runtime time limit reaches at most an hour ahead, so while this process runs the adapter extends it every five minutes, never past the end asked for. If the process ends first, the sandbox ends at its last limit, at most an hour later. Over 24 hours throws `InvalidArgumentError`. |
58
72
  | `metadata` | labels (at most 32). |
59
73
  | `envs` | Given to every command and code run through this object. |
60
74
  | `allowInternetAccess: false` | `network: { internet: false }`. |
@@ -73,7 +87,7 @@ machine. An E2B key (`e2b_...`) is never sent anywhere. If one is passed as
73
87
  | Relative paths | Resolved against the home directory, `/workspace` on Runtime. |
74
88
  | `/home/user` (E2B's home) | The first time a path, `cwd` or command names it, it becomes a link to `/workspace`, unless something is already there. |
75
89
  | `sandbox.kill()`, `Sandbox.kill(id)` | `stop`, without waiting. `false` when the sandbox was not found or had already ended. |
76
- | `sandbox.setTimeout(ms)`, `Sandbox.setTimeout(id, ms)` | `extend`: the end moves to now + ms, up to 24 hours (past an hour it is carried on as for `timeoutMs`). An earlier end is refused: Runtime cannot end a lease early. |
90
+ | `sandbox.setTimeout(ms)`, `Sandbox.setTimeout(id, ms)` | `extend`: the end moves to now + ms, up to 24 hours (past an hour it is carried on as for `timeoutMs`). An earlier end is refused: Runtime cannot end a lease early. A sandbox with no time limit has no end to move, and nothing changes. |
77
91
  | `Sandbox.connect(id, { timeoutMs })`, `sandbox.connect()` | `get`. A paused sandbox is woken. A running one's end moves later, never earlier, up to 24 hours. An ended one throws `SandboxNotFoundError`. |
78
92
  | `sandbox.pause()`, `betaPause`, `Sandbox.pause(id)` | `pause`, keeping memory and files. `false` when it was already paused. |
79
93
  | `getInfo`, `isRunning` | `get`. `templateId` is `base` or the image or snapshot id. `envdVersion` is `runtime`. |
package/README.md CHANGED
@@ -31,8 +31,8 @@ Node 22 call `await sbx.stop()`). With no arguments you get the free
31
31
  trial while it lasts (100 free hours, up to eight sandboxes running at once), 2 vCPU, 4 GiB of
32
32
  memory and a 4 GiB disk. The current default image includes NumPy, pandas and
33
33
  matplotlib; see the [sandbox environment](https://withruntime.com/docs/sandbox-environment).
34
- Omitting `funding` can use prepaid credit after the trial is exhausted. Use
35
- `Sandbox.create({ funding: "trial" })` for free use only; it never falls back to paid.
34
+ The trial's hours are spent first, then prepaid credit, with nothing to
35
+ choose; `funding` is accepted and ignored.
36
36
 
37
37
  The sandbox object does the rest:
38
38
 
@@ -45,7 +45,7 @@ The sandbox object does the rest:
45
45
  directories with `upload` and `download`;
46
46
  - `pause`, `wake`, `extend`, `fork` and `snapshot` (a paused sandbox also wakes
47
47
  by itself on the next call), `update` for its name, environment, automatic
48
- wake, idle pause and persistence, `keepAlive` to extend its lease while your
48
+ wake, idle pause and persistence, `keepAlive` to move a time limit on while your
49
49
  process runs, `switchImage` to move it to a new image keeping /workspace,
50
50
  and `delete` to remove it for good;
51
51
  - `interpreter`, `network`, `previews`, `desktop`, `mounts` (your S3, R2 or
package/README.src.md CHANGED
@@ -33,8 +33,8 @@ Node 22 call `await sbx.stop()`). With no arguments you get the free
33
33
  trial while it lasts ({{trial-hours}} free hours, up to {{trial-sandboxes}} sandboxes running at once), 2 vCPU, 4 GiB of
34
34
  memory and a 4 GiB disk. The current default image includes NumPy, pandas and
35
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.
36
+ The trial's hours are spent first, then prepaid credit, with nothing to
37
+ choose; `funding` is accepted and ignored.
38
38
 
39
39
  The sandbox object does the rest:
40
40
 
@@ -47,7 +47,7 @@ The sandbox object does the rest:
47
47
  directories with `upload` and `download`;
48
48
  - `pause`, `wake`, `extend`, `fork` and `snapshot` (a paused sandbox also wakes
49
49
  by itself on the next call), `update` for its name, environment, automatic
50
- wake, idle pause and persistence, `keepAlive` to extend its lease while your
50
+ wake, idle pause and persistence, `keepAlive` to move a time limit on while your
51
51
  process runs, `switchImage` to move it to a new image keeping /workspace,
52
52
  and `delete` to remove it for good;
53
53
  - `interpreter`, `network`, `previews`, `desktop`, `mounts` (your S3, R2 or
package/VERCEL.md CHANGED
@@ -33,8 +33,9 @@ are ignored.
33
33
 
34
34
  - **Machine:** Vercel's default, 2 vCPUs with 2048 MiB each.
35
35
  `resources: { vcpus }` sets another, with 2048 MiB per vCPU.
36
- - **Timeout:** Vercel's default of 5 minutes. Runtime's leases run 60 seconds to
37
- an hour. A longer `timeout` (Python `execution_time_limit`) starts with an
36
+ - **Timeout:** none unless you pass one: the sandbox runs while it works and
37
+ pauses itself when idle. A `timeout` is kept as a Runtime time limit, which
38
+ runs 60 seconds to an hour. A longer `timeout` (Python `execution_time_limit`) starts with an
38
39
  hour's lease, and the sandbox object renews it toward the timeout while it
39
40
  lives, never past it; once the program ends, the sandbox pauses within an
40
41
  hour. `extendTimeout` moves the end later, as often as needed.
@@ -229,6 +229,34 @@
229
229
  "artifacts": {
230
230
  "prime_sandboxes-0.4.1-py3-none-any.whl": "sha256-1a1bb96afacc66d690815be82c0b7af180ea141421b7d180b4a0adbada4f38f0",
231
231
  "prime_sandboxes-0.4.1.tar.gz": "sha256-53f7577dc9070e3b8e30db360f9e3cce3bcc35f8765b26f24f64dbc3c7763b06"
232
+ },
233
+ "latestReview": {
234
+ "version": "0.4.2",
235
+ "artifacts": {
236
+ "prime_sandboxes-0.4.2-py3-none-any.whl": "sha256-ed59f001c7575339d6c903166c5468f783eda0997586ec6476fd8b35f65746f9",
237
+ "prime_sandboxes-0.4.2.tar.gz": "sha256-4990a724cac0a662fde6e12e7eccfee092a78679cc568653b8ef52008dfa28e4"
238
+ },
239
+ "reviewedAt": "2026-10-02",
240
+ "disposition": "held-for-sdk-qualification",
241
+ "evidence": {
242
+ "sourceUrls": [
243
+ "https://pypi.org/pypi/prime-sandboxes/0.4.2/json",
244
+ "https://files.pythonhosted.org/packages/3f/4d/a379cdf82d6f06c970bd4f0333c2360c08aba65c809d7492f49aece84c66/prime_sandboxes-0.4.2.tar.gz",
245
+ "https://files.pythonhosted.org/packages/da/b5/6a9fe6ae1342872c1c0cc45072cc0c58a5fa2783c4c45770e585f2535c8f/prime_sandboxes-0.4.2-py3-none-any.whl",
246
+ "https://github.com/PrimeIntellect-ai/prime/tree/main/packages/prime-sandboxes"
247
+ ],
248
+ "artifactSha256": {
249
+ "prime_sandboxes-0.4.2-py3-none-any.whl": "ed59f001c7575339d6c903166c5468f783eda0997586ec6476fd8b35f65746f9",
250
+ "prime_sandboxes-0.4.2.tar.gz": "4990a724cac0a662fde6e12e7eccfee092a78679cc568653b8ef52008dfa28e4"
251
+ },
252
+ "changes": [
253
+ "Source diff of the 0.4.1 and 0.4.2 sdists, downloaded and hashed on the Mac, nothing installed or run: README, __init__, models.py, sandbox.py, one test changed; tests/test_checkpoints.py added.",
254
+ "Adds filesystem checkpoints: SandboxCheckpoint, CheckpointLookupError and BatchCheckpointResponse models; checkpoint, get_checkpoint, get_checkpoints (batch of up to 100 ids), wait_for_checkpoint and list_checkpoints on the sync and async clients; CreateSandboxRequest takes checkpoint_id with a validate_restore_source check.",
255
+ "CreateSandboxRequest.docker_image becomes Optional (None when restoring from a checkpoint) and disk_size_gb's default 5.0 becomes None.",
256
+ "Background job launch: the working_dir prefix is 'cd DIR || exit 1;' instead of 'cd DIR &&', and the launch cwd is / for an absolute working_dir.",
257
+ "No existing method was removed or renamed. Installed consumer qualification against withruntime/prime is not run; the supported pin stays 0.4.1."
258
+ ]
259
+ }
232
260
  }
233
261
  }
234
262
  ]
@@ -252,6 +280,33 @@
252
280
  "artifacts": {
253
281
  "modal-1.6.0-py3-none-any.whl": "sha256-2f5981ba656570ad96379d29f0ddff876880708c84966024ce130e9e1794fa68",
254
282
  "modal-1.6.0.tar.gz": "sha256-c5dd551b5e0a283f3c764476c0a4ebe5bdef1e4a69432fa27e1b1ad8ca5e0817"
283
+ },
284
+ "latestReview": {
285
+ "version": "1.6.1",
286
+ "artifacts": {
287
+ "modal-1.6.1-py3-none-any.whl": "sha256-f408ef88563003a83e491a6be09d4221eaa6e038fa4faa2ad8b15b25afe949a4",
288
+ "modal-1.6.1.tar.gz": "sha256-ff17768f67a65595aa7882e893e6cb78e2e001b3653aa52cfe98d4051d8d9e9e"
289
+ },
290
+ "reviewedAt": "2026-10-03",
291
+ "disposition": "held-for-sdk-qualification",
292
+ "evidence": {
293
+ "sourceUrls": [
294
+ "https://pypi.org/pypi/modal/1.6.1/json",
295
+ "https://files.pythonhosted.org/packages/65/ba/2b36899ea5633bf101e6ba6f7e95a4da3e4b1f57f52bd0ba8f4cfd12e808/modal-1.6.1.tar.gz",
296
+ "https://files.pythonhosted.org/packages/d2/3c/6b7d9a15dab9af2833ce674bbc72b5654d4099d5b088f1cc1eda3f8249ca/modal-1.6.1-py3-none-any.whl"
297
+ ],
298
+ "artifactSha256": {
299
+ "modal-1.6.1-py3-none-any.whl": "f408ef88563003a83e491a6be09d4221eaa6e038fa4faa2ad8b15b25afe949a4",
300
+ "modal-1.6.1.tar.gz": "ff17768f67a65595aa7882e893e6cb78e2e001b3653aa52cfe98d4051d8d9e9e"
301
+ },
302
+ "changes": [
303
+ "Source diff of the 1.6.0 and 1.6.1 sdists, downloaded and hashed on the Mac, nothing installed or run; the 1.6.0 hashes match the pin. No file added or removed; 37 files changed, most in sandbox.py, the CLI and the generated protos.",
304
+ "Sandbox v2 calls send a per-sandbox token (x-modal-sandbox-token, from the create response) beside the auth token through a new _v2_metadata helper, including command router access.",
305
+ "Experimental sidecars take include_oidc_identity_token and proxy, refused for GPU sandboxes. Server sessions retry start with an idempotency key for up to 25 minutes; the CLI gains endpoint commands; stats types gain container_total_count; agent detection reads tool variables before AGENT and AI_AGENT.",
306
+ "Requirements: protobuf below 8.0 instead of 7.0, synchronicity ~=0.12.6 instead of ~=0.12.5. Some iterator return types become AsyncGenerator.",
307
+ "No public Sandbox, ContainerProcess, Image, App or file method used by withruntime/modal was removed or renamed; that drop-in does not import modal. Installed consumer qualification is not run; the supported pin stays 1.6.0."
308
+ ]
309
+ }
255
310
  }
256
311
  }
257
312
  ]
@@ -2,7 +2,7 @@
2
2
  export declare const WAIT_FOR_TIMEOUT_SECONDS = 60;
3
3
  /** A streamed exec's command limit when none is given: the API's longest. */
4
4
  export declare const STREAMED_EXEC_TIMEOUT_MS = 86400000;
5
- /** The lease keepAlive keeps ahead of now, and so the most it extends by. */
5
+ /** The time keepAlive keeps ahead of a time limit, and so the most it extends by. */
6
6
  export declare const KEEP_ALIVE_MARGIN_SECONDS = 600;
7
7
  /** A webhook watch runs until stopped. */
8
8
  export declare const WEBHOOK_WATCH_TIMEOUT_MS = 0;
@@ -7,7 +7,7 @@
7
7
  export const WAIT_FOR_TIMEOUT_SECONDS = 60;
8
8
  /** A streamed exec's command limit when none is given: the API's longest. */
9
9
  export const STREAMED_EXEC_TIMEOUT_MS = 86_400_000;
10
- /** The lease keepAlive keeps ahead of now, and so the most it extends by. */
10
+ /** The time keepAlive keeps ahead of a time limit, and so the most it extends by. */
11
11
  export const KEEP_ALIVE_MARGIN_SECONDS = 600;
12
12
  /** A webhook watch runs until stopped. */
13
13
  export const WEBHOOK_WATCH_TIMEOUT_MS = 0;
@@ -23,7 +23,8 @@ export declare const STANDBY: {
23
23
  idlePauseSeconds: number;
24
24
  autoWake: true;
25
25
  };
26
- /** Runtime's longest lease. The adapter renews it while the sandbox is used. */
26
+ /** Runtime's longest time limit. A sandbox made before 0300 has it, and the
27
+ * adapter renews it while the sandbox is used; one made since has none. */
27
28
  export declare const LEASE_SECONDS = 3600;
28
29
  /** The live sandbox with this name (or Runtime id): one call. A 404 when
29
30
  * there is none, as Blaxel answers. */
@@ -30,7 +30,8 @@ export const STANDBY = {
30
30
  idlePauseSeconds: 60,
31
31
  autoWake: true,
32
32
  };
33
- /** Runtime's longest lease. The adapter renews it while the sandbox is used. */
33
+ /** Runtime's longest time limit. A sandbox made before 0300 has it, and the
34
+ * adapter renews it while the sandbox is used; one made since has none. */
34
35
  export const LEASE_SECONDS = 3600;
35
36
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
36
37
  /** The live sandbox with this name (or Runtime id): one call. A 404 when
@@ -58,16 +58,18 @@ type Resolved = {
58
58
  };
59
59
  /** A duration as Blaxel writes it ("30m", "24h", "7d", "1w", "1h30m"). */
60
60
  export declare function durationMs(value: string): number;
61
- /** The lease and the paused retention that honour Blaxel's TTL, expiry and
62
- * lifecycle: never deleted before Blaxel would delete it. A fixed deadline
63
- * within the hour is a lease that ends the sandbox; any other deadline keeps
64
- * a paused sandbox that many days, rounded up; none keeps it 365 days. */
61
+ /** The time limit and the paused retention that honour Blaxel's TTL, expiry
62
+ * and lifecycle: never deleted before Blaxel would delete it. A fixed
63
+ * deadline within the hour is a time limit that ends the sandbox; past that
64
+ * there is none, so it runs while it works and pauses when idle (0300), and
65
+ * a paused sandbox is kept that many days, rounded up; none keeps it 365
66
+ * days. */
65
67
  export declare function lifetimeOf(asked: {
66
68
  ttl?: string | null;
67
69
  expires?: Date;
68
70
  lifecycle?: SandboxLifecycle | null;
69
71
  }): {
70
- timeoutSeconds: number;
72
+ timeoutSeconds?: number;
71
73
  onLeaseEnd: "pause" | "stop";
72
74
  days: number;
73
75
  };
@@ -71,10 +71,12 @@ export function durationMs(value) {
71
71
  total += Number(count) * unit[letter];
72
72
  return total;
73
73
  }
74
- /** The lease and the paused retention that honour Blaxel's TTL, expiry and
75
- * lifecycle: never deleted before Blaxel would delete it. A fixed deadline
76
- * within the hour is a lease that ends the sandbox; any other deadline keeps
77
- * a paused sandbox that many days, rounded up; none keeps it 365 days. */
74
+ /** The time limit and the paused retention that honour Blaxel's TTL, expiry
75
+ * and lifecycle: never deleted before Blaxel would delete it. A fixed
76
+ * deadline within the hour is a time limit that ends the sandbox; past that
77
+ * there is none, so it runs while it works and pauses when idle (0300), and
78
+ * a paused sandbox is kept that many days, rounded up; none keeps it 365
79
+ * days. */
78
80
  export function lifetimeOf(asked) {
79
81
  const now = Date.now();
80
82
  const fixed = [];
@@ -110,7 +112,7 @@ export function lifetimeOf(asked) {
110
112
  : KEEP_DAYS;
111
113
  return soonest <= LEASE_SECONDS * 1000
112
114
  ? { timeoutSeconds: Math.max(60, Math.ceil(soonest / 1000)), onLeaseEnd: "stop", days }
113
- : { timeoutSeconds: LEASE_SECONDS, onLeaseEnd: "pause", days };
115
+ : { onLeaseEnd: "pause", days };
114
116
  }
115
117
  /** Blaxel's network settings as Runtime's rules: allowed and forbidden
116
118
  * domains, from the network itself or from its proxy (Blaxel's current
@@ -402,9 +404,10 @@ export class SandboxInstance {
402
404
  }
403
405
  // ---- the live sandbox ---------------------------------------------------
404
406
  /** The sandbox, ready for a call. A paused one wakes by itself on the call
405
- * (autoWake); one made without autoWake is woken here. A lease nearing its
406
- * end is renewed in the background, so a sandbox in use is not paused by
407
- * its lease. */
407
+ * (autoWake); one made without autoWake is woken here. A time limit
408
+ * nearing its end (an hour, on a sandbox made before 0300) is renewed in
409
+ * the background, so a sandbox in use is not paused by it; one with no
410
+ * time limit renews itself. */
408
411
  async #live() {
409
412
  if (this.#deleted)
410
413
  throw responseError(404, `Sandbox ${this.name} was deleted.`);
@@ -414,6 +417,8 @@ export class SandboxInstance {
414
417
  await this.#wake();
415
418
  else if (state === "running" &&
416
419
  rt.info.onLeaseEnd === "pause" &&
420
+ // One with no time limit renews itself (0300).
421
+ rt.info.endsAt !== null &&
417
422
  !this.#renewing &&
418
423
  Date.parse(rt.info.expiresAt) - Date.now() < RENEW_BELOW_MS)
419
424
  this.#renewing = rt
@@ -461,7 +466,7 @@ export class SandboxInstance {
461
466
  labels: { ...labels, [IDLE_PAUSE]: labels[IDLE_PAUSE] ?? String(current ?? 0) },
462
467
  }))
463
468
  : undefined,
464
- rt.info.onLeaseEnd === "pause" && need >= 1
469
+ rt.info.onLeaseEnd === "pause" && rt.info.endsAt !== null && need >= 1
465
470
  ? rt.extend(Math.ceil(need)).catch(() => undefined)
466
471
  : undefined,
467
472
  ]);
@@ -545,7 +550,7 @@ export class SandboxInstance {
545
550
  vcpu: Math.min(MAX_VCPU, Math.max(1, Math.round(memory / MEMORY_PER_VCPU))),
546
551
  memoryMiB: memory,
547
552
  ...STANDBY,
548
- timeoutSeconds: lifetime.timeoutSeconds,
553
+ ...(lifetime.timeoutSeconds === undefined ? {} : { timeoutSeconds: lifetime.timeoutSeconds }),
549
554
  onLeaseEnd: lifetime.onLeaseEnd,
550
555
  ...(config.name ? { name: config.name } : {}),
551
556
  labels,
package/dist/cli.js CHANGED
@@ -140,10 +140,10 @@ const SANDBOX_HELP = `runtime sandbox <command>
140
140
  resize <id> [--vcpu 2] [--memory 4096] --restart
141
141
  Change its size by a restart: the whole disk stays,
142
142
  programs stop (snapshot it first to keep memory)
143
- extend <id> <seconds> More time before the lease ends
143
+ extend <id> <seconds> Move a time limit on (one with none needs nothing)
144
144
  update <id> [--name n] [--label k=v]... [--env K=V]... [--unset-env K]... [--idle-pause <seconds>] [--auto-wake on|off]
145
145
  [--persistent on|off] [--max-total-cost <usd>|none]
146
- Change its settings; persistent keeps it running
146
+ Change its settings; persistent keeps it running until stopped
147
147
  snapshot <id> [--name n] [--retention days] Keep its whole machine; prints the snapshot id
148
148
  mount <id> <s3|r2|gcs>://<bucket>[/prefix] <path> [--secret NAME] [--region r]
149
149
  [--endpoint https://...] [--account-id id] [--read-only]
@@ -517,8 +517,18 @@ function describe(info) {
517
517
  ],
518
518
  ["funding", info.funding],
519
519
  ["region", info.region],
520
- ["expires", info.expiresAt ?? "-"],
521
- ["at lease end", info.onLeaseEnd ?? "-"],
520
+ // A sandbox with no time limit runs while it works (0300): endsAt is
521
+ // null. An older server sends no endsAt; its expiresAt was the end.
522
+ ["time limit", info.timeoutSeconds ? `${info.timeoutSeconds} s` : "none"],
523
+ [
524
+ "ends",
525
+ info.endsAt === null
526
+ ? info.state === "running"
527
+ ? "when idle, stopped or out of credit"
528
+ : "-"
529
+ : (info.endsAt ?? info.expiresAt ?? "-"),
530
+ ],
531
+ ["at its end", info.onLeaseEnd ?? "-"],
522
532
  // Why a sandbox is no longer running (troubleshooting.md, "A sandbox
523
533
  // stopped on its own"); `get` left both out until 30 September 2026.
524
534
  ...(info.pausedAt ? [["paused at", info.pausedAt]] : []),
@@ -1071,10 +1081,18 @@ export function usageCsv(u) {
1071
1081
  .join(","));
1072
1082
  return [head.join(","), ...rows].join("\n");
1073
1083
  }
1084
+ /** Milliseconds as hours, to a tenth: "8,731.2". */
1085
+ const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
1074
1086
  /** The free trial's time left, as `runtime usage` and `runtime limits` say it. */
1075
1087
  function trialLeft(trial) {
1076
- const hours = (ms) => (ms / 3_600_000).toLocaleString("en-US", { maximumFractionDigits: 1 });
1077
- return `${hours(trial.availableMs)} of ${hours(trial.totalMs)} hours left${trial.reservedMs > 0 ? `, ${hours(trial.reservedMs)} held by running sandboxes` : ""}`;
1088
+ return `${hours(trial.availableMs)} of ${hours(trial.totalMs)} hours left`;
1089
+ }
1090
+ /** A pilot's sandbox hours left, as `runtime usage` says them. */
1091
+ function pilotLeft(pilot) {
1092
+ const left = `${hours(pilot.leftMs)} of ${pilot.hours.toLocaleString("en-US")} left`;
1093
+ return pilot.leftMs > 0
1094
+ ? `${left}; new pilot sandboxes until ${pilot.endsAt.slice(0, 16).replace("T", " ")} UTC`
1095
+ : "This pilot's sandbox hours are used.";
1078
1096
  }
1079
1097
  function usageSummary(u) {
1080
1098
  const micros = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint"
@@ -1088,15 +1106,17 @@ function usageSummary(u) {
1088
1106
  // its own line, so "spent" reads as what was used.
1089
1107
  ["used", dollars(micros(u.spent) - takenBack)],
1090
1108
  ...(takenBack > 0n ? [["returned by refunds and disputes", dollars(takenBack)]] : []),
1091
- // Holds cover running sandboxes and the hour ahead of stored snapshots,
1092
- // images and volumes (pricing guide), so an account with nothing running
1093
- // can still show one.
1094
- ["held for running sandboxes and this hour's storage", dollars(micros(u.held))],
1109
+ // What running sandboxes and the hour ahead of stored snapshots, images
1110
+ // and volumes are about to use, so an account with nothing running can
1111
+ // still show some. Said without "held": no guide teaches holds.
1112
+ ["set aside for running sandboxes and this hour's storage", dollars(micros(u.held))],
1095
1113
  ];
1096
1114
  if (micros(u.expired) > 0n)
1097
1115
  rows.push(["expired or taken back", dollars(micros(u.expired))]);
1098
1116
  if (u.trial)
1099
1117
  rows.push(["free trial", trialLeft(u.trial)]);
1118
+ if (u.pilot)
1119
+ rows.push(["pilot sandbox hours", pilotLeft(u.pilot)]);
1100
1120
  if (u.outbound) {
1101
1121
  const gib = (bytes) => `${(bytes / 1_073_741_824).toLocaleString("en-US", { maximumFractionDigits: 1 })} GiB`;
1102
1122
  const left = Math.max(0, u.outbound.allowanceBytes - u.outbound.freeBytes);
@@ -1474,11 +1494,13 @@ async function sandbox(argv, env, out) {
1474
1494
  s.state,
1475
1495
  `${s.info.vcpu}/${s.info.memoryMiB}`,
1476
1496
  s.info.funding,
1477
- s.info.expiresAt ?? "-",
1497
+ // When it stops or pauses by itself (0381): its time limit's end, or
1498
+ // "-" when it never will. An older server sends no endsAt.
1499
+ (s.info.endsAt === undefined ? s.info.expiresAt : s.info.endsAt) ?? "-",
1478
1500
  ]);
1479
1501
  print([
1480
1502
  rows.length
1481
- ? table([["ID", "NAME", "STATE", "CPU/MIB", "FUNDING", "EXPIRES"], ...rows])
1503
+ ? table([["ID", "NAME", "STATE", "CPU/MIB", "FUNDING", "ENDS"], ...rows])
1482
1504
  : stopped
1483
1505
  ? "No live sandboxes."
1484
1506
  : "No sandboxes. Create one: runtime sandbox create",
@@ -1704,7 +1726,9 @@ async function sandbox(argv, env, out) {
1704
1726
  const sbx = await get(rest[0]);
1705
1727
  const seconds = Number(need(rest[1], "seconds, e.g. runtime sandbox extend <id> 600"));
1706
1728
  await sbx.extend(seconds);
1707
- print(`${sbx.id} now ends at ${sbx.info.expiresAt}`, sbx.info);
1729
+ print(sbx.info.endsAt === null
1730
+ ? `${sbx.id} has no time limit: it runs while it works, so nothing was extended`
1731
+ : `${sbx.id} now ends at ${sbx.info.endsAt ?? sbx.info.expiresAt}`, sbx.info);
1708
1732
  return 0;
1709
1733
  }
1710
1734
  case "exec": {
@@ -2783,8 +2807,8 @@ read = readSecret) {
2783
2807
  }
2784
2808
  const JOB_HELP = `runtime job <command>
2785
2809
 
2786
- A job runs a command in a fresh sandbox, once or on a schedule. Each run is a
2787
- paid sandbox billed at the sandbox rates; the trial's hours do not fund jobs.
2810
+ A job runs a command in a fresh sandbox, once or on a schedule. Each run spends
2811
+ the free trial's hours first, then credit at the sandbox rates.
2788
2812
 
2789
2813
  create <name> (--cron "<min hour day month weekday>" [--timezone Europe/Berlin] | --at <time|now>)
2790
2814
  [--vcpu 2] [--memory 4096] [--disk 4096] [--cpu shared|reserved] [--cpu-floor <thousandths>]
package/dist/client.d.ts CHANGED
@@ -16,7 +16,11 @@ export type RuntimeOptions = {
16
16
  timeoutMs?: number;
17
17
  /** Retries of transport failures, 429, 502, 503 and 504. Default 4. */
18
18
  maxRetries?: number;
19
- /** Calls in flight at once; more wait their turn and reuse connections. Default 32. */
19
+ /** Connections held at once, calls and streams (a command's output, a
20
+ * download) together; more wait their turn, first come first served, and
21
+ * reuse them. Default 48: one address may hold 64 connections to the API
22
+ * before it has used a valid key. 8 are kept for calls whatever the
23
+ * streams hold. */
20
24
  maxConnections?: number;
21
25
  /** How long `sandboxes.create` keeps retrying, with the same key and input,
22
26
  * when the trial's slots, the account's quota or the region is full
package/dist/client.js CHANGED
@@ -4,7 +4,7 @@ import { clientFactories, clientProductAliases } from "./products/index.js";
4
4
  import { usageExport as makeUsageExport } from "./products/observability.js";
5
5
  import { Sandbox, deleteSandbox, sandboxPage } from "./sandbox.js";
6
6
  import { Snapshots } from "./snapshots.js";
7
- import { Transport, missingKey } from "./transport.js";
7
+ import { DEFAULT_BASE_URL, Transport, missingKey } from "./transport.js";
8
8
  /** The key `runtime login` saved for this machine and this API origin, found
9
9
  * on first use. A constructor override must never receive another API's key. */
10
10
  const savedKey = async (apiOrigin) => {
@@ -30,10 +30,14 @@ export class Runtime {
30
30
  /** Date-range settlements, paged with exact charges and the server's CSV. */
31
31
  usageExport;
32
32
  constructor(options = {}) {
33
+ // The saved key is found by the origin as given, as `runtime login` saved
34
+ // it; the calls go where that origin is reachable from here, which in a
35
+ // sandbox is runtime.internal (transport.ts, `reachable`).
36
+ const origin = options.baseUrl || env("RUNTIME_API_URL") || DEFAULT_BASE_URL;
33
37
  this.transport = new Transport({
34
38
  // A key given, then RUNTIME_API_KEY, then (in Node and Bun) the key
35
39
  // `runtime login` saved for this machine, read on the first call.
36
- apiKey: options.apiKey ?? env("RUNTIME_API_KEY") ?? (() => savedKey(this.transport.baseUrl)),
40
+ apiKey: options.apiKey ?? env("RUNTIME_API_KEY") ?? (() => savedKey(origin)),
37
41
  baseUrl: options.baseUrl ?? env("RUNTIME_API_URL"),
38
42
  ...(options.fetch ? { fetch: options.fetch } : {}),
39
43
  ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
@@ -97,6 +101,9 @@ export class Sandboxes {
97
101
  * free, up to `waitForCapacityMs` (the client's, two minutes by default). */
98
102
  async create(input = {}, options = {}) {
99
103
  const { wait, ...body } = input;
104
+ const began = performance.now();
105
+ // Time spent waiting for capacity is the room's, not the deadline's.
106
+ let waited = 0;
100
107
  const info = await this.t.json({
101
108
  method: "POST",
102
109
  path: "/v1/sandboxes",
@@ -104,13 +111,22 @@ export class Sandboxes {
104
111
  wait: wait === false ? 0 : 60,
105
112
  ...options,
106
113
  waitForCapacityMs: options.waitForCapacityMs ?? this.t.waitForCapacityMs,
114
+ onCapacityWait: (refusal, waitMs) => {
115
+ waited += waitMs;
116
+ options.onCapacityWait?.(refusal, waitMs);
117
+ },
107
118
  });
108
119
  const sandbox = new Sandbox(this.t, info);
109
120
  if (wait !== false && info.state !== "running") {
121
+ // One deadline for the whole create: the wait for running gets what the
122
+ // create left of it (1 ms when nothing is left, so it ends as a timeout).
123
+ const { timeoutMs } = options;
110
124
  await sandbox.waitFor("running", {
111
125
  timeoutSeconds: WAIT_FOR_TIMEOUT_SECONDS,
112
126
  signal: options.signal,
113
- timeoutMs: options.timeoutMs,
127
+ timeoutMs: timeoutMs
128
+ ? Math.max(1, Math.ceil(timeoutMs - (performance.now() - began - waited)))
129
+ : timeoutMs,
114
130
  idempotencyKey: options.idempotencyKey,
115
131
  });
116
132
  if (sandbox.state !== "running")
@@ -63,10 +63,14 @@ export class GuestFiles {
63
63
  if (!overwrite) {
64
64
  // cp -n alone reports success when it skips a raced destination. Stage
65
65
  // privately, then publish without replacement and verify it moved.
66
+ // mv -n that skips succeeds before coreutils 9.2 and fails from it on
67
+ // (the guest's 9.4 says "not replacing"), so a skip is told by the
68
+ // staged copy still being there beside a destination; with none there,
69
+ // mv's own error stands.
66
70
  await checked(sandbox, [
67
71
  "bash",
68
72
  "-c",
69
- 'set -e; parent=$(dirname -- "$2"); stage="$parent/.runtime-copy-$4"; mkdir -m 700 -- "$stage"; trap \'rm -rf -- "$stage"\' EXIT; if [ "$3" = true ]; then cp -R -n -- "$1" "$stage/item"; else cp -n -- "$1" "$stage/item"; fi; mv -n -T -- "$stage/item" "$2"; if [ -e "$stage/item" ] || [ -L "$stage/item" ]; then printf "Destination exists: %s\\n" "$2" >&2; exit 73; fi',
73
+ 'set -e; parent=$(dirname -- "$2"); stage="$parent/.runtime-copy-$4"; mkdir -m 700 -- "$stage"; trap \'rm -rf -- "$stage"\' EXIT; if [ "$3" = true ]; then cp -R -- "$1" "$stage/item"; else cp -- "$1" "$stage/item"; fi; mv -n -T -- "$stage/item" "$2" 2>"$stage/mv.err" || :; if [ -e "$stage/item" ] || [ -L "$stage/item" ]; then if [ -e "$2" ] || [ -L "$2" ]; then printf "Destination exists: %s\\n" "$2" >&2; exit 73; fi; cat -- "$stage/mv.err" >&2; exit 1; fi',
70
74
  "bash",
71
75
  this.path(from),
72
76
  this.path(to),
@@ -5,6 +5,7 @@ export interface Leased {
5
5
  readonly state: string;
6
6
  readonly info: {
7
7
  expiresAt: string;
8
+ endsAt?: string | null;
8
9
  };
9
10
  extend(seconds: number): Promise<unknown>;
10
11
  refresh(): Promise<unknown>;