@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.
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ConnectionConfig: () => ConnectionConfig,
34
+ DEFAULT_EGRESS_PORT: () => DEFAULT_EGRESS_PORT,
34
35
  DEFAULT_ENTRYPOINT: () => DEFAULT_ENTRYPOINT,
35
36
  DEFAULT_EXECD_PORT: () => DEFAULT_EXECD_PORT,
36
37
  DEFAULT_HEALTH_CHECK_POLLING_INTERVAL_MILLIS: () => DEFAULT_HEALTH_CHECK_POLLING_INTERVAL_MILLIS,
@@ -69,25 +70,26 @@ var SandboxException = class extends Error {
69
70
  name = "SandboxException";
70
71
  error;
71
72
  cause;
73
+ requestId;
72
74
  constructor(opts = {}) {
73
75
  super(opts.message);
74
76
  this.cause = opts.cause;
75
77
  this.error = opts.error ?? new SandboxError(SandboxError.INTERNAL_UNKNOWN_ERROR);
78
+ this.requestId = opts.requestId;
76
79
  }
77
80
  };
78
81
  var SandboxApiException = class extends SandboxException {
79
82
  name = "SandboxApiException";
80
83
  statusCode;
81
- requestId;
82
84
  rawBody;
83
85
  constructor(opts) {
84
86
  super({
85
87
  message: opts.message,
86
88
  cause: opts.cause,
87
- error: opts.error ?? new SandboxError(SandboxError.UNEXPECTED_RESPONSE, opts.message)
89
+ error: opts.error ?? new SandboxError(SandboxError.UNEXPECTED_RESPONSE, opts.message),
90
+ requestId: opts.requestId
88
91
  });
89
92
  this.statusCode = opts.statusCode;
90
- this.requestId = opts.requestId;
91
93
  this.rawBody = opts.rawBody;
92
94
  }
93
95
  };
@@ -143,8 +145,19 @@ function createExecdClient(opts) {
143
145
  });
144
146
  }
145
147
 
146
- // src/openapi/lifecycleClient.ts
148
+ // src/openapi/egressClient.ts
147
149
  var import_openapi_fetch2 = __toESM(require("openapi-fetch"), 1);
150
+ function createEgressClient(opts) {
151
+ const createClientFn = import_openapi_fetch2.default.default ?? import_openapi_fetch2.default;
152
+ return createClientFn({
153
+ baseUrl: opts.baseUrl,
154
+ headers: opts.headers,
155
+ fetch: opts.fetch
156
+ });
157
+ }
158
+
159
+ // src/openapi/lifecycleClient.ts
160
+ var import_openapi_fetch3 = __toESM(require("openapi-fetch"), 1);
148
161
  function readEnvApiKey() {
149
162
  const env = globalThis?.process?.env;
150
163
  const v = env?.OPEN_SANDBOX_API_KEY;
@@ -158,7 +171,7 @@ function createLifecycleClient(opts = {}) {
158
171
  if (apiKey && !headers["OPEN-SANDBOX-API-KEY"]) {
159
172
  headers["OPEN-SANDBOX-API-KEY"] = apiKey;
160
173
  }
161
- const createClientFn = import_openapi_fetch2.default.default ?? import_openapi_fetch2.default;
174
+ const createClientFn = import_openapi_fetch3.default.default ?? import_openapi_fetch3.default;
162
175
  return createClientFn({
163
176
  baseUrl: opts.baseUrl ?? "http://localhost:8080/v1",
164
177
  headers,
@@ -322,11 +335,61 @@ function joinUrl(baseUrl, pathname) {
322
335
  return `${base}${path}`;
323
336
  }
324
337
  function toRunCommandRequest(command, opts) {
325
- return {
338
+ if (opts?.gid != null && opts.uid == null) {
339
+ throw new Error("uid is required when gid is provided");
340
+ }
341
+ const body = {
326
342
  command,
327
343
  cwd: opts?.workingDirectory,
328
344
  background: !!opts?.background
329
345
  };
346
+ if (opts?.timeoutSeconds != null) {
347
+ body.timeout = Math.round(opts.timeoutSeconds * 1e3);
348
+ }
349
+ if (opts?.uid != null) {
350
+ body.uid = opts.uid;
351
+ }
352
+ if (opts?.gid != null) {
353
+ body.gid = opts.gid;
354
+ }
355
+ if (opts?.envs != null) {
356
+ body.envs = opts.envs;
357
+ }
358
+ return body;
359
+ }
360
+ function toRunInSessionRequest(command, opts) {
361
+ const body = {
362
+ command
363
+ };
364
+ if (opts?.workingDirectory != null) {
365
+ body.cwd = opts.workingDirectory;
366
+ }
367
+ if (opts?.timeout != null) {
368
+ body.timeout = opts.timeout;
369
+ }
370
+ return body;
371
+ }
372
+ function inferForegroundExitCode(execution) {
373
+ const errorValue = execution.error?.value?.trim();
374
+ const parsedExitCode = errorValue && /^-?\d+$/.test(errorValue) ? Number(errorValue) : Number.NaN;
375
+ return execution.error != null ? Number.isFinite(parsedExitCode) ? parsedExitCode : null : execution.complete ? 0 : null;
376
+ }
377
+ function assertNonBlank(value, field) {
378
+ if (!value.trim()) {
379
+ throw new Error(`${field} cannot be empty`);
380
+ }
381
+ }
382
+ function parseOptionalDate(value, field) {
383
+ if (value == null) return void 0;
384
+ if (value instanceof Date) return value;
385
+ if (typeof value !== "string") {
386
+ throw new Error(`Invalid ${field}: expected ISO string, got ${typeof value}`);
387
+ }
388
+ const parsed = new Date(value);
389
+ if (Number.isNaN(parsed.getTime())) {
390
+ throw new Error(`Invalid ${field}: ${value}`);
391
+ }
392
+ return parsed;
330
393
  }
331
394
  var CommandsAdapter = class {
332
395
  constructor(client, opts) {
@@ -335,43 +398,172 @@ var CommandsAdapter = class {
335
398
  this.fetch = opts.fetch ?? fetch;
336
399
  }
337
400
  fetch;
338
- async interrupt(sessionId) {
339
- const { error, response } = await this.client.DELETE("/command", {
340
- params: { query: { id: sessionId } }
341
- });
342
- throwOnOpenApiFetchError({ error, response }, "Interrupt command failed");
401
+ buildRunStreamSpec(command, opts) {
402
+ assertNonBlank(command, "command");
403
+ return {
404
+ pathname: "/command",
405
+ body: toRunCommandRequest(command, opts),
406
+ fallbackErrorMessage: "Run command failed"
407
+ };
343
408
  }
344
- async *runStream(command, opts, signal) {
345
- const url = joinUrl(this.opts.baseUrl, "/command");
346
- const body = JSON.stringify(toRunCommandRequest(command, opts));
409
+ buildRunInSessionStreamSpec(sessionId, command, opts) {
410
+ assertNonBlank(sessionId, "sessionId");
411
+ assertNonBlank(command, "command");
412
+ return {
413
+ pathname: `/session/${encodeURIComponent(sessionId)}/run`,
414
+ body: toRunInSessionRequest(command, opts),
415
+ fallbackErrorMessage: "Run in session failed"
416
+ };
417
+ }
418
+ async *streamExecution(spec, signal) {
419
+ const url = joinUrl(this.opts.baseUrl, spec.pathname);
347
420
  const res = await this.fetch(url, {
348
421
  method: "POST",
349
422
  headers: {
350
- "accept": "text/event-stream",
423
+ accept: "text/event-stream",
351
424
  "content-type": "application/json",
352
425
  ...this.opts.headers ?? {}
353
426
  },
354
- body,
427
+ body: JSON.stringify(spec.body),
355
428
  signal
356
429
  });
357
- for await (const ev of parseJsonEventStream(res, { fallbackErrorMessage: "Run command failed" })) {
430
+ for await (const ev of parseJsonEventStream(res, {
431
+ fallbackErrorMessage: spec.fallbackErrorMessage
432
+ })) {
358
433
  yield ev;
359
434
  }
360
435
  }
361
- async run(command, opts, handlers, signal) {
436
+ async consumeExecutionStream(stream, handlers, inferExitCode = false) {
362
437
  const execution = {
363
438
  logs: { stdout: [], stderr: [] },
364
439
  result: []
365
440
  };
366
441
  const dispatcher = new ExecutionEventDispatcher(execution, handlers);
367
- for await (const ev of this.runStream(command, opts, signal)) {
442
+ for await (const ev of stream) {
368
443
  if (ev.type === "init" && (ev.text ?? "") === "" && execution.id) {
369
444
  ev.text = execution.id;
370
445
  }
371
446
  await dispatcher.dispatch(ev);
372
447
  }
448
+ if (inferExitCode) {
449
+ execution.exitCode = inferForegroundExitCode(execution);
450
+ }
373
451
  return execution;
374
452
  }
453
+ async interrupt(sessionId) {
454
+ const { error, response } = await this.client.DELETE("/command", {
455
+ params: { query: { id: sessionId } }
456
+ });
457
+ throwOnOpenApiFetchError({ error, response }, "Interrupt command failed");
458
+ }
459
+ async getCommandStatus(commandId) {
460
+ const { data, error, response } = await this.client.GET("/command/status/{id}", {
461
+ params: { path: { id: commandId } }
462
+ });
463
+ throwOnOpenApiFetchError({ error, response }, "Get command status failed");
464
+ const ok = data;
465
+ if (!ok || typeof ok !== "object") {
466
+ throw new Error("Get command status failed: unexpected response shape");
467
+ }
468
+ return {
469
+ id: ok.id,
470
+ content: ok.content,
471
+ running: ok.running,
472
+ exitCode: ok.exit_code ?? null,
473
+ error: ok.error,
474
+ startedAt: parseOptionalDate(ok.started_at, "startedAt"),
475
+ finishedAt: parseOptionalDate(ok.finished_at, "finishedAt") ?? null
476
+ };
477
+ }
478
+ async getBackgroundCommandLogs(commandId, cursor) {
479
+ const { data, error, response } = await this.client.GET("/command/{id}/logs", {
480
+ params: { path: { id: commandId }, query: cursor == null ? {} : { cursor } },
481
+ parseAs: "text"
482
+ });
483
+ throwOnOpenApiFetchError({ error, response }, "Get command logs failed");
484
+ const ok = data;
485
+ if (typeof ok !== "string") {
486
+ throw new Error("Get command logs failed: unexpected response shape");
487
+ }
488
+ const cursorHeader = response.headers.get("EXECD-COMMANDS-TAIL-CURSOR");
489
+ const parsedCursor = cursorHeader != null && cursorHeader !== "" ? Number(cursorHeader) : void 0;
490
+ return {
491
+ content: ok,
492
+ cursor: Number.isFinite(parsedCursor ?? NaN) ? parsedCursor : void 0
493
+ };
494
+ }
495
+ async *runStream(command, opts, signal) {
496
+ for await (const ev of this.streamExecution(
497
+ this.buildRunStreamSpec(command, opts),
498
+ signal
499
+ )) {
500
+ yield ev;
501
+ }
502
+ }
503
+ async run(command, opts, handlers, signal) {
504
+ return this.consumeExecutionStream(
505
+ this.runStream(command, opts, signal),
506
+ handlers,
507
+ !opts?.background
508
+ );
509
+ }
510
+ async createSession(options) {
511
+ const body = options?.workingDirectory != null ? { cwd: options.workingDirectory } : {};
512
+ const { data, error, response } = await this.client.POST("/session", {
513
+ body
514
+ });
515
+ throwOnOpenApiFetchError({ error, response }, "Create session failed");
516
+ const ok = data;
517
+ if (!ok || typeof ok.session_id !== "string") {
518
+ throw new Error("Create session failed: unexpected response shape");
519
+ }
520
+ return ok.session_id;
521
+ }
522
+ async *runInSessionStream(sessionId, command, opts, signal) {
523
+ for await (const ev of this.streamExecution(
524
+ this.buildRunInSessionStreamSpec(sessionId, command, opts),
525
+ signal
526
+ )) {
527
+ yield ev;
528
+ }
529
+ }
530
+ async runInSession(sessionId, command, options, handlers, signal) {
531
+ return this.consumeExecutionStream(
532
+ this.runInSessionStream(sessionId, command, options, signal),
533
+ handlers,
534
+ true
535
+ );
536
+ }
537
+ async deleteSession(sessionId) {
538
+ const { error, response } = await this.client.DELETE(
539
+ "/session/{sessionId}",
540
+ { params: { path: { sessionId } } }
541
+ );
542
+ throwOnOpenApiFetchError({ error, response }, "Delete session failed");
543
+ }
544
+ };
545
+
546
+ // src/adapters/egressAdapter.ts
547
+ var EgressAdapter = class {
548
+ constructor(client) {
549
+ this.client = client;
550
+ }
551
+ async getPolicy() {
552
+ const { data, error, response } = await this.client.GET("/policy");
553
+ throwOnOpenApiFetchError({ error, response }, "Get sandbox egress policy failed");
554
+ const raw = data;
555
+ if (!raw || typeof raw !== "object" || !raw.policy || typeof raw.policy !== "object") {
556
+ throw new Error("Get sandbox egress policy failed: unexpected response shape");
557
+ }
558
+ return raw.policy;
559
+ }
560
+ async patchRules(rules) {
561
+ const body = rules;
562
+ const { error, response } = await this.client.PATCH("/policy", {
563
+ body
564
+ });
565
+ throwOnOpenApiFetchError({ error, response }, "Patch sandbox egress rules failed");
566
+ }
375
567
  };
376
568
 
377
569
  // src/adapters/filesystemAdapter.ts
@@ -859,11 +1051,15 @@ var SandboxesAdapter = class {
859
1051
  }
860
1052
  return d;
861
1053
  }
1054
+ parseOptionalIsoDate(field, v) {
1055
+ if (v == null) return null;
1056
+ return this.parseIsoDate(field, v);
1057
+ }
862
1058
  mapSandboxInfo(raw) {
863
1059
  return {
864
1060
  ...raw ?? {},
865
1061
  createdAt: this.parseIsoDate("createdAt", raw?.createdAt),
866
- expiresAt: this.parseIsoDate("expiresAt", raw?.expiresAt)
1062
+ expiresAt: this.parseOptionalIsoDate("expiresAt", raw?.expiresAt)
867
1063
  };
868
1064
  }
869
1065
  async createSandbox(req) {
@@ -879,7 +1075,7 @@ var SandboxesAdapter = class {
879
1075
  return {
880
1076
  ...raw ?? {},
881
1077
  createdAt: this.parseIsoDate("createdAt", raw?.createdAt),
882
- expiresAt: this.parseIsoDate("expiresAt", raw?.expiresAt)
1078
+ expiresAt: this.parseOptionalIsoDate("expiresAt", raw?.expiresAt)
883
1079
  };
884
1080
  }
885
1081
  async getSandbox(sandboxId) {
@@ -950,9 +1146,9 @@ var SandboxesAdapter = class {
950
1146
  expiresAt: raw?.expiresAt ? this.parseIsoDate("expiresAt", raw.expiresAt) : void 0
951
1147
  };
952
1148
  }
953
- async getSandboxEndpoint(sandboxId, port) {
1149
+ async getSandboxEndpoint(sandboxId, port, useServerProxy = false) {
954
1150
  const { data, error, response } = await this.client.GET("/sandboxes/{sandboxId}/endpoints/{port}", {
955
- params: { path: { sandboxId, port } }
1151
+ params: { path: { sandboxId, port }, query: { use_server_proxy: useServerProxy } }
956
1152
  });
957
1153
  throwOnOpenApiFetchError({ error, response }, "Get sandbox endpoint failed");
958
1154
  const ok = data;
@@ -976,9 +1172,13 @@ var DefaultAdapterFactory = class {
976
1172
  return { sandboxes };
977
1173
  }
978
1174
  createExecdStack(opts) {
1175
+ const headers = {
1176
+ ...opts.connectionConfig.headers ?? {},
1177
+ ...opts.endpointHeaders ?? {}
1178
+ };
979
1179
  const execdClient = createExecdClient({
980
1180
  baseUrl: opts.execdBaseUrl,
981
- headers: opts.connectionConfig.headers,
1181
+ headers,
982
1182
  fetch: opts.connectionConfig.fetch
983
1183
  });
984
1184
  const health = new HealthAdapter(execdClient);
@@ -986,12 +1186,12 @@ var DefaultAdapterFactory = class {
986
1186
  const files = new FilesystemAdapter(execdClient, {
987
1187
  baseUrl: opts.execdBaseUrl,
988
1188
  fetch: opts.connectionConfig.fetch,
989
- headers: opts.connectionConfig.headers
1189
+ headers
990
1190
  });
991
1191
  const commands = new CommandsAdapter(execdClient, {
992
1192
  baseUrl: opts.execdBaseUrl,
993
1193
  fetch: opts.connectionConfig.sseFetch,
994
- headers: opts.connectionConfig.headers
1194
+ headers
995
1195
  });
996
1196
  return {
997
1197
  commands,
@@ -1000,6 +1200,20 @@ var DefaultAdapterFactory = class {
1000
1200
  metrics
1001
1201
  };
1002
1202
  }
1203
+ createEgressStack(opts) {
1204
+ const headers = {
1205
+ ...opts.connectionConfig.headers ?? {},
1206
+ ...opts.endpointHeaders ?? {}
1207
+ };
1208
+ const egressClient = createEgressClient({
1209
+ baseUrl: opts.egressBaseUrl,
1210
+ headers,
1211
+ fetch: opts.connectionConfig.fetch
1212
+ });
1213
+ return {
1214
+ egress: new EgressAdapter(egressClient)
1215
+ };
1216
+ }
1003
1217
  };
1004
1218
  function createDefaultAdapterFactory() {
1005
1219
  return new DefaultAdapterFactory();
@@ -1007,6 +1221,7 @@ function createDefaultAdapterFactory() {
1007
1221
 
1008
1222
  // src/core/constants.ts
1009
1223
  var DEFAULT_EXECD_PORT = 44772;
1224
+ var DEFAULT_EGRESS_PORT = 18080;
1010
1225
  var DEFAULT_ENTRYPOINT = ["tail", "-f", "/dev/null"];
1011
1226
  var DEFAULT_RESOURCE_LIMITS = {
1012
1227
  cpu: "1",
@@ -1016,7 +1231,7 @@ var DEFAULT_TIMEOUT_SECONDS = 600;
1016
1231
  var DEFAULT_READY_TIMEOUT_SECONDS = 30;
1017
1232
  var DEFAULT_HEALTH_CHECK_POLLING_INTERVAL_MILLIS = 200;
1018
1233
  var DEFAULT_REQUEST_TIMEOUT_SECONDS = 30;
1019
- var DEFAULT_USER_AGENT = "OpenSandbox-JS-SDK/0.1.1";
1234
+ var DEFAULT_USER_AGENT = "OpenSandbox-JS-SDK/0.1.5";
1020
1235
 
1021
1236
  // src/config/connection.ts
1022
1237
  function isNodeRuntime2() {
@@ -1168,6 +1383,10 @@ var ConnectionConfig = class _ConnectionConfig {
1168
1383
  requestTimeoutSeconds;
1169
1384
  debug;
1170
1385
  userAgent = DEFAULT_USER_AGENT;
1386
+ /**
1387
+ * Use sandbox server as proxy for endpoint requests (default false).
1388
+ */
1389
+ useServerProxy;
1171
1390
  _closeTransport;
1172
1391
  _closePromise = null;
1173
1392
  _transportInitialized = false;
@@ -1188,6 +1407,7 @@ var ConnectionConfig = class _ConnectionConfig {
1188
1407
  this.apiKey = opts.apiKey ?? envApiKey;
1189
1408
  this.requestTimeoutSeconds = typeof opts.requestTimeoutSeconds === "number" ? opts.requestTimeoutSeconds : 30;
1190
1409
  this.debug = !!opts.debug;
1410
+ this.useServerProxy = !!opts.useServerProxy;
1191
1411
  const headers = { ...opts.headers ?? {} };
1192
1412
  if (this.apiKey && !headers["OPEN-SANDBOX-API-KEY"]) {
1193
1413
  headers["OPEN-SANDBOX-API-KEY"] = this.apiKey;
@@ -1251,7 +1471,8 @@ var ConnectionConfig = class _ConnectionConfig {
1251
1471
  apiKey: this.apiKey,
1252
1472
  headers: { ...this.headers },
1253
1473
  requestTimeoutSeconds: this.requestTimeoutSeconds,
1254
- debug: this.debug
1474
+ debug: this.debug,
1475
+ useServerProxy: this.useServerProxy
1255
1476
  });
1256
1477
  clone.initializeTransport();
1257
1478
  return clone;
@@ -1367,7 +1588,8 @@ var Sandbox = class _Sandbox {
1367
1588
  _Sandbox._priv.set(this, {
1368
1589
  adapterFactory: opts.adapterFactory,
1369
1590
  lifecycleBaseUrl: opts.lifecycleBaseUrl,
1370
- execdBaseUrl: opts.execdBaseUrl
1591
+ execdBaseUrl: opts.execdBaseUrl,
1592
+ egress: opts.egress
1371
1593
  });
1372
1594
  this.sandboxes = opts.sandboxes;
1373
1595
  this.commands = opts.commands;
@@ -1390,10 +1612,31 @@ var Sandbox = class _Sandbox {
1390
1612
  await connectionConfig.closeTransport();
1391
1613
  throw err;
1392
1614
  }
1615
+ if (opts.volumes) {
1616
+ for (const vol of opts.volumes) {
1617
+ const backendsSpecified = [vol.host, vol.pvc, vol.ossfs].filter((b) => b != null).length;
1618
+ if (backendsSpecified === 0) {
1619
+ throw new Error(
1620
+ `Volume '${vol.name}' must specify exactly one backend (host, pvc, ossfs), but none was provided.`
1621
+ );
1622
+ }
1623
+ if (backendsSpecified > 1) {
1624
+ throw new Error(
1625
+ `Volume '${vol.name}' must specify exactly one backend (host, pvc, ossfs), but multiple were provided.`
1626
+ );
1627
+ }
1628
+ }
1629
+ }
1630
+ const rawTimeout = opts.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
1631
+ const timeoutSeconds = opts.timeoutSeconds === null ? null : Math.floor(rawTimeout);
1632
+ if (timeoutSeconds !== null && !Number.isFinite(timeoutSeconds)) {
1633
+ throw new Error(
1634
+ `timeoutSeconds must be a finite number, got ${opts.timeoutSeconds}`
1635
+ );
1636
+ }
1393
1637
  const req = {
1394
1638
  image: toImageSpec(opts.image),
1395
1639
  entrypoint: opts.entrypoint ?? DEFAULT_ENTRYPOINT,
1396
- timeout: Math.floor(opts.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS),
1397
1640
  resourceLimits: opts.resource ?? DEFAULT_RESOURCE_LIMITS,
1398
1641
  env: opts.env ?? {},
1399
1642
  metadata: opts.metadata ?? {},
@@ -1401,20 +1644,37 @@ var Sandbox = class _Sandbox {
1401
1644
  ...opts.networkPolicy,
1402
1645
  defaultAction: opts.networkPolicy.defaultAction ?? "deny"
1403
1646
  } : void 0,
1647
+ volumes: opts.volumes,
1404
1648
  extensions: opts.extensions ?? {}
1405
1649
  };
1650
+ if (timeoutSeconds !== null) {
1651
+ req.timeout = timeoutSeconds;
1652
+ }
1406
1653
  let sandboxId;
1407
1654
  try {
1408
1655
  const created = await sandboxes.createSandbox(req);
1409
1656
  sandboxId = created.id;
1410
1657
  const endpoint = await sandboxes.getSandboxEndpoint(
1411
1658
  sandboxId,
1412
- DEFAULT_EXECD_PORT
1659
+ DEFAULT_EXECD_PORT,
1660
+ connectionConfig.useServerProxy
1661
+ );
1662
+ const egressEndpoint = await sandboxes.getSandboxEndpoint(
1663
+ sandboxId,
1664
+ DEFAULT_EGRESS_PORT,
1665
+ connectionConfig.useServerProxy
1413
1666
  );
1414
1667
  const execdBaseUrl = `${connectionConfig.protocol}://${endpoint.endpoint}`;
1668
+ const egressBaseUrl = `${connectionConfig.protocol}://${egressEndpoint.endpoint}`;
1415
1669
  const { commands, files, health, metrics } = adapterFactory.createExecdStack({
1416
1670
  connectionConfig,
1417
- execdBaseUrl
1671
+ execdBaseUrl,
1672
+ endpointHeaders: endpoint.headers
1673
+ });
1674
+ const { egress } = adapterFactory.createEgressStack({
1675
+ connectionConfig,
1676
+ egressBaseUrl,
1677
+ endpointHeaders: egressEndpoint.headers
1418
1678
  });
1419
1679
  const sbx = new _Sandbox({
1420
1680
  id: sandboxId,
@@ -1426,7 +1686,8 @@ var Sandbox = class _Sandbox {
1426
1686
  commands,
1427
1687
  files,
1428
1688
  health,
1429
- metrics
1689
+ metrics,
1690
+ egress
1430
1691
  });
1431
1692
  if (!(opts.skipHealthCheck ?? false)) {
1432
1693
  await sbx.waitUntilReady({
@@ -1465,12 +1726,25 @@ var Sandbox = class _Sandbox {
1465
1726
  try {
1466
1727
  const endpoint = await sandboxes.getSandboxEndpoint(
1467
1728
  opts.sandboxId,
1468
- DEFAULT_EXECD_PORT
1729
+ DEFAULT_EXECD_PORT,
1730
+ connectionConfig.useServerProxy
1731
+ );
1732
+ const egressEndpoint = await sandboxes.getSandboxEndpoint(
1733
+ opts.sandboxId,
1734
+ DEFAULT_EGRESS_PORT,
1735
+ connectionConfig.useServerProxy
1469
1736
  );
1470
1737
  const execdBaseUrl = `${connectionConfig.protocol}://${endpoint.endpoint}`;
1738
+ const egressBaseUrl = `${connectionConfig.protocol}://${egressEndpoint.endpoint}`;
1471
1739
  const { commands, files, health, metrics } = adapterFactory.createExecdStack({
1472
1740
  connectionConfig,
1473
- execdBaseUrl
1741
+ execdBaseUrl,
1742
+ endpointHeaders: endpoint.headers
1743
+ });
1744
+ const { egress } = adapterFactory.createEgressStack({
1745
+ connectionConfig,
1746
+ egressBaseUrl,
1747
+ endpointHeaders: egressEndpoint.headers
1474
1748
  });
1475
1749
  const sbx = new _Sandbox({
1476
1750
  id: opts.sandboxId,
@@ -1482,7 +1756,8 @@ var Sandbox = class _Sandbox {
1482
1756
  commands,
1483
1757
  files,
1484
1758
  health,
1485
- metrics
1759
+ metrics,
1760
+ egress
1486
1761
  });
1487
1762
  if (!(opts.skipHealthCheck ?? false)) {
1488
1763
  await sbx.waitUntilReady({
@@ -1570,11 +1845,21 @@ var Sandbox = class _Sandbox {
1570
1845
  ).toISOString();
1571
1846
  return await this.sandboxes.renewSandboxExpiration(this.id, { expiresAt });
1572
1847
  }
1848
+ async getEgressPolicy() {
1849
+ return await _Sandbox._priv.get(this).egress.getPolicy();
1850
+ }
1851
+ async patchEgressRules(rules) {
1852
+ await _Sandbox._priv.get(this).egress.patchRules(rules);
1853
+ }
1573
1854
  /**
1574
1855
  * Get sandbox endpoint for a port (STRICT: no scheme), e.g. "localhost:44772" or "domain/route/.../44772".
1575
1856
  */
1576
1857
  async getEndpoint(port) {
1577
- return await this.sandboxes.getSandboxEndpoint(this.id, port);
1858
+ return await this.sandboxes.getSandboxEndpoint(
1859
+ this.id,
1860
+ port,
1861
+ this.connectionConfig.useServerProxy
1862
+ );
1578
1863
  }
1579
1864
  /**
1580
1865
  * Get absolute endpoint URL with scheme (convenience for HTTP clients).
@@ -1585,21 +1870,39 @@ var Sandbox = class _Sandbox {
1585
1870
  }
1586
1871
  async waitUntilReady(opts) {
1587
1872
  const deadline = Date.now() + opts.readyTimeoutSeconds * 1e3;
1873
+ let attempt = 0;
1874
+ let errorDetail = "Health check returned false continuously.";
1875
+ const buildTimeoutMessage = () => {
1876
+ const context = `domain=${this.connectionConfig.domain}, useServerProxy=${this.connectionConfig.useServerProxy}`;
1877
+ let suggestion = "If this sandbox runs in Docker bridge or remote-network mode, consider enabling useServerProxy=true.";
1878
+ if (!this.connectionConfig.useServerProxy) {
1879
+ suggestion += " You can also configure server-side [docker].host_ip for direct endpoint access.";
1880
+ }
1881
+ return `Sandbox health check timed out after ${opts.readyTimeoutSeconds}s (${attempt} attempts). ${errorDetail} Connection context: ${context}. ${suggestion}`;
1882
+ };
1588
1883
  while (true) {
1589
1884
  if (Date.now() > deadline) {
1590
1885
  throw new SandboxReadyTimeoutException({
1591
- message: `Sandbox not ready: timed out waiting for health check (timeoutSeconds=${opts.readyTimeoutSeconds})`
1886
+ message: buildTimeoutMessage()
1592
1887
  });
1593
1888
  }
1889
+ attempt++;
1594
1890
  try {
1595
1891
  if (opts.healthCheck) {
1596
1892
  const ok = await opts.healthCheck(this);
1597
- if (ok) return;
1893
+ if (ok) {
1894
+ return;
1895
+ }
1598
1896
  } else {
1599
1897
  const ok = await this.health.ping();
1600
- if (ok) return;
1898
+ if (ok) {
1899
+ return;
1900
+ }
1601
1901
  }
1602
- } catch {
1902
+ errorDetail = "Health check returned false continuously.";
1903
+ } catch (err) {
1904
+ const message = err instanceof Error ? err.message : String(err);
1905
+ errorDetail = `Last health check error: ${message}`;
1603
1906
  }
1604
1907
  await sleep(opts.pollingIntervalMillis);
1605
1908
  }
@@ -1608,6 +1911,7 @@ var Sandbox = class _Sandbox {
1608
1911
  // Annotate the CommonJS export names for ESM import in node:
1609
1912
  0 && (module.exports = {
1610
1913
  ConnectionConfig,
1914
+ DEFAULT_EGRESS_PORT,
1611
1915
  DEFAULT_ENTRYPOINT,
1612
1916
  DEFAULT_EXECD_PORT,
1613
1917
  DEFAULT_HEALTH_CHECK_POLLING_INTERVAL_MILLIS,