@alibaba-group/opensandbox 0.1.3 → 0.1.5

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.
package/README.md CHANGED
@@ -55,7 +55,10 @@ try {
55
55
  await sandbox.close();
56
56
  } catch (err) {
57
57
  if (err instanceof SandboxException) {
58
- console.error(`Sandbox Error: [${err.error.code}] ${err.error.message ?? ""}`);
58
+ console.error(
59
+ `Sandbox Error: [${err.error.code}] ${err.error.message ?? ""}`,
60
+ );
61
+ console.error(`Request ID: ${err.requestId ?? "N/A"}`);
59
62
  } else {
60
63
  console.error(err);
61
64
  }
@@ -72,7 +75,7 @@ Manage the sandbox lifecycle, including renewal, pausing, and resuming.
72
75
  const info = await sandbox.getInfo();
73
76
  console.log("State:", info.status.state);
74
77
  console.log("Created:", info.createdAt);
75
- console.log("Expires:", info.expiresAt);
78
+ console.log("Expires:", info.expiresAt); // null when manual cleanup mode is used
76
79
 
77
80
  await sandbox.pause();
78
81
 
@@ -83,6 +86,16 @@ const resumed = await sandbox.resume();
83
86
  await resumed.renew(30 * 60);
84
87
  ```
85
88
 
89
+ Create a non-expiring sandbox by passing `timeoutSeconds: null`:
90
+
91
+ ```ts
92
+ const manual = await Sandbox.create({
93
+ connectionConfig: config,
94
+ image: "ubuntu",
95
+ timeoutSeconds: null,
96
+ });
97
+ ```
98
+
86
99
  ### 2. Custom Health Check
87
100
 
88
101
  Define custom logic to determine whether the sandbox is ready/healthy. This overrides the default ping check used during readiness checks.
@@ -109,7 +122,8 @@ import type { ExecutionHandlers } from "@alibaba-group/opensandbox";
109
122
  const handlers: ExecutionHandlers = {
110
123
  onStdout: (m) => console.log("STDOUT:", m.text),
111
124
  onStderr: (m) => console.error("STDERR:", m.text),
112
- onExecutionComplete: (c) => console.log("Finished in", c.executionTimeMs, "ms"),
125
+ onExecutionComplete: (c) =>
126
+ console.log("Finished in", c.executionTimeMs, "ms"),
113
127
  };
114
128
 
115
129
  await sandbox.commands.run(
@@ -124,16 +138,19 @@ await sandbox.commands.run(
124
138
  Manage files and directories, including read, write, list/search, and delete.
125
139
 
126
140
  ```ts
127
- await sandbox.files.createDirectories([{ path: "/tmp/demo", mode: 0o755 }]);
141
+ await sandbox.files.createDirectories([{ path: "/tmp/demo", mode: 755 }]);
128
142
 
129
143
  await sandbox.files.writeFiles([
130
- { path: "/tmp/demo/hello.txt", data: "Hello World", mode: 0o644 },
144
+ { path: "/tmp/demo/hello.txt", data: "Hello World", mode: 644 },
131
145
  ]);
132
146
 
133
147
  const content = await sandbox.files.readFile("/tmp/demo/hello.txt");
134
148
  console.log("Content:", content);
135
149
 
136
- const files = await sandbox.files.search({ path: "/tmp/demo", pattern: "*.txt" });
150
+ const files = await sandbox.files.search({
151
+ path: "/tmp/demo",
152
+ pattern: "*.txt",
153
+ });
137
154
  console.log(files.map((f) => f.path));
138
155
 
139
156
  await sandbox.files.deleteDirectories(["/tmp/demo"]);
@@ -156,7 +173,10 @@ Use `SandboxManager` for administrative tasks and finding existing sandboxes.
156
173
  import { SandboxManager } from "@alibaba-group/opensandbox";
157
174
 
158
175
  const manager = SandboxManager.create({ connectionConfig: config });
159
- const list = await manager.listSandboxInfos({ states: ["Running"], pageSize: 10 });
176
+ const list = await manager.listSandboxInfos({
177
+ states: ["Running"],
178
+ pageSize: 10,
179
+ });
160
180
  console.log(list.items.map((s) => s.id));
161
181
  await manager.close();
162
182
  ```
@@ -168,17 +188,19 @@ await manager.close();
168
188
  The `ConnectionConfig` class manages API server connection settings.
169
189
 
170
190
  Runtime notes:
191
+
171
192
  - In browsers, the SDK uses the global `fetch` implementation.
172
193
  - In Node.js, every `Sandbox` and `SandboxManager` clones the base `ConnectionConfig` via `withTransportIfMissing()`, so each instance gets an isolated `undici` keep-alive pool. Call `sandbox.close()` or `manager.close()` when you are done so the SDK can release the associated agent.
173
194
 
174
- | Parameter | Description | Default | Environment Variable |
175
- | --- | --- | --- | --- |
176
- | `apiKey` | API key for authentication | Optional | `OPEN_SANDBOX_API_KEY` |
177
- | `domain` | Sandbox service domain (`host[:port]`) | `localhost:8080` | `OPEN_SANDBOX_DOMAIN` |
178
- | `protocol` | HTTP protocol (`http`/`https`) | `http` | - |
179
- | `requestTimeoutSeconds` | Request timeout applied to SDK HTTP calls | `30` | - |
180
- | `debug` | Enable basic HTTP debug logging | `false` | - |
181
- | `headers` | Extra headers applied to every request | `{}` | - |
195
+ | Parameter | Description | Default | Environment Variable |
196
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------- | ---------------------- |
197
+ | `apiKey` | API key for authentication | Optional | `OPEN_SANDBOX_API_KEY` |
198
+ | `domain` | Sandbox service domain (`host[:port]`) | `localhost:8080` | `OPEN_SANDBOX_DOMAIN` |
199
+ | `protocol` | HTTP protocol (`http`/`https`) | `http` | - |
200
+ | `requestTimeoutSeconds` | Request timeout applied to SDK HTTP calls | `30` | - |
201
+ | `debug` | Enable basic HTTP debug logging | `false` | - |
202
+ | `headers` | Extra headers applied to every request | `{}` | - |
203
+ | `useServerProxy` | Use sandbox server as proxy for execd/endpoint requests (e.g. when client cannot reach the sandbox directly) | `false` | - |
182
204
 
183
205
  ```ts
184
206
  import { ConnectionConfig } from "@alibaba-group/opensandbox";
@@ -202,20 +224,23 @@ const config2 = new ConnectionConfig({
202
224
 
203
225
  `Sandbox.create()` allows configuring the sandbox environment.
204
226
 
205
- | Parameter | Description | Default |
206
- | --- | --- | --- |
207
- | `image` | Docker image to use | Required |
208
- | `timeoutSeconds` | Automatic termination timeout (server-side TTL) | 10 minutes |
209
- | `entrypoint` | Container entrypoint command | `["tail","-f","/dev/null"]` |
210
- | `resource` | CPU and memory limits (string map) | `{"cpu":"1","memory":"2Gi"}` |
211
- | `env` | Environment variables | `{}` |
212
- | `metadata` | Custom metadata tags | `{}` |
213
- | `networkPolicy` | Optional outbound network policy (egress) | - |
214
- | `extensions` | Extra server-defined fields | `{}` |
215
- | `skipHealthCheck` | Skip readiness checks (`Running` + health check) | `false` |
216
- | `healthCheck` | Custom readiness check | - |
217
- | `readyTimeoutSeconds` | Max time to wait for readiness | 30 seconds |
218
- | `healthCheckPollingInterval` | Poll interval while waiting (milliseconds) | 200 ms |
227
+ | Parameter | Description | Default |
228
+ | ---------------------------- | ------------------------------------------------ | ---------------------------- |
229
+ | `image` | Docker image to use | Required |
230
+ | `timeoutSeconds` | Automatic termination timeout (server-side TTL) | 10 minutes |
231
+ | `entrypoint` | Container entrypoint command | `["tail","-f","/dev/null"]` |
232
+ | `resource` | CPU and memory limits (string map) | `{"cpu":"1","memory":"2Gi"}` |
233
+ | `env` | Environment variables | `{}` |
234
+ | `metadata` | Custom metadata tags | `{}` |
235
+ | `networkPolicy` | Optional outbound network policy (egress) | - |
236
+ | `extensions` | Extra server-defined fields | `{}` |
237
+ | `skipHealthCheck` | Skip readiness checks (`Running` + health check) | `false` |
238
+ | `healthCheck` | Custom readiness check | - |
239
+ | `readyTimeoutSeconds` | Max time to wait for readiness | 30 seconds |
240
+ | `healthCheckPollingInterval` | Poll interval while waiting (milliseconds) | 200 ms |
241
+
242
+ Note: metadata keys under `opensandbox.io/` are reserved for system-managed
243
+ labels and will be rejected by the server.
219
244
 
220
245
  ```ts
221
246
  const sandbox = await Sandbox.create({
@@ -228,16 +253,35 @@ const sandbox = await Sandbox.create({
228
253
  });
229
254
  ```
230
255
 
231
- ### 3. Resource cleanup
256
+ ### 3. Runtime Egress Policy Updates
257
+
258
+ Runtime egress reads and patches go directly to the sandbox egress sidecar.
259
+ The SDK first resolves the sandbox endpoint on port `18080`, then calls the sidecar `/policy` API.
260
+
261
+ Patch uses merge semantics:
262
+ - Incoming rules take priority over existing rules with the same `target`.
263
+ - Existing rules for other targets remain unchanged.
264
+ - Within a single patch payload, the first rule for a `target` wins.
265
+ - The current `defaultAction` is preserved.
266
+
267
+ ```ts
268
+ const policy = await sandbox.getEgressPolicy();
269
+
270
+ await sandbox.patchEgressRules([
271
+ { action: "allow", target: "www.github.com" },
272
+ { action: "deny", target: "pypi.org" },
273
+ ]);
274
+ ```
275
+
276
+ ### 4. Resource cleanup
232
277
 
233
278
  Both `Sandbox` and `SandboxManager` own a scoped HTTP agent when running on Node.js
234
279
  so you can safely reuse the same `ConnectionConfig`. Once you are finished interacting
235
280
  with the sandbox or administration APIs, call `sandbox.close()` / `manager.close()` to
236
- release the underlying agent.
281
+ release the underlying agent.
237
282
 
238
283
  ## Browser Notes
239
284
 
240
285
  - The SDK can run in browsers, but **streaming file uploads are Node-only**.
241
286
  - If you pass `ReadableStream` or `AsyncIterable` for `writeFiles`, the browser will fall back to **buffering in memory** before upload.
242
287
  - Reason: browsers do not support streaming `multipart/form-data` bodies with custom boundaries (required by the execd upload API).
243
-
@@ -14,25 +14,26 @@ var SandboxException = class extends Error {
14
14
  name = "SandboxException";
15
15
  error;
16
16
  cause;
17
+ requestId;
17
18
  constructor(opts = {}) {
18
19
  super(opts.message);
19
20
  this.cause = opts.cause;
20
21
  this.error = opts.error ?? new SandboxError(SandboxError.INTERNAL_UNKNOWN_ERROR);
22
+ this.requestId = opts.requestId;
21
23
  }
22
24
  };
23
25
  var SandboxApiException = class extends SandboxException {
24
26
  name = "SandboxApiException";
25
27
  statusCode;
26
- requestId;
27
28
  rawBody;
28
29
  constructor(opts) {
29
30
  super({
30
31
  message: opts.message,
31
32
  cause: opts.cause,
32
- error: opts.error ?? new SandboxError(SandboxError.UNEXPECTED_RESPONSE, opts.message)
33
+ error: opts.error ?? new SandboxError(SandboxError.UNEXPECTED_RESPONSE, opts.message),
34
+ requestId: opts.requestId
33
35
  });
34
36
  this.statusCode = opts.statusCode;
35
- this.requestId = opts.requestId;
36
37
  this.rawBody = opts.rawBody;
37
38
  }
38
39
  };
@@ -267,11 +268,61 @@ function joinUrl(baseUrl, pathname) {
267
268
  return `${base}${path}`;
268
269
  }
269
270
  function toRunCommandRequest(command, opts) {
270
- return {
271
+ if (opts?.gid != null && opts.uid == null) {
272
+ throw new Error("uid is required when gid is provided");
273
+ }
274
+ const body = {
271
275
  command,
272
276
  cwd: opts?.workingDirectory,
273
277
  background: !!opts?.background
274
278
  };
279
+ if (opts?.timeoutSeconds != null) {
280
+ body.timeout = Math.round(opts.timeoutSeconds * 1e3);
281
+ }
282
+ if (opts?.uid != null) {
283
+ body.uid = opts.uid;
284
+ }
285
+ if (opts?.gid != null) {
286
+ body.gid = opts.gid;
287
+ }
288
+ if (opts?.envs != null) {
289
+ body.envs = opts.envs;
290
+ }
291
+ return body;
292
+ }
293
+ function toRunInSessionRequest(command, opts) {
294
+ const body = {
295
+ command
296
+ };
297
+ if (opts?.workingDirectory != null) {
298
+ body.cwd = opts.workingDirectory;
299
+ }
300
+ if (opts?.timeout != null) {
301
+ body.timeout = opts.timeout;
302
+ }
303
+ return body;
304
+ }
305
+ function inferForegroundExitCode(execution) {
306
+ const errorValue = execution.error?.value?.trim();
307
+ const parsedExitCode = errorValue && /^-?\d+$/.test(errorValue) ? Number(errorValue) : Number.NaN;
308
+ return execution.error != null ? Number.isFinite(parsedExitCode) ? parsedExitCode : null : execution.complete ? 0 : null;
309
+ }
310
+ function assertNonBlank(value, field) {
311
+ if (!value.trim()) {
312
+ throw new Error(`${field} cannot be empty`);
313
+ }
314
+ }
315
+ function parseOptionalDate(value, field) {
316
+ if (value == null) return void 0;
317
+ if (value instanceof Date) return value;
318
+ if (typeof value !== "string") {
319
+ throw new Error(`Invalid ${field}: expected ISO string, got ${typeof value}`);
320
+ }
321
+ const parsed = new Date(value);
322
+ if (Number.isNaN(parsed.getTime())) {
323
+ throw new Error(`Invalid ${field}: ${value}`);
324
+ }
325
+ return parsed;
275
326
  }
276
327
  var CommandsAdapter = class {
277
328
  constructor(client, opts) {
@@ -280,43 +331,149 @@ var CommandsAdapter = class {
280
331
  this.fetch = opts.fetch ?? fetch;
281
332
  }
282
333
  fetch;
283
- async interrupt(sessionId) {
284
- const { error, response } = await this.client.DELETE("/command", {
285
- params: { query: { id: sessionId } }
286
- });
287
- throwOnOpenApiFetchError({ error, response }, "Interrupt command failed");
334
+ buildRunStreamSpec(command, opts) {
335
+ assertNonBlank(command, "command");
336
+ return {
337
+ pathname: "/command",
338
+ body: toRunCommandRequest(command, opts),
339
+ fallbackErrorMessage: "Run command failed"
340
+ };
288
341
  }
289
- async *runStream(command, opts, signal) {
290
- const url = joinUrl(this.opts.baseUrl, "/command");
291
- const body = JSON.stringify(toRunCommandRequest(command, opts));
342
+ buildRunInSessionStreamSpec(sessionId, command, opts) {
343
+ assertNonBlank(sessionId, "sessionId");
344
+ assertNonBlank(command, "command");
345
+ return {
346
+ pathname: `/session/${encodeURIComponent(sessionId)}/run`,
347
+ body: toRunInSessionRequest(command, opts),
348
+ fallbackErrorMessage: "Run in session failed"
349
+ };
350
+ }
351
+ async *streamExecution(spec, signal) {
352
+ const url = joinUrl(this.opts.baseUrl, spec.pathname);
292
353
  const res = await this.fetch(url, {
293
354
  method: "POST",
294
355
  headers: {
295
- "accept": "text/event-stream",
356
+ accept: "text/event-stream",
296
357
  "content-type": "application/json",
297
358
  ...this.opts.headers ?? {}
298
359
  },
299
- body,
360
+ body: JSON.stringify(spec.body),
300
361
  signal
301
362
  });
302
- for await (const ev of parseJsonEventStream(res, { fallbackErrorMessage: "Run command failed" })) {
363
+ for await (const ev of parseJsonEventStream(res, {
364
+ fallbackErrorMessage: spec.fallbackErrorMessage
365
+ })) {
303
366
  yield ev;
304
367
  }
305
368
  }
306
- async run(command, opts, handlers, signal) {
369
+ async consumeExecutionStream(stream, handlers, inferExitCode = false) {
307
370
  const execution = {
308
371
  logs: { stdout: [], stderr: [] },
309
372
  result: []
310
373
  };
311
374
  const dispatcher = new ExecutionEventDispatcher(execution, handlers);
312
- for await (const ev of this.runStream(command, opts, signal)) {
375
+ for await (const ev of stream) {
313
376
  if (ev.type === "init" && (ev.text ?? "") === "" && execution.id) {
314
377
  ev.text = execution.id;
315
378
  }
316
379
  await dispatcher.dispatch(ev);
317
380
  }
381
+ if (inferExitCode) {
382
+ execution.exitCode = inferForegroundExitCode(execution);
383
+ }
318
384
  return execution;
319
385
  }
386
+ async interrupt(sessionId) {
387
+ const { error, response } = await this.client.DELETE("/command", {
388
+ params: { query: { id: sessionId } }
389
+ });
390
+ throwOnOpenApiFetchError({ error, response }, "Interrupt command failed");
391
+ }
392
+ async getCommandStatus(commandId) {
393
+ const { data, error, response } = await this.client.GET("/command/status/{id}", {
394
+ params: { path: { id: commandId } }
395
+ });
396
+ throwOnOpenApiFetchError({ error, response }, "Get command status failed");
397
+ const ok = data;
398
+ if (!ok || typeof ok !== "object") {
399
+ throw new Error("Get command status failed: unexpected response shape");
400
+ }
401
+ return {
402
+ id: ok.id,
403
+ content: ok.content,
404
+ running: ok.running,
405
+ exitCode: ok.exit_code ?? null,
406
+ error: ok.error,
407
+ startedAt: parseOptionalDate(ok.started_at, "startedAt"),
408
+ finishedAt: parseOptionalDate(ok.finished_at, "finishedAt") ?? null
409
+ };
410
+ }
411
+ async getBackgroundCommandLogs(commandId, cursor) {
412
+ const { data, error, response } = await this.client.GET("/command/{id}/logs", {
413
+ params: { path: { id: commandId }, query: cursor == null ? {} : { cursor } },
414
+ parseAs: "text"
415
+ });
416
+ throwOnOpenApiFetchError({ error, response }, "Get command logs failed");
417
+ const ok = data;
418
+ if (typeof ok !== "string") {
419
+ throw new Error("Get command logs failed: unexpected response shape");
420
+ }
421
+ const cursorHeader = response.headers.get("EXECD-COMMANDS-TAIL-CURSOR");
422
+ const parsedCursor = cursorHeader != null && cursorHeader !== "" ? Number(cursorHeader) : void 0;
423
+ return {
424
+ content: ok,
425
+ cursor: Number.isFinite(parsedCursor ?? NaN) ? parsedCursor : void 0
426
+ };
427
+ }
428
+ async *runStream(command, opts, signal) {
429
+ for await (const ev of this.streamExecution(
430
+ this.buildRunStreamSpec(command, opts),
431
+ signal
432
+ )) {
433
+ yield ev;
434
+ }
435
+ }
436
+ async run(command, opts, handlers, signal) {
437
+ return this.consumeExecutionStream(
438
+ this.runStream(command, opts, signal),
439
+ handlers,
440
+ !opts?.background
441
+ );
442
+ }
443
+ async createSession(options) {
444
+ const body = options?.workingDirectory != null ? { cwd: options.workingDirectory } : {};
445
+ const { data, error, response } = await this.client.POST("/session", {
446
+ body
447
+ });
448
+ throwOnOpenApiFetchError({ error, response }, "Create session failed");
449
+ const ok = data;
450
+ if (!ok || typeof ok.session_id !== "string") {
451
+ throw new Error("Create session failed: unexpected response shape");
452
+ }
453
+ return ok.session_id;
454
+ }
455
+ async *runInSessionStream(sessionId, command, opts, signal) {
456
+ for await (const ev of this.streamExecution(
457
+ this.buildRunInSessionStreamSpec(sessionId, command, opts),
458
+ signal
459
+ )) {
460
+ yield ev;
461
+ }
462
+ }
463
+ async runInSession(sessionId, command, options, handlers, signal) {
464
+ return this.consumeExecutionStream(
465
+ this.runInSessionStream(sessionId, command, options, signal),
466
+ handlers,
467
+ true
468
+ );
469
+ }
470
+ async deleteSession(sessionId) {
471
+ const { error, response } = await this.client.DELETE(
472
+ "/session/{sessionId}",
473
+ { params: { path: { sessionId } } }
474
+ );
475
+ throwOnOpenApiFetchError({ error, response }, "Delete session failed");
476
+ }
320
477
  };
321
478
 
322
479
  // src/adapters/filesystemAdapter.ts
@@ -804,11 +961,15 @@ var SandboxesAdapter = class {
804
961
  }
805
962
  return d;
806
963
  }
964
+ parseOptionalIsoDate(field, v) {
965
+ if (v == null) return null;
966
+ return this.parseIsoDate(field, v);
967
+ }
807
968
  mapSandboxInfo(raw) {
808
969
  return {
809
970
  ...raw ?? {},
810
971
  createdAt: this.parseIsoDate("createdAt", raw?.createdAt),
811
- expiresAt: this.parseIsoDate("expiresAt", raw?.expiresAt)
972
+ expiresAt: this.parseOptionalIsoDate("expiresAt", raw?.expiresAt)
812
973
  };
813
974
  }
814
975
  async createSandbox(req) {
@@ -824,7 +985,7 @@ var SandboxesAdapter = class {
824
985
  return {
825
986
  ...raw ?? {},
826
987
  createdAt: this.parseIsoDate("createdAt", raw?.createdAt),
827
- expiresAt: this.parseIsoDate("expiresAt", raw?.expiresAt)
988
+ expiresAt: this.parseOptionalIsoDate("expiresAt", raw?.expiresAt)
828
989
  };
829
990
  }
830
991
  async getSandbox(sandboxId) {
@@ -895,9 +1056,9 @@ var SandboxesAdapter = class {
895
1056
  expiresAt: raw?.expiresAt ? this.parseIsoDate("expiresAt", raw.expiresAt) : void 0
896
1057
  };
897
1058
  }
898
- async getSandboxEndpoint(sandboxId, port) {
1059
+ async getSandboxEndpoint(sandboxId, port, useServerProxy = false) {
899
1060
  const { data, error, response } = await this.client.GET("/sandboxes/{sandboxId}/endpoints/{port}", {
900
- params: { path: { sandboxId, port } }
1061
+ params: { path: { sandboxId, port }, query: { use_server_proxy: useServerProxy } }
901
1062
  });
902
1063
  throwOnOpenApiFetchError({ error, response }, "Get sandbox endpoint failed");
903
1064
  const ok = data;
@@ -918,6 +1079,7 @@ export {
918
1079
  InvalidArgumentException,
919
1080
  createExecdClient,
920
1081
  createLifecycleClient,
1082
+ throwOnOpenApiFetchError,
921
1083
  ExecutionEventDispatcher,
922
1084
  CommandsAdapter,
923
1085
  FilesystemAdapter,
@@ -925,4 +1087,4 @@ export {
925
1087
  MetricsAdapter,
926
1088
  SandboxesAdapter
927
1089
  };
928
- //# sourceMappingURL=chunk-4EF4ODU2.js.map
1090
+ //# sourceMappingURL=chunk-XHEWHFQ6.js.map