withruntime 0.8.4 → 0.9.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 (91) hide show
  1. package/COMPATIBILITY.md +145 -0
  2. package/E2B.md +23 -13
  3. package/README.md +8 -5
  4. package/README.src.md +8 -5
  5. package/compatibility-lock.json +281 -0
  6. package/dist/ai-harness/index.js +2 -2
  7. package/dist/api-defaults.d.ts +17 -0
  8. package/dist/api-defaults.js +22 -0
  9. package/dist/blaxel/preview.d.ts +1 -1
  10. package/dist/blaxel/preview.js +1 -1
  11. package/dist/blaxel/process.d.ts +1 -1
  12. package/dist/blaxel/process.js +11 -2
  13. package/dist/cli-words.d.ts +27 -0
  14. package/dist/cli-words.js +135 -0
  15. package/dist/cli.js +162 -31
  16. package/dist/client.d.ts +4 -1
  17. package/dist/client.js +8 -2
  18. package/dist/cloudflare/index.d.ts +294 -0
  19. package/dist/cloudflare/index.js +554 -0
  20. package/dist/cloudflare/worker.d.ts +415 -0
  21. package/dist/cloudflare/worker.js +297 -0
  22. package/dist/codesandbox/guest-service.d.ts +31 -0
  23. package/dist/codesandbox/guest-service.js +453 -0
  24. package/dist/codesandbox/index.d.ts +386 -0
  25. package/dist/codesandbox/index.js +1313 -0
  26. package/dist/compat/core.d.ts +39 -0
  27. package/dist/compat/core.js +199 -0
  28. package/dist/compat/files.d.ts +26 -0
  29. package/dist/compat/files.js +115 -0
  30. package/dist/compat/shell.d.ts +4 -0
  31. package/dist/compat/shell.js +165 -0
  32. package/dist/daytona/context.js +2 -1
  33. package/dist/daytona/process.d.ts +1 -1
  34. package/dist/daytona/process.js +27 -4
  35. package/dist/e2b/code-interpreter.d.ts +4 -5
  36. package/dist/e2b/code-interpreter.js +54 -56
  37. package/dist/e2b/commands.d.ts +5 -4
  38. package/dist/e2b/commands.js +37 -29
  39. package/dist/e2b/errors.js +1 -1
  40. package/dist/e2b/filesystem.d.ts +8 -6
  41. package/dist/e2b/filesystem.js +127 -28
  42. package/dist/e2b/sandbox.d.ts +4 -3
  43. package/dist/e2b/sandbox.js +10 -10
  44. package/dist/errors.d.ts +8 -0
  45. package/dist/errors.js +15 -2
  46. package/dist/freestyle/index.d.ts +301 -0
  47. package/dist/freestyle/index.js +310 -0
  48. package/dist/index.d.ts +1 -0
  49. package/dist/mcp.js +2 -2
  50. package/dist/modal/index.d.ts +205 -0
  51. package/dist/modal/index.js +485 -0
  52. package/dist/network-cli.js +1 -1
  53. package/dist/openai-agents/index.js +9 -26
  54. package/dist/products/audit.d.ts +3 -0
  55. package/dist/products/index.d.ts +2 -0
  56. package/dist/products/index.js +2 -0
  57. package/dist/products/interpreter.d.ts +11 -4
  58. package/dist/products/interpreter.js +33 -9
  59. package/dist/products/jobs.js +3 -2
  60. package/dist/products/limits.d.ts +10 -0
  61. package/dist/products/previews.d.ts +10 -2
  62. package/dist/products/previews.js +3 -2
  63. package/dist/products/tailscale.d.ts +57 -0
  64. package/dist/products/tailscale.js +17 -0
  65. package/dist/products/watch.d.ts +25 -4
  66. package/dist/products/watch.js +75 -14
  67. package/dist/runloop/index.d.ts +340 -0
  68. package/dist/runloop/index.js +438 -0
  69. package/dist/runloop/pagination.d.ts +46 -0
  70. package/dist/runloop/pagination.js +82 -0
  71. package/dist/runloop/snapshots.d.ts +52 -0
  72. package/dist/runloop/snapshots.js +244 -0
  73. package/dist/sandbox.d.ts +40 -9
  74. package/dist/sandbox.js +273 -76
  75. package/dist/snapshots.d.ts +11 -1
  76. package/dist/snapshots.js +11 -1
  77. package/dist/sprites/index.d.ts +251 -0
  78. package/dist/sprites/index.js +552 -0
  79. package/dist/tar.d.ts +8 -1
  80. package/dist/tar.js +185 -35
  81. package/dist/tools/index.js +2 -1
  82. package/dist/transport.d.ts +20 -2
  83. package/dist/transport.js +48 -11
  84. package/dist/types.d.ts +52 -3
  85. package/dist/vercel/command.js +12 -1
  86. package/dist/vercel/errors.d.ts +2 -2
  87. package/dist/vercel/errors.js +12 -1
  88. package/dist/vercel/filesystem.d.ts +21 -18
  89. package/dist/vercel/filesystem.js +95 -48
  90. package/dist/vercel/sandbox.js +5 -5
  91. package/package.json +45 -3
@@ -1,3 +1,4 @@
1
+ import { RuntimeError } from "../errors.js";
1
2
  /** A stateful interpreter in the sandbox, in Python, JavaScript, TypeScript,
2
3
  * R, Java, Bash or Go:
3
4
  * `await sbx.interpreter.run("import pandas as pd; pd.DataFrame({'a': [1]})")`.
@@ -7,33 +8,56 @@ export function sandboxInterpreter(t, sandbox) {
7
8
  return {
8
9
  /** Run a cell. With any of the on* callbacks, output streams as it happens. */
9
10
  async run(code, options = {}) {
10
- const { language, context, timeoutMs, onStdout, onStderr, onResult, onError, ...request } = options;
11
+ const { language, context, timeoutMs, requestTimeoutMs, interruptOnDisconnect, onStdout, onStderr, onResult, onError, ...request } = options;
12
+ const transportOptions = {
13
+ ...request,
14
+ ...(requestTimeoutMs !== undefined
15
+ ? { timeoutMs: requestTimeoutMs }
16
+ : timeoutMs !== undefined
17
+ ? { timeoutMs: timeoutMs === 0 ? 0 : timeoutMs + 60_000 }
18
+ : {}),
19
+ };
11
20
  const body = {
12
21
  code,
13
22
  ...(language ? { language } : {}),
14
23
  ...(context ? { context } : {}),
15
- ...(timeoutMs ? { timeoutMs } : {}),
24
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
25
+ ...(interruptOnDisconnect !== undefined ? { interruptOnDisconnect } : {}),
16
26
  };
17
27
  if (!onStdout && !onStderr && !onResult && !onError)
18
- return t.json({ method: "POST", path: `${base()}:run`, body, ...request });
28
+ return t.json({
29
+ method: "POST",
30
+ path: `${base()}:run`,
31
+ body,
32
+ ...transportOptions,
33
+ });
19
34
  for await (const event of t.events({
20
35
  method: "POST",
21
36
  path: `${base()}:run`,
22
37
  body: { ...body, stream: true },
23
- ...request,
38
+ ...transportOptions,
24
39
  })) {
25
40
  if (event.k === "stdout")
26
- onStdout?.(event.text);
41
+ await onStdout?.(event.text);
27
42
  else if (event.k === "stderr")
28
- onStderr?.(event.text);
43
+ await onStderr?.(event.text);
29
44
  else if (event.k === "result")
30
- onResult?.({ main: event.main, data: event.data, refs: event.refs });
45
+ await onResult?.({ main: event.main, data: event.data, refs: event.refs });
31
46
  else if (event.k === "error")
32
- onError?.(event);
47
+ await onError?.(event);
33
48
  else if (event.k === "execution")
34
49
  return event.execution;
35
50
  else if (event.k === "failure")
36
- throw new Error(`${event.code}: ${event.message}`);
51
+ // As every other refusal: its code, words, hint and request id,
52
+ // printed by the CLI as it prints the rest (28 September 2026:
53
+ // "Error: forbidden: cloud forbidden", with no hint and no id).
54
+ throw new RuntimeError({
55
+ code: event.code,
56
+ status: event.status ?? 0,
57
+ message: event.message,
58
+ ...(event.hint ? { hint: event.hint } : {}),
59
+ ...(event.requestId ? { requestId: event.requestId } : {}),
60
+ });
37
61
  }
38
62
  throw new Error("The interpreter stream ended without a result");
39
63
  },
@@ -1,3 +1,4 @@
1
+ import { JOB_START_SECONDS, JOB_TIMEOUT_SECONDS } from "../api-defaults.js";
1
2
  import { Page } from "../page.js";
2
3
  import { RuntimeError, ServiceUnavailableError } from "../errors.js";
3
4
  /** The size a run gets when `compute` leaves a field out: the sandbox
@@ -9,9 +10,9 @@ export const JOB_DEFAULTS = {
9
10
  cpuFloorMillis: 50,
10
11
  memoryMiB: 4096,
11
12
  diskMiB: 4096,
12
- timeoutSeconds: 1800,
13
+ timeoutSeconds: JOB_TIMEOUT_SECONDS,
13
14
  /** Added to the timeout for the sandbox to start, within the hour. */
14
- startSeconds: 60,
15
+ startSeconds: JOB_START_SECONDS,
15
16
  };
16
17
  /** A run is one sandbox: at most 16 vCPU and 64 GiB, a disk no smaller than
17
18
  * the system image, and paid for at most an hour. */
@@ -14,6 +14,16 @@ export type KeyLimits = {
14
14
  remainingMicros: string | null;
15
15
  window: "24h";
16
16
  };
17
+ /** The account's free trial time in milliseconds, the same figures as
18
+ * `usage.get()`, or null when the account has no trial. A trial sandbox
19
+ * that has not ended holds its whole lease in `reservedMs`;
20
+ * `availableMs = totalMs - usedMs - reservedMs`. */
21
+ trial: {
22
+ totalMs: number;
23
+ usedMs: number;
24
+ reservedMs: number;
25
+ availableMs: number;
26
+ } | null;
17
27
  };
18
28
  /** `runtime.limits.get()`: whether this key is read-only, and what its agent
19
29
  * may still spend today. An owner sets the limit at
@@ -17,6 +17,8 @@ export type Preview = {
17
17
  urlWithToken: string | null;
18
18
  /** Turned off by Runtime after a report. */
19
19
  disabled: boolean;
20
+ /** The sites that may show it in an iframe; null for any site. */
21
+ embedOrigins: string[] | null;
20
22
  createdAt: string;
21
23
  /** On create only: how to share the address, in one line. */
22
24
  hint?: string;
@@ -27,6 +29,11 @@ export type CreatePreview = {
27
29
  visibility?: "private" | "public";
28
30
  /** How long the returned token lasts, 60 s to 7 days (default 1 day). */
29
31
  ttlSeconds?: number;
32
+ /** The sites that may show it in an iframe, as origins
33
+ * (`https://app.example.com`, `https://*.example.com`, `http://localhost:3000`),
34
+ * up to 16. Any other site's iframe is refused. Omitted: a shared port keeps
35
+ * its list; `[]` or null: any site, the default. */
36
+ embedOrigins?: string[] | null;
30
37
  };
31
38
  /** `sbx.previews`: share ports of this sandbox at public HTTPS addresses.
32
39
  *
@@ -35,7 +42,8 @@ export type CreatePreview = {
35
42
  *
36
43
  * WebSockets work. The server must listen on 0.0.0.0 or localhost. */
37
44
  export declare function sandboxPreviews(t: Transport, sandbox: Sandbox): {
38
- /** Shares `port`, or changes its visibility if it is shared already. */
45
+ /** Shares `port`, or changes its visibility and embedOrigins if it is
46
+ * shared already. */
39
47
  create: (port: number, input?: CreatePreview, options?: RequestOptions) => Promise<Preview>;
40
48
  /** Every shared port, each private one with a fresh token. */
41
49
  list: (options?: RequestOptions) => Promise<Preview[]>;
@@ -43,7 +51,7 @@ export declare function sandboxPreviews(t: Transport, sandbox: Sandbox): {
43
51
  get: (port: number, ttlSeconds?: number, options?: RequestOptions) => Promise<Preview>;
44
52
  /** Refuses every token issued for this port so far and returns a new one. */
45
53
  rotate: (port: number, options?: RequestOptions) => Promise<Preview>;
46
- /** Stops sharing `port`. Open connections close within seconds. */
54
+ /** Stops sharing `port`. Its open connections are closed before this returns. */
47
55
  delete: (port: number, options?: RequestOptions) => Promise<{
48
56
  id: string;
49
57
  sandboxId: string;
@@ -7,7 +7,8 @@
7
7
  export function sandboxPreviews(t, sandbox) {
8
8
  const base = () => `/v1/sandboxes/${encodeURIComponent(sandbox.id)}/previews`;
9
9
  return {
10
- /** Shares `port`, or changes its visibility if it is shared already. */
10
+ /** Shares `port`, or changes its visibility and embedOrigins if it is
11
+ * shared already. */
11
12
  create: (port, input = {}, options) => t.json({ method: "POST", path: base(), body: { port, ...input }, ...options }),
12
13
  /** Every shared port, each private one with a fresh token. */
13
14
  list: async (options) => (await t.json({ method: "GET", path: base(), ...options })).data,
@@ -20,7 +21,7 @@ export function sandboxPreviews(t, sandbox) {
20
21
  }),
21
22
  /** Refuses every token issued for this port so far and returns a new one. */
22
23
  rotate: (port, options) => t.json({ method: "POST", path: `${base()}/${port}:rotate`, ...options }),
23
- /** Stops sharing `port`. Open connections close within seconds. */
24
+ /** Stops sharing `port`. Its open connections are closed before this returns. */
24
25
  delete: (port, options) => t.json({
25
26
  method: "DELETE",
26
27
  path: `${base()}/${port}`,
@@ -0,0 +1,57 @@
1
+ import type { Sandbox } from "../sandbox.js";
2
+ import type { RequestOptions, Transport } from "../transport.js";
3
+ /** Joining a sandbox to your tailnet. */
4
+ export type TailscaleJoin = {
5
+ /** The name of a secret stored for jobs (`runtime secrets set NAME --jobs`)
6
+ * that holds a Tailscale auth key. Use an ephemeral, tagged key. */
7
+ authKeySecret: string;
8
+ /** The machine's name on the tailnet, a DNS label. Default: the sandbox's
9
+ * name, or runtime- and the first part of its id. */
10
+ hostname?: string;
11
+ /** Tags to advertise, such as `tag:agents`; the key must be allowed them. */
12
+ tags?: string[];
13
+ };
14
+ /** A sandbox's machine on your tailnet, as the sandbox reports it. */
15
+ export type TailscaleNode = {
16
+ /** Logged in and running now. */
17
+ running: boolean;
18
+ /** Tailscale's own word: Running, NeedsLogin, Starting, Stopped. */
19
+ state: string | null;
20
+ /** kernel: a tailscale0 interface reaches 100.x directly. userspace: the
21
+ * tailnet reaches the sandbox's ports, and programs in it reach the tailnet
22
+ * through `proxy`. */
23
+ mode: "kernel" | "userspace" | null;
24
+ /** Its MagicDNS name, such as agent-1.tail1234.ts.net. */
25
+ dnsName: string | null;
26
+ /** Its tailnet addresses, 100.x and fd7a:… */
27
+ addresses: string[];
28
+ /** SOCKS5 and HTTP proxy to the tailnet in userspace mode. */
29
+ proxy: string | null;
30
+ };
31
+ /** What a join answers: the node, and what Runtime recorded. */
32
+ export type TailscaleJoined = TailscaleNode & {
33
+ hostname: string;
34
+ tags: string[];
35
+ /** The secret the key was read from; never the key. */
36
+ authKeySecret: string | null;
37
+ joinedAt: number;
38
+ };
39
+ /** What `status()` answers: the node, and the join, or null on none. */
40
+ export type TailscaleStatus = TailscaleNode & {
41
+ tailnet: Pick<TailscaleJoined, "hostname" | "tags" | "authKeySecret" | "joinedAt"> | null;
42
+ };
43
+ /** `sbx.tailscale`: this sandbox on your own Tailscale network. Paid
44
+ * sandboxes only.
45
+ *
46
+ * const node = await sbx.tailscale.up({ authKeySecret: "TS_AUTHKEY", tags: ["tag:agents"] });
47
+ * console.log(node.addresses);
48
+ *
49
+ * The auth key stays in the sandbox's memory. `down()` takes the machine off
50
+ * the tailnet at once; after a stop or a delete, Tailscale removes it. */
51
+ export declare function sandboxTailscale(t: Transport, sandbox: Sandbox): {
52
+ up: (input: TailscaleJoin, options?: RequestOptions) => Promise<TailscaleJoined>;
53
+ status: (options?: RequestOptions) => Promise<TailscaleStatus>;
54
+ down: (options?: RequestOptions) => Promise<{
55
+ left: boolean;
56
+ }>;
57
+ };
@@ -0,0 +1,17 @@
1
+ const enc = encodeURIComponent;
2
+ /** `sbx.tailscale`: this sandbox on your own Tailscale network. Paid
3
+ * sandboxes only.
4
+ *
5
+ * const node = await sbx.tailscale.up({ authKeySecret: "TS_AUTHKEY", tags: ["tag:agents"] });
6
+ * console.log(node.addresses);
7
+ *
8
+ * The auth key stays in the sandbox's memory. `down()` takes the machine off
9
+ * the tailnet at once; after a stop or a delete, Tailscale removes it. */
10
+ export function sandboxTailscale(t, sandbox) {
11
+ const path = () => `/v1/sandboxes/${enc(sandbox.id)}/tailscale`;
12
+ return {
13
+ up: (input, options) => t.json({ method: "POST", path: path(), body: input, ...options }),
14
+ status: (options) => t.json({ method: "GET", path: path(), ...options }),
15
+ down: (options) => t.json({ method: "DELETE", path: path(), ...options }),
16
+ };
17
+ }
@@ -1,5 +1,5 @@
1
1
  import type { RequestOptions, Transport } from "../transport.js";
2
- export type FileEventType = "create" | "write" | "remove" | "rename" | "chmod";
2
+ export type FileEventType = "create" | "write" | "remove" | "rename" | "chmod" | "access";
3
3
  export type FileEvent = {
4
4
  type: FileEventType;
5
5
  /** Absolute path in the sandbox. */
@@ -23,7 +23,7 @@ export type WatchNotice = {
23
23
  };
24
24
  export type WatchOptions = Omit<RequestOptions, "timeoutMs"> & {
25
25
  recursive?: boolean;
26
- /** Only these event types; all five by default. */
26
+ /** Only these event types; the five mutation types by default; access is opt-in. */
27
27
  events?: FileEventType[];
28
28
  /** Globs relative to the path: only matching paths are reported. */
29
29
  include?: string[];
@@ -31,7 +31,7 @@ export type WatchOptions = Omit<RequestOptions, "timeoutMs"> & {
31
31
  exclude?: string[];
32
32
  /** Events are gathered this long and delivered together. Default 100. */
33
33
  batchMs?: number;
34
- /** The watch ends by itself after running this long. Default 1 hour. */
34
+ /** The watch ends by itself after running this long. Default 1 hour; 0 runs until stopped or the sandbox ends. */
35
35
  timeoutMs?: number;
36
36
  maxWatches?: number;
37
37
  /** A batch at a time, rather than one call per event. */
@@ -49,6 +49,16 @@ export type WatchInfo = {
49
49
  processId: string;
50
50
  state: string;
51
51
  startedAt: number;
52
+ /** Its changes go to the account's webhooks as `sandbox.files.changed`. */
53
+ webhook?: boolean;
54
+ };
55
+ /** A watch whose changes Runtime sends to the account's webhooks. */
56
+ export type WebhookWatchOptions = Omit<WatchOptions, "onBatch" | "onNotice" | "onExit" | "timeoutMs"> & {
57
+ /** The watch ends by itself after running this long; 0 (the default here)
58
+ * runs until stopped or the sandbox ends. */
59
+ timeoutMs?: number;
60
+ /** Its own id, 1 to 40 lowercase letters, digits or dashes. */
61
+ id?: string;
52
62
  };
53
63
  /** A running watch. Events arrive through the callbacks until `stop()`. */
54
64
  export declare class WatchHandle {
@@ -65,7 +75,9 @@ export declare class WatchHandle {
65
75
  get done(): Promise<void>;
66
76
  /** Start delivering again from where it left off, after a pause. */
67
77
  resume(): void;
68
- /** Stop the watch in the sandbox, and stop delivering. */
78
+ /** Stop the watch in the sandbox. What changed before the stop is still
79
+ * delivered: the watch reports it and ends, and this returns once that has
80
+ * arrived (at most 5 s). Called from inside a callback, it stops at once. */
69
81
  stop(options?: RequestOptions): Promise<void>;
70
82
  /** @internal */
71
83
  begin(): this;
@@ -85,6 +97,15 @@ export declare function sandboxWatches(t: Transport, sandboxId: string): {
85
97
  lostBytes: number;
86
98
  ended: boolean;
87
99
  }>;
100
+ /** Starts a watch whose changes Runtime sends to the account's webhooks
101
+ * as `sandbox.files.changed` events, with nothing reading it here:
102
+ *
103
+ * await sbx.files.watches.webhook("/workspace/app", { recursive: true });
104
+ *
105
+ * Runs until stopped (timeoutMs 0) unless given a timeout. */
106
+ webhook: (path: string, input?: WebhookWatchOptions) => Promise<WatchInfo & {
107
+ cursor: number;
108
+ }>;
88
109
  stop: (id: string, options?: RequestOptions) => Promise<{
89
110
  stopped: boolean;
90
111
  }>;
@@ -1,3 +1,5 @@
1
+ import { WEBHOOK_WATCH_TIMEOUT_MS } from "../api-defaults.js";
2
+ import { RuntimeError } from "../errors.js";
1
3
  /** A running watch. Events arrive through the callbacks until `stop()`. */
2
4
  export class WatchHandle {
3
5
  t;
@@ -8,6 +10,11 @@ export class WatchHandle {
8
10
  onEvent;
9
11
  cursor;
10
12
  #stopped = false;
13
+ /** stop() was called: deliver what the watch reports until its end. */
14
+ #stopping = false;
15
+ /** Callbacks under way. */
16
+ #delivering = 0;
17
+ #exitNotified = false;
11
18
  #following;
12
19
  #abort = new AbortController();
13
20
  constructor(t, base, id, path, cursor, options, onEvent) {
@@ -25,22 +32,42 @@ export class WatchHandle {
25
32
  }
26
33
  /** Start delivering again from where it left off, after a pause. */
27
34
  resume() {
28
- if (this.#stopped)
35
+ if (this.#stopped || this.#stopping)
29
36
  throw new Error("This watch was stopped");
30
37
  this.#abort = new AbortController();
31
38
  this.begin();
32
39
  }
33
- /** Stop the watch in the sandbox, and stop delivering. */
40
+ /** Stop the watch in the sandbox. What changed before the stop is still
41
+ * delivered: the watch reports it and ends, and this returns once that has
42
+ * arrived (at most 5 s). Called from inside a callback, it stops at once. */
34
43
  async stop(options) {
35
- if (this.#stopped)
44
+ if (this.#stopped || this.#stopping)
36
45
  return;
37
- this.#stopped = true;
38
- this.#abort.abort();
46
+ this.#stopping = true;
39
47
  await this.t
40
48
  .json({ method: "DELETE", path: `${this.base}/${encodeURIComponent(this.id)}`, ...options })
41
49
  .catch(() => undefined);
42
- await this.#following?.catch(() => undefined);
43
- this.options.onExit?.("stopped");
50
+ // Inside a callback, #following is awaiting the caller: waiting for it
51
+ // here would make that callback await itself.
52
+ if (this.#following && this.#delivering === 0) {
53
+ let timer;
54
+ await Promise.race([
55
+ this.#following.catch(() => undefined),
56
+ new Promise((resolve) => {
57
+ timer = setTimeout(resolve, 5_000);
58
+ }),
59
+ ]);
60
+ clearTimeout(timer);
61
+ }
62
+ this.#stopped = true;
63
+ this.#abort.abort();
64
+ this.#notifyExit("stopped");
65
+ }
66
+ #notifyExit(reason) {
67
+ if (this.#exitNotified)
68
+ return;
69
+ this.#exitNotified = true;
70
+ this.options.onExit?.(reason);
44
71
  }
45
72
  async #follow() {
46
73
  for (;;) {
@@ -55,21 +82,38 @@ export class WatchHandle {
55
82
  })) {
56
83
  this.cursor = event.cursor ?? this.cursor;
57
84
  if (event.k === "events") {
58
- if (this.options.onBatch)
59
- await this.options.onBatch(event.events);
60
- if (this.onEvent)
61
- for (const item of event.events)
62
- await this.onEvent(item);
85
+ this.#delivering++;
86
+ try {
87
+ if (this.options.onBatch)
88
+ await this.options.onBatch(event.events);
89
+ if (this.#stopped)
90
+ return;
91
+ if (this.onEvent)
92
+ for (const item of event.events) {
93
+ await this.onEvent(item);
94
+ if (this.#stopped)
95
+ return;
96
+ }
97
+ }
98
+ finally {
99
+ this.#delivering--;
100
+ }
63
101
  }
64
102
  else if (event.k === "continue")
65
103
  next = "continue";
66
104
  else if (event.k === "end" || event.k === "paused") {
67
105
  if (!this.#stopped)
68
- this.options.onExit?.(event.k === "paused" ? "paused" : event.reason);
106
+ this.#notifyExit(event.k === "paused" ? "paused" : event.reason);
69
107
  return;
70
108
  }
71
109
  else if (event.k === "failure")
72
- throw new Error(`${event.code}: ${event.message}`);
110
+ throw new RuntimeError({
111
+ code: event.code,
112
+ status: event.status ?? 0,
113
+ message: event.message,
114
+ ...(event.hint ? { hint: event.hint } : {}),
115
+ ...(event.requestId ? { requestId: event.requestId } : {}),
116
+ });
73
117
  else
74
118
  this.options.onNotice?.(event);
75
119
  }
@@ -85,6 +129,7 @@ export class WatchHandle {
85
129
  }
86
130
  /** @internal */
87
131
  begin() {
132
+ this.#exitNotified = false;
88
133
  this.#following = this.#follow();
89
134
  // An error surfaces through `done`; nothing is left unhandled meanwhile.
90
135
  this.#following.catch(() => undefined);
@@ -126,6 +171,22 @@ export function sandboxWatches(t, sandboxId) {
126
171
  query: { cursor, ...(input.waitMs !== undefined ? { waitMs: input.waitMs } : {}) },
127
172
  ...options,
128
173
  }),
174
+ /** Starts a watch whose changes Runtime sends to the account's webhooks
175
+ * as `sandbox.files.changed` events, with nothing reading it here:
176
+ *
177
+ * await sbx.files.watches.webhook("/workspace/app", { recursive: true });
178
+ *
179
+ * Runs until stopped (timeoutMs 0) unless given a timeout. */
180
+ webhook: (path, input = {}) => {
181
+ const { idempotencyKey, signal, timeoutMs, ...rest } = input;
182
+ return t.json({
183
+ method: "POST",
184
+ path: base,
185
+ body: { path, ...rest, timeoutMs: timeoutMs ?? WEBHOOK_WATCH_TIMEOUT_MS, webhook: true },
186
+ ...(idempotencyKey ? { idempotencyKey } : {}),
187
+ ...(signal ? { signal } : {}),
188
+ });
189
+ },
129
190
  stop: (id, options) => t.json({
130
191
  method: "DELETE",
131
192
  path: `${base}/${encodeURIComponent(id)}`,