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.
Files changed (105) hide show
  1. package/DAYTONA.md +129 -0
  2. package/E2B.md +146 -0
  3. package/README.md +84 -12
  4. package/VERCEL.md +127 -0
  5. package/dist/ai/index.d.ts +33 -0
  6. package/dist/ai/index.js +38 -0
  7. package/dist/claude-agent-sdk/index.d.ts +7 -0
  8. package/dist/claude-agent-sdk/index.js +65 -0
  9. package/dist/cli.d.ts +6 -0
  10. package/dist/cli.js +949 -105
  11. package/dist/client.d.ts +25 -2
  12. package/dist/client.js +14 -1
  13. package/dist/credentials.d.ts +10 -0
  14. package/dist/credentials.js +56 -1
  15. package/dist/daytona/client.d.ts +44 -0
  16. package/dist/daytona/client.js +40 -0
  17. package/dist/daytona/context.d.ts +26 -0
  18. package/dist/daytona/context.js +22 -0
  19. package/dist/daytona/daytona.d.ts +185 -0
  20. package/dist/daytona/daytona.js +416 -0
  21. package/dist/daytona/errors.d.ts +106 -0
  22. package/dist/daytona/errors.js +182 -0
  23. package/dist/daytona/filesystem.d.ts +84 -0
  24. package/dist/daytona/filesystem.js +206 -0
  25. package/dist/daytona/git.d.ts +33 -0
  26. package/dist/daytona/git.js +128 -0
  27. package/dist/daytona/image.d.ts +45 -0
  28. package/dist/daytona/image.js +140 -0
  29. package/dist/daytona/index.d.ts +50 -0
  30. package/dist/daytona/index.js +50 -0
  31. package/dist/daytona/process.d.ts +86 -0
  32. package/dist/daytona/process.js +339 -0
  33. package/dist/daytona/sandbox.d.ts +196 -0
  34. package/dist/daytona/sandbox.js +448 -0
  35. package/dist/e2b/client.d.ts +58 -0
  36. package/dist/e2b/client.js +66 -0
  37. package/dist/e2b/code-interpreter.d.ts +144 -0
  38. package/dist/e2b/code-interpreter.js +244 -0
  39. package/dist/e2b/commands.d.ts +100 -0
  40. package/dist/e2b/commands.js +268 -0
  41. package/dist/e2b/errors.d.ts +72 -0
  42. package/dist/e2b/errors.js +151 -0
  43. package/dist/e2b/filesystem.d.ts +82 -0
  44. package/dist/e2b/filesystem.js +143 -0
  45. package/dist/e2b/index.d.ts +28 -0
  46. package/dist/e2b/index.js +61 -0
  47. package/dist/e2b/sandbox.d.ts +203 -0
  48. package/dist/e2b/sandbox.js +490 -0
  49. package/dist/e2b/unsupported.d.ts +19 -0
  50. package/dist/e2b/unsupported.js +27 -0
  51. package/dist/errors.d.ts +10 -4
  52. package/dist/errors.js +26 -6
  53. package/dist/image-context.d.ts +39 -0
  54. package/dist/image-context.js +176 -0
  55. package/dist/index.d.ts +19 -2
  56. package/dist/index.js +17 -2
  57. package/dist/keys.d.ts +43 -0
  58. package/dist/keys.js +196 -0
  59. package/dist/login.js +27 -6
  60. package/dist/mcp.d.ts +2 -2
  61. package/dist/mcp.js +4 -3
  62. package/dist/openai-agents/index.d.ts +132 -0
  63. package/dist/openai-agents/index.js +791 -0
  64. package/dist/products/audit.d.ts +48 -0
  65. package/dist/products/audit.js +10 -0
  66. package/dist/products/images.d.ts +113 -7
  67. package/dist/products/images.js +186 -27
  68. package/dist/products/index.d.ts +11 -0
  69. package/dist/products/index.js +17 -1
  70. package/dist/products/observability.d.ts +186 -0
  71. package/dist/products/observability.js +117 -0
  72. package/dist/products/secrets.d.ts +40 -0
  73. package/dist/products/secrets.js +24 -0
  74. package/dist/products/switching.d.ts +89 -0
  75. package/dist/products/switching.js +25 -0
  76. package/dist/proxy.d.ts +30 -0
  77. package/dist/proxy.js +178 -0
  78. package/dist/sandbox.d.ts +30 -2
  79. package/dist/sandbox.js +101 -1
  80. package/dist/ssh.d.ts +21 -0
  81. package/dist/ssh.js +312 -0
  82. package/dist/tar.d.ts +1 -0
  83. package/dist/tar.js +4 -4
  84. package/dist/tools/index.d.ts +62 -0
  85. package/dist/tools/index.js +180 -0
  86. package/dist/transport.d.ts +12 -2
  87. package/dist/transport.js +76 -16
  88. package/dist/tunnel.d.ts +67 -0
  89. package/dist/tunnel.js +311 -0
  90. package/dist/types.d.ts +57 -1
  91. package/dist/vercel/client.d.ts +40 -0
  92. package/dist/vercel/client.js +39 -0
  93. package/dist/vercel/command.d.ts +70 -0
  94. package/dist/vercel/command.js +148 -0
  95. package/dist/vercel/errors.d.ts +53 -0
  96. package/dist/vercel/errors.js +113 -0
  97. package/dist/vercel/filesystem.d.ts +150 -0
  98. package/dist/vercel/filesystem.js +248 -0
  99. package/dist/vercel/index.d.ts +30 -0
  100. package/dist/vercel/index.js +38 -0
  101. package/dist/vercel/sandbox.d.ts +317 -0
  102. package/dist/vercel/sandbox.js +726 -0
  103. package/dist/vercel/snapshot.d.ts +93 -0
  104. package/dist/vercel/snapshot.js +123 -0
  105. package/package.json +69 -5
package/dist/ssh.js ADDED
@@ -0,0 +1,312 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
3
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { isAbsolute, join } from "node:path";
6
+ import { RuntimeError } from "./errors.js";
7
+ import { VERSION } from "./transport.js";
8
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
9
+ export const SSH_HELP = (me) => `${me} sandbox ssh <id|name> [-- <command...>] Log in over SSH (an interactive shell, or run the command)
10
+ ${me} sandbox ssh config [--install] The ssh config for \`ssh <id|name>.runtime\`, also from
11
+ VS Code Remote-SSH and JetBrains Gateway; --install adds
12
+ it to ~/.ssh/config
13
+ ${me} sandbox ssh proxy <id|name>[.runtime] ProxyCommand mode: SSH on stdin and stdout
14
+ ${me} sandbox port-forward <id|name> <port|local:remote>... [--address 127.0.0.1]
15
+ Forward local ports to ports in the sandbox, until Ctrl-C
16
+
17
+ SSH and port forwarding go through Runtime's API with your key: the sandbox opens
18
+ no port to the internet. The login is the sandbox's user, runtime, with sudo.
19
+ A paused sandbox wakes when you connect, like any command.
20
+ `;
21
+ function configDirectory(env) {
22
+ const root = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
23
+ if (!isAbsolute(root))
24
+ throw new Error("XDG_CONFIG_HOME must be an absolute path.");
25
+ return join(root, "runtime-cloud");
26
+ }
27
+ /** This machine's key for Runtime SSH logins, made on first use. Only its
28
+ * public half ever leaves the machine, and only into the one login it opens. */
29
+ export async function sshKey(env) {
30
+ const dir = join(configDirectory(env), "ssh");
31
+ const path = join(dir, "id_ed25519");
32
+ try {
33
+ return { path, publicKey: (await readFile(`${path}.pub`, "utf8")).trim() };
34
+ }
35
+ catch {
36
+ // Made below.
37
+ }
38
+ await mkdir(dir, { recursive: true, mode: 0o700 });
39
+ await chmod(dir, 0o700);
40
+ const made = spawnSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-C", "runtime-cloud", "-f", path], {
41
+ stdio: ["ignore", "ignore", "pipe"],
42
+ });
43
+ if (made.error || made.status !== 0)
44
+ throw new RuntimeError({
45
+ message: `Could not make an SSH key with ssh-keygen${made.error ? `: ${made.error.message}` : `: ${made.stderr.toString().trim()}`}.`,
46
+ code: "ssh_keygen_failed",
47
+ status: 0,
48
+ hint: "Install OpenSSH (it has ssh and ssh-keygen), or pass --key with a public key file.",
49
+ });
50
+ return { path, publicKey: (await readFile(`${path}.pub`, "utf8")).trim() };
51
+ }
52
+ /** A sandbox by id or by its name, as `ssh` hands it over (`<id>.runtime`). */
53
+ export async function resolveSandbox(runtime, target) {
54
+ const name = target.replace(/\.runtime$/, "");
55
+ if (UUID.test(name))
56
+ return runtime.sandboxes.get(name);
57
+ const page = await runtime.sandboxes.list({ name, limit: 2 });
58
+ if (page.data.length === 1)
59
+ return page.data[0];
60
+ throw new RuntimeError({
61
+ message: page.data.length
62
+ ? `More than one live sandbox is named ${name}.`
63
+ : `No live sandbox is named ${name}.`,
64
+ code: page.data.length ? "ambiguous_name" : "sandbox_not_found",
65
+ status: 0,
66
+ hint: "Use its id: runtime sandbox ls",
67
+ });
68
+ }
69
+ const quote = (text) => /^[A-Za-z0-9_./:@=-]+$/.test(text) ? text : `'${text.replace(/'/g, "'\\''")}'`;
70
+ /** How ssh should start this CLI again: the same program and script, or npx
71
+ * at this version when it ran from npx's temporary cache. */
72
+ function selfCommand(env) {
73
+ const script = process.argv[1];
74
+ const program = script && !/[\\/]_npx[\\/]/.test(script)
75
+ ? [process.execPath, realpathSync(script)]
76
+ : ["npx", "--yes", `withruntime@${VERSION}`];
77
+ const origin = env.RUNTIME_API_URL ? [`RUNTIME_API_URL=${env.RUNTIME_API_URL}`] : [];
78
+ return [...(origin.length ? ["env", ...origin] : []), ...program].map(quote).join(" ");
79
+ }
80
+ export async function sshConfig(env) {
81
+ const { path } = await sshKey(env);
82
+ return `# Runtime Cloud: \`ssh <sandbox id or name>.runtime\`, and the same host in
83
+ # VS Code Remote-SSH or JetBrains Gateway. The connection is Runtime's
84
+ # authenticated tunnel, which already proves which sandbox answers; each login
85
+ # gets a fresh host key, so there is nothing to pin in known_hosts.
86
+ Host *.runtime
87
+ User runtime
88
+ ProxyCommand ${selfCommand(env)} sandbox ssh proxy %h
89
+ IdentityFile "${path}"
90
+ IdentitiesOnly yes
91
+ StrictHostKeyChecking no
92
+ UserKnownHostsFile /dev/null
93
+ LogLevel ERROR
94
+ ServerAliveInterval 30
95
+ `;
96
+ }
97
+ /** Writes the block to its own file and includes it from ~/.ssh/config once. */
98
+ async function installConfig(env) {
99
+ const file = join(configDirectory(env), "ssh", "config");
100
+ await writeFile(file, await sshConfig(env), { mode: 0o600 });
101
+ const sshDir = join(homedir(), ".ssh");
102
+ await mkdir(sshDir, { recursive: true, mode: 0o700 });
103
+ const main = join(sshDir, "config");
104
+ const current = await readFile(main, "utf8").catch(() => "");
105
+ const include = `Include "${file}"`;
106
+ if (!current.split("\n").some((line) => line.trim() === include))
107
+ // First, because ssh reads an Include inside a Host block as part of it.
108
+ await writeFile(main, `${include}\n${current ? `\n${current}` : ""}`, { mode: 0o600 });
109
+ return file;
110
+ }
111
+ /** ProxyCommand mode: ssh speaks on this process's stdin and stdout. */
112
+ async function proxy(runtime, target, publicKey) {
113
+ const sbx = await resolveSandbox(runtime, target);
114
+ const tunnel = await sbx.tunnel();
115
+ const stream = await tunnel.ssh(publicKey).catch((error) => {
116
+ tunnel.close();
117
+ throw error;
118
+ });
119
+ return new Promise(() => {
120
+ // When the sandbox side ends, the session is over: flush what ssh has not
121
+ // read yet, then exit, which is what closes the pipe ssh is reading (an
122
+ // ended process.stdout does not close it on every runtime).
123
+ let leaving = false;
124
+ const leave = (code) => {
125
+ if (leaving)
126
+ return;
127
+ leaving = true;
128
+ tunnel.close();
129
+ process.stdout.write("", () => process.exit(code));
130
+ };
131
+ stream.onData = (data) => void process.stdout.write(data);
132
+ stream.onEnd = () => leave(0);
133
+ stream.onLog = (text) => process.stderr.write(`sandbox sshd: ${text}`);
134
+ stream.onClose = (reason) => {
135
+ if (reason !== "done")
136
+ process.stderr.write(`Runtime: the SSH connection ended (${reason}).\n`);
137
+ leave(reason === "done" ? 0 : 1);
138
+ };
139
+ // Async iteration reads the next chunk only after this one is inside the
140
+ // window: backpressure that behaves the same on Node and Bun.
141
+ void (async () => {
142
+ for await (const data of process.stdin)
143
+ await stream.write(new Uint8Array(data));
144
+ stream.end();
145
+ })().catch(() => stream.close());
146
+ });
147
+ }
148
+ /** Installs the OpenSSH server in a sandbox that lacks it (an image made
149
+ * before it was in the base image, or a custom one), saying so first. */
150
+ async function ensureSshd(sbx, out) {
151
+ const probe = await sbx.exec(["sh", "-c", "command -v sshd || test -x /usr/sbin/sshd"], {
152
+ timeoutMs: 30_000,
153
+ });
154
+ if (probe.exitCode === 0)
155
+ return;
156
+ out.error("This sandbox has no OpenSSH server yet; installing it once...");
157
+ const installed = await sbx.exec([
158
+ "sh",
159
+ "-c",
160
+ "sudo -n apt-get update -qq && sudo -n env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends openssh-server",
161
+ ], { timeoutMs: 300_000 });
162
+ if (installed.exitCode !== 0)
163
+ throw new RuntimeError({
164
+ message: `Installing openssh-server in the sandbox failed: ${(installed.stderr || installed.stdout).trim().slice(-400)}`,
165
+ code: "sshd_missing",
166
+ status: 0,
167
+ hint: "Install an SSH server in the sandbox yourself, or build an image that has one.",
168
+ });
169
+ }
170
+ export async function sshCommand(argv, env, out, connect, me) {
171
+ const dash = argv.indexOf("--");
172
+ const words = dash === -1 ? argv : argv.slice(0, dash);
173
+ const remote = dash === -1 ? [] : argv.slice(dash + 1);
174
+ const keyAt = words.indexOf("--key");
175
+ const keyFile = keyAt === -1 ? undefined : words[keyAt + 1];
176
+ const positional = words.filter((word, i) => !word.startsWith("--") && (keyAt === -1 || i !== keyAt + 1));
177
+ const [first, second] = positional;
178
+ if (!first || first === "help") {
179
+ out.write(SSH_HELP(me));
180
+ return first ? 0 : 64;
181
+ }
182
+ const publicKey = async () => keyFile ? (await readFile(keyFile, "utf8")).trim() : (await sshKey(env)).publicKey;
183
+ if (first === "config") {
184
+ if (words.includes("--install")) {
185
+ const file = await installConfig(env);
186
+ out.write(out.json
187
+ ? JSON.stringify({ installed: file })
188
+ : `Added Include "${file}" to ~/.ssh/config. Connect with: ssh <sandbox id or name>.runtime`);
189
+ }
190
+ else
191
+ out.write(out.json ? JSON.stringify({ config: await sshConfig(env) }) : await sshConfig(env));
192
+ return 0;
193
+ }
194
+ if (first === "proxy") {
195
+ if (!second)
196
+ throw usageError(`Give the host: ${me} sandbox ssh proxy <id>.runtime`, me);
197
+ return proxy(await connect(), second, await publicKey());
198
+ }
199
+ const runtime = await connect();
200
+ const sbx = await resolveSandbox(runtime, first);
201
+ await ensureSshd(sbx, out);
202
+ const key = keyFile ? undefined : await sshKey(env);
203
+ const args = [
204
+ "-o",
205
+ `ProxyCommand=${selfCommand(env)} sandbox ssh proxy${keyFile ? ` --key ${quote(keyFile)}` : ""} %h`,
206
+ ...(key ? ["-i", key.path, "-o", "IdentitiesOnly=yes"] : []),
207
+ "-o",
208
+ "StrictHostKeyChecking=no",
209
+ "-o",
210
+ "UserKnownHostsFile=/dev/null",
211
+ "-o",
212
+ "LogLevel=ERROR",
213
+ "-o",
214
+ "ServerAliveInterval=30",
215
+ ...(remote.length ? [] : ["-t"]),
216
+ `runtime@${sbx.id}.runtime`,
217
+ // ssh joins what follows the host into one remote command line.
218
+ ...remote,
219
+ ];
220
+ return new Promise((resolve, reject) => {
221
+ const child = spawn("ssh", args, { stdio: "inherit" });
222
+ child.on("error", (error) => reject(new RuntimeError({
223
+ message: `Could not run ssh: ${error.message}`,
224
+ code: "ssh_missing",
225
+ status: 0,
226
+ hint: "Install OpenSSH, or use `runtime sandbox shell <id>`, which needs nothing.",
227
+ })));
228
+ child.on("exit", (code) => resolve(code ?? 255));
229
+ });
230
+ }
231
+ function usageError(message, me) {
232
+ return new RuntimeError({
233
+ message,
234
+ code: "usage",
235
+ status: 0,
236
+ hint: `Run \`${me} sandbox ssh help\`.`,
237
+ });
238
+ }
239
+ /** `runtime sandbox port-forward <id> 5432 8080:3000 ...` */
240
+ export async function portForwardCommand(argv, out, connect, me) {
241
+ const at = argv.indexOf("--address");
242
+ const address = at === -1 ? "127.0.0.1" : argv[at + 1];
243
+ const positional = argv.filter((word, i) => !word.startsWith("--") && (at === -1 || i !== at + 1));
244
+ const [target, ...specs] = positional;
245
+ if (!target || !specs.length || !address)
246
+ throw new RuntimeError({
247
+ message: `Give a sandbox and at least one port: ${me} sandbox port-forward <id> 5432 8080:3000`,
248
+ code: "usage",
249
+ status: 0,
250
+ hint: `Run \`${me} sandbox ssh help\`.`,
251
+ });
252
+ const ports = specs.map((spec) => {
253
+ const match = /^(?:(\d{1,5}):)?(\d{1,5})$/.exec(spec);
254
+ const remote = Number(match?.[2]);
255
+ const local = match?.[1] === undefined ? remote : Number(match[1]);
256
+ if (!match || remote < 1 || remote > 65_535 || local > 65_535)
257
+ throw new RuntimeError({
258
+ message: `${spec} is not a port or local:remote pair.`,
259
+ code: "usage",
260
+ status: 0,
261
+ hint: "For example 5432, or 15432:5432 to listen on 15432 here.",
262
+ });
263
+ return { local, remote };
264
+ });
265
+ const sbx = await resolveSandbox(await connect(), target);
266
+ const tunnel = await sbx.tunnel();
267
+ const forwards = [];
268
+ try {
269
+ for (const { local, remote } of ports)
270
+ forwards.push(await tunnel.forward(remote, { localPort: local, host: address }));
271
+ }
272
+ catch (error) {
273
+ tunnel.close();
274
+ throw new RuntimeError({
275
+ message: `Could not listen here: ${error instanceof Error ? error.message : String(error)}`,
276
+ code: "listen_failed",
277
+ status: 0,
278
+ hint: "Pick another local port with local:remote, for example 15432:5432.",
279
+ });
280
+ }
281
+ out.write(out.json
282
+ ? JSON.stringify({
283
+ sandboxId: sbx.id,
284
+ forwarding: forwards.map((f) => ({
285
+ address: f.host,
286
+ localPort: f.localPort,
287
+ port: f.port,
288
+ })),
289
+ })
290
+ : forwards
291
+ .map((f) => `Forwarding ${f.host}:${f.localPort} -> sandbox ${sbx.id} port ${f.port}`)
292
+ .join("\n") + "\nPress Ctrl-C to stop.");
293
+ return new Promise((resolve) => {
294
+ let stopping = false;
295
+ const stop = () => {
296
+ stopping = true;
297
+ process.off("SIGINT", stop);
298
+ void Promise.all(forwards.map((f) => f.close())).then(() => {
299
+ tunnel.close();
300
+ resolve(0);
301
+ });
302
+ };
303
+ process.once("SIGINT", stop);
304
+ void tunnel.closed.then((reason) => {
305
+ if (stopping)
306
+ return;
307
+ process.off("SIGINT", stop);
308
+ out.error(`The tunnel closed: ${reason}`);
309
+ void Promise.all(forwards.map((f) => f.close())).then(() => resolve(1));
310
+ });
311
+ });
312
+ }
package/dist/tar.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare function tarHeader(name: string, size: number, mode: number, type: "0" | "5" | "2", link?: string, mtime?: number): Uint8Array;
1
2
  export declare function packDirectory(root: string): Promise<Uint8Array>;
2
3
  /** Unpacks into `target`, refusing any entry that would land outside it. */
3
4
  export declare function unpackArchive(archive: Uint8Array, target: string): Promise<void>;
package/dist/tar.js CHANGED
@@ -4,7 +4,7 @@ import { gunzipSync, gzipSync } from "node:zlib";
4
4
  /* A small ustar writer and reader for directory uploads and downloads, so the
5
5
  SDK needs no dependency. Regular files, directories and symbolic links;
6
6
  names up to 255 bytes through the ustar prefix field. */
7
- function header(name, size, mode, type, link = "", mtime = 0) {
7
+ export function tarHeader(name, size, mode, type, link = "", mtime = 0) {
8
8
  const block = new Uint8Array(512);
9
9
  const bytes = new TextEncoder().encode(name);
10
10
  let prefix = new Uint8Array();
@@ -48,16 +48,16 @@ export async function packDirectory(root) {
48
48
  const info = await lstat(full);
49
49
  const mtime = Math.floor(info.mtimeMs / 1000);
50
50
  if (info.isDirectory()) {
51
- parts.push(header(`${path}/`, 0, info.mode, "5", "", mtime));
51
+ parts.push(tarHeader(`${path}/`, 0, info.mode, "5", "", mtime));
52
52
  await walk(full);
53
53
  }
54
54
  else if (info.isSymbolicLink()) {
55
55
  const { readlink } = await import("node:fs/promises");
56
- parts.push(header(path, 0, info.mode, "2", await readlink(full), mtime));
56
+ parts.push(tarHeader(path, 0, info.mode, "2", await readlink(full), mtime));
57
57
  }
58
58
  else if (info.isFile()) {
59
59
  const data = await readFile(full);
60
- parts.push(header(path, data.length, info.mode, "0", "", mtime), data);
60
+ parts.push(tarHeader(path, data.length, info.mode, "0", "", mtime), data);
61
61
  const pad = (512 - (data.length % 512)) % 512;
62
62
  if (pad)
63
63
  parts.push(new Uint8Array(pad));
@@ -0,0 +1,62 @@
1
+ import type { Sandbox } from "../sandbox.js";
2
+ export type ToolInputSchema = {
3
+ type: "object";
4
+ properties: Record<string, {
5
+ type: "string" | "integer";
6
+ description: string;
7
+ minimum?: number;
8
+ }>;
9
+ required: string[];
10
+ additionalProperties: false;
11
+ };
12
+ export type SandboxTool<Input, Output> = {
13
+ name: string;
14
+ description: string;
15
+ inputSchema: ToolInputSchema;
16
+ execute(input: Input): Promise<Output>;
17
+ };
18
+ export type ExecInput = {
19
+ command: string;
20
+ cwd?: string;
21
+ timeoutSeconds?: number;
22
+ };
23
+ export type ExecOutput = {
24
+ exitCode: number | null;
25
+ stdout: string;
26
+ stderr: string;
27
+ timedOut: boolean;
28
+ };
29
+ export type ListEntry = {
30
+ path: string;
31
+ type: "file" | "directory" | "symlink" | "other";
32
+ size: number;
33
+ };
34
+ export type SandboxToolOptions = {
35
+ /** Where relative paths resolve. Default /workspace. */
36
+ root?: string;
37
+ /** For a command that names no timeout. Default 300. */
38
+ timeoutSeconds?: number;
39
+ /** Characters kept from each of stdout, stderr and a file read; the rest is cut from the front. Default 20 000. */
40
+ maxOutputChars?: number;
41
+ };
42
+ export type SandboxTools = [
43
+ SandboxTool<ExecInput, ExecOutput>,
44
+ SandboxTool<{
45
+ path: string;
46
+ }, string>,
47
+ SandboxTool<{
48
+ path: string;
49
+ content: string;
50
+ }, string>,
51
+ SandboxTool<{
52
+ path?: string;
53
+ depth?: number;
54
+ }, ListEntry[] | {
55
+ error: string;
56
+ }>
57
+ ];
58
+ /** A POSIX path joined and normalized: `..` cannot climb past `/`. */
59
+ export declare function resolvePath(root: string, path: string | undefined): string;
60
+ export declare function clip(text: string, limit: number): string;
61
+ /** runtime_exec, runtime_read_file, runtime_write_file and runtime_list_files, bound to `sandbox`. */
62
+ export declare function sandboxTools(sandbox: Sandbox, options?: SandboxToolOptions): SandboxTools;
@@ -0,0 +1,180 @@
1
+ /** Four tools bound to one Runtime sandbox, in no framework's format: a name,
2
+ * a description, a JSON Schema and a function. The adapters for the Vercel AI
3
+ * SDK (and Mastra), the Claude Agent SDK and the OpenAI Agents SDK wrap these,
4
+ * and any other framework can too:
5
+ *
6
+ * import { sandboxTools } from "withruntime/tools";
7
+ * for (const t of sandboxTools(sbx)) register(t.name, t.description, t.inputSchema, t.execute);
8
+ *
9
+ * The application creates the sandbox and binds the tools to it: the model
10
+ * chooses commands and paths, never the sandbox, the account or the key.
11
+ * Relative paths are under /workspace, and output is capped so one noisy
12
+ * command cannot flood the model's context. */
13
+ import { NotFoundError } from "../errors.js";
14
+ const WORKSPACE = "/workspace";
15
+ /** A POSIX path joined and normalized: `..` cannot climb past `/`. */
16
+ export function resolvePath(root, path) {
17
+ const joined = (path ?? ".").startsWith("/") ? path : `${root}/${path ?? "."}`;
18
+ const parts = [];
19
+ for (const part of joined.split("/")) {
20
+ if (!part || part === ".")
21
+ continue;
22
+ if (part === "..")
23
+ parts.pop();
24
+ else
25
+ parts.push(part);
26
+ }
27
+ return `/${parts.join("/")}`;
28
+ }
29
+ export function clip(text, limit) {
30
+ return text.length <= limit
31
+ ? text
32
+ : `[${text.length - limit} earlier characters omitted]\n${text.slice(-limit)}`;
33
+ }
34
+ /** runtime_exec, runtime_read_file, runtime_write_file and runtime_list_files, bound to `sandbox`. */
35
+ export function sandboxTools(sandbox, options = {}) {
36
+ const root = resolvePath("/", options.root ?? WORKSPACE);
37
+ const timeoutSeconds = options.timeoutSeconds ?? 300;
38
+ const limit = options.maxOutputChars ?? 20_000;
39
+ const inFilesApi = (path) => path.startsWith(`${WORKSPACE}/`);
40
+ return [
41
+ {
42
+ name: "runtime_exec",
43
+ description: "Run a shell command (bash) in the sandbox and return its exit code, stdout and stderr.",
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: {
47
+ command: {
48
+ type: "string",
49
+ description: "The command line, run under `bash -c`, for example `pytest -q`.",
50
+ },
51
+ cwd: {
52
+ type: "string",
53
+ description: "Directory to run in. Relative paths are under /workspace, the default.",
54
+ },
55
+ timeoutSeconds: {
56
+ type: "integer",
57
+ minimum: 1,
58
+ description: "Seconds before the command is stopped and reported as timed out.",
59
+ },
60
+ },
61
+ required: ["command"],
62
+ additionalProperties: false,
63
+ },
64
+ async execute({ command, cwd, timeoutSeconds: seconds }) {
65
+ const result = await sandbox.exec(command, {
66
+ cwd: resolvePath(root, cwd),
67
+ timeoutMs: (seconds ?? timeoutSeconds) * 1000,
68
+ });
69
+ return {
70
+ exitCode: result.exitCode,
71
+ stdout: clip(result.stdout, limit),
72
+ stderr: clip(result.stderr, limit),
73
+ timedOut: result.timedOut,
74
+ };
75
+ },
76
+ },
77
+ {
78
+ name: "runtime_read_file",
79
+ description: "Read a text file from the sandbox.",
80
+ inputSchema: {
81
+ type: "object",
82
+ properties: {
83
+ path: {
84
+ type: "string",
85
+ description: "The file's path. Relative paths are under /workspace.",
86
+ },
87
+ },
88
+ required: ["path"],
89
+ additionalProperties: false,
90
+ },
91
+ async execute({ path }) {
92
+ const target = resolvePath(root, path);
93
+ if (inFilesApi(target)) {
94
+ try {
95
+ return clip(await sandbox.files.readText(target), limit);
96
+ }
97
+ catch (error) {
98
+ if (error instanceof NotFoundError)
99
+ return `No such file: ${target}`;
100
+ throw error;
101
+ }
102
+ }
103
+ const result = await sandbox.exec(["cat", "--", target]);
104
+ return clip(result.exitCode === 0 ? result.stdout : result.stderr, limit);
105
+ },
106
+ },
107
+ {
108
+ name: "runtime_write_file",
109
+ description: "Create or replace a text file in the sandbox, making parent directories.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ path: {
114
+ type: "string",
115
+ description: "The file's path. Relative paths are under /workspace.",
116
+ },
117
+ content: { type: "string", description: "The file's full new content." },
118
+ },
119
+ required: ["path", "content"],
120
+ additionalProperties: false,
121
+ },
122
+ async execute({ path, content }) {
123
+ const target = resolvePath(root, path);
124
+ if (inFilesApi(target))
125
+ await sandbox.files.write(target, content);
126
+ else {
127
+ const result = await sandbox.exec(["sh", "-c", 'mkdir -p -- "$(dirname -- "$1")" && cat > "$1"', "sh", target], { stdin: content });
128
+ if (result.exitCode !== 0)
129
+ return `Could not write ${target}: ${result.stderr.trim()}`;
130
+ }
131
+ return `Wrote ${new TextEncoder().encode(content).length} bytes to ${target}`;
132
+ },
133
+ },
134
+ {
135
+ name: "runtime_list_files",
136
+ description: "List a directory in the sandbox: each entry's path, type and size.",
137
+ inputSchema: {
138
+ type: "object",
139
+ properties: {
140
+ path: {
141
+ type: "string",
142
+ description: "The directory. Relative paths are under /workspace, the default.",
143
+ },
144
+ depth: {
145
+ type: "integer",
146
+ minimum: 1,
147
+ description: "How many levels below it to include; 1 lists only its children.",
148
+ },
149
+ },
150
+ required: [],
151
+ additionalProperties: false,
152
+ },
153
+ async execute({ path, depth }) {
154
+ const levels = String(Math.max(1, Math.min(depth ?? 1, 10)));
155
+ const result = await sandbox.exec([
156
+ "sh",
157
+ "-c",
158
+ 'find "$1" -mindepth 1 -maxdepth "$2" -printf "%y\\t%s\\t%p\\n" | sort -k3 | head -n 1000',
159
+ "sh",
160
+ resolvePath(root, path),
161
+ levels,
162
+ ]);
163
+ if (result.exitCode !== 0 && !result.stdout)
164
+ return { error: result.stderr.trim() || `find exited with ${result.exitCode}` };
165
+ const kinds = {
166
+ f: "file",
167
+ d: "directory",
168
+ l: "symlink",
169
+ };
170
+ return result.stdout
171
+ .split("\n")
172
+ .filter(Boolean)
173
+ .map((line) => {
174
+ const [kind = "", size = "0", ...rest] = line.split("\t");
175
+ return { path: rest.join("\t"), type: kinds[kind] ?? "other", size: Number(size) || 0 };
176
+ });
177
+ },
178
+ },
179
+ ];
180
+ }
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.3.1";
2
+ export declare const VERSION = "0.5.0";
3
3
  export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
4
4
  export type RequestOptions = {
5
5
  /** Sent as Idempotency-Key. Omit it: the SDK makes one per call and keeps
@@ -25,6 +25,11 @@ export type Call = RequestOptions & {
25
25
  * reads and for writes (the key makes them safe); false only for calls
26
26
  * the server cannot deduplicate. */
27
27
  retry?: boolean;
28
+ /** How long to keep retrying, with the same key and input, a refusal that
29
+ * clears when a sandbox stops or a host frees room (WAITS_FOR_ROOM): a
30
+ * full trial, a full quota, a full region. Then the refusal is thrown as
31
+ * it came. Only a create sets it; 0 or absent fails at once. */
32
+ waitForCapacityMs?: number;
28
33
  };
29
34
  export declare function apiOrigin(value: string): string;
30
35
  export declare function encodeQuery(query: Query | undefined): string;
@@ -39,6 +44,8 @@ export declare function encodeQuery(query: Query | undefined): string;
39
44
  export declare class Transport {
40
45
  #private;
41
46
  readonly baseUrl: string;
47
+ /** What `sandboxes.create` waits for room by default. */
48
+ readonly waitForCapacityMs: number;
42
49
  constructor(options: {
43
50
  /** A key, or where to find one when the first call is made. */
44
51
  apiKey: string | (() => Promise<string>);
@@ -49,10 +56,13 @@ export declare class Transport {
49
56
  client?: string;
50
57
  /** Calls read whole that may be in flight at once. Default 32. */
51
58
  maxConnections?: number;
59
+ /** How long a create waits for room. Default 120 000; 0 fails at once. */
60
+ waitForCapacityMs?: number;
52
61
  });
53
62
  /** Sends a call and returns the raw Response once it succeeded. Retries
54
63
  * transport failures, 429, 502, 503 and 504 with backoff and jitter, with
55
- * the same key every time. */
64
+ * the same key every time, and waits out a full house for a call that sets
65
+ * waitForCapacityMs. */
56
66
  send(call: Call): Promise<Response>;
57
67
  json<T>(call: Call): Promise<T>;
58
68
  bytes(call: Call): Promise<Uint8Array>;