@alibaba-group/opensandbox 0.1.4 → 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,18 +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 | `{}` | - |
182
- | `useServerProxy` | Use sandbox server as proxy for execd/endpoint requests (e.g. when client cannot reach the sandbox directly) | `false` | - |
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` | - |
183
204
 
184
205
  ```ts
185
206
  import { ConnectionConfig } from "@alibaba-group/opensandbox";
@@ -203,20 +224,23 @@ const config2 = new ConnectionConfig({
203
224
 
204
225
  `Sandbox.create()` allows configuring the sandbox environment.
205
226
 
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 |
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.
220
244
 
221
245
  ```ts
222
246
  const sandbox = await Sandbox.create({
@@ -229,16 +253,35 @@ const sandbox = await Sandbox.create({
229
253
  });
230
254
  ```
231
255
 
232
- ### 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
233
277
 
234
278
  Both `Sandbox` and `SandboxManager` own a scoped HTTP agent when running on Node.js
235
279
  so you can safely reuse the same `ConnectionConfig`. Once you are finished interacting
236
280
  with the sandbox or administration APIs, call `sandbox.close()` / `manager.close()` to
237
- release the underlying agent.
281
+ release the underlying agent.
238
282
 
239
283
  ## Browser Notes
240
284
 
241
285
  - The SDK can run in browsers, but **streaming file uploads are Node-only**.
242
286
  - If you pass `ReadableStream` or `AsyncIterable` for `writeFiles`, the browser will fall back to **buffering in memory** before upload.
243
287
  - 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?.timeout != null) {
301
+ body.timeout = opts.timeout;
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");
369
452
  }
370
- return execution;
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");
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-XHEWHFQ6.js.map