withruntime 0.5.1 → 0.6.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.
Files changed (47) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +8 -4
  3. package/dist/ai-harness/index.d.ts +194 -0
  4. package/dist/ai-harness/index.js +504 -0
  5. package/dist/cli-extras.d.ts +24 -0
  6. package/dist/cli-extras.js +205 -0
  7. package/dist/cli.d.ts +20 -0
  8. package/dist/cli.js +403 -28
  9. package/dist/e2b/filesystem.d.ts +26 -1
  10. package/dist/e2b/filesystem.js +30 -2
  11. package/dist/e2b/index.d.ts +1 -1
  12. package/dist/e2b/index.js +1 -1
  13. package/dist/errors.js +2 -0
  14. package/dist/identity.d.ts +27 -0
  15. package/dist/identity.js +56 -0
  16. package/dist/index.d.ts +14 -0
  17. package/dist/index.js +7 -0
  18. package/dist/mcp.js +16 -3
  19. package/dist/network-cli.d.ts +15 -0
  20. package/dist/network-cli.js +323 -0
  21. package/dist/products/desktop.d.ts +41 -4
  22. package/dist/products/desktop.js +73 -3
  23. package/dist/products/index.d.ts +12 -0
  24. package/dist/products/index.js +12 -0
  25. package/dist/products/interpreter.d.ts +5 -3
  26. package/dist/products/interpreter.js +2 -1
  27. package/dist/products/mcp.d.ts +79 -0
  28. package/dist/products/mcp.js +37 -0
  29. package/dist/products/mounts.d.ts +49 -0
  30. package/dist/products/mounts.js +21 -0
  31. package/dist/products/network-products.d.ts +167 -0
  32. package/dist/products/network-products.js +82 -0
  33. package/dist/products/network.d.ts +1 -1
  34. package/dist/products/sso.d.ts +44 -0
  35. package/dist/products/sso.js +8 -0
  36. package/dist/products/volumes.d.ts +68 -5
  37. package/dist/products/volumes.js +52 -4
  38. package/dist/products/watch.d.ts +91 -0
  39. package/dist/products/watch.js +135 -0
  40. package/dist/sandbox.d.ts +32 -2
  41. package/dist/sandbox.js +93 -16
  42. package/dist/snapshots.d.ts +11 -1
  43. package/dist/transport.d.ts +1 -1
  44. package/dist/transport.js +4 -1
  45. package/dist/wireguard.d.ts +7 -0
  46. package/dist/wireguard.js +13 -0
  47. package/package.json +49 -18
package/dist/sandbox.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { CommandError, RuntimeError } from "./errors.js";
2
2
  import { Page } from "./page.js";
3
+ import { sandboxWatches, watchDirectory, } from "./products/watch.js";
3
4
  import { sandboxFactories } from "./products/index.js";
4
5
  import { Tunnel } from "./tunnel.js";
5
6
  const CHUNK = 1_048_576;
@@ -274,6 +275,13 @@ export class Sandbox {
274
275
  async snapshot(options = {}) {
275
276
  const { idempotencyKey, signal, timeoutMs, ...body } = options;
276
277
  await this.refresh();
278
+ // Straight after a fork or a wake the sandbox is still `resuming`, and
279
+ // after a pause still `pausing`: wait for where it is going, or a
280
+ // snapshot of it is refused as not paused (user lane, 23 September 2026).
281
+ if (this.state === "resuming" || this.state === "starting")
282
+ await this.waitFor("running");
283
+ else if (this.state === "pausing")
284
+ await this.waitFor("paused");
277
285
  const running = this.state === "running";
278
286
  if (running)
279
287
  await this.pause();
@@ -552,6 +560,22 @@ export class Files {
552
560
  this.sandboxId = sandboxId;
553
561
  }
554
562
  #path = (suffix) => `/v1/sandboxes/${enc(this.sandboxId)}${suffix}`;
563
+ /** Watch a directory: `onEvent` gets create, write, remove, rename and
564
+ * chmod events until `handle.stop()`. E2B's `watchDir`, plus filters,
565
+ * batches, and a watch that loses nothing across a pause:
566
+ *
567
+ * const watch = await sbx.files.watch("/workspace", (e) => console.log(e.type, e.path),
568
+ * { recursive: true, exclude: ["node_modules"] });
569
+ * ...
570
+ * await watch.stop();
571
+ */
572
+ watch(path, onEvent, options) {
573
+ return watchDirectory(this.t, this.sandboxId, path, onEvent, options);
574
+ }
575
+ /** The watches running in the sandbox, and polling reads by cursor. */
576
+ get watches() {
577
+ return sandboxWatches(this.t, this.sandboxId);
578
+ }
555
579
  /** A file's bytes, any size. */
556
580
  async read(path, options = {}) {
557
581
  return this.t.bytes({
@@ -566,8 +590,11 @@ export class Files {
566
590
  return new TextDecoder().decode(await this.read(path, options));
567
591
  }
568
592
  /** Writes a file of any size, atomically, making parent directories. Large
569
- * ones go in parallel 1 MiB chunks checked against their SHA-256. */
570
- async write(path, data, options = {}) {
593
+ * ones go in parallel 1 MiB chunks checked against their SHA-256. `mode`
594
+ * sets its permissions (0o755 for a program); 0o644 when left out. */
595
+ async write(path, data, writeOptions = {}) {
596
+ const { mode, ...options } = writeOptions;
597
+ const octal = mode === undefined ? undefined : (mode & 0o777).toString(8).padStart(3, "0");
571
598
  const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
572
599
  if (bytes.length <= CHUNK) {
573
600
  // bytes(), not send(): the answer is read, so its connection is free
@@ -575,7 +602,7 @@ export class Files {
575
602
  await this.t.bytes({
576
603
  method: "PUT",
577
604
  path: this.#path("/files/content"),
578
- query: { path },
605
+ query: { path, ...(octal ? { mode: octal } : {}) },
579
606
  bytes,
580
607
  ...options,
581
608
  });
@@ -583,12 +610,55 @@ export class Files {
583
610
  }
584
611
  const { createHash } = await import("node:crypto");
585
612
  const sha256 = createHash("sha256").update(bytes).digest("hex");
586
- const begin = await this.t.json({
587
- method: "POST",
588
- path: this.#path("/uploads"),
589
- body: { path, size: bytes.length, sha256 },
613
+ // A guest journals mutations across operations, not per URL. One logical
614
+ // write therefore owns separate stable keys for each mutation phase.
615
+ const uploadKey = options.idempotencyKey ?? crypto.randomUUID();
616
+ const phaseOptions = (phase) => ({
590
617
  ...options,
618
+ idempotencyKey: createHash("sha256")
619
+ .update(JSON.stringify(["files.write", uploadKey, phase]))
620
+ .digest("hex"),
621
+ });
622
+ let begin;
623
+ try {
624
+ begin = await this.t.json({
625
+ method: "POST",
626
+ path: this.#path("/uploads"),
627
+ body: { path, size: bytes.length, sha256, ...(octal ? { mode: octal } : {}) },
628
+ ...phaseOptions("begin"),
629
+ });
630
+ }
631
+ catch (error) {
632
+ if (!octal || !(error instanceof RuntimeError) || error.code !== "guest_upgrade_required")
633
+ throw error;
634
+ // The API aborted the unsupported transfer before accepting chunks.
635
+ // A different body needs a different key; retain deterministic retries.
636
+ begin = await this.t.json({
637
+ method: "POST",
638
+ path: this.#path("/uploads"),
639
+ body: { path, size: bytes.length, sha256 },
640
+ ...phaseOptions("legacy-begin"),
641
+ });
642
+ }
643
+ const commit = () => this.t.json({
644
+ method: "POST",
645
+ path: this.#path(`/uploads/${enc(begin.uploadId)}:commit`),
646
+ body: {},
647
+ ...phaseOptions("commit"),
591
648
  });
649
+ let committed = false;
650
+ if (begin.replayed) {
651
+ // A completed commit removed the transfer. Ask its journal before
652
+ // resending chunks; an incomplete transfer alone is safe to continue.
653
+ try {
654
+ await commit();
655
+ committed = true;
656
+ }
657
+ catch (error) {
658
+ if (!(error instanceof RuntimeError) || error.code !== "upload_incomplete")
659
+ throw error;
660
+ }
661
+ }
592
662
  const offsets = [];
593
663
  for (let offset = 0; offset < bytes.length; offset += begin.chunkBytes)
594
664
  offsets.push(offset);
@@ -605,13 +675,18 @@ export class Files {
605
675
  }
606
676
  };
607
677
  try {
608
- await Promise.all(Array.from({ length: Math.min(PARALLEL, offsets.length) }, worker));
609
- await this.t.json({
610
- method: "POST",
611
- path: this.#path(`/uploads/${enc(begin.uploadId)}:commit`),
612
- body: {},
613
- ...options,
614
- });
678
+ if (!committed) {
679
+ await Promise.all(Array.from({ length: Math.min(PARALLEL, offsets.length) }, worker));
680
+ await commit();
681
+ }
682
+ // An older sandbox image ignores the mode in the upload; set it after.
683
+ if (octal && begin.mode !== octal)
684
+ await this.t.json({
685
+ method: "POST",
686
+ path: this.#path("/files:chmod"),
687
+ body: { path, mode: octal },
688
+ ...phaseOptions("chmod"),
689
+ });
615
690
  }
616
691
  catch (error) {
617
692
  await this.t
@@ -619,6 +694,7 @@ export class Files {
619
694
  method: "POST",
620
695
  path: this.#path(`/uploads/${enc(begin.uploadId)}:abort`),
621
696
  body: {},
697
+ ...phaseOptions("abort"),
622
698
  })
623
699
  .catch(() => undefined);
624
700
  throw error;
@@ -669,7 +745,8 @@ export class Files {
669
745
  const { stat, readFile } = await import("node:fs/promises");
670
746
  const info = await stat(localPath);
671
747
  if (info.isFile()) {
672
- await this.write(remotePath, await readFile(localPath));
748
+ // Its permissions travel with it: an uploaded script stays runnable.
749
+ await this.write(remotePath, await readFile(localPath), { mode: info.mode & 0o777 });
673
750
  return;
674
751
  }
675
752
  const { packDirectory } = await import("./tar.js");
@@ -681,7 +758,7 @@ export class Files {
681
758
  const result = await new Sandbox(this.t, { id: this.sandboxId }).exec([
682
759
  "sh",
683
760
  "-c",
684
- 'mkdir -p "$1" && tar -xzf "$2" -C "$1"; code=$?; rm -f "$2"; exit $code',
761
+ 'mkdir -p "$1" && tar -xpzf "$2" -C "$1"; code=$?; rm -f "$2"; exit $code',
685
762
  "sh",
686
763
  remotePath,
687
764
  staging,
@@ -1,7 +1,8 @@
1
1
  import { Page } from "./page.js";
2
2
  import type { RequestOptions, Transport } from "./transport.js";
3
3
  /** A sandbox's whole machine (files, memory, running processes), kept on its
4
- * host to start new sandboxes from. */
4
+ * host to start new sandboxes from, and copied off the host so it survives
5
+ * losing that host. */
5
6
  export type Snapshot = {
6
7
  id: string;
7
8
  kind: "snapshot";
@@ -19,6 +20,15 @@ export type Snapshot = {
19
20
  /** What it stores, and what storage is metered for: the bytes it alone holds. */
20
21
  storedBytes: number | null;
21
22
  meteredBytes: number | null;
23
+ /** Whether its copy off the host is made and checked. */
24
+ backedUp: boolean;
25
+ /** Where that copy stands. `restoring`: its host was lost and it is being
26
+ * restored onto another; forks answer `snapshot_restoring` until it is. */
27
+ durability: {
28
+ state: "none" | "pending" | "durable" | "failed" | "restoring";
29
+ durableAt: string | null;
30
+ storedBytes: number | null;
31
+ };
22
32
  error: string | null;
23
33
  createdAt: string;
24
34
  readyAt: string | null;
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.5.1";
2
+ export declare const VERSION = "0.6.1";
3
3
  export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
4
4
  export type RequestOptions = {
5
5
  /** Sent as Idempotency-Key. Omit it: the SDK makes one per call and keeps
package/dist/transport.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ConnectionError, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
2
2
  import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
3
- export const VERSION = "0.5.1";
3
+ export const VERSION = "0.6.1";
4
4
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
5
5
  export function apiOrigin(value) {
6
6
  const url = new URL(value);
@@ -289,6 +289,9 @@ export class Transport {
289
289
  yield JSON.parse(buffer);
290
290
  }
291
291
  finally {
292
+ // Returning early from the iterator must close the HTTP stream too.
293
+ // Cleanup must not replace a parse, network or callback error.
294
+ await reader.cancel().catch(() => undefined);
292
295
  reader.releaseLock();
293
296
  }
294
297
  }
@@ -0,0 +1,7 @@
1
+ /** A WireGuard key pair, as `wg genkey | wg pubkey` makes: X25519, each half
2
+ * 32 bytes of base64. Made on the machine that will use it, so the private
3
+ * key is never sent anywhere. */
4
+ export declare function generateWireGuardKeyPair(): {
5
+ privateKey: string;
6
+ publicKey: string;
7
+ };
@@ -0,0 +1,13 @@
1
+ import { generateKeyPairSync } from "node:crypto";
2
+ /** A WireGuard key pair, as `wg genkey | wg pubkey` makes: X25519, each half
3
+ * 32 bytes of base64. Made on the machine that will use it, so the private
4
+ * key is never sent anywhere. */
5
+ export function generateWireGuardKeyPair() {
6
+ const { privateKey, publicKey } = generateKeyPairSync("x25519");
7
+ const d = privateKey.export({ format: "jwk" }).d;
8
+ const x = publicKey.export({ format: "jwk" }).x;
9
+ return {
10
+ privateKey: Buffer.from(d, "base64url").toString("base64"),
11
+ publicKey: Buffer.from(x, "base64url").toString("base64"),
12
+ };
13
+ }
package/package.json CHANGED
@@ -1,44 +1,59 @@
1
1
  {
2
2
  "name": "withruntime",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
+ "license": "Apache-2.0",
4
5
  "mcpName": "com.withruntime/runtime",
5
6
  "type": "module",
6
7
  "exports": {
7
8
  ".": {
8
9
  "types": "./dist/index.d.ts",
9
- "import": "./dist/index.js"
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
10
12
  },
11
13
  "./e2b": {
12
14
  "types": "./dist/e2b/index.d.ts",
13
- "import": "./dist/e2b/index.js"
15
+ "import": "./dist/e2b/index.js",
16
+ "default": "./dist/e2b/index.js"
14
17
  },
15
18
  "./e2b/code-interpreter": {
16
19
  "types": "./dist/e2b/code-interpreter.d.ts",
17
- "import": "./dist/e2b/code-interpreter.js"
20
+ "import": "./dist/e2b/code-interpreter.js",
21
+ "default": "./dist/e2b/code-interpreter.js"
18
22
  },
19
23
  "./daytona": {
20
24
  "types": "./dist/daytona/index.d.ts",
21
- "import": "./dist/daytona/index.js"
25
+ "import": "./dist/daytona/index.js",
26
+ "default": "./dist/daytona/index.js"
22
27
  },
23
28
  "./vercel": {
24
29
  "types": "./dist/vercel/index.d.ts",
25
- "import": "./dist/vercel/index.js"
30
+ "import": "./dist/vercel/index.js",
31
+ "default": "./dist/vercel/index.js"
26
32
  },
27
33
  "./tools": {
28
34
  "types": "./dist/tools/index.d.ts",
29
- "import": "./dist/tools/index.js"
35
+ "import": "./dist/tools/index.js",
36
+ "default": "./dist/tools/index.js"
30
37
  },
31
38
  "./ai": {
32
39
  "types": "./dist/ai/index.d.ts",
33
- "import": "./dist/ai/index.js"
40
+ "import": "./dist/ai/index.js",
41
+ "default": "./dist/ai/index.js"
34
42
  },
35
43
  "./claude-agent-sdk": {
36
44
  "types": "./dist/claude-agent-sdk/index.d.ts",
37
- "import": "./dist/claude-agent-sdk/index.js"
45
+ "import": "./dist/claude-agent-sdk/index.js",
46
+ "default": "./dist/claude-agent-sdk/index.js"
38
47
  },
39
48
  "./openai-agents": {
40
49
  "types": "./dist/openai-agents/index.d.ts",
41
- "import": "./dist/openai-agents/index.js"
50
+ "import": "./dist/openai-agents/index.js",
51
+ "default": "./dist/openai-agents/index.js"
52
+ },
53
+ "./ai-harness": {
54
+ "types": "./dist/ai-harness/index.d.ts",
55
+ "import": "./dist/ai-harness/index.js",
56
+ "default": "./dist/ai-harness/index.js"
42
57
  }
43
58
  },
44
59
  "bin": {
@@ -50,7 +65,8 @@
50
65
  "README.md",
51
66
  "E2B.md",
52
67
  "DAYTONA.md",
53
- "VERCEL.md"
68
+ "VERCEL.md",
69
+ "LICENSE"
54
70
  ],
55
71
  "engines": {
56
72
  "node": ">=22"
@@ -60,23 +76,34 @@
60
76
  "prepack": "bun run build",
61
77
  "check-types": "tsc --noEmit",
62
78
  "lint": "eslint",
63
- "test": "bun test && bun scripts/test-python.ts && bun scripts/test-go.ts",
79
+ "test": "bun test && bun scripts/test-python.ts && bun scripts/test-go.ts && bun ../../sdks/java/scripts/check.ts && bun ../../sdks/ruby/scripts/check.ts",
80
+ "test:java": "bun ../../sdks/java/scripts/check.ts",
81
+ "test:ruby": "bun ../../sdks/ruby/scripts/check.ts",
64
82
  "test:python": "bun scripts/test-python.ts",
65
83
  "test:go": "bun scripts/test-go.ts"
66
84
  },
67
85
  "devDependencies": {
86
+ "@ai-sdk/harness": "1.0.124",
87
+ "@anthropic-ai/claude-agent-sdk": "0.3.277",
88
+ "@anthropic-ai/sdk": "0.128.0",
89
+ "@google/genai": "2.24.0",
90
+ "@mastra/core": "1.71.0",
91
+ "@mistralai/mistralai": "2.7.0",
92
+ "@modelcontextprotocol/sdk": "^1.30.0",
93
+ "@openai/agents": "0.18.0",
94
+ "@openai/agents-core": "0.18.0",
95
+ "@openrouter/sdk": "1.3.27",
68
96
  "@runtime/eslint-config": "workspace:*",
69
97
  "@runtime/typescript-config": "workspace:*",
70
98
  "@types/bun": "^1.4.0",
71
99
  "@types/node": "^20",
100
+ "ai": "7.0.87",
72
101
  "eslint": "^9",
102
+ "groq-sdk": "1.6.0",
103
+ "openai": "7.23.0",
104
+ "together-ai": "0.55.0",
73
105
  "typescript": "^5",
74
- "@openai/agents": "0.18.0",
75
- "@openai/agents-core": "0.18.0",
76
- "@anthropic-ai/claude-agent-sdk": "0.3.277",
77
- "ai": "7.0.87",
78
- "zod": "^4.1.8",
79
- "@modelcontextprotocol/sdk": "^1.30.0"
106
+ "zod": "^4.1.8"
80
107
  },
81
108
  "publishConfig": {
82
109
  "access": "public",
@@ -96,6 +123,7 @@
96
123
  "undici": "^7.29.1"
97
124
  },
98
125
  "peerDependencies": {
126
+ "@ai-sdk/harness": "^1.0.0",
99
127
  "@anthropic-ai/claude-agent-sdk": ">=0.2.0",
100
128
  "@openai/agents": ">=0.18.0",
101
129
  "@openai/agents-core": ">=0.18.0",
@@ -103,6 +131,9 @@
103
131
  "zod": ">=3.25.0"
104
132
  },
105
133
  "peerDependenciesMeta": {
134
+ "@ai-sdk/harness": {
135
+ "optional": true
136
+ },
106
137
  "@anthropic-ai/claude-agent-sdk": {
107
138
  "optional": true
108
139
  },