@sandboxrouter/sdk 0.1.0 → 0.3.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 (97) hide show
  1. package/README.md +271 -62
  2. package/dist/bench.js +4 -4
  3. package/dist/catalog.d.ts +232 -3
  4. package/dist/catalog.js +569 -14
  5. package/dist/check.d.ts +28 -0
  6. package/dist/check.js +185 -0
  7. package/dist/cli.js +149 -12
  8. package/dist/computers.d.ts +55 -13
  9. package/dist/computers.js +132 -6
  10. package/dist/conformance.d.ts +46 -0
  11. package/dist/conformance.js +65 -0
  12. package/dist/cost.d.ts +67 -0
  13. package/dist/cost.js +153 -0
  14. package/dist/gc.d.ts +29 -0
  15. package/dist/gc.js +54 -0
  16. package/dist/index.d.ts +34 -2
  17. package/dist/index.js +34 -2
  18. package/dist/leaderboard.d.ts +6 -0
  19. package/dist/leaderboard.js +5 -4
  20. package/dist/ledger.d.ts +38 -0
  21. package/dist/ledger.js +89 -0
  22. package/dist/providers/agent37.d.ts +40 -0
  23. package/dist/providers/agent37.js +224 -0
  24. package/dist/providers/agentcore.d.ts +71 -0
  25. package/dist/providers/agentcore.js +446 -0
  26. package/dist/providers/alibaba.d.ts +18 -0
  27. package/dist/providers/alibaba.js +106 -0
  28. package/dist/providers/azure.d.ts +43 -0
  29. package/dist/providers/azure.js +330 -0
  30. package/dist/providers/beam.d.ts +11 -0
  31. package/dist/providers/beam.js +179 -0
  32. package/dist/providers/blaxel.d.ts +3 -1
  33. package/dist/providers/blaxel.js +32 -22
  34. package/dist/providers/boat.d.ts +25 -0
  35. package/dist/providers/boat.js +263 -0
  36. package/dist/providers/cloudflare.d.ts +30 -0
  37. package/dist/providers/cloudflare.js +152 -0
  38. package/dist/providers/codesandbox.d.ts +59 -0
  39. package/dist/providers/codesandbox.js +287 -0
  40. package/dist/providers/daytona.d.ts +25 -4
  41. package/dist/providers/daytona.js +449 -28
  42. package/dist/providers/dedalus.d.ts +12 -0
  43. package/dist/providers/dedalus.js +244 -0
  44. package/dist/providers/deno.d.ts +50 -0
  45. package/dist/providers/deno.js +170 -0
  46. package/dist/providers/docker.d.ts +17 -0
  47. package/dist/providers/docker.js +154 -0
  48. package/dist/providers/e2b.d.ts +70 -3
  49. package/dist/providers/e2b.js +376 -8
  50. package/dist/providers/exedev.d.ts +23 -0
  51. package/dist/providers/exedev.js +290 -0
  52. package/dist/providers/freestyle.d.ts +49 -0
  53. package/dist/providers/freestyle.js +261 -0
  54. package/dist/providers/hopx.d.ts +15 -0
  55. package/dist/providers/hopx.js +177 -0
  56. package/dist/providers/instabox.d.ts +76 -0
  57. package/dist/providers/instabox.js +360 -0
  58. package/dist/providers/koyeb.d.ts +71 -0
  59. package/dist/providers/koyeb.js +379 -0
  60. package/dist/providers/lambda.d.ts +51 -0
  61. package/dist/providers/lambda.js +467 -0
  62. package/dist/providers/langsmith.d.ts +16 -0
  63. package/dist/providers/langsmith.js +149 -0
  64. package/dist/providers/limrun.d.ts +77 -0
  65. package/dist/providers/limrun.js +626 -0
  66. package/dist/providers/maritime.d.ts +5 -1
  67. package/dist/providers/maritime.js +39 -8
  68. package/dist/providers/mobilerun.d.ts +44 -0
  69. package/dist/providers/mobilerun.js +417 -0
  70. package/dist/providers/modal.js +12 -4
  71. package/dist/providers/morph.js +12 -4
  72. package/dist/providers/northflank.d.ts +21 -0
  73. package/dist/providers/northflank.js +259 -0
  74. package/dist/providers/novita.d.ts +12 -0
  75. package/dist/providers/novita.js +81 -0
  76. package/dist/providers/openscale.d.ts +17 -0
  77. package/dist/providers/openscale.js +87 -0
  78. package/dist/providers/orgo.d.ts +45 -0
  79. package/dist/providers/orgo.js +414 -0
  80. package/dist/providers/sail.d.ts +32 -0
  81. package/dist/providers/sail.js +372 -0
  82. package/dist/providers/superserve.d.ts +25 -0
  83. package/dist/providers/superserve.js +337 -0
  84. package/dist/providers/tensorlake.js +17 -14
  85. package/dist/providers/upstash.d.ts +30 -0
  86. package/dist/providers/upstash.js +258 -0
  87. package/dist/providers/vercel.js +14 -3
  88. package/dist/providers/workspaces.d.ts +60 -0
  89. package/dist/providers/workspaces.js +662 -0
  90. package/dist/router.d.ts +36 -5
  91. package/dist/router.js +174 -25
  92. package/dist/server.d.ts +1 -1
  93. package/dist/server.js +22 -3
  94. package/dist/types.d.ts +18 -6
  95. package/dist/util.d.ts +28 -1
  96. package/dist/util.js +56 -7
  97. package/package.json +106 -17
@@ -1,10 +1,77 @@
1
- import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
- /** E2B via the official `e2b` SDK. vCPU and memory come from the template, not the create call. */
3
- export declare class E2BProvider implements Provider {
1
+ import { type ExecOptions, type ExecResult, type Provider, type ProviderId, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ import type { ComputerOs, ComputerProvider, ComputerSpec, ProviderComputer } from "../computers.js";
3
+ /**
4
+ * E2B via the official `e2b` SDK. vCPU and memory come from the template, not the create call.
5
+ * Computers (Linux desktops) via `@e2b/desktop`: the public `desktop` template (Xvfb + XFCE),
6
+ * driven with xdotool and scrot inside the sandbox.
7
+ */
8
+ export declare class E2BProvider implements Provider, ComputerProvider {
4
9
  private apiKey;
5
10
  readonly id: "e2b";
11
+ readonly computerOs: ComputerOs[];
6
12
  constructor(apiKey?: string | undefined);
7
13
  configured(): boolean;
8
14
  create(spec: SandboxSpec): Promise<ProviderSandbox>;
9
15
  connect(nativeId: string): Promise<ProviderSandbox>;
16
+ /**
17
+ * A fresh desktop every call. E2B has no get-or-create by user, so `externalUserId` (and `name`)
18
+ * are only stored as sandbox metadata; keep the returned id to come back to the same desktop.
19
+ */
20
+ createComputer(spec: ComputerSpec): Promise<ProviderComputer>;
21
+ /** Reattach by sandbox id. A paused (slept) desktop is resumed. */
22
+ connectComputer(nativeId: string): Promise<ProviderComputer>;
23
+ }
24
+ /**
25
+ * The members of an `e2b` Sandbox that the shared box below uses. SDKs forked from E2B's
26
+ * (e.g. `novita-sandbox`) and E2B-API-compatible services reached through the `e2b` SDK have the
27
+ * same shape, so their adapters reuse these classes.
28
+ */
29
+ export interface E2BLikeSandbox {
30
+ readonly sandboxId: string;
31
+ commands: {
32
+ /** Started in the background: returns a handle to wait on or kill. */
33
+ run(cmd: string, opts: {
34
+ cwd?: string;
35
+ envs?: Record<string, string>;
36
+ timeoutMs?: number;
37
+ background: true;
38
+ onStdout?: (chunk: string) => void;
39
+ onStderr?: (chunk: string) => void;
40
+ }): Promise<E2BCommandHandle>;
41
+ };
42
+ files: {
43
+ write(path: string, data: Uint8Array): Promise<unknown>;
44
+ read(path: string, opts: {
45
+ format: "bytes";
46
+ }): Promise<Uint8Array>;
47
+ };
48
+ kill(): Promise<unknown>;
49
+ pause(): Promise<unknown>;
50
+ }
51
+ /** A background command (the SDK's CommandHandle). */
52
+ export interface E2BCommandHandle {
53
+ /** Resolves when the command exits; like the SDK, rejects with CommandExitError on a nonzero exit. */
54
+ wait(): Promise<{
55
+ exitCode: number;
56
+ stdout: string;
57
+ stderr: string;
58
+ }>;
59
+ /** SIGKILL the command inside the sandbox. */
60
+ kill(): Promise<boolean>;
61
+ }
62
+ /** exec/files/kill over an E2B-style SDK sandbox. `provider` labels errors. No pause. */
63
+ export declare class E2BCompatBox implements ProviderSandbox {
64
+ protected sbx: E2BLikeSandbox;
65
+ protected provider: ProviderId;
66
+ readonly nativeId: string;
67
+ constructor(sbx: E2BLikeSandbox, provider?: ProviderId);
68
+ exec(cmd: string, opts?: ExecOptions): Promise<ExecResult>;
69
+ writeFile(path: string, content: string | Uint8Array): Promise<void>;
70
+ readFile(path: string): Promise<Uint8Array>;
71
+ kill(): Promise<void>;
72
+ }
73
+ /** E2BCompatBox plus pause, for services where pause/resume is generally available. */
74
+ export declare class E2BBox extends E2BCompatBox {
75
+ /** Snapshot memory and disk. Sandbox.connect (router.get) resumes it. */
76
+ pause(): Promise<void>;
10
77
  }
@@ -1,9 +1,16 @@
1
1
  import { ProviderError } from "../types.js";
2
- import { optionalImport, toBytes } from "../util.js";
3
- /** E2B via the official `e2b` SDK. vCPU and memory come from the template, not the create call. */
2
+ import { DEFAULT_EXEC_TIMEOUT_S, optionalImport, timeoutError, toBytes } from "../util.js";
3
+ /** How long a desktop lives without activity. 1 hour is the Hobby tier's cap (Pro allows 24 h). */
4
+ const DESKTOP_TIMEOUT_MS = 60 * 60_000;
5
+ /**
6
+ * E2B via the official `e2b` SDK. vCPU and memory come from the template, not the create call.
7
+ * Computers (Linux desktops) via `@e2b/desktop`: the public `desktop` template (Xvfb + XFCE),
8
+ * driven with xdotool and scrot inside the sandbox.
9
+ */
4
10
  export class E2BProvider {
5
11
  apiKey;
6
12
  id = "e2b";
13
+ computerOs = ["linux"];
7
14
  constructor(apiKey = process.env.E2B_API_KEY) {
8
15
  this.apiKey = apiKey;
9
16
  }
@@ -25,32 +32,96 @@ export class E2BProvider {
25
32
  const { Sandbox } = await optionalImport("e2b", "e2b");
26
33
  return new E2BBox(await Sandbox.connect(nativeId, { apiKey: this.apiKey }));
27
34
  }
35
+ /**
36
+ * A fresh desktop every call. E2B has no get-or-create by user, so `externalUserId` (and `name`)
37
+ * are only stored as sandbox metadata; keep the returned id to come back to the same desktop.
38
+ */
39
+ async createComputer(spec) {
40
+ const os = spec.os ?? "linux";
41
+ if (os !== "linux")
42
+ throw new ProviderError(this.id, `E2B only offers linux desktops, not ${os}`);
43
+ const { Sandbox } = await optionalImport("@e2b/desktop", "e2b computers");
44
+ const metadata = {};
45
+ if (spec.externalUserId !== undefined)
46
+ metadata.externalUserId = spec.externalUserId;
47
+ if (spec.name !== undefined)
48
+ metadata.name = spec.name;
49
+ try {
50
+ const sbx = await Sandbox.create({
51
+ apiKey: this.apiKey,
52
+ timeoutMs: DESKTOP_TIMEOUT_MS,
53
+ metadata,
54
+ ...(spec.screen ? { resolution: [Math.round(spec.screen.width), Math.round(spec.screen.height)] } : {}),
55
+ });
56
+ return new E2BComputer(sbx);
57
+ }
58
+ catch (err) {
59
+ throw new ProviderError(this.id, err?.message ?? String(err));
60
+ }
61
+ }
62
+ /** Reattach by sandbox id. A paused (slept) desktop is resumed. */
63
+ async connectComputer(nativeId) {
64
+ const { Sandbox } = await optionalImport("@e2b/desktop", "e2b computers");
65
+ try {
66
+ return new E2BComputer(await Sandbox.connect(nativeId, { apiKey: this.apiKey, timeoutMs: DESKTOP_TIMEOUT_MS }));
67
+ }
68
+ catch (err) {
69
+ throw new ProviderError(this.id, err?.message ?? String(err));
70
+ }
71
+ }
28
72
  }
29
- class E2BBox {
73
+ /** exec/files/kill over an E2B-style SDK sandbox. `provider` labels errors. No pause. */
74
+ export class E2BCompatBox {
30
75
  sbx;
76
+ provider;
31
77
  nativeId;
32
- constructor(sbx) {
78
+ constructor(sbx, provider = "e2b") {
33
79
  this.sbx = sbx;
80
+ this.provider = provider;
34
81
  this.nativeId = sbx.sandboxId;
35
82
  }
36
83
  async exec(cmd, opts = {}) {
37
84
  const t0 = Date.now();
85
+ const seconds = opts.timeoutSeconds ?? DEFAULT_EXEC_TIMEOUT_S;
86
+ let timer;
87
+ const expired = new Promise((resolve) => {
88
+ timer = setTimeout(() => resolve("timeout"), seconds * 1000);
89
+ timer.unref();
90
+ });
38
91
  try {
39
- const r = await this.sbx.commands.run(cmd, {
92
+ // The command runs in the background so the deadline can kill it: the SDK's own `timeoutMs`
93
+ // only stops listening, and the process would keep running (and billing) in the sandbox.
94
+ // That stream deadline is set past ours so it never ends a command we still want.
95
+ const handle = await this.sbx.commands.run(cmd, {
40
96
  cwd: opts.cwd,
41
97
  envs: opts.env,
42
- timeoutMs: (opts.timeoutSeconds ?? 60) * 1000,
98
+ background: true,
99
+ timeoutMs: (seconds + 15) * 1000,
43
100
  onStdout: opts.onStdout,
44
101
  onStderr: opts.onStderr,
45
102
  });
103
+ const finished = handle.wait();
104
+ const r = await Promise.race([finished, expired]);
105
+ if (r === "timeout") {
106
+ finished.catch(() => { });
107
+ await handle.kill().catch(() => { });
108
+ throw timeoutError(this.provider, seconds);
109
+ }
46
110
  return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr, durationMs: Date.now() - t0 };
47
111
  }
48
112
  catch (err) {
113
+ if (err instanceof ProviderError)
114
+ throw err;
49
115
  // The SDK throws CommandExitError for a nonzero exit; that's a result, not a failure.
50
116
  if (err?.name === "CommandExitError" || typeof err?.exitCode === "number") {
51
117
  return { exitCode: err.exitCode, stdout: err.stdout ?? "", stderr: err.stderr ?? "", durationMs: Date.now() - t0 };
52
118
  }
53
- throw new ProviderError("e2b", err?.message ?? String(err));
119
+ if (err?.name === "TimeoutError")
120
+ throw timeoutError(this.provider, seconds);
121
+ throw new ProviderError(this.provider, err?.message ?? String(err));
122
+ }
123
+ finally {
124
+ clearTimeout(timer);
54
125
  }
55
126
  }
56
127
  async writeFile(path, content) {
@@ -59,11 +130,308 @@ class E2BBox {
59
130
  async readFile(path) {
60
131
  return await this.sbx.files.read(path, { format: "bytes" });
61
132
  }
133
+ async kill() {
134
+ await this.sbx.kill();
135
+ }
136
+ }
137
+ /** E2BCompatBox plus pause, for services where pause/resume is generally available. */
138
+ export class E2BBox extends E2BCompatBox {
62
139
  /** Snapshot memory and disk. Sandbox.connect (router.get) resumes it. */
63
140
  async pause() {
64
141
  await this.sbx.pause();
65
142
  }
66
- async kill() {
143
+ }
144
+ /** Friendly names models send that xdotool doesn't know; anything else passes through verbatim. */
145
+ const KEY_ALIASES = {
146
+ enter: "Return",
147
+ return: "Return",
148
+ esc: "Escape",
149
+ escape: "Escape",
150
+ backspace: "BackSpace",
151
+ del: "Delete",
152
+ delete: "Delete",
153
+ tab: "Tab",
154
+ space: "space",
155
+ up: "Up",
156
+ down: "Down",
157
+ left: "Left",
158
+ right: "Right",
159
+ home: "Home",
160
+ end: "End",
161
+ pageup: "Page_Up",
162
+ page_up: "Page_Up",
163
+ pagedown: "Page_Down",
164
+ page_down: "Page_Down",
165
+ cmd: "super",
166
+ command: "super",
167
+ win: "super",
168
+ windows: "super",
169
+ meta: "super",
170
+ control: "ctrl",
171
+ option: "alt",
172
+ };
173
+ /** "ctrl+Return" -> "ctrl+Return"; "Enter" -> "Return". Space-separated combos stay separate. */
174
+ function xdoKeys(text) {
175
+ return text
176
+ .trim()
177
+ .split(/\s+/)
178
+ .filter(Boolean)
179
+ .map((combo) => combo
180
+ .split("+")
181
+ .map((k) => KEY_ALIASES[k.toLowerCase()] ?? k)
182
+ .join("+"));
183
+ }
184
+ function sh(s) {
185
+ return `'${s.replace(/'/g, `'"'"'`)}'`;
186
+ }
187
+ /** Width and height from a PNG's IHDR chunk. */
188
+ function pngSize(png) {
189
+ if (png.length < 24 || png[0] !== 0x89 || png[1] !== 0x50)
190
+ return {};
191
+ const v = new DataView(png.buffer, png.byteOffset, png.byteLength);
192
+ return { width: v.getUint32(16), height: v.getUint32(20) };
193
+ }
194
+ function errMessage(err) {
195
+ return err?.stderr?.trim() || err?.message || String(err);
196
+ }
197
+ class ActionError extends Error {
198
+ }
199
+ /**
200
+ * An E2B desktop sandbox. Canonical actions map to `@e2b/desktop` calls where the SDK has one, and
201
+ * to xdotool/scrot commands where it doesn't (triple click, horizontal scroll, hold_key, zoom).
202
+ */
203
+ class E2BComputer {
204
+ sbx;
205
+ nativeId;
206
+ frame = 0;
207
+ streamKey;
208
+ constructor(sbx) {
209
+ this.sbx = sbx;
210
+ this.nativeId = sbx.sandboxId;
211
+ }
212
+ async info() {
213
+ const i = await this.sbx.getInfo();
214
+ const running = i.state === "running";
215
+ return {
216
+ id: this.nativeId,
217
+ os: "linux",
218
+ status: running ? "ready" : "sleeping",
219
+ screen: running ? await this.sbx.getScreenSize().catch(() => undefined) : undefined,
220
+ externalUserId: i.metadata?.externalUserId ?? null,
221
+ };
222
+ }
223
+ async act(action) {
224
+ try {
225
+ const extra = await this.perform(action);
226
+ if (action.action === "screenshot" || action.action === "zoom" || !action.noScreenshot) {
227
+ const shot = extra.image ?? (await this.sbx.screenshot());
228
+ delete extra.image;
229
+ return { action: action.action, ok: true, ...extra, ...this.frameOf(shot) };
230
+ }
231
+ delete extra.image;
232
+ return { action: action.action, ok: true, ...extra };
233
+ }
234
+ catch (err) {
235
+ return { action: action.action, ok: false, error: errMessage(err) };
236
+ }
237
+ }
238
+ /** Runs in order and stops at the first failed action (later coordinates may no longer apply). */
239
+ async actBatch(actions) {
240
+ const out = [];
241
+ for (const a of actions) {
242
+ const r = await this.act(a);
243
+ out.push(r);
244
+ if (!r.ok)
245
+ break;
246
+ }
247
+ return out;
248
+ }
249
+ frameOf(png) {
250
+ return { frameId: ++this.frame, imageB64: Buffer.from(png).toString("base64"), mime: "image/png", ...pngSize(png) };
251
+ }
252
+ /** Run a desktop command, with an explicit timeout rather than the SDK's 60 s default. */
253
+ run(cmd, timeoutSeconds = 60) {
254
+ return this.sbx.commands.run(cmd, { timeoutMs: timeoutSeconds * 1000 });
255
+ }
256
+ async withModifier(modifier, fn) {
257
+ if (!modifier)
258
+ return fn();
259
+ const keys = xdoKeys(modifier.replace(/\s+/g, "+"))[0]?.split("+") ?? [];
260
+ await this.run(`xdotool keydown ${keys.map(sh).join(" ")}`);
261
+ try {
262
+ return await fn();
263
+ }
264
+ finally {
265
+ await this.run(`xdotool keyup ${[...keys].reverse().map(sh).join(" ")}`).catch(() => { });
266
+ }
267
+ }
268
+ async moveTo(a, required) {
269
+ if (!a.coordinate) {
270
+ if (required)
271
+ throw new ActionError(`${a.action} needs a coordinate`);
272
+ return;
273
+ }
274
+ const [x, y] = a.coordinate;
275
+ // The SDK's click helpers skip the move when x or y is 0, so always move explicitly.
276
+ await this.sbx.moveMouse(Math.round(x), Math.round(y));
277
+ }
278
+ /** Does the action; returns extra result fields (and `image` for actions that produce their own). */
279
+ async perform(a) {
280
+ const click = async (fn) => {
281
+ await this.moveTo(a, false);
282
+ await this.withModifier(a.modifier, async () => {
283
+ for (let i = 0; i < Math.max(1, a.repeat ?? 1); i++)
284
+ await fn();
285
+ });
286
+ return {};
287
+ };
288
+ switch (a.action) {
289
+ case "screenshot":
290
+ return {};
291
+ case "left_click":
292
+ return click(() => this.sbx.leftClick());
293
+ case "right_click":
294
+ return click(() => this.sbx.rightClick());
295
+ case "middle_click":
296
+ return click(() => this.sbx.middleClick());
297
+ case "double_click":
298
+ return click(() => this.sbx.doubleClick());
299
+ case "triple_click":
300
+ return click(() => this.run("xdotool click --repeat 3 1"));
301
+ case "mouse_move":
302
+ await this.moveTo(a, true);
303
+ return {};
304
+ case "left_click_drag": {
305
+ const to = a.coordinate;
306
+ if (!to)
307
+ throw new ActionError("left_click_drag needs a coordinate");
308
+ let from = a.startCoordinate;
309
+ if (!from) {
310
+ const p = await this.sbx.getCursorPosition();
311
+ from = [p.x, p.y];
312
+ }
313
+ const start = [Math.round(from[0]), Math.round(from[1])];
314
+ const end = [Math.round(to[0]), Math.round(to[1])];
315
+ await this.withModifier(a.modifier, () => this.sbx.drag(start, end));
316
+ return {};
317
+ }
318
+ case "left_mouse_down":
319
+ await this.moveTo(a, false);
320
+ await this.sbx.mousePress("left");
321
+ return {};
322
+ case "left_mouse_up":
323
+ await this.moveTo(a, false);
324
+ await this.sbx.mouseRelease("left");
325
+ return {};
326
+ case "scroll": {
327
+ const dir = a.scrollDirection ?? "down";
328
+ const amount = Math.max(1, Math.round(a.scrollAmount ?? 3));
329
+ await this.moveTo(a, false);
330
+ await this.withModifier(a.modifier, async () => {
331
+ if (dir === "up" || dir === "down")
332
+ await this.sbx.scroll(dir, amount);
333
+ else
334
+ await this.run(`xdotool click --repeat ${amount} ${dir === "left" ? 6 : 7}`);
335
+ });
336
+ return {};
337
+ }
338
+ case "type":
339
+ if (a.text === undefined)
340
+ throw new ActionError("type needs text");
341
+ await this.sbx.write(a.text);
342
+ return {};
343
+ case "key": {
344
+ if (!a.text)
345
+ throw new ActionError("key needs text, e.g. \"Return\" or \"ctrl+s\"");
346
+ const repeat = Math.max(1, Math.round(a.repeat ?? 1));
347
+ await this.run(`xdotool key${repeat > 1 ? ` --repeat ${repeat}` : ""} -- ${xdoKeys(a.text).map(sh).join(" ")}`);
348
+ return {};
349
+ }
350
+ case "hold_key": {
351
+ if (!a.text)
352
+ throw new ActionError("hold_key needs text");
353
+ const keys = xdoKeys(a.text).map(sh).join(" ");
354
+ const secs = a.duration ?? 1;
355
+ if (!(secs >= 0 && secs <= 100))
356
+ throw new ActionError("duration must be 0 to 100 seconds");
357
+ // The SDK's default command timeout is 60 s and would end a longer hold with the key still down.
358
+ await this.run(`xdotool keydown ${keys}; sleep ${secs}; xdotool keyup ${keys}`, secs + 30);
359
+ return {};
360
+ }
361
+ case "wait":
362
+ await new Promise((r) => setTimeout(r, Math.max(0, a.duration ?? 1) * 1000));
363
+ return {};
364
+ case "zoom": {
365
+ if (!a.region)
366
+ throw new ActionError("zoom needs a region [x0, y0, x1, y1]");
367
+ const [x0, y0, x1, y1] = a.region.map((n) => Math.round(n));
368
+ if (x1 <= x0 || y1 <= y0 || x0 < 0 || y0 < 0)
369
+ throw new ActionError(`bad zoom region ${JSON.stringify(a.region)}`);
370
+ return { image: await this.capture(`-a ${x0},${y0},${x1 - x0},${y1 - y0}`, "png"), region: [x0, y0, x1, y1] };
371
+ }
372
+ case "cursor_position": {
373
+ const p = await this.sbx.getCursorPosition();
374
+ return { x: p.x, y: p.y, coordinate: [p.x, p.y] };
375
+ }
376
+ default:
377
+ throw new ActionError(`e2b does not support the "${a.action}" action`);
378
+ }
379
+ }
380
+ /** scrot straight to a temp file (format from the extension), read it back, clean up. */
381
+ async capture(flags, ext) {
382
+ const path = `/tmp/sbr-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;
383
+ await this.run(`scrot ${flags} ${path}`);
384
+ const bytes = await this.sbx.files.read(path, { format: "bytes" });
385
+ this.sbx.files.remove(path).catch(() => { });
386
+ return bytes;
387
+ }
388
+ async screenshot(opts = {}) {
389
+ if (opts.format === "jpeg")
390
+ return this.capture(`--pointer -q ${Math.round(opts.quality ?? 75)}`, "jpg");
391
+ return await this.sbx.screenshot();
392
+ }
393
+ async exec(command, timeoutSeconds = 60) {
394
+ try {
395
+ const r = await this.sbx.commands.run(command, { timeoutMs: timeoutSeconds * 1000 });
396
+ return { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr };
397
+ }
398
+ catch (err) {
399
+ // CommandExitError for a nonzero exit is a result, not a failure.
400
+ if (err?.name === "CommandExitError" || typeof err?.exitCode === "number") {
401
+ return { exitCode: err.exitCode, stdout: err.stdout ?? "", stderr: err.stderr ?? "" };
402
+ }
403
+ throw new ProviderError("e2b", err?.message ?? String(err));
404
+ }
405
+ }
406
+ async readFile(path) {
407
+ return await this.sbx.files.read(path, { format: "bytes" });
408
+ }
409
+ async writeFile(path, content) {
410
+ await this.sbx.files.write(path, toBytes(content));
411
+ }
412
+ /**
413
+ * A noVNC link to the desktop, password in the URL, valid while the sandbox lives (`ttlSeconds`
414
+ * is not supported, so `expiresAt` is null). `watch` sets noVNC's view_only flag, which is
415
+ * enforced by the client page only: whoever holds a watch link can edit it into a control link.
416
+ * The first call from a fresh connection restarts the VNC server with a new password, which
417
+ * invalidates links handed out earlier.
418
+ */
419
+ async viewer(mode = "watch", _opts = {}) {
420
+ if (!this.streamKey) {
421
+ // After connect/resume the SDK has lost the stream state; restart x11vnc so we know the key.
422
+ await this.sbx.stream.stop().catch(() => { });
423
+ await this.sbx.stream.start({ requireAuth: true });
424
+ this.streamKey = this.sbx.stream.getAuthKey();
425
+ }
426
+ const url = this.sbx.stream.getUrl({ autoConnect: true, viewOnly: mode === "watch", resize: "scale", authKey: this.streamKey });
427
+ return { url, expiresAt: null };
428
+ }
429
+ /** E2B pause: memory and disk are snapshotted. connectComputer resumes it; open viewers drop. */
430
+ async sleep() {
431
+ await this.sbx.pause();
432
+ this.streamKey = undefined;
433
+ }
434
+ async delete() {
67
435
  await this.sbx.kill();
68
436
  }
69
437
  }
@@ -0,0 +1,23 @@
1
+ import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
2
+ export declare class ExeDevProvider implements Provider {
3
+ private token;
4
+ private baseUrl;
5
+ readonly id: "exedev";
6
+ constructor(token?: string | undefined, baseUrl?: string);
7
+ configured(): boolean;
8
+ /** @internal POST /exec with a command line. */
9
+ request(command: string, accept?: string): Promise<Response>;
10
+ /** @internal Lobby command with JSON output. */
11
+ lobby<T>(command: string): Promise<T>;
12
+ /**
13
+ * @internal Run a shell script on the VM. The script is base64-encoded so it survives both quoting
14
+ * layers (the lobby's shell lexer, then the VM's shell) untouched.
15
+ */
16
+ ssh(vm: string, script: string): Promise<{
17
+ exitCode: number;
18
+ stdout: Uint8Array;
19
+ stderr: Uint8Array;
20
+ }>;
21
+ create(spec: SandboxSpec): Promise<ProviderSandbox>;
22
+ connect(nativeId: string): Promise<ProviderSandbox>;
23
+ }