withruntime 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/E2B.md +32 -32
- package/README.md +3 -3
- package/dist/ai/index.d.ts +21 -5
- package/dist/ai/index.js +14 -21
- package/dist/ai-harness/index.js +15 -2
- package/dist/billing-cli.d.ts +21 -0
- package/dist/billing-cli.js +158 -0
- package/dist/cli-extras.d.ts +15 -0
- package/dist/cli-extras.js +60 -4
- package/dist/cli-name.d.ts +6 -0
- package/dist/cli-name.js +8 -0
- package/dist/cli.d.ts +17 -1
- package/dist/cli.js +543 -132
- package/dist/client.d.ts +35 -6
- package/dist/client.js +23 -0
- package/dist/credentials.js +1 -2
- package/dist/daytona/filesystem.d.ts +2 -0
- package/dist/daytona/filesystem.js +9 -7
- package/dist/e2b/commands.js +11 -2
- package/dist/e2b/errors.d.ts +5 -0
- package/dist/e2b/errors.js +3 -0
- package/dist/e2b/filesystem.js +3 -2
- package/dist/e2b/sandbox.d.ts +9 -6
- package/dist/e2b/sandbox.js +31 -7
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +7 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +1 -0
- package/dist/keys.js +1 -1
- package/dist/login.d.ts +2 -6
- package/dist/login.js +3 -8
- package/dist/network-cli.d.ts +1 -1
- package/dist/network-cli.js +64 -2
- package/dist/products/billing.d.ts +35 -0
- package/dist/products/billing.js +13 -0
- package/dist/products/index.d.ts +4 -1
- package/dist/products/index.js +4 -1
- package/dist/products/network-products.d.ts +45 -0
- package/dist/products/network-products.js +27 -0
- package/dist/products/previews.d.ts +1 -2
- package/dist/products/secrets.d.ts +14 -0
- package/dist/sandbox.d.ts +17 -3
- package/dist/sandbox.js +233 -62
- package/dist/transport.d.ts +17 -1
- package/dist/transport.js +124 -7
- package/dist/types.d.ts +0 -2
- package/dist/wallet.d.ts +30 -0
- package/dist/wallet.js +48 -0
- package/package.json +14 -3
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RequestOptions, Transport } from "../transport.js";
|
|
2
|
+
/** Where to pay for a stablecoin top-up, and whether the payment arrived. */
|
|
3
|
+
export type Topup = {
|
|
4
|
+
/** Poll this until `status` is `paid`. */
|
|
5
|
+
purchaseId: string;
|
|
6
|
+
/** open: waiting for your payment. pending: Stripe saw it and is confirming.
|
|
7
|
+
* paid: the credit is on the account. expired: nothing arrived in time. */
|
|
8
|
+
status: "open" | "pending" | "paid" | "failed" | "expired";
|
|
9
|
+
/** The credit this buys, in dollars, such as "20.00". */
|
|
10
|
+
amountUsd: string;
|
|
11
|
+
/** The exact token amount to send, to six decimals, such as "20.000000".
|
|
12
|
+
* A different amount, token or network cannot be matched or returned
|
|
13
|
+
* automatically. */
|
|
14
|
+
amount: string;
|
|
15
|
+
/** Send before this. */
|
|
16
|
+
payBy: string | null;
|
|
17
|
+
/** Pay on any one of these, with one of its tokens. */
|
|
18
|
+
networks: {
|
|
19
|
+
network: string;
|
|
20
|
+
address: string;
|
|
21
|
+
tokens: {
|
|
22
|
+
currency: string;
|
|
23
|
+
contract: string;
|
|
24
|
+
}[];
|
|
25
|
+
}[];
|
|
26
|
+
};
|
|
27
|
+
export declare function billing(t: Transport): {
|
|
28
|
+
/** Open a stablecoin top-up of `usd` dollars ($10 to $10,000). Pass the
|
|
29
|
+
* same `idempotencyKey` when retrying to get the same top-up back. */
|
|
30
|
+
topup: (input: {
|
|
31
|
+
usd: number;
|
|
32
|
+
}, options?: RequestOptions) => Promise<Topup>;
|
|
33
|
+
/** Read a top-up: whether its payment has arrived. */
|
|
34
|
+
topupStatus: (purchaseId: string, options?: RequestOptions) => Promise<Topup>;
|
|
35
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function billing(t) {
|
|
2
|
+
return {
|
|
3
|
+
/** Open a stablecoin top-up of `usd` dollars ($10 to $10,000). Pass the
|
|
4
|
+
* same `idempotencyKey` when retrying to get the same top-up back. */
|
|
5
|
+
topup: (input, options) => t.json({ method: "POST", path: "/v1/billing/topups", body: input, ...options }),
|
|
6
|
+
/** Read a top-up: whether its payment has arrived. */
|
|
7
|
+
topupStatus: (purchaseId, options) => t.json({
|
|
8
|
+
method: "GET",
|
|
9
|
+
path: `/v1/billing/topups/${encodeURIComponent(purchaseId)}`,
|
|
10
|
+
...options,
|
|
11
|
+
}),
|
|
12
|
+
};
|
|
13
|
+
}
|
package/dist/products/index.d.ts
CHANGED
|
@@ -4,9 +4,10 @@ import { images } from "./images.js";
|
|
|
4
4
|
import { sandboxDesktop } from "./desktop.js";
|
|
5
5
|
import { sandboxInterpreter } from "./interpreter.js";
|
|
6
6
|
import { sandboxNetwork } from "./network.js";
|
|
7
|
-
import { addresses, domains, ports, tunnel } from "./network-products.js";
|
|
7
|
+
import { addresses, domains, network, ports, tunnel } from "./network-products.js";
|
|
8
8
|
import { sandboxPreviews } from "./previews.js";
|
|
9
9
|
import { referrals } from "./referrals.js";
|
|
10
|
+
import { billing } from "./billing.js";
|
|
10
11
|
import { audit } from "./audit.js";
|
|
11
12
|
import { sso } from "./sso.js";
|
|
12
13
|
import { switching } from "./switching.js";
|
|
@@ -20,6 +21,7 @@ export declare const clientExtensions: {
|
|
|
20
21
|
images: typeof images;
|
|
21
22
|
volumes: typeof volumes;
|
|
22
23
|
referrals: typeof referrals;
|
|
24
|
+
billing: typeof billing;
|
|
23
25
|
limits: typeof limits;
|
|
24
26
|
switching: typeof switching;
|
|
25
27
|
secrets: typeof secrets;
|
|
@@ -33,6 +35,7 @@ export declare const clientExtensions: {
|
|
|
33
35
|
ports: typeof ports;
|
|
34
36
|
addresses: typeof addresses;
|
|
35
37
|
tunnel: typeof tunnel;
|
|
38
|
+
network: typeof network;
|
|
36
39
|
};
|
|
37
40
|
export declare const sandboxExtensions: {
|
|
38
41
|
interpreter: typeof sandboxInterpreter;
|
package/dist/products/index.js
CHANGED
|
@@ -2,9 +2,10 @@ import { images } from "./images.js";
|
|
|
2
2
|
import { sandboxDesktop } from "./desktop.js";
|
|
3
3
|
import { sandboxInterpreter } from "./interpreter.js";
|
|
4
4
|
import { sandboxNetwork } from "./network.js";
|
|
5
|
-
import { addresses, domains, ports, tunnel } from "./network-products.js";
|
|
5
|
+
import { addresses, domains, network, ports, tunnel } from "./network-products.js";
|
|
6
6
|
import { sandboxPreviews } from "./previews.js";
|
|
7
7
|
import { referrals } from "./referrals.js";
|
|
8
|
+
import { billing } from "./billing.js";
|
|
8
9
|
import { audit } from "./audit.js";
|
|
9
10
|
import { sso } from "./sso.js";
|
|
10
11
|
import { switching } from "./switching.js";
|
|
@@ -33,6 +34,7 @@ export const clientExtensions = {
|
|
|
33
34
|
images,
|
|
34
35
|
volumes,
|
|
35
36
|
referrals,
|
|
37
|
+
billing,
|
|
36
38
|
limits,
|
|
37
39
|
switching,
|
|
38
40
|
secrets,
|
|
@@ -46,6 +48,7 @@ export const clientExtensions = {
|
|
|
46
48
|
ports,
|
|
47
49
|
addresses,
|
|
48
50
|
tunnel,
|
|
51
|
+
network,
|
|
49
52
|
};
|
|
50
53
|
export const sandboxExtensions = {
|
|
51
54
|
interpreter: sandboxInterpreter,
|
|
@@ -165,3 +165,48 @@ export declare function tunnel(t: Transport): {
|
|
|
165
165
|
}, options?: RequestOptions) => Promise<TunnelPeerCreated>;
|
|
166
166
|
removePeer: (peerId: string, options?: RequestOptions) => Promise<Tunnel>;
|
|
167
167
|
};
|
|
168
|
+
/** The proxy your sandboxes' outbound connections go through. */
|
|
169
|
+
export type UpstreamProxy = {
|
|
170
|
+
/** http://host:port or https://host:port. */
|
|
171
|
+
url: string;
|
|
172
|
+
/** The secret sent as Proxy-Authorization. */
|
|
173
|
+
secret?: string;
|
|
174
|
+
/** The destinations that go through it; every destination when absent. */
|
|
175
|
+
hosts?: string[];
|
|
176
|
+
updatedAt: string;
|
|
177
|
+
};
|
|
178
|
+
export type SetUpstreamProxy = {
|
|
179
|
+
/** http://host:port or https://host:port. An https proxy's certificate must
|
|
180
|
+
* verify against the public roots. */
|
|
181
|
+
url: string;
|
|
182
|
+
/** One of your secrets, sent as the Proxy-Authorization header's whole
|
|
183
|
+
* value (such as `Basic dXNlcjpwYXNz`). Its hosts must name the proxy's host. */
|
|
184
|
+
secret?: string;
|
|
185
|
+
/** Only connections to these hosts (or `*.domain`) go through the proxy;
|
|
186
|
+
* every connection when absent. 1 to 16. */
|
|
187
|
+
hosts?: string[];
|
|
188
|
+
};
|
|
189
|
+
/** `runtime.network`: the network settings of the whole account.
|
|
190
|
+
*
|
|
191
|
+
* await runtime.network.upstreamProxy.set({ url: "http://proxy.example.com:3128" });
|
|
192
|
+
*
|
|
193
|
+
* `upstreamProxy`: after Runtime's own rules allow a connection, the host
|
|
194
|
+
* reaches it with CONNECT through your proxy, so your proxy's controls and
|
|
195
|
+
* logs apply. A proxy that refuses or cannot be reached fails the connection
|
|
196
|
+
* (upstream-proxy-failed); nothing goes around it. Paid accounts only. */
|
|
197
|
+
export declare function network(t: Transport): {
|
|
198
|
+
upstreamProxy: {
|
|
199
|
+
/** Send outbound connections through your proxy, replacing any set before. */
|
|
200
|
+
set: (input: SetUpstreamProxy, options?: RequestOptions) => Promise<UpstreamProxy & {
|
|
201
|
+
enforced: boolean;
|
|
202
|
+
}>;
|
|
203
|
+
/** The proxy in use. Throws `not_found` (404) when none is set. */
|
|
204
|
+
get: (options?: RequestOptions) => Promise<UpstreamProxy>;
|
|
205
|
+
/** Connections leave directly again, within seconds. Throws `not_found`
|
|
206
|
+
* (404) when none is set. */
|
|
207
|
+
remove: (options?: RequestOptions) => Promise<{
|
|
208
|
+
deleted: true;
|
|
209
|
+
enforced: boolean;
|
|
210
|
+
}>;
|
|
211
|
+
};
|
|
212
|
+
};
|
|
@@ -80,3 +80,30 @@ export function tunnel(t) {
|
|
|
80
80
|
removePeer: (peerId, options) => t.json({ method: "DELETE", path: `/v1/tunnel/peers/${enc(peerId)}`, ...options }),
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
+
/** `runtime.network`: the network settings of the whole account.
|
|
84
|
+
*
|
|
85
|
+
* await runtime.network.upstreamProxy.set({ url: "http://proxy.example.com:3128" });
|
|
86
|
+
*
|
|
87
|
+
* `upstreamProxy`: after Runtime's own rules allow a connection, the host
|
|
88
|
+
* reaches it with CONNECT through your proxy, so your proxy's controls and
|
|
89
|
+
* logs apply. A proxy that refuses or cannot be reached fails the connection
|
|
90
|
+
* (upstream-proxy-failed); nothing goes around it. Paid accounts only. */
|
|
91
|
+
export function network(t) {
|
|
92
|
+
const path = "/v1/network/upstream-proxy";
|
|
93
|
+
return {
|
|
94
|
+
upstreamProxy: {
|
|
95
|
+
/** Send outbound connections through your proxy, replacing any set before. */
|
|
96
|
+
set: (input, options) => t.json({
|
|
97
|
+
method: "PUT",
|
|
98
|
+
path,
|
|
99
|
+
body: input,
|
|
100
|
+
...options,
|
|
101
|
+
}),
|
|
102
|
+
/** The proxy in use. Throws `not_found` (404) when none is set. */
|
|
103
|
+
get: (options) => t.json({ method: "GET", path, ...options }),
|
|
104
|
+
/** Connections leave directly again, within seconds. Throws `not_found`
|
|
105
|
+
* (404) when none is set. */
|
|
106
|
+
remove: (options) => t.json({ method: "DELETE", path, ...options }),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -18,8 +18,7 @@ export type Preview = {
|
|
|
18
18
|
/** Turned off by Runtime after a report. */
|
|
19
19
|
disabled: boolean;
|
|
20
20
|
createdAt: string;
|
|
21
|
-
/** On create only: how to share the address,
|
|
22
|
-
* pays you both when someone you share it with signs up. */
|
|
21
|
+
/** On create only: how to share the address, in one line. */
|
|
23
22
|
hint?: string;
|
|
24
23
|
};
|
|
25
24
|
export type CreatePreview = {
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { RequestOptions, Transport } from "../transport.js";
|
|
2
|
+
/** Which requests to a secret's hosts carry it: `methods` (every method when
|
|
3
|
+
* absent) and `paths`, each exact (`/v1/chat/completions`) or a prefix ending
|
|
4
|
+
* in `/*` (`/repos/acme/*`), written canonically: starting with `/`, no `.` or
|
|
5
|
+
* `..` segments, no `;` or `\`, no encoded slash. A request whose path could be
|
|
6
|
+
* read two ways gets no secret that has rules. Paid accounts only. */
|
|
7
|
+
export type SecretRule = {
|
|
8
|
+
methods?: string[];
|
|
9
|
+
paths: string[];
|
|
10
|
+
};
|
|
2
11
|
/** A secret your sandboxes use without seeing it. Every sandbox of your
|
|
3
12
|
* organization has an environment variable of the secret's name holding
|
|
4
13
|
* `placeholder`; the egress proxy puts the value into HTTPS requests to
|
|
@@ -9,6 +18,7 @@ export type Secret = {
|
|
|
9
18
|
hosts: string[];
|
|
10
19
|
header?: string;
|
|
11
20
|
format?: string;
|
|
21
|
+
rules?: SecretRule[];
|
|
12
22
|
placeholder: string;
|
|
13
23
|
valueBytes: number;
|
|
14
24
|
createdAt: string;
|
|
@@ -23,6 +33,10 @@ export type SetSecret = {
|
|
|
23
33
|
header?: string;
|
|
24
34
|
/** With `header`: its value, `{value}` where the secret goes. Default `{value}`. */
|
|
25
35
|
format?: string;
|
|
36
|
+
/** Only requests a rule allows carry the value. Paid accounts only. 1 to 16.
|
|
37
|
+
* Absent: every request to the hosts; replacing a secret without rules
|
|
38
|
+
* clears them. */
|
|
39
|
+
rules?: SecretRule[];
|
|
26
40
|
};
|
|
27
41
|
export declare function secrets(t: Transport): {
|
|
28
42
|
/** Store or replace a secret. Replacing keeps its placeholder. */
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -30,7 +30,9 @@ export declare class Sandbox implements AsyncDisposable {
|
|
|
30
30
|
* far), never an error; `check: true` throws CommandError on a non-zero exit. */
|
|
31
31
|
exec(command: string | readonly string[], options?: ExecOptions): Promise<CommandResult>;
|
|
32
32
|
/** The command's events as they happen: start, stdout, stderr, exit. Resumes
|
|
33
|
-
* by itself when the server ends a long stream
|
|
33
|
+
* by itself when the server ends a long stream or the connection drops after
|
|
34
|
+
* the command started, and yields `truncated` for any output it did not
|
|
35
|
+
* receive, so lost output never passes as whole. */
|
|
34
36
|
execStream(command: string | readonly string[], options?: ExecOptions): AsyncGenerator<OutputEvent>;
|
|
35
37
|
/** Starts a background process (a server, a watcher, a REPL) and returns at
|
|
36
38
|
* once. `stdin: "pipe"` keeps its input open for write(); `pty` gives it a
|
|
@@ -202,8 +204,19 @@ export declare class Files {
|
|
|
202
204
|
stopped: boolean;
|
|
203
205
|
}>;
|
|
204
206
|
};
|
|
205
|
-
/** A file's bytes, any size
|
|
207
|
+
/** A file's bytes, any size, checked against the length (and for a small
|
|
208
|
+
* file the SHA-256) the API sends first: a read that arrives short is read
|
|
209
|
+
* again, then throws `download_incomplete`, never returns short. For a file
|
|
210
|
+
* too big to hold in memory use `readStream` or `download`. */
|
|
206
211
|
read(path: string, options?: RequestOptions): Promise<Uint8Array>;
|
|
212
|
+
/** A file's bytes as they arrive, for files of any size without holding
|
|
213
|
+
* them in memory (Daytona's `downloadFileStream`). The stream errors with
|
|
214
|
+
* `download_incomplete` if it ends short of the file's length.
|
|
215
|
+
*
|
|
216
|
+
* const stream = await sbx.files.readStream("/workspace/big.tar");
|
|
217
|
+
* await stream.pipeTo(Writable.toWeb(fs.createWriteStream("big.tar")));
|
|
218
|
+
*/
|
|
219
|
+
readStream(path: string, options?: RequestOptions): Promise<ReadableStream<Uint8Array>>;
|
|
207
220
|
readText(path: string, options?: RequestOptions): Promise<string>;
|
|
208
221
|
/** Writes a file of any size, atomically, making parent directories. Large
|
|
209
222
|
* ones go in parallel 1 MiB chunks checked against their SHA-256. `mode`
|
|
@@ -241,7 +254,8 @@ export declare class Files {
|
|
|
241
254
|
/** Copies a local file or directory into the sandbox. A directory travels as
|
|
242
255
|
* one gzipped tar and is unpacked in place. */
|
|
243
256
|
upload(localPath: string, remotePath: string): Promise<void>;
|
|
244
|
-
/** Copies a file or directory out of the sandbox.
|
|
257
|
+
/** Copies a file or directory out of the sandbox. A file streams to disk,
|
|
258
|
+
* any size, checked against its length. */
|
|
245
259
|
download(remotePath: string, localPath: string): Promise<void>;
|
|
246
260
|
}
|
|
247
261
|
export declare function sandboxPage(t: Transport, body: {
|
package/dist/sandbox.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CommandError, RuntimeError } from "./errors.js";
|
|
1
|
+
import { CommandError, ConnectionError, NotFoundError, RuntimeError } from "./errors.js";
|
|
2
2
|
import { Page } from "./page.js";
|
|
3
3
|
import { sandboxWatches, watchDirectory, } from "./products/watch.js";
|
|
4
4
|
import { sandboxFactories } from "./products/index.js";
|
|
@@ -83,17 +83,22 @@ export class Sandbox {
|
|
|
83
83
|
let exit;
|
|
84
84
|
let processId;
|
|
85
85
|
let dropped = false;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
try {
|
|
87
|
+
for await (const event of this.execStream(command, options)) {
|
|
88
|
+
if (event.type === "start")
|
|
89
|
+
processId = event.processId;
|
|
90
|
+
else if (event.type === "stdout" || event.type === "stderr") {
|
|
91
|
+
out[event.type] += event.data;
|
|
92
|
+
(event.type === "stdout" ? options.onStdout : options.onStderr)?.(event.data);
|
|
93
|
+
}
|
|
94
|
+
else if (event.type === "truncated")
|
|
95
|
+
dropped = true;
|
|
96
|
+
else if (event.type === "exit")
|
|
97
|
+
exit = event;
|
|
92
98
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
exit = event;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw await this.#unfinished(error, processId, options.signal);
|
|
97
102
|
}
|
|
98
103
|
result = {
|
|
99
104
|
exitCode: exit?.exitCode ?? null,
|
|
@@ -108,11 +113,49 @@ export class Sandbox {
|
|
|
108
113
|
throw new CommandError(result);
|
|
109
114
|
return result;
|
|
110
115
|
}
|
|
116
|
+
/** An exec that ended before its command did. Cancelled through `signal`,
|
|
117
|
+
* its command is sent SIGTERM, as Ctrl-C would stop it at a terminal; cut
|
|
118
|
+
* off, it is left running. Either way the error names the process, so the
|
|
119
|
+
* caller can read or stop what is still there. */
|
|
120
|
+
async #unfinished(error, processId, signal) {
|
|
121
|
+
if (!processId)
|
|
122
|
+
return error;
|
|
123
|
+
const id = `${this.id} ${processId}`;
|
|
124
|
+
if (signal?.aborted) {
|
|
125
|
+
const stopped = await new Process(this.#t, this.id, { id: processId })
|
|
126
|
+
.kill("SIGTERM")
|
|
127
|
+
.then(() => true, () => false);
|
|
128
|
+
return new RuntimeError({
|
|
129
|
+
message: stopped
|
|
130
|
+
? `Cancelled; process ${processId} was sent SIGTERM.`
|
|
131
|
+
: `Cancelled, but process ${processId} may still be running: stopping it failed.`,
|
|
132
|
+
code: "cancelled",
|
|
133
|
+
status: 0,
|
|
134
|
+
hint: stopped
|
|
135
|
+
? `See what it printed: runtime sandbox logs ${id}`
|
|
136
|
+
: `Stop it: runtime sandbox kill ${id}, or sandbox.processes.get("${processId}") and kill().`,
|
|
137
|
+
details: { sandboxId: this.id, processId, stopped },
|
|
138
|
+
cause: error,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (error instanceof RuntimeError && !(error instanceof ConnectionError))
|
|
142
|
+
return error;
|
|
143
|
+
return new ConnectionError({
|
|
144
|
+
message: `Lost the output of process ${processId}; the command may still be running.`,
|
|
145
|
+
code: "connection_error",
|
|
146
|
+
status: 0,
|
|
147
|
+
hint: `Follow it: runtime sandbox logs ${id} -f (sandbox.processes.get("${processId}")), or stop it: runtime sandbox kill ${id}.`,
|
|
148
|
+
details: { sandboxId: this.id, processId },
|
|
149
|
+
cause: error,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
111
152
|
/** The command's events as they happen: start, stdout, stderr, exit. Resumes
|
|
112
|
-
* by itself when the server ends a long stream
|
|
153
|
+
* by itself when the server ends a long stream or the connection drops after
|
|
154
|
+
* the command started, and yields `truncated` for any output it did not
|
|
155
|
+
* receive, so lost output never passes as whole. */
|
|
113
156
|
async *execStream(command, options = {}) {
|
|
114
157
|
let processId;
|
|
115
|
-
|
|
158
|
+
const read = new OutputCursor();
|
|
116
159
|
const events = this.#t.events({
|
|
117
160
|
method: "POST",
|
|
118
161
|
path: `/v1/sandboxes/${enc(this.id)}:exec`,
|
|
@@ -120,26 +163,36 @@ export class Sandbox {
|
|
|
120
163
|
...pick(options),
|
|
121
164
|
timeoutMs: options.timeoutMs === undefined ? 86_400_000 : options.timeoutMs + 60_000,
|
|
122
165
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
try {
|
|
167
|
+
for await (const event of events) {
|
|
168
|
+
if (event.type === "start")
|
|
169
|
+
processId = event.processId;
|
|
170
|
+
if (event.type === "continue") {
|
|
171
|
+
yield* this.processes.follow(event.processId, {
|
|
172
|
+
cursor: event.cursor,
|
|
173
|
+
signal: options.signal,
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
yield* read.pass(event);
|
|
178
|
+
if (event.type === "exit")
|
|
179
|
+
return;
|
|
132
180
|
}
|
|
133
|
-
if (event.type === "error")
|
|
134
|
-
throw new RuntimeError({
|
|
135
|
-
message: event.error.message,
|
|
136
|
-
code: event.error.code,
|
|
137
|
-
status: 0,
|
|
138
|
-
...(event.error.requestId ? { requestId: event.error.requestId } : {}),
|
|
139
|
-
});
|
|
140
|
-
yield event;
|
|
141
181
|
}
|
|
142
|
-
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (options.signal?.aborted || !processId || !cutOff(error))
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
// The connection closed after the command started: its output is kept on
|
|
187
|
+
// the sandbox, so follow it from where this stream stopped.
|
|
188
|
+
if (!processId)
|
|
189
|
+
throw new ConnectionError({
|
|
190
|
+
message: "The exec stream closed before the command started.",
|
|
191
|
+
code: "connection_error",
|
|
192
|
+
status: 0,
|
|
193
|
+
hint: "Run it again; to be sure it runs once, pass the same idempotencyKey.",
|
|
194
|
+
});
|
|
195
|
+
yield* this.processes.follow(processId, { cursor: read.cursor, signal: options.signal });
|
|
143
196
|
}
|
|
144
197
|
/** Starts a background process (a server, a watcher, a REPL) and returns at
|
|
145
198
|
* once. `stdin: "pipe"` keeps its input open for write(); `pty` gives it a
|
|
@@ -207,7 +260,19 @@ export class Sandbox {
|
|
|
207
260
|
/** Carries on a paused sandbox; `timeoutSeconds` is its new lease. */
|
|
208
261
|
async wake(options = {}) {
|
|
209
262
|
const { timeoutSeconds, ...rest } = options;
|
|
210
|
-
|
|
263
|
+
try {
|
|
264
|
+
return await this.#lifecycle("wake", rest, timeoutSeconds === undefined ? {} : { timeoutSeconds });
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
// Waking one that is already awake is done, not a mistake. With a new
|
|
268
|
+
// lease asked for, the refusal stands: it was not given.
|
|
269
|
+
if (!(error instanceof RuntimeError) || error.code !== "not_paused" || timeoutSeconds)
|
|
270
|
+
throw error;
|
|
271
|
+
await this.refresh(pick(rest));
|
|
272
|
+
if (this.state !== "running" && this.state !== "starting")
|
|
273
|
+
throw error;
|
|
274
|
+
return this;
|
|
275
|
+
}
|
|
211
276
|
}
|
|
212
277
|
/** More time before the lease ends (at most an hour ahead of now). */
|
|
213
278
|
async extend(seconds, options = {}) {
|
|
@@ -343,6 +408,42 @@ export class Sandbox {
|
|
|
343
408
|
return this.#info;
|
|
344
409
|
}
|
|
345
410
|
}
|
|
411
|
+
/** Where a reader of a process's output is, in bytes of stdout and stderr
|
|
412
|
+
* together. Output it did not receive, named by the server or not, becomes a
|
|
413
|
+
* `truncated` event: the next chunk starting past the cursor means the bytes
|
|
414
|
+
* between were lost. A server error event becomes a thrown error. */
|
|
415
|
+
class OutputCursor {
|
|
416
|
+
cursor;
|
|
417
|
+
constructor(cursor = 0) {
|
|
418
|
+
this.cursor = cursor;
|
|
419
|
+
}
|
|
420
|
+
*pass(event) {
|
|
421
|
+
if (event.type === "error")
|
|
422
|
+
throw new RuntimeError({
|
|
423
|
+
message: event.error.message,
|
|
424
|
+
code: event.error.code,
|
|
425
|
+
status: 0,
|
|
426
|
+
...(event.error.requestId ? { requestId: event.error.requestId } : {}),
|
|
427
|
+
});
|
|
428
|
+
if (event.type === "truncated")
|
|
429
|
+
this.cursor = Math.max(this.cursor, event.resumeAt);
|
|
430
|
+
if (event.type === "stdout" || event.type === "stderr") {
|
|
431
|
+
if (event.offset > this.cursor)
|
|
432
|
+
yield {
|
|
433
|
+
type: "truncated",
|
|
434
|
+
droppedBytes: event.offset - this.cursor,
|
|
435
|
+
resumeAt: event.offset,
|
|
436
|
+
};
|
|
437
|
+
this.cursor = Math.max(this.cursor, event.offset + Buffer.byteLength(event.data));
|
|
438
|
+
}
|
|
439
|
+
yield event;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/** A stream cut off by the network rather than refused by Runtime: worth
|
|
443
|
+
* reconnecting to from the cursor. */
|
|
444
|
+
function cutOff(error) {
|
|
445
|
+
return !(error instanceof RuntimeError) || error instanceof ConnectionError;
|
|
446
|
+
}
|
|
346
447
|
/** A streamed command keeps all its output unless the reader fell more than the
|
|
347
448
|
* process's output buffer behind, which the server marks with a "truncated"
|
|
348
449
|
* event. That event does not say which stream lost bytes, so both flags carry
|
|
@@ -379,35 +480,44 @@ export class Processes {
|
|
|
379
480
|
}
|
|
380
481
|
/** Output events from `cursor` until the process exits, across stream slices. */
|
|
381
482
|
async *follow(processId, options = {}) {
|
|
382
|
-
|
|
483
|
+
const read = new OutputCursor(options.cursor ?? 0);
|
|
484
|
+
// Reconnections in a row that brought nothing: a stream that keeps
|
|
485
|
+
// closing without output or an exit is given up, not followed forever.
|
|
486
|
+
let idle = 0;
|
|
383
487
|
for (;;) {
|
|
488
|
+
const before = read.cursor;
|
|
384
489
|
let resumed = false;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
490
|
+
try {
|
|
491
|
+
for await (const event of this.t.events({
|
|
492
|
+
method: "GET",
|
|
493
|
+
path: `/v1/sandboxes/${enc(this.sandboxId)}/processes/${enc(processId)}/output`,
|
|
494
|
+
query: { cursor: read.cursor, follow: true },
|
|
495
|
+
timeoutMs: 180_000,
|
|
496
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
497
|
+
})) {
|
|
498
|
+
if (event.type === "continue") {
|
|
499
|
+
read.cursor = Math.max(read.cursor, event.cursor);
|
|
500
|
+
resumed = true;
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
yield* read.pass(event);
|
|
504
|
+
if (event.type === "exit")
|
|
505
|
+
return;
|
|
396
506
|
}
|
|
397
|
-
if (event.type === "stdout" || event.type === "stderr")
|
|
398
|
-
cursor = event.offset + Buffer.byteLength(event.data);
|
|
399
|
-
if (event.type === "error")
|
|
400
|
-
throw new RuntimeError({
|
|
401
|
-
message: event.error.message,
|
|
402
|
-
code: event.error.code,
|
|
403
|
-
status: 0,
|
|
404
|
-
});
|
|
405
|
-
yield event;
|
|
406
|
-
if (event.type === "exit")
|
|
407
|
-
return;
|
|
408
507
|
}
|
|
409
|
-
|
|
410
|
-
|
|
508
|
+
catch (error) {
|
|
509
|
+
if (options.signal?.aborted || !cutOff(error))
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
idle = resumed || read.cursor > before ? 0 : idle + 1;
|
|
513
|
+
if (idle > 3)
|
|
514
|
+
throw new ConnectionError({
|
|
515
|
+
message: `The output stream of process ${processId} keeps closing before it ends.`,
|
|
516
|
+
code: "connection_error",
|
|
517
|
+
status: 0,
|
|
518
|
+
hint: `Read what it printed so far: runtime sandbox logs ${this.sandboxId} ${processId}`,
|
|
519
|
+
details: { sandboxId: this.sandboxId, processId },
|
|
520
|
+
});
|
|
411
521
|
}
|
|
412
522
|
}
|
|
413
523
|
}
|
|
@@ -576,9 +686,28 @@ export class Files {
|
|
|
576
686
|
get watches() {
|
|
577
687
|
return sandboxWatches(this.t, this.sandboxId);
|
|
578
688
|
}
|
|
579
|
-
/** A file's bytes, any size
|
|
689
|
+
/** A file's bytes, any size, checked against the length (and for a small
|
|
690
|
+
* file the SHA-256) the API sends first: a read that arrives short is read
|
|
691
|
+
* again, then throws `download_incomplete`, never returns short. For a file
|
|
692
|
+
* too big to hold in memory use `readStream` or `download`. */
|
|
580
693
|
async read(path, options = {}) {
|
|
581
|
-
return this.t.
|
|
694
|
+
return this.t.fileBytes({
|
|
695
|
+
method: "GET",
|
|
696
|
+
path: this.#path("/files/content"),
|
|
697
|
+
query: { path },
|
|
698
|
+
accept: "application/octet-stream",
|
|
699
|
+
...options,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
/** A file's bytes as they arrive, for files of any size without holding
|
|
703
|
+
* them in memory (Daytona's `downloadFileStream`). The stream errors with
|
|
704
|
+
* `download_incomplete` if it ends short of the file's length.
|
|
705
|
+
*
|
|
706
|
+
* const stream = await sbx.files.readStream("/workspace/big.tar");
|
|
707
|
+
* await stream.pipeTo(Writable.toWeb(fs.createWriteStream("big.tar")));
|
|
708
|
+
*/
|
|
709
|
+
async readStream(path, options = {}) {
|
|
710
|
+
return this.t.fileStream({
|
|
582
711
|
method: "GET",
|
|
583
712
|
path: this.#path("/files/content"),
|
|
584
713
|
query: { path },
|
|
@@ -746,7 +875,16 @@ export class Files {
|
|
|
746
875
|
const info = await stat(localPath);
|
|
747
876
|
if (info.isFile()) {
|
|
748
877
|
// Its permissions travel with it: an uploaded script stays runnable.
|
|
749
|
-
|
|
878
|
+
const data = await readFile(localPath);
|
|
879
|
+
const mode = info.mode & 0o777;
|
|
880
|
+
await this.write(remotePath, data, { mode }).catch(async (error) => {
|
|
881
|
+
// A missing parent is made, as a directory upload makes its own.
|
|
882
|
+
const parent = remotePath.slice(0, remotePath.lastIndexOf("/"));
|
|
883
|
+
if (!(error instanceof NotFoundError) || !parent)
|
|
884
|
+
throw error;
|
|
885
|
+
await this.mkdir(parent, { parents: true });
|
|
886
|
+
await this.write(remotePath, data, { mode });
|
|
887
|
+
});
|
|
750
888
|
return;
|
|
751
889
|
}
|
|
752
890
|
const { packDirectory } = await import("./tar.js");
|
|
@@ -766,7 +904,40 @@ export class Files {
|
|
|
766
904
|
if (result.exitCode !== 0)
|
|
767
905
|
throw new CommandError(result);
|
|
768
906
|
}
|
|
769
|
-
/**
|
|
907
|
+
/** Streams a file to disk through a partial file beside the target, renamed
|
|
908
|
+
* into place only once every byte has arrived: a failed copy never leaves
|
|
909
|
+
* a short file under the name asked for. A short stream is tried again,
|
|
910
|
+
* twice, as a short `read` is. */
|
|
911
|
+
async #streamTo(remotePath, localPath) {
|
|
912
|
+
const fs = await import("node:fs/promises");
|
|
913
|
+
const partial = `${localPath}.runtime-partial-${crypto.randomUUID().slice(0, 8)}`;
|
|
914
|
+
for (let attempt = 0;; attempt++) {
|
|
915
|
+
try {
|
|
916
|
+
const stream = await this.readStream(remotePath);
|
|
917
|
+
// The handle is closed before anything else touches the file, so a
|
|
918
|
+
// failed copy's partial file is really gone when the error returns.
|
|
919
|
+
const file = await fs.open(partial, "w");
|
|
920
|
+
try {
|
|
921
|
+
for await (const chunk of stream)
|
|
922
|
+
await file.write(chunk);
|
|
923
|
+
}
|
|
924
|
+
finally {
|
|
925
|
+
await file.close();
|
|
926
|
+
}
|
|
927
|
+
await fs.rename(partial, localPath);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
await fs.rm(partial, { force: true });
|
|
932
|
+
if (attempt >= 2 ||
|
|
933
|
+
!(error instanceof RuntimeError) ||
|
|
934
|
+
error.code !== "download_incomplete")
|
|
935
|
+
throw error;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
/** Copies a file or directory out of the sandbox. A file streams to disk,
|
|
940
|
+
* any size, checked against its length. */
|
|
770
941
|
async download(remotePath, localPath) {
|
|
771
942
|
const fs = await import("node:fs/promises");
|
|
772
943
|
const paths = await import("node:path");
|
|
@@ -779,7 +950,7 @@ export class Files {
|
|
|
779
950
|
});
|
|
780
951
|
if (entry.type !== "directory") {
|
|
781
952
|
await fs.mkdir(paths.dirname(localPath), { recursive: true });
|
|
782
|
-
await
|
|
953
|
+
await this.#streamTo(remotePath, localPath);
|
|
783
954
|
return;
|
|
784
955
|
}
|
|
785
956
|
const staging = `/tmp/.runtime-download-${crypto.randomUUID()}.tar.gz`;
|