@cloudflare/sandbox 0.7.20 → 0.8.0

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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { _ as filterEnvVars, a as isExecResult, c as parseSSEFrames, d as createNoOpLogger, f as TraceContext, g as extractRepoName, h as GitLogger, i as isWSStreamChunk, l as shellEscape, m as ResultImpl, n as isWSError, o as isProcess, p as Execution, r as isWSResponse, s as isProcessStatus, t as generateRequestId, u as createLogger, v as getEnvString, y as partitionEnvVars } from "./dist-CwUZf_TJ.js";
1
+ import { _ as extractRepoName, a as isExecResult, b as partitionEnvVars, c as parseSSEFrames, d as createNoOpLogger, f as TraceContext, g as GitLogger, h as ResultImpl, i as isWSStreamChunk, l as shellEscape, m as Execution, n as isWSError, o as isProcess, p as logCanonicalEvent, r as isWSResponse, s as isProcessStatus, t as generateRequestId, u as createLogger, v as filterEnvVars, y as getEnvString } from "./dist-CmfvOT-w.js";
2
2
  import { t as ErrorCode } from "./errors-CaSfB5Bm.js";
3
- import { Container, getContainer, switchPort } from "@cloudflare/containers";
3
+ import { Container, ContainerProxy, getContainer, switchPort } from "@cloudflare/containers";
4
4
  import { AwsClient } from "aws4fetch";
5
5
  import path from "node:path/posix";
6
6
 
@@ -804,9 +804,9 @@ var HttpTransport = class extends BaseTransport {
804
804
  if (this.config.stub) return this.config.stub.containerFetch(url, options || {}, this.config.port);
805
805
  return globalThis.fetch(url, options);
806
806
  }
807
- async fetchStream(path$1, body, method = "POST") {
807
+ async fetchStream(path$1, body, method = "POST", headers) {
808
808
  const url = this.buildUrl(path$1);
809
- const options = this.buildStreamOptions(body, method);
809
+ const options = this.buildStreamOptions(body, method, headers);
810
810
  let response;
811
811
  if (this.config.stub) response = await this.config.stub.containerFetch(url, options, this.config.port);
812
812
  else response = await globalThis.fetch(url, options);
@@ -821,10 +821,13 @@ var HttpTransport = class extends BaseTransport {
821
821
  if (this.config.stub) return `http://localhost:${this.config.port}${path$1}`;
822
822
  return `${this.baseUrl}${path$1}`;
823
823
  }
824
- buildStreamOptions(body, method) {
824
+ buildStreamOptions(body, method, headers) {
825
825
  return {
826
826
  method,
827
- headers: body && method === "POST" ? { "Content-Type": "application/json" } : void 0,
827
+ headers: body && method === "POST" ? {
828
+ ...headers,
829
+ "Content-Type": "application/json"
830
+ } : headers,
828
831
  body: body && method === "POST" ? JSON.stringify(body) : void 0
829
832
  };
830
833
  }
@@ -896,7 +899,8 @@ var WebSocketTransport = class extends BaseTransport {
896
899
  await this.connect();
897
900
  const method = options?.method || "GET";
898
901
  const body = this.parseBody(options?.body);
899
- const result = await this.request(method, path$1, body);
902
+ const headers = this.normalizeHeaders(options?.headers);
903
+ const result = await this.request(method, path$1, body, headers);
900
904
  return new Response(JSON.stringify(result.body), {
901
905
  status: result.status,
902
906
  headers: { "Content-Type": "application/json" }
@@ -905,8 +909,8 @@ var WebSocketTransport = class extends BaseTransport {
905
909
  /**
906
910
  * Streaming fetch implementation
907
911
  */
908
- async fetchStream(path$1, body, method = "POST") {
909
- return this.requestStream(method, path$1, body);
912
+ async fetchStream(path$1, body, method = "POST", headers) {
913
+ return this.requestStream(method, path$1, body, headers);
910
914
  }
911
915
  /**
912
916
  * Parse request body from RequestInit
@@ -921,6 +925,17 @@ var WebSocketTransport = class extends BaseTransport {
921
925
  throw new Error(`WebSocket transport only supports string bodies. Got: ${typeof body}`);
922
926
  }
923
927
  /**
928
+ * Normalize RequestInit headers into a plain object for WSRequest.
929
+ */
930
+ normalizeHeaders(headers) {
931
+ if (!headers) return;
932
+ const normalized = {};
933
+ new Headers(headers).forEach((value, key) => {
934
+ normalized[key] = value;
935
+ });
936
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
937
+ }
938
+ /**
924
939
  * Internal connection logic
925
940
  */
926
941
  async doConnect() {
@@ -1009,7 +1024,7 @@ var WebSocketTransport = class extends BaseTransport {
1009
1024
  /**
1010
1025
  * Send a request and wait for response
1011
1026
  */
1012
- async request(method, path$1, body) {
1027
+ async request(method, path$1, body, headers) {
1013
1028
  await this.connect();
1014
1029
  const id = generateRequestId();
1015
1030
  const request = {
@@ -1017,7 +1032,8 @@ var WebSocketTransport = class extends BaseTransport {
1017
1032
  id,
1018
1033
  method,
1019
1034
  path: path$1,
1020
- body
1035
+ body,
1036
+ headers
1021
1037
  };
1022
1038
  return new Promise((resolve, reject) => {
1023
1039
  const timeoutMs = this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
@@ -1065,7 +1081,7 @@ var WebSocketTransport = class extends BaseTransport {
1065
1081
  * long-running streams (e.g. execStream from an agent) stay alive as long
1066
1082
  * as data is flowing. The timer resets on every chunk or response message.
1067
1083
  */
1068
- async requestStream(method, path$1, body) {
1084
+ async requestStream(method, path$1, body, headers) {
1069
1085
  await this.connect();
1070
1086
  const id = generateRequestId();
1071
1087
  const request = {
@@ -1073,7 +1089,8 @@ var WebSocketTransport = class extends BaseTransport {
1073
1089
  id,
1074
1090
  method,
1075
1091
  path: path$1,
1076
- body
1092
+ body,
1093
+ headers
1077
1094
  };
1078
1095
  const idleTimeoutMs = this.config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
1079
1096
  return new Promise((resolveStream, rejectStream) => {
@@ -1398,6 +1415,14 @@ var BaseHttpClient = class {
1398
1415
  * Core fetch method - delegates to Transport which handles retry logic
1399
1416
  */
1400
1417
  async doFetch(path$1, options) {
1418
+ const { defaultHeaders } = this.options;
1419
+ if (defaultHeaders) options = {
1420
+ ...options,
1421
+ headers: {
1422
+ ...defaultHeaders,
1423
+ ...options?.headers
1424
+ }
1425
+ };
1401
1426
  return this.transport.fetch(path$1, options);
1402
1427
  }
1403
1428
  /**
@@ -1482,12 +1507,11 @@ var BaseHttpClient = class {
1482
1507
  * @param method - HTTP method (default: POST, use GET for process logs)
1483
1508
  */
1484
1509
  async doStreamFetch(path$1, body, method = "POST") {
1485
- if (this.transport.getMode() === "websocket") try {
1486
- return await this.transport.fetchStream(path$1, body, method);
1487
- } catch (error) {
1488
- this.logError(`stream ${method} ${path$1}`, error);
1489
- throw error;
1490
- }
1510
+ const streamHeaders = method === "POST" ? {
1511
+ ...this.options.defaultHeaders,
1512
+ "Content-Type": "application/json"
1513
+ } : this.options.defaultHeaders;
1514
+ if (this.transport.getMode() === "websocket") return this.transport.fetchStream(path$1, body, method, streamHeaders);
1491
1515
  const response = await this.doFetch(path$1, {
1492
1516
  method,
1493
1517
  headers: { "Content-Type": "application/json" },
@@ -1495,25 +1519,6 @@ var BaseHttpClient = class {
1495
1519
  });
1496
1520
  return this.handleStreamResponse(response);
1497
1521
  }
1498
- /**
1499
- * Utility method to log successful operations
1500
- */
1501
- logSuccess(operation, details) {
1502
- this.logger.info(operation, details ? { details } : void 0);
1503
- }
1504
- /**
1505
- * Utility method to log errors intelligently
1506
- * Only logs unexpected errors (5xx), not expected errors (4xx)
1507
- *
1508
- * - 4xx errors (validation, not found, conflicts): Don't log (expected client errors)
1509
- * - 5xx errors (server failures, internal errors): DO log (unexpected server errors)
1510
- */
1511
- logError(operation, error) {
1512
- if (error && typeof error === "object" && "httpStatus" in error) {
1513
- const httpStatus = error.httpStatus;
1514
- if (httpStatus >= 500) this.logger.error(`Unexpected error in ${operation}`, error instanceof Error ? error : new Error(String(error)), { httpStatus });
1515
- } else this.logger.error(`Error in ${operation}`, error instanceof Error ? error : new Error(String(error)));
1516
- }
1517
1522
  };
1518
1523
 
1519
1524
  //#endregion
@@ -1541,11 +1546,8 @@ var BackupClient = class extends BaseHttpClient {
1541
1546
  excludes,
1542
1547
  sessionId
1543
1548
  };
1544
- const response = await this.post("/api/backup/create", data);
1545
- this.logSuccess("Backup archive created", `${dir} -> ${archivePath}`);
1546
- return response;
1549
+ return await this.post("/api/backup/create", data);
1547
1550
  } catch (error) {
1548
- this.logError("createArchive", error);
1549
1551
  throw error;
1550
1552
  }
1551
1553
  }
@@ -1562,11 +1564,8 @@ var BackupClient = class extends BaseHttpClient {
1562
1564
  archivePath,
1563
1565
  sessionId
1564
1566
  };
1565
- const response = await this.post("/api/backup/restore", data);
1566
- this.logSuccess("Backup archive restored", `${archivePath} -> ${dir}`);
1567
- return response;
1567
+ return await this.post("/api/backup/restore", data);
1568
1568
  } catch (error) {
1569
- this.logError("restoreArchive", error);
1570
1569
  throw error;
1571
1570
  }
1572
1571
  }
@@ -1593,14 +1592,13 @@ var CommandClient = class extends BaseHttpClient {
1593
1592
  sessionId,
1594
1593
  ...options?.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
1595
1594
  ...options?.env !== void 0 && { env: options.env },
1596
- ...options?.cwd !== void 0 && { cwd: options.cwd }
1595
+ ...options?.cwd !== void 0 && { cwd: options.cwd },
1596
+ ...options?.origin !== void 0 && { origin: options.origin }
1597
1597
  };
1598
1598
  const response = await this.post("/api/execute", data);
1599
- this.logSuccess("Command executed", `${command}, Success: ${response.success}`);
1600
1599
  this.options.onCommandComplete?.(response.success, response.exitCode, response.stdout, response.stderr, response.command);
1601
1600
  return response;
1602
1601
  } catch (error) {
1603
- this.logError("execute", error);
1604
1602
  this.options.onError?.(error instanceof Error ? error.message : String(error), command);
1605
1603
  throw error;
1606
1604
  }
@@ -1618,13 +1616,11 @@ var CommandClient = class extends BaseHttpClient {
1618
1616
  sessionId,
1619
1617
  ...options?.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
1620
1618
  ...options?.env !== void 0 && { env: options.env },
1621
- ...options?.cwd !== void 0 && { cwd: options.cwd }
1619
+ ...options?.cwd !== void 0 && { cwd: options.cwd },
1620
+ ...options?.origin !== void 0 && { origin: options.origin }
1622
1621
  };
1623
- const stream = await this.doStreamFetch("/api/execute/stream", data);
1624
- this.logSuccess("Command stream started", command);
1625
- return stream;
1622
+ return await this.doStreamFetch("/api/execute/stream", data);
1626
1623
  } catch (error) {
1627
- this.logError("executeStream", error);
1628
1624
  this.options.onError?.(error instanceof Error ? error.message : String(error), command);
1629
1625
  throw error;
1630
1626
  }
@@ -1646,11 +1642,8 @@ var DesktopClient = class extends BaseHttpClient {
1646
1642
  ...options?.resolution !== void 0 && { resolution: options.resolution },
1647
1643
  ...options?.dpi !== void 0 && { dpi: options.dpi }
1648
1644
  };
1649
- const response = await this.post("/api/desktop/start", data);
1650
- this.logSuccess("Desktop started", `${response.resolution[0]}x${response.resolution[1]}`);
1651
- return response;
1645
+ return await this.post("/api/desktop/start", data);
1652
1646
  } catch (error) {
1653
- this.logError("desktop.start", error);
1654
1647
  this.options.onError?.(error instanceof Error ? error.message : String(error));
1655
1648
  throw error;
1656
1649
  }
@@ -1660,11 +1653,8 @@ var DesktopClient = class extends BaseHttpClient {
1660
1653
  */
1661
1654
  async stop() {
1662
1655
  try {
1663
- const response = await this.post("/api/desktop/stop", {});
1664
- this.logSuccess("Desktop stopped");
1665
- return response;
1656
+ return await this.post("/api/desktop/stop", {});
1666
1657
  } catch (error) {
1667
- this.logError("desktop.stop", error);
1668
1658
  this.options.onError?.(error instanceof Error ? error.message : String(error));
1669
1659
  throw error;
1670
1660
  }
@@ -1674,11 +1664,8 @@ var DesktopClient = class extends BaseHttpClient {
1674
1664
  */
1675
1665
  async status() {
1676
1666
  try {
1677
- const response = await this.get("/api/desktop/status");
1678
- this.logSuccess("Desktop status retrieved", response.status);
1679
- return response;
1667
+ return await this.get("/api/desktop/status");
1680
1668
  } catch (error) {
1681
- this.logError("desktop.status", error);
1682
1669
  throw error;
1683
1670
  }
1684
1671
  }
@@ -1692,7 +1679,6 @@ var DesktopClient = class extends BaseHttpClient {
1692
1679
  ...options?.showCursor !== void 0 && { showCursor: options.showCursor }
1693
1680
  };
1694
1681
  const response = await this.post("/api/desktop/screenshot", data);
1695
- this.logSuccess("Screenshot captured", `${response.width}x${response.height}`);
1696
1682
  if (wantsBytes) {
1697
1683
  const binaryString = atob(response.data);
1698
1684
  const bytes = new Uint8Array(binaryString.length);
@@ -1704,7 +1690,6 @@ var DesktopClient = class extends BaseHttpClient {
1704
1690
  }
1705
1691
  return response;
1706
1692
  } catch (error) {
1707
- this.logError("desktop.screenshot", error);
1708
1693
  throw error;
1709
1694
  }
1710
1695
  }
@@ -1719,7 +1704,6 @@ var DesktopClient = class extends BaseHttpClient {
1719
1704
  ...options?.showCursor !== void 0 && { showCursor: options.showCursor }
1720
1705
  };
1721
1706
  const response = await this.post("/api/desktop/screenshot/region", data);
1722
- this.logSuccess("Region screenshot captured", `${region.width}x${region.height}`);
1723
1707
  if (wantsBytes) {
1724
1708
  const binaryString = atob(response.data);
1725
1709
  const bytes = new Uint8Array(binaryString.length);
@@ -1731,7 +1715,6 @@ var DesktopClient = class extends BaseHttpClient {
1731
1715
  }
1732
1716
  return response;
1733
1717
  } catch (error) {
1734
- this.logError("desktop.screenshotRegion", error);
1735
1718
  throw error;
1736
1719
  }
1737
1720
  }
@@ -1746,9 +1729,7 @@ var DesktopClient = class extends BaseHttpClient {
1746
1729
  button: options?.button ?? "left",
1747
1730
  clickCount: 1
1748
1731
  });
1749
- this.logSuccess("Mouse click", `(${x}, ${y})`);
1750
1732
  } catch (error) {
1751
- this.logError("desktop.click", error);
1752
1733
  throw error;
1753
1734
  }
1754
1735
  }
@@ -1763,9 +1744,7 @@ var DesktopClient = class extends BaseHttpClient {
1763
1744
  button: options?.button ?? "left",
1764
1745
  clickCount: 2
1765
1746
  });
1766
- this.logSuccess("Mouse double click", `(${x}, ${y})`);
1767
1747
  } catch (error) {
1768
- this.logError("desktop.doubleClick", error);
1769
1748
  throw error;
1770
1749
  }
1771
1750
  }
@@ -1780,9 +1759,7 @@ var DesktopClient = class extends BaseHttpClient {
1780
1759
  button: options?.button ?? "left",
1781
1760
  clickCount: 3
1782
1761
  });
1783
- this.logSuccess("Mouse triple click", `(${x}, ${y})`);
1784
1762
  } catch (error) {
1785
- this.logError("desktop.tripleClick", error);
1786
1763
  throw error;
1787
1764
  }
1788
1765
  }
@@ -1797,9 +1774,7 @@ var DesktopClient = class extends BaseHttpClient {
1797
1774
  button: "right",
1798
1775
  clickCount: 1
1799
1776
  });
1800
- this.logSuccess("Mouse right click", `(${x}, ${y})`);
1801
1777
  } catch (error) {
1802
- this.logError("desktop.rightClick", error);
1803
1778
  throw error;
1804
1779
  }
1805
1780
  }
@@ -1814,9 +1789,7 @@ var DesktopClient = class extends BaseHttpClient {
1814
1789
  button: "middle",
1815
1790
  clickCount: 1
1816
1791
  });
1817
- this.logSuccess("Mouse middle click", `(${x}, ${y})`);
1818
1792
  } catch (error) {
1819
- this.logError("desktop.middleClick", error);
1820
1793
  throw error;
1821
1794
  }
1822
1795
  }
@@ -1830,9 +1803,7 @@ var DesktopClient = class extends BaseHttpClient {
1830
1803
  ...y !== void 0 && { y },
1831
1804
  button: options?.button ?? "left"
1832
1805
  });
1833
- this.logSuccess("Mouse down", x !== void 0 ? `(${x}, ${y})` : "current position");
1834
1806
  } catch (error) {
1835
- this.logError("desktop.mouseDown", error);
1836
1807
  throw error;
1837
1808
  }
1838
1809
  }
@@ -1846,9 +1817,7 @@ var DesktopClient = class extends BaseHttpClient {
1846
1817
  ...y !== void 0 && { y },
1847
1818
  button: options?.button ?? "left"
1848
1819
  });
1849
- this.logSuccess("Mouse up", x !== void 0 ? `(${x}, ${y})` : "current position");
1850
1820
  } catch (error) {
1851
- this.logError("desktop.mouseUp", error);
1852
1821
  throw error;
1853
1822
  }
1854
1823
  }
@@ -1861,9 +1830,7 @@ var DesktopClient = class extends BaseHttpClient {
1861
1830
  x,
1862
1831
  y
1863
1832
  });
1864
- this.logSuccess("Mouse move", `(${x}, ${y})`);
1865
1833
  } catch (error) {
1866
- this.logError("desktop.moveMouse", error);
1867
1834
  throw error;
1868
1835
  }
1869
1836
  }
@@ -1879,9 +1846,7 @@ var DesktopClient = class extends BaseHttpClient {
1879
1846
  endY,
1880
1847
  button: options?.button ?? "left"
1881
1848
  });
1882
- this.logSuccess("Mouse drag", `(${startX},${startY}) -> (${endX},${endY})`);
1883
1849
  } catch (error) {
1884
- this.logError("desktop.drag", error);
1885
1850
  throw error;
1886
1851
  }
1887
1852
  }
@@ -1896,9 +1861,7 @@ var DesktopClient = class extends BaseHttpClient {
1896
1861
  direction,
1897
1862
  amount
1898
1863
  });
1899
- this.logSuccess("Mouse scroll", `${direction} ${amount} at (${x}, ${y})`);
1900
1864
  } catch (error) {
1901
- this.logError("desktop.scroll", error);
1902
1865
  throw error;
1903
1866
  }
1904
1867
  }
@@ -1907,11 +1870,8 @@ var DesktopClient = class extends BaseHttpClient {
1907
1870
  */
1908
1871
  async getCursorPosition() {
1909
1872
  try {
1910
- const response = await this.get("/api/desktop/mouse/position");
1911
- this.logSuccess("Cursor position retrieved", `(${response.x}, ${response.y})`);
1912
- return response;
1873
+ return await this.get("/api/desktop/mouse/position");
1913
1874
  } catch (error) {
1914
- this.logError("desktop.getCursorPosition", error);
1915
1875
  throw error;
1916
1876
  }
1917
1877
  }
@@ -1924,9 +1884,7 @@ var DesktopClient = class extends BaseHttpClient {
1924
1884
  text,
1925
1885
  ...options?.delayMs !== void 0 && { delayMs: options.delayMs }
1926
1886
  });
1927
- this.logSuccess("Keyboard type", `${text.length} chars`);
1928
1887
  } catch (error) {
1929
- this.logError("desktop.type", error);
1930
1888
  throw error;
1931
1889
  }
1932
1890
  }
@@ -1936,9 +1894,7 @@ var DesktopClient = class extends BaseHttpClient {
1936
1894
  async press(key) {
1937
1895
  try {
1938
1896
  await this.post("/api/desktop/keyboard/press", { key });
1939
- this.logSuccess("Key press", key);
1940
1897
  } catch (error) {
1941
- this.logError("desktop.press", error);
1942
1898
  throw error;
1943
1899
  }
1944
1900
  }
@@ -1948,9 +1904,7 @@ var DesktopClient = class extends BaseHttpClient {
1948
1904
  async keyDown(key) {
1949
1905
  try {
1950
1906
  await this.post("/api/desktop/keyboard/down", { key });
1951
- this.logSuccess("Key down", key);
1952
1907
  } catch (error) {
1953
- this.logError("desktop.keyDown", error);
1954
1908
  throw error;
1955
1909
  }
1956
1910
  }
@@ -1960,9 +1914,7 @@ var DesktopClient = class extends BaseHttpClient {
1960
1914
  async keyUp(key) {
1961
1915
  try {
1962
1916
  await this.post("/api/desktop/keyboard/up", { key });
1963
- this.logSuccess("Key up", key);
1964
1917
  } catch (error) {
1965
- this.logError("desktop.keyUp", error);
1966
1918
  throw error;
1967
1919
  }
1968
1920
  }
@@ -1971,11 +1923,8 @@ var DesktopClient = class extends BaseHttpClient {
1971
1923
  */
1972
1924
  async getScreenSize() {
1973
1925
  try {
1974
- const response = await this.get("/api/desktop/screen/size");
1975
- this.logSuccess("Screen size retrieved", `${response.width}x${response.height}`);
1976
- return response;
1926
+ return await this.get("/api/desktop/screen/size");
1977
1927
  } catch (error) {
1978
- this.logError("desktop.getScreenSize", error);
1979
1928
  throw error;
1980
1929
  }
1981
1930
  }
@@ -1984,11 +1933,8 @@ var DesktopClient = class extends BaseHttpClient {
1984
1933
  */
1985
1934
  async getProcessStatus(name) {
1986
1935
  try {
1987
- const response = await this.get(`/api/desktop/process/${encodeURIComponent(name)}/status`);
1988
- this.logSuccess("Desktop process status retrieved", name);
1989
- return response;
1936
+ return await this.get(`/api/desktop/process/${encodeURIComponent(name)}/status`);
1990
1937
  } catch (error) {
1991
- this.logError("desktop.getProcessStatus", error);
1992
1938
  throw error;
1993
1939
  }
1994
1940
  }
@@ -2013,11 +1959,8 @@ var FileClient = class extends BaseHttpClient {
2013
1959
  sessionId,
2014
1960
  recursive: options?.recursive ?? false
2015
1961
  };
2016
- const response = await this.post("/api/mkdir", data);
2017
- this.logSuccess("Directory created", `${path$1} (recursive: ${data.recursive})`);
2018
- return response;
1962
+ return await this.post("/api/mkdir", data);
2019
1963
  } catch (error) {
2020
- this.logError("mkdir", error);
2021
1964
  throw error;
2022
1965
  }
2023
1966
  }
@@ -2036,11 +1979,8 @@ var FileClient = class extends BaseHttpClient {
2036
1979
  sessionId,
2037
1980
  encoding: options?.encoding
2038
1981
  };
2039
- const response = await this.post("/api/write", data);
2040
- this.logSuccess("File written", `${path$1} (${content.length} chars)`);
2041
- return response;
1982
+ return await this.post("/api/write", data);
2042
1983
  } catch (error) {
2043
- this.logError("writeFile", error);
2044
1984
  throw error;
2045
1985
  }
2046
1986
  }
@@ -2057,11 +1997,8 @@ var FileClient = class extends BaseHttpClient {
2057
1997
  sessionId,
2058
1998
  encoding: options?.encoding
2059
1999
  };
2060
- const response = await this.post("/api/read", data);
2061
- this.logSuccess("File read", `${path$1} (${response.content.length} chars)`);
2062
- return response;
2000
+ return await this.post("/api/read", data);
2063
2001
  } catch (error) {
2064
- this.logError("readFile", error);
2065
2002
  throw error;
2066
2003
  }
2067
2004
  }
@@ -2077,11 +2014,8 @@ var FileClient = class extends BaseHttpClient {
2077
2014
  path: path$1,
2078
2015
  sessionId
2079
2016
  };
2080
- const stream = await this.doStreamFetch("/api/read/stream", data);
2081
- this.logSuccess("File stream started", path$1);
2082
- return stream;
2017
+ return await this.doStreamFetch("/api/read/stream", data);
2083
2018
  } catch (error) {
2084
- this.logError("readFileStream", error);
2085
2019
  throw error;
2086
2020
  }
2087
2021
  }
@@ -2096,11 +2030,8 @@ var FileClient = class extends BaseHttpClient {
2096
2030
  path: path$1,
2097
2031
  sessionId
2098
2032
  };
2099
- const response = await this.post("/api/delete", data);
2100
- this.logSuccess("File deleted", path$1);
2101
- return response;
2033
+ return await this.post("/api/delete", data);
2102
2034
  } catch (error) {
2103
- this.logError("deleteFile", error);
2104
2035
  throw error;
2105
2036
  }
2106
2037
  }
@@ -2117,11 +2048,8 @@ var FileClient = class extends BaseHttpClient {
2117
2048
  newPath,
2118
2049
  sessionId
2119
2050
  };
2120
- const response = await this.post("/api/rename", data);
2121
- this.logSuccess("File renamed", `${path$1} -> ${newPath}`);
2122
- return response;
2051
+ return await this.post("/api/rename", data);
2123
2052
  } catch (error) {
2124
- this.logError("renameFile", error);
2125
2053
  throw error;
2126
2054
  }
2127
2055
  }
@@ -2138,11 +2066,8 @@ var FileClient = class extends BaseHttpClient {
2138
2066
  destinationPath: newPath,
2139
2067
  sessionId
2140
2068
  };
2141
- const response = await this.post("/api/move", data);
2142
- this.logSuccess("File moved", `${path$1} -> ${newPath}`);
2143
- return response;
2069
+ return await this.post("/api/move", data);
2144
2070
  } catch (error) {
2145
- this.logError("moveFile", error);
2146
2071
  throw error;
2147
2072
  }
2148
2073
  }
@@ -2159,11 +2084,8 @@ var FileClient = class extends BaseHttpClient {
2159
2084
  sessionId,
2160
2085
  options: options || {}
2161
2086
  };
2162
- const response = await this.post("/api/list-files", data);
2163
- this.logSuccess("Files listed", `${path$1} (${response.count} files)`);
2164
- return response;
2087
+ return await this.post("/api/list-files", data);
2165
2088
  } catch (error) {
2166
- this.logError("listFiles", error);
2167
2089
  throw error;
2168
2090
  }
2169
2091
  }
@@ -2178,11 +2100,8 @@ var FileClient = class extends BaseHttpClient {
2178
2100
  path: path$1,
2179
2101
  sessionId
2180
2102
  };
2181
- const response = await this.post("/api/exists", data);
2182
- this.logSuccess("Path existence checked", `${path$1} (exists: ${response.exists})`);
2183
- return response;
2103
+ return await this.post("/api/exists", data);
2184
2104
  } catch (error) {
2185
- this.logError("exists", error);
2186
2105
  throw error;
2187
2106
  }
2188
2107
  }
@@ -2218,11 +2137,8 @@ var GitClient = class extends BaseHttpClient {
2218
2137
  if (!Number.isInteger(options.depth) || options.depth <= 0) throw new Error(`Invalid depth value: ${options.depth}. Must be a positive integer (e.g., 1, 5, 10).`);
2219
2138
  data.depth = options.depth;
2220
2139
  }
2221
- const response = await this.post("/api/git/checkout", data);
2222
- this.logSuccess("Repository cloned", `${repoUrl} (branch: ${response.branch}) -> ${response.targetDir}`);
2223
- return response;
2140
+ return await this.post("/api/git/checkout", data);
2224
2141
  } catch (error) {
2225
- this.logError("checkout", error);
2226
2142
  throw error;
2227
2143
  }
2228
2144
  }
@@ -2313,7 +2229,6 @@ var InterpreterClient = class extends BaseHttpClient {
2313
2229
  for (let attempt = 0; attempt < this.maxRetries; attempt++) try {
2314
2230
  return await operation();
2315
2231
  } catch (error) {
2316
- this.logError("executeWithRetry", error);
2317
2232
  lastError = error;
2318
2233
  if (this.isRetryableError(error)) {
2319
2234
  if (attempt < this.maxRetries - 1) {
@@ -2401,9 +2316,7 @@ var InterpreterClient = class extends BaseHttpClient {
2401
2316
  break;
2402
2317
  case "execution_complete": break;
2403
2318
  }
2404
- } catch (error) {
2405
- this.logError("parseExecutionResult", error);
2406
- }
2319
+ } catch {}
2407
2320
  }
2408
2321
  };
2409
2322
 
@@ -2426,11 +2339,8 @@ var PortClient = class extends BaseHttpClient {
2426
2339
  sessionId,
2427
2340
  name
2428
2341
  };
2429
- const response = await this.post("/api/expose-port", data);
2430
- this.logSuccess("Port exposed", `${port} exposed at ${response.url}${name ? ` (${name})` : ""}`);
2431
- return response;
2342
+ return await this.post("/api/expose-port", data);
2432
2343
  } catch (error) {
2433
- this.logError("exposePort", error);
2434
2344
  throw error;
2435
2345
  }
2436
2346
  }
@@ -2442,11 +2352,8 @@ var PortClient = class extends BaseHttpClient {
2442
2352
  async unexposePort(port, sessionId) {
2443
2353
  try {
2444
2354
  const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(sessionId)}`;
2445
- const response = await this.delete(url);
2446
- this.logSuccess("Port unexposed", `${port}`);
2447
- return response;
2355
+ return await this.delete(url);
2448
2356
  } catch (error) {
2449
- this.logError("unexposePort", error);
2450
2357
  throw error;
2451
2358
  }
2452
2359
  }
@@ -2457,11 +2364,8 @@ var PortClient = class extends BaseHttpClient {
2457
2364
  async getExposedPorts(sessionId) {
2458
2365
  try {
2459
2366
  const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
2460
- const response = await this.get(url);
2461
- this.logSuccess("Exposed ports retrieved", `${response.ports.length} ports exposed`);
2462
- return response;
2367
+ return await this.get(url);
2463
2368
  } catch (error) {
2464
- this.logError("getExposedPorts", error);
2465
2369
  throw error;
2466
2370
  }
2467
2371
  }
@@ -2472,11 +2376,8 @@ var PortClient = class extends BaseHttpClient {
2472
2376
  */
2473
2377
  async watchPort(request) {
2474
2378
  try {
2475
- const stream = await this.doStreamFetch("/api/port-watch", request);
2476
- this.logSuccess("Port watch started", `port ${request.port}`);
2477
- return stream;
2379
+ return await this.doStreamFetch("/api/port-watch", request);
2478
2380
  } catch (error) {
2479
- this.logError("watchPort", error);
2480
2381
  throw error;
2481
2382
  }
2482
2383
  }
@@ -2499,6 +2400,7 @@ var ProcessClient = class extends BaseHttpClient {
2499
2400
  const data = {
2500
2401
  command,
2501
2402
  sessionId,
2403
+ ...options?.origin !== void 0 && { origin: options.origin },
2502
2404
  ...options?.processId !== void 0 && { processId: options.processId },
2503
2405
  ...options?.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
2504
2406
  ...options?.env !== void 0 && { env: options.env },
@@ -2506,11 +2408,8 @@ var ProcessClient = class extends BaseHttpClient {
2506
2408
  ...options?.encoding !== void 0 && { encoding: options.encoding },
2507
2409
  ...options?.autoCleanup !== void 0 && { autoCleanup: options.autoCleanup }
2508
2410
  };
2509
- const response = await this.post("/api/process/start", data);
2510
- this.logSuccess("Process started", `${command} (ID: ${response.processId})`);
2511
- return response;
2411
+ return await this.post("/api/process/start", data);
2512
2412
  } catch (error) {
2513
- this.logError("startProcess", error);
2514
2413
  throw error;
2515
2414
  }
2516
2415
  }
@@ -2519,11 +2418,8 @@ var ProcessClient = class extends BaseHttpClient {
2519
2418
  */
2520
2419
  async listProcesses() {
2521
2420
  try {
2522
- const response = await this.get(`/api/process/list`);
2523
- this.logSuccess("Processes listed", `${response.processes.length} processes`);
2524
- return response;
2421
+ return await this.get(`/api/process/list`);
2525
2422
  } catch (error) {
2526
- this.logError("listProcesses", error);
2527
2423
  throw error;
2528
2424
  }
2529
2425
  }
@@ -2534,11 +2430,8 @@ var ProcessClient = class extends BaseHttpClient {
2534
2430
  async getProcess(processId) {
2535
2431
  try {
2536
2432
  const url = `/api/process/${processId}`;
2537
- const response = await this.get(url);
2538
- this.logSuccess("Process retrieved", `ID: ${processId}`);
2539
- return response;
2433
+ return await this.get(url);
2540
2434
  } catch (error) {
2541
- this.logError("getProcess", error);
2542
2435
  throw error;
2543
2436
  }
2544
2437
  }
@@ -2549,11 +2442,8 @@ var ProcessClient = class extends BaseHttpClient {
2549
2442
  async killProcess(processId) {
2550
2443
  try {
2551
2444
  const url = `/api/process/${processId}`;
2552
- const response = await this.delete(url);
2553
- this.logSuccess("Process killed", `ID: ${processId}`);
2554
- return response;
2445
+ return await this.delete(url);
2555
2446
  } catch (error) {
2556
- this.logError("killProcess", error);
2557
2447
  throw error;
2558
2448
  }
2559
2449
  }
@@ -2562,11 +2452,8 @@ var ProcessClient = class extends BaseHttpClient {
2562
2452
  */
2563
2453
  async killAllProcesses() {
2564
2454
  try {
2565
- const response = await this.delete(`/api/process/kill-all`);
2566
- this.logSuccess("All processes killed", `${response.cleanedCount} processes terminated`);
2567
- return response;
2455
+ return await this.delete(`/api/process/kill-all`);
2568
2456
  } catch (error) {
2569
- this.logError("killAllProcesses", error);
2570
2457
  throw error;
2571
2458
  }
2572
2459
  }
@@ -2577,11 +2464,8 @@ var ProcessClient = class extends BaseHttpClient {
2577
2464
  async getProcessLogs(processId) {
2578
2465
  try {
2579
2466
  const url = `/api/process/${processId}/logs`;
2580
- const response = await this.get(url);
2581
- this.logSuccess("Process logs retrieved", `ID: ${processId}, stdout: ${response.stdout.length} chars, stderr: ${response.stderr.length} chars`);
2582
- return response;
2467
+ return await this.get(url);
2583
2468
  } catch (error) {
2584
- this.logError("getProcessLogs", error);
2585
2469
  throw error;
2586
2470
  }
2587
2471
  }
@@ -2592,11 +2476,8 @@ var ProcessClient = class extends BaseHttpClient {
2592
2476
  async streamProcessLogs(processId) {
2593
2477
  try {
2594
2478
  const url = `/api/process/${processId}/stream`;
2595
- const stream = await this.doStreamFetch(url, void 0, "GET");
2596
- this.logSuccess("Process log stream started", `ID: ${processId}`);
2597
- return stream;
2479
+ return await this.doStreamFetch(url, void 0, "GET");
2598
2480
  } catch (error) {
2599
- this.logError("streamProcessLogs", error);
2600
2481
  throw error;
2601
2482
  }
2602
2483
  }
@@ -2613,11 +2494,8 @@ var UtilityClient = class extends BaseHttpClient {
2613
2494
  */
2614
2495
  async ping() {
2615
2496
  try {
2616
- const response = await this.get("/api/ping");
2617
- this.logSuccess("Ping successful", response.message);
2618
- return response.message;
2497
+ return (await this.get("/api/ping")).message;
2619
2498
  } catch (error) {
2620
- this.logError("ping", error);
2621
2499
  throw error;
2622
2500
  }
2623
2501
  }
@@ -2626,11 +2504,8 @@ var UtilityClient = class extends BaseHttpClient {
2626
2504
  */
2627
2505
  async getCommands() {
2628
2506
  try {
2629
- const response = await this.get("/api/commands");
2630
- this.logSuccess("Commands retrieved", `${response.count} commands available`);
2631
- return response.availableCommands;
2507
+ return (await this.get("/api/commands")).availableCommands;
2632
2508
  } catch (error) {
2633
- this.logError("getCommands", error);
2634
2509
  throw error;
2635
2510
  }
2636
2511
  }
@@ -2640,11 +2515,8 @@ var UtilityClient = class extends BaseHttpClient {
2640
2515
  */
2641
2516
  async createSession(options) {
2642
2517
  try {
2643
- const response = await this.post("/api/session/create", options);
2644
- this.logSuccess("Session created", `ID: ${options.id}`);
2645
- return response;
2518
+ return await this.post("/api/session/create", options);
2646
2519
  } catch (error) {
2647
- this.logError("createSession", error);
2648
2520
  throw error;
2649
2521
  }
2650
2522
  }
@@ -2654,11 +2526,8 @@ var UtilityClient = class extends BaseHttpClient {
2654
2526
  */
2655
2527
  async deleteSession(sessionId) {
2656
2528
  try {
2657
- const response = await this.post("/api/session/delete", { sessionId });
2658
- this.logSuccess("Session deleted", `ID: ${sessionId}`);
2659
- return response;
2529
+ return await this.post("/api/session/delete", { sessionId });
2660
2530
  } catch (error) {
2661
- this.logError("deleteSession", error);
2662
2531
  throw error;
2663
2532
  }
2664
2533
  }
@@ -2668,9 +2537,7 @@ var UtilityClient = class extends BaseHttpClient {
2668
2537
  */
2669
2538
  async getVersion() {
2670
2539
  try {
2671
- const response = await this.get("/api/version");
2672
- this.logSuccess("Version retrieved", response.version);
2673
- return response.version;
2540
+ return (await this.get("/api/version")).version;
2674
2541
  } catch (error) {
2675
2542
  this.logger.debug("Failed to get container version (may be old container)", { error });
2676
2543
  return "unknown";
@@ -2700,11 +2567,8 @@ var WatchClient = class extends BaseHttpClient {
2700
2567
  async watch(request) {
2701
2568
  try {
2702
2569
  const stream = await this.doStreamFetch("/api/watch", request);
2703
- const readyStream = await this.waitForReadiness(stream);
2704
- this.logSuccess("File watch started", request.path);
2705
- return readyStream;
2570
+ return await this.waitForReadiness(stream);
2706
2571
  } catch (error) {
2707
- this.logError("watch", error);
2708
2572
  throw error;
2709
2573
  }
2710
2574
  }
@@ -3698,7 +3562,7 @@ function buildS3fsSource(bucket, prefix) {
3698
3562
  * This file is auto-updated by .github/changeset-version.ts during releases
3699
3563
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
3700
3564
  */
3701
- const SDK_VERSION = "0.7.20";
3565
+ const SDK_VERSION = "0.8.0";
3702
3566
 
3703
3567
  //#endregion
3704
3568
  //#region src/sandbox.ts
@@ -3929,6 +3793,7 @@ var Sandbox = class Sandbox extends Container {
3929
3793
  port: 3e3,
3930
3794
  stub: this,
3931
3795
  retryTimeoutMs: this.computeRetryTimeoutMs(),
3796
+ defaultHeaders: { "X-Sandbox-Id": this.ctx.id.toString() },
3932
3797
  ...this.transport === "websocket" && {
3933
3798
  transportMode: "websocket",
3934
3799
  wsUrl: "ws://localhost:3000/ws"
@@ -4022,12 +3887,12 @@ var Sandbox = class Sandbox extends Container {
4022
3887
  if (this.defaultSession) {
4023
3888
  for (const key of toUnset) {
4024
3889
  const unsetCommand = `unset ${key}`;
4025
- const result = await this.client.commands.execute(unsetCommand, this.defaultSession);
3890
+ const result = await this.client.commands.execute(unsetCommand, this.defaultSession, { origin: "internal" });
4026
3891
  if (result.exitCode !== 0) throw new Error(`Failed to unset ${key}: ${result.stderr || "Unknown error"}`);
4027
3892
  }
4028
3893
  for (const [key, value] of Object.entries(toSet)) {
4029
3894
  const exportCommand = `export ${key}=${shellEscape(value)}`;
4030
- const result = await this.client.commands.execute(exportCommand, this.defaultSession);
3895
+ const result = await this.client.commands.execute(exportCommand, this.defaultSession, { origin: "internal" });
4031
3896
  if (result.exitCode !== 0) throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
4032
3897
  }
4033
3898
  }
@@ -4093,7 +3958,6 @@ var Sandbox = class Sandbox extends Container {
4093
3958
  * @throws InvalidMountConfigError if bucket name, mount path, or endpoint is invalid
4094
3959
  */
4095
3960
  async mountBucket(bucket, mountPath, options) {
4096
- this.logger.info(`Mounting bucket ${bucket} to ${mountPath}`);
4097
3961
  if ("localBucket" in options && options.localBucket) {
4098
3962
  await this.mountBucketLocal(bucket, mountPath, options);
4099
3963
  return;
@@ -4104,82 +3968,118 @@ var Sandbox = class Sandbox extends Container {
4104
3968
  * Local dev mount: bidirectional sync via R2 binding + file/watch APIs
4105
3969
  */
4106
3970
  async mountBucketLocal(bucket, mountPath, options) {
4107
- const r2Binding = this.env[bucket];
4108
- if (!r2Binding || !isR2Bucket(r2Binding)) throw new InvalidMountConfigError(`R2 binding "${bucket}" not found in env or is not an R2Bucket. Make sure the binding name matches your wrangler.jsonc R2 binding.`);
4109
- if (!mountPath || !mountPath.startsWith("/")) throw new InvalidMountConfigError(`Invalid mount path: "${mountPath}". Must be an absolute path starting with /`);
4110
- if (this.activeMounts.has(mountPath)) throw new InvalidMountConfigError(`Mount path already in use: ${mountPath}`);
4111
- const sessionId = await this.ensureDefaultSession();
4112
- const syncManager = new LocalMountSyncManager({
4113
- bucket: r2Binding,
4114
- mountPath,
4115
- prefix: options.prefix,
4116
- readOnly: options.readOnly ?? false,
4117
- client: this.client,
4118
- sessionId,
4119
- logger: this.logger
4120
- });
4121
- const mountInfo = {
4122
- mountType: "local-sync",
4123
- bucket,
4124
- mountPath,
4125
- syncManager,
4126
- mounted: false
4127
- };
4128
- this.activeMounts.set(mountPath, mountInfo);
3971
+ const mountStartTime = Date.now();
3972
+ let mountOutcome = "error";
3973
+ let mountError;
4129
3974
  try {
4130
- await syncManager.start();
4131
- mountInfo.mounted = true;
4132
- this.logger.info(`Successfully mounted bucket ${bucket} to ${mountPath} (local sync)`);
3975
+ const r2Binding = this.env[bucket];
3976
+ if (!r2Binding || !isR2Bucket(r2Binding)) throw new InvalidMountConfigError(`R2 binding "${bucket}" not found in env or is not an R2Bucket. Make sure the binding name matches your wrangler.jsonc R2 binding.`);
3977
+ if (!mountPath || !mountPath.startsWith("/")) throw new InvalidMountConfigError(`Invalid mount path: "${mountPath}". Must be an absolute path starting with /`);
3978
+ if (this.activeMounts.has(mountPath)) throw new InvalidMountConfigError(`Mount path already in use: ${mountPath}`);
3979
+ const sessionId = await this.ensureDefaultSession();
3980
+ const syncManager = new LocalMountSyncManager({
3981
+ bucket: r2Binding,
3982
+ mountPath,
3983
+ prefix: options.prefix,
3984
+ readOnly: options.readOnly ?? false,
3985
+ client: this.client,
3986
+ sessionId,
3987
+ logger: this.logger
3988
+ });
3989
+ const mountInfo = {
3990
+ mountType: "local-sync",
3991
+ bucket,
3992
+ mountPath,
3993
+ syncManager,
3994
+ mounted: false
3995
+ };
3996
+ this.activeMounts.set(mountPath, mountInfo);
3997
+ try {
3998
+ await syncManager.start();
3999
+ mountInfo.mounted = true;
4000
+ } catch (error) {
4001
+ await syncManager.stop();
4002
+ this.activeMounts.delete(mountPath);
4003
+ throw error;
4004
+ }
4005
+ mountOutcome = "success";
4133
4006
  } catch (error) {
4134
- await syncManager.stop();
4135
- this.activeMounts.delete(mountPath);
4007
+ mountError = error instanceof Error ? error : new Error(String(error));
4136
4008
  throw error;
4009
+ } finally {
4010
+ logCanonicalEvent(this.logger, {
4011
+ event: "bucket.mount",
4012
+ outcome: mountOutcome,
4013
+ durationMs: Date.now() - mountStartTime,
4014
+ bucket,
4015
+ mountPath,
4016
+ provider: "local-sync",
4017
+ prefix: options.prefix,
4018
+ error: mountError
4019
+ });
4137
4020
  }
4138
4021
  }
4139
4022
  /**
4140
4023
  * Production mount: S3FS-FUSE inside the container
4141
4024
  */
4142
4025
  async mountBucketFuse(bucket, mountPath, options) {
4026
+ const mountStartTime = Date.now();
4143
4027
  const prefix = options.prefix || void 0;
4144
- this.validateMountOptions(bucket, mountPath, {
4145
- ...options,
4146
- prefix
4147
- });
4148
- const s3fsSource = buildS3fsSource(bucket, prefix);
4149
- const provider = options.provider || detectProviderFromUrl(options.endpoint);
4150
- this.logger.debug(`Detected provider: ${provider || "unknown"}`, {
4151
- explicitProvider: options.provider,
4152
- prefix
4153
- });
4154
- const envObj = this.env;
4155
- const credentials = detectCredentials(options, {
4156
- AWS_ACCESS_KEY_ID: getEnvString(envObj, "AWS_ACCESS_KEY_ID"),
4157
- AWS_SECRET_ACCESS_KEY: getEnvString(envObj, "AWS_SECRET_ACCESS_KEY"),
4158
- R2_ACCESS_KEY_ID: this.r2AccessKeyId || void 0,
4159
- R2_SECRET_ACCESS_KEY: this.r2SecretAccessKey || void 0,
4160
- ...this.envVars
4161
- });
4162
- const passwordFilePath = this.generatePasswordFilePath();
4163
- const mountInfo = {
4164
- mountType: "fuse",
4165
- bucket: s3fsSource,
4166
- mountPath,
4167
- endpoint: options.endpoint,
4168
- provider,
4169
- passwordFilePath,
4170
- mounted: false
4171
- };
4172
- this.activeMounts.set(mountPath, mountInfo);
4028
+ let mountOutcome = "error";
4029
+ let mountError;
4030
+ let passwordFilePath;
4031
+ let provider = null;
4173
4032
  try {
4033
+ this.validateMountOptions(bucket, mountPath, {
4034
+ ...options,
4035
+ prefix
4036
+ });
4037
+ const s3fsSource = buildS3fsSource(bucket, prefix);
4038
+ provider = options.provider || detectProviderFromUrl(options.endpoint);
4039
+ this.logger.debug(`Detected provider: ${provider || "unknown"}`, {
4040
+ explicitProvider: options.provider,
4041
+ prefix
4042
+ });
4043
+ const envObj = this.env;
4044
+ const credentials = detectCredentials(options, {
4045
+ AWS_ACCESS_KEY_ID: getEnvString(envObj, "AWS_ACCESS_KEY_ID"),
4046
+ AWS_SECRET_ACCESS_KEY: getEnvString(envObj, "AWS_SECRET_ACCESS_KEY"),
4047
+ R2_ACCESS_KEY_ID: this.r2AccessKeyId || void 0,
4048
+ R2_SECRET_ACCESS_KEY: this.r2SecretAccessKey || void 0,
4049
+ ...this.envVars
4050
+ });
4051
+ passwordFilePath = this.generatePasswordFilePath();
4052
+ const mountInfo = {
4053
+ mountType: "fuse",
4054
+ bucket: s3fsSource,
4055
+ mountPath,
4056
+ endpoint: options.endpoint,
4057
+ provider,
4058
+ passwordFilePath,
4059
+ mounted: false
4060
+ };
4061
+ this.activeMounts.set(mountPath, mountInfo);
4174
4062
  await this.createPasswordFile(passwordFilePath, bucket, credentials);
4175
- await this.exec(`mkdir -p ${shellEscape(mountPath)}`);
4063
+ await this.execInternal(`mkdir -p ${shellEscape(mountPath)}`);
4176
4064
  await this.executeS3FSMount(s3fsSource, mountPath, options, provider, passwordFilePath);
4177
4065
  mountInfo.mounted = true;
4178
- this.logger.info(`Successfully mounted bucket ${bucket} to ${mountPath}`);
4066
+ mountOutcome = "success";
4179
4067
  } catch (error) {
4180
- await this.deletePasswordFile(passwordFilePath);
4068
+ mountError = error instanceof Error ? error : new Error(String(error));
4069
+ if (passwordFilePath) await this.deletePasswordFile(passwordFilePath);
4181
4070
  this.activeMounts.delete(mountPath);
4182
4071
  throw error;
4072
+ } finally {
4073
+ logCanonicalEvent(this.logger, {
4074
+ event: "bucket.mount",
4075
+ outcome: mountOutcome,
4076
+ durationMs: Date.now() - mountStartTime,
4077
+ bucket,
4078
+ mountPath,
4079
+ provider: provider || "unknown",
4080
+ prefix,
4081
+ error: mountError
4082
+ });
4183
4083
  }
4184
4084
  }
4185
4085
  /**
@@ -4189,21 +4089,37 @@ var Sandbox = class Sandbox extends Container {
4189
4089
  * @throws InvalidMountConfigError if mount path doesn't exist or isn't mounted
4190
4090
  */
4191
4091
  async unmountBucket(mountPath) {
4192
- this.logger.info(`Unmounting bucket from ${mountPath}`);
4092
+ const unmountStartTime = Date.now();
4093
+ let unmountOutcome = "error";
4094
+ let unmountError;
4193
4095
  const mountInfo = this.activeMounts.get(mountPath);
4194
- if (!mountInfo) throw new InvalidMountConfigError(`No active mount found at path: ${mountPath}`);
4195
- if (mountInfo.mountType === "local-sync") {
4196
- await mountInfo.syncManager.stop();
4197
- mountInfo.mounted = false;
4198
- this.activeMounts.delete(mountPath);
4199
- } else try {
4200
- await this.exec(`fusermount -u ${shellEscape(mountPath)}`);
4201
- mountInfo.mounted = false;
4202
- this.activeMounts.delete(mountPath);
4096
+ try {
4097
+ if (!mountInfo) throw new InvalidMountConfigError(`No active mount found at path: ${mountPath}`);
4098
+ if (mountInfo.mountType === "local-sync") {
4099
+ await mountInfo.syncManager.stop();
4100
+ mountInfo.mounted = false;
4101
+ this.activeMounts.delete(mountPath);
4102
+ } else try {
4103
+ await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
4104
+ mountInfo.mounted = false;
4105
+ this.activeMounts.delete(mountPath);
4106
+ } finally {
4107
+ await this.deletePasswordFile(mountInfo.passwordFilePath);
4108
+ }
4109
+ unmountOutcome = "success";
4110
+ } catch (error) {
4111
+ unmountError = error instanceof Error ? error : new Error(String(error));
4112
+ throw error;
4203
4113
  } finally {
4204
- await this.deletePasswordFile(mountInfo.passwordFilePath);
4114
+ logCanonicalEvent(this.logger, {
4115
+ event: "bucket.unmount",
4116
+ outcome: unmountOutcome,
4117
+ durationMs: Date.now() - unmountStartTime,
4118
+ mountPath,
4119
+ bucket: mountInfo?.bucket,
4120
+ error: unmountError
4121
+ });
4205
4122
  }
4206
- this.logger.info(`Successfully unmounted bucket from ${mountPath}`);
4207
4123
  }
4208
4124
  /**
4209
4125
  * Validate mount options
@@ -4232,18 +4148,19 @@ var Sandbox = class Sandbox extends Container {
4232
4148
  async createPasswordFile(passwordFilePath, bucket, credentials) {
4233
4149
  const content = `${bucket}:${credentials.accessKeyId}:${credentials.secretAccessKey}`;
4234
4150
  await this.writeFile(passwordFilePath, content);
4235
- await this.exec(`chmod 0600 ${shellEscape(passwordFilePath)}`);
4236
- this.logger.debug(`Created password file: ${passwordFilePath}`);
4151
+ await this.execInternal(`chmod 0600 ${shellEscape(passwordFilePath)}`);
4237
4152
  }
4238
4153
  /**
4239
4154
  * Delete password file
4240
4155
  */
4241
4156
  async deletePasswordFile(passwordFilePath) {
4242
4157
  try {
4243
- await this.exec(`rm -f ${shellEscape(passwordFilePath)}`);
4244
- this.logger.debug(`Deleted password file: ${passwordFilePath}`);
4158
+ await this.execInternal(`rm -f ${shellEscape(passwordFilePath)}`);
4245
4159
  } catch (error) {
4246
- this.logger.warn(`Failed to delete password file ${passwordFilePath}`, { error: error instanceof Error ? error.message : String(error) });
4160
+ this.logger.warn("password file cleanup failed", {
4161
+ passwordFilePath,
4162
+ error: error instanceof Error ? error.message : String(error)
4163
+ });
4247
4164
  }
4248
4165
  }
4249
4166
  /**
@@ -4258,44 +4175,61 @@ var Sandbox = class Sandbox extends Container {
4258
4175
  s3fsArgs.push(`url=${options.endpoint}`);
4259
4176
  const optionsStr = shellEscape(s3fsArgs.join(","));
4260
4177
  const mountCmd = `s3fs ${shellEscape(bucket)} ${shellEscape(mountPath)} -o ${optionsStr}`;
4261
- this.logger.debug("Executing s3fs mount", {
4262
- bucket,
4263
- mountPath,
4264
- provider,
4265
- resolvedOptions
4266
- });
4267
- const result = await this.exec(mountCmd);
4178
+ const result = await this.execInternal(mountCmd);
4268
4179
  if (result.exitCode !== 0) throw new S3FSMountError(`S3FS mount failed: ${result.stderr || result.stdout || "Unknown error"}`);
4269
- this.logger.debug("Mount command executed successfully");
4270
4180
  }
4271
4181
  /**
4272
4182
  * Cleanup and destroy the sandbox container
4273
4183
  */
4274
4184
  async destroy() {
4275
- this.logger.info("Destroying sandbox container");
4276
- if (this.ctx.container?.running) try {
4277
- await this.client.desktop.stop();
4278
- } catch {}
4279
- this.client.disconnect();
4280
- for (const [mountPath, mountInfo] of this.activeMounts.entries()) if (mountInfo.mountType === "local-sync") try {
4281
- await mountInfo.syncManager.stop();
4282
- mountInfo.mounted = false;
4283
- } catch (error) {
4284
- const errorMsg = error instanceof Error ? error.message : String(error);
4285
- this.logger.warn(`Failed to stop local sync for ${mountPath}: ${errorMsg}`);
4286
- }
4287
- else {
4288
- if (mountInfo.mounted) try {
4289
- this.logger.info(`Unmounting bucket ${mountInfo.bucket} from ${mountPath}`);
4290
- await this.exec(`fusermount -u ${shellEscape(mountPath)}`);
4291
- mountInfo.mounted = false;
4292
- } catch (error) {
4293
- const errorMsg = error instanceof Error ? error.message : String(error);
4294
- this.logger.warn(`Failed to unmount bucket ${mountInfo.bucket} from ${mountPath}: ${errorMsg}`);
4185
+ const startTime = Date.now();
4186
+ let mountsProcessed = 0;
4187
+ let mountFailures = 0;
4188
+ let outcome = "error";
4189
+ let caughtError;
4190
+ try {
4191
+ if (this.ctx.container?.running) try {
4192
+ await this.client.desktop.stop();
4193
+ } catch {}
4194
+ this.client.disconnect();
4195
+ for (const [mountPath, mountInfo] of this.activeMounts.entries()) {
4196
+ mountsProcessed++;
4197
+ if (mountInfo.mountType === "local-sync") try {
4198
+ await mountInfo.syncManager.stop();
4199
+ mountInfo.mounted = false;
4200
+ } catch (error) {
4201
+ mountFailures++;
4202
+ const errorMsg = error instanceof Error ? error.message : String(error);
4203
+ this.logger.warn(`Failed to stop local sync for ${mountPath}: ${errorMsg}`);
4204
+ }
4205
+ else {
4206
+ if (mountInfo.mounted) try {
4207
+ this.logger.debug(`Unmounting bucket ${mountInfo.bucket} from ${mountPath}`);
4208
+ await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
4209
+ mountInfo.mounted = false;
4210
+ } catch (error) {
4211
+ mountFailures++;
4212
+ const errorMsg = error instanceof Error ? error.message : String(error);
4213
+ this.logger.warn(`Failed to unmount bucket ${mountInfo.bucket} from ${mountPath}: ${errorMsg}`);
4214
+ }
4215
+ await this.deletePasswordFile(mountInfo.passwordFilePath);
4216
+ }
4295
4217
  }
4296
- await this.deletePasswordFile(mountInfo.passwordFilePath);
4218
+ outcome = "success";
4219
+ await super.destroy();
4220
+ } catch (error) {
4221
+ caughtError = error instanceof Error ? error : new Error(String(error));
4222
+ throw error;
4223
+ } finally {
4224
+ logCanonicalEvent(this.logger, {
4225
+ event: "sandbox.destroy",
4226
+ outcome,
4227
+ durationMs: Date.now() - startTime,
4228
+ mountsProcessed,
4229
+ mountFailures,
4230
+ error: caughtError
4231
+ });
4297
4232
  }
4298
- await super.destroy();
4299
4233
  }
4300
4234
  onStart() {
4301
4235
  this.logger.debug("Sandbox started");
@@ -4308,23 +4242,27 @@ var Sandbox = class Sandbox extends Container {
4308
4242
  * Logs a warning if there's a mismatch
4309
4243
  */
4310
4244
  async checkVersionCompatibility() {
4245
+ const sdkVersion = SDK_VERSION;
4246
+ let containerVersion;
4247
+ let outcome;
4311
4248
  try {
4312
- const sdkVersion = SDK_VERSION;
4313
- const containerVersion = await this.client.utils.getVersion();
4314
- if (containerVersion === "unknown") {
4315
- this.logger.warn("Container version check: Container version could not be determined. This may indicate an outdated container image. Please update your container to match SDK version " + sdkVersion);
4316
- return;
4317
- }
4318
- if (containerVersion !== sdkVersion) {
4319
- const message = `Version mismatch detected! SDK version (${sdkVersion}) does not match container version (${containerVersion}). This may cause compatibility issues. Please update your container image to version ${sdkVersion}`;
4320
- this.logger.warn(message);
4321
- } else this.logger.debug("Version check passed", {
4322
- sdkVersion,
4323
- containerVersion
4324
- });
4249
+ containerVersion = await this.client.utils.getVersion();
4250
+ if (containerVersion === "unknown") outcome = "container_version_unknown";
4251
+ else if (containerVersion !== sdkVersion) outcome = "version_mismatch";
4252
+ else outcome = "compatible";
4325
4253
  } catch (error) {
4326
- this.logger.debug("Version compatibility check encountered an error", { error: error instanceof Error ? error.message : String(error) });
4327
- }
4254
+ outcome = "check_failed";
4255
+ containerVersion = void 0;
4256
+ }
4257
+ const successLevel = outcome === "compatible" ? "debug" : outcome === "container_version_unknown" ? "info" : "warn";
4258
+ logCanonicalEvent(this.logger, {
4259
+ event: "version.check",
4260
+ outcome: "success",
4261
+ durationMs: 0,
4262
+ sdkVersion,
4263
+ containerVersion: containerVersion ?? "unknown",
4264
+ versionOutcome: outcome
4265
+ }, { successLevel });
4328
4266
  }
4329
4267
  async onStop() {
4330
4268
  this.logger.debug("Sandbox stopped");
@@ -4345,84 +4283,66 @@ var Sandbox = class Sandbox extends Container {
4345
4283
  const state = await this.getState();
4346
4284
  const containerRunning = this.ctx.container?.running;
4347
4285
  const staleStateDetected = state.status === "healthy" && containerRunning === false;
4348
- if (state.status !== "healthy" || containerRunning === false) {
4349
- if (staleStateDetected) this.logger.debug("Stale container state detected: persisted state is healthy but container is not running");
4350
- try {
4351
- this.logger.debug("Starting container with configured timeouts", {
4352
- instanceTimeout: this.containerTimeouts.instanceGetTimeoutMS,
4353
- portTimeout: this.containerTimeouts.portReadyTimeoutMS
4354
- });
4355
- await this.startAndWaitForPorts({
4356
- ports: port,
4357
- cancellationOptions: {
4358
- instanceGetTimeoutMS: this.containerTimeouts.instanceGetTimeoutMS,
4359
- portReadyTimeoutMS: this.containerTimeouts.portReadyTimeoutMS,
4360
- waitInterval: this.containerTimeouts.waitIntervalMS,
4361
- abort: request.signal
4286
+ if (state.status !== "healthy" || containerRunning === false) try {
4287
+ await this.startAndWaitForPorts({
4288
+ ports: port,
4289
+ cancellationOptions: {
4290
+ instanceGetTimeoutMS: this.containerTimeouts.instanceGetTimeoutMS,
4291
+ portReadyTimeoutMS: this.containerTimeouts.portReadyTimeoutMS,
4292
+ waitInterval: this.containerTimeouts.waitIntervalMS,
4293
+ abort: request.signal
4294
+ }
4295
+ });
4296
+ } catch (e) {
4297
+ if (this.isNoInstanceError(e)) {
4298
+ const errorBody$1 = {
4299
+ code: ErrorCode.INTERNAL_ERROR,
4300
+ message: "Container is currently provisioning. This can take several minutes on first deployment.",
4301
+ context: { phase: "provisioning" },
4302
+ httpStatus: 503,
4303
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4304
+ suggestion: "This is expected during first deployment. The SDK will retry automatically."
4305
+ };
4306
+ return new Response(JSON.stringify(errorBody$1), {
4307
+ status: 503,
4308
+ headers: {
4309
+ "Content-Type": "application/json",
4310
+ "Retry-After": "10"
4362
4311
  }
4363
4312
  });
4364
- } catch (e) {
4365
- if (this.isNoInstanceError(e)) {
4366
- const errorBody$1 = {
4367
- code: ErrorCode.INTERNAL_ERROR,
4368
- message: "Container is currently provisioning. This can take several minutes on first deployment.",
4369
- context: { phase: "provisioning" },
4370
- httpStatus: 503,
4371
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4372
- suggestion: "This is expected during first deployment. The SDK will retry automatically."
4373
- };
4374
- return new Response(JSON.stringify(errorBody$1), {
4375
- status: 503,
4376
- headers: {
4377
- "Content-Type": "application/json",
4378
- "Retry-After": "10"
4379
- }
4380
- });
4381
- }
4382
- if (this.isPermanentStartupError(e)) {
4383
- this.logger.error("Permanent container startup error, returning 500", e instanceof Error ? e : new Error(String(e)));
4384
- const errorBody$1 = {
4385
- code: ErrorCode.INTERNAL_ERROR,
4386
- message: "Container failed to start due to a permanent error. Check your container configuration.",
4387
- context: {
4388
- phase: "startup",
4389
- error: e instanceof Error ? e.message : String(e)
4390
- },
4391
- httpStatus: 500,
4392
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4393
- suggestion: "This error will not resolve with retries. Check container logs, image name, and resource limits."
4394
- };
4395
- return new Response(JSON.stringify(errorBody$1), {
4396
- status: 500,
4397
- headers: { "Content-Type": "application/json" }
4398
- });
4399
- }
4400
- if (this.isTransientStartupError(e)) {
4401
- if (staleStateDetected) {
4402
- this.logger.warn("Container startup failed after stale state detection, aborting DO for recovery", { error: e instanceof Error ? e.message : String(e) });
4403
- this.ctx.abort();
4404
- } else this.logger.debug("Transient container startup error, returning 503", { error: e instanceof Error ? e.message : String(e) });
4405
- const errorBody$1 = {
4406
- code: ErrorCode.INTERNAL_ERROR,
4407
- message: "Container is starting. Please retry in a moment.",
4408
- context: {
4409
- phase: "startup",
4410
- error: e instanceof Error ? e.message : String(e)
4411
- },
4412
- httpStatus: 503,
4413
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4414
- suggestion: "The container is booting. The SDK will retry automatically."
4415
- };
4416
- return new Response(JSON.stringify(errorBody$1), {
4417
- status: 503,
4418
- headers: {
4419
- "Content-Type": "application/json",
4420
- "Retry-After": "3"
4421
- }
4313
+ }
4314
+ if (this.isPermanentStartupError(e)) {
4315
+ this.logger.error("Permanent container startup error, returning 500", e instanceof Error ? e : new Error(String(e)));
4316
+ const errorBody$1 = {
4317
+ code: ErrorCode.INTERNAL_ERROR,
4318
+ message: "Container failed to start due to a permanent error. Check your container configuration.",
4319
+ context: {
4320
+ phase: "startup",
4321
+ error: e instanceof Error ? e.message : String(e)
4322
+ },
4323
+ httpStatus: 500,
4324
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4325
+ suggestion: "This error will not resolve with retries. Check container logs, image name, and resource limits."
4326
+ };
4327
+ return new Response(JSON.stringify(errorBody$1), {
4328
+ status: 500,
4329
+ headers: { "Content-Type": "application/json" }
4330
+ });
4331
+ }
4332
+ if (this.isTransientStartupError(e)) {
4333
+ if (staleStateDetected) {
4334
+ this.logger.warn("container.startup", {
4335
+ outcome: "stale_state_abort",
4336
+ staleStateDetected: true,
4337
+ error: e instanceof Error ? e.message : String(e)
4422
4338
  });
4423
- }
4424
- this.logger.warn("Unrecognized container startup error, returning 503 for retry", { error: e instanceof Error ? e.message : String(e) });
4425
- const errorBody = {
4339
+ this.ctx.abort();
4340
+ } else this.logger.debug("container.startup", {
4341
+ outcome: "transient_error",
4342
+ staleStateDetected,
4343
+ error: e instanceof Error ? e.message : String(e)
4344
+ });
4345
+ const errorBody$1 = {
4426
4346
  code: ErrorCode.INTERNAL_ERROR,
4427
4347
  message: "Container is starting. Please retry in a moment.",
4428
4348
  context: {
@@ -4431,16 +4351,39 @@ var Sandbox = class Sandbox extends Container {
4431
4351
  },
4432
4352
  httpStatus: 503,
4433
4353
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4434
- suggestion: "The SDK will retry automatically. If this persists, the container may need redeployment."
4354
+ suggestion: "The container is booting. The SDK will retry automatically."
4435
4355
  };
4436
- return new Response(JSON.stringify(errorBody), {
4356
+ return new Response(JSON.stringify(errorBody$1), {
4437
4357
  status: 503,
4438
4358
  headers: {
4439
4359
  "Content-Type": "application/json",
4440
- "Retry-After": "5"
4360
+ "Retry-After": "3"
4441
4361
  }
4442
4362
  });
4443
4363
  }
4364
+ this.logger.warn("container.startup", {
4365
+ outcome: "unrecognized_error",
4366
+ staleStateDetected,
4367
+ error: e instanceof Error ? e.message : String(e)
4368
+ });
4369
+ const errorBody = {
4370
+ code: ErrorCode.INTERNAL_ERROR,
4371
+ message: "Container is starting. Please retry in a moment.",
4372
+ context: {
4373
+ phase: "startup",
4374
+ error: e instanceof Error ? e.message : String(e)
4375
+ },
4376
+ httpStatus: 503,
4377
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4378
+ suggestion: "The SDK will retry automatically. If this persists, the container may need redeployment."
4379
+ };
4380
+ return new Response(JSON.stringify(errorBody), {
4381
+ status: 503,
4382
+ headers: {
4383
+ "Content-Type": "application/json",
4384
+ "Retry-After": "5"
4385
+ }
4386
+ });
4444
4387
  }
4445
4388
  return await super.containerFetch(requestOrUrl, portOrInit, portParam);
4446
4389
  }
@@ -4610,31 +4553,59 @@ var Sandbox = class Sandbox extends Container {
4610
4553
  return this.execWithSession(command, session, options);
4611
4554
  }
4612
4555
  /**
4556
+ * Execute an infrastructure command (backup, mount, env setup, etc.)
4557
+ * tagged with origin: 'internal' so logging demotes it to debug level.
4558
+ */
4559
+ async execInternal(command) {
4560
+ const session = await this.ensureDefaultSession();
4561
+ return this.execWithSession(command, session, { origin: "internal" });
4562
+ }
4563
+ /**
4613
4564
  * Internal session-aware exec implementation
4614
4565
  * Used by both public exec() and session wrappers
4615
4566
  */
4616
4567
  async execWithSession(command, sessionId, options) {
4617
4568
  const startTime = Date.now();
4618
4569
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4570
+ let execOutcome;
4571
+ let execError;
4619
4572
  try {
4620
4573
  if (options?.signal?.aborted) throw new Error("Operation was aborted");
4621
4574
  let result;
4622
4575
  if (options?.stream && options?.onOutput) result = await this.executeWithStreaming(command, sessionId, options, startTime, timestamp);
4623
4576
  else {
4624
- const commandOptions = options && (options.timeout !== void 0 || options.env !== void 0 || options.cwd !== void 0) ? {
4577
+ const commandOptions = options && (options.timeout !== void 0 || options.env !== void 0 || options.cwd !== void 0 || options.origin !== void 0) ? {
4625
4578
  timeoutMs: options.timeout,
4626
4579
  env: options.env,
4627
- cwd: options.cwd
4580
+ cwd: options.cwd,
4581
+ origin: options.origin
4628
4582
  } : void 0;
4629
4583
  const response = await this.client.commands.execute(command, sessionId, commandOptions);
4630
4584
  const duration = Date.now() - startTime;
4631
4585
  result = this.mapExecuteResponseToExecResult(response, duration, sessionId);
4632
4586
  }
4587
+ execOutcome = {
4588
+ exitCode: result.exitCode,
4589
+ success: result.success
4590
+ };
4633
4591
  if (options?.onComplete) options.onComplete(result);
4634
4592
  return result;
4635
4593
  } catch (error) {
4594
+ execError = error instanceof Error ? error : new Error(String(error));
4636
4595
  if (options?.onError && error instanceof Error) options.onError(error);
4637
4596
  throw error;
4597
+ } finally {
4598
+ logCanonicalEvent(this.logger, {
4599
+ event: "sandbox.exec",
4600
+ outcome: execError ? "error" : "success",
4601
+ command,
4602
+ exitCode: execOutcome?.exitCode,
4603
+ durationMs: Date.now() - startTime,
4604
+ sessionId,
4605
+ origin: options?.origin ?? "user",
4606
+ error: execError ?? void 0,
4607
+ errorMessage: execError?.message
4608
+ });
4638
4609
  }
4639
4610
  }
4640
4611
  async executeWithStreaming(command, sessionId, options, startTime, timestamp) {
@@ -4644,7 +4615,8 @@ var Sandbox = class Sandbox extends Container {
4644
4615
  const stream = await this.client.commands.executeStream(command, sessionId, {
4645
4616
  timeoutMs: options.timeout,
4646
4617
  env: options.env,
4647
- cwd: options.cwd
4618
+ cwd: options.cwd,
4619
+ origin: options.origin
4648
4620
  });
4649
4621
  for await (const event of parseSSEStream(stream)) {
4650
4622
  if (options.signal?.aborted) throw new Error("Operation was aborted");
@@ -5197,41 +5169,78 @@ var Sandbox = class Sandbox extends Container {
5197
5169
  * // url: https://8080-sandbox-id-my_token_v1.example.com
5198
5170
  */
5199
5171
  async exposePort(port, options) {
5200
- if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be 1024-65535, excluding 3000 (sandbox control plane).`);
5201
- if (options.hostname.endsWith(".workers.dev")) throw new CustomDomainRequiredError({
5202
- code: ErrorCode.CUSTOM_DOMAIN_REQUIRED,
5203
- message: `Port exposure requires a custom domain. .workers.dev domains do not support wildcard subdomains required for port proxying.`,
5204
- context: { originalError: options.hostname },
5205
- httpStatus: 400,
5206
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5207
- });
5208
- if (!this.sandboxName) throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
5209
- let token;
5210
- if (options.token !== void 0) {
5211
- this.validateCustomToken(options.token);
5212
- token = options.token;
5213
- } else token = this.generatePortToken();
5214
- const tokens = await this.ctx.storage.get("portTokens") || {};
5215
- const existingPort = Object.entries(tokens).find(([p, t]) => t === token && p !== port.toString());
5216
- if (existingPort) throw new SecurityError(`Token '${token}' is already in use by port ${existingPort[0]}. Please use a different token.`);
5217
- const sessionId = await this.ensureDefaultSession();
5218
- await this.client.ports.exposePort(port, sessionId, options?.name);
5219
- tokens[port.toString()] = token;
5220
- await this.ctx.storage.put("portTokens", tokens);
5221
- return {
5222
- url: this.constructPreviewUrl(port, this.sandboxName, options.hostname, token),
5223
- port,
5224
- name: options?.name
5225
- };
5172
+ const exposeStartTime = Date.now();
5173
+ let outcome = "error";
5174
+ let caughtError;
5175
+ try {
5176
+ if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be 1024-65535, excluding 3000 (sandbox control plane).`);
5177
+ if (options.hostname.endsWith(".workers.dev")) throw new CustomDomainRequiredError({
5178
+ code: ErrorCode.CUSTOM_DOMAIN_REQUIRED,
5179
+ message: `Port exposure requires a custom domain. .workers.dev domains do not support wildcard subdomains required for port proxying.`,
5180
+ context: { originalError: options.hostname },
5181
+ httpStatus: 400,
5182
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5183
+ });
5184
+ if (!this.sandboxName) throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
5185
+ let token;
5186
+ if (options.token !== void 0) {
5187
+ this.validateCustomToken(options.token);
5188
+ token = options.token;
5189
+ } else token = this.generatePortToken();
5190
+ const tokens = await this.ctx.storage.get("portTokens") || {};
5191
+ const existingPort = Object.entries(tokens).find(([p, t]) => t === token && p !== port.toString());
5192
+ if (existingPort) throw new SecurityError(`Token '${token}' is already in use by port ${existingPort[0]}. Please use a different token.`);
5193
+ const sessionId = await this.ensureDefaultSession();
5194
+ await this.client.ports.exposePort(port, sessionId, options?.name);
5195
+ tokens[port.toString()] = token;
5196
+ await this.ctx.storage.put("portTokens", tokens);
5197
+ const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname, token);
5198
+ outcome = "success";
5199
+ return {
5200
+ url,
5201
+ port,
5202
+ name: options?.name
5203
+ };
5204
+ } catch (error) {
5205
+ caughtError = error instanceof Error ? error : new Error(String(error));
5206
+ throw error;
5207
+ } finally {
5208
+ logCanonicalEvent(this.logger, {
5209
+ event: "port.expose",
5210
+ outcome,
5211
+ port,
5212
+ durationMs: Date.now() - exposeStartTime,
5213
+ name: options?.name,
5214
+ hostname: options.hostname,
5215
+ error: caughtError
5216
+ });
5217
+ }
5226
5218
  }
5227
5219
  async unexposePort(port) {
5228
- if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be 1024-65535, excluding 3000 (sandbox control plane).`);
5229
- const sessionId = await this.ensureDefaultSession();
5230
- await this.client.ports.unexposePort(port, sessionId);
5231
- const tokens = await this.ctx.storage.get("portTokens") || {};
5232
- if (tokens[port.toString()]) {
5233
- delete tokens[port.toString()];
5234
- await this.ctx.storage.put("portTokens", tokens);
5220
+ const unexposeStartTime = Date.now();
5221
+ let outcome = "error";
5222
+ let caughtError;
5223
+ try {
5224
+ if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be 1024-65535, excluding 3000 (sandbox control plane).`);
5225
+ const sessionId = await this.ensureDefaultSession();
5226
+ await this.client.ports.unexposePort(port, sessionId);
5227
+ const tokens = await this.ctx.storage.get("portTokens") || {};
5228
+ if (tokens[port.toString()]) {
5229
+ delete tokens[port.toString()];
5230
+ await this.ctx.storage.put("portTokens", tokens);
5231
+ }
5232
+ outcome = "success";
5233
+ } catch (error) {
5234
+ caughtError = error instanceof Error ? error : new Error(String(error));
5235
+ throw error;
5236
+ } finally {
5237
+ logCanonicalEvent(this.logger, {
5238
+ event: "port.unexpose",
5239
+ outcome,
5240
+ port,
5241
+ durationMs: Date.now() - unexposeStartTime,
5242
+ error: caughtError
5243
+ });
5235
5244
  }
5236
5245
  }
5237
5246
  async getExposedPorts(hostname) {
@@ -5405,12 +5414,12 @@ var Sandbox = class Sandbox extends Container {
5405
5414
  try {
5406
5415
  for (const key of toUnset) {
5407
5416
  const unsetCommand = `unset ${key}`;
5408
- const result = await this.client.commands.execute(unsetCommand, sessionId);
5417
+ const result = await this.client.commands.execute(unsetCommand, sessionId, { origin: "internal" });
5409
5418
  if (result.exitCode !== 0) throw new Error(`Failed to unset ${key}: ${result.stderr || "Unknown error"}`);
5410
5419
  }
5411
5420
  for (const [key, value] of Object.entries(toSet)) {
5412
5421
  const exportCommand = `export ${key}=${shellEscape(value)}`;
5413
- const result = await this.client.commands.execute(exportCommand, sessionId);
5422
+ const result = await this.client.commands.execute(exportCommand, sessionId, { origin: "internal" });
5414
5423
  if (result.exitCode !== 0) throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
5415
5424
  }
5416
5425
  } catch (error) {
@@ -5490,20 +5499,17 @@ var Sandbox = class Sandbox extends Container {
5490
5499
  }
5491
5500
  static PRESIGNED_URL_EXPIRY_SECONDS = 3600;
5492
5501
  /**
5493
- * Ensure a dedicated session for backup operations exists.
5494
- * Isolates backup shell commands (curl, stat, rm, mkdir) from user exec()
5495
- * calls to prevent session state interference and interleaving.
5502
+ * Create a unique, dedicated session for a single backup operation.
5503
+ * Each call produces a fresh session ID so concurrent or sequential
5504
+ * operations never share shell state. Callers must destroy the session
5505
+ * in a finally block via `client.utils.deleteSession()`.
5496
5506
  */
5497
5507
  async ensureBackupSession() {
5498
- const sessionId = "__sandbox_backup__";
5499
- try {
5500
- await this.client.utils.createSession({
5501
- id: sessionId,
5502
- cwd: "/"
5503
- });
5504
- } catch (error) {
5505
- if (!(error instanceof SessionAlreadyExistsError)) throw error;
5506
- }
5508
+ const sessionId = `__sandbox_backup_${crypto.randomUUID()}`;
5509
+ await this.client.utils.createSession({
5510
+ id: sessionId,
5511
+ cwd: "/"
5512
+ });
5507
5513
  return sessionId;
5508
5514
  }
5509
5515
  /**
@@ -5562,11 +5568,6 @@ var Sandbox = class Sandbox extends Container {
5562
5568
  */
5563
5569
  async uploadBackupPresigned(archivePath, r2Key, archiveSize, backupId, dir, backupSession) {
5564
5570
  const presignedUrl = await this.generatePresignedPutUrl(r2Key);
5565
- this.logger.info("Uploading backup via presigned PUT", {
5566
- r2Key,
5567
- archiveSize,
5568
- backupId
5569
- });
5570
5571
  const curlCmd = [
5571
5572
  "curl -sSf",
5572
5573
  "-X PUT",
@@ -5578,7 +5579,10 @@ var Sandbox = class Sandbox extends Container {
5578
5579
  `-T ${shellEscape(archivePath)}`,
5579
5580
  shellEscape(presignedUrl)
5580
5581
  ].join(" ");
5581
- const result = await this.execWithSession(curlCmd, backupSession, { timeout: 181e4 });
5582
+ const result = await this.execWithSession(curlCmd, backupSession, {
5583
+ timeout: 181e4,
5584
+ origin: "internal"
5585
+ });
5582
5586
  if (result.exitCode !== 0) throw new BackupCreateError({
5583
5587
  message: `Presigned URL upload failed (exit code ${result.exitCode}): ${result.stderr}`,
5584
5588
  code: ErrorCode.BACKUP_CREATE_FAILED,
@@ -5611,12 +5615,7 @@ var Sandbox = class Sandbox extends Container {
5611
5615
  */
5612
5616
  async downloadBackupPresigned(archivePath, r2Key, expectedSize, backupId, dir, backupSession) {
5613
5617
  const presignedUrl = await this.generatePresignedGetUrl(r2Key);
5614
- this.logger.info("Downloading backup via presigned GET", {
5615
- r2Key,
5616
- expectedSize,
5617
- backupId
5618
- });
5619
- await this.execWithSession("mkdir -p /var/backups", backupSession);
5618
+ await this.execWithSession("mkdir -p /var/backups", backupSession, { origin: "internal" });
5620
5619
  const tmpPath = `${archivePath}.tmp`;
5621
5620
  const curlCmd = [
5622
5621
  "curl -sSf",
@@ -5627,9 +5626,12 @@ var Sandbox = class Sandbox extends Container {
5627
5626
  `-o ${shellEscape(tmpPath)}`,
5628
5627
  shellEscape(presignedUrl)
5629
5628
  ].join(" ");
5630
- const result = await this.execWithSession(curlCmd, backupSession, { timeout: 181e4 });
5629
+ const result = await this.execWithSession(curlCmd, backupSession, {
5630
+ timeout: 181e4,
5631
+ origin: "internal"
5632
+ });
5631
5633
  if (result.exitCode !== 0) {
5632
- await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession).catch(() => {});
5634
+ await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" }).catch(() => {});
5633
5635
  throw new BackupRestoreError({
5634
5636
  message: `Presigned URL download failed (exit code ${result.exitCode}): ${result.stderr}`,
5635
5637
  code: ErrorCode.BACKUP_RESTORE_FAILED,
@@ -5641,10 +5643,10 @@ var Sandbox = class Sandbox extends Container {
5641
5643
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5642
5644
  });
5643
5645
  }
5644
- const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(tmpPath)}`, backupSession);
5646
+ const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" });
5645
5647
  const actualSize = parseInt(sizeCheck.stdout.trim(), 10);
5646
5648
  if (actualSize !== expectedSize) {
5647
- await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession).catch(() => {});
5649
+ await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" }).catch(() => {});
5648
5650
  throw new BackupRestoreError({
5649
5651
  message: `Downloaded archive size mismatch: expected ${expectedSize}, got ${actualSize}`,
5650
5652
  code: ErrorCode.BACKUP_RESTORE_FAILED,
@@ -5656,9 +5658,9 @@ var Sandbox = class Sandbox extends Container {
5656
5658
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5657
5659
  });
5658
5660
  }
5659
- const mvResult = await this.execWithSession(`mv ${shellEscape(tmpPath)} ${shellEscape(archivePath)}`, backupSession);
5661
+ const mvResult = await this.execWithSession(`mv ${shellEscape(tmpPath)} ${shellEscape(archivePath)}`, backupSession, { origin: "internal" });
5660
5662
  if (mvResult.exitCode !== 0) {
5661
- await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession).catch(() => {});
5663
+ await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" }).catch(() => {});
5662
5664
  throw new BackupRestoreError({
5663
5665
  message: `Failed to finalize downloaded archive: ${mvResult.stderr}`,
5664
5666
  code: ErrorCode.BACKUP_RESTORE_FAILED,
@@ -5713,68 +5715,68 @@ var Sandbox = class Sandbox extends Container {
5713
5715
  const DEFAULT_TTL_SECONDS = 259200;
5714
5716
  const MAX_NAME_LENGTH = 256;
5715
5717
  const { dir, name, ttl = DEFAULT_TTL_SECONDS, gitignore = false, excludes = [] } = options;
5716
- Sandbox.validateBackupDir(dir, "BackupOptions.dir");
5717
- if (name !== void 0) {
5718
- if (typeof name !== "string" || name.length > MAX_NAME_LENGTH) throw new InvalidBackupConfigError({
5719
- message: `BackupOptions.name must be a string of at most ${MAX_NAME_LENGTH} characters`,
5718
+ const backupStartTime = Date.now();
5719
+ let backupId;
5720
+ let sizeBytes;
5721
+ let outcome = "error";
5722
+ let caughtError;
5723
+ let backupSession;
5724
+ try {
5725
+ Sandbox.validateBackupDir(dir, "BackupOptions.dir");
5726
+ if (name !== void 0) {
5727
+ if (typeof name !== "string" || name.length > MAX_NAME_LENGTH) throw new InvalidBackupConfigError({
5728
+ message: `BackupOptions.name must be a string of at most ${MAX_NAME_LENGTH} characters`,
5729
+ code: ErrorCode.INVALID_BACKUP_CONFIG,
5730
+ httpStatus: 400,
5731
+ context: { reason: `name must be a string of at most ${MAX_NAME_LENGTH} characters` },
5732
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5733
+ });
5734
+ if (/[\u0000-\u001f\u007f]/.test(name)) throw new InvalidBackupConfigError({
5735
+ message: "BackupOptions.name must not contain control characters",
5736
+ code: ErrorCode.INVALID_BACKUP_CONFIG,
5737
+ httpStatus: 400,
5738
+ context: { reason: "name must not contain control characters" },
5739
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5740
+ });
5741
+ }
5742
+ if (ttl <= 0) throw new InvalidBackupConfigError({
5743
+ message: "BackupOptions.ttl must be a positive number of seconds",
5720
5744
  code: ErrorCode.INVALID_BACKUP_CONFIG,
5721
5745
  httpStatus: 400,
5722
- context: { reason: `name must be a string of at most ${MAX_NAME_LENGTH} characters` },
5746
+ context: { reason: "ttl must be a positive number of seconds" },
5723
5747
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5724
5748
  });
5725
- if (/[\u0000-\u001f\u007f]/.test(name)) throw new InvalidBackupConfigError({
5726
- message: "BackupOptions.name must not contain control characters",
5749
+ if (typeof gitignore !== "boolean") throw new InvalidBackupConfigError({
5750
+ message: "BackupOptions.gitignore must be a boolean",
5727
5751
  code: ErrorCode.INVALID_BACKUP_CONFIG,
5728
5752
  httpStatus: 400,
5729
- context: { reason: "name must not contain control characters" },
5753
+ context: { reason: "gitignore must be a boolean" },
5730
5754
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5731
5755
  });
5732
- }
5733
- if (ttl <= 0) throw new InvalidBackupConfigError({
5734
- message: "BackupOptions.ttl must be a positive number of seconds",
5735
- code: ErrorCode.INVALID_BACKUP_CONFIG,
5736
- httpStatus: 400,
5737
- context: { reason: "ttl must be a positive number of seconds" },
5738
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5739
- });
5740
- if (typeof gitignore !== "boolean") throw new InvalidBackupConfigError({
5741
- message: "BackupOptions.gitignore must be a boolean",
5742
- code: ErrorCode.INVALID_BACKUP_CONFIG,
5743
- httpStatus: 400,
5744
- context: { reason: "gitignore must be a boolean" },
5745
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5746
- });
5747
- if (!Array.isArray(excludes) || !excludes.every((e) => typeof e === "string")) throw new InvalidBackupConfigError({
5748
- message: "BackupOptions.excludes must be an array of strings",
5749
- code: ErrorCode.INVALID_BACKUP_CONFIG,
5750
- httpStatus: 400,
5751
- context: { reason: "excludes must be an array of strings" },
5752
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5753
- });
5754
- const backupSession = await this.ensureBackupSession();
5755
- const backupId = crypto.randomUUID();
5756
- const archivePath = `/var/backups/${backupId}.sqsh`;
5757
- this.logger.info("Creating backup", {
5758
- backupId,
5759
- dir,
5760
- name,
5761
- gitignore,
5762
- excludes
5763
- });
5764
- const createResult = await this.client.backup.createArchive(dir, archivePath, backupSession, gitignore, excludes);
5765
- if (!createResult.success) throw new BackupCreateError({
5766
- message: "Container failed to create backup archive",
5767
- code: ErrorCode.BACKUP_CREATE_FAILED,
5768
- httpStatus: 500,
5769
- context: {
5770
- dir,
5771
- backupId
5772
- },
5773
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5774
- });
5775
- const r2Key = `backups/${backupId}/data.sqsh`;
5776
- const metaKey = `backups/${backupId}/meta.json`;
5777
- try {
5756
+ if (!Array.isArray(excludes) || !excludes.every((e) => typeof e === "string")) throw new InvalidBackupConfigError({
5757
+ message: "BackupOptions.excludes must be an array of strings",
5758
+ code: ErrorCode.INVALID_BACKUP_CONFIG,
5759
+ httpStatus: 400,
5760
+ context: { reason: "excludes must be an array of strings" },
5761
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5762
+ });
5763
+ backupSession = await this.ensureBackupSession();
5764
+ backupId = crypto.randomUUID();
5765
+ const archivePath = `/var/backups/${backupId}.sqsh`;
5766
+ const createResult = await this.client.backup.createArchive(dir, archivePath, backupSession, gitignore, excludes);
5767
+ if (!createResult.success) throw new BackupCreateError({
5768
+ message: "Container failed to create backup archive",
5769
+ code: ErrorCode.BACKUP_CREATE_FAILED,
5770
+ httpStatus: 500,
5771
+ context: {
5772
+ dir,
5773
+ backupId
5774
+ },
5775
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5776
+ });
5777
+ sizeBytes = createResult.sizeBytes;
5778
+ const r2Key = `backups/${backupId}/data.sqsh`;
5779
+ const metaKey = `backups/${backupId}/meta.json`;
5778
5780
  await this.uploadBackupPresigned(archivePath, r2Key, createResult.sizeBytes, backupId, dir, backupSession);
5779
5781
  const metadata = {
5780
5782
  id: backupId,
@@ -5785,21 +5787,35 @@ var Sandbox = class Sandbox extends Container {
5785
5787
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
5786
5788
  };
5787
5789
  await bucket.put(metaKey, JSON.stringify(metadata));
5788
- this.logger.info("Backup uploaded to R2", {
5789
- backupId,
5790
- r2Key,
5791
- sizeBytes: createResult.sizeBytes
5792
- });
5793
- await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession).catch(() => {});
5790
+ outcome = "success";
5791
+ await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession, { origin: "internal" }).catch(() => {});
5794
5792
  return {
5795
5793
  id: backupId,
5796
5794
  dir
5797
5795
  };
5798
5796
  } catch (error) {
5799
- await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession).catch(() => {});
5800
- await bucket.delete(r2Key).catch(() => {});
5801
- await bucket.delete(metaKey).catch(() => {});
5797
+ caughtError = error instanceof Error ? error : new Error(String(error));
5798
+ if (backupId && backupSession) {
5799
+ const archivePath = `/var/backups/${backupId}.sqsh`;
5800
+ const r2Key = `backups/${backupId}/data.sqsh`;
5801
+ const metaKey = `backups/${backupId}/meta.json`;
5802
+ await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession, { origin: "internal" }).catch(() => {});
5803
+ await bucket.delete(r2Key).catch(() => {});
5804
+ await bucket.delete(metaKey).catch(() => {});
5805
+ }
5802
5806
  throw error;
5807
+ } finally {
5808
+ if (backupSession) await this.client.utils.deleteSession(backupSession).catch(() => {});
5809
+ logCanonicalEvent(this.logger, {
5810
+ event: "backup.create",
5811
+ outcome,
5812
+ durationMs: Date.now() - backupStartTime,
5813
+ backupId,
5814
+ dir,
5815
+ name,
5816
+ sizeBytes,
5817
+ error: caughtError
5818
+ });
5803
5819
  }
5804
5820
  }
5805
5821
  /**
@@ -5834,77 +5850,77 @@ var Sandbox = class Sandbox extends Container {
5834
5850
  return this.enqueueBackupOp(() => this.doRestoreBackup(backup));
5835
5851
  }
5836
5852
  async doRestoreBackup(backup) {
5853
+ const restoreStartTime = Date.now();
5837
5854
  const bucket = this.requireBackupBucket();
5838
5855
  this.requirePresignedUrlSupport();
5839
5856
  const { id: backupId, dir } = backup;
5840
- if (!backupId || typeof backupId !== "string") throw new InvalidBackupConfigError({
5841
- message: "Invalid backup: missing or invalid id",
5842
- code: ErrorCode.INVALID_BACKUP_CONFIG,
5843
- httpStatus: 400,
5844
- context: { reason: "missing or invalid id" },
5845
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5846
- });
5847
- if (!Sandbox.UUID_REGEX.test(backupId)) throw new InvalidBackupConfigError({
5848
- message: "Invalid backup: id must be a valid UUID (e.g. from createBackup)",
5849
- code: ErrorCode.INVALID_BACKUP_CONFIG,
5850
- httpStatus: 400,
5851
- context: { reason: "id must be a valid UUID" },
5852
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5853
- });
5854
- Sandbox.validateBackupDir(dir, "Invalid backup: dir");
5855
- this.logger.info("Restoring backup", {
5856
- backupId,
5857
- dir
5858
- });
5859
- const metaKey = `backups/${backupId}/meta.json`;
5860
- const metaObject = await bucket.get(metaKey);
5861
- if (!metaObject) throw new BackupNotFoundError({
5862
- message: `Backup not found: ${backupId}. Verify the backup ID is correct and the backup has not been deleted.`,
5863
- code: ErrorCode.BACKUP_NOT_FOUND,
5864
- httpStatus: 404,
5865
- context: { backupId },
5866
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5867
- });
5868
- const metadata = await metaObject.json();
5869
- const TTL_BUFFER_MS = 60 * 1e3;
5870
- const createdAt = new Date(metadata.createdAt).getTime();
5871
- if (Number.isNaN(createdAt)) throw new BackupRestoreError({
5872
- message: `Backup metadata has invalid createdAt timestamp: ${metadata.createdAt}`,
5873
- code: ErrorCode.BACKUP_RESTORE_FAILED,
5874
- httpStatus: 500,
5875
- context: {
5876
- dir,
5877
- backupId
5878
- },
5879
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5880
- });
5881
- const expiresAt = createdAt + metadata.ttl * 1e3;
5882
- if (Date.now() + TTL_BUFFER_MS > expiresAt) throw new BackupExpiredError({
5883
- message: `Backup ${backupId} has expired (created: ${metadata.createdAt}, TTL: ${metadata.ttl}s). Create a new backup.`,
5884
- code: ErrorCode.BACKUP_EXPIRED,
5885
- httpStatus: 400,
5886
- context: {
5887
- backupId,
5888
- expiredAt: new Date(expiresAt).toISOString()
5889
- },
5890
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5891
- });
5892
- const r2Key = `backups/${backupId}/data.sqsh`;
5893
- const archiveHead = await bucket.head(r2Key);
5894
- if (!archiveHead) throw new BackupNotFoundError({
5895
- message: `Backup archive not found in R2: ${backupId}. The archive may have been deleted by R2 lifecycle rules.`,
5896
- code: ErrorCode.BACKUP_NOT_FOUND,
5897
- httpStatus: 404,
5898
- context: { backupId },
5899
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5900
- });
5901
- const backupSession = await this.ensureBackupSession();
5902
- const archivePath = `/var/backups/${backupId}.sqsh`;
5857
+ let outcome = "error";
5858
+ let caughtError;
5859
+ let backupSession;
5903
5860
  try {
5861
+ if (!backupId || typeof backupId !== "string") throw new InvalidBackupConfigError({
5862
+ message: "Invalid backup: missing or invalid id",
5863
+ code: ErrorCode.INVALID_BACKUP_CONFIG,
5864
+ httpStatus: 400,
5865
+ context: { reason: "missing or invalid id" },
5866
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5867
+ });
5868
+ if (!Sandbox.UUID_REGEX.test(backupId)) throw new InvalidBackupConfigError({
5869
+ message: "Invalid backup: id must be a valid UUID (e.g. from createBackup)",
5870
+ code: ErrorCode.INVALID_BACKUP_CONFIG,
5871
+ httpStatus: 400,
5872
+ context: { reason: "id must be a valid UUID" },
5873
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5874
+ });
5875
+ Sandbox.validateBackupDir(dir, "Invalid backup: dir");
5876
+ const metaKey = `backups/${backupId}/meta.json`;
5877
+ const metaObject = await bucket.get(metaKey);
5878
+ if (!metaObject) throw new BackupNotFoundError({
5879
+ message: `Backup not found: ${backupId}. Verify the backup ID is correct and the backup has not been deleted.`,
5880
+ code: ErrorCode.BACKUP_NOT_FOUND,
5881
+ httpStatus: 404,
5882
+ context: { backupId },
5883
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5884
+ });
5885
+ const metadata = await metaObject.json();
5886
+ const TTL_BUFFER_MS = 60 * 1e3;
5887
+ const createdAt = new Date(metadata.createdAt).getTime();
5888
+ if (Number.isNaN(createdAt)) throw new BackupRestoreError({
5889
+ message: `Backup metadata has invalid createdAt timestamp: ${metadata.createdAt}`,
5890
+ code: ErrorCode.BACKUP_RESTORE_FAILED,
5891
+ httpStatus: 500,
5892
+ context: {
5893
+ dir,
5894
+ backupId
5895
+ },
5896
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5897
+ });
5898
+ const expiresAt = createdAt + metadata.ttl * 1e3;
5899
+ if (Date.now() + TTL_BUFFER_MS > expiresAt) throw new BackupExpiredError({
5900
+ message: `Backup ${backupId} has expired (created: ${metadata.createdAt}, TTL: ${metadata.ttl}s). Create a new backup.`,
5901
+ code: ErrorCode.BACKUP_EXPIRED,
5902
+ httpStatus: 400,
5903
+ context: {
5904
+ backupId,
5905
+ expiredAt: new Date(expiresAt).toISOString()
5906
+ },
5907
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5908
+ });
5909
+ const r2Key = `backups/${backupId}/data.sqsh`;
5910
+ const archiveHead = await bucket.head(r2Key);
5911
+ if (!archiveHead) throw new BackupNotFoundError({
5912
+ message: `Backup archive not found in R2: ${backupId}. The archive may have been deleted by R2 lifecycle rules.`,
5913
+ code: ErrorCode.BACKUP_NOT_FOUND,
5914
+ httpStatus: 404,
5915
+ context: { backupId },
5916
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5917
+ });
5918
+ backupSession = await this.ensureBackupSession();
5919
+ const archivePath = `/var/backups/${backupId}.sqsh`;
5904
5920
  const mountGlob = `/var/backups/mounts/${backupId}`;
5905
- await this.execWithSession(`/usr/bin/fusermount3 -uz ${shellEscape(dir)} 2>/dev/null || true`, backupSession).catch(() => {});
5906
- await this.execWithSession(`for d in ${shellEscape(mountGlob)}_*/lower ${shellEscape(mountGlob)}/lower; do [ -d "$d" ] && /usr/bin/fusermount3 -uz "$d" 2>/dev/null; done; true`, backupSession).catch(() => {});
5907
- const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(archivePath)} 2>/dev/null || echo 0`, backupSession).catch(() => ({ stdout: "0" }));
5921
+ await this.execWithSession(`/usr/bin/fusermount3 -uz ${shellEscape(dir)} 2>/dev/null || true`, backupSession, { origin: "internal" }).catch(() => {});
5922
+ await this.execWithSession(`for d in ${shellEscape(mountGlob)}_*/lower ${shellEscape(mountGlob)}/lower; do [ -d "$d" ] && /usr/bin/fusermount3 -uz "$d" 2>/dev/null; done; true`, backupSession, { origin: "internal" }).catch(() => {});
5923
+ const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(archivePath)} 2>/dev/null || echo 0`, backupSession, { origin: "internal" }).catch(() => ({ stdout: "0" }));
5908
5924
  if (Number.parseInt((sizeCheck.stdout ?? "0").trim(), 10) !== archiveHead.size) await this.downloadBackupPresigned(archivePath, r2Key, archiveHead.size, backupId, dir, backupSession);
5909
5925
  if (!(await this.client.backup.restoreArchive(dir, archivePath, backupSession)).success) throw new BackupRestoreError({
5910
5926
  message: "Container failed to restore backup archive",
@@ -5916,18 +5932,29 @@ var Sandbox = class Sandbox extends Container {
5916
5932
  },
5917
5933
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5918
5934
  });
5919
- this.logger.info("Backup restored", {
5920
- backupId,
5921
- dir
5922
- });
5935
+ outcome = "success";
5923
5936
  return {
5924
5937
  success: true,
5925
5938
  dir,
5926
5939
  id: backupId
5927
5940
  };
5928
5941
  } catch (error) {
5929
- await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession).catch(() => {});
5942
+ caughtError = error instanceof Error ? error : new Error(String(error));
5943
+ if (backupId && backupSession) {
5944
+ const archivePath = `/var/backups/${backupId}.sqsh`;
5945
+ await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession, { origin: "internal" }).catch(() => {});
5946
+ }
5930
5947
  throw error;
5948
+ } finally {
5949
+ if (backupSession) await this.client.utils.deleteSession(backupSession).catch(() => {});
5950
+ logCanonicalEvent(this.logger, {
5951
+ event: "backup.restore",
5952
+ outcome,
5953
+ durationMs: Date.now() - restoreStartTime,
5954
+ backupId,
5955
+ dir,
5956
+ error: caughtError
5957
+ });
5931
5958
  }
5932
5959
  }
5933
5960
  };
@@ -6055,5 +6082,5 @@ async function collectFile(stream) {
6055
6082
  }
6056
6083
 
6057
6084
  //#endregion
6058
- export { BackupClient, BackupCreateError, BackupExpiredError, BackupNotFoundError, BackupRestoreError, BucketMountError, CodeInterpreter, CommandClient, DesktopClient, DesktopInvalidCoordinatesError, DesktopInvalidOptionsError, DesktopNotStartedError, DesktopProcessCrashedError, DesktopStartFailedError, DesktopUnavailableError, FileClient, GitClient, InvalidBackupConfigError, InvalidMountConfigError, MissingCredentialsError, PortClient, ProcessClient, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, Sandbox, SandboxClient, UtilityClient, asyncIterableToSSEStream, collectFile, getSandbox, isExecResult, isProcess, isProcessStatus, parseSSEStream, proxyTerminal, proxyToSandbox, responseToAsyncIterable, streamFile };
6085
+ export { BackupClient, BackupCreateError, BackupExpiredError, BackupNotFoundError, BackupRestoreError, BucketMountError, CodeInterpreter, CommandClient, ContainerProxy, DesktopClient, DesktopInvalidCoordinatesError, DesktopInvalidOptionsError, DesktopNotStartedError, DesktopProcessCrashedError, DesktopStartFailedError, DesktopUnavailableError, FileClient, GitClient, InvalidBackupConfigError, InvalidMountConfigError, MissingCredentialsError, PortClient, ProcessClient, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, Sandbox, SandboxClient, UtilityClient, asyncIterableToSSEStream, collectFile, getSandbox, isExecResult, isProcess, isProcessStatus, parseSSEStream, proxyTerminal, proxyToSandbox, responseToAsyncIterable, streamFile };
6059
6086
  //# sourceMappingURL=index.js.map