withruntime 0.7.2 → 0.8.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.
package/BLAXEL.md ADDED
@@ -0,0 +1,221 @@
1
+ # Code written for Blaxel, on Runtime
2
+
3
+ Run code written for [Blaxel](https://docs.blaxel.ai/Sandboxes/Overview)'s
4
+ TypeScript sandbox SDK on Runtime Cloud by changing one import. It is part of
5
+ Runtime's SDK (`withruntime`), built on the same client, so it gets that SDK's
6
+ retries, idempotency keys and errors. Importing `withruntime` alone does not
7
+ load it.
8
+
9
+ ```ts no-run
10
+ import { SandboxInstance } from "withruntime/blaxel"; // was: from "@blaxel/core"
11
+ ```
12
+
13
+ The mapping was written against `@blaxel/core` 0.3.23 and Blaxel's sandbox API
14
+ source, checked 27 September 2026. Every name `@blaxel/core` exports can be
15
+ imported: code that only mentions an agent, model or MCP helper still loads.
16
+
17
+ ## Keys
18
+
19
+ The package uses, in order: a Runtime key given to `initialize({ apikey })`,
20
+ `RUNTIME_API_KEY`, a Runtime key (`rtcloud_...`) left in `BL_API_KEY`, and the
21
+ key `npx withruntime login` saved on this machine. A Blaxel key is never sent
22
+ anywhere: in `BL_API_KEY` it is ignored, and passed to `initialize` while
23
+ `RUNTIME_API_KEY` is not set it throws `CredentialsError`. `BL_WORKSPACE` means
24
+ nothing on Runtime. With no Runtime key at all, the first call throws
25
+ `CredentialsError` saying what to set.
26
+
27
+ **A key sees only the sandboxes it created.** A sandbox made with another of
28
+ the account's keys answers 404, though its name stays taken
29
+ ([teams](https://withruntime.com/docs/teams)). Services that share sandboxes by
30
+ name, or through `getByExternalId`, use the same Runtime key.
31
+
32
+ ## What a sandbox gets
33
+
34
+ - **Machine:** Blaxel's default of 4096 MB. vCPUs follow Blaxel's rule of one
35
+ per 2048 MB (at least one, at most 16): 2 for the default, 4 for 8192 MB. A
36
+ trial sandbox has at most 4096 MB (2 vCPUs); `memory: 8192` or more needs
37
+ prepaid credit, and on the trial is refused with a 400 (`invalid_trial`)
38
+ saying so.
39
+ - **Image:** Blaxel's `base-image`, `py-app`, `ts-app`, `node`,
40
+ `docker-in-sandbox` and `jupyter-server` are Runtime's stock image (Ubuntu
41
+ 24.04 with Node.js 24, Python 3.12, Bun, git and a compiler; Docker after
42
+ `sudo enable-docker`). Any other image `ns/name:tag` is the ready Runtime
43
+ image named `ns-name` (every `/` becomes `-`, as Runtime names hold none) at
44
+ tag `tag` (`latest` when none is given). Build it once with
45
+ `npx withruntime image build --dockerfile Dockerfile --name ns-name -t ns-name:tag`,
46
+ and the code keeps `image: "ns/name:tag"` as it is.
47
+ - **Standby and lifetime:** a Blaxel sandbox goes to standby when unused and
48
+ resumes on the next call. On Runtime it pauses after 60 seconds without a
49
+ call (Runtime's shortest idle pause; Blaxel's is about 15 seconds), keeping
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.
54
+ - **How long it is kept:** Blaxel keeps a sandbox until it is deleted or its
55
+ TTL ends. A paused Runtime sandbox is kept 365 days when no TTL, expiry or
56
+ lifecycle is given. With one, it is never deleted before Blaxel would delete
57
+ 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;
60
+ - any other deadline, and any `ttl-idle`, keeps a paused sandbox that many
61
+ days, rounded up, counted from each pause; with several, the shortest wins;
62
+ - at most 365 days.
63
+
64
+ On the free trial a paused sandbox is kept the trial's seven days, whatever
65
+ the TTL; on credit, as above.
66
+
67
+ - **Envs:** `envs` are written once at create to a file on the sandbox
68
+ (`/etc/runtime-blaxel/env`, readable by the sandbox user, changed only with
69
+ sudo), and every process the adapter starts reads it, from any client, so
70
+ `SandboxInstance.get(name)` in another program sees them too. A process's own
71
+ `env` wins over the sandbox's. Values never appear in a command line.
72
+ - **Region:** `us-pdx-1`, `us-was-1` and `auto` are accepted (Runtime runs in
73
+ one US region, east); any other, from `region` or `BL_REGION`, is refused.
74
+ - **Paths:** Blaxel's images work in `/blaxel`; Runtime's in `/workspace`.
75
+ Relative paths, `~` and paths under `/blaxel` resolve against `/workspace`, in
76
+ file calls and as a process's `workingDir`, and a command that names
77
+ `/blaxel` finds a link to `/workspace`. Other absolute paths are themselves.
78
+ - **Funding:** left to Runtime, as `withruntime`'s own create: the free trial
79
+ while the account has trial time, then prepaid credit. Pass
80
+ `withruntime: { create: { funding: "trial" } }` to use only the trial.
81
+ - **Anything else Runtime offers:** `withruntime: { create: { ... } }` sends
82
+ Runtime's create fields as they are; `sandbox.withruntime` is the Runtime
83
+ sandbox underneath. Static calls take `{ withruntime: { client } }` as a last
84
+ argument to use a given Runtime client.
85
+
86
+ ## Mapping
87
+
88
+ | Blaxel | On Runtime |
89
+ | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
90
+ | `SandboxInstance.create({ name, image, memory, labels, ports, envs, ... })` | `sandboxes.create`. A name another live sandbox holds is a 409, as in Blaxel; one held by a sandbox still being deleted is waited for. |
91
+ | `createIfNotExists` | `create` with `getOrCreate`: Runtime settles two callers racing for one name, and both get the one sandbox. An existing sandbox is answered as it is. |
92
+ | `get(name)`, `getByExternalId`, `list({ limit, externalId, showTerminated })` | The live sandbox with that name, or with the `externalId` label; Blaxel's paginated list (`data`, `nextPage`, `for await`, `autoPagingToArray`). |
93
+ | `delete` (static and on the sandbox) | `stop`: the sandbox ends at once. Using that object afterwards is a 404. |
94
+ | `metadata`, `spec`, `status`, `state`, `lastUsedAt`, `expiresIn` | From the Runtime sandbox and its `blaxel/` labels (image, memory, ports, region, TTL, expiry, lifecycle, archived), which any client reads; `state` is `STANDBY` while paused. |
95
+ | `updateMetadata`, `updateTtl`, `updateLifecycle`, `updateNetwork` | Labels (the display name and `externalId` are labels); the paused retention, as at create; network rules. |
96
+ | `archive`, `unarchive` | `pause`, `wake`. Runtime keeps memory and processes too, and the next call wakes it. |
97
+ | `network: { allowedDomains, forbiddenDomains, proxy }` | Runtime network rules: `allow` and `deny`. |
98
+ | `volumes: [{ name, mountPath }]` | The Runtime volume with that name, mounted read-write. |
99
+ | `fetch(port, path, init)` | The request goes to a private Runtime preview of the port (made on first use, then reused) with its token. |
100
+ | `process.exec` without `waitForCompletion` | One Runtime process, answered running. Its Blaxel name is kept in its command line, so `get("name")` finds it from any client. |
101
+ | `process.exec({ waitForCompletion: true })` | One streamed call, answered with `status`, `exitCode`, `stdout`, `stderr` and `logs`. Past `timeout` the wait ends with a 422 and the process runs on. |
102
+ | `onLog`, `onStdout`, `onStderr`, `streamLogs` | The process's output as it comes; `streamLogs` gives whole lines, as Blaxel's does. |
103
+ | `process.get`, `wait`, `list`, `logs`, `stop`, `kill`, `writeStdin`, `closeStdin` | Runtime processes by id or name. `wait` follows the output instead of polling. `stop` is SIGTERM, `kill` SIGKILL. |
104
+ | `keepAlive`, `timeout` | The sandbox does not pause for idleness while the process may run (its `timeout`, 600 s by default, 0 for as long as a lease allows); once no `keepAlive` process runs, it gets its idle pause back. |
105
+ | `restartOnFailure`, `maxRestarts`, `waitForPorts`, `workingDir`, `env`, `stdin` | A restart loop in the same process, with Blaxel's note in the output (`restartCount` counts the notes); a wait in the sandbox for the ports; as in Blaxel. |
106
+ | `fs.read`, `write`, `readBinary`, `writeBinary`, `writeTree`, `mkdir`, `rm`, `cp` | Runtime's files API, or one command. A path only root may read or write (outside `/workspace`, or made by a root process) goes through sudo, as Blaxel's root-owned API could. |
107
+ | `fs.ls`, `find`, `grep`, `search` | One command each, with Blaxel's defaults, limits and excluded directories. `ls` reports owners. |
108
+ | `fs.watch(path, callback, { withContent, ignore })`, `fs.download` | A Runtime watch (`path/**` for subdirectories); a download to the local disk. |
109
+ | `previews.create`, `createIfNotExists`, `get`, `list`, `delete` | Runtime previews of `spec.port`, public or private. The Blaxel name is kept in a label, so any client finds it by name. |
110
+ | `preview.tokens.create(expiresAt)` | A token for that long: at least a minute, at most a week. It is read from Blaxel's `X-Blaxel-Preview-Token` header or `?bl_preview_token=` as from Runtime's `x-runtime-preview-token` and `?runtime_preview_token=`, and never passed on to your server. |
111
+ | `snapshots.create`, `list`, `get`, `delete`; `Snapshot.create`, `get`, `list`, `fork` | Runtime snapshots, kept 365 days: files, memory and running processes. |
112
+ | `fork(name)`, `fork(name, { snapshotId, envs })` | A Runtime fork of the running sandbox, or a new sandbox from the snapshot (by id, or by name among this sandbox's); `envs` are added over the source's. |
113
+ | `CodeInterpreter.create`, `runCode`, `createCodeContext` | Runtime's interpreter, with the same result classes. The sandbox's envs reach every context. |
114
+ | `ResponseError`, `SandboxGatewayError`, `isGatewayError`, `isGatewayTimeout` | Runtime's errors by Blaxel's names: `status` and `code` are the HTTP status; `runtimeCode`, `hint` and `requestId` are Runtime's. |
115
+
116
+ ## Gaps
117
+
118
+ Each of these throws `NotSupportedError` before anything happens. Its `feature`
119
+ names the gap and its `alternative` says what to use.
120
+
121
+ | Blaxel | Use instead |
122
+ | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
123
+ | An `image` with no ready Runtime image of the name it maps to (Blaxel's other templates too) | Build it: `npx withruntime image build --dockerfile Dockerfile --name ns-name -t ns-name:tag`; the error gives the exact command. |
124
+ | A region outside the US | Runtime runs in one US region. |
125
+ | `extraArgs` other than `iptables`; network `egress`, `firewall`, `subnet`, and a `proxy` with more than its domain lists | Remove them; `allowedDomains` and `forbiddenDomains` (on the network or its proxy); Runtime secrets. |
126
+ | Read-only and ephemeral volumes; a volume with no Runtime volume of that name | Mount read-write; write scratch files under `/workspace`; `runtime.volumes.create`. |
127
+ | `snapshots.restore`, `restore` | Fork from the snapshot into a new sandbox. |
128
+ | Forking into an application | Fork into a sandbox and share its port. |
129
+ | A preview's `prefixUrl`, `customDomain`, `requestHeaders`, `responseHeaders`, `ttl` | Runtime's fixed preview address; set headers (CORS) in your server; delete the preview. |
130
+ | `preview.tokens.list()`; a token for over a week | Keep the tokens you create; create a new one when it runs out. |
131
+ | `runCode` with `envs` for one run; a language Runtime's interpreter lacks | Sandbox envs at create, or set them in the code; `process.exec`. |
132
+ | Listing by `cursor`, `q`, `anchor`, a status other than `DEPLOYED`, or newest first | List and filter the result yourself; `nextPage()` walks the pages. |
133
+ | `sandbox.sessions`, `schedules`, `codegen`, `system`, `drives`; `fromSession` | Previews and tokens; your own scheduler; `fs.write`; nothing to upgrade; Runtime volumes. |
134
+ | Agents, models, tools, MCP transports, jobs, applications, images, volumes, drives | Your own agent with Runtime sandboxes, `withruntime/tools`, `npx withruntime mcp`, `runtime.images`, `runtime.volumes`. |
135
+ | Blaxel's generated API functions (`createSandbox`, `postProcess`, ...) | `SandboxInstance` here, or `import { Runtime } from "withruntime"`. |
136
+
137
+ Some differences are not refusals:
138
+
139
+ - **A public preview needs a paid sandbox;** on the trial, share a port
140
+ privately and use a token.
141
+ - **Commands run as root,** as on Blaxel: every process the adapter starts
142
+ (`exec`, `keepAlive`, restart loops) hands over to passwordless `sudo`,
143
+ keeping the envs, the sandbox user's `PATH` and `HOME` (`/workspace`, which
144
+ stands for Blaxel's `/blaxel`). `id -u` prints 0 and `pwd` prints
145
+ `/workspace`. Stopping or killing a process ends it and everything it
146
+ started. The file calls act as the sandbox user and fall back to `sudo`
147
+ where only root may go, so files a root process made stay readable,
148
+ writable and removable through `fs`; a file the adapter rewrites that way
149
+ becomes the sandbox user's. `ls`, `find`, `grep`, `search` and `writeTree`
150
+ run as the sandbox user, so they skip what only root may read. The code
151
+ interpreter's contexts run as the sandbox user (Blaxel's Jupyter server runs
152
+ as root).
153
+ - **A paused sandbox keeps its memory and processes** across `archive`, where
154
+ Blaxel's archive keeps only files and starts the processes again.
155
+ - **Standby starts after 60 seconds idle** rather than about 15, and a running
156
+ process does not count as use unless it was started with `keepAlive`. A
157
+ `keepAlive` process raises the sandbox's idle pause to its `timeout` (off for
158
+ `timeout: 0`) and keeps the old one in the `blaxel/idlePauseSeconds` label.
159
+ The old idle pause comes back once no `keepAlive` process runs, when a client
160
+ sees one end (a waited exec, `wait` or `get`), stops or kills one, or calls
161
+ `SandboxInstance.get`. With no such call, the sandbox pauses when it has been
162
+ idle for that `timeout`. A `keepAlive` process past Runtime's hour-long lease
163
+ is paused with the sandbox when the lease ends with no call to renew it, and
164
+ carries on at the next call.
165
+ - **A name is not refused while its process runs:** a second process may take a
166
+ running process's name, and `get(name)` answers the newest.
167
+ - **`process.list()` leaves each process's output empty** (Blaxel includes a
168
+ tail of it); `get(name)` or `logs(name)` reads it. A process ended by a signal
169
+ reports exit code -1 and `stopped` (SIGTERM) or `killed` (SIGKILL, or its
170
+ time limit), from any client.
171
+ - **Runtime records a command's first 256 characters.** A process started with
172
+ a longer command keeps its name; from another client its `command` is the
173
+ start of what was run.
174
+ - **`fs.search`** scores matches by its own rule (characters in order, runs and
175
+ word starts first), not Blaxel's fzf scores. `grep`'s `contextLines` is
176
+ accepted and, as in Blaxel's API, not used.
177
+ - **`ls`, `find`, `grep`, `search` and `writeTree` run a short Python script,**
178
+ so on an image without `python3` they throw `NotSupportedError`; `ls` then
179
+ lists through the files API, without owners.
180
+ - **`spec.runtime.envs`, `network` and `volumes`** are known only to the client
181
+ that created the sandbox; the envs themselves reach every process from any
182
+ client. Port names are not kept: `ports` come back as numbers.
183
+ - A preview token shorter than a minute lasts a minute. Deleting one token
184
+ revokes every token of that preview.
185
+
186
+ ## The process line
187
+
188
+ A process's command line, which both this package and the Python one write
189
+ (`processLine` in `src/blaxel/context.ts`), so either finds and follows the
190
+ other's processes. One line, parts joined by `; `, quotes single with `'`
191
+ written `'\''`:
192
+
193
+ ```text
194
+ : rt-blaxel '<name>'; [: rt-blaxel-keep; ]export HOST="${HOST:-0.0.0.0}"; [ -r /etc/runtime-blaxel/env ] && . /etc/runtime-blaxel/env; unset RUNTIME_BLAXEL_KEEP; [{ [ -e /blaxel ] || sudo ln -s /workspace /blaxel; } 2>/dev/null; ]__rt_cmd='<command>'; export __rt_cmd; exec sudo -E env "PATH=$PATH" "HOME=$HOME" bash -c '<run>'
195
+ ```
196
+
197
+ `: rt-blaxel-keep;` is there for a `keepAlive` process; the link part when the
198
+ command or its env names `/blaxel`. `<run>` is `eval "$__rt_cmd"`, or with
199
+ `restartOnFailure` the restart loop:
200
+ `__rt_n=0; while :; do ( eval "$__rt_cmd" ); __rt_c=$?; [ $__rt_c -eq 0 ] && exit 0; [ $__rt_n -ge <max> ] && exit $__rt_c; __rt_n=$((__rt_n+1)); printf '\n[Process failed with exit code %d. Attempting restart %d/%s...]\n' $__rt_c $__rt_n '<max or unlimited>'; done`
201
+ (the `-ge` test left out when `maxRestarts` is negative). A process's own
202
+ `env` is sent with `RUNTIME_BLAXEL_KEEP=":NAME1:NAME2:"`, so the env file
203
+ leaves those names alone.
204
+
205
+ ## Tests
206
+
207
+ `bun run test` in `packages/cloud-sdk` runs the unit tests against a fake of
208
+ Runtime's SDK: `tests/blaxel`, including a check that every name
209
+ `@blaxel/core` 0.3.23 exports is exported here.
210
+
211
+ The end-to-end test runs Blaxel's guides, unmodified but for the import,
212
+ against the real API: `tests/fixtures/blaxel-quickstart.mjs` (create, processes,
213
+ log streaming and files) and `tests/fixtures/blaxel-previews.mjs` (envs, ports,
214
+ previews, snapshots and forks). It then times a new sandbox's first command
215
+ through the adapter and through `withruntime`. It refuses to start unless the
216
+ free trial has an hour left, and ends every sandbox it creates. It is not part
217
+ of `bun run test`.
218
+
219
+ ```bash no-run
220
+ bun packages/cloud-sdk/scripts/dropin-e2e.ts --only blaxel
221
+ ```
package/README.md CHANGED
@@ -107,6 +107,24 @@ Runtime does not do the same way throws `NotSupportedError` before anything
107
107
  happens, naming what to use instead. `DAYTONA.md` and `VERCEL.md` in this
108
108
  package list every mapping and gap.
109
109
 
110
+ ## Code written for Blaxel
111
+
112
+ `withruntime/blaxel` runs code written for Blaxel's sandbox SDK. Change the
113
+ import:
114
+
115
+ ```ts no-run
116
+ import { SandboxInstance } from "withruntime/blaxel"; // was: from "@blaxel/core"
117
+ ```
118
+
119
+ Sandboxes get Blaxel's default of 4096 MB, with one vCPU for every 2048 MB.
120
+ Standby becomes Runtime's pause: after 60 seconds without a call a sandbox
121
+ pauses with its memory and processes, and the next call wakes it. Envs,
122
+ processes by name, files, previews, snapshots, forks and the code interpreter
123
+ work as they do on Blaxel. A Blaxel key is never sent anywhere. Sessions,
124
+ drives, codegen, schedules and Blaxel's agent, model and MCP hosting throw
125
+ `NotSupportedError` before anything happens, naming what to use instead.
126
+ `BLAXEL.md` in this package lists every mapping and gap.
127
+
110
128
  ## Agent frameworks
111
129
 
112
130
  `withruntime/openai-agents` is a sandbox client for the OpenAI Agents SDK's
@@ -0,0 +1,48 @@
1
+ import { Runtime } from "../client.js";
2
+ /** The Runtime sandbox the adapter drives. */
3
+ export type RuntimeSandbox = Awaited<ReturnType<Runtime["sandboxes"]["create"]>>;
4
+ /** What `SandboxInstance.create` sends to Runtime. */
5
+ export type RuntimeCreate = NonNullable<Parameters<Runtime["sandboxes"]["create"]>[0]>;
6
+ /** Runtime-only options, for code that has been through the switch. */
7
+ export interface WithRuntime {
8
+ /** A Runtime client to use instead of one made from the key. */
9
+ client?: Runtime;
10
+ /** Fields sent as they are with the create, over the adapter's own: for
11
+ * example `{ funding: "trial" }` or `{ vcpu: 4 }`. */
12
+ create?: Partial<RuntimeCreate>;
13
+ }
14
+ /** Options every static call takes, for code that has been through the
15
+ * switch: `SandboxInstance.get(name, { withruntime: { client } })`. */
16
+ export interface RuntimeOptions {
17
+ withruntime?: WithRuntime;
18
+ }
19
+ /** Blaxel's `initialize` / `settings.setConfig` fields, as far as they mean
20
+ * anything on Runtime. */
21
+ export interface Config {
22
+ /** A Runtime key (`rtcloud_...`) is used; a Blaxel key is refused, never sent. */
23
+ apikey?: string;
24
+ apiKey?: string;
25
+ workspace?: string;
26
+ proxy?: string;
27
+ clientCredentials?: string | {
28
+ clientId: string;
29
+ clientSecret: string;
30
+ };
31
+ [key: string]: unknown;
32
+ }
33
+ /** Blaxel's `initialize(config)`: a Runtime key given as `apikey` is used from
34
+ * then on. A Blaxel key is refused while RUNTIME_API_KEY is not set, and never
35
+ * sent. `workspace` means nothing on Runtime and is ignored. */
36
+ export declare function initialize(config?: Config): void;
37
+ /** The key to use: one given to `initialize`, then RUNTIME_API_KEY, then
38
+ * BL_API_KEY when it holds a Runtime key; undefined lets the Runtime SDK find
39
+ * the key `npx withruntime login` saved. A Blaxel key in BL_API_KEY is never
40
+ * sent anywhere. */
41
+ export declare function pickKey(): string | undefined;
42
+ /** The Runtime client for these options: an explicit client, else one per
43
+ * key, made once and reused so its connections stay open. */
44
+ export declare function clientFor(options?: RuntimeOptions): Runtime;
45
+ /** Test hook: forget the cached clients and any key from `initialize`. */
46
+ export declare function resetClients(): void;
47
+ /** Blaxel's default region, from BL_REGION. */
48
+ export declare function defaultRegion(): string | undefined;
@@ -0,0 +1,58 @@
1
+ import { Runtime } from "../client.js";
2
+ import { CredentialsError, NotSupportedError } from "./errors.js";
3
+ const env = (name) => typeof process === "undefined" ? undefined : process.env[name];
4
+ const isRuntimeKey = (key) => key.startsWith("rtcloud_");
5
+ let configured;
6
+ /** Blaxel's `initialize(config)`: a Runtime key given as `apikey` is used from
7
+ * then on. A Blaxel key is refused while RUNTIME_API_KEY is not set, and never
8
+ * sent. `workspace` means nothing on Runtime and is ignored. */
9
+ export function initialize(config = {}) {
10
+ if (config.clientCredentials !== undefined)
11
+ throw new NotSupportedError("Blaxel client credentials (clientCredentials)", "Use a Runtime key: set RUNTIME_API_KEY, or run `npx withruntime login` once.");
12
+ const key = config.apikey ?? config.apiKey;
13
+ if (key === undefined || key === "")
14
+ return;
15
+ if (isRuntimeKey(key)) {
16
+ configured = key;
17
+ return;
18
+ }
19
+ if (!env("RUNTIME_API_KEY"))
20
+ throw new CredentialsError("The apikey passed is a Blaxel API key, and it was not sent. Set RUNTIME_API_KEY to a Runtime key " +
21
+ "(https://withruntime.com/account/keys, or run `npx withruntime login`), or pass the Runtime key as apikey.");
22
+ }
23
+ /** The key to use: one given to `initialize`, then RUNTIME_API_KEY, then
24
+ * BL_API_KEY when it holds a Runtime key; undefined lets the Runtime SDK find
25
+ * the key `npx withruntime login` saved. A Blaxel key in BL_API_KEY is never
26
+ * sent anywhere. */
27
+ export function pickKey() {
28
+ if (configured)
29
+ return configured;
30
+ const runtimeKey = env("RUNTIME_API_KEY");
31
+ if (runtimeKey)
32
+ return runtimeKey;
33
+ const blaxel = env("BL_API_KEY");
34
+ return blaxel && isRuntimeKey(blaxel) ? blaxel : undefined;
35
+ }
36
+ const clients = new Map();
37
+ /** The Runtime client for these options: an explicit client, else one per
38
+ * key, made once and reused so its connections stay open. */
39
+ export function clientFor(options = {}) {
40
+ if (options.withruntime?.client)
41
+ return options.withruntime.client;
42
+ const key = pickKey();
43
+ let client = clients.get(key ?? "");
44
+ if (!client) {
45
+ client = new Runtime(key ? { apiKey: key } : {});
46
+ clients.set(key ?? "", client);
47
+ }
48
+ return client;
49
+ }
50
+ /** Test hook: forget the cached clients and any key from `initialize`. */
51
+ export function resetClients() {
52
+ clients.clear();
53
+ configured = undefined;
54
+ }
55
+ /** Blaxel's default region, from BL_REGION. */
56
+ export function defaultRegion() {
57
+ return env("BL_REGION") || undefined;
58
+ }
@@ -0,0 +1,59 @@
1
+ /** Blaxel's stock images work in /blaxel (WORKDIR and HOME); Runtime's in
2
+ * /workspace. */
3
+ export declare const HOME = "/blaxel";
4
+ export declare const RUNTIME_HOME = "/workspace";
5
+ /** The sandbox's envs, kept on the sandbox itself so every process sees them,
6
+ * from any client. Readable by the sandbox user, writable only with sudo. */
7
+ export declare const ENV_FILE = "/etc/runtime-blaxel/env";
8
+ /** A Blaxel path as the Runtime path it stands for: relative paths, `~` and
9
+ * `/blaxel` resolve against /workspace, as they resolve against /blaxel on
10
+ * Blaxel; any other absolute path is itself. */
11
+ export declare function toRuntimePath(path: string): string;
12
+ /** A Runtime path as Blaxel code expects to see it: /workspace is /blaxel. */
13
+ export declare function toBlaxelPath(path: string): string;
14
+ /** Single-quoted for bash: any text, taken literally. */
15
+ export declare function quote(text: string): string;
16
+ /** Refuses an env name no process could receive. */
17
+ export declare function checkEnvNames(names: Iterable<string>): void;
18
+ /** The env file's lines: each exports its variable unless the process was
19
+ * given its own value (named in RUNTIME_BLAXEL_KEEP), so a process's `env`
20
+ * wins over the sandbox's, as in Blaxel. Lines stand alone, so a fork's
21
+ * envs are appended after the source's and win. */
22
+ export declare function envFileLines(envs: Record<string, string>): string;
23
+ /** The envs an env file holds; later lines win, as when it is sourced. */
24
+ export declare function parseEnvFile(text: string): Record<string, string>;
25
+ /** Writes the env file (replacing it, or appending to it): one command, with
26
+ * the content on standard input so no value appears in a command line. */
27
+ export declare function envFileCommand(append: boolean): string[];
28
+ /** Marks a keepAlive process's command line, so any client can tell whether
29
+ * one still runs before it gives the sandbox its idle pause back. */
30
+ export declare const KEEP_MARK = ": rt-blaxel-keep";
31
+ /** The shell line a Blaxel process runs as. The Python adapter writes the
32
+ * same line, byte for byte, so either reads what the other started:
33
+ *
34
+ * : rt-blaxel '<name>'; [: rt-blaxel-keep; ]export HOST="${HOST:-0.0.0.0}";
35
+ * [ -r /etc/runtime-blaxel/env ] && . /etc/runtime-blaxel/env;
36
+ * unset RUNTIME_BLAXEL_KEEP; [{ [ -e /blaxel ] || sudo ln -s /workspace
37
+ * /blaxel; } 2>/dev/null; ]__rt_cmd='<command>'; export __rt_cmd; exec sudo
38
+ * -E env "PATH=$PATH" "HOME=$HOME" bash -c '<run>'
39
+ *
40
+ * joined with "; " on one line, where <run> is `eval "$__rt_cmd"`, or the
41
+ * restart loop below. Quotes are single, with ' written '\''. */
42
+ export declare function processLine(command: string, options: {
43
+ name: string;
44
+ maxRestarts?: number;
45
+ linkHome?: boolean;
46
+ keepAlive?: boolean;
47
+ }): string;
48
+ /** The Blaxel name and command of a process this adapter started, from the
49
+ * command Runtime recorded (cut at 256 characters: a long command is cut
50
+ * too, its name kept); undefined for any other process. */
51
+ export declare function parseProcessLine(line: string): {
52
+ name: string;
53
+ command: string;
54
+ keepAlive: boolean;
55
+ } | undefined;
56
+ /** A time as Blaxel's sandbox API writes it: "Mon, 02 Jan 2006 15:04:05 GMT". */
57
+ export declare function httpDate(time: string | number | Date | null | undefined): string;
58
+ /** A random name, as Blaxel gives an unnamed process. */
59
+ export declare function randomName(): string;
@@ -0,0 +1,162 @@
1
+ import { NotSupportedError } from "./errors.js";
2
+ /* What every part of the adapter shares: where Blaxel's paths land on
3
+ Runtime, the sandbox's environment file, and the shell line each Blaxel
4
+ process runs in. */
5
+ /** Blaxel's stock images work in /blaxel (WORKDIR and HOME); Runtime's in
6
+ * /workspace. */
7
+ export const HOME = "/blaxel";
8
+ export const RUNTIME_HOME = "/workspace";
9
+ /** The sandbox's envs, kept on the sandbox itself so every process sees them,
10
+ * from any client. Readable by the sandbox user, writable only with sudo. */
11
+ export const ENV_FILE = "/etc/runtime-blaxel/env";
12
+ /** A Blaxel path as the Runtime path it stands for: relative paths, `~` and
13
+ * `/blaxel` resolve against /workspace, as they resolve against /blaxel on
14
+ * Blaxel; any other absolute path is itself. */
15
+ export function toRuntimePath(path) {
16
+ let absolute = path.replace(/\/{2,}/g, "/");
17
+ if (absolute === "~" || absolute.startsWith("~/"))
18
+ absolute = HOME + absolute.slice(1);
19
+ if (!absolute.startsWith("/")) {
20
+ const relative = absolute.replace(/^(\.\/)+/, "").replace(/^\.$/, "");
21
+ absolute = relative ? `${HOME}/${relative}` : HOME;
22
+ }
23
+ const normal = normalize(absolute);
24
+ if (normal === HOME || normal.startsWith(`${HOME}/`))
25
+ return RUNTIME_HOME + normal.slice(HOME.length);
26
+ return normal;
27
+ }
28
+ /** A Runtime path as Blaxel code expects to see it: /workspace is /blaxel. */
29
+ export function toBlaxelPath(path) {
30
+ if (path === RUNTIME_HOME || path.startsWith(`${RUNTIME_HOME}/`))
31
+ return HOME + path.slice(RUNTIME_HOME.length);
32
+ return path;
33
+ }
34
+ function normalize(path) {
35
+ const parts = [];
36
+ for (const part of path.split("/")) {
37
+ if (part === "" || part === ".")
38
+ continue;
39
+ if (part === "..")
40
+ parts.pop();
41
+ else
42
+ parts.push(part);
43
+ }
44
+ return `/${parts.join("/")}`;
45
+ }
46
+ /** Single-quoted for bash: any text, taken literally. */
47
+ export function quote(text) {
48
+ return `'${text.replace(/'/g, `'\\''`)}'`;
49
+ }
50
+ function unquote(text) {
51
+ return text.replace(/'\\''/g, "'");
52
+ }
53
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
54
+ /** Refuses an env name no process could receive. */
55
+ export function checkEnvNames(names) {
56
+ for (const name of names)
57
+ if (!IDENTIFIER.test(name))
58
+ throw new NotSupportedError(`The environment variable name "${name}"`, "Use a name of letters, digits and underscores that does not start with a digit.");
59
+ }
60
+ /** The env file's lines: each exports its variable unless the process was
61
+ * given its own value (named in RUNTIME_BLAXEL_KEEP), so a process's `env`
62
+ * wins over the sandbox's, as in Blaxel. Lines stand alone, so a fork's
63
+ * envs are appended after the source's and win. */
64
+ export function envFileLines(envs) {
65
+ checkEnvNames(Object.keys(envs));
66
+ return Object.entries(envs)
67
+ .map(([name, value]) => `case ":\${RUNTIME_BLAXEL_KEEP-}:" in *:${name}:*) ;; *) export ${name}=${quote(value)} ;; esac\n`)
68
+ .join("");
69
+ }
70
+ /** The envs an env file holds; later lines win, as when it is sourced. */
71
+ export function parseEnvFile(text) {
72
+ const envs = {};
73
+ for (const [, name, value] of text.matchAll(/\*\) export ([A-Za-z_][A-Za-z0-9_]*)='((?:[^']|'\\'')*)' ;; esac/g))
74
+ envs[name] = unquote(value);
75
+ return envs;
76
+ }
77
+ /** Writes the env file (replacing it, or appending to it): one command, with
78
+ * the content on standard input so no value appears in a command line. */
79
+ export function envFileCommand(append) {
80
+ const script = append
81
+ ? `sudo mkdir -p /etc/runtime-blaxel && sudo sh -c 'cat >> ${ENV_FILE}' && sudo chgrp "$(id -g)" ${ENV_FILE} && sudo chmod 0640 ${ENV_FILE}`
82
+ : `sudo install -D -m 0640 -g "$(id -g)" /dev/stdin ${ENV_FILE}`;
83
+ return ["bash", "-c", script];
84
+ }
85
+ const MARK = ": rt-blaxel";
86
+ /** Marks a keepAlive process's command line, so any client can tell whether
87
+ * one still runs before it gives the sandbox its idle pause back. */
88
+ export const KEEP_MARK = ": rt-blaxel-keep";
89
+ /** How a Blaxel process becomes root, as on Blaxel: the sandbox user's shell
90
+ * hands over to sudo (passwordless in Runtime's image), keeping every
91
+ * variable, the sandbox user's PATH (sudo's secure_path would drop
92
+ * /workspace/.local/bin) and HOME. `exec` leaves no extra shell: a signal to
93
+ * the process reaches sudo, which Runtime ends with the whole process tree
94
+ * (checked 27 September 2026: SIGTERM exits -15, SIGKILL is killed -9, and
95
+ * no process of the command is left). */
96
+ const AS_ROOT = 'export __rt_cmd; exec sudo -E env "PATH=$PATH" "HOME=$HOME" bash -c';
97
+ /** The shell line a Blaxel process runs as. The Python adapter writes the
98
+ * same line, byte for byte, so either reads what the other started:
99
+ *
100
+ * : rt-blaxel '<name>'; [: rt-blaxel-keep; ]export HOST="${HOST:-0.0.0.0}";
101
+ * [ -r /etc/runtime-blaxel/env ] && . /etc/runtime-blaxel/env;
102
+ * unset RUNTIME_BLAXEL_KEEP; [{ [ -e /blaxel ] || sudo ln -s /workspace
103
+ * /blaxel; } 2>/dev/null; ]__rt_cmd='<command>'; export __rt_cmd; exec sudo
104
+ * -E env "PATH=$PATH" "HOME=$HOME" bash -c '<run>'
105
+ *
106
+ * joined with "; " on one line, where <run> is `eval "$__rt_cmd"`, or the
107
+ * restart loop below. Quotes are single, with ' written '\''. */
108
+ export function processLine(command, options) {
109
+ // Runtime keeps the first 256 characters of a command line: the name and
110
+ // the keepAlive mark come first, so a long command costs only its own tail.
111
+ const prelude = [
112
+ `${MARK} ${quote(options.name)}`,
113
+ ...(options.keepAlive ? [KEEP_MARK] : []),
114
+ 'export HOST="${HOST:-0.0.0.0}"',
115
+ `[ -r ${ENV_FILE} ] && . ${ENV_FILE}`,
116
+ "unset RUNTIME_BLAXEL_KEEP",
117
+ ...(options.linkHome
118
+ ? [`{ [ -e ${HOME} ] || sudo ln -s ${RUNTIME_HOME} ${HOME}; } 2>/dev/null`]
119
+ : []),
120
+ `__rt_cmd=${quote(command)}`,
121
+ ];
122
+ if (options.maxRestarts === undefined)
123
+ return [...prelude, `${AS_ROOT} ${quote('eval "$__rt_cmd"')}`].join("; ");
124
+ const limit = options.maxRestarts < 0 ? "unlimited" : String(options.maxRestarts);
125
+ // Blaxel's restart loop: a failed run starts again, up to maxRestarts times
126
+ // (negative is unlimited), with the same note in the output.
127
+ const loop = `__rt_n=0; while :; do ( eval "$__rt_cmd" ); __rt_c=$?; [ $__rt_c -eq 0 ] && exit 0; ` +
128
+ `${options.maxRestarts < 0 ? "" : `[ $__rt_n -ge ${options.maxRestarts} ] && exit $__rt_c; `}` +
129
+ `__rt_n=$((__rt_n+1)); printf '\\n[Process failed with exit code %d. Attempting restart %d/%s...]\\n' $__rt_c $__rt_n '${limit}'; done`;
130
+ return [...prelude, `${AS_ROOT} ${quote(loop)}`].join("; ");
131
+ }
132
+ /** The Blaxel name and command of a process this adapter started, from the
133
+ * command Runtime recorded (cut at 256 characters: a long command is cut
134
+ * too, its name kept); undefined for any other process. */
135
+ export function parseProcessLine(line) {
136
+ const name = /: rt-blaxel '((?:[^']|'\\'')*)'/.exec(line);
137
+ if (!name)
138
+ return undefined;
139
+ const whole = /__rt_cmd='((?:[^']|'\\'')*)'/.exec(line);
140
+ const partial = /__rt_cmd='(.*)$/.exec(line);
141
+ const command = whole ? whole[1] : partial ? partial[1].replace(/'\\?'?$/, "") : "";
142
+ return {
143
+ name: unquote(name[1]),
144
+ command: unquote(command),
145
+ keepAlive: line.includes(`${KEEP_MARK};`),
146
+ };
147
+ }
148
+ /** A time as Blaxel's sandbox API writes it: "Mon, 02 Jan 2006 15:04:05 GMT". */
149
+ export function httpDate(time) {
150
+ if (time === null || time === undefined || time === "")
151
+ return "";
152
+ const date = new Date(time);
153
+ return Number.isNaN(date.getTime()) ? "" : date.toUTCString();
154
+ }
155
+ /** A random name, as Blaxel gives an unnamed process. */
156
+ export function randomName() {
157
+ const letters = "abcdefghijklmnopqrstuvwxyz0123456789";
158
+ let name = "";
159
+ for (let i = 0; i < 8; i++)
160
+ name += letters[Math.floor(Math.random() * letters.length)];
161
+ return name;
162
+ }
@@ -0,0 +1,51 @@
1
+ /** An error answer. `status` is the HTTP status; `code` is the same number,
2
+ * as Blaxel's control-plane errors carry it (`e.code === 404`); `data` is
3
+ * `{ error, code, hint, requestId }`. */
4
+ export declare class ResponseError extends Error {
5
+ response: Response;
6
+ error: unknown;
7
+ readonly status?: number;
8
+ readonly statusText?: string;
9
+ readonly data: unknown;
10
+ code?: number;
11
+ runtimeCode?: string;
12
+ hint?: string;
13
+ requestId?: string;
14
+ constructor(response: Response, data: unknown, error: unknown);
15
+ }
16
+ /** A 502, 503 or 504: nothing usable came back. Safe to retry an idempotent
17
+ * call. */
18
+ export declare class SandboxGatewayError extends ResponseError {
19
+ }
20
+ /** True when `err` carries a gateway status (502, 503 or 504). */
21
+ export declare function isGatewayError(err: unknown): boolean;
22
+ /** True when `err` is a gateway timeout (504). */
23
+ export declare function isGatewayTimeout(err: unknown): boolean;
24
+ /** No Runtime key was found. A Blaxel key (BL_API_KEY) is never sent. */
25
+ export declare class CredentialsError extends Error {
26
+ constructor(message: string);
27
+ }
28
+ /** A call that Blaxel supports and Runtime does not, or not in the same way.
29
+ * Thrown before anything is done, never after doing something different.
30
+ * `feature` names what was asked for; `alternative` says what to use on
31
+ * Runtime instead. */
32
+ export declare class NotSupportedError extends Error {
33
+ readonly code = "not_supported";
34
+ readonly feature: string;
35
+ readonly alternative: string;
36
+ constructor(feature: string, alternative: string, message?: string);
37
+ }
38
+ /** A ResponseError of this status and message, made here rather than by
39
+ * Runtime: a process or preview that is not there, a wait that ran out. */
40
+ export declare function responseError(status: number, message: string): ResponseError;
41
+ /** A Runtime SDK error as Blaxel's: ResponseError (SandboxGatewayError for a
42
+ * 502, 503 or 504), NotSupportedError for a product Runtime has switched off,
43
+ * CredentialsError when there is no Runtime key. Anything else passes through
44
+ * unchanged. */
45
+ export declare function translate(error: unknown): unknown;
46
+ /** Runs `work`, turning any Runtime error into Blaxel's. */
47
+ export declare function guard<T>(work: () => Promise<T>): Promise<T>;
48
+ /** True for a 404 in either shape: Runtime's or Blaxel's. */
49
+ export declare function isNotFound(error: unknown): boolean;
50
+ /** Runtime's error code on either shape. */
51
+ export declare function codeOf(error: unknown): string | undefined;