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.
- package/BLAXEL.md +4 -4
- package/DAYTONA.md +52 -40
- package/E2B.md +65 -62
- package/README.md +15 -7
- package/README.src.md +15 -7
- package/VERCEL.md +34 -22
- package/compatibility-lock.json +16 -16
- package/dist/blaxel/errors.js +1 -1
- package/dist/blaxel/lookup.d.ts +3 -2
- package/dist/blaxel/lookup.js +3 -2
- package/dist/blaxel/pagination.d.ts +2 -2
- package/dist/blaxel/pagination.js +45 -17
- package/dist/blaxel/preview.d.ts +2 -1
- package/dist/blaxel/preview.js +15 -13
- package/dist/blaxel/process.js +56 -10
- package/dist/blaxel/sandbox.d.ts +2 -0
- package/dist/blaxel/sandbox.js +2 -0
- package/dist/blaxel/snapshot.js +3 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +262 -56
- package/dist/client.d.ts +41 -3
- package/dist/client.js +67 -11
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +9 -0
- package/dist/cloudflare/index.js +47 -9
- package/dist/cloudflare/worker.d.ts +4 -0
- package/dist/cloudflare/worker.js +166 -16
- package/dist/codesandbox/guest-service.js +57 -20
- package/dist/codesandbox/index.js +42 -8
- package/dist/compat/core.d.ts +9 -8
- package/dist/compat/core.js +60 -21
- package/dist/compat/files.js +22 -4
- package/dist/compat/lease.d.ts +36 -0
- package/dist/compat/lease.js +87 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/computer-use.d.ts +162 -0
- package/dist/daytona/computer-use.js +324 -0
- package/dist/daytona/context.d.ts +10 -0
- package/dist/daytona/context.js +24 -0
- package/dist/daytona/daytona.d.ts +20 -0
- package/dist/daytona/daytona.js +134 -20
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +129 -38
- package/dist/daytona/index.d.ts +3 -27
- package/dist/daytona/index.js +2 -7
- package/dist/daytona/process.d.ts +63 -6
- package/dist/daytona/process.js +224 -23
- package/dist/daytona/sandbox.d.ts +20 -6
- package/dist/daytona/sandbox.js +55 -32
- package/dist/e2b/client.d.ts +5 -0
- package/dist/e2b/client.js +6 -2
- package/dist/e2b/commands.d.ts +43 -12
- package/dist/e2b/commands.js +217 -64
- package/dist/e2b/errors.d.ts +7 -0
- package/dist/e2b/errors.js +20 -0
- package/dist/e2b/filesystem.d.ts +2 -1
- package/dist/e2b/filesystem.js +43 -6
- package/dist/e2b/index.d.ts +3 -2
- package/dist/e2b/index.js +2 -1
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +83 -0
- package/dist/e2b/sandbox.d.ts +17 -6
- package/dist/e2b/sandbox.js +239 -43
- package/dist/e2b/users.d.ts +30 -0
- package/dist/e2b/users.js +185 -0
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +9 -1
- package/dist/freestyle/index.js +38 -5
- package/dist/index.d.ts +3 -1
- package/dist/login.js +16 -10
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +6 -1
- package/dist/modal/index.js +2 -1
- package/dist/network-cli.js +139 -43
- package/dist/openai-agents/index.js +9 -45
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/browser.d.ts +60 -0
- package/dist/products/browser.js +37 -0
- package/dist/products/desktop.d.ts +2 -2
- package/dist/products/desktop.js +2 -2
- package/dist/products/images.d.ts +3 -1
- package/dist/products/images.js +14 -2
- package/dist/products/index.d.ts +14 -0
- package/dist/products/index.js +12 -0
- package/dist/products/network-products.d.ts +7 -0
- package/dist/products/network-products.js +3 -0
- package/dist/products/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +8 -2
- package/dist/products/previews.js +11 -3
- package/dist/products/private-network.d.ts +22 -0
- package/dist/products/private-network.js +10 -0
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.js +61 -8
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +13 -27
- package/dist/runloop/index.js +149 -52
- package/dist/sandbox.d.ts +59 -8
- package/dist/sandbox.js +479 -68
- package/dist/snapshots.d.ts +9 -4
- package/dist/sprites/index.js +69 -33
- package/dist/tar.d.ts +3 -1
- package/dist/tar.js +140 -14
- package/dist/transport.d.ts +4 -7
- package/dist/transport.js +205 -122
- package/dist/types.d.ts +5 -2
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +3 -2
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +7 -1
- package/dist/vercel/index.d.ts +1 -7
- package/dist/vercel/index.js +1 -3
- package/dist/vercel/sandbox-user.d.ts +70 -0
- package/dist/vercel/sandbox-user.js +206 -0
- package/dist/vercel/sandbox.d.ts +55 -20
- package/dist/vercel/sandbox.js +353 -99
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- package/package.json +2 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
2
|
import type { Page } from "./page.js";
|
|
3
3
|
import { type ClientExtensions } from "./products/index.js";
|
|
4
|
+
import { usageExport as makeUsageExport } from "./products/observability.js";
|
|
4
5
|
import { Sandbox } from "./sandbox.js";
|
|
5
6
|
import { Snapshots } from "./snapshots.js";
|
|
6
7
|
import { Transport, type RequestOptions } from "./transport.js";
|
|
@@ -11,7 +12,7 @@ export type RuntimeOptions = {
|
|
|
11
12
|
/** Default: RUNTIME_API_URL, then https://api.withruntime.com. */
|
|
12
13
|
baseUrl?: string;
|
|
13
14
|
fetch?: typeof fetch;
|
|
14
|
-
/** Per call, retries included. Default 5 minutes. */
|
|
15
|
+
/** Per call, queue and retries included. Default 5 minutes; 0 disables it. */
|
|
15
16
|
timeoutMs?: number;
|
|
16
17
|
/** Retries of transport failures, 429, 502, 503 and 504. Default 4. */
|
|
17
18
|
maxRetries?: number;
|
|
@@ -30,11 +31,16 @@ export interface Runtime extends ClientExtensions {
|
|
|
30
31
|
}
|
|
31
32
|
export declare class Runtime {
|
|
32
33
|
readonly transport: Transport;
|
|
34
|
+
/** The sandbox product; `sandboxes` remains the same client for compatibility. */
|
|
35
|
+
readonly sandbox: Sandboxes;
|
|
33
36
|
readonly sandboxes: Sandboxes;
|
|
37
|
+
readonly snapshot: Snapshots;
|
|
34
38
|
readonly snapshots: Snapshots;
|
|
35
39
|
readonly feedback: FeedbackApi;
|
|
36
40
|
readonly support: SupportApi;
|
|
37
41
|
readonly account: AccountApi;
|
|
42
|
+
/** Date-range settlements, paged with exact charges and the server's CSV. */
|
|
43
|
+
readonly usageExport: ReturnType<typeof makeUsageExport>;
|
|
38
44
|
constructor(options?: RuntimeOptions);
|
|
39
45
|
/** Who this key is: organization, agent and credential. */
|
|
40
46
|
me(options?: RequestOptions): Promise<{
|
|
@@ -50,6 +56,25 @@ export declare class Runtime {
|
|
|
50
56
|
/** Balance, holds, trial time and per-resource charges, in integer microdollars. */
|
|
51
57
|
/** Credit, holds, trial time and per-resource charges. */
|
|
52
58
|
usage(options?: RequestOptions): Promise<Usage>;
|
|
59
|
+
/** This account's authenticated API answers, in hourly windows. Counts are
|
|
60
|
+
* exact decimal strings, and an empty window has a null error percentage.
|
|
61
|
+
* A key needs usage permission. */
|
|
62
|
+
usageRequests(range?: "24h" | "7d" | "30d" | "90d", options?: RequestOptions): Promise<{
|
|
63
|
+
range: "24h" | "7d" | "30d" | "90d";
|
|
64
|
+
since: string;
|
|
65
|
+
until: string;
|
|
66
|
+
calls: string;
|
|
67
|
+
clientErrors: string;
|
|
68
|
+
serverErrors: string;
|
|
69
|
+
errorPercent: number | null;
|
|
70
|
+
operations: Array<{
|
|
71
|
+
operation: string;
|
|
72
|
+
calls: string;
|
|
73
|
+
clientErrors: string;
|
|
74
|
+
serverErrors: string;
|
|
75
|
+
errorPercent: number | null;
|
|
76
|
+
}>;
|
|
77
|
+
}>;
|
|
53
78
|
}
|
|
54
79
|
/** How `sandboxes.create` waits when every trial slot, the account's quota or
|
|
55
80
|
* the region is full. */
|
|
@@ -83,7 +108,8 @@ export declare class Sandboxes {
|
|
|
83
108
|
/** Deletes a sandbox for good, by id, without reading it first: see
|
|
84
109
|
* `sandbox.delete()`. Deleting it again answers the same. */
|
|
85
110
|
delete(id: string, options?: RequestOptions): Promise<DeletedSandbox>;
|
|
86
|
-
/** Live sandboxes, oldest first. Await for a page, or `for await` over all.
|
|
111
|
+
/** Live sandboxes, oldest first. Await for a page, or `for await` over all.
|
|
112
|
+
* Request options apply to the initial request and every subsequent page. */
|
|
87
113
|
list(filter?: {
|
|
88
114
|
state?: SandboxInfo["state"][];
|
|
89
115
|
includeStopped?: boolean;
|
|
@@ -91,7 +117,19 @@ export declare class Sandboxes {
|
|
|
91
117
|
labels?: Record<string, string>;
|
|
92
118
|
name?: string;
|
|
93
119
|
limit?: number;
|
|
94
|
-
}): Promise<Page<Sandbox>>;
|
|
120
|
+
}, options?: RequestOptions): Promise<Page<Sandbox>>;
|
|
121
|
+
/** Stops every live sandbox whose labels all match, eight at a time, and
|
|
122
|
+
* says which stopped and which failed; one failure does not stop the rest.
|
|
123
|
+
* Needs at least one label, so stopping everything is never one call. */
|
|
124
|
+
stopAll(filter: {
|
|
125
|
+
labels: Record<string, string>;
|
|
126
|
+
}, options?: RequestOptions): Promise<{
|
|
127
|
+
stopped: string[];
|
|
128
|
+
failed: {
|
|
129
|
+
id: string;
|
|
130
|
+
error: unknown;
|
|
131
|
+
}[];
|
|
132
|
+
}>;
|
|
95
133
|
}
|
|
96
134
|
export declare class FeedbackApi {
|
|
97
135
|
private readonly t;
|
package/dist/client.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { WAIT_FOR_TIMEOUT_SECONDS } from "./api-defaults.js";
|
|
2
2
|
import { RuntimeError } from "./errors.js";
|
|
3
|
-
import { clientFactories } from "./products/index.js";
|
|
3
|
+
import { clientFactories, clientProductAliases } from "./products/index.js";
|
|
4
|
+
import { usageExport as makeUsageExport } from "./products/observability.js";
|
|
4
5
|
import { Sandbox, deleteSandbox, sandboxPage } from "./sandbox.js";
|
|
5
6
|
import { Snapshots } from "./snapshots.js";
|
|
6
7
|
import { Transport, missingKey } from "./transport.js";
|
|
7
|
-
/** The key `runtime login` saved for this machine
|
|
8
|
-
|
|
8
|
+
/** The key `runtime login` saved for this machine and this API origin, found
|
|
9
|
+
* on first use. A constructor override must never receive another API's key. */
|
|
10
|
+
const savedKey = async (apiOrigin) => {
|
|
9
11
|
if (typeof process === "undefined" || !process.versions?.node)
|
|
10
12
|
throw missingKey();
|
|
11
13
|
const { resolveCredential } = await import("./credentials.js");
|
|
12
|
-
return resolveCredential(process.env);
|
|
14
|
+
return resolveCredential({ ...process.env, RUNTIME_API_URL: apiOrigin });
|
|
13
15
|
};
|
|
14
16
|
const env = (name) => typeof process === "undefined"
|
|
15
17
|
? undefined
|
|
@@ -17,32 +19,42 @@ const env = (name) => typeof process === "undefined"
|
|
|
17
19
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
18
20
|
export class Runtime {
|
|
19
21
|
transport;
|
|
22
|
+
/** The sandbox product; `sandboxes` remains the same client for compatibility. */
|
|
23
|
+
sandbox;
|
|
20
24
|
sandboxes;
|
|
25
|
+
snapshot;
|
|
21
26
|
snapshots;
|
|
22
27
|
feedback;
|
|
23
28
|
support;
|
|
24
29
|
account;
|
|
30
|
+
/** Date-range settlements, paged with exact charges and the server's CSV. */
|
|
31
|
+
usageExport;
|
|
25
32
|
constructor(options = {}) {
|
|
26
33
|
this.transport = new Transport({
|
|
27
34
|
// A key given, then RUNTIME_API_KEY, then (in Node and Bun) the key
|
|
28
35
|
// `runtime login` saved for this machine, read on the first call.
|
|
29
|
-
apiKey: options.apiKey ?? env("RUNTIME_API_KEY") ?? savedKey,
|
|
36
|
+
apiKey: options.apiKey ?? env("RUNTIME_API_KEY") ?? (() => savedKey(this.transport.baseUrl)),
|
|
30
37
|
baseUrl: options.baseUrl ?? env("RUNTIME_API_URL"),
|
|
31
38
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
32
|
-
...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}),
|
|
39
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
33
40
|
...(options.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
|
|
34
|
-
...(options.maxConnections ? { maxConnections: options.maxConnections } : {}),
|
|
41
|
+
...(options.maxConnections !== undefined ? { maxConnections: options.maxConnections } : {}),
|
|
35
42
|
...(options.waitForCapacityMs !== undefined
|
|
36
43
|
? { waitForCapacityMs: options.waitForCapacityMs }
|
|
37
44
|
: {}),
|
|
38
45
|
});
|
|
39
46
|
this.sandboxes = new Sandboxes(this.transport);
|
|
47
|
+
this.sandbox = this.sandboxes;
|
|
40
48
|
this.snapshots = new Snapshots(this.transport);
|
|
49
|
+
this.snapshot = this.snapshots;
|
|
41
50
|
this.feedback = new FeedbackApi(this.transport);
|
|
42
51
|
this.support = new SupportApi(this.transport);
|
|
43
52
|
this.account = new AccountApi(this.transport);
|
|
53
|
+
this.usageExport = makeUsageExport(this.transport);
|
|
44
54
|
for (const [name, make] of clientFactories())
|
|
45
55
|
Object.defineProperty(this, name, { value: make(this.transport), enumerable: true });
|
|
56
|
+
for (const [alias, name] of Object.entries(clientProductAliases))
|
|
57
|
+
Object.defineProperty(this, alias, { value: this[name], enumerable: true });
|
|
46
58
|
}
|
|
47
59
|
/** Who this key is: organization, agent and credential. */
|
|
48
60
|
me(options = {}) {
|
|
@@ -61,6 +73,17 @@ export class Runtime {
|
|
|
61
73
|
...options,
|
|
62
74
|
});
|
|
63
75
|
}
|
|
76
|
+
/** This account's authenticated API answers, in hourly windows. Counts are
|
|
77
|
+
* exact decimal strings, and an empty window has a null error percentage.
|
|
78
|
+
* A key needs usage permission. */
|
|
79
|
+
usageRequests(range = "24h", options = {}) {
|
|
80
|
+
return this.transport.json({
|
|
81
|
+
method: "GET",
|
|
82
|
+
path: "/v1/usage/requests",
|
|
83
|
+
query: { range },
|
|
84
|
+
...options,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
64
87
|
}
|
|
65
88
|
export class Sandboxes {
|
|
66
89
|
t;
|
|
@@ -84,7 +107,12 @@ export class Sandboxes {
|
|
|
84
107
|
});
|
|
85
108
|
const sandbox = new Sandbox(this.t, info);
|
|
86
109
|
if (wait !== false && info.state !== "running") {
|
|
87
|
-
await sandbox.waitFor("running", {
|
|
110
|
+
await sandbox.waitFor("running", {
|
|
111
|
+
timeoutSeconds: WAIT_FOR_TIMEOUT_SECONDS,
|
|
112
|
+
signal: options.signal,
|
|
113
|
+
timeoutMs: options.timeoutMs,
|
|
114
|
+
idempotencyKey: options.idempotencyKey,
|
|
115
|
+
});
|
|
88
116
|
if (sandbox.state !== "running")
|
|
89
117
|
throw new RuntimeError({
|
|
90
118
|
message: `Sandbox ${info.id} is ${sandbox.state}, not running.`,
|
|
@@ -114,8 +142,9 @@ export class Sandboxes {
|
|
|
114
142
|
delete(id, options = {}) {
|
|
115
143
|
return deleteSandbox(this.t, id, options);
|
|
116
144
|
}
|
|
117
|
-
/** Live sandboxes, oldest first. Await for a page, or `for await` over all.
|
|
118
|
-
|
|
145
|
+
/** Live sandboxes, oldest first. Await for a page, or `for await` over all.
|
|
146
|
+
* Request options apply to the initial request and every subsequent page. */
|
|
147
|
+
async list(filter = {}, options = {}) {
|
|
119
148
|
const query = {
|
|
120
149
|
...(filter.state ? { state: filter.state } : {}),
|
|
121
150
|
...(filter.includeStopped ? { includeStopped: true } : {}),
|
|
@@ -125,7 +154,34 @@ export class Sandboxes {
|
|
|
125
154
|
...(filter.name ? { name: filter.name } : {}),
|
|
126
155
|
...(filter.limit ? { limit: filter.limit } : {}),
|
|
127
156
|
};
|
|
128
|
-
return sandboxPage(this.t, await this.t.json({ method: "GET", path: "/v1/sandboxes", query }), query);
|
|
157
|
+
return sandboxPage(this.t, await this.t.json({ method: "GET", path: "/v1/sandboxes", query, ...options }), query, options);
|
|
158
|
+
}
|
|
159
|
+
/** Stops every live sandbox whose labels all match, eight at a time, and
|
|
160
|
+
* says which stopped and which failed; one failure does not stop the rest.
|
|
161
|
+
* Needs at least one label, so stopping everything is never one call. */
|
|
162
|
+
async stopAll(filter, options = {}) {
|
|
163
|
+
if (!filter?.labels || Object.keys(filter.labels).length === 0)
|
|
164
|
+
throw new RuntimeError({
|
|
165
|
+
message: "stopAll needs at least one label to match.",
|
|
166
|
+
code: "invalid_request",
|
|
167
|
+
status: 0,
|
|
168
|
+
hint: "Stop one sandbox with sandbox.stop(), or label the ones to stop together.",
|
|
169
|
+
});
|
|
170
|
+
const live = await (await this.list({ labels: filter.labels, limit: 100 }, options)).toArray(Infinity);
|
|
171
|
+
// One key cannot name several stops: each stop makes its own.
|
|
172
|
+
const { idempotencyKey: _one, ...each } = options;
|
|
173
|
+
// In list order, oldest first, whichever finishes first.
|
|
174
|
+
const errors = new Array(live.length);
|
|
175
|
+
let next = 0;
|
|
176
|
+
const worker = async () => {
|
|
177
|
+
for (let at = next++; at < live.length; at = next++)
|
|
178
|
+
await live[at].stop(each).catch((error) => (errors[at] = error ?? "failed"));
|
|
179
|
+
};
|
|
180
|
+
await Promise.all(Array.from({ length: Math.min(8, live.length) }, worker));
|
|
181
|
+
return {
|
|
182
|
+
stopped: live.filter((_, at) => errors[at] === undefined).map((sbx) => sbx.id),
|
|
183
|
+
failed: live.flatMap((sbx, at) => errors[at] === undefined ? [] : [{ id: sbx.id, error: errors[at] }]),
|
|
184
|
+
};
|
|
129
185
|
}
|
|
130
186
|
}
|
|
131
187
|
export class FeedbackApi {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A Durable Object ID includes its namespace; a display name does not. */
|
|
2
|
+
export function workerSandboxIdentity(objectId, stored) {
|
|
3
|
+
if (stored.nativeSandboxId !== undefined) {
|
|
4
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(stored.nativeSandboxId))
|
|
5
|
+
throw new Error("The stored Runtime sandbox identity is invalid");
|
|
6
|
+
return stored.nativeSandboxId;
|
|
7
|
+
}
|
|
8
|
+
if (stored.sandboxName)
|
|
9
|
+
throw new Error("This legacy Durable Object has no namespace-safe Runtime identity. Retain its existing sandbox and migrate its identity explicitly before using it.");
|
|
10
|
+
if (!/^[a-f0-9]{64}$/.test(objectId))
|
|
11
|
+
throw new Error("The Durable Object identity is invalid");
|
|
12
|
+
return `cf-do-${objectId}`;
|
|
13
|
+
}
|
|
@@ -70,6 +70,7 @@ export interface ProcessOptions {
|
|
|
70
70
|
onStart?: (process: Process) => void;
|
|
71
71
|
onError?: (error: Error) => void;
|
|
72
72
|
}
|
|
73
|
+
export declare function validateSandboxConfiguration(options: Pick<SandboxOptions, "sleepAfter" | "keepAlive">): void;
|
|
73
74
|
/** Worker bindings are deliberately not inspected or invoked. Supply Runtime credentials
|
|
74
75
|
* through options.runtime in Workers; Node also supports RUNTIME_API_KEY/login. */
|
|
75
76
|
export declare function getSandbox(namespace: unknown, id: string, options?: SandboxOptions): Sandbox;
|
|
@@ -77,9 +78,17 @@ export declare class Sandbox {
|
|
|
77
78
|
readonly runtime: Runtime;
|
|
78
79
|
private readonly options;
|
|
79
80
|
private pending?;
|
|
81
|
+
private existing?;
|
|
80
82
|
readonly id: string;
|
|
81
83
|
constructor(runtime: Runtime, id: string, options?: SandboxOptions);
|
|
82
84
|
configure(options: Pick<SandboxOptions, "sleepAfter" | "keepAlive">): Promise<void>;
|
|
85
|
+
/** Only a successfully initialized native handle can supply a recovery id. */
|
|
86
|
+
existingNativeId(): string | undefined;
|
|
87
|
+
/** Reconcile an uncertain update from an authoritative read, without allocation. */
|
|
88
|
+
reconcileConfiguration(nativeId?: string | undefined): Promise<{
|
|
89
|
+
sleepAfter: number;
|
|
90
|
+
keepAlive: boolean;
|
|
91
|
+
}>;
|
|
83
92
|
native(): Promise<NativeSandbox>;
|
|
84
93
|
private session;
|
|
85
94
|
exec(command: string, options?: ExecOptions): Promise<ExecResult>;
|
package/dist/cloudflare/index.js
CHANGED
|
@@ -22,12 +22,24 @@ export function sanitizeSandboxId(id) {
|
|
|
22
22
|
}
|
|
23
23
|
const cleanEnv = (env = {}) => Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== undefined));
|
|
24
24
|
function sleepSeconds(value = 600) {
|
|
25
|
+
let seconds;
|
|
25
26
|
if (typeof value === "number")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
seconds = value;
|
|
28
|
+
else {
|
|
29
|
+
const match = /^(\d+)(s|m|h)$/.exec(value);
|
|
30
|
+
if (!match)
|
|
31
|
+
throw new TypeError("sleepAfter must be seconds or a duration such as 5m");
|
|
32
|
+
seconds = Number(match[1]) * ({ s: 1, m: 60, h: 3600 }[match[2]] ?? 1);
|
|
33
|
+
}
|
|
34
|
+
if (!Number.isSafeInteger(seconds) || seconds < 0 || seconds > 86_400)
|
|
35
|
+
throw new CompatibilityError("Cloudflare", "sleepAfter outside whole seconds from 0 to 86400");
|
|
36
|
+
return seconds;
|
|
37
|
+
}
|
|
38
|
+
export function validateSandboxConfiguration(options) {
|
|
39
|
+
if (options.sleepAfter !== undefined)
|
|
40
|
+
sleepSeconds(options.sleepAfter);
|
|
41
|
+
if (options.keepAlive !== undefined && typeof options.keepAlive !== "boolean")
|
|
42
|
+
throw new TypeError("keepAlive must be a boolean");
|
|
31
43
|
}
|
|
32
44
|
/** Worker bindings are deliberately not inspected or invoked. Supply Runtime credentials
|
|
33
45
|
* through options.runtime in Workers; Node also supports RUNTIME_API_KEY/login. */
|
|
@@ -41,25 +53,50 @@ export class Sandbox {
|
|
|
41
53
|
runtime;
|
|
42
54
|
options;
|
|
43
55
|
pending;
|
|
56
|
+
existing;
|
|
44
57
|
id;
|
|
45
58
|
constructor(runtime, id, options = {}) {
|
|
46
59
|
this.runtime = runtime;
|
|
47
60
|
this.options = options;
|
|
61
|
+
validateSandboxConfiguration(options);
|
|
48
62
|
this.id = id;
|
|
49
63
|
}
|
|
50
64
|
async configure(options) {
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
only("Cloudflare configure", options, ["sleepAfter", "keepAlive"]);
|
|
66
|
+
validateSandboxConfiguration(options);
|
|
53
67
|
const changed = (options.sleepAfter !== undefined && options.sleepAfter !== this.options.sleepAfter) ||
|
|
54
68
|
(options.keepAlive !== undefined && options.keepAlive !== this.options.keepAlive);
|
|
55
|
-
|
|
69
|
+
const next = { ...this.options, ...options };
|
|
56
70
|
if (changed && this.pending) {
|
|
57
71
|
const s = await this.pending;
|
|
58
72
|
await s.update({
|
|
59
|
-
idlePauseSeconds:
|
|
73
|
+
idlePauseSeconds: next.keepAlive ? 0 : sleepSeconds(next.sleepAfter),
|
|
60
74
|
...(options.keepAlive !== undefined ? { persistent: options.keepAlive } : {}),
|
|
61
75
|
});
|
|
62
76
|
}
|
|
77
|
+
Object.assign(this.options, options);
|
|
78
|
+
}
|
|
79
|
+
/** Only a successfully initialized native handle can supply a recovery id. */
|
|
80
|
+
existingNativeId() {
|
|
81
|
+
return this.existing?.id;
|
|
82
|
+
}
|
|
83
|
+
/** Reconcile an uncertain update from an authoritative read, without allocation. */
|
|
84
|
+
async reconcileConfiguration(nativeId = this.existingNativeId()) {
|
|
85
|
+
if (!nativeId)
|
|
86
|
+
throw new Error("The uncertain Runtime configuration has no existing sandbox identity.");
|
|
87
|
+
const sandbox = await this.runtime.sandboxes.get(nativeId);
|
|
88
|
+
const { idlePauseSeconds, persistent } = sandbox.info;
|
|
89
|
+
if (typeof idlePauseSeconds !== "number" ||
|
|
90
|
+
!Number.isSafeInteger(idlePauseSeconds) ||
|
|
91
|
+
idlePauseSeconds < 0 ||
|
|
92
|
+
idlePauseSeconds > 86_400 ||
|
|
93
|
+
typeof persistent !== "boolean")
|
|
94
|
+
throw new Error("Runtime did not return authoritative idle and persistence settings.");
|
|
95
|
+
const configuration = { sleepAfter: idlePauseSeconds, keepAlive: persistent };
|
|
96
|
+
Object.assign(this.options, configuration);
|
|
97
|
+
this.existing = sandbox;
|
|
98
|
+
this.pending = Promise.resolve(sandbox);
|
|
99
|
+
return configuration;
|
|
63
100
|
}
|
|
64
101
|
async native() {
|
|
65
102
|
if (!this.pending) {
|
|
@@ -82,6 +119,7 @@ export class Sandbox {
|
|
|
82
119
|
throw e;
|
|
83
120
|
}
|
|
84
121
|
}
|
|
122
|
+
this.existing = s;
|
|
85
123
|
return s;
|
|
86
124
|
})();
|
|
87
125
|
this.pending.catch(() => {
|
|
@@ -14,12 +14,16 @@ type Configuration = {
|
|
|
14
14
|
name: string;
|
|
15
15
|
normalizeId?: boolean;
|
|
16
16
|
};
|
|
17
|
+
nativeSandboxId?: string;
|
|
17
18
|
sleepAfter?: string | number;
|
|
18
19
|
keepAlive?: boolean;
|
|
19
20
|
};
|
|
20
21
|
export declare class Sandbox<Env extends RuntimeWorkerEnv = RuntimeWorkerEnv> extends DurableObject<Env> {
|
|
21
22
|
private configuration;
|
|
23
|
+
private configurationIntent?;
|
|
22
24
|
private facade?;
|
|
25
|
+
private configurationTail;
|
|
26
|
+
private configurationOperation;
|
|
23
27
|
private previewTail;
|
|
24
28
|
private previewOperation;
|
|
25
29
|
private readonly initialized;
|
|
@@ -4,8 +4,47 @@
|
|
|
4
4
|
import { DurableObject } from "cloudflare:workers";
|
|
5
5
|
import { only, CompatibilityError } from "../compat/core.js";
|
|
6
6
|
import { Runtime } from "../client.js";
|
|
7
|
-
import {
|
|
7
|
+
import { workerSandboxIdentity } from "./identity.js";
|
|
8
|
+
import { Sandbox as RuntimeSandbox, sanitizeSandboxId, validateSandboxConfiguration, } from "./index.js";
|
|
8
9
|
export * from "./index.js";
|
|
10
|
+
const CONFIGURATION_KEY = "runtime.configuration";
|
|
11
|
+
const CONFIGURATION_INTENT_KEY = "runtime.configuration.pending";
|
|
12
|
+
/** Preserve the original failure even when durable recovery also fails. */
|
|
13
|
+
function configurationFailure(error, uncertain, cleanup) {
|
|
14
|
+
if (error !== null && (typeof error === "object" || typeof error === "function")) {
|
|
15
|
+
try {
|
|
16
|
+
if (uncertain)
|
|
17
|
+
Object.defineProperty(error, "configurationUncertain", { value: true, configurable: true });
|
|
18
|
+
if (cleanup !== undefined)
|
|
19
|
+
Object.defineProperty(error, "cause" in error && error.cause !== undefined ? "cleanupError" : "cause", {
|
|
20
|
+
value: cleanup,
|
|
21
|
+
configurable: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// A frozen exception still keeps its original identity and type.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
/** Queue waits share the caller's existing signal and never reset its deadline. */
|
|
31
|
+
async function waitForConfiguration(pending, signal) {
|
|
32
|
+
signal?.throwIfAborted();
|
|
33
|
+
if (!signal)
|
|
34
|
+
return pending;
|
|
35
|
+
let cancel;
|
|
36
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rejects with the caller's abort reason as is, as the upstream SDK does
|
|
38
|
+
cancel = () => reject(signal.reason);
|
|
39
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
return await Promise.race([pending, aborted]);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
signal.removeEventListener("abort", cancel);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
9
48
|
/** The official Worker SDK returns plain records with callable RPC properties,
|
|
10
49
|
* so ids/status are values rather than remote-property promises. */
|
|
11
50
|
function sessionTarget(session) {
|
|
@@ -48,7 +87,14 @@ function processTarget(process) {
|
|
|
48
87
|
}
|
|
49
88
|
export class Sandbox extends DurableObject {
|
|
50
89
|
configuration = {};
|
|
90
|
+
configurationIntent;
|
|
51
91
|
facade;
|
|
92
|
+
configurationTail = Promise.resolve();
|
|
93
|
+
configurationOperation(operation) {
|
|
94
|
+
const result = this.configurationTail.catch(() => undefined).then(operation);
|
|
95
|
+
this.configurationTail = result;
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
52
98
|
previewTail = Promise.resolve();
|
|
53
99
|
previewOperation(operation) {
|
|
54
100
|
const result = this.previewTail.catch(() => undefined).then(operation);
|
|
@@ -59,29 +105,133 @@ export class Sandbox extends DurableObject {
|
|
|
59
105
|
constructor(ctx, env) {
|
|
60
106
|
super(ctx, env);
|
|
61
107
|
this.initialized = ctx.blockConcurrencyWhile(async () => {
|
|
62
|
-
this.configuration = (await ctx.storage.get(
|
|
108
|
+
this.configuration = (await ctx.storage.get(CONFIGURATION_KEY)) ?? {};
|
|
109
|
+
this.configurationIntent =
|
|
110
|
+
await ctx.storage.get(CONFIGURATION_INTENT_KEY);
|
|
63
111
|
});
|
|
64
112
|
}
|
|
65
|
-
async sandbox() {
|
|
66
|
-
await this.initialized;
|
|
113
|
+
async sandbox(signal) {
|
|
114
|
+
await waitForConfiguration(this.initialized, signal);
|
|
115
|
+
// A staged intent is transient while an update is still running. Wait for
|
|
116
|
+
// the queued update before deciding whether uncertainty was retained.
|
|
117
|
+
let configurationTail;
|
|
118
|
+
do {
|
|
119
|
+
configurationTail = this.configurationTail;
|
|
120
|
+
await waitForConfiguration(configurationTail.catch(() => undefined), signal);
|
|
121
|
+
} while (configurationTail !== this.configurationTail);
|
|
122
|
+
signal?.throwIfAborted();
|
|
123
|
+
if (this.configurationIntent)
|
|
124
|
+
throw configurationFailure(new Error("Runtime's configuration is uncertain. Call configure() to reconcile it before use."), true);
|
|
67
125
|
if (!this.configuration.sandboxName)
|
|
68
126
|
throw new Error("Call getSandbox(namespace, id) before using the Durable Object");
|
|
69
127
|
if (!this.env.RUNTIME_API_KEY?.startsWith("rtcloud_"))
|
|
70
128
|
throw new Error("Set the RUNTIME_API_KEY Worker secret to a Runtime API key");
|
|
71
|
-
this.facade ??= new RuntimeSandbox(new Runtime({ apiKey: this.env.RUNTIME_API_KEY, baseUrl: this.env.RUNTIME_API_URL }), this.
|
|
129
|
+
this.facade ??= new RuntimeSandbox(new Runtime({ apiKey: this.env.RUNTIME_API_KEY, baseUrl: this.env.RUNTIME_API_URL }), workerSandboxIdentity(this.ctx.id.toString(), this.configuration), { ...this.configuration });
|
|
72
130
|
return this.facade;
|
|
73
131
|
}
|
|
74
132
|
async configure(configuration) {
|
|
75
133
|
only("Cloudflare Worker configure", configuration, ["sandboxName", "sleepAfter", "keepAlive"]);
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
configuration.sandboxName
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
134
|
+
validateSandboxConfiguration(configuration);
|
|
135
|
+
if (configuration.sandboxName !== undefined) {
|
|
136
|
+
only("Cloudflare Worker sandboxName", configuration.sandboxName, ["name", "normalizeId"]);
|
|
137
|
+
sanitizeSandboxId(configuration.sandboxName.name);
|
|
138
|
+
if (configuration.sandboxName.normalizeId !== undefined &&
|
|
139
|
+
typeof configuration.sandboxName.normalizeId !== "boolean")
|
|
140
|
+
throw new TypeError("normalizeId must be a boolean");
|
|
141
|
+
}
|
|
142
|
+
const requested = {
|
|
143
|
+
...(configuration.sandboxName === undefined
|
|
144
|
+
? {}
|
|
145
|
+
: { sandboxName: { ...configuration.sandboxName } }),
|
|
146
|
+
...(configuration.sleepAfter === undefined ? {} : { sleepAfter: configuration.sleepAfter }),
|
|
147
|
+
...(configuration.keepAlive === undefined ? {} : { keepAlive: configuration.keepAlive }),
|
|
148
|
+
};
|
|
149
|
+
return this.configurationOperation(async () => {
|
|
150
|
+
await this.initialized;
|
|
151
|
+
if (this.configuration.sandboxName &&
|
|
152
|
+
requested.sandboxName &&
|
|
153
|
+
this.configuration.sandboxName.name !== requested.sandboxName.name)
|
|
154
|
+
throw new Error("A Durable Object cannot change sandbox identity");
|
|
155
|
+
if (this.configurationIntent) {
|
|
156
|
+
const facade = this.facade ??
|
|
157
|
+
new RuntimeSandbox(new Runtime({ apiKey: this.env.RUNTIME_API_KEY, baseUrl: this.env.RUNTIME_API_URL }), workerSandboxIdentity(this.ctx.id.toString(), this.configuration), { ...this.configuration });
|
|
158
|
+
try {
|
|
159
|
+
// Recovery uses the saved UUID even after a Durable Object restart.
|
|
160
|
+
const actual = await facade.reconcileConfiguration(this.configurationIntent.sandboxId);
|
|
161
|
+
const reconciled = { ...this.configuration, ...actual };
|
|
162
|
+
await this.ctx.storage.put(CONFIGURATION_KEY, reconciled);
|
|
163
|
+
await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
|
|
164
|
+
this.configuration = reconciled;
|
|
165
|
+
this.facade = facade;
|
|
166
|
+
this.configurationIntent = undefined;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
throw configurationFailure(error, true);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const previous = this.configuration;
|
|
173
|
+
const stored = await this.ctx.storage.get(CONFIGURATION_KEY);
|
|
174
|
+
const nativeSandboxId = workerSandboxIdentity(this.ctx.id.toString(), previous);
|
|
175
|
+
const next = { ...previous, ...requested, nativeSandboxId };
|
|
176
|
+
if (!this.facade) {
|
|
177
|
+
await this.ctx.storage.put(CONFIGURATION_KEY, next);
|
|
178
|
+
this.configuration = next;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.configurationIntent = { sandboxId: this.facade.existingNativeId(), previous, next };
|
|
182
|
+
try {
|
|
183
|
+
await this.ctx.storage.put(CONFIGURATION_INTENT_KEY, this.configurationIntent);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
let cleanup;
|
|
187
|
+
try {
|
|
188
|
+
// Native mutation has not started. A failed write may still have
|
|
189
|
+
// persisted the intent, so clear this operation's staging record.
|
|
190
|
+
await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
|
|
191
|
+
this.configurationIntent = undefined;
|
|
192
|
+
}
|
|
193
|
+
catch (failure) {
|
|
194
|
+
cleanup = failure;
|
|
195
|
+
}
|
|
196
|
+
throw configurationFailure(error, this.configurationIntent !== undefined, cleanup);
|
|
197
|
+
}
|
|
198
|
+
let nativeAttempted = false;
|
|
199
|
+
try {
|
|
200
|
+
await this.ctx.storage.put(CONFIGURATION_KEY, next);
|
|
201
|
+
nativeAttempted = true;
|
|
202
|
+
await this.facade.configure({
|
|
203
|
+
...(requested.sleepAfter === undefined ? {} : { sleepAfter: requested.sleepAfter }),
|
|
204
|
+
...(requested.keepAlive === undefined ? {} : { keepAlive: requested.keepAlive }),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
let cleanup;
|
|
209
|
+
try {
|
|
210
|
+
if (stored === undefined)
|
|
211
|
+
await this.ctx.storage.delete(CONFIGURATION_KEY);
|
|
212
|
+
else
|
|
213
|
+
await this.ctx.storage.put(CONFIGURATION_KEY, stored);
|
|
214
|
+
if (!nativeAttempted) {
|
|
215
|
+
await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
|
|
216
|
+
this.configurationIntent = undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (failure) {
|
|
220
|
+
cleanup = failure;
|
|
221
|
+
}
|
|
222
|
+
// A rejected native request may have applied remotely. Keep its intent
|
|
223
|
+
// until an authoritative read succeeds; rollback is only local state.
|
|
224
|
+
throw configurationFailure(error, this.configurationIntent !== undefined, cleanup);
|
|
225
|
+
}
|
|
226
|
+
this.configuration = next;
|
|
227
|
+
try {
|
|
228
|
+
await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
|
|
229
|
+
this.configurationIntent = undefined;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
throw configurationFailure(error, true);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
85
235
|
}
|
|
86
236
|
setSandboxName(name, normalizeId = false) {
|
|
87
237
|
return this.configure({
|
|
@@ -95,10 +245,10 @@ export class Sandbox extends DurableObject {
|
|
|
95
245
|
return this.configure({ keepAlive });
|
|
96
246
|
}
|
|
97
247
|
async exec(command, options) {
|
|
98
|
-
return (await this.sandbox()).exec(command, options);
|
|
248
|
+
return (await this.sandbox(options?.signal)).exec(command, options);
|
|
99
249
|
}
|
|
100
250
|
async execStream(command, options) {
|
|
101
|
-
return (await this.sandbox()).execStream(command, options);
|
|
251
|
+
return (await this.sandbox(options?.signal)).execStream(command, options);
|
|
102
252
|
}
|
|
103
253
|
async startProcess(command, options) {
|
|
104
254
|
return processTarget(await (await this.sandbox()).startProcess(command, options));
|