@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.
@@ -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,6 +335,9 @@ function joinUrl(baseUrl, pathname) {
322
335
  return `${base}${path}`;
323
336
  }
324
337
  function toRunCommandRequest(command, opts) {
338
+ if (opts?.gid != null && opts.uid == null) {
339
+ throw new Error("uid is required when gid is provided");
340
+ }
325
341
  const body = {
326
342
  command,
327
343
  cwd: opts?.workingDirectory,
@@ -330,8 +346,39 @@ function toRunCommandRequest(command, opts) {
330
346
  if (opts?.timeoutSeconds != null) {
331
347
  body.timeout = Math.round(opts.timeoutSeconds * 1e3);
332
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
+ }
333
358
  return body;
334
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?.timeoutSeconds != null) {
368
+ body.timeout = Math.round(opts.timeoutSeconds * 1e3);
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
+ }
335
382
  function parseOptionalDate(value, field) {
336
383
  if (value == null) return void 0;
337
384
  if (value instanceof Date) return value;
@@ -351,6 +398,58 @@ var CommandsAdapter = class {
351
398
  this.fetch = opts.fetch ?? fetch;
352
399
  }
353
400
  fetch;
401
+ buildRunStreamSpec(command, opts) {
402
+ assertNonBlank(command, "command");
403
+ return {
404
+ pathname: "/command",
405
+ body: toRunCommandRequest(command, opts),
406
+ fallbackErrorMessage: "Run command failed"
407
+ };
408
+ }
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);
420
+ const res = await this.fetch(url, {
421
+ method: "POST",
422
+ headers: {
423
+ accept: "text/event-stream",
424
+ "content-type": "application/json",
425
+ ...this.opts.headers ?? {}
426
+ },
427
+ body: JSON.stringify(spec.body),
428
+ signal
429
+ });
430
+ for await (const ev of parseJsonEventStream(res, {
431
+ fallbackErrorMessage: spec.fallbackErrorMessage
432
+ })) {
433
+ yield ev;
434
+ }
435
+ }
436
+ async consumeExecutionStream(stream, handlers, inferExitCode = false) {
437
+ const execution = {
438
+ logs: { stdout: [], stderr: [] },
439
+ result: []
440
+ };
441
+ const dispatcher = new ExecutionEventDispatcher(execution, handlers);
442
+ for await (const ev of stream) {
443
+ if (ev.type === "init" && (ev.text ?? "") === "" && execution.id) {
444
+ ev.text = execution.id;
445
+ }
446
+ await dispatcher.dispatch(ev);
447
+ }
448
+ if (inferExitCode) {
449
+ execution.exitCode = inferForegroundExitCode(execution);
450
+ }
451
+ return execution;
452
+ }
354
453
  async interrupt(sessionId) {
355
454
  const { error, response } = await this.client.DELETE("/command", {
356
455
  params: { query: { id: sessionId } }
@@ -394,35 +493,76 @@ var CommandsAdapter = class {
394
493
  };
395
494
  }
396
495
  async *runStream(command, opts, signal) {
397
- const url = joinUrl(this.opts.baseUrl, "/command");
398
- const body = JSON.stringify(toRunCommandRequest(command, opts));
399
- const res = await this.fetch(url, {
400
- method: "POST",
401
- headers: {
402
- "accept": "text/event-stream",
403
- "content-type": "application/json",
404
- ...this.opts.headers ?? {}
405
- },
406
- body,
496
+ for await (const ev of this.streamExecution(
497
+ this.buildRunStreamSpec(command, opts),
407
498
  signal
408
- });
409
- for await (const ev of parseJsonEventStream(res, { fallbackErrorMessage: "Run command failed" })) {
499
+ )) {
410
500
  yield ev;
411
501
  }
412
502
  }
413
503
  async run(command, opts, handlers, signal) {
414
- const execution = {
415
- logs: { stdout: [], stderr: [] },
416
- result: []
417
- };
418
- const dispatcher = new ExecutionEventDispatcher(execution, handlers);
419
- for await (const ev of this.runStream(command, opts, signal)) {
420
- if (ev.type === "init" && (ev.text ?? "") === "" && execution.id) {
421
- ev.text = execution.id;
422
- }
423
- await dispatcher.dispatch(ev);
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");
424
519
  }
425
- return execution;
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");
426
566
  }
427
567
  };
428
568
 
@@ -911,11 +1051,15 @@ var SandboxesAdapter = class {
911
1051
  }
912
1052
  return d;
913
1053
  }
1054
+ parseOptionalIsoDate(field, v) {
1055
+ if (v == null) return null;
1056
+ return this.parseIsoDate(field, v);
1057
+ }
914
1058
  mapSandboxInfo(raw) {
915
1059
  return {
916
1060
  ...raw ?? {},
917
1061
  createdAt: this.parseIsoDate("createdAt", raw?.createdAt),
918
- expiresAt: this.parseIsoDate("expiresAt", raw?.expiresAt)
1062
+ expiresAt: this.parseOptionalIsoDate("expiresAt", raw?.expiresAt)
919
1063
  };
920
1064
  }
921
1065
  async createSandbox(req) {
@@ -931,7 +1075,7 @@ var SandboxesAdapter = class {
931
1075
  return {
932
1076
  ...raw ?? {},
933
1077
  createdAt: this.parseIsoDate("createdAt", raw?.createdAt),
934
- expiresAt: this.parseIsoDate("expiresAt", raw?.expiresAt)
1078
+ expiresAt: this.parseOptionalIsoDate("expiresAt", raw?.expiresAt)
935
1079
  };
936
1080
  }
937
1081
  async getSandbox(sandboxId) {
@@ -1056,6 +1200,20 @@ var DefaultAdapterFactory = class {
1056
1200
  metrics
1057
1201
  };
1058
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
+ }
1059
1217
  };
1060
1218
  function createDefaultAdapterFactory() {
1061
1219
  return new DefaultAdapterFactory();
@@ -1063,6 +1221,7 @@ function createDefaultAdapterFactory() {
1063
1221
 
1064
1222
  // src/core/constants.ts
1065
1223
  var DEFAULT_EXECD_PORT = 44772;
1224
+ var DEFAULT_EGRESS_PORT = 18080;
1066
1225
  var DEFAULT_ENTRYPOINT = ["tail", "-f", "/dev/null"];
1067
1226
  var DEFAULT_RESOURCE_LIMITS = {
1068
1227
  cpu: "1",
@@ -1072,7 +1231,7 @@ var DEFAULT_TIMEOUT_SECONDS = 600;
1072
1231
  var DEFAULT_READY_TIMEOUT_SECONDS = 30;
1073
1232
  var DEFAULT_HEALTH_CHECK_POLLING_INTERVAL_MILLIS = 200;
1074
1233
  var DEFAULT_REQUEST_TIMEOUT_SECONDS = 30;
1075
- var DEFAULT_USER_AGENT = "OpenSandbox-JS-SDK/0.1.4";
1234
+ var DEFAULT_USER_AGENT = "OpenSandbox-JS-SDK/0.1.6";
1076
1235
 
1077
1236
  // src/config/connection.ts
1078
1237
  function isNodeRuntime2() {
@@ -1393,6 +1552,7 @@ var SandboxManager = class _SandboxManager {
1393
1552
  };
1394
1553
 
1395
1554
  // src/sandbox.ts
1555
+ var HOST_PATH_PATTERN = /^([/]|[A-Za-z]:[\\/])/;
1396
1556
  function sleep(ms) {
1397
1557
  return new Promise((r) => setTimeout(r, ms));
1398
1558
  }
@@ -1429,7 +1589,8 @@ var Sandbox = class _Sandbox {
1429
1589
  _Sandbox._priv.set(this, {
1430
1590
  adapterFactory: opts.adapterFactory,
1431
1591
  lifecycleBaseUrl: opts.lifecycleBaseUrl,
1432
- execdBaseUrl: opts.execdBaseUrl
1592
+ execdBaseUrl: opts.execdBaseUrl,
1593
+ egress: opts.egress
1433
1594
  });
1434
1595
  this.sandboxes = opts.sandboxes;
1435
1596
  this.commands = opts.commands;
@@ -1438,6 +1599,26 @@ var Sandbox = class _Sandbox {
1438
1599
  this.metrics = opts.metrics;
1439
1600
  }
1440
1601
  static async create(opts) {
1602
+ if (opts.volumes) {
1603
+ for (const vol of opts.volumes) {
1604
+ const backendsSpecified = [vol.host, vol.pvc, vol.ossfs].filter((b) => b != null).length;
1605
+ if (backendsSpecified === 0) {
1606
+ throw new Error(
1607
+ `Volume '${vol.name}' must specify exactly one backend (host, pvc, ossfs), but none was provided.`
1608
+ );
1609
+ }
1610
+ if (backendsSpecified > 1) {
1611
+ throw new Error(
1612
+ `Volume '${vol.name}' must specify exactly one backend (host, pvc, ossfs), but multiple were provided.`
1613
+ );
1614
+ }
1615
+ if (vol.host && !HOST_PATH_PATTERN.test(vol.host.path)) {
1616
+ throw new Error(
1617
+ "Host path must be an absolute path starting with '/' or a Windows drive letter (e.g. 'C:\\' or 'D:/')"
1618
+ );
1619
+ }
1620
+ }
1621
+ }
1441
1622
  const baseConnectionConfig = opts.connectionConfig instanceof ConnectionConfig ? opts.connectionConfig : new ConnectionConfig(opts.connectionConfig);
1442
1623
  const connectionConfig = baseConnectionConfig.withTransportIfMissing();
1443
1624
  const lifecycleBaseUrl = connectionConfig.getBaseUrl();
@@ -1452,25 +1633,16 @@ var Sandbox = class _Sandbox {
1452
1633
  await connectionConfig.closeTransport();
1453
1634
  throw err;
1454
1635
  }
1455
- if (opts.volumes) {
1456
- for (const vol of opts.volumes) {
1457
- const backendsSpecified = [vol.host, vol.pvc].filter((b) => b !== void 0).length;
1458
- if (backendsSpecified === 0) {
1459
- throw new Error(
1460
- `Volume '${vol.name}' must specify exactly one backend (host, pvc), but none was provided.`
1461
- );
1462
- }
1463
- if (backendsSpecified > 1) {
1464
- throw new Error(
1465
- `Volume '${vol.name}' must specify exactly one backend (host, pvc), but multiple were provided.`
1466
- );
1467
- }
1468
- }
1636
+ const rawTimeout = opts.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
1637
+ const timeoutSeconds = opts.timeoutSeconds === null ? null : Math.floor(rawTimeout);
1638
+ if (timeoutSeconds !== null && !Number.isFinite(timeoutSeconds)) {
1639
+ throw new Error(
1640
+ `timeoutSeconds must be a finite number, got ${opts.timeoutSeconds}`
1641
+ );
1469
1642
  }
1470
1643
  const req = {
1471
1644
  image: toImageSpec(opts.image),
1472
1645
  entrypoint: opts.entrypoint ?? DEFAULT_ENTRYPOINT,
1473
- timeout: Math.floor(opts.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS),
1474
1646
  resourceLimits: opts.resource ?? DEFAULT_RESOURCE_LIMITS,
1475
1647
  env: opts.env ?? {},
1476
1648
  metadata: opts.metadata ?? {},
@@ -1481,6 +1653,9 @@ var Sandbox = class _Sandbox {
1481
1653
  volumes: opts.volumes,
1482
1654
  extensions: opts.extensions ?? {}
1483
1655
  };
1656
+ if (timeoutSeconds !== null) {
1657
+ req.timeout = timeoutSeconds;
1658
+ }
1484
1659
  let sandboxId;
1485
1660
  try {
1486
1661
  const created = await sandboxes.createSandbox(req);
@@ -1490,12 +1665,23 @@ var Sandbox = class _Sandbox {
1490
1665
  DEFAULT_EXECD_PORT,
1491
1666
  connectionConfig.useServerProxy
1492
1667
  );
1668
+ const egressEndpoint = await sandboxes.getSandboxEndpoint(
1669
+ sandboxId,
1670
+ DEFAULT_EGRESS_PORT,
1671
+ connectionConfig.useServerProxy
1672
+ );
1493
1673
  const execdBaseUrl = `${connectionConfig.protocol}://${endpoint.endpoint}`;
1674
+ const egressBaseUrl = `${connectionConfig.protocol}://${egressEndpoint.endpoint}`;
1494
1675
  const { commands, files, health, metrics } = adapterFactory.createExecdStack({
1495
1676
  connectionConfig,
1496
1677
  execdBaseUrl,
1497
1678
  endpointHeaders: endpoint.headers
1498
1679
  });
1680
+ const { egress } = adapterFactory.createEgressStack({
1681
+ connectionConfig,
1682
+ egressBaseUrl,
1683
+ endpointHeaders: egressEndpoint.headers
1684
+ });
1499
1685
  const sbx = new _Sandbox({
1500
1686
  id: sandboxId,
1501
1687
  connectionConfig,
@@ -1506,7 +1692,8 @@ var Sandbox = class _Sandbox {
1506
1692
  commands,
1507
1693
  files,
1508
1694
  health,
1509
- metrics
1695
+ metrics,
1696
+ egress
1510
1697
  });
1511
1698
  if (!(opts.skipHealthCheck ?? false)) {
1512
1699
  await sbx.waitUntilReady({
@@ -1548,12 +1735,23 @@ var Sandbox = class _Sandbox {
1548
1735
  DEFAULT_EXECD_PORT,
1549
1736
  connectionConfig.useServerProxy
1550
1737
  );
1738
+ const egressEndpoint = await sandboxes.getSandboxEndpoint(
1739
+ opts.sandboxId,
1740
+ DEFAULT_EGRESS_PORT,
1741
+ connectionConfig.useServerProxy
1742
+ );
1551
1743
  const execdBaseUrl = `${connectionConfig.protocol}://${endpoint.endpoint}`;
1744
+ const egressBaseUrl = `${connectionConfig.protocol}://${egressEndpoint.endpoint}`;
1552
1745
  const { commands, files, health, metrics } = adapterFactory.createExecdStack({
1553
1746
  connectionConfig,
1554
1747
  execdBaseUrl,
1555
1748
  endpointHeaders: endpoint.headers
1556
1749
  });
1750
+ const { egress } = adapterFactory.createEgressStack({
1751
+ connectionConfig,
1752
+ egressBaseUrl,
1753
+ endpointHeaders: egressEndpoint.headers
1754
+ });
1557
1755
  const sbx = new _Sandbox({
1558
1756
  id: opts.sandboxId,
1559
1757
  connectionConfig,
@@ -1564,7 +1762,8 @@ var Sandbox = class _Sandbox {
1564
1762
  commands,
1565
1763
  files,
1566
1764
  health,
1567
- metrics
1765
+ metrics,
1766
+ egress
1568
1767
  });
1569
1768
  if (!(opts.skipHealthCheck ?? false)) {
1570
1769
  await sbx.waitUntilReady({
@@ -1652,6 +1851,12 @@ var Sandbox = class _Sandbox {
1652
1851
  ).toISOString();
1653
1852
  return await this.sandboxes.renewSandboxExpiration(this.id, { expiresAt });
1654
1853
  }
1854
+ async getEgressPolicy() {
1855
+ return await _Sandbox._priv.get(this).egress.getPolicy();
1856
+ }
1857
+ async patchEgressRules(rules) {
1858
+ await _Sandbox._priv.get(this).egress.patchRules(rules);
1859
+ }
1655
1860
  /**
1656
1861
  * Get sandbox endpoint for a port (STRICT: no scheme), e.g. "localhost:44772" or "domain/route/.../44772".
1657
1862
  */
@@ -1671,21 +1876,39 @@ var Sandbox = class _Sandbox {
1671
1876
  }
1672
1877
  async waitUntilReady(opts) {
1673
1878
  const deadline = Date.now() + opts.readyTimeoutSeconds * 1e3;
1879
+ let attempt = 0;
1880
+ let errorDetail = "Health check returned false continuously.";
1881
+ const buildTimeoutMessage = () => {
1882
+ const context = `domain=${this.connectionConfig.domain}, useServerProxy=${this.connectionConfig.useServerProxy}`;
1883
+ let suggestion = "If this sandbox runs in Docker bridge or remote-network mode, consider enabling useServerProxy=true.";
1884
+ if (!this.connectionConfig.useServerProxy) {
1885
+ suggestion += " You can also configure server-side [docker].host_ip for direct endpoint access.";
1886
+ }
1887
+ return `Sandbox health check timed out after ${opts.readyTimeoutSeconds}s (${attempt} attempts). ${errorDetail} Connection context: ${context}. ${suggestion}`;
1888
+ };
1674
1889
  while (true) {
1675
1890
  if (Date.now() > deadline) {
1676
1891
  throw new SandboxReadyTimeoutException({
1677
- message: `Sandbox not ready: timed out waiting for health check (timeoutSeconds=${opts.readyTimeoutSeconds})`
1892
+ message: buildTimeoutMessage()
1678
1893
  });
1679
1894
  }
1895
+ attempt++;
1680
1896
  try {
1681
1897
  if (opts.healthCheck) {
1682
1898
  const ok = await opts.healthCheck(this);
1683
- if (ok) return;
1899
+ if (ok) {
1900
+ return;
1901
+ }
1684
1902
  } else {
1685
1903
  const ok = await this.health.ping();
1686
- if (ok) return;
1904
+ if (ok) {
1905
+ return;
1906
+ }
1687
1907
  }
1688
- } catch {
1908
+ errorDetail = "Health check returned false continuously.";
1909
+ } catch (err) {
1910
+ const message = err instanceof Error ? err.message : String(err);
1911
+ errorDetail = `Last health check error: ${message}`;
1689
1912
  }
1690
1913
  await sleep(opts.pollingIntervalMillis);
1691
1914
  }
@@ -1694,6 +1917,7 @@ var Sandbox = class _Sandbox {
1694
1917
  // Annotate the CommonJS export names for ESM import in node:
1695
1918
  0 && (module.exports = {
1696
1919
  ConnectionConfig,
1920
+ DEFAULT_EGRESS_PORT,
1697
1921
  DEFAULT_ENTRYPOINT,
1698
1922
  DEFAULT_EXECD_PORT,
1699
1923
  DEFAULT_HEALTH_CHECK_POLLING_INTERVAL_MILLIS,