withruntime 0.3.1 → 0.5.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 +129 -0
- package/E2B.md +146 -0
- package/README.md +84 -12
- package/VERCEL.md +127 -0
- package/dist/ai/index.d.ts +33 -0
- package/dist/ai/index.js +38 -0
- package/dist/claude-agent-sdk/index.d.ts +7 -0
- package/dist/claude-agent-sdk/index.js +65 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +949 -105
- package/dist/client.d.ts +25 -2
- package/dist/client.js +14 -1
- package/dist/credentials.d.ts +10 -0
- package/dist/credentials.js +56 -1
- package/dist/daytona/client.d.ts +44 -0
- package/dist/daytona/client.js +40 -0
- package/dist/daytona/context.d.ts +26 -0
- package/dist/daytona/context.js +22 -0
- package/dist/daytona/daytona.d.ts +185 -0
- package/dist/daytona/daytona.js +416 -0
- package/dist/daytona/errors.d.ts +106 -0
- package/dist/daytona/errors.js +182 -0
- package/dist/daytona/filesystem.d.ts +84 -0
- package/dist/daytona/filesystem.js +206 -0
- package/dist/daytona/git.d.ts +33 -0
- package/dist/daytona/git.js +128 -0
- package/dist/daytona/image.d.ts +45 -0
- package/dist/daytona/image.js +140 -0
- package/dist/daytona/index.d.ts +50 -0
- package/dist/daytona/index.js +50 -0
- package/dist/daytona/process.d.ts +86 -0
- package/dist/daytona/process.js +339 -0
- package/dist/daytona/sandbox.d.ts +196 -0
- package/dist/daytona/sandbox.js +448 -0
- package/dist/e2b/client.d.ts +58 -0
- package/dist/e2b/client.js +66 -0
- package/dist/e2b/code-interpreter.d.ts +144 -0
- package/dist/e2b/code-interpreter.js +244 -0
- package/dist/e2b/commands.d.ts +100 -0
- package/dist/e2b/commands.js +268 -0
- package/dist/e2b/errors.d.ts +72 -0
- package/dist/e2b/errors.js +151 -0
- package/dist/e2b/filesystem.d.ts +82 -0
- package/dist/e2b/filesystem.js +143 -0
- package/dist/e2b/index.d.ts +28 -0
- package/dist/e2b/index.js +61 -0
- package/dist/e2b/sandbox.d.ts +203 -0
- package/dist/e2b/sandbox.js +490 -0
- package/dist/e2b/unsupported.d.ts +19 -0
- package/dist/e2b/unsupported.js +27 -0
- package/dist/errors.d.ts +10 -4
- package/dist/errors.js +26 -6
- package/dist/image-context.d.ts +39 -0
- package/dist/image-context.js +176 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.js +17 -2
- package/dist/keys.d.ts +43 -0
- package/dist/keys.js +196 -0
- package/dist/login.js +27 -6
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +4 -3
- package/dist/openai-agents/index.d.ts +132 -0
- package/dist/openai-agents/index.js +791 -0
- package/dist/products/audit.d.ts +48 -0
- package/dist/products/audit.js +10 -0
- package/dist/products/images.d.ts +113 -7
- package/dist/products/images.js +186 -27
- package/dist/products/index.d.ts +11 -0
- package/dist/products/index.js +17 -1
- package/dist/products/observability.d.ts +186 -0
- package/dist/products/observability.js +117 -0
- package/dist/products/secrets.d.ts +40 -0
- package/dist/products/secrets.js +24 -0
- package/dist/products/switching.d.ts +89 -0
- package/dist/products/switching.js +25 -0
- package/dist/proxy.d.ts +30 -0
- package/dist/proxy.js +178 -0
- package/dist/sandbox.d.ts +30 -2
- package/dist/sandbox.js +101 -1
- package/dist/ssh.d.ts +21 -0
- package/dist/ssh.js +312 -0
- package/dist/tar.d.ts +1 -0
- package/dist/tar.js +4 -4
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.js +180 -0
- package/dist/transport.d.ts +12 -2
- package/dist/transport.js +76 -16
- package/dist/tunnel.d.ts +67 -0
- package/dist/tunnel.js +311 -0
- package/dist/types.d.ts +57 -1
- package/dist/vercel/client.d.ts +40 -0
- package/dist/vercel/client.js +39 -0
- package/dist/vercel/command.d.ts +70 -0
- package/dist/vercel/command.js +148 -0
- package/dist/vercel/errors.d.ts +53 -0
- package/dist/vercel/errors.js +113 -0
- package/dist/vercel/filesystem.d.ts +150 -0
- package/dist/vercel/filesystem.js +248 -0
- package/dist/vercel/index.d.ts +30 -0
- package/dist/vercel/index.js +38 -0
- package/dist/vercel/sandbox.d.ts +317 -0
- package/dist/vercel/sandbox.js +726 -0
- package/dist/vercel/snapshot.d.ts +93 -0
- package/dist/vercel/snapshot.js +123 -0
- package/package.json +69 -5
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* Proxies named by the environment: HTTPS_PROXY, HTTP_PROXY and NO_PROXY, in
|
|
2
|
+
lower or upper case, the lower winning when both are set (as curl reads
|
|
3
|
+
them). Bun's fetch reads them itself; its WebSocket and Node's fetch and
|
|
4
|
+
WebSocket do not unless NODE_USE_ENV_PROXY was set before the process
|
|
5
|
+
started, which a library cannot do for its host. So every call the SDK and
|
|
6
|
+
the CLI make goes through `envFetch` and `openWebSocket`, which route by the
|
|
7
|
+
same rule on every runtime:
|
|
8
|
+
|
|
9
|
+
- an https:// or wss:// address uses HTTPS_PROXY; an http:// or ws:// one
|
|
10
|
+
uses HTTP_PROXY. HTTP_PROXY is never used for HTTPS (curl, Bun, Python
|
|
11
|
+
and Go agree; Node's own env support would fall back to it). A connection
|
|
12
|
+
error says so when that is why nothing was tried.
|
|
13
|
+
- NO_PROXY is a list of host names split by commas or spaces. A name
|
|
14
|
+
covers its subdomains, a leading dot is allowed, host:port limits it to
|
|
15
|
+
one port, and `*` alone means every host.
|
|
16
|
+
- A proxy address without a scheme is http://. Only http:// and https://
|
|
17
|
+
proxies are spoken to.
|
|
18
|
+
|
|
19
|
+
On Node the proxy is undici's ProxyAgent: undici is Node's own HTTP client
|
|
20
|
+
(the code under its fetch and WebSocket), loaded only when a proxy applies.
|
|
21
|
+
On Bun the proxy is passed to fetch and WebSocket as their `proxy` option. */
|
|
22
|
+
import { RuntimeError } from "./errors.js";
|
|
23
|
+
const processEnv = () => (typeof process === "undefined" ? {} : process.env);
|
|
24
|
+
function variable(env, name) {
|
|
25
|
+
for (const candidate of [name.toLowerCase(), name.toUpperCase()]) {
|
|
26
|
+
const value = env[candidate]?.trim();
|
|
27
|
+
if (value)
|
|
28
|
+
return { value, name: candidate };
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/** Whether a NO_PROXY list names this host (and port). Returns the entry. */
|
|
33
|
+
export function exemption(list, hostname, port) {
|
|
34
|
+
const host = hostname
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/^\[|\]$/g, "")
|
|
37
|
+
.replace(/\.$/, "");
|
|
38
|
+
for (const raw of list.split(/[\s,]+/)) {
|
|
39
|
+
const entry = raw.trim().toLowerCase();
|
|
40
|
+
if (!entry)
|
|
41
|
+
continue;
|
|
42
|
+
if (entry === "*")
|
|
43
|
+
return entry;
|
|
44
|
+
const match = /^(.*?)(?::(\d+))?$/.exec(entry.replace(/^\[(.*)\]/, "$1"));
|
|
45
|
+
const name = match[1].replace(/^\./, "").replace(/\.$/, "");
|
|
46
|
+
if (!name || (match[2] && match[2] !== port))
|
|
47
|
+
continue;
|
|
48
|
+
if (host === name || host.endsWith(`.${name}`))
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/** The route the SDK takes to `target`, read from the environment now. */
|
|
54
|
+
export function routeFor(target, env = processEnv()) {
|
|
55
|
+
const url = new URL(target);
|
|
56
|
+
const secure = url.protocol === "https:" || url.protocol === "wss:";
|
|
57
|
+
const port = url.port || (secure ? "443" : "80");
|
|
58
|
+
const found = variable(env, secure ? "https_proxy" : "http_proxy");
|
|
59
|
+
if (!found) {
|
|
60
|
+
const http = secure ? variable(env, "http_proxy") : undefined;
|
|
61
|
+
return http ? { httpOnly: http.name } : {};
|
|
62
|
+
}
|
|
63
|
+
const skip = variable(env, "no_proxy");
|
|
64
|
+
const exempt = skip && exemption(skip.value, url.hostname, port);
|
|
65
|
+
if (skip && exempt)
|
|
66
|
+
return { exempt, variable: skip.name };
|
|
67
|
+
const proxy = /^[a-z][a-z0-9+.-]*:\/\//i.test(found.value)
|
|
68
|
+
? found.value
|
|
69
|
+
: `http://${found.value}`;
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = new URL(proxy);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw invalidProxy(found.name, found.value, "is not an address");
|
|
76
|
+
}
|
|
77
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
78
|
+
throw invalidProxy(found.name, `${parsed.protocol}//${parsed.host}`, "is not an HTTP proxy");
|
|
79
|
+
return { proxy, shown: `${parsed.protocol}//${parsed.host}`, variable: found.name };
|
|
80
|
+
}
|
|
81
|
+
function invalidProxy(name, shown, problem) {
|
|
82
|
+
return new RuntimeError({
|
|
83
|
+
message: `${name} (${shown}) ${problem}; Runtime connects through http:// and https:// proxies.`,
|
|
84
|
+
code: "invalid_proxy",
|
|
85
|
+
status: 0,
|
|
86
|
+
hint: `Set ${name} to the proxy's address, for example http://proxy.example.com:3128, or unset it.`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** Words for a failed connection: the route it took, and what to check.
|
|
90
|
+
* `cause` is the error fetch threw, or the status a proxy answered with (Bun's
|
|
91
|
+
* fetch hands a refused tunnel back as a response). */
|
|
92
|
+
export function describeRoute(target, cause, env = processEnv()) {
|
|
93
|
+
let route;
|
|
94
|
+
try {
|
|
95
|
+
route = routeFor(target, env);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { via: "" };
|
|
99
|
+
}
|
|
100
|
+
const url = new URL(target);
|
|
101
|
+
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
102
|
+
if (route.proxy) {
|
|
103
|
+
const status = typeof cause === "number" ? cause : proxyStatus(cause);
|
|
104
|
+
return {
|
|
105
|
+
via: ` through the proxy ${route.shown} (${route.variable})${status ? `, which answered HTTP ${status} to the tunnel request` : ""}`,
|
|
106
|
+
hint: status === 407
|
|
107
|
+
? `The proxy wants credentials: put them in ${route.variable}, as http://user:password@host:port.`
|
|
108
|
+
: `Check that the proxy is running and allows CONNECT to ${url.hostname}:${port}, or list ${url.hostname} in NO_PROXY to connect directly.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (route.exempt)
|
|
112
|
+
return {
|
|
113
|
+
via: ` (directly: ${route.variable} lists ${route.exempt})`,
|
|
114
|
+
hint: `Check the network, or remove ${route.exempt} from ${route.variable} to go through the proxy.`,
|
|
115
|
+
};
|
|
116
|
+
if (route.httpOnly)
|
|
117
|
+
return {
|
|
118
|
+
via: " (directly: no HTTPS proxy is set)",
|
|
119
|
+
hint: `${route.httpOnly} is set but HTTPS_PROXY is not, and ${url.hostname} is HTTPS. Set HTTPS_PROXY to the proxy to use it.`,
|
|
120
|
+
};
|
|
121
|
+
return { via: "" };
|
|
122
|
+
}
|
|
123
|
+
/** The status a proxy refused a tunnel with, from undici's or Bun's error. */
|
|
124
|
+
function proxyStatus(cause) {
|
|
125
|
+
for (let error = cause, depth = 0; error && depth < 5; depth++) {
|
|
126
|
+
const message = error.message;
|
|
127
|
+
const text = typeof message === "string" ? message : "";
|
|
128
|
+
const found = /proxy response \((\d{3})\)|proxy.*?\b(40[37]|50[0-9])\b/i.exec(text);
|
|
129
|
+
if (found)
|
|
130
|
+
return Number(found[1] ?? found[2]);
|
|
131
|
+
error = error.cause;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const isBun = () => typeof process !== "undefined" && !!process.versions?.bun;
|
|
136
|
+
const isNode = () => typeof process !== "undefined" &&
|
|
137
|
+
!!process.versions?.node &&
|
|
138
|
+
!process.versions.bun &&
|
|
139
|
+
!("Deno" in globalThis);
|
|
140
|
+
const agents = new Map();
|
|
141
|
+
/** One undici ProxyAgent per proxy address, so connections are reused. */
|
|
142
|
+
function undici(proxy) {
|
|
143
|
+
let agent = agents.get(proxy);
|
|
144
|
+
if (!agent) {
|
|
145
|
+
agent = import("undici").then((module) => ({
|
|
146
|
+
fetch: module.fetch,
|
|
147
|
+
WebSocket: module.WebSocket,
|
|
148
|
+
dispatcher: new module.ProxyAgent({ uri: proxy }),
|
|
149
|
+
}));
|
|
150
|
+
agent.catch(() => agents.delete(proxy));
|
|
151
|
+
agents.set(proxy, agent);
|
|
152
|
+
}
|
|
153
|
+
return agent;
|
|
154
|
+
}
|
|
155
|
+
/** fetch, through the proxy the environment names for the address. */
|
|
156
|
+
export const envFetch = (async (input, init) => {
|
|
157
|
+
const target = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
158
|
+
const { proxy } = routeFor(target);
|
|
159
|
+
if (proxy && isBun())
|
|
160
|
+
return fetch(input, { ...init, proxy });
|
|
161
|
+
if (proxy && isNode()) {
|
|
162
|
+
const { fetch: proxied, dispatcher } = await undici(proxy);
|
|
163
|
+
return (await proxied(target, { ...init, dispatcher }));
|
|
164
|
+
}
|
|
165
|
+
return fetch(input, init);
|
|
166
|
+
});
|
|
167
|
+
/** A WebSocket, through the proxy the environment names for the address. */
|
|
168
|
+
export async function openWebSocket(url, headers) {
|
|
169
|
+
const { proxy } = routeFor(url);
|
|
170
|
+
if (proxy && isNode()) {
|
|
171
|
+
const { WebSocket: Proxied, dispatcher } = await undici(proxy);
|
|
172
|
+
return new Proxied(url, { headers, dispatcher });
|
|
173
|
+
}
|
|
174
|
+
const Socket = globalThis.WebSocket;
|
|
175
|
+
if (!Socket)
|
|
176
|
+
throw new Error("This runtime has no WebSocket; use Node 22+ or Bun.");
|
|
177
|
+
return new Socket(url, proxy && isBun() ? { headers, proxy } : { headers });
|
|
178
|
+
}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Page } from "./page.js";
|
|
2
2
|
import type { RequestOptions, Transport } from "./transport.js";
|
|
3
|
-
import type { CommandResult, ExecOptions, FileEntry, OutputEvent, ProcessInfo, SandboxInfo } from "./types.js";
|
|
3
|
+
import type { CommandResult, ExecOptions, FileEntry, OutputEvent, KeepAliveOptions, ProcessInfo, SandboxInfo, SandboxSettings } from "./types.js";
|
|
4
4
|
import { type SandboxExtensions } from "./products/index.js";
|
|
5
5
|
import type { Snapshot, SnapshotOptions } from "./snapshots.js";
|
|
6
|
+
import { Tunnel, type PortForward } from "./tunnel.js";
|
|
6
7
|
/** A running (or stopped) sandbox. Every method is one API call or a short
|
|
7
8
|
* sequence of them; `await using sbx = await Sandbox.create()` stops it for
|
|
8
9
|
* you at the end of the block. */
|
|
@@ -52,6 +53,18 @@ export declare class Sandbox implements AsyncDisposable {
|
|
|
52
53
|
processId?: string;
|
|
53
54
|
onData?: (data: Uint8Array) => void;
|
|
54
55
|
}): Promise<Terminal>;
|
|
56
|
+
/** A tunnel into the sandbox: TCP connections to any port on its loopback,
|
|
57
|
+
* and SSH logins, over one authenticated WebSocket. No port is opened to the
|
|
58
|
+
* internet. `tunnel.connect(5432)` gives one connection;
|
|
59
|
+
* `tunnel.forward(5432)` listens locally (Node and Bun). */
|
|
60
|
+
tunnel(): Promise<Tunnel>;
|
|
61
|
+
/** Listens on a local port (the same number unless `localPort` says, 0 for
|
|
62
|
+
* any) and forwards each connection to `port` inside the sandbox, until
|
|
63
|
+
* `close()`. Node and Bun. */
|
|
64
|
+
forwardPort(port: number, options?: {
|
|
65
|
+
localPort?: number;
|
|
66
|
+
host?: string;
|
|
67
|
+
}): Promise<PortForward>;
|
|
55
68
|
stop(options?: RequestOptions & {
|
|
56
69
|
wait?: boolean;
|
|
57
70
|
}): Promise<this>;
|
|
@@ -65,6 +78,18 @@ export declare class Sandbox implements AsyncDisposable {
|
|
|
65
78
|
}): Promise<this>;
|
|
66
79
|
/** More time before the lease ends (at most an hour ahead of now). */
|
|
67
80
|
extend(seconds: number, options?: RequestOptions): Promise<this>;
|
|
81
|
+
/** Changes its name, labels, automatic wake, idle pause or persistence;
|
|
82
|
+
* fields left out stay as they are. `persistent: true` keeps it running
|
|
83
|
+
* while credit lasts (paid only). */
|
|
84
|
+
update(settings: SandboxSettings, options?: RequestOptions): Promise<this>;
|
|
85
|
+
/** Keeps a running sandbox's lease ahead of now, in the background, until
|
|
86
|
+
* stop() or the returned function ends it: every `everySeconds` (60) it
|
|
87
|
+
* extends the lease so that `marginSeconds` (600) remain, never more than
|
|
88
|
+
* the hour ahead the API allows. Running time is billed as it is used, as
|
|
89
|
+
* for any extension. A paused sandbox is left paused (a request wakes it,
|
|
90
|
+
* unless autoWake is off); a stopped one ends the loop. It does not keep a
|
|
91
|
+
* Node or Bun process alive by itself. */
|
|
92
|
+
keepAlive(options?: KeepAliveOptions): () => void;
|
|
68
93
|
/** Days a paused sandbox is kept before deletion (1 to 365). */
|
|
69
94
|
setRetention(days: number, options?: RequestOptions): Promise<this>;
|
|
70
95
|
/** Keeps this sandbox's whole machine (files, memory, running processes) as
|
|
@@ -74,17 +99,20 @@ export declare class Sandbox implements AsyncDisposable {
|
|
|
74
99
|
/** Copies of this sandbox as it is now (files, memory, running processes),
|
|
75
100
|
* each its own sandbox, answered once they run. A running sandbox is paused
|
|
76
101
|
* for the moment its snapshot takes, then woken. One without `count`; an
|
|
77
|
-
* array with it.
|
|
102
|
+
* array with it. `funding` is what the copies run on, as for a create;
|
|
103
|
+
* omitted, they keep this sandbox's. */
|
|
78
104
|
fork(options?: {
|
|
79
105
|
name?: string;
|
|
80
106
|
labels?: Record<string, string>;
|
|
81
107
|
keepSnapshot?: boolean;
|
|
108
|
+
funding?: "trial" | "paid";
|
|
82
109
|
} & RequestOptions): Promise<Sandbox>;
|
|
83
110
|
fork(options: {
|
|
84
111
|
count: number;
|
|
85
112
|
name?: string;
|
|
86
113
|
labels?: Record<string, string>;
|
|
87
114
|
keepSnapshot?: boolean;
|
|
115
|
+
funding?: "trial" | "paid";
|
|
88
116
|
} & RequestOptions): Promise<Sandbox[]>;
|
|
89
117
|
/** Starts a stopped persistent sandbox again from its disk (memory is not kept). */
|
|
90
118
|
restart(options?: RequestOptions & {
|
package/dist/sandbox.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CommandError, RuntimeError } from "./errors.js";
|
|
2
2
|
import { Page } from "./page.js";
|
|
3
3
|
import { sandboxFactories } from "./products/index.js";
|
|
4
|
+
import { Tunnel } from "./tunnel.js";
|
|
4
5
|
const CHUNK = 1_048_576;
|
|
5
6
|
const PARALLEL = 4;
|
|
6
7
|
const enc = (id) => encodeURIComponent(id);
|
|
@@ -21,6 +22,7 @@ function commandBody(command, options) {
|
|
|
21
22
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
22
23
|
export class Sandbox {
|
|
23
24
|
#info;
|
|
25
|
+
#keepAlive;
|
|
24
26
|
#t;
|
|
25
27
|
files;
|
|
26
28
|
processes;
|
|
@@ -79,6 +81,7 @@ export class Sandbox {
|
|
|
79
81
|
const out = { stdout: "", stderr: "" };
|
|
80
82
|
let exit;
|
|
81
83
|
let processId;
|
|
84
|
+
let dropped = false;
|
|
82
85
|
for await (const event of this.execStream(command, options)) {
|
|
83
86
|
if (event.type === "start")
|
|
84
87
|
processId = event.processId;
|
|
@@ -86,6 +89,8 @@ export class Sandbox {
|
|
|
86
89
|
out[event.type] += event.data;
|
|
87
90
|
(event.type === "stdout" ? options.onStdout : options.onStderr)?.(event.data);
|
|
88
91
|
}
|
|
92
|
+
else if (event.type === "truncated")
|
|
93
|
+
dropped = true;
|
|
89
94
|
else if (event.type === "exit")
|
|
90
95
|
exit = event;
|
|
91
96
|
}
|
|
@@ -93,6 +98,7 @@ export class Sandbox {
|
|
|
93
98
|
exitCode: exit?.exitCode ?? null,
|
|
94
99
|
...out,
|
|
95
100
|
timedOut: exit?.timedOut ?? false,
|
|
101
|
+
...truncation(dropped),
|
|
96
102
|
...(exit?.durationMs === undefined ? {} : { durationMs: exit.durationMs }),
|
|
97
103
|
...(processId ? { processId } : {}),
|
|
98
104
|
};
|
|
@@ -163,7 +169,35 @@ export class Sandbox {
|
|
|
163
169
|
});
|
|
164
170
|
return Terminal.open(socket, options.onData);
|
|
165
171
|
}
|
|
172
|
+
/** A tunnel into the sandbox: TCP connections to any port on its loopback,
|
|
173
|
+
* and SSH logins, over one authenticated WebSocket. No port is opened to the
|
|
174
|
+
* internet. `tunnel.connect(5432)` gives one connection;
|
|
175
|
+
* `tunnel.forward(5432)` listens locally (Node and Bun). */
|
|
176
|
+
async tunnel() {
|
|
177
|
+
return Tunnel.open(await this.#t.websocket(`/v1/sandboxes/${enc(this.id)}/tunnel`));
|
|
178
|
+
}
|
|
179
|
+
/** Listens on a local port (the same number unless `localPort` says, 0 for
|
|
180
|
+
* any) and forwards each connection to `port` inside the sandbox, until
|
|
181
|
+
* `close()`. Node and Bun. */
|
|
182
|
+
async forwardPort(port, options = {}) {
|
|
183
|
+
const tunnel = await this.tunnel();
|
|
184
|
+
try {
|
|
185
|
+
const forward = await tunnel.forward(port, options);
|
|
186
|
+
return {
|
|
187
|
+
...forward,
|
|
188
|
+
close: async () => {
|
|
189
|
+
await forward.close();
|
|
190
|
+
tunnel.close();
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
tunnel.close();
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
166
199
|
async stop(options = {}) {
|
|
200
|
+
this.#keepAlive?.();
|
|
167
201
|
return this.#lifecycle("stop", options);
|
|
168
202
|
}
|
|
169
203
|
async pause(options = {}) {
|
|
@@ -178,6 +212,58 @@ export class Sandbox {
|
|
|
178
212
|
async extend(seconds, options = {}) {
|
|
179
213
|
return this.#lifecycle("extend", { ...options, wait: false }, { seconds });
|
|
180
214
|
}
|
|
215
|
+
/** Changes its name, labels, automatic wake, idle pause or persistence;
|
|
216
|
+
* fields left out stay as they are. `persistent: true` keeps it running
|
|
217
|
+
* while credit lasts (paid only). */
|
|
218
|
+
async update(settings, options = {}) {
|
|
219
|
+
return this.#lifecycle("update", { ...options, wait: false }, { ...settings });
|
|
220
|
+
}
|
|
221
|
+
/** Keeps a running sandbox's lease ahead of now, in the background, until
|
|
222
|
+
* stop() or the returned function ends it: every `everySeconds` (60) it
|
|
223
|
+
* extends the lease so that `marginSeconds` (600) remain, never more than
|
|
224
|
+
* the hour ahead the API allows. Running time is billed as it is used, as
|
|
225
|
+
* for any extension. A paused sandbox is left paused (a request wakes it,
|
|
226
|
+
* unless autoWake is off); a stopped one ends the loop. It does not keep a
|
|
227
|
+
* Node or Bun process alive by itself. */
|
|
228
|
+
keepAlive(options = {}) {
|
|
229
|
+
this.#keepAlive?.();
|
|
230
|
+
const every = Math.max(10, options.everySeconds ?? 60) * 1000;
|
|
231
|
+
const margin = Math.min(3600, Math.max(60, options.marginSeconds ?? 600));
|
|
232
|
+
let ended = false;
|
|
233
|
+
let timer;
|
|
234
|
+
const end = () => {
|
|
235
|
+
ended = true;
|
|
236
|
+
if (timer)
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
if (this.#keepAlive === end)
|
|
239
|
+
this.#keepAlive = undefined;
|
|
240
|
+
};
|
|
241
|
+
const tick = async () => {
|
|
242
|
+
if (ended)
|
|
243
|
+
return;
|
|
244
|
+
try {
|
|
245
|
+
await this.refresh();
|
|
246
|
+
if (this.state === "stopped" || this.state === "stopping")
|
|
247
|
+
return end();
|
|
248
|
+
if (this.state === "running") {
|
|
249
|
+
const left = (Date.parse(this.info.expiresAt) - Date.now()) / 1000;
|
|
250
|
+
const need = Math.ceil(margin - left);
|
|
251
|
+
if (need >= 1)
|
|
252
|
+
await this.extend(Math.min(3600, need));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
options.onError?.(error);
|
|
257
|
+
}
|
|
258
|
+
if (ended)
|
|
259
|
+
return;
|
|
260
|
+
timer = setTimeout(() => void tick(), every);
|
|
261
|
+
timer.unref?.();
|
|
262
|
+
};
|
|
263
|
+
this.#keepAlive = end;
|
|
264
|
+
void tick();
|
|
265
|
+
return end;
|
|
266
|
+
}
|
|
181
267
|
/** Days a paused sandbox is kept before deletion (1 to 365). */
|
|
182
268
|
async setRetention(days, options = {}) {
|
|
183
269
|
return this.#lifecycle("retention", { ...options, wait: false }, { days });
|
|
@@ -240,6 +326,7 @@ export class Sandbox {
|
|
|
240
326
|
return this;
|
|
241
327
|
}
|
|
242
328
|
async [Symbol.asyncDispose]() {
|
|
329
|
+
this.#keepAlive?.();
|
|
243
330
|
if (this.#info.state === "stopped")
|
|
244
331
|
return;
|
|
245
332
|
await this.stop({ wait: false }).catch(() => undefined);
|
|
@@ -248,6 +335,13 @@ export class Sandbox {
|
|
|
248
335
|
return this.#info;
|
|
249
336
|
}
|
|
250
337
|
}
|
|
338
|
+
/** A streamed command keeps all its output unless the reader fell more than the
|
|
339
|
+
* process's output buffer behind, which the server marks with a "truncated"
|
|
340
|
+
* event. That event does not say which stream lost bytes, so both flags carry
|
|
341
|
+
* it; without one, both are false, as a synchronous result's are. */
|
|
342
|
+
function truncation(dropped) {
|
|
343
|
+
return { stdoutTruncated: dropped, stderrTruncated: dropped };
|
|
344
|
+
}
|
|
251
345
|
function pick(options) {
|
|
252
346
|
return {
|
|
253
347
|
...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
@@ -332,9 +426,12 @@ export class Process {
|
|
|
332
426
|
async wait(options = {}) {
|
|
333
427
|
const out = { stdout: "", stderr: "" };
|
|
334
428
|
let exit;
|
|
429
|
+
let dropped = false;
|
|
335
430
|
for await (const event of this.output(options)) {
|
|
336
431
|
if (event.type === "stdout" || event.type === "stderr")
|
|
337
432
|
out[event.type] += event.data;
|
|
433
|
+
if (event.type === "truncated")
|
|
434
|
+
dropped = true;
|
|
338
435
|
if (event.type === "exit")
|
|
339
436
|
exit = event;
|
|
340
437
|
}
|
|
@@ -342,6 +439,7 @@ export class Process {
|
|
|
342
439
|
exitCode: exit?.exitCode ?? null,
|
|
343
440
|
...out,
|
|
344
441
|
timedOut: exit?.timedOut ?? false,
|
|
442
|
+
...truncation(dropped),
|
|
345
443
|
processId: this.id,
|
|
346
444
|
};
|
|
347
445
|
}
|
|
@@ -576,7 +674,9 @@ export class Files {
|
|
|
576
674
|
}
|
|
577
675
|
const { packDirectory } = await import("./tar.js");
|
|
578
676
|
const archive = await packDirectory(localPath);
|
|
579
|
-
|
|
677
|
+
// Large writes go only to /workspace (the server refuses others), so the
|
|
678
|
+
// archive is staged there and removed after unpacking.
|
|
679
|
+
const staging = `/workspace/.runtime-upload-${crypto.randomUUID()}.tar.gz`;
|
|
580
680
|
await this.write(staging, archive);
|
|
581
681
|
const result = await new Sandbox(this.t, { id: this.sandboxId }).exec([
|
|
582
682
|
"sh",
|
package/dist/ssh.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Runtime } from "./client.js";
|
|
2
|
+
import type { Sandbox } from "./sandbox.js";
|
|
3
|
+
type Out = {
|
|
4
|
+
json: boolean;
|
|
5
|
+
write: (text: string) => void;
|
|
6
|
+
error: (text: string) => void;
|
|
7
|
+
};
|
|
8
|
+
export declare const SSH_HELP: (me: string) => string;
|
|
9
|
+
/** This machine's key for Runtime SSH logins, made on first use. Only its
|
|
10
|
+
* public half ever leaves the machine, and only into the one login it opens. */
|
|
11
|
+
export declare function sshKey(env: NodeJS.ProcessEnv): Promise<{
|
|
12
|
+
path: string;
|
|
13
|
+
publicKey: string;
|
|
14
|
+
}>;
|
|
15
|
+
/** A sandbox by id or by its name, as `ssh` hands it over (`<id>.runtime`). */
|
|
16
|
+
export declare function resolveSandbox(runtime: Runtime, target: string): Promise<Sandbox>;
|
|
17
|
+
export declare function sshConfig(env: NodeJS.ProcessEnv): Promise<string>;
|
|
18
|
+
export declare function sshCommand(argv: string[], env: NodeJS.ProcessEnv, out: Out, connect: () => Promise<Runtime>, me: string): Promise<number>;
|
|
19
|
+
/** `runtime sandbox port-forward <id> 5432 8080:3000 ...` */
|
|
20
|
+
export declare function portForwardCommand(argv: string[], out: Out, connect: () => Promise<Runtime>, me: string): Promise<number>;
|
|
21
|
+
export {};
|