withruntime 0.8.4 → 0.10.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/COMPATIBILITY.md +145 -0
- package/DAYTONA.md +13 -7
- package/E2B.md +34 -20
- package/README.md +22 -11
- package/README.src.md +22 -11
- package/VERCEL.md +13 -3
- package/compatibility-lock.json +357 -0
- package/dist/ai-harness/index.js +2 -2
- package/dist/api-defaults.d.ts +17 -0
- package/dist/api-defaults.js +22 -0
- 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 +3 -2
- package/dist/blaxel/preview.js +16 -14
- package/dist/blaxel/process.d.ts +1 -1
- package/dist/blaxel/process.js +66 -11
- 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-words.d.ts +27 -0
- package/dist/cli-words.js +135 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +351 -85
- package/dist/client.d.ts +33 -4
- package/dist/client.js +47 -12
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +303 -0
- package/dist/cloudflare/index.js +592 -0
- package/dist/cloudflare/worker.d.ts +419 -0
- package/dist/cloudflare/worker.js +447 -0
- package/dist/codesandbox/guest-service.d.ts +31 -0
- package/dist/codesandbox/guest-service.js +490 -0
- package/dist/codesandbox/index.d.ts +386 -0
- package/dist/codesandbox/index.js +1347 -0
- package/dist/compat/core.d.ts +40 -0
- package/dist/compat/core.js +238 -0
- package/dist/compat/files.d.ts +26 -0
- package/dist/compat/files.js +133 -0
- package/dist/compat/shell.d.ts +4 -0
- package/dist/compat/shell.js +165 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/context.js +2 -1
- package/dist/daytona/daytona.js +11 -4
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +121 -38
- package/dist/daytona/index.d.ts +1 -1
- package/dist/daytona/process.d.ts +1 -1
- package/dist/daytona/process.js +27 -4
- package/dist/daytona/sandbox.d.ts +1 -1
- package/dist/daytona/sandbox.js +16 -7
- package/dist/e2b/client.js +1 -1
- package/dist/e2b/code-interpreter.d.ts +4 -5
- package/dist/e2b/code-interpreter.js +54 -56
- package/dist/e2b/commands.d.ts +35 -12
- package/dist/e2b/commands.js +169 -73
- package/dist/e2b/errors.js +1 -1
- package/dist/e2b/filesystem.d.ts +8 -6
- package/dist/e2b/filesystem.js +127 -28
- package/dist/e2b/index.d.ts +1 -0
- package/dist/e2b/index.js +1 -0
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +80 -0
- package/dist/e2b/sandbox.d.ts +6 -4
- package/dist/e2b/sandbox.js +59 -25
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -2
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +309 -0
- package/dist/freestyle/index.js +343 -0
- package/dist/index.d.ts +3 -1
- package/dist/login.js +16 -10
- package/dist/mcp.js +2 -2
- package/dist/modal/index.d.ts +205 -0
- package/dist/modal/index.js +486 -0
- package/dist/network-cli.js +112 -44
- package/dist/openai-agents/index.js +18 -71
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/audit.d.ts +3 -0
- package/dist/products/index.d.ts +14 -0
- package/dist/products/index.js +12 -0
- package/dist/products/interpreter.d.ts +11 -4
- package/dist/products/interpreter.js +33 -9
- package/dist/products/jobs.js +3 -2
- package/dist/products/limits.d.ts +10 -0
- package/dist/products/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +18 -4
- package/dist/products/previews.js +14 -5
- package/dist/products/tailscale.d.ts +57 -0
- package/dist/products/tailscale.js +17 -0
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.d.ts +25 -4
- package/dist/products/watch.js +132 -18
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +326 -0
- package/dist/runloop/index.js +535 -0
- package/dist/runloop/pagination.d.ts +46 -0
- package/dist/runloop/pagination.js +82 -0
- package/dist/runloop/snapshots.d.ts +52 -0
- package/dist/runloop/snapshots.js +244 -0
- package/dist/sandbox.d.ts +76 -16
- package/dist/sandbox.js +696 -135
- package/dist/snapshots.d.ts +20 -5
- package/dist/snapshots.js +11 -1
- package/dist/sprites/index.d.ts +251 -0
- package/dist/sprites/index.js +588 -0
- package/dist/tar.d.ts +10 -1
- package/dist/tar.js +311 -35
- package/dist/tools/index.js +2 -1
- package/dist/transport.d.ts +23 -8
- package/dist/transport.js +247 -127
- package/dist/types.d.ts +52 -3
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +15 -3
- package/dist/vercel/errors.d.ts +2 -2
- package/dist/vercel/errors.js +19 -2
- package/dist/vercel/filesystem.d.ts +21 -18
- package/dist/vercel/filesystem.js +95 -48
- package/dist/vercel/sandbox.d.ts +8 -8
- package/dist/vercel/sandbox.js +159 -64
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- package/package.json +46 -3
package/dist/credentials.js
CHANGED
|
@@ -12,6 +12,14 @@ export function connectionOrigins(env) {
|
|
|
12
12
|
auth: apiOrigin(env.RUNTIME_AUTH_URL ?? "https://withruntime.com"),
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
+
/** Browser-issued production credentials must only reach the production API.
|
|
16
|
+
* CLI and MCP share this boundary, including resumed device requests. */
|
|
17
|
+
export function checkBrowserOrigins(origins) {
|
|
18
|
+
const local = (origin) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(origin).hostname);
|
|
19
|
+
if (!((origins.auth === "https://withruntime.com" && origins.api === "https://api.withruntime.com") ||
|
|
20
|
+
(local(origins.auth) && local(origins.api))))
|
|
21
|
+
throw new Error("Browser login supports Runtime's production endpoints or local test endpoints. Check RUNTIME_AUTH_URL and RUNTIME_API_URL.");
|
|
22
|
+
}
|
|
15
23
|
function defaultDirectory(env) {
|
|
16
24
|
const root = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
17
25
|
if (!isAbsolute(root))
|
|
@@ -93,8 +101,75 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
93
101
|
throw error;
|
|
94
102
|
});
|
|
95
103
|
}
|
|
104
|
+
const lockFile = join(directory, `${name}.lock`);
|
|
105
|
+
/** ARCHITECTURE.md section 3.9: concurrent CLI processes keep each account's
|
|
106
|
+
* connection. Serialize the entire read/modify/write, not only each rename.
|
|
107
|
+
* Never steal a lock: stale cleanup can otherwise delete a new live lock. */
|
|
108
|
+
async function updateAccounts(update) {
|
|
109
|
+
await privateDirectory(true);
|
|
110
|
+
const until = performance.now() + 10_000;
|
|
111
|
+
let handle;
|
|
112
|
+
for (;;) {
|
|
113
|
+
try {
|
|
114
|
+
handle = await open(lockFile, "wx", 0o600);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code !== "EEXIST")
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
const text = await readPrivate(lockFile, 1024, "credential update lock");
|
|
122
|
+
let pid;
|
|
123
|
+
try {
|
|
124
|
+
pid = JSON.parse(text ?? "null")?.pid;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// The holder may not have finished writing its process id yet.
|
|
128
|
+
}
|
|
129
|
+
if (typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0) {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code === "ESRCH")
|
|
135
|
+
throw new Error(`A previous Runtime credential update was interrupted. Remove ${lockFile} and retry.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (performance.now() >= until)
|
|
139
|
+
throw new Error(`Another Runtime command is updating saved accounts. Retry when it finishes. If no command is running, remove ${lockFile} and retry.`);
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid }));
|
|
144
|
+
return await update();
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
try {
|
|
148
|
+
await handle.close();
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await unlink(lockFile);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
96
155
|
const pendingFile = join(directory, `${name}.pending.json`);
|
|
97
156
|
const accountsFile = join(directory, `${name}.accounts.json`);
|
|
157
|
+
/** Active and archived connections must satisfy the same contract: switching
|
|
158
|
+
* accounts cannot publish a credential the next command refuses to read. */
|
|
159
|
+
function validConnection(value) {
|
|
160
|
+
if (value === null || typeof value !== "object")
|
|
161
|
+
return false;
|
|
162
|
+
const saved = value;
|
|
163
|
+
return (saved.version === 1 &&
|
|
164
|
+
saved.apiOrigin === origins.api &&
|
|
165
|
+
saved.authOrigin === origins.auth &&
|
|
166
|
+
typeof saved.key === "string" &&
|
|
167
|
+
/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(saved.key) &&
|
|
168
|
+
typeof saved.connectionId === "string" &&
|
|
169
|
+
typeof saved.orgId === "string" &&
|
|
170
|
+
typeof saved.agentName === "string" &&
|
|
171
|
+
(saved.orgName === undefined || typeof saved.orgName === "string"));
|
|
172
|
+
}
|
|
98
173
|
return {
|
|
99
174
|
async read() {
|
|
100
175
|
const text = await readPrivate(file, 4096, "saved connection");
|
|
@@ -107,15 +182,7 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
107
182
|
catch {
|
|
108
183
|
throw new Error("Runtime's saved connection is invalid. Connect again.");
|
|
109
184
|
}
|
|
110
|
-
if (saved
|
|
111
|
-
saved.apiOrigin !== origins.api ||
|
|
112
|
-
saved.authOrigin !== origins.auth ||
|
|
113
|
-
typeof saved.key !== "string" ||
|
|
114
|
-
!/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(saved.key) ||
|
|
115
|
-
typeof saved.connectionId !== "string" ||
|
|
116
|
-
typeof saved.orgId !== "string" ||
|
|
117
|
-
typeof saved.agentName !== "string" ||
|
|
118
|
-
(saved.orgName !== undefined && typeof saved.orgName !== "string"))
|
|
185
|
+
if (!validConnection(saved))
|
|
119
186
|
throw new Error("Runtime's saved connection is invalid. Connect again.");
|
|
120
187
|
return saved;
|
|
121
188
|
},
|
|
@@ -125,12 +192,14 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
125
192
|
async save(connection) {
|
|
126
193
|
if (connection.apiOrigin !== origins.api || connection.authOrigin !== origins.auth)
|
|
127
194
|
throw new Error("Connection origins do not match.");
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
195
|
+
return updateAccounts(async () => {
|
|
196
|
+
const previous = await this.read().catch(() => null);
|
|
197
|
+
if (previous && previous.orgId !== connection.orgId) {
|
|
198
|
+
const others = (await this.others()).filter((one) => one.orgId !== previous.orgId && one.orgId !== connection.orgId);
|
|
199
|
+
await writePrivate(accountsFile, JSON.stringify([previous, ...others].slice(0, SAVED_ACCOUNTS)));
|
|
200
|
+
}
|
|
201
|
+
await writePrivate(file, JSON.stringify(connection));
|
|
202
|
+
});
|
|
134
203
|
},
|
|
135
204
|
/** The other accounts this machine has connected, newest first. */
|
|
136
205
|
async others() {
|
|
@@ -145,15 +214,7 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
145
214
|
return [];
|
|
146
215
|
try {
|
|
147
216
|
const list = JSON.parse(text);
|
|
148
|
-
return Array.isArray(list)
|
|
149
|
-
? list.filter((one) => one &&
|
|
150
|
-
one.version === 1 &&
|
|
151
|
-
one.apiOrigin === origins.api &&
|
|
152
|
-
one.authOrigin === origins.auth &&
|
|
153
|
-
typeof one.key === "string" &&
|
|
154
|
-
/^rtcloud_[a-f0-9-]{36}_[A-Za-z0-9_-]{43}$/.test(one.key) &&
|
|
155
|
-
typeof one.orgId === "string")
|
|
156
|
-
: [];
|
|
217
|
+
return Array.isArray(list) ? list.filter(validConnection) : [];
|
|
157
218
|
}
|
|
158
219
|
catch {
|
|
159
220
|
return [];
|
|
@@ -162,21 +223,29 @@ export function connectionStore(env, directory = defaultDirectory(env)) {
|
|
|
162
223
|
/** Use another saved account's connection, by account id or name. The one
|
|
163
224
|
* in use moves to the saved accounts. Null when none matches. */
|
|
164
225
|
async use(which) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
others.find((one) => one.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
226
|
+
return updateAccounts(async () => {
|
|
227
|
+
const others = await this.others();
|
|
228
|
+
const wanted = which.trim().toLowerCase();
|
|
229
|
+
const chosen = others.find((one) => one.orgId === which) ??
|
|
230
|
+
others.find((one) => one.orgName?.toLowerCase() === wanted) ??
|
|
231
|
+
null;
|
|
232
|
+
if (!chosen)
|
|
233
|
+
return null;
|
|
234
|
+
const current = await this.read().catch(() => null);
|
|
235
|
+
const rest = others.filter((one) => one.orgId !== chosen.orgId);
|
|
236
|
+
await writePrivate(accountsFile, JSON.stringify((current ? [current, ...rest] : rest).slice(0, SAVED_ACCOUNTS)));
|
|
237
|
+
await writePrivate(file, JSON.stringify(chosen));
|
|
238
|
+
return chosen;
|
|
239
|
+
});
|
|
177
240
|
},
|
|
178
|
-
|
|
179
|
-
|
|
241
|
+
/** Forget the expected credential after a remote revoke or refusal. A
|
|
242
|
+
* concurrent login may have saved another connection during that request. */
|
|
243
|
+
async remove(expectedKey) {
|
|
244
|
+
await updateAccounts(async () => {
|
|
245
|
+
if (expectedKey !== undefined && (await this.read())?.key !== expectedKey)
|
|
246
|
+
return;
|
|
247
|
+
await removePrivate(file);
|
|
248
|
+
});
|
|
180
249
|
},
|
|
181
250
|
/** A browser approval that was started and not yet answered, so a command
|
|
182
251
|
* that was stopped while it waited picks up the same link and code rather
|
package/dist/daytona/context.js
CHANGED
|
@@ -17,7 +17,8 @@ export function resolvePath(path) {
|
|
|
17
17
|
return `${HOME}/${path.slice(2)}`;
|
|
18
18
|
if (path.startsWith("/"))
|
|
19
19
|
return path;
|
|
20
|
-
|
|
20
|
+
// Only "." components are dropped: ".env" and "..x" keep their dots.
|
|
21
|
+
return `${HOME}/${path.replace(/^(?:\.(?:\/+|$))+/, "")}`.replace(/\/+$/, "") || HOME;
|
|
21
22
|
}
|
|
22
23
|
/** Quotes a word for bash. */
|
|
23
24
|
export function quote(word) {
|
package/dist/daytona/daytona.js
CHANGED
|
@@ -105,6 +105,8 @@ export class SnapshotService {
|
|
|
105
105
|
/** Builds a Runtime image named `name` from a registry image or a Daytona
|
|
106
106
|
* Image, streaming the build log to `onLogs`. */
|
|
107
107
|
async create(params, options = {}) {
|
|
108
|
+
if (params.regionId)
|
|
109
|
+
throw new NotSupportedError("Choosing a snapshot region (regionId)", "Omit regionId: Runtime places image builds on its available hosts.");
|
|
108
110
|
if (params.resources)
|
|
109
111
|
throw new NotSupportedError("Resources on a snapshot", "Pass resources when creating each sandbox from it: daytona.create({ snapshot, resources }).");
|
|
110
112
|
if (params.entrypoint)
|
|
@@ -183,8 +185,6 @@ export class Daytona {
|
|
|
183
185
|
const client = params.withruntime?.client ?? this.#client;
|
|
184
186
|
const lifecycle = lifecycleOf(params);
|
|
185
187
|
const resources = { ...DEFAULT_RESOURCES, ...params.resources };
|
|
186
|
-
if (resources.gpu)
|
|
187
|
-
throw new NotSupportedError("GPUs", "Runtime runs sandboxes on CPUs; remove gpu from resources.");
|
|
188
188
|
const source = await this.#source(client, params, options.onSnapshotCreateLogs);
|
|
189
189
|
const volumes = await this.#volumes(client, params.volumes);
|
|
190
190
|
const network = networkRules(params);
|
|
@@ -267,8 +267,6 @@ export class Daytona {
|
|
|
267
267
|
async #volumes(client, mounts) {
|
|
268
268
|
const out = [];
|
|
269
269
|
for (const mount of mounts ?? []) {
|
|
270
|
-
if (mount.subpath)
|
|
271
|
-
throw new NotSupportedError("Mounting part of a volume (subpath)", "Mount the whole volume.");
|
|
272
270
|
let id = mount.volumeId;
|
|
273
271
|
if (!UUID.test(id)) {
|
|
274
272
|
const found = (await guard("other", () => client.volumes.list({ name: id, limit: 1 })))
|
|
@@ -363,6 +361,15 @@ export class Daytona {
|
|
|
363
361
|
}
|
|
364
362
|
}
|
|
365
363
|
function refuseCreate(params) {
|
|
364
|
+
const gpuType = params.resources?.gpuType;
|
|
365
|
+
if (params.resources?.gpu ||
|
|
366
|
+
(gpuType !== undefined &&
|
|
367
|
+
gpuType !== null &&
|
|
368
|
+
gpuType !== "" &&
|
|
369
|
+
(!Array.isArray(gpuType) || gpuType.length > 0)))
|
|
370
|
+
throw new NotSupportedError("GPUs", "Runtime runs sandboxes on CPUs; remove gpu and gpuType from resources.");
|
|
371
|
+
if (params.volumes?.some((mount) => mount.subpath))
|
|
372
|
+
throw new NotSupportedError("Mounting part of a volume (subpath)", "Mount the whole volume.");
|
|
366
373
|
const refusals = [
|
|
367
374
|
["spot", "Spot GPU sandboxes (spot)", "Remove it: Runtime runs sandboxes on CPUs."],
|
|
368
375
|
[
|
|
@@ -49,6 +49,14 @@ export interface FileDownloadResponse {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
export type UploadSource = Buffer | Uint8Array | string | Readable | ReadableStream<Uint8Array>;
|
|
52
|
+
export type UploadProgress = {
|
|
53
|
+
bytesSent: number;
|
|
54
|
+
};
|
|
55
|
+
export type UploadStreamOptions = {
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
timeout?: number;
|
|
58
|
+
onProgress?: (progress: UploadProgress) => void;
|
|
59
|
+
};
|
|
52
60
|
/** `sandbox.fs`: Daytona's file calls over Runtime's files API. Relative
|
|
53
61
|
* paths resolve from the working directory, /workspace on Runtime. */
|
|
54
62
|
export declare class FileSystem {
|
|
@@ -62,9 +70,12 @@ export declare class FileSystem {
|
|
|
62
70
|
downloadFile(remotePath: string, localPath: string, timeout?: number): Promise<void>;
|
|
63
71
|
/** The file as it arrives, without holding it in memory. It errors with
|
|
64
72
|
* `download_incomplete` if it ends short of the file's length. */
|
|
65
|
-
downloadFileStream(remotePath: string
|
|
73
|
+
downloadFileStream(remotePath: string, timeoutOrOptions?: number | {
|
|
74
|
+
timeout?: number;
|
|
75
|
+
signal?: AbortSignal;
|
|
76
|
+
}): Promise<Readable>;
|
|
66
77
|
/** Several files; a file that fails carries `error` instead of `result`. */
|
|
67
|
-
downloadFiles(files: FileDownloadRequest[]): Promise<FileDownloadResponse[]>;
|
|
78
|
+
downloadFiles(files: FileDownloadRequest[], timeout?: number): Promise<FileDownloadResponse[]>;
|
|
68
79
|
/** Lines matching `pattern` (a grep pattern) in files under `path`. */
|
|
69
80
|
findFiles(path: string, pattern: string): Promise<Match[]>;
|
|
70
81
|
getFileDetails(path: string): Promise<FileInfo>;
|
|
@@ -81,6 +92,8 @@ export declare class FileSystem {
|
|
|
81
92
|
* directories and replacing one that exists. */
|
|
82
93
|
uploadFile(file: Buffer, remotePath: string, timeout?: number): Promise<void>;
|
|
83
94
|
uploadFile(localPath: string, remotePath: string, timeout?: number): Promise<void>;
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
/** Uploads with bounded memory. Unknown-length streams stage in a private
|
|
96
|
+
* temporary file before the native atomic upload; local paths avoid staging. */
|
|
97
|
+
uploadFileStream(source: UploadSource, remotePath: string, options?: UploadStreamOptions): Promise<void>;
|
|
98
|
+
uploadFiles(files: FileUpload[], timeout?: number): Promise<void>;
|
|
86
99
|
}
|
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
import { resolvePath, WHOLE_OUTPUT } from "./context.js";
|
|
2
2
|
import { DaytonaError, DaytonaFileNotFoundError, guard } from "./errors.js";
|
|
3
3
|
const PERMISSIONS = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"];
|
|
4
|
+
function downloadRequest(timeout, signal) {
|
|
5
|
+
if (!Number.isFinite(timeout) || timeout < 0)
|
|
6
|
+
throw new DaytonaError("Download timeout must be a nonnegative finite number.");
|
|
7
|
+
signal?.throwIfAborted();
|
|
8
|
+
const deadline = timeout > 0 ? AbortSignal.timeout(Math.ceil(timeout * 1000)) : undefined;
|
|
9
|
+
return {
|
|
10
|
+
timeoutMs: 0,
|
|
11
|
+
...(deadline || signal
|
|
12
|
+
? {
|
|
13
|
+
signal: deadline && signal ? AbortSignal.any([deadline, signal]) : (deadline ?? signal),
|
|
14
|
+
}
|
|
15
|
+
: {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Wait on client-local setup within the transfer's existing deadline. The
|
|
19
|
+
* setup may finish later; the caller cannot proceed to an upload after abort. */
|
|
20
|
+
function waitForRequest(pending, request) {
|
|
21
|
+
const signal = request.signal;
|
|
22
|
+
if (!signal)
|
|
23
|
+
return pending;
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rejects with the caller's abort reason as is, as the upstream SDK does
|
|
26
|
+
const abort = () => reject(signal.reason);
|
|
27
|
+
if (signal.aborted)
|
|
28
|
+
abort();
|
|
29
|
+
else
|
|
30
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
31
|
+
pending.then((value) => {
|
|
32
|
+
signal.removeEventListener("abort", abort);
|
|
33
|
+
resolve(value);
|
|
34
|
+
}, (error) => {
|
|
35
|
+
signal.removeEventListener("abort", abort);
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- passes the pending call's own failure through unchanged
|
|
37
|
+
reject(error);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
4
41
|
function fileInfo(entry) {
|
|
5
42
|
const bits = (parseInt(entry.mode, 8) || 0) & 0o777;
|
|
6
43
|
const kind = entry.type === "directory" ? "d" : entry.type === "symlink" ? "l" : "-";
|
|
@@ -15,20 +52,6 @@ function fileInfo(entry) {
|
|
|
15
52
|
group: "",
|
|
16
53
|
};
|
|
17
54
|
}
|
|
18
|
-
async function bytesOf(source) {
|
|
19
|
-
if (typeof source === "string") {
|
|
20
|
-
const { readFile } = await import("node:fs/promises");
|
|
21
|
-
return readFile(source);
|
|
22
|
-
}
|
|
23
|
-
if (source instanceof Uint8Array)
|
|
24
|
-
return source;
|
|
25
|
-
if (typeof source.getReader === "function")
|
|
26
|
-
return new Uint8Array(await new Response(source).arrayBuffer());
|
|
27
|
-
const chunks = [];
|
|
28
|
-
for await (const chunk of source)
|
|
29
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk));
|
|
30
|
-
return Buffer.concat(chunks);
|
|
31
|
-
}
|
|
32
55
|
/** `sandbox.fs`: Daytona's file calls over Runtime's files API. Relative
|
|
33
56
|
* paths resolve from the working directory, /workspace on Runtime. */
|
|
34
57
|
export class FileSystem {
|
|
@@ -66,36 +89,56 @@ export class FileSystem {
|
|
|
66
89
|
if (!removed)
|
|
67
90
|
throw new DaytonaFileNotFoundError(`${target} does not exist.`, 404);
|
|
68
91
|
}
|
|
69
|
-
async downloadFile(remotePath, localOrTimeout) {
|
|
92
|
+
async downloadFile(remotePath, localOrTimeout, timeout = 30 * 60) {
|
|
93
|
+
// The numeric second argument is the published buffer overload. In that
|
|
94
|
+
// overload zero falls back to the default, as the pinned SDK does.
|
|
95
|
+
const request = downloadRequest(typeof localOrTimeout === "number" && localOrTimeout !== 0 ? localOrTimeout : timeout);
|
|
96
|
+
return this.#downloadFile(remotePath, typeof localOrTimeout === "string" ? localOrTimeout : undefined, request);
|
|
97
|
+
}
|
|
98
|
+
async #downloadFile(remotePath, localPath, request) {
|
|
99
|
+
request.signal?.throwIfAborted();
|
|
70
100
|
const target = await this.#path(remotePath);
|
|
71
101
|
const files = await this.#files();
|
|
72
|
-
|
|
73
|
-
|
|
102
|
+
request.signal?.throwIfAborted();
|
|
103
|
+
if (localPath === undefined)
|
|
104
|
+
return Buffer.from(await guard("file", () => files.read(target, request)));
|
|
74
105
|
// Streamed to disk, any size, checked against the file's length.
|
|
75
|
-
await guard("file", () => files.download(target,
|
|
106
|
+
await guard("file", () => files.download(target, localPath, request));
|
|
76
107
|
}
|
|
77
108
|
/** The file as it arrives, without holding it in memory. It errors with
|
|
78
109
|
* `download_incomplete` if it ends short of the file's length. */
|
|
79
|
-
async downloadFileStream(remotePath) {
|
|
110
|
+
async downloadFileStream(remotePath, timeoutOrOptions) {
|
|
111
|
+
const options = typeof timeoutOrOptions === "number"
|
|
112
|
+
? { timeout: timeoutOrOptions }
|
|
113
|
+
: (timeoutOrOptions ?? {});
|
|
114
|
+
const request = downloadRequest(options.timeout ?? 30 * 60, options.signal);
|
|
80
115
|
const { Readable } = await import("node:stream");
|
|
81
116
|
const target = await this.#path(remotePath);
|
|
82
117
|
const files = await this.#files();
|
|
83
|
-
|
|
118
|
+
request.signal?.throwIfAborted();
|
|
119
|
+
const stream = await guard("file", () => files.readStream(target, request));
|
|
84
120
|
return Readable.fromWeb(stream);
|
|
85
121
|
}
|
|
86
122
|
/** Several files; a file that fails carries `error` instead of `result`. */
|
|
87
|
-
async downloadFiles(files) {
|
|
123
|
+
async downloadFiles(files, timeout = 30 * 60) {
|
|
124
|
+
const request = downloadRequest(timeout);
|
|
88
125
|
const out = [];
|
|
89
126
|
for (const file of files) {
|
|
127
|
+
request.signal?.throwIfAborted();
|
|
90
128
|
try {
|
|
91
129
|
if (file.destination) {
|
|
92
|
-
await this
|
|
130
|
+
await this.#downloadFile(file.source, file.destination, request);
|
|
93
131
|
out.push({ source: file.source, result: file.destination });
|
|
94
132
|
}
|
|
95
133
|
else
|
|
96
|
-
out.push({
|
|
134
|
+
out.push({
|
|
135
|
+
source: file.source,
|
|
136
|
+
result: (await this.#downloadFile(file.source, undefined, request)),
|
|
137
|
+
});
|
|
97
138
|
}
|
|
98
139
|
catch (error) {
|
|
140
|
+
if (request.signal?.aborted)
|
|
141
|
+
throw error;
|
|
99
142
|
const failure = error;
|
|
100
143
|
out.push({
|
|
101
144
|
source: file.source,
|
|
@@ -185,24 +228,64 @@ export class FileSystem {
|
|
|
185
228
|
target,
|
|
186
229
|
]);
|
|
187
230
|
}
|
|
188
|
-
async uploadFile(source, remotePath) {
|
|
189
|
-
|
|
190
|
-
const bytes = await bytesOf(source);
|
|
191
|
-
const files = await this.#files();
|
|
192
|
-
await guard("file", () => files.write(target, bytes));
|
|
231
|
+
async uploadFile(source, remotePath, timeout = 30 * 60) {
|
|
232
|
+
await this.#uploadFile(source, remotePath, downloadRequest(timeout));
|
|
193
233
|
}
|
|
194
|
-
async
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
234
|
+
async #uploadFile(source, remotePath, request, onProgress) {
|
|
235
|
+
request.signal?.throwIfAborted();
|
|
236
|
+
const readable = typeof source !== "string" && typeof source.destroy === "function"
|
|
237
|
+
? source
|
|
238
|
+
: undefined;
|
|
239
|
+
const stopInput = () => {
|
|
240
|
+
readable?.destroy();
|
|
241
|
+
};
|
|
242
|
+
request.signal?.addEventListener("abort", stopInput, { once: true });
|
|
243
|
+
try {
|
|
244
|
+
const target = await waitForRequest(this.#path(remotePath), request);
|
|
245
|
+
request.signal?.throwIfAborted();
|
|
246
|
+
const files = await waitForRequest(this.#files(), request);
|
|
247
|
+
request.signal?.throwIfAborted();
|
|
248
|
+
// Node Readables can emit strings after setEncoding; Daytona uploads
|
|
249
|
+
// their UTF-8 bytes. The generator retains only the current source chunk.
|
|
250
|
+
const input = typeof source === "string" ||
|
|
251
|
+
source instanceof Uint8Array ||
|
|
252
|
+
typeof source.getReader === "function"
|
|
253
|
+
? source
|
|
254
|
+
: (async function* () {
|
|
255
|
+
for await (const chunk of source)
|
|
256
|
+
yield typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
257
|
+
})();
|
|
258
|
+
await guard("file", () => input instanceof Uint8Array && !onProgress
|
|
259
|
+
? files.write(target, input, request)
|
|
260
|
+
: files.writeStream(target, input, {
|
|
261
|
+
...request,
|
|
262
|
+
...(onProgress ? { onProgress } : {}),
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
request.signal?.removeEventListener("abort", stopInput);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Uploads with bounded memory. Unknown-length streams stage in a private
|
|
270
|
+
* temporary file before the native atomic upload; local paths avoid staging. */
|
|
271
|
+
async uploadFileStream(source, remotePath, options = {}) {
|
|
272
|
+
const cancelled = () => new DaytonaError(`Upload cancelled: ${remotePath}`);
|
|
273
|
+
if (options.signal?.aborted)
|
|
274
|
+
throw cancelled();
|
|
275
|
+
const request = downloadRequest(options.timeout ?? 30 * 60, options.signal);
|
|
276
|
+
try {
|
|
277
|
+
await this.#uploadFile(source, remotePath, request, options.onProgress);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (options.signal?.aborted)
|
|
281
|
+
throw cancelled();
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
199
284
|
}
|
|
200
|
-
async uploadFiles(files) {
|
|
285
|
+
async uploadFiles(files, timeout = 30 * 60) {
|
|
286
|
+
const request = downloadRequest(timeout);
|
|
201
287
|
for (const file of files) {
|
|
202
|
-
|
|
203
|
-
const bytes = await bytesOf(file.source);
|
|
204
|
-
const api = await this.#files();
|
|
205
|
-
await guard("file", () => api.write(target, bytes));
|
|
288
|
+
await this.#uploadFile(file.source, file.destination, request);
|
|
206
289
|
}
|
|
207
290
|
}
|
|
208
291
|
}
|
package/dist/daytona/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { Daytona, CodeLanguage, SnapshotService, VolumeService, DEFAULT_RESOURCES, DEFAULT_AUTO_STOP_MINUTES, lifecycleOf, type CreateSandboxBaseParams, type CreateSandboxFromImageParams, type CreateSandboxFromSnapshotParams, type CreateSnapshotParams, type ForkSandboxParams, type ListSandboxesQuery, type PaginatedSnapshots, type Resources, type Snapshot, type Volume, type VolumeMount, } from "./daytona.js";
|
|
2
2
|
export { Sandbox, CodeInterpreter, networkRules, windowSeconds, type ExecutionError, type ExecutionResult, type InterpreterContext, type OutputMessage, type PortPreviewUrl, } from "./sandbox.js";
|
|
3
3
|
export { Process, CodeRunParams, type Command, type ExecuteResponse, type Session, type SessionCommandLogsResponse, type SessionExecuteRequest, type SessionExecuteResponse, } from "./process.js";
|
|
4
|
-
export { FileSystem, type FileDownloadRequest, type FileDownloadResponse, type FileInfo, type FilePermissionsParams, type FileUpload, type Match, type ReplaceResult, type SearchFilesResponse, type UploadSource, } from "./filesystem.js";
|
|
4
|
+
export { FileSystem, type FileDownloadRequest, type FileDownloadResponse, type FileInfo, type FilePermissionsParams, type FileUpload, type Match, type ReplaceResult, type SearchFilesResponse, type UploadSource, type UploadProgress, type UploadStreamOptions, } from "./filesystem.js";
|
|
5
5
|
export { Git, type GitStatus, type ListBranchResponse } from "./git.js";
|
|
6
6
|
export { Image } from "./image.js";
|
|
7
7
|
export { resolvePath } from "./context.js";
|
|
@@ -63,7 +63,7 @@ export declare class Process {
|
|
|
63
63
|
getSessionCommand(sessionId: string, commandId: string): Promise<Command>;
|
|
64
64
|
/** Runs a command in the session. It waits and returns the output and exit
|
|
65
65
|
* code, or with `runAsync: true` returns the cmdId at once. */
|
|
66
|
-
executeSessionCommand(sessionId: string, req: SessionExecuteRequest,
|
|
66
|
+
executeSessionCommand(sessionId: string, req: SessionExecuteRequest, timeout?: number): Promise<SessionExecuteResponse>;
|
|
67
67
|
/** A command's output so far, or with callbacks, its output from the start
|
|
68
68
|
* as it happens until it ends. */
|
|
69
69
|
getSessionCommandLogs(sessionId: string, commandId: string): Promise<SessionCommandLogsResponse>;
|
package/dist/daytona/process.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { HOME, LONGEST_MS, quote, resolvePath, WHOLE_OUTPUT, } from "./context.js";
|
|
3
|
-
import { DaytonaCommandAlreadyCompletedError, DaytonaConflictError, DaytonaNotFoundError, DaytonaProcessExecutionTimeoutError, DaytonaSessionEndedError, guard, NotSupportedError, } from "./errors.js";
|
|
3
|
+
import { DaytonaCommandAlreadyCompletedError, DaytonaConflictError, DaytonaConnectionError, DaytonaNotFoundError, DaytonaProcessExecutionTimeoutError, DaytonaSessionEndedError, guard, NotSupportedError, } from "./errors.js";
|
|
4
4
|
export class CodeRunParams {
|
|
5
5
|
argv;
|
|
6
6
|
env;
|
|
@@ -11,7 +11,7 @@ const MARK = "\x1e";
|
|
|
11
11
|
The command travels base64-encoded and runs through eval, keeping the
|
|
12
12
|
shell's directory, variables and functions from one command to the next;
|
|
13
13
|
the markers say where its output ends and what it exited with. */
|
|
14
|
-
const PRELUDE = '
|
|
14
|
+
const PRELUDE = '__rt_eval() { eval "$(printf %s "$1" | base64 -d)"; }; __rt_run() { __rt_eval "$1"; __rt_c=$?; ' +
|
|
15
15
|
'printf \'\\036RT%s:%s\\036\' "$2" "$__rt_c"; printf \'\\036RT%s\\036\' "$2" >&2; return $__rt_c; }\n';
|
|
16
16
|
const TAG = "daytona-session:";
|
|
17
17
|
class SessionCommand {
|
|
@@ -78,6 +78,8 @@ export class Process {
|
|
|
78
78
|
timeoutMs,
|
|
79
79
|
...WHOLE_OUTPUT,
|
|
80
80
|
}));
|
|
81
|
+
if (result.stdoutTruncated || result.stderrTruncated)
|
|
82
|
+
throw new DaytonaConnectionError("Some command output is no longer available.", 502, undefined, "output_truncated");
|
|
81
83
|
if (result.timedOut)
|
|
82
84
|
throw new DaytonaProcessExecutionTimeoutError(`Command timed out after ${timeout} s. Pass a larger timeout, or 0 for none.`, 408);
|
|
83
85
|
const output = result.stdout + result.stderr;
|
|
@@ -100,6 +102,8 @@ export class Process {
|
|
|
100
102
|
timeoutMs,
|
|
101
103
|
...WHOLE_OUTPUT,
|
|
102
104
|
}));
|
|
105
|
+
if (result.stdoutTruncated || result.stderrTruncated)
|
|
106
|
+
throw new DaytonaConnectionError("Some command output is no longer available.", 502, undefined, "output_truncated");
|
|
103
107
|
if (result.timedOut)
|
|
104
108
|
throw new DaytonaProcessExecutionTimeoutError(`Code run timed out after ${timeout} s.`, 408);
|
|
105
109
|
return {
|
|
@@ -162,7 +166,7 @@ export class Process {
|
|
|
162
166
|
}
|
|
163
167
|
/** Runs a command in the session. It waits and returns the output and exit
|
|
164
168
|
* code, or with `runAsync: true` returns the cmdId at once. */
|
|
165
|
-
async executeSessionCommand(sessionId, req,
|
|
169
|
+
async executeSessionCommand(sessionId, req, timeout) {
|
|
166
170
|
const session = await this.#session(sessionId);
|
|
167
171
|
await this.#ctx.ensureHome(req.command);
|
|
168
172
|
await this.#ctx.live();
|
|
@@ -192,7 +196,22 @@ export class Process {
|
|
|
192
196
|
command.done.catch(() => undefined);
|
|
193
197
|
return { cmdId: command.id };
|
|
194
198
|
}
|
|
195
|
-
|
|
199
|
+
if (timeout && timeout > 0) {
|
|
200
|
+
let timer;
|
|
201
|
+
try {
|
|
202
|
+
await Promise.race([
|
|
203
|
+
command.done,
|
|
204
|
+
new Promise((_, reject) => {
|
|
205
|
+
timer = setTimeout(() => reject(new DaytonaProcessExecutionTimeoutError(`Waiting for session command timed out after ${timeout} s. The command continues in the session.`, 408)), timeout * 1000);
|
|
206
|
+
}),
|
|
207
|
+
]);
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else
|
|
214
|
+
await command.done;
|
|
196
215
|
return {
|
|
197
216
|
cmdId: command.id,
|
|
198
217
|
output: command.output,
|
|
@@ -216,6 +235,10 @@ export class Process {
|
|
|
216
235
|
};
|
|
217
236
|
await guard("process", async () => {
|
|
218
237
|
for await (const event of session.process.output({ cursor: session.cursor })) {
|
|
238
|
+
if (event.type === "truncated") {
|
|
239
|
+
session.ended = true;
|
|
240
|
+
throw new DaytonaConnectionError("Session command output was lost.", 502, undefined, "output_truncated");
|
|
241
|
+
}
|
|
219
242
|
if (event.type === "exit") {
|
|
220
243
|
session.ended = true;
|
|
221
244
|
flush("stdout", buffer.stdout.length);
|
|
@@ -162,7 +162,7 @@ export declare class Sandbox {
|
|
|
162
162
|
createSnapshot(name: string, _timeout?: number): Promise<void>;
|
|
163
163
|
_experimental_createSnapshot(name: string, timeout?: number): Promise<void>;
|
|
164
164
|
get computerUse(): never;
|
|
165
|
-
setLabels(): Promise<
|
|
165
|
+
setLabels(labels: Record<string, string>): Promise<Record<string, string>>;
|
|
166
166
|
recover(): Promise<never>;
|
|
167
167
|
resize(): Promise<never>;
|
|
168
168
|
waitForResizeComplete(): Promise<never>;
|
package/dist/daytona/sandbox.js
CHANGED
|
@@ -22,11 +22,18 @@ export class CodeInterpreter {
|
|
|
22
22
|
const env = this.#env();
|
|
23
23
|
if (!Object.keys(env).length)
|
|
24
24
|
return "python";
|
|
25
|
-
this.#envContext
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
if (!this.#envContext) {
|
|
26
|
+
const pending = (async () => {
|
|
27
|
+
const runtime = await this.#sandbox();
|
|
28
|
+
const made = await guard("sandbox", () => runtime.interpreter.contexts.create({ language: "python", cwd: HOME, env }));
|
|
29
|
+
return made.id;
|
|
30
|
+
})();
|
|
31
|
+
this.#envContext = pending;
|
|
32
|
+
pending.catch(() => {
|
|
33
|
+
if (this.#envContext === pending)
|
|
34
|
+
this.#envContext = undefined;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
30
37
|
return this.#envContext;
|
|
31
38
|
}
|
|
32
39
|
async runCode(code, options = {}) {
|
|
@@ -373,8 +380,10 @@ export class Sandbox {
|
|
|
373
380
|
get computerUse() {
|
|
374
381
|
throw new NotSupportedError("Daytona's computer use", "Use Runtime's desktop: `await sandbox.withruntime.desktop.start()` (see the desktop guide).");
|
|
375
382
|
}
|
|
376
|
-
setLabels() {
|
|
377
|
-
|
|
383
|
+
async setLabels(labels) {
|
|
384
|
+
const internal = Object.fromEntries(Object.entries(this.#rt.info.labels).filter(([key]) => key === "code-toolbox-language" || key.startsWith("compat.")));
|
|
385
|
+
await guard("sandbox", () => this.#rt.update({ labels: { ...labels, ...internal } }));
|
|
386
|
+
return this.labels;
|
|
378
387
|
}
|
|
379
388
|
recover() {
|
|
380
389
|
return refusal("Recovering a failed sandbox", "Create a new sandbox, or start one from a snapshot.");
|
package/dist/e2b/client.js
CHANGED
|
@@ -60,7 +60,7 @@ export function resetClients() {
|
|
|
60
60
|
/** Request options for a Runtime call from E2B's. */
|
|
61
61
|
export function request(opts = {}) {
|
|
62
62
|
return {
|
|
63
|
-
...(opts.requestTimeoutMs ? { timeoutMs: opts.requestTimeoutMs }
|
|
63
|
+
...(opts.requestTimeoutMs === undefined ? {} : { timeoutMs: opts.requestTimeoutMs }),
|
|
64
64
|
...(opts.signal ? { signal: opts.signal } : {}),
|
|
65
65
|
};
|
|
66
66
|
}
|