@alibaba-group/opensandbox 0.1.4 → 0.1.6

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"]);
@@ -148,7 +165,32 @@ const { endpoint } = await sandbox.getEndpoint(44772);
148
165
  const url = await sandbox.getEndpointUrl(44772);
149
166
  ```
150
167
 
151
- ### 6. Sandbox Management (Admin)
168
+ ### 6. Volume Mounts
169
+
170
+ `volumes` supports `host`, `pvc`, and `ossfs` backends. Each volume must specify exactly one backend.
171
+
172
+ ```ts
173
+ const sandbox = await Sandbox.create({
174
+ connectionConfig: config,
175
+ image: "ubuntu",
176
+ volumes: [
177
+ {
178
+ name: "oss-data",
179
+ ossfs: {
180
+ bucket: "bucket-a",
181
+ endpoint: "oss-cn-hangzhou.aliyuncs.com",
182
+ accessKeyId: process.env.OSS_ACCESS_KEY_ID!,
183
+ accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET!,
184
+ version: "2.0",
185
+ },
186
+ mountPath: "/mnt/oss",
187
+ subPath: "prefix",
188
+ },
189
+ ],
190
+ });
191
+ ```
192
+
193
+ ### 7. Sandbox Management (Admin)
152
194
 
153
195
  Use `SandboxManager` for administrative tasks and finding existing sandboxes.
154
196
 
@@ -156,7 +198,10 @@ Use `SandboxManager` for administrative tasks and finding existing sandboxes.
156
198
  import { SandboxManager } from "@alibaba-group/opensandbox";
157
199
 
158
200
  const manager = SandboxManager.create({ connectionConfig: config });
159
- const list = await manager.listSandboxInfos({ states: ["Running"], pageSize: 10 });
201
+ const list = await manager.listSandboxInfos({
202
+ states: ["Running"],
203
+ pageSize: 10,
204
+ });
160
205
  console.log(list.items.map((s) => s.id));
161
206
  await manager.close();
162
207
  ```
@@ -168,18 +213,19 @@ await manager.close();
168
213
  The `ConnectionConfig` class manages API server connection settings.
169
214
 
170
215
  Runtime notes:
216
+
171
217
  - In browsers, the SDK uses the global `fetch` implementation.
172
218
  - 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
219
 
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 | `{}` | - |
182
- | `useServerProxy` | Use sandbox server as proxy for execd/endpoint requests (e.g. when client cannot reach the sandbox directly) | `false` | - |
220
+ | Parameter | Description | Default | Environment Variable |
221
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------- | ---------------------- |
222
+ | `apiKey` | API key for authentication | Optional | `OPEN_SANDBOX_API_KEY` |
223
+ | `domain` | Sandbox service domain (`host[:port]`) | `localhost:8080` | `OPEN_SANDBOX_DOMAIN` |
224
+ | `protocol` | HTTP protocol (`http`/`https`) | `http` | - |
225
+ | `requestTimeoutSeconds` | Request timeout applied to SDK HTTP calls | `30` | - |
226
+ | `debug` | Enable basic HTTP debug logging | `false` | - |
227
+ | `headers` | Extra headers applied to every request | `{}` | - |
228
+ | `useServerProxy` | Use sandbox server as proxy for execd/endpoint requests (e.g. when client cannot reach the sandbox directly) | `false` | - |
183
229
 
184
230
  ```ts
185
231
  import { ConnectionConfig } from "@alibaba-group/opensandbox";
@@ -203,20 +249,23 @@ const config2 = new ConnectionConfig({
203
249
 
204
250
  `Sandbox.create()` allows configuring the sandbox environment.
205
251
 
206
- | Parameter | Description | Default |
207
- | --- | --- | --- |
208
- | `image` | Docker image to use | Required |
209
- | `timeoutSeconds` | Automatic termination timeout (server-side TTL) | 10 minutes |
210
- | `entrypoint` | Container entrypoint command | `["tail","-f","/dev/null"]` |
211
- | `resource` | CPU and memory limits (string map) | `{"cpu":"1","memory":"2Gi"}` |
212
- | `env` | Environment variables | `{}` |
213
- | `metadata` | Custom metadata tags | `{}` |
214
- | `networkPolicy` | Optional outbound network policy (egress) | - |
215
- | `extensions` | Extra server-defined fields | `{}` |
216
- | `skipHealthCheck` | Skip readiness checks (`Running` + health check) | `false` |
217
- | `healthCheck` | Custom readiness check | - |
218
- | `readyTimeoutSeconds` | Max time to wait for readiness | 30 seconds |
219
- | `healthCheckPollingInterval` | Poll interval while waiting (milliseconds) | 200 ms |
252
+ | Parameter | Description | Default |
253
+ | ---------------------------- | ------------------------------------------------ | ---------------------------- |
254
+ | `image` | Docker image to use | Required |
255
+ | `timeoutSeconds` | Automatic termination timeout (server-side TTL) | 10 minutes |
256
+ | `entrypoint` | Container entrypoint command | `["tail","-f","/dev/null"]` |
257
+ | `resource` | CPU and memory limits (string map) | `{"cpu":"1","memory":"2Gi"}` |
258
+ | `env` | Environment variables | `{}` |
259
+ | `metadata` | Custom metadata tags | `{}` |
260
+ | `networkPolicy` | Optional outbound network policy (egress) | - |
261
+ | `extensions` | Extra server-defined fields | `{}` |
262
+ | `skipHealthCheck` | Skip readiness checks (`Running` + health check) | `false` |
263
+ | `healthCheck` | Custom readiness check | - |
264
+ | `readyTimeoutSeconds` | Max time to wait for readiness | 30 seconds |
265
+ | `healthCheckPollingInterval` | Poll interval while waiting (milliseconds) | 200 ms |
266
+
267
+ Note: metadata keys under `opensandbox.io/` are reserved for system-managed
268
+ labels and will be rejected by the server.
220
269
 
221
270
  ```ts
222
271
  const sandbox = await Sandbox.create({
@@ -229,16 +278,35 @@ const sandbox = await Sandbox.create({
229
278
  });
230
279
  ```
231
280
 
232
- ### 3. Resource cleanup
281
+ ### 3. Runtime Egress Policy Updates
282
+
283
+ Runtime egress reads and patches go directly to the sandbox egress sidecar.
284
+ The SDK first resolves the sandbox endpoint on port `18080`, then calls the sidecar `/policy` API.
285
+
286
+ Patch uses merge semantics:
287
+ - Incoming rules take priority over existing rules with the same `target`.
288
+ - Existing rules for other targets remain unchanged.
289
+ - Within a single patch payload, the first rule for a `target` wins.
290
+ - The current `defaultAction` is preserved.
291
+
292
+ ```ts
293
+ const policy = await sandbox.getEgressPolicy();
294
+
295
+ await sandbox.patchEgressRules([
296
+ { action: "allow", target: "www.github.com" },
297
+ { action: "deny", target: "pypi.org" },
298
+ ]);
299
+ ```
300
+
301
+ ### 4. Resource cleanup
233
302
 
234
303
  Both `Sandbox` and `SandboxManager` own a scoped HTTP agent when running on Node.js
235
304
  so you can safely reuse the same `ConnectionConfig`. Once you are finished interacting
236
305
  with the sandbox or administration APIs, call `sandbox.close()` / `manager.close()` to
237
- release the underlying agent.
306
+ release the underlying agent.
238
307
 
239
308
  ## Browser Notes
240
309
 
241
310
  - The SDK can run in browsers, but **streaming file uploads are Node-only**.
242
311
  - If you pass `ReadableStream` or `AsyncIterable` for `writeFiles`, the browser will fall back to **buffering in memory** before upload.
243
312
  - Reason: browsers do not support streaming `multipart/form-data` bodies with custom boundaries (required by the execd upload API).
244
-
@@ -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,6 +268,9 @@ function joinUrl(baseUrl, pathname) {
267
268
  return `${base}${path}`;
268
269
  }
269
270
  function toRunCommandRequest(command, opts) {
271
+ if (opts?.gid != null && opts.uid == null) {
272
+ throw new Error("uid is required when gid is provided");
273
+ }
270
274
  const body = {
271
275
  command,
272
276
  cwd: opts?.workingDirectory,
@@ -275,8 +279,39 @@ function toRunCommandRequest(command, opts) {
275
279
  if (opts?.timeoutSeconds != null) {
276
280
  body.timeout = Math.round(opts.timeoutSeconds * 1e3);
277
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?.timeoutSeconds != null) {
301
+ body.timeout = Math.round(opts.timeoutSeconds * 1e3);
302
+ }
278
303
  return body;
279
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
+ }
280
315
  function parseOptionalDate(value, field) {
281
316
  if (value == null) return void 0;
282
317
  if (value instanceof Date) return value;
@@ -296,6 +331,58 @@ var CommandsAdapter = class {
296
331
  this.fetch = opts.fetch ?? fetch;
297
332
  }
298
333
  fetch;
334
+ buildRunStreamSpec(command, opts) {
335
+ assertNonBlank(command, "command");
336
+ return {
337
+ pathname: "/command",
338
+ body: toRunCommandRequest(command, opts),
339
+ fallbackErrorMessage: "Run command failed"
340
+ };
341
+ }
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);
353
+ const res = await this.fetch(url, {
354
+ method: "POST",
355
+ headers: {
356
+ accept: "text/event-stream",
357
+ "content-type": "application/json",
358
+ ...this.opts.headers ?? {}
359
+ },
360
+ body: JSON.stringify(spec.body),
361
+ signal
362
+ });
363
+ for await (const ev of parseJsonEventStream(res, {
364
+ fallbackErrorMessage: spec.fallbackErrorMessage
365
+ })) {
366
+ yield ev;
367
+ }
368
+ }
369
+ async consumeExecutionStream(stream, handlers, inferExitCode = false) {
370
+ const execution = {
371
+ logs: { stdout: [], stderr: [] },
372
+ result: []
373
+ };
374
+ const dispatcher = new ExecutionEventDispatcher(execution, handlers);
375
+ for await (const ev of stream) {
376
+ if (ev.type === "init" && (ev.text ?? "") === "" && execution.id) {
377
+ ev.text = execution.id;
378
+ }
379
+ await dispatcher.dispatch(ev);
380
+ }
381
+ if (inferExitCode) {
382
+ execution.exitCode = inferForegroundExitCode(execution);
383
+ }
384
+ return execution;
385
+ }
299
386
  async interrupt(sessionId) {
300
387
  const { error, response } = await this.client.DELETE("/command", {
301
388
  params: { query: { id: sessionId } }
@@ -339,35 +426,53 @@ var CommandsAdapter = class {
339
426
  };
340
427
  }
341
428
  async *runStream(command, opts, signal) {
342
- const url = joinUrl(this.opts.baseUrl, "/command");
343
- const body = JSON.stringify(toRunCommandRequest(command, opts));
344
- const res = await this.fetch(url, {
345
- method: "POST",
346
- headers: {
347
- "accept": "text/event-stream",
348
- "content-type": "application/json",
349
- ...this.opts.headers ?? {}
350
- },
351
- body,
429
+ for await (const ev of this.streamExecution(
430
+ this.buildRunStreamSpec(command, opts),
352
431
  signal
353
- });
354
- for await (const ev of parseJsonEventStream(res, { fallbackErrorMessage: "Run command failed" })) {
432
+ )) {
355
433
  yield ev;
356
434
  }
357
435
  }
358
436
  async run(command, opts, handlers, signal) {
359
- const execution = {
360
- logs: { stdout: [], stderr: [] },
361
- result: []
362
- };
363
- const dispatcher = new ExecutionEventDispatcher(execution, handlers);
364
- for await (const ev of this.runStream(command, opts, signal)) {
365
- if (ev.type === "init" && (ev.text ?? "") === "" && execution.id) {
366
- ev.text = execution.id;
367
- }
368
- await dispatcher.dispatch(ev);
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;
369
461
  }
370
- return execution;
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");
371
476
  }
372
477
  };
373
478
 
@@ -856,11 +961,15 @@ var SandboxesAdapter = class {
856
961
  }
857
962
  return d;
858
963
  }
964
+ parseOptionalIsoDate(field, v) {
965
+ if (v == null) return null;
966
+ return this.parseIsoDate(field, v);
967
+ }
859
968
  mapSandboxInfo(raw) {
860
969
  return {
861
970
  ...raw ?? {},
862
971
  createdAt: this.parseIsoDate("createdAt", raw?.createdAt),
863
- expiresAt: this.parseIsoDate("expiresAt", raw?.expiresAt)
972
+ expiresAt: this.parseOptionalIsoDate("expiresAt", raw?.expiresAt)
864
973
  };
865
974
  }
866
975
  async createSandbox(req) {
@@ -876,7 +985,7 @@ var SandboxesAdapter = class {
876
985
  return {
877
986
  ...raw ?? {},
878
987
  createdAt: this.parseIsoDate("createdAt", raw?.createdAt),
879
- expiresAt: this.parseIsoDate("expiresAt", raw?.expiresAt)
988
+ expiresAt: this.parseOptionalIsoDate("expiresAt", raw?.expiresAt)
880
989
  };
881
990
  }
882
991
  async getSandbox(sandboxId) {
@@ -970,6 +1079,7 @@ export {
970
1079
  InvalidArgumentException,
971
1080
  createExecdClient,
972
1081
  createLifecycleClient,
1082
+ throwOnOpenApiFetchError,
973
1083
  ExecutionEventDispatcher,
974
1084
  CommandsAdapter,
975
1085
  FilesystemAdapter,
@@ -977,4 +1087,4 @@ export {
977
1087
  MetricsAdapter,
978
1088
  SandboxesAdapter
979
1089
  };
980
- //# sourceMappingURL=chunk-OYTPXLWE.js.map
1090
+ //# sourceMappingURL=chunk-AFWIGM3C.js.map