withruntime 0.5.0 → 0.6.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/DAYTONA.md +25 -25
- package/E2B.md +1 -1
- package/LICENSE +55 -0
- package/README.md +3 -2
- package/dist/cli-extras.d.ts +24 -0
- package/dist/cli-extras.js +205 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +430 -37
- package/dist/daytona/context.d.ts +5 -0
- package/dist/daytona/context.js +3 -0
- package/dist/daytona/filesystem.js +2 -2
- package/dist/daytona/process.js +3 -1
- package/dist/e2b/commands.js +6 -2
- package/dist/e2b/filesystem.d.ts +26 -1
- package/dist/e2b/filesystem.js +30 -2
- package/dist/e2b/index.d.ts +1 -1
- package/dist/e2b/index.js +1 -1
- package/dist/errors.js +2 -0
- package/dist/identity.d.ts +27 -0
- package/dist/identity.js +56 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/mcp.js +16 -3
- package/dist/network-cli.d.ts +15 -0
- package/dist/network-cli.js +323 -0
- package/dist/products/desktop.d.ts +41 -4
- package/dist/products/desktop.js +73 -3
- package/dist/products/index.d.ts +12 -0
- package/dist/products/index.js +12 -0
- package/dist/products/interpreter.d.ts +5 -3
- package/dist/products/interpreter.js +2 -1
- package/dist/products/mcp.d.ts +79 -0
- package/dist/products/mcp.js +37 -0
- package/dist/products/mounts.d.ts +49 -0
- package/dist/products/mounts.js +21 -0
- package/dist/products/network-products.d.ts +167 -0
- package/dist/products/network-products.js +82 -0
- package/dist/products/network.d.ts +1 -1
- package/dist/products/sso.d.ts +44 -0
- package/dist/products/sso.js +8 -0
- package/dist/products/switching.d.ts +7 -0
- package/dist/products/volumes.d.ts +68 -5
- package/dist/products/volumes.js +52 -4
- package/dist/products/watch.d.ts +91 -0
- package/dist/products/watch.js +135 -0
- package/dist/sandbox.d.ts +32 -2
- package/dist/sandbox.js +93 -16
- package/dist/snapshots.d.ts +11 -1
- package/dist/ssh.js +13 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +4 -1
- package/dist/tunnel.d.ts +4 -1
- package/dist/tunnel.js +8 -3
- package/dist/wireguard.d.ts +7 -0
- package/dist/wireguard.js +13 -0
- package/package.json +7 -3
package/dist/products/volumes.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Page } from "../page.js";
|
|
2
|
+
const enc = encodeURIComponent;
|
|
2
3
|
/** Persistent disks: create once, attach when creating a sandbox with
|
|
3
|
-
* `volumes: [{ volumeId: v.id, path: "/data" }]`.
|
|
4
|
+
* `volumes: [{ volumeId: v.id, path: "/data" }]`. Backed up off their host
|
|
5
|
+
* daily unless that is turned off; restore one with `create({ fromBackup })`. */
|
|
4
6
|
export function volumes(t) {
|
|
5
|
-
const get = (id, options) => t.json({ method: "GET", path: `/v1/volumes/${
|
|
7
|
+
const get = (id, options) => t.json({ method: "GET", path: `/v1/volumes/${enc(id)}`, ...options });
|
|
6
8
|
const list = async (query = {}, options) => {
|
|
7
9
|
const page = await t.json({
|
|
8
10
|
method: "GET",
|
|
@@ -12,16 +14,62 @@ export function volumes(t) {
|
|
|
12
14
|
});
|
|
13
15
|
return new Page(page.data, page.nextCursor, (cursor) => list({ ...query, cursor }, options));
|
|
14
16
|
};
|
|
17
|
+
const backups = async (query = {}, options) => {
|
|
18
|
+
const page = await t.json({
|
|
19
|
+
method: "GET",
|
|
20
|
+
path: "/v1/volume-backups",
|
|
21
|
+
query,
|
|
22
|
+
...options,
|
|
23
|
+
});
|
|
24
|
+
return new Page(page.data, page.nextCursor, (cursor) => backups({ ...query, cursor }, options));
|
|
25
|
+
};
|
|
15
26
|
return {
|
|
16
|
-
/** Create and wait (up to 10 s) until it is ready.
|
|
27
|
+
/** Create and wait (up to 10 s) until it is ready. With `fromBackup`, a
|
|
28
|
+
* restore; it is ready once the backup is downloaded and checked. */
|
|
17
29
|
create: (input, options) => t.json({ method: "POST", path: "/v1/volumes", body: input, wait: 10, ...options }),
|
|
18
30
|
get,
|
|
19
31
|
list,
|
|
20
32
|
delete: (id, options) => t.json({
|
|
21
33
|
method: "POST",
|
|
22
|
-
path: `/v1/volumes/${
|
|
34
|
+
path: `/v1/volumes/${enc(id)}:delete`,
|
|
23
35
|
body: {},
|
|
24
36
|
...options,
|
|
25
37
|
}),
|
|
38
|
+
/** Back a volume up now, off its host, and wait (up to a minute, or
|
|
39
|
+
* `wait` seconds) for it to be ready. */
|
|
40
|
+
backup: (id, input = {}, options = {}) => {
|
|
41
|
+
const { wait = 60, ...rest } = options;
|
|
42
|
+
return t.json({
|
|
43
|
+
method: "POST",
|
|
44
|
+
path: `/v1/volumes/${enc(id)}:backup`,
|
|
45
|
+
body: input,
|
|
46
|
+
wait,
|
|
47
|
+
...rest,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
/** Daily backups on or off, and how many days each is kept. */
|
|
51
|
+
setBackupPolicy: (id, input, options) => t.json({
|
|
52
|
+
method: "POST",
|
|
53
|
+
path: `/v1/volumes/${enc(id)}:backup-policy`,
|
|
54
|
+
body: input,
|
|
55
|
+
...options,
|
|
56
|
+
}),
|
|
57
|
+
/** Backups, newest first; `{ volumeId }` for one volume's. */
|
|
58
|
+
backups,
|
|
59
|
+
getBackup: (id, options) => t.json({ method: "GET", path: `/v1/volume-backups/${enc(id)}`, ...options }),
|
|
60
|
+
deleteBackup: (id, options) => t.json({
|
|
61
|
+
method: "POST",
|
|
62
|
+
path: `/v1/volume-backups/${enc(id)}:delete`,
|
|
63
|
+
body: {},
|
|
64
|
+
...options,
|
|
65
|
+
}),
|
|
66
|
+
/** A new volume from a backup: `create({ fromBackup: id })`. */
|
|
67
|
+
restore: (backupId, input = {}, options) => t.json({
|
|
68
|
+
method: "POST",
|
|
69
|
+
path: "/v1/volumes",
|
|
70
|
+
body: { ...input, fromBackup: backupId },
|
|
71
|
+
wait: 10,
|
|
72
|
+
...options,
|
|
73
|
+
}),
|
|
26
74
|
};
|
|
27
75
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { RequestOptions, Transport } from "../transport.js";
|
|
2
|
+
export type FileEventType = "create" | "write" | "remove" | "rename" | "chmod";
|
|
3
|
+
export type FileEvent = {
|
|
4
|
+
type: FileEventType;
|
|
5
|
+
/** Absolute path in the sandbox. */
|
|
6
|
+
path: string;
|
|
7
|
+
isDir: boolean;
|
|
8
|
+
/** rename: where it was. */
|
|
9
|
+
oldPath?: string;
|
|
10
|
+
/** write: how many writes were folded into this one event. */
|
|
11
|
+
count?: number;
|
|
12
|
+
};
|
|
13
|
+
export type WatchNotice = {
|
|
14
|
+
k: "overflow";
|
|
15
|
+
dropped: number | null;
|
|
16
|
+
reason: "kernel" | "rate";
|
|
17
|
+
} | {
|
|
18
|
+
k: "limit";
|
|
19
|
+
watches: number;
|
|
20
|
+
} | {
|
|
21
|
+
k: "lost";
|
|
22
|
+
bytes: number;
|
|
23
|
+
};
|
|
24
|
+
export type WatchOptions = Omit<RequestOptions, "timeoutMs"> & {
|
|
25
|
+
recursive?: boolean;
|
|
26
|
+
/** Only these event types; all five by default. */
|
|
27
|
+
events?: FileEventType[];
|
|
28
|
+
/** Globs relative to the path: only matching paths are reported. */
|
|
29
|
+
include?: string[];
|
|
30
|
+
/** Globs relative to the path. An excluded directory is not watched at all. */
|
|
31
|
+
exclude?: string[];
|
|
32
|
+
/** Events are gathered this long and delivered together. Default 100. */
|
|
33
|
+
batchMs?: number;
|
|
34
|
+
/** The watch ends by itself after running this long. Default 1 hour. */
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
maxWatches?: number;
|
|
37
|
+
/** A batch at a time, rather than one call per event. */
|
|
38
|
+
onBatch?: (events: FileEvent[]) => void | Promise<void>;
|
|
39
|
+
/** Events were lost (a flood past the rate cap, the kernel's queue, or a
|
|
40
|
+
* reader that fell more than 1 MiB behind): rescan what you care about. */
|
|
41
|
+
onNotice?: (notice: WatchNotice) => void;
|
|
42
|
+
/** The watch stopped: "stopped", "timeout", "root-removed", or "paused"
|
|
43
|
+
* (the sandbox paused; call `resume()` once it runs again: nothing is lost). */
|
|
44
|
+
onExit?: (reason: string) => void;
|
|
45
|
+
};
|
|
46
|
+
export type WatchInfo = {
|
|
47
|
+
id: string;
|
|
48
|
+
path: string;
|
|
49
|
+
processId: string;
|
|
50
|
+
state: string;
|
|
51
|
+
startedAt: number;
|
|
52
|
+
};
|
|
53
|
+
/** A running watch. Events arrive through the callbacks until `stop()`. */
|
|
54
|
+
export declare class WatchHandle {
|
|
55
|
+
#private;
|
|
56
|
+
private readonly t;
|
|
57
|
+
private readonly base;
|
|
58
|
+
readonly id: string;
|
|
59
|
+
readonly path: string;
|
|
60
|
+
private readonly options;
|
|
61
|
+
private readonly onEvent?;
|
|
62
|
+
cursor: number;
|
|
63
|
+
constructor(t: Transport, base: string, id: string, path: string, cursor: number, options: WatchOptions, onEvent?: ((event: FileEvent) => void | Promise<void>) | undefined);
|
|
64
|
+
/** Settles when the watch stops delivering (stopped, ended or paused). */
|
|
65
|
+
get done(): Promise<void>;
|
|
66
|
+
/** Start delivering again from where it left off, after a pause. */
|
|
67
|
+
resume(): void;
|
|
68
|
+
/** Stop the watch in the sandbox, and stop delivering. */
|
|
69
|
+
stop(options?: RequestOptions): Promise<void>;
|
|
70
|
+
/** @internal */
|
|
71
|
+
begin(): this;
|
|
72
|
+
}
|
|
73
|
+
/** `sbx.files.watch(path, onEvent, options)`: E2B's `watchDir`, with
|
|
74
|
+
* filters, batches, and a watch that survives a pause without losing events. */
|
|
75
|
+
export declare function watchDirectory(t: Transport, sandboxId: string, path: string, onEvent?: (event: FileEvent) => void | Promise<void>, options?: WatchOptions): Promise<WatchHandle>;
|
|
76
|
+
export declare function sandboxWatches(t: Transport, sandboxId: string): {
|
|
77
|
+
list: (options?: RequestOptions) => Promise<WatchInfo[]>;
|
|
78
|
+
/** Events after a cursor, without a stream: for polling. */
|
|
79
|
+
read: (id: string, cursor?: number, input?: {
|
|
80
|
+
waitMs?: number;
|
|
81
|
+
}, options?: RequestOptions) => Promise<{
|
|
82
|
+
events: FileEvent[];
|
|
83
|
+
notices: Record<string, unknown>[];
|
|
84
|
+
nextCursor: number;
|
|
85
|
+
lostBytes: number;
|
|
86
|
+
ended: boolean;
|
|
87
|
+
}>;
|
|
88
|
+
stop: (id: string, options?: RequestOptions) => Promise<{
|
|
89
|
+
stopped: boolean;
|
|
90
|
+
}>;
|
|
91
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** A running watch. Events arrive through the callbacks until `stop()`. */
|
|
2
|
+
export class WatchHandle {
|
|
3
|
+
t;
|
|
4
|
+
base;
|
|
5
|
+
id;
|
|
6
|
+
path;
|
|
7
|
+
options;
|
|
8
|
+
onEvent;
|
|
9
|
+
cursor;
|
|
10
|
+
#stopped = false;
|
|
11
|
+
#following;
|
|
12
|
+
#abort = new AbortController();
|
|
13
|
+
constructor(t, base, id, path, cursor, options, onEvent) {
|
|
14
|
+
this.t = t;
|
|
15
|
+
this.base = base;
|
|
16
|
+
this.id = id;
|
|
17
|
+
this.path = path;
|
|
18
|
+
this.options = options;
|
|
19
|
+
this.onEvent = onEvent;
|
|
20
|
+
this.cursor = cursor;
|
|
21
|
+
}
|
|
22
|
+
/** Settles when the watch stops delivering (stopped, ended or paused). */
|
|
23
|
+
get done() {
|
|
24
|
+
return this.#following ?? Promise.resolve();
|
|
25
|
+
}
|
|
26
|
+
/** Start delivering again from where it left off, after a pause. */
|
|
27
|
+
resume() {
|
|
28
|
+
if (this.#stopped)
|
|
29
|
+
throw new Error("This watch was stopped");
|
|
30
|
+
this.#abort = new AbortController();
|
|
31
|
+
this.begin();
|
|
32
|
+
}
|
|
33
|
+
/** Stop the watch in the sandbox, and stop delivering. */
|
|
34
|
+
async stop(options) {
|
|
35
|
+
if (this.#stopped)
|
|
36
|
+
return;
|
|
37
|
+
this.#stopped = true;
|
|
38
|
+
this.#abort.abort();
|
|
39
|
+
await this.t
|
|
40
|
+
.json({ method: "DELETE", path: `${this.base}/${encodeURIComponent(this.id)}`, ...options })
|
|
41
|
+
.catch(() => undefined);
|
|
42
|
+
await this.#following?.catch(() => undefined);
|
|
43
|
+
this.options.onExit?.("stopped");
|
|
44
|
+
}
|
|
45
|
+
async #follow() {
|
|
46
|
+
for (;;) {
|
|
47
|
+
let next = "exit";
|
|
48
|
+
try {
|
|
49
|
+
for await (const event of this.t.events({
|
|
50
|
+
method: "GET",
|
|
51
|
+
path: `${this.base}/${encodeURIComponent(this.id)}/events`,
|
|
52
|
+
query: { cursor: this.cursor, follow: true },
|
|
53
|
+
signal: this.#abort.signal,
|
|
54
|
+
timeoutMs: 150_000,
|
|
55
|
+
})) {
|
|
56
|
+
this.cursor = event.cursor ?? this.cursor;
|
|
57
|
+
if (event.k === "events") {
|
|
58
|
+
if (this.options.onBatch)
|
|
59
|
+
await this.options.onBatch(event.events);
|
|
60
|
+
if (this.onEvent)
|
|
61
|
+
for (const item of event.events)
|
|
62
|
+
await this.onEvent(item);
|
|
63
|
+
}
|
|
64
|
+
else if (event.k === "continue")
|
|
65
|
+
next = "continue";
|
|
66
|
+
else if (event.k === "end" || event.k === "paused") {
|
|
67
|
+
if (!this.#stopped)
|
|
68
|
+
this.options.onExit?.(event.k === "paused" ? "paused" : event.reason);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
else if (event.k === "failure")
|
|
72
|
+
throw new Error(`${event.code}: ${event.message}`);
|
|
73
|
+
else
|
|
74
|
+
this.options.onNotice?.(event);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (this.#stopped || this.#abort.signal.aborted)
|
|
79
|
+
return;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
if (next !== "continue" || this.#stopped)
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** @internal */
|
|
87
|
+
begin() {
|
|
88
|
+
this.#following = this.#follow();
|
|
89
|
+
// An error surfaces through `done`; nothing is left unhandled meanwhile.
|
|
90
|
+
this.#following.catch(() => undefined);
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** `sbx.files.watch(path, onEvent, options)`: E2B's `watchDir`, with
|
|
95
|
+
* filters, batches, and a watch that survives a pause without losing events. */
|
|
96
|
+
export function watchDirectory(t, sandboxId, path, onEvent, options = {}) {
|
|
97
|
+
const base = `/v1/sandboxes/${encodeURIComponent(sandboxId)}/files/watches`;
|
|
98
|
+
const { recursive, events, include, exclude, batchMs, timeoutMs, maxWatches, idempotencyKey, signal, } = options;
|
|
99
|
+
return t
|
|
100
|
+
.json({
|
|
101
|
+
method: "POST",
|
|
102
|
+
path: base,
|
|
103
|
+
body: {
|
|
104
|
+
path,
|
|
105
|
+
...(recursive !== undefined ? { recursive } : {}),
|
|
106
|
+
...(events ? { events } : {}),
|
|
107
|
+
...(include ? { include } : {}),
|
|
108
|
+
...(exclude ? { exclude } : {}),
|
|
109
|
+
...(batchMs !== undefined ? { batchMs } : {}),
|
|
110
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
111
|
+
...(maxWatches !== undefined ? { maxWatches } : {}),
|
|
112
|
+
},
|
|
113
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
114
|
+
...(signal ? { signal } : {}),
|
|
115
|
+
})
|
|
116
|
+
.then((watch) => new WatchHandle(t, base, watch.id, watch.path, watch.cursor ?? 0, options, onEvent).begin());
|
|
117
|
+
}
|
|
118
|
+
export function sandboxWatches(t, sandboxId) {
|
|
119
|
+
const base = `/v1/sandboxes/${encodeURIComponent(sandboxId)}/files/watches`;
|
|
120
|
+
return {
|
|
121
|
+
list: async (options) => (await t.json({ method: "GET", path: base, ...options })).data,
|
|
122
|
+
/** Events after a cursor, without a stream: for polling. */
|
|
123
|
+
read: (id, cursor = 0, input = {}, options) => t.json({
|
|
124
|
+
method: "GET",
|
|
125
|
+
path: `${base}/${encodeURIComponent(id)}/events`,
|
|
126
|
+
query: { cursor, ...(input.waitMs !== undefined ? { waitMs: input.waitMs } : {}) },
|
|
127
|
+
...options,
|
|
128
|
+
}),
|
|
129
|
+
stop: (id, options) => t.json({
|
|
130
|
+
method: "DELETE",
|
|
131
|
+
path: `${base}/${encodeURIComponent(id)}`,
|
|
132
|
+
...options,
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Page } from "./page.js";
|
|
2
|
+
import { type FileEvent, type WatchHandle, type WatchOptions } from "./products/watch.js";
|
|
2
3
|
import type { RequestOptions, Transport } from "./transport.js";
|
|
3
4
|
import type { CommandResult, ExecOptions, FileEntry, OutputEvent, KeepAliveOptions, ProcessInfo, SandboxInfo, SandboxSettings } from "./types.js";
|
|
4
5
|
import { type SandboxExtensions } from "./products/index.js";
|
|
@@ -175,12 +176,41 @@ export declare class Files {
|
|
|
175
176
|
private readonly t;
|
|
176
177
|
private readonly sandboxId;
|
|
177
178
|
constructor(t: Transport, sandboxId: string);
|
|
179
|
+
/** Watch a directory: `onEvent` gets create, write, remove, rename and
|
|
180
|
+
* chmod events until `handle.stop()`. E2B's `watchDir`, plus filters,
|
|
181
|
+
* batches, and a watch that loses nothing across a pause:
|
|
182
|
+
*
|
|
183
|
+
* const watch = await sbx.files.watch("/workspace", (e) => console.log(e.type, e.path),
|
|
184
|
+
* { recursive: true, exclude: ["node_modules"] });
|
|
185
|
+
* ...
|
|
186
|
+
* await watch.stop();
|
|
187
|
+
*/
|
|
188
|
+
watch(path: string, onEvent?: (event: FileEvent) => void | Promise<void>, options?: WatchOptions): Promise<WatchHandle>;
|
|
189
|
+
/** The watches running in the sandbox, and polling reads by cursor. */
|
|
190
|
+
get watches(): {
|
|
191
|
+
list: (options?: RequestOptions) => Promise<import("./products/watch.js").WatchInfo[]>;
|
|
192
|
+
read: (id: string, cursor?: number, input?: {
|
|
193
|
+
waitMs?: number;
|
|
194
|
+
}, options?: RequestOptions) => Promise<{
|
|
195
|
+
events: FileEvent[];
|
|
196
|
+
notices: Record<string, unknown>[];
|
|
197
|
+
nextCursor: number;
|
|
198
|
+
lostBytes: number;
|
|
199
|
+
ended: boolean;
|
|
200
|
+
}>;
|
|
201
|
+
stop: (id: string, options?: RequestOptions) => Promise<{
|
|
202
|
+
stopped: boolean;
|
|
203
|
+
}>;
|
|
204
|
+
};
|
|
178
205
|
/** A file's bytes, any size. */
|
|
179
206
|
read(path: string, options?: RequestOptions): Promise<Uint8Array>;
|
|
180
207
|
readText(path: string, options?: RequestOptions): Promise<string>;
|
|
181
208
|
/** Writes a file of any size, atomically, making parent directories. Large
|
|
182
|
-
* ones go in parallel 1 MiB chunks checked against their SHA-256.
|
|
183
|
-
|
|
209
|
+
* ones go in parallel 1 MiB chunks checked against their SHA-256. `mode`
|
|
210
|
+
* sets its permissions (0o755 for a program); 0o644 when left out. */
|
|
211
|
+
write(path: string, data: string | Uint8Array, writeOptions?: RequestOptions & {
|
|
212
|
+
mode?: number;
|
|
213
|
+
}): Promise<{
|
|
184
214
|
path: string;
|
|
185
215
|
size: number;
|
|
186
216
|
}>;
|
package/dist/sandbox.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CommandError, RuntimeError } from "./errors.js";
|
|
2
2
|
import { Page } from "./page.js";
|
|
3
|
+
import { sandboxWatches, watchDirectory, } from "./products/watch.js";
|
|
3
4
|
import { sandboxFactories } from "./products/index.js";
|
|
4
5
|
import { Tunnel } from "./tunnel.js";
|
|
5
6
|
const CHUNK = 1_048_576;
|
|
@@ -274,6 +275,13 @@ export class Sandbox {
|
|
|
274
275
|
async snapshot(options = {}) {
|
|
275
276
|
const { idempotencyKey, signal, timeoutMs, ...body } = options;
|
|
276
277
|
await this.refresh();
|
|
278
|
+
// Straight after a fork or a wake the sandbox is still `resuming`, and
|
|
279
|
+
// after a pause still `pausing`: wait for where it is going, or a
|
|
280
|
+
// snapshot of it is refused as not paused (user lane, 23 September 2026).
|
|
281
|
+
if (this.state === "resuming" || this.state === "starting")
|
|
282
|
+
await this.waitFor("running");
|
|
283
|
+
else if (this.state === "pausing")
|
|
284
|
+
await this.waitFor("paused");
|
|
277
285
|
const running = this.state === "running";
|
|
278
286
|
if (running)
|
|
279
287
|
await this.pause();
|
|
@@ -552,6 +560,22 @@ export class Files {
|
|
|
552
560
|
this.sandboxId = sandboxId;
|
|
553
561
|
}
|
|
554
562
|
#path = (suffix) => `/v1/sandboxes/${enc(this.sandboxId)}${suffix}`;
|
|
563
|
+
/** Watch a directory: `onEvent` gets create, write, remove, rename and
|
|
564
|
+
* chmod events until `handle.stop()`. E2B's `watchDir`, plus filters,
|
|
565
|
+
* batches, and a watch that loses nothing across a pause:
|
|
566
|
+
*
|
|
567
|
+
* const watch = await sbx.files.watch("/workspace", (e) => console.log(e.type, e.path),
|
|
568
|
+
* { recursive: true, exclude: ["node_modules"] });
|
|
569
|
+
* ...
|
|
570
|
+
* await watch.stop();
|
|
571
|
+
*/
|
|
572
|
+
watch(path, onEvent, options) {
|
|
573
|
+
return watchDirectory(this.t, this.sandboxId, path, onEvent, options);
|
|
574
|
+
}
|
|
575
|
+
/** The watches running in the sandbox, and polling reads by cursor. */
|
|
576
|
+
get watches() {
|
|
577
|
+
return sandboxWatches(this.t, this.sandboxId);
|
|
578
|
+
}
|
|
555
579
|
/** A file's bytes, any size. */
|
|
556
580
|
async read(path, options = {}) {
|
|
557
581
|
return this.t.bytes({
|
|
@@ -566,8 +590,11 @@ export class Files {
|
|
|
566
590
|
return new TextDecoder().decode(await this.read(path, options));
|
|
567
591
|
}
|
|
568
592
|
/** Writes a file of any size, atomically, making parent directories. Large
|
|
569
|
-
* ones go in parallel 1 MiB chunks checked against their SHA-256.
|
|
570
|
-
|
|
593
|
+
* ones go in parallel 1 MiB chunks checked against their SHA-256. `mode`
|
|
594
|
+
* sets its permissions (0o755 for a program); 0o644 when left out. */
|
|
595
|
+
async write(path, data, writeOptions = {}) {
|
|
596
|
+
const { mode, ...options } = writeOptions;
|
|
597
|
+
const octal = mode === undefined ? undefined : (mode & 0o777).toString(8).padStart(3, "0");
|
|
571
598
|
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
572
599
|
if (bytes.length <= CHUNK) {
|
|
573
600
|
// bytes(), not send(): the answer is read, so its connection is free
|
|
@@ -575,7 +602,7 @@ export class Files {
|
|
|
575
602
|
await this.t.bytes({
|
|
576
603
|
method: "PUT",
|
|
577
604
|
path: this.#path("/files/content"),
|
|
578
|
-
query: { path },
|
|
605
|
+
query: { path, ...(octal ? { mode: octal } : {}) },
|
|
579
606
|
bytes,
|
|
580
607
|
...options,
|
|
581
608
|
});
|
|
@@ -583,12 +610,55 @@ export class Files {
|
|
|
583
610
|
}
|
|
584
611
|
const { createHash } = await import("node:crypto");
|
|
585
612
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
613
|
+
// A guest journals mutations across operations, not per URL. One logical
|
|
614
|
+
// write therefore owns separate stable keys for each mutation phase.
|
|
615
|
+
const uploadKey = options.idempotencyKey ?? crypto.randomUUID();
|
|
616
|
+
const phaseOptions = (phase) => ({
|
|
590
617
|
...options,
|
|
618
|
+
idempotencyKey: createHash("sha256")
|
|
619
|
+
.update(JSON.stringify(["files.write", uploadKey, phase]))
|
|
620
|
+
.digest("hex"),
|
|
621
|
+
});
|
|
622
|
+
let begin;
|
|
623
|
+
try {
|
|
624
|
+
begin = await this.t.json({
|
|
625
|
+
method: "POST",
|
|
626
|
+
path: this.#path("/uploads"),
|
|
627
|
+
body: { path, size: bytes.length, sha256, ...(octal ? { mode: octal } : {}) },
|
|
628
|
+
...phaseOptions("begin"),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
if (!octal || !(error instanceof RuntimeError) || error.code !== "guest_upgrade_required")
|
|
633
|
+
throw error;
|
|
634
|
+
// The API aborted the unsupported transfer before accepting chunks.
|
|
635
|
+
// A different body needs a different key; retain deterministic retries.
|
|
636
|
+
begin = await this.t.json({
|
|
637
|
+
method: "POST",
|
|
638
|
+
path: this.#path("/uploads"),
|
|
639
|
+
body: { path, size: bytes.length, sha256 },
|
|
640
|
+
...phaseOptions("legacy-begin"),
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const commit = () => this.t.json({
|
|
644
|
+
method: "POST",
|
|
645
|
+
path: this.#path(`/uploads/${enc(begin.uploadId)}:commit`),
|
|
646
|
+
body: {},
|
|
647
|
+
...phaseOptions("commit"),
|
|
591
648
|
});
|
|
649
|
+
let committed = false;
|
|
650
|
+
if (begin.replayed) {
|
|
651
|
+
// A completed commit removed the transfer. Ask its journal before
|
|
652
|
+
// resending chunks; an incomplete transfer alone is safe to continue.
|
|
653
|
+
try {
|
|
654
|
+
await commit();
|
|
655
|
+
committed = true;
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
if (!(error instanceof RuntimeError) || error.code !== "upload_incomplete")
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
592
662
|
const offsets = [];
|
|
593
663
|
for (let offset = 0; offset < bytes.length; offset += begin.chunkBytes)
|
|
594
664
|
offsets.push(offset);
|
|
@@ -605,13 +675,18 @@ export class Files {
|
|
|
605
675
|
}
|
|
606
676
|
};
|
|
607
677
|
try {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
678
|
+
if (!committed) {
|
|
679
|
+
await Promise.all(Array.from({ length: Math.min(PARALLEL, offsets.length) }, worker));
|
|
680
|
+
await commit();
|
|
681
|
+
}
|
|
682
|
+
// An older sandbox image ignores the mode in the upload; set it after.
|
|
683
|
+
if (octal && begin.mode !== octal)
|
|
684
|
+
await this.t.json({
|
|
685
|
+
method: "POST",
|
|
686
|
+
path: this.#path("/files:chmod"),
|
|
687
|
+
body: { path, mode: octal },
|
|
688
|
+
...phaseOptions("chmod"),
|
|
689
|
+
});
|
|
615
690
|
}
|
|
616
691
|
catch (error) {
|
|
617
692
|
await this.t
|
|
@@ -619,6 +694,7 @@ export class Files {
|
|
|
619
694
|
method: "POST",
|
|
620
695
|
path: this.#path(`/uploads/${enc(begin.uploadId)}:abort`),
|
|
621
696
|
body: {},
|
|
697
|
+
...phaseOptions("abort"),
|
|
622
698
|
})
|
|
623
699
|
.catch(() => undefined);
|
|
624
700
|
throw error;
|
|
@@ -669,7 +745,8 @@ export class Files {
|
|
|
669
745
|
const { stat, readFile } = await import("node:fs/promises");
|
|
670
746
|
const info = await stat(localPath);
|
|
671
747
|
if (info.isFile()) {
|
|
672
|
-
|
|
748
|
+
// Its permissions travel with it: an uploaded script stays runnable.
|
|
749
|
+
await this.write(remotePath, await readFile(localPath), { mode: info.mode & 0o777 });
|
|
673
750
|
return;
|
|
674
751
|
}
|
|
675
752
|
const { packDirectory } = await import("./tar.js");
|
|
@@ -681,7 +758,7 @@ export class Files {
|
|
|
681
758
|
const result = await new Sandbox(this.t, { id: this.sandboxId }).exec([
|
|
682
759
|
"sh",
|
|
683
760
|
"-c",
|
|
684
|
-
'mkdir -p "$1" && tar -
|
|
761
|
+
'mkdir -p "$1" && tar -xpzf "$2" -C "$1"; code=$?; rm -f "$2"; exit $code',
|
|
685
762
|
"sh",
|
|
686
763
|
remotePath,
|
|
687
764
|
staging,
|
package/dist/snapshots.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Page } from "./page.js";
|
|
2
2
|
import type { RequestOptions, Transport } from "./transport.js";
|
|
3
3
|
/** A sandbox's whole machine (files, memory, running processes), kept on its
|
|
4
|
-
* host to start new sandboxes from
|
|
4
|
+
* host to start new sandboxes from, and copied off the host so it survives
|
|
5
|
+
* losing that host. */
|
|
5
6
|
export type Snapshot = {
|
|
6
7
|
id: string;
|
|
7
8
|
kind: "snapshot";
|
|
@@ -19,6 +20,15 @@ export type Snapshot = {
|
|
|
19
20
|
/** What it stores, and what storage is metered for: the bytes it alone holds. */
|
|
20
21
|
storedBytes: number | null;
|
|
21
22
|
meteredBytes: number | null;
|
|
23
|
+
/** Whether its copy off the host is made and checked. */
|
|
24
|
+
backedUp: boolean;
|
|
25
|
+
/** Where that copy stands. `restoring`: its host was lost and it is being
|
|
26
|
+
* restored onto another; forks answer `snapshot_restoring` until it is. */
|
|
27
|
+
durability: {
|
|
28
|
+
state: "none" | "pending" | "durable" | "failed" | "restoring";
|
|
29
|
+
durableAt: string | null;
|
|
30
|
+
storedBytes: number | null;
|
|
31
|
+
};
|
|
22
32
|
error: string | null;
|
|
23
33
|
createdAt: string;
|
|
24
34
|
readyAt: string | null;
|
package/dist/ssh.js
CHANGED
|
@@ -265,9 +265,21 @@ export async function portForwardCommand(argv, out, connect, me) {
|
|
|
265
265
|
const sbx = await resolveSandbox(await connect(), target);
|
|
266
266
|
const tunnel = await sbx.tunnel();
|
|
267
267
|
const forwards = [];
|
|
268
|
+
/* A connection the sandbox refused is closed at once, which a browser or
|
|
269
|
+
curl reports only as an empty reply, so say why here, once per reason. */
|
|
270
|
+
const said = new Set();
|
|
271
|
+
const refused = (remote) => (error) => {
|
|
272
|
+
const line = `Port ${remote}: ${error.message}${error.hint ? ` ${error.hint}` : ""}`;
|
|
273
|
+
if (said.has(line))
|
|
274
|
+
return;
|
|
275
|
+
said.add(line);
|
|
276
|
+
out.error(out.json
|
|
277
|
+
? JSON.stringify({ port: remote, error: { code: error.code, message: error.message } })
|
|
278
|
+
: line);
|
|
279
|
+
};
|
|
268
280
|
try {
|
|
269
281
|
for (const { local, remote } of ports)
|
|
270
|
-
forwards.push(await tunnel.forward(remote, { localPort: local, host: address }));
|
|
282
|
+
forwards.push(await tunnel.forward(remote, { localPort: local, host: address, onError: refused(remote) }));
|
|
271
283
|
}
|
|
272
284
|
catch (error) {
|
|
273
285
|
tunnel.close();
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.6.0";
|
|
3
3
|
export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
4
4
|
export type RequestOptions = {
|
|
5
5
|
/** Sent as Idempotency-Key. Omit it: the SDK makes one per call and keeps
|
package/dist/transport.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConnectionError, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
|
|
2
2
|
import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
|
|
3
|
-
export const VERSION = "0.
|
|
3
|
+
export const VERSION = "0.6.0";
|
|
4
4
|
export const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
5
5
|
export function apiOrigin(value) {
|
|
6
6
|
const url = new URL(value);
|
|
@@ -289,6 +289,9 @@ export class Transport {
|
|
|
289
289
|
yield JSON.parse(buffer);
|
|
290
290
|
}
|
|
291
291
|
finally {
|
|
292
|
+
// Returning early from the iterator must close the HTTP stream too.
|
|
293
|
+
// Cleanup must not replace a parse, network or callback error.
|
|
294
|
+
await reader.cancel().catch(() => undefined);
|
|
292
295
|
reader.releaseLock();
|
|
293
296
|
}
|
|
294
297
|
}
|
package/dist/tunnel.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RuntimeError } from "./errors.js";
|
|
1
2
|
/** One TCP connection (or SSH login) inside a tunnel. Set the handlers before
|
|
2
3
|
* data can arrive, which is as soon as the promise that gave you this resolves. */
|
|
3
4
|
export declare class TunnelStream {
|
|
@@ -49,10 +50,12 @@ export declare class Tunnel {
|
|
|
49
50
|
* (the text of an OpenSSH `.pub` file). Speak SSH over the stream. */
|
|
50
51
|
ssh(publicKey: string): Promise<TunnelStream>;
|
|
51
52
|
/** Listens on a local port and carries each connection to `port` in the
|
|
52
|
-
* sandbox. Node and Bun only.
|
|
53
|
+
* sandbox. Node and Bun only. A connection the sandbox cannot open, such as
|
|
54
|
+
* one to a port nothing listens on, is closed at once; `onError` says why. */
|
|
53
55
|
forward(port: number, options?: {
|
|
54
56
|
localPort?: number;
|
|
55
57
|
host?: string;
|
|
58
|
+
onError?: (error: RuntimeError) => void;
|
|
56
59
|
}): Promise<PortForward>;
|
|
57
60
|
/** Closes the tunnel and every connection in it. */
|
|
58
61
|
close(): void;
|
package/dist/tunnel.js
CHANGED
|
@@ -160,7 +160,8 @@ export class Tunnel {
|
|
|
160
160
|
return this.#open(`ssh ${publicKey.trim()}`);
|
|
161
161
|
}
|
|
162
162
|
/** Listens on a local port and carries each connection to `port` in the
|
|
163
|
-
* sandbox. Node and Bun only.
|
|
163
|
+
* sandbox. Node and Bun only. A connection the sandbox cannot open, such as
|
|
164
|
+
* one to a port nothing listens on, is closed at once; `onError` says why. */
|
|
164
165
|
async forward(port, options = {}) {
|
|
165
166
|
const net = await import("node:net");
|
|
166
167
|
const host = options.host ?? "127.0.0.1";
|
|
@@ -183,7 +184,11 @@ export class Tunnel {
|
|
|
183
184
|
socket.on("end", () => stream.end());
|
|
184
185
|
socket.on("close", () => stream.close());
|
|
185
186
|
socket.resume();
|
|
186
|
-
}, () =>
|
|
187
|
+
}, (error) => {
|
|
188
|
+
socket.destroy();
|
|
189
|
+
if (error instanceof RuntimeError)
|
|
190
|
+
options.onError?.(error);
|
|
191
|
+
});
|
|
187
192
|
});
|
|
188
193
|
await new Promise((resolve, reject) => {
|
|
189
194
|
server.once("error", reject);
|
|
@@ -217,7 +222,7 @@ export class Tunnel {
|
|
|
217
222
|
message: `Nothing is listening on port ${port} in the sandbox.`,
|
|
218
223
|
code: "port_closed",
|
|
219
224
|
status: 0,
|
|
220
|
-
hint: "Start the server
|
|
225
|
+
hint: "Start the server with spawn (`runtime sandbox spawn <id> -- <command>`, sbx.spawn()), which keeps it running: a process started by exec ends with its command. Let it listen on 127.0.0.1, ::1 or every address.",
|
|
221
226
|
})
|
|
222
227
|
: error.message === "reserved"
|
|
223
228
|
? new RuntimeError({
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A WireGuard key pair, as `wg genkey | wg pubkey` makes: X25519, each half
|
|
2
|
+
* 32 bytes of base64. Made on the machine that will use it, so the private
|
|
3
|
+
* key is never sent anywhere. */
|
|
4
|
+
export declare function generateWireGuardKeyPair(): {
|
|
5
|
+
privateKey: string;
|
|
6
|
+
publicKey: string;
|
|
7
|
+
};
|