@agentuity/sandbox 3.0.12 → 3.1.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 (122) hide show
  1. package/AGENTS.md +3 -3
  2. package/dist/api-reference.d.ts +1221 -0
  3. package/dist/api-reference.d.ts.map +1 -0
  4. package/dist/api-reference.js +1046 -0
  5. package/dist/api-reference.js.map +1 -0
  6. package/dist/base64.d.ts +2 -0
  7. package/dist/base64.d.ts.map +1 -0
  8. package/dist/base64.js +14 -0
  9. package/dist/base64.js.map +1 -0
  10. package/dist/client.d.ts +431 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +632 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/create.d.ts +203 -0
  15. package/dist/create.d.ts.map +1 -0
  16. package/dist/create.js +235 -0
  17. package/dist/create.js.map +1 -0
  18. package/dist/destroy.d.ts +23 -0
  19. package/dist/destroy.d.ts.map +1 -0
  20. package/dist/destroy.js +30 -0
  21. package/dist/destroy.js.map +1 -0
  22. package/dist/disk-checkpoint.d.ts +108 -0
  23. package/dist/disk-checkpoint.d.ts.map +1 -0
  24. package/dist/disk-checkpoint.js +124 -0
  25. package/dist/disk-checkpoint.js.map +1 -0
  26. package/dist/events.d.ts +56 -0
  27. package/dist/events.d.ts.map +1 -0
  28. package/dist/events.js +54 -0
  29. package/dist/events.js.map +1 -0
  30. package/dist/execute.d.ts +99 -0
  31. package/dist/execute.d.ts.map +1 -0
  32. package/dist/execute.js +138 -0
  33. package/dist/execute.js.map +1 -0
  34. package/dist/execution.d.ts +150 -0
  35. package/dist/execution.d.ts.map +1 -0
  36. package/dist/execution.js +120 -0
  37. package/dist/execution.js.map +1 -0
  38. package/dist/files.d.ts +283 -0
  39. package/dist/files.d.ts.map +1 -0
  40. package/dist/files.js +471 -0
  41. package/dist/files.js.map +1 -0
  42. package/dist/get.d.ts +288 -0
  43. package/dist/get.d.ts.map +1 -0
  44. package/dist/get.js +256 -0
  45. package/dist/get.js.map +1 -0
  46. package/dist/getStatus.d.ts +23 -0
  47. package/dist/getStatus.d.ts.map +1 -0
  48. package/dist/getStatus.js +53 -0
  49. package/dist/getStatus.js.map +1 -0
  50. package/dist/index.d.ts +42 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +22 -1
  53. package/dist/index.js.map +1 -1
  54. package/dist/job.d.ts +227 -0
  55. package/dist/job.d.ts.map +1 -0
  56. package/dist/job.js +109 -0
  57. package/dist/job.js.map +1 -0
  58. package/dist/list.d.ts +330 -0
  59. package/dist/list.d.ts.map +1 -0
  60. package/dist/list.js +209 -0
  61. package/dist/list.js.map +1 -0
  62. package/dist/pause.d.ts +39 -0
  63. package/dist/pause.d.ts.map +1 -0
  64. package/dist/pause.js +48 -0
  65. package/dist/pause.js.map +1 -0
  66. package/dist/resolve.d.ts +75 -0
  67. package/dist/resolve.d.ts.map +1 -0
  68. package/dist/resolve.js +76 -0
  69. package/dist/resolve.js.map +1 -0
  70. package/dist/resume.d.ts +23 -0
  71. package/dist/resume.d.ts.map +1 -0
  72. package/dist/resume.js +30 -0
  73. package/dist/resume.js.map +1 -0
  74. package/dist/run.d.ts +73 -0
  75. package/dist/run.d.ts.map +1 -0
  76. package/dist/run.js +568 -0
  77. package/dist/run.js.map +1 -0
  78. package/dist/runtime.d.ts +94 -0
  79. package/dist/runtime.d.ts.map +1 -0
  80. package/dist/runtime.js +82 -0
  81. package/dist/runtime.js.map +1 -0
  82. package/dist/snapshot-build.d.ts +48 -0
  83. package/dist/snapshot-build.d.ts.map +1 -0
  84. package/dist/snapshot-build.js +72 -0
  85. package/dist/snapshot-build.js.map +1 -0
  86. package/dist/snapshot.d.ts +596 -0
  87. package/dist/snapshot.d.ts.map +1 -0
  88. package/dist/snapshot.js +612 -0
  89. package/dist/snapshot.js.map +1 -0
  90. package/dist/types.d.ts +1010 -0
  91. package/dist/types.d.ts.map +1 -0
  92. package/dist/types.js +853 -0
  93. package/dist/types.js.map +1 -0
  94. package/dist/util.d.ts +296 -0
  95. package/dist/util.d.ts.map +1 -0
  96. package/dist/util.js +234 -0
  97. package/dist/util.js.map +1 -0
  98. package/package.json +7 -2
  99. package/src/api-reference.ts +1094 -0
  100. package/src/base64.ts +14 -0
  101. package/src/client.ts +998 -0
  102. package/src/create.ts +273 -0
  103. package/src/destroy.ts +43 -0
  104. package/src/disk-checkpoint.ts +184 -0
  105. package/src/events.ts +72 -0
  106. package/src/execute.ts +167 -0
  107. package/src/execution.ts +152 -0
  108. package/src/files.ts +637 -0
  109. package/src/get.ts +291 -0
  110. package/src/getStatus.ts +72 -0
  111. package/src/index.ts +252 -18
  112. package/src/job.ts +161 -0
  113. package/src/list.ts +239 -0
  114. package/src/pause.ts +75 -0
  115. package/src/resolve.ts +96 -0
  116. package/src/resume.ts +41 -0
  117. package/src/run.ts +783 -0
  118. package/src/runtime.ts +106 -0
  119. package/src/snapshot-build.ts +94 -0
  120. package/src/snapshot.ts +791 -0
  121. package/src/types.ts +1033 -0
  122. package/src/util.ts +280 -0
package/src/client.ts ADDED
@@ -0,0 +1,998 @@
1
+ import {
2
+ ExecuteOptionsSchema as CoreExecuteOptionsSchema,
3
+ type SandboxCreateOptions,
4
+ type SandboxFileInfo,
5
+ type SandboxInfo,
6
+ type SandboxStatus,
7
+ type Execution,
8
+ type ExecutionStatus,
9
+ type FileToWrite,
10
+ type SandboxRunOptions,
11
+ type SandboxRunResult,
12
+ type ListSandboxesParams,
13
+ type ListSandboxesResponse,
14
+ type ListRuntimesParams,
15
+ type ListRuntimesResponse,
16
+ type Job,
17
+ type CreateJobOptions,
18
+ } from './types.ts';
19
+ import type { Logger } from '@agentuity/client';
20
+ import type { Readable, Writable } from 'node:stream';
21
+ import { z } from 'zod';
22
+ import { APIClient } from '@agentuity/api';
23
+ import { getEnv } from '@agentuity/config';
24
+ import { sandboxCreate } from './create.ts';
25
+ import { sandboxDestroy } from './destroy.ts';
26
+ import { sandboxGet } from './get.ts';
27
+ import { sandboxExecute } from './execute.ts';
28
+ import {
29
+ sandboxWriteFiles,
30
+ sandboxReadFile,
31
+ sandboxListFiles,
32
+ sandboxMkDir,
33
+ sandboxRmFile,
34
+ sandboxRmDir,
35
+ sandboxSetEnv,
36
+ } from './files.ts';
37
+ import { sandboxPause, type SandboxPauseResult } from './pause.ts';
38
+ import { sandboxResume } from './resume.ts';
39
+ import { sandboxRun } from './run.ts';
40
+ import {
41
+ executionGet,
42
+ executionList,
43
+ type ExecutionInfo,
44
+ type ExecutionListResponse,
45
+ } from './execution.ts';
46
+ import { createMinimalLogger } from '@agentuity/client';
47
+ import { getServiceUrls } from '@agentuity/config';
48
+ import { writeAndDrain } from './util.ts';
49
+ import { sandboxList } from './list.ts';
50
+ import { runtimeList } from './runtime.ts';
51
+ import { jobCreate, jobGet, jobList, jobStop, type JobListResponse } from './job.ts';
52
+ import {
53
+ diskCheckpointCreate,
54
+ diskCheckpointList,
55
+ diskCheckpointRestore,
56
+ diskCheckpointDelete,
57
+ type DiskCheckpointInfo,
58
+ } from './disk-checkpoint.ts';
59
+ import {
60
+ snapshotCreate,
61
+ snapshotGet,
62
+ snapshotList,
63
+ snapshotDelete,
64
+ snapshotTag,
65
+ snapshotLineage,
66
+ type SnapshotInfo,
67
+ type SnapshotListResponse,
68
+ type SnapshotLineageResponse,
69
+ type SnapshotListParams,
70
+ type SnapshotLineageParams,
71
+ } from './snapshot.ts';
72
+ import { sandboxEventList, type SandboxEventListResponse } from './events.ts';
73
+
74
+ // Server-side long-poll wait duration per iteration (max 5 minutes supported by server)
75
+ const EXECUTION_WAIT_DURATION = '5m';
76
+
77
+ /** Terminal execution statuses that indicate the command has finished. */
78
+ const TERMINAL_STATUSES: Set<ExecutionStatus> = new Set([
79
+ 'completed',
80
+ 'failed',
81
+ 'timeout',
82
+ 'cancelled',
83
+ ]);
84
+
85
+ /**
86
+ * Wait for execution completion using server-side long-polling with automatic retry.
87
+ *
88
+ * Each iteration asks the server to hold the connection for up to
89
+ * EXECUTION_WAIT_DURATION. If the execution is still running when the
90
+ * server-side wait expires, we loop and issue another long-poll request.
91
+ * This continues until the execution reaches a terminal state or the
92
+ * caller's AbortSignal fires.
93
+ */
94
+ async function waitForExecution(
95
+ client: APIClient,
96
+ executionId: string,
97
+ orgId?: string,
98
+ signal?: AbortSignal
99
+ ): Promise<ExecutionInfo> {
100
+ while (true) {
101
+ if (signal?.aborted) {
102
+ throw new DOMException('The operation was aborted.', 'AbortError');
103
+ }
104
+
105
+ // Use server-side long-polling - the server will hold the connection
106
+ // until the execution reaches a terminal state or the wait duration expires.
107
+ // The signal is forwarded so the in-flight fetch is cancelled immediately
108
+ // when the caller aborts, rather than waiting the full poll duration.
109
+ const result = await executionGet(client, {
110
+ executionId,
111
+ orgId,
112
+ wait: EXECUTION_WAIT_DURATION,
113
+ signal,
114
+ });
115
+
116
+ // If the execution reached a terminal state, return immediately
117
+ if (TERMINAL_STATUSES.has(result.status as ExecutionStatus)) {
118
+ return result;
119
+ }
120
+
121
+ // Non-terminal status (e.g., 'running', 'queued') — the server-side
122
+ // long-poll expired before the command finished. Loop to poll again.
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Pipes a remote stream URL to a local writable stream with proper backpressure handling
128
+ */
129
+ async function pipeStreamToWritable(
130
+ streamUrl: string,
131
+ writable: Writable,
132
+ signal?: AbortSignal
133
+ ): Promise<void> {
134
+ const response = await fetch(streamUrl, { signal });
135
+ if (!response.ok) {
136
+ throw new Error(`Failed to fetch stream: ${response.status} ${response.statusText}`);
137
+ }
138
+ if (!response.body) {
139
+ return;
140
+ }
141
+
142
+ const reader = response.body.getReader();
143
+ try {
144
+ while (true) {
145
+ const { done, value } = await reader.read();
146
+ if (done) break;
147
+ if (value) {
148
+ await writeAndDrain(writable, value);
149
+ }
150
+ }
151
+ } finally {
152
+ try {
153
+ await reader.cancel();
154
+ } catch {
155
+ // Ignore cancel errors - stream may already be closed
156
+ }
157
+ reader.releaseLock();
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Extended execute options that support piping output to writable streams
163
+ */
164
+ export const ExecuteOptionsSchema = CoreExecuteOptionsSchema.extend({
165
+ /** Pipe stdout/stderr to writable streams (e.g., process.stdout) */
166
+ pipe: z
167
+ .object({
168
+ stdout: z.custom<Writable>().optional().describe('stdout writable stream'),
169
+ stderr: z.custom<Writable>().optional().describe('stderr writable stream'),
170
+ })
171
+ .optional()
172
+ .describe('Pipe stdout/stderr to writable streams (e.g., process.stdout)'),
173
+ });
174
+ export type ExecuteOptions = z.infer<typeof ExecuteOptionsSchema>;
175
+
176
+ export const SandboxClientOptionsSchema = z.object({
177
+ /** API key for authentication. Defaults to AGENTUITY_SDK_KEY/AGENTUITY_CLI_KEY */
178
+ apiKey: z.string().optional().describe('API key for authentication'),
179
+ /** Base URL for the sandbox API */
180
+ url: z.string().optional().describe('Base URL for the sandbox API'),
181
+ /** Organization ID for multi-tenant operations */
182
+ orgId: z.string().optional().describe('Organization ID for multi-tenant operations'),
183
+ /** Custom logger instance */
184
+ logger: z.custom<Logger>().optional().describe('Custom logger instance'),
185
+ });
186
+ export type SandboxClientOptions = z.infer<typeof SandboxClientOptionsSchema>;
187
+
188
+ /**
189
+ * I/O options for one-shot sandbox execution via run()
190
+ */
191
+ export const SandboxClientRunIOSchema = z.object({
192
+ /** AbortSignal to cancel the execution */
193
+ signal: z.custom<AbortSignal>().optional().describe('AbortSignal to cancel the execution'),
194
+ /** Readable stream for stdin input */
195
+ stdin: z.custom<Readable>().optional().describe('Readable stream for stdin input'),
196
+ /** Writable stream for stdout output */
197
+ stdout: z.custom<Writable>().optional().describe('Writable stream for stdout output'),
198
+ /** Writable stream for stderr output */
199
+ stderr: z.custom<Writable>().optional().describe('Writable stream for stderr output'),
200
+ /** Optional logger override for this run */
201
+ logger: z.custom<Logger>().optional().describe('Optional logger override for this run'),
202
+ });
203
+ export type SandboxClientRunIO = z.infer<typeof SandboxClientRunIOSchema>;
204
+
205
+ /**
206
+ * A sandbox instance returned by SandboxClient.create() or SandboxClient.connect()
207
+ */
208
+ export interface SandboxInstance {
209
+ /**
210
+ * Unique sandbox identifier
211
+ */
212
+ id: string;
213
+
214
+ /**
215
+ * Sandbox status at creation or connection time
216
+ */
217
+ status: SandboxStatus;
218
+
219
+ /**
220
+ * URL to stream stdout output
221
+ */
222
+ stdoutStreamUrl?: string;
223
+
224
+ /**
225
+ * URL to stream stderr output
226
+ */
227
+ stderrStreamUrl?: string;
228
+
229
+ /**
230
+ * URL to stream audit events (eBPF/Tetragon security events)
231
+ */
232
+ auditStreamUrl?: string;
233
+
234
+ /**
235
+ * Execute a command in the sandbox
236
+ */
237
+ execute(options: ExecuteOptions): Promise<Execution>;
238
+
239
+ /**
240
+ * Write files to the sandbox workspace
241
+ */
242
+ writeFiles(files: FileToWrite[]): Promise<number>;
243
+
244
+ /**
245
+ * Read a file from the sandbox workspace
246
+ */
247
+ readFile(path: string): Promise<ReadableStream<Uint8Array>>;
248
+
249
+ /**
250
+ * List files in the sandbox workspace
251
+ */
252
+ listFiles(path?: string): Promise<SandboxFileInfo[]>;
253
+
254
+ /**
255
+ * Create a directory in the sandbox workspace
256
+ */
257
+ mkDir(path: string, recursive?: boolean): Promise<void>;
258
+
259
+ /**
260
+ * Remove a file from the sandbox workspace.
261
+ * @returns Object with `found` indicating whether the file existed before removal
262
+ */
263
+ rmFile(path: string): Promise<{ found: boolean }>;
264
+
265
+ /**
266
+ * Remove a directory from the sandbox workspace.
267
+ * @returns Object with `found` indicating whether the directory existed before removal
268
+ */
269
+ rmDir(path: string, recursive?: boolean): Promise<{ found: boolean }>;
270
+
271
+ /**
272
+ * Set environment variables on the sandbox. Pass null to delete a variable.
273
+ */
274
+ setEnv(env: Record<string, string | null>): Promise<Record<string, string>>;
275
+
276
+ /**
277
+ * Get current sandbox information
278
+ */
279
+ get(): Promise<SandboxInfo>;
280
+
281
+ /**
282
+ * Pause the sandbox, creating a checkpoint of its current state
283
+ */
284
+ pause(): Promise<SandboxPauseResult>;
285
+
286
+ /**
287
+ * Resume the sandbox from a paused or evacuated state
288
+ */
289
+ resume(): Promise<void>;
290
+
291
+ /**
292
+ * Destroy the sandbox and release all resources
293
+ */
294
+ destroy(): Promise<void>;
295
+ }
296
+
297
+ /**
298
+ * Creates the method implementations shared by both create() and connect().
299
+ * Modelled after the similar helper in packages/runtime/src/services/sandbox/http.ts.
300
+ */
301
+ function createSandboxInstanceMethods(
302
+ client: APIClient,
303
+ sandboxId: string,
304
+ orgId?: string
305
+ ): Omit<
306
+ SandboxInstance,
307
+ 'id' | 'status' | 'stdoutStreamUrl' | 'stderrStreamUrl' | 'auditStreamUrl'
308
+ > {
309
+ return {
310
+ async execute(executeOptions: ExecuteOptions): Promise<Execution> {
311
+ const { pipe, ...coreOptions } = executeOptions;
312
+
313
+ const initialResult = await sandboxExecute(client, {
314
+ sandboxId,
315
+ options: coreOptions,
316
+ orgId,
317
+ signal: coreOptions.signal,
318
+ });
319
+
320
+ // If pipe options provided, stream the output to the writable streams
321
+ if (pipe) {
322
+ const streamPromises: Promise<void>[] = [];
323
+
324
+ if (pipe.stdout && initialResult.stdoutStreamUrl) {
325
+ streamPromises.push(
326
+ pipeStreamToWritable(
327
+ initialResult.stdoutStreamUrl,
328
+ pipe.stdout,
329
+ coreOptions.signal
330
+ )
331
+ );
332
+ }
333
+ if (pipe.stderr && initialResult.stderrStreamUrl) {
334
+ streamPromises.push(
335
+ pipeStreamToWritable(
336
+ initialResult.stderrStreamUrl,
337
+ pipe.stderr,
338
+ coreOptions.signal
339
+ )
340
+ );
341
+ }
342
+
343
+ // Wait for all streams to complete
344
+ if (streamPromises.length > 0) {
345
+ await Promise.all(streamPromises);
346
+ }
347
+ }
348
+
349
+ // Wait for execution to complete and get final result with exit code
350
+ const finalResult = await waitForExecution(
351
+ client,
352
+ initialResult.executionId,
353
+ orgId,
354
+ coreOptions.signal
355
+ );
356
+
357
+ return {
358
+ executionId: finalResult.executionId,
359
+ status: finalResult.status,
360
+ exitCode: finalResult.exitCode,
361
+ durationMs: finalResult.durationMs,
362
+ stdoutStreamUrl: initialResult.stdoutStreamUrl,
363
+ stderrStreamUrl: initialResult.stderrStreamUrl,
364
+ };
365
+ },
366
+
367
+ async writeFiles(files: FileToWrite[]): Promise<number> {
368
+ const result = await sandboxWriteFiles(client, { sandboxId, files, orgId });
369
+ return result.filesWritten;
370
+ },
371
+
372
+ async readFile(path: string): Promise<ReadableStream<Uint8Array>> {
373
+ return sandboxReadFile(client, { sandboxId, path, orgId });
374
+ },
375
+
376
+ async listFiles(path?: string): Promise<SandboxFileInfo[]> {
377
+ const result = await sandboxListFiles(client, { sandboxId, path, orgId });
378
+ return result.files;
379
+ },
380
+
381
+ async mkDir(path: string, recursive?: boolean): Promise<void> {
382
+ await sandboxMkDir(client, { sandboxId, path, recursive, orgId });
383
+ },
384
+
385
+ async rmFile(path: string): Promise<{ found: boolean }> {
386
+ return sandboxRmFile(client, { sandboxId, path, orgId });
387
+ },
388
+
389
+ async rmDir(path: string, recursive?: boolean): Promise<{ found: boolean }> {
390
+ return sandboxRmDir(client, { sandboxId, path, recursive, orgId });
391
+ },
392
+
393
+ async setEnv(env: Record<string, string | null>): Promise<Record<string, string>> {
394
+ const result = await sandboxSetEnv(client, { sandboxId, env, orgId });
395
+ return result.env;
396
+ },
397
+
398
+ async get(): Promise<SandboxInfo> {
399
+ return sandboxGet(client, { sandboxId, orgId });
400
+ },
401
+
402
+ async pause(): Promise<SandboxPauseResult> {
403
+ return sandboxPause(client, { sandboxId, orgId });
404
+ },
405
+
406
+ async resume(): Promise<void> {
407
+ return sandboxResume(client, { sandboxId, orgId });
408
+ },
409
+
410
+ async destroy(): Promise<void> {
411
+ return sandboxDestroy(client, { sandboxId, orgId });
412
+ },
413
+ };
414
+ }
415
+
416
+ /**
417
+ * A job instance returned by SandboxClient.createJob() or SandboxClient.getJob()
418
+ */
419
+ export interface JobInstance {
420
+ /**
421
+ * Unique job identifier
422
+ */
423
+ readonly id: string;
424
+
425
+ /**
426
+ * ID of the sandbox this job belongs to
427
+ */
428
+ readonly sandboxId: string;
429
+
430
+ /**
431
+ * Current job status
432
+ */
433
+ readonly status: string;
434
+
435
+ /**
436
+ * Get the current job status and details
437
+ */
438
+ get(): Promise<Job>;
439
+
440
+ /**
441
+ * Stop the job
442
+ * @param force - Force termination with SIGKILL
443
+ */
444
+ stop(force?: boolean): Promise<Job>;
445
+ }
446
+
447
+ /**
448
+ * Creates the method implementations for JobInstance
449
+ */
450
+ function createJobInstanceMethods(
451
+ client: APIClient,
452
+ sandboxId: string,
453
+ jobId: string,
454
+ orgId?: string
455
+ ): Omit<JobInstance, 'id' | 'sandboxId' | 'status'> {
456
+ return {
457
+ async get(): Promise<Job> {
458
+ return jobGet(client, { sandboxId, jobId, orgId });
459
+ },
460
+
461
+ async stop(force?: boolean): Promise<Job> {
462
+ return jobStop(client, { sandboxId, jobId, force, orgId });
463
+ },
464
+ };
465
+ }
466
+
467
+ /**
468
+ * A disk checkpoint instance returned by SandboxClient.createDiskCheckpoint() or SandboxClient.getDiskCheckpoint()
469
+ */
470
+ export interface DiskCheckpointInstance {
471
+ /**
472
+ * Unique checkpoint identifier
473
+ */
474
+ readonly id: string;
475
+
476
+ /**
477
+ * User-provided checkpoint name
478
+ */
479
+ readonly name: string;
480
+
481
+ /**
482
+ * ID of the sandbox this checkpoint belongs to
483
+ */
484
+ readonly sandboxId: string;
485
+
486
+ /**
487
+ * ISO timestamp of creation
488
+ */
489
+ readonly createdAt: string;
490
+
491
+ /**
492
+ * Parent checkpoint name
493
+ */
494
+ readonly parent: string;
495
+
496
+ /**
497
+ * Restore the sandbox to this checkpoint
498
+ */
499
+ restore(): Promise<void>;
500
+
501
+ /**
502
+ * Delete this checkpoint
503
+ */
504
+ delete(): Promise<void>;
505
+ }
506
+
507
+ /**
508
+ * Creates the method implementations for DiskCheckpointInstance
509
+ */
510
+ function createDiskCheckpointInstanceMethods(
511
+ client: APIClient,
512
+ sandboxId: string,
513
+ checkpointId: string,
514
+ orgId?: string
515
+ ): Omit<DiskCheckpointInstance, 'id' | 'name' | 'sandboxId' | 'createdAt' | 'parent'> {
516
+ return {
517
+ async restore(): Promise<void> {
518
+ return diskCheckpointRestore(client, { sandboxId, checkpointId, orgId });
519
+ },
520
+
521
+ async delete(): Promise<void> {
522
+ return diskCheckpointDelete(client, { sandboxId, checkpointId, orgId });
523
+ },
524
+ };
525
+ }
526
+
527
+ /**
528
+ * Convenience client for sandbox operations.
529
+ *
530
+ * @example
531
+ * ```typescript
532
+ * // Interactive sandbox usage
533
+ * const client = new SandboxClient();
534
+ * const sandbox = await client.create();
535
+ * const result = await sandbox.execute({ command: ['echo', 'hello'] });
536
+ * await sandbox.destroy();
537
+ *
538
+ * // One-shot execution with streaming
539
+ * const result = await client.run(
540
+ * { command: { exec: ['bun', 'run', 'script.ts'] } },
541
+ * { stdout: process.stdout, stderr: process.stderr }
542
+ * );
543
+ * ```
544
+ */
545
+ export class SandboxClient {
546
+ readonly #client: APIClient;
547
+ readonly #orgId?: string;
548
+ readonly #apiKey?: string;
549
+ readonly #region: string;
550
+ readonly #logger: Logger;
551
+
552
+ constructor(options: SandboxClientOptions = {}) {
553
+ const apiKey = options.apiKey || getEnv('AGENTUITY_SDK_KEY') || getEnv('AGENTUITY_CLI_KEY');
554
+
555
+ const region = getEnv('AGENTUITY_REGION') ?? 'usc';
556
+ const serviceUrls = getServiceUrls(region);
557
+
558
+ const url =
559
+ options.url ||
560
+ getEnv('AGENTUITY_SANDBOX_URL') ||
561
+ getEnv('AGENTUITY_CATALYST_URL') ||
562
+ getEnv('AGENTUITY_TRANSPORT_URL') ||
563
+ serviceUrls.sandbox;
564
+
565
+ const logger = options.logger ?? createMinimalLogger();
566
+
567
+ // Disable retries for sandbox operations - 409 Conflict means sandbox is busy,
568
+ // not a retryable rate limit. Retrying would waste ~360s (4 attempts × 90s timeout).
569
+ this.#client = new APIClient(url, logger, apiKey ?? '', { maxRetries: 0 });
570
+ this.#orgId = options.orgId;
571
+ this.#apiKey = apiKey;
572
+ this.#region = region;
573
+ this.#logger = logger;
574
+ }
575
+
576
+ /**
577
+ * Run a one-shot command in a new sandbox (creates, executes, destroys)
578
+ *
579
+ * This is a high-level convenience method that handles the full lifecycle:
580
+ * creating a sandbox, streaming I/O, polling for completion, and cleanup.
581
+ *
582
+ * @param options - Execution options including command and configuration
583
+ * @param io - Optional I/O streams and abort signal
584
+ * @returns The run result including exit code and duration
585
+ * @throws {Error} If stdin is provided without an API key
586
+ *
587
+ * @example
588
+ * ```typescript
589
+ * const client = new SandboxClient();
590
+ * const result = await client.run(
591
+ * { command: { exec: ['bun', 'run', 'script.ts'] } },
592
+ * { stdout: process.stdout, stderr: process.stderr }
593
+ * );
594
+ * console.log('Exit code:', result.exitCode);
595
+ * ```
596
+ */
597
+ async run(options: SandboxRunOptions, io: SandboxClientRunIO = {}): Promise<SandboxRunResult> {
598
+ if (io.stdin && !this.#apiKey) {
599
+ throw new Error('SandboxClient.run(): stdin streaming requires an API key');
600
+ }
601
+
602
+ return sandboxRun(this.#client, {
603
+ options,
604
+ orgId: this.#orgId,
605
+ region: this.#region,
606
+ apiKey: this.#apiKey,
607
+ signal: io.signal,
608
+ stdin: io.stdin,
609
+ stdout: io.stdout,
610
+ stderr: io.stderr,
611
+ logger: io.logger ?? this.#logger,
612
+ });
613
+ }
614
+
615
+ /**
616
+ * Create a new sandbox instance
617
+ *
618
+ * @param options - Optional sandbox configuration
619
+ * @returns A sandbox instance with execute and destroy methods
620
+ */
621
+ async create(options?: SandboxCreateOptions): Promise<SandboxInstance> {
622
+ const response = await sandboxCreate(this.#client, {
623
+ options,
624
+ orgId: this.#orgId,
625
+ });
626
+
627
+ return {
628
+ id: response.sandboxId,
629
+ status: response.status,
630
+ stdoutStreamUrl: response.stdoutStreamUrl,
631
+ stderrStreamUrl: response.stderrStreamUrl,
632
+ auditStreamUrl: response.auditStreamUrl,
633
+ ...createSandboxInstanceMethods(this.#client, response.sandboxId, this.#orgId),
634
+ };
635
+ }
636
+
637
+ /**
638
+ * Get sandbox information by ID
639
+ *
640
+ * @param sandboxId - The sandbox ID
641
+ * @returns Sandbox information
642
+ */
643
+ async get(sandboxId: string): Promise<SandboxInfo> {
644
+ return sandboxGet(this.#client, { sandboxId, orgId: this.#orgId });
645
+ }
646
+
647
+ /**
648
+ * Destroy a sandbox by ID
649
+ *
650
+ * @param sandboxId - The sandbox ID to destroy
651
+ */
652
+ async destroy(sandboxId: string): Promise<void> {
653
+ return sandboxDestroy(this.#client, { sandboxId, orgId: this.#orgId });
654
+ }
655
+
656
+ /**
657
+ * Write files to a sandbox workspace
658
+ *
659
+ * @param sandboxId - The sandbox ID
660
+ * @param files - Array of files to write with path and content
661
+ * @param signal - Optional AbortSignal to cancel the operation
662
+ * @returns The number of files written
663
+ */
664
+ async writeFiles(
665
+ sandboxId: string,
666
+ files: FileToWrite[],
667
+ signal?: AbortSignal
668
+ ): Promise<number> {
669
+ const result = await sandboxWriteFiles(this.#client, {
670
+ sandboxId,
671
+ files,
672
+ orgId: this.#orgId,
673
+ signal,
674
+ });
675
+ return result.filesWritten;
676
+ }
677
+
678
+ /**
679
+ * Read a file from a sandbox workspace
680
+ *
681
+ * @param sandboxId - The sandbox ID
682
+ * @param path - Path to the file relative to the sandbox workspace
683
+ * @param signal - Optional AbortSignal to cancel the operation
684
+ * @returns A ReadableStream of the file contents
685
+ */
686
+ async readFile(
687
+ sandboxId: string,
688
+ path: string,
689
+ signal?: AbortSignal
690
+ ): Promise<ReadableStream<Uint8Array>> {
691
+ return sandboxReadFile(this.#client, {
692
+ sandboxId,
693
+ path,
694
+ orgId: this.#orgId,
695
+ signal,
696
+ });
697
+ }
698
+
699
+ /**
700
+ * Get a full sandbox instance for an existing sandbox by ID.
701
+ *
702
+ * Unlike `get()` which returns read-only metadata, `connect()` returns
703
+ * a `SandboxInstance` with `execute()`, `writeFiles()`, and all other
704
+ * interaction methods — allowing you to resume working with a sandbox
705
+ * using just its ID.
706
+ *
707
+ * @param sandboxId - The sandbox ID to connect to
708
+ * @returns A sandbox instance with all interaction methods
709
+ */
710
+ async connect(sandboxId: string): Promise<SandboxInstance> {
711
+ const info = await sandboxGet(this.#client, { sandboxId, orgId: this.#orgId });
712
+
713
+ return {
714
+ id: info.sandboxId,
715
+ status: info.status,
716
+ stdoutStreamUrl: info.stdoutStreamUrl,
717
+ stderrStreamUrl: info.stderrStreamUrl,
718
+ auditStreamUrl: info.auditStreamUrl,
719
+ ...createSandboxInstanceMethods(this.#client, info.sandboxId, this.#orgId),
720
+ };
721
+ }
722
+
723
+ /**
724
+ * Pause a running sandbox, creating a checkpoint of its current state
725
+ *
726
+ * @param sandboxId - The sandbox ID to pause
727
+ */
728
+ async pause(sandboxId: string): Promise<SandboxPauseResult> {
729
+ return sandboxPause(this.#client, { sandboxId, orgId: this.#orgId });
730
+ }
731
+
732
+ /**
733
+ * Resume a paused or evacuated sandbox from its checkpoint
734
+ *
735
+ * @param sandboxId - The sandbox ID to resume
736
+ */
737
+ async resume(sandboxId: string): Promise<void> {
738
+ return sandboxResume(this.#client, { sandboxId, orgId: this.#orgId });
739
+ }
740
+
741
+ // ===== List Operations =====
742
+
743
+ /**
744
+ * List all sandboxes with optional filtering and pagination
745
+ *
746
+ * @param params - Optional parameters for filtering by project, status, and pagination
747
+ * @returns Paginated list of sandboxes with total count
748
+ */
749
+ async list(params?: ListSandboxesParams): Promise<ListSandboxesResponse> {
750
+ return sandboxList(this.#client, { ...params, orgId: this.#orgId });
751
+ }
752
+
753
+ /**
754
+ * List available sandbox runtimes
755
+ *
756
+ * @param params - Optional parameters for pagination
757
+ * @returns List of runtimes with total count
758
+ */
759
+ async listRuntimes(params?: ListRuntimesParams): Promise<ListRuntimesResponse> {
760
+ return runtimeList(this.#client, { ...params, orgId: this.#orgId });
761
+ }
762
+
763
+ // ===== Job Operations =====
764
+
765
+ /**
766
+ * Create a new job in a sandbox
767
+ *
768
+ * @param sandboxId - The sandbox ID where the job should run
769
+ * @param options - Job creation options including command
770
+ * @returns A job instance with get() and stop() methods
771
+ */
772
+ async createJob(sandboxId: string, options: CreateJobOptions): Promise<JobInstance> {
773
+ const job = await jobCreate(this.#client, { sandboxId, options, orgId: this.#orgId });
774
+
775
+ return {
776
+ id: job.jobId,
777
+ sandboxId,
778
+ status: job.status,
779
+ ...createJobInstanceMethods(this.#client, sandboxId, job.jobId, this.#orgId),
780
+ };
781
+ }
782
+
783
+ /**
784
+ * Get a job instance by ID
785
+ *
786
+ * @param sandboxId - The sandbox ID
787
+ * @param jobId - The job ID
788
+ * @returns A job instance with get() and stop() methods
789
+ */
790
+ async getJob(sandboxId: string, jobId: string): Promise<JobInstance> {
791
+ const job = await jobGet(this.#client, { sandboxId, jobId, orgId: this.#orgId });
792
+
793
+ return {
794
+ id: job.jobId,
795
+ sandboxId,
796
+ status: job.status,
797
+ ...createJobInstanceMethods(this.#client, sandboxId, job.jobId, this.#orgId),
798
+ };
799
+ }
800
+
801
+ /**
802
+ * List all jobs in a sandbox
803
+ *
804
+ * @param sandboxId - The sandbox ID
805
+ * @param limit - Maximum number of results
806
+ * @returns List of jobs
807
+ */
808
+ async listJobs(sandboxId: string, limit?: number): Promise<JobListResponse> {
809
+ return jobList(this.#client, { sandboxId, limit, orgId: this.#orgId });
810
+ }
811
+
812
+ // ===== Disk Checkpoint Operations =====
813
+
814
+ /**
815
+ * Create a disk checkpoint of a sandbox
816
+ *
817
+ * @param sandboxId - The sandbox ID
818
+ * @param name - Name for the checkpoint
819
+ * @returns A checkpoint instance with restore() and delete() methods
820
+ */
821
+ async createDiskCheckpoint(sandboxId: string, name: string): Promise<DiskCheckpointInstance> {
822
+ const checkpoint = await diskCheckpointCreate(this.#client, {
823
+ sandboxId,
824
+ name,
825
+ orgId: this.#orgId,
826
+ });
827
+
828
+ return {
829
+ id: checkpoint.id,
830
+ name: checkpoint.name,
831
+ sandboxId,
832
+ createdAt: checkpoint.createdAt,
833
+ parent: checkpoint.parent,
834
+ ...createDiskCheckpointInstanceMethods(
835
+ this.#client,
836
+ sandboxId,
837
+ checkpoint.id,
838
+ this.#orgId
839
+ ),
840
+ };
841
+ }
842
+
843
+ /**
844
+ * List all disk checkpoints for a sandbox
845
+ *
846
+ * @param sandboxId - The sandbox ID
847
+ * @returns List of checkpoint info objects
848
+ */
849
+ async listDiskCheckpoints(sandboxId: string): Promise<DiskCheckpointInfo[]> {
850
+ return diskCheckpointList(this.#client, { sandboxId, orgId: this.#orgId });
851
+ }
852
+
853
+ /**
854
+ * Get a disk checkpoint instance by ID
855
+ *
856
+ * @param sandboxId - The sandbox ID
857
+ * @param checkpointId - The checkpoint ID
858
+ * @returns A checkpoint instance with restore() and delete() methods
859
+ */
860
+ async getDiskCheckpoint(
861
+ sandboxId: string,
862
+ checkpointId: string
863
+ ): Promise<DiskCheckpointInstance> {
864
+ const checkpoints = await diskCheckpointList(this.#client, {
865
+ sandboxId,
866
+ orgId: this.#orgId,
867
+ });
868
+ const checkpoint = checkpoints.find((c) => c.id === checkpointId);
869
+ if (!checkpoint) {
870
+ throw new Error(`Checkpoint ${checkpointId} not found in sandbox ${sandboxId}`);
871
+ }
872
+
873
+ return {
874
+ id: checkpoint.id,
875
+ name: checkpoint.name,
876
+ sandboxId,
877
+ createdAt: checkpoint.createdAt,
878
+ parent: checkpoint.parent,
879
+ ...createDiskCheckpointInstanceMethods(
880
+ this.#client,
881
+ sandboxId,
882
+ checkpoint.id,
883
+ this.#orgId
884
+ ),
885
+ };
886
+ }
887
+
888
+ // ===== Snapshot Operations =====
889
+
890
+ /**
891
+ * Create a snapshot of a sandbox
892
+ *
893
+ * @param sandboxId - The sandbox ID to snapshot
894
+ * @param params - Optional snapshot parameters (name, tag, public, etc.)
895
+ * @returns The created snapshot information
896
+ */
897
+ async createSnapshot(
898
+ sandboxId: string,
899
+ params?: {
900
+ name?: string;
901
+ description?: string;
902
+ tag?: string;
903
+ public?: boolean;
904
+ }
905
+ ): Promise<SnapshotInfo> {
906
+ return snapshotCreate(this.#client, { sandboxId, ...params, orgId: this.#orgId });
907
+ }
908
+
909
+ /**
910
+ * Get snapshot information by ID
911
+ *
912
+ * @param snapshotId - The snapshot ID
913
+ * @returns Snapshot information
914
+ */
915
+ async getSnapshot(snapshotId: string): Promise<SnapshotInfo> {
916
+ return snapshotGet(this.#client, { snapshotId, orgId: this.#orgId });
917
+ }
918
+
919
+ /**
920
+ * List snapshots with optional filtering and pagination
921
+ *
922
+ * @param params - Optional parameters for filtering and pagination
923
+ * @returns Paginated list of snapshots
924
+ */
925
+ async listSnapshots(params?: SnapshotListParams): Promise<SnapshotListResponse> {
926
+ return snapshotList(this.#client, { ...params, orgId: this.#orgId });
927
+ }
928
+
929
+ /**
930
+ * Delete a snapshot
931
+ *
932
+ * @param snapshotId - The snapshot ID to delete
933
+ */
934
+ async deleteSnapshot(snapshotId: string): Promise<void> {
935
+ return snapshotDelete(this.#client, { snapshotId, orgId: this.#orgId });
936
+ }
937
+
938
+ /**
939
+ * Update the tag on a snapshot
940
+ *
941
+ * @param snapshotId - The snapshot ID
942
+ * @param tag - New tag (or null to remove)
943
+ * @returns Updated snapshot information
944
+ */
945
+ async tagSnapshot(snapshotId: string, tag: string | null): Promise<SnapshotInfo> {
946
+ return snapshotTag(this.#client, { snapshotId, tag, orgId: this.#orgId });
947
+ }
948
+
949
+ /**
950
+ * Get the lineage (ancestry chain) of a snapshot
951
+ *
952
+ * @param params - Parameters specifying which snapshot to get lineage for
953
+ * @returns Ordered list of snapshots in the lineage
954
+ */
955
+ async getSnapshotLineage(params?: SnapshotLineageParams): Promise<SnapshotLineageResponse> {
956
+ return snapshotLineage(this.#client, { ...params, orgId: this.#orgId });
957
+ }
958
+
959
+ // ===== Execution Operations =====
960
+
961
+ /**
962
+ * Get execution information by ID
963
+ *
964
+ * @param executionId - The execution ID
965
+ * @param wait - Optional wait duration for long-polling (e.g., "5m")
966
+ * @returns Execution information
967
+ */
968
+ async getExecution(executionId: string, wait?: string): Promise<ExecutionInfo> {
969
+ return executionGet(this.#client, { executionId, wait, orgId: this.#orgId });
970
+ }
971
+
972
+ /**
973
+ * List executions for a sandbox
974
+ *
975
+ * @param sandboxId - The sandbox ID
976
+ * @param limit - Maximum number of results
977
+ * @returns List of executions
978
+ */
979
+ async listExecutions(sandboxId: string, limit?: number): Promise<ExecutionListResponse> {
980
+ return executionList(this.#client, { sandboxId, limit, orgId: this.#orgId });
981
+ }
982
+
983
+ // ===== Event Operations =====
984
+
985
+ /**
986
+ * List events for a sandbox
987
+ *
988
+ * @param sandboxId - The sandbox ID
989
+ * @param params - Optional parameters for limit and sort direction
990
+ * @returns List of sandbox events
991
+ */
992
+ async listEvents(
993
+ sandboxId: string,
994
+ params?: { limit?: number; direction?: 'asc' | 'desc' }
995
+ ): Promise<SandboxEventListResponse> {
996
+ return sandboxEventList(this.#client, { sandboxId, ...params, orgId: this.#orgId });
997
+ }
998
+ }