@alchemy.run/e2b 0.0.0 → 2.0.0-beta.86

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.
Files changed (284) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +56 -4
  3. package/lib/ApiKey.d.ts +53 -0
  4. package/lib/ApiKey.d.ts.map +1 -0
  5. package/lib/ApiKey.js +107 -0
  6. package/lib/ApiKey.js.map +1 -0
  7. package/lib/AuthProvider.d.ts +60 -0
  8. package/lib/AuthProvider.d.ts.map +1 -0
  9. package/lib/AuthProvider.js +293 -0
  10. package/lib/AuthProvider.js.map +1 -0
  11. package/lib/Bootstrap.d.ts +7 -0
  12. package/lib/Bootstrap.d.ts.map +1 -0
  13. package/lib/Bootstrap.js +25 -0
  14. package/lib/Bootstrap.js.map +1 -0
  15. package/lib/ConnectSandbox.d.ts +67 -0
  16. package/lib/ConnectSandbox.d.ts.map +1 -0
  17. package/lib/ConnectSandbox.js +3 -0
  18. package/lib/ConnectSandbox.js.map +1 -0
  19. package/lib/ConnectSandboxHttp.d.ts +22 -0
  20. package/lib/ConnectSandboxHttp.d.ts.map +1 -0
  21. package/lib/ConnectSandboxHttp.js +30 -0
  22. package/lib/ConnectSandboxHttp.js.map +1 -0
  23. package/lib/Contributions.d.ts +7 -0
  24. package/lib/Contributions.d.ts.map +1 -0
  25. package/lib/Contributions.js +6 -0
  26. package/lib/Contributions.js.map +1 -0
  27. package/lib/CreateSandbox.d.ts +139 -0
  28. package/lib/CreateSandbox.d.ts.map +1 -0
  29. package/lib/CreateSandbox.js +3 -0
  30. package/lib/CreateSandbox.js.map +1 -0
  31. package/lib/CreateSandboxHttp.d.ts +22 -0
  32. package/lib/CreateSandboxHttp.d.ts.map +1 -0
  33. package/lib/CreateSandboxHttp.js +99 -0
  34. package/lib/CreateSandboxHttp.js.map +1 -0
  35. package/lib/CreateSecret.d.ts +57 -0
  36. package/lib/CreateSecret.d.ts.map +1 -0
  37. package/lib/CreateSecret.js +3 -0
  38. package/lib/CreateSecret.js.map +1 -0
  39. package/lib/CreateSecretHttp.d.ts +20 -0
  40. package/lib/CreateSecretHttp.d.ts.map +1 -0
  41. package/lib/CreateSecretHttp.js +64 -0
  42. package/lib/CreateSecretHttp.js.map +1 -0
  43. package/lib/CreateSnapshot.d.ts +53 -0
  44. package/lib/CreateSnapshot.d.ts.map +1 -0
  45. package/lib/CreateSnapshot.js +3 -0
  46. package/lib/CreateSnapshot.js.map +1 -0
  47. package/lib/CreateSnapshotHttp.d.ts +20 -0
  48. package/lib/CreateSnapshotHttp.d.ts.map +1 -0
  49. package/lib/CreateSnapshotHttp.js +41 -0
  50. package/lib/CreateSnapshotHttp.js.map +1 -0
  51. package/lib/CreateTemplate.d.ts +96 -0
  52. package/lib/CreateTemplate.d.ts.map +1 -0
  53. package/lib/CreateTemplate.js +3 -0
  54. package/lib/CreateTemplate.js.map +1 -0
  55. package/lib/CreateTemplateHttp.d.ts +20 -0
  56. package/lib/CreateTemplateHttp.d.ts.map +1 -0
  57. package/lib/CreateTemplateHttp.js +99 -0
  58. package/lib/CreateTemplateHttp.js.map +1 -0
  59. package/lib/CreateVolume.d.ts +58 -0
  60. package/lib/CreateVolume.d.ts.map +1 -0
  61. package/lib/CreateVolume.js +3 -0
  62. package/lib/CreateVolume.js.map +1 -0
  63. package/lib/CreateVolumeHttp.d.ts +20 -0
  64. package/lib/CreateVolumeHttp.d.ts.map +1 -0
  65. package/lib/CreateVolumeHttp.js +51 -0
  66. package/lib/CreateVolumeHttp.js.map +1 -0
  67. package/lib/Credentials.d.ts +18 -0
  68. package/lib/Credentials.d.ts.map +1 -0
  69. package/lib/Credentials.js +55 -0
  70. package/lib/Credentials.js.map +1 -0
  71. package/lib/E2BBindings.d.ts +35 -0
  72. package/lib/E2BBindings.d.ts.map +1 -0
  73. package/lib/E2BBindings.js +127 -0
  74. package/lib/E2BBindings.js.map +1 -0
  75. package/lib/Errors.d.ts +61 -0
  76. package/lib/Errors.d.ts.map +1 -0
  77. package/lib/Errors.js +23 -0
  78. package/lib/Errors.js.map +1 -0
  79. package/lib/InferEnv.d.ts +26 -0
  80. package/lib/InferEnv.d.ts.map +1 -0
  81. package/lib/InferEnv.js +1 -0
  82. package/lib/InferEnv.js.map +1 -0
  83. package/lib/MountVolume.d.ts +86 -0
  84. package/lib/MountVolume.d.ts.map +1 -0
  85. package/lib/MountVolume.js +3 -0
  86. package/lib/MountVolume.js.map +1 -0
  87. package/lib/MountVolumeLive.d.ts +23 -0
  88. package/lib/MountVolumeLive.d.ts.map +1 -0
  89. package/lib/MountVolumeLive.js +41 -0
  90. package/lib/MountVolumeLive.js.map +1 -0
  91. package/lib/Providers.d.ts +14 -0
  92. package/lib/Providers.d.ts.map +1 -0
  93. package/lib/Providers.js +23 -0
  94. package/lib/Providers.js.map +1 -0
  95. package/lib/ReadTeam.d.ts +41 -0
  96. package/lib/ReadTeam.d.ts.map +1 -0
  97. package/lib/ReadTeam.js +3 -0
  98. package/lib/ReadTeam.js.map +1 -0
  99. package/lib/ReadTeamHttp.d.ts +20 -0
  100. package/lib/ReadTeamHttp.d.ts.map +1 -0
  101. package/lib/ReadTeamHttp.js +40 -0
  102. package/lib/ReadTeamHttp.js.map +1 -0
  103. package/lib/ReadVolume.d.ts +25 -0
  104. package/lib/ReadVolume.d.ts.map +1 -0
  105. package/lib/ReadVolume.js +3 -0
  106. package/lib/ReadVolume.js.map +1 -0
  107. package/lib/ReadVolumeHttp.d.ts +20 -0
  108. package/lib/ReadVolumeHttp.d.ts.map +1 -0
  109. package/lib/ReadVolumeHttp.js +27 -0
  110. package/lib/ReadVolumeHttp.js.map +1 -0
  111. package/lib/ReadWebhook.d.ts +32 -0
  112. package/lib/ReadWebhook.d.ts.map +1 -0
  113. package/lib/ReadWebhook.js +3 -0
  114. package/lib/ReadWebhook.js.map +1 -0
  115. package/lib/ReadWebhookHttp.d.ts +20 -0
  116. package/lib/ReadWebhookHttp.d.ts.map +1 -0
  117. package/lib/ReadWebhookHttp.js +31 -0
  118. package/lib/ReadWebhookHttp.js.map +1 -0
  119. package/lib/ReadWriteVolume.d.ts +26 -0
  120. package/lib/ReadWriteVolume.d.ts.map +1 -0
  121. package/lib/ReadWriteVolume.js +3 -0
  122. package/lib/ReadWriteVolume.js.map +1 -0
  123. package/lib/ReadWriteVolumeHttp.d.ts +20 -0
  124. package/lib/ReadWriteVolumeHttp.d.ts.map +1 -0
  125. package/lib/ReadWriteVolumeHttp.js +27 -0
  126. package/lib/ReadWriteVolumeHttp.js.map +1 -0
  127. package/lib/Sandbox.d.ts +295 -0
  128. package/lib/Sandbox.d.ts.map +1 -0
  129. package/lib/Sandbox.js +351 -0
  130. package/lib/Sandbox.js.map +1 -0
  131. package/lib/SandboxClient.d.ts +226 -0
  132. package/lib/SandboxClient.d.ts.map +1 -0
  133. package/lib/SandboxClient.js +1 -0
  134. package/lib/SandboxClient.js.map +1 -0
  135. package/lib/SandboxEvents.d.ts +87 -0
  136. package/lib/SandboxEvents.d.ts.map +1 -0
  137. package/lib/SandboxEvents.js +39 -0
  138. package/lib/SandboxEvents.js.map +1 -0
  139. package/lib/SandboxEventsLive.d.ts +13 -0
  140. package/lib/SandboxEventsLive.d.ts.map +1 -0
  141. package/lib/SandboxEventsLive.js +80 -0
  142. package/lib/SandboxEventsLive.js.map +1 -0
  143. package/lib/Secret.d.ts +105 -0
  144. package/lib/Secret.d.ts.map +1 -0
  145. package/lib/Secret.js +161 -0
  146. package/lib/Secret.js.map +1 -0
  147. package/lib/Snapshot.d.ts +118 -0
  148. package/lib/Snapshot.d.ts.map +1 -0
  149. package/lib/Snapshot.js +158 -0
  150. package/lib/Snapshot.js.map +1 -0
  151. package/lib/Template.d.ts +313 -0
  152. package/lib/Template.d.ts.map +1 -0
  153. package/lib/Template.js +421 -0
  154. package/lib/Template.js.map +1 -0
  155. package/lib/Volume.d.ts +88 -0
  156. package/lib/Volume.d.ts.map +1 -0
  157. package/lib/Volume.js +136 -0
  158. package/lib/Volume.js.map +1 -0
  159. package/lib/VolumeFiles.d.ts +39 -0
  160. package/lib/VolumeFiles.d.ts.map +1 -0
  161. package/lib/VolumeFiles.js +1 -0
  162. package/lib/VolumeFiles.js.map +1 -0
  163. package/lib/Webhook.d.ts +83 -0
  164. package/lib/Webhook.d.ts.map +1 -0
  165. package/lib/Webhook.js +138 -0
  166. package/lib/Webhook.js.map +1 -0
  167. package/lib/WriteSecret.d.ts +34 -0
  168. package/lib/WriteSecret.d.ts.map +1 -0
  169. package/lib/WriteSecret.js +3 -0
  170. package/lib/WriteSecret.js.map +1 -0
  171. package/lib/WriteSecretHttp.d.ts +20 -0
  172. package/lib/WriteSecretHttp.d.ts.map +1 -0
  173. package/lib/WriteSecretHttp.js +37 -0
  174. package/lib/WriteSecretHttp.js.map +1 -0
  175. package/lib/WriteVolume.d.ts +25 -0
  176. package/lib/WriteVolume.d.ts.map +1 -0
  177. package/lib/WriteVolume.js +3 -0
  178. package/lib/WriteVolume.js.map +1 -0
  179. package/lib/WriteVolumeHttp.d.ts +20 -0
  180. package/lib/WriteVolumeHttp.d.ts.map +1 -0
  181. package/lib/WriteVolumeHttp.js +27 -0
  182. package/lib/WriteVolumeHttp.js.map +1 -0
  183. package/lib/index.d.ts +43 -0
  184. package/lib/index.d.ts.map +1 -0
  185. package/lib/index.js +43 -0
  186. package/lib/index.js.map +1 -0
  187. package/lib/internal/Declared.d.ts +29 -0
  188. package/lib/internal/Declared.d.ts.map +1 -0
  189. package/lib/internal/Declared.js +31 -0
  190. package/lib/internal/Declared.js.map +1 -0
  191. package/lib/internal/Login.d.ts +45 -0
  192. package/lib/internal/Login.d.ts.map +1 -0
  193. package/lib/internal/Login.js +157 -0
  194. package/lib/internal/Login.js.map +1 -0
  195. package/lib/internal/Ownership.d.ts +29 -0
  196. package/lib/internal/Ownership.d.ts.map +1 -0
  197. package/lib/internal/Ownership.js +49 -0
  198. package/lib/internal/Ownership.js.map +1 -0
  199. package/lib/internal/Program.d.ts +31 -0
  200. package/lib/internal/Program.d.ts.map +1 -0
  201. package/lib/internal/Program.js +60 -0
  202. package/lib/internal/Program.js.map +1 -0
  203. package/lib/internal/SandboxHandle.d.ts +25 -0
  204. package/lib/internal/SandboxHandle.d.ts.map +1 -0
  205. package/lib/internal/SandboxHandle.js +316 -0
  206. package/lib/internal/SandboxHandle.js.map +1 -0
  207. package/lib/internal/SnapshotBuild.d.ts +19 -0
  208. package/lib/internal/SnapshotBuild.d.ts.map +1 -0
  209. package/lib/internal/SnapshotBuild.js +49 -0
  210. package/lib/internal/SnapshotBuild.js.map +1 -0
  211. package/lib/internal/Tar.d.ts +10 -0
  212. package/lib/internal/Tar.d.ts.map +1 -0
  213. package/lib/internal/Tar.js +61 -0
  214. package/lib/internal/Tar.js.map +1 -0
  215. package/lib/internal/TemplateBuild.d.ts +142 -0
  216. package/lib/internal/TemplateBuild.d.ts.map +1 -0
  217. package/lib/internal/TemplateBuild.js +220 -0
  218. package/lib/internal/TemplateBuild.js.map +1 -0
  219. package/lib/internal/TemplatePlan.d.ts +57 -0
  220. package/lib/internal/TemplatePlan.d.ts.map +1 -0
  221. package/lib/internal/TemplatePlan.js +185 -0
  222. package/lib/internal/TemplatePlan.js.map +1 -0
  223. package/lib/internal/VolumeHttp.d.ts +5 -0
  224. package/lib/internal/VolumeHttp.d.ts.map +1 -0
  225. package/lib/internal/VolumeHttp.js +46 -0
  226. package/lib/internal/VolumeHttp.js.map +1 -0
  227. package/package.json +62 -3
  228. package/src/ApiKey.ts +148 -0
  229. package/src/AuthProvider.ts +437 -0
  230. package/src/Bootstrap.ts +51 -0
  231. package/src/ConnectSandbox.ts +71 -0
  232. package/src/ConnectSandboxHttp.ts +38 -0
  233. package/src/Contributions.ts +9 -0
  234. package/src/CreateSandbox.ts +157 -0
  235. package/src/CreateSandboxHttp.ts +131 -0
  236. package/src/CreateSecret.ts +65 -0
  237. package/src/CreateSecretHttp.ts +94 -0
  238. package/src/CreateSnapshot.ts +64 -0
  239. package/src/CreateSnapshotHttp.ts +55 -0
  240. package/src/CreateTemplate.ts +135 -0
  241. package/src/CreateTemplateHttp.ts +152 -0
  242. package/src/CreateVolume.ts +65 -0
  243. package/src/CreateVolumeHttp.ts +80 -0
  244. package/src/Credentials.ts +81 -0
  245. package/src/E2BBindings.ts +160 -0
  246. package/src/Errors.ts +47 -0
  247. package/src/InferEnv.ts +27 -0
  248. package/src/MountVolume.ts +92 -0
  249. package/src/MountVolumeLive.ts +49 -0
  250. package/src/Providers.ts +48 -0
  251. package/src/ReadTeam.ts +55 -0
  252. package/src/ReadTeamHttp.ts +44 -0
  253. package/src/ReadVolume.ts +29 -0
  254. package/src/ReadVolumeHttp.ts +32 -0
  255. package/src/ReadWebhook.ts +41 -0
  256. package/src/ReadWebhookHttp.ts +42 -0
  257. package/src/ReadWriteVolume.ts +30 -0
  258. package/src/ReadWriteVolumeHttp.ts +32 -0
  259. package/src/Sandbox.ts +581 -0
  260. package/src/SandboxClient.ts +268 -0
  261. package/src/SandboxEvents.ts +101 -0
  262. package/src/SandboxEventsLive.ts +118 -0
  263. package/src/Secret.ts +229 -0
  264. package/src/Snapshot.ts +234 -0
  265. package/src/Template.ts +636 -0
  266. package/src/Volume.ts +173 -0
  267. package/src/VolumeFiles.ts +53 -0
  268. package/src/Webhook.ts +215 -0
  269. package/src/WriteSecret.ts +39 -0
  270. package/src/WriteSecretHttp.ts +43 -0
  271. package/src/WriteVolume.ts +29 -0
  272. package/src/WriteVolumeHttp.ts +32 -0
  273. package/src/globals.d.ts +2 -0
  274. package/src/index.ts +42 -0
  275. package/src/internal/Declared.ts +80 -0
  276. package/src/internal/Login.ts +210 -0
  277. package/src/internal/Ownership.ts +67 -0
  278. package/src/internal/Program.ts +75 -0
  279. package/src/internal/SandboxHandle.ts +534 -0
  280. package/src/internal/SnapshotBuild.ts +74 -0
  281. package/src/internal/Tar.ts +74 -0
  282. package/src/internal/TemplateBuild.ts +383 -0
  283. package/src/internal/TemplatePlan.ts +230 -0
  284. package/src/internal/VolumeHttp.ts +88 -0
@@ -0,0 +1,534 @@
1
+ /**
2
+ * Builds sandbox handles over E2B's control plane (lifecycle) and envd
3
+ * (processes, files, terminals). envd calls address the sandbox with its
4
+ * own access token; the host's team key is used only for control-plane
5
+ * calls.
6
+ */
7
+ import { sandboxConnection } from "@distilled.cloud/e2b";
8
+ import * as envd from "@distilled.cloud/e2b/envd";
9
+ import * as filesystem from "@distilled.cloud/e2b/filesystem";
10
+ import * as process from "@distilled.cloud/e2b/process";
11
+ import * as sandboxes from "@distilled.cloud/e2b/sandboxes";
12
+ import type { RuntimeContext } from "alchemy/RuntimeContext";
13
+ import * as Sandbox from "alchemy/Sandbox";
14
+ import type * as Cause from "effect/Cause";
15
+ import * as Deferred from "effect/Deferred";
16
+ import * as Duration from "effect/Duration";
17
+ import * as Effect from "effect/Effect";
18
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
19
+ import * as HttpClient from "effect/http/HttpClient";
20
+ import * as HttpClientRequest from "effect/http/HttpClientRequest";
21
+ import * as Layer from "effect/Layer";
22
+ import * as Queue from "effect/Queue";
23
+ import * as Redacted from "effect/Redacted";
24
+ import type * as Scope from "effect/Scope";
25
+ import * as Stream from "effect/Stream";
26
+ import type { E2BAuth } from "../E2BBindings.ts";
27
+ import { E2BUnexpectedResponse, ExecTimedOut } from "../Errors.ts";
28
+ import type {
29
+ Command,
30
+ ConnectedSandbox,
31
+ E2BError,
32
+ E2BSandbox,
33
+ ExecResult,
34
+ ForkResult,
35
+ Port,
36
+ Process,
37
+ ProcessOutput,
38
+ ProcessSelector,
39
+ SandboxOperations,
40
+ StartOptions,
41
+ } from "../SandboxClient.ts";
42
+
43
+ const encoder = new TextEncoder();
44
+
45
+ const toBase64 = (data: string | Uint8Array) => {
46
+ const bytes = typeof data === "string" ? encoder.encode(data) : data;
47
+ let binary = "";
48
+ for (const byte of bytes) binary += String.fromCharCode(byte);
49
+ return btoa(binary);
50
+ };
51
+
52
+ const seconds = (duration: Duration.Input) =>
53
+ Math.max(1, Math.round(Duration.toMillis(Duration.fromInputUnsafe(duration)) / 1000));
54
+
55
+ const redact = (token: string | Redacted.Redacted<string> | null | undefined) =>
56
+ token === undefined || token === null
57
+ ? undefined
58
+ : Redacted.isRedacted(token)
59
+ ? token
60
+ : Redacted.make(token);
61
+
62
+ /**
63
+ * `allowOut` lists the only destinations a sandbox may reach: E2B allows
64
+ * everything else unless `denyOut` blocks it, so an `allowOut` without a
65
+ * `denyOut` denies all other traffic.
66
+ */
67
+ export const denyUnlisted = <
68
+ N extends { allowOut?: ReadonlyArray<string>; denyOut?: ReadonlyArray<string> },
69
+ >(
70
+ network: N,
71
+ ): N =>
72
+ network.allowOut !== undefined && network.denyOut === undefined
73
+ ? { ...network, denyOut: ["0.0.0.0/0"] }
74
+ : network;
75
+
76
+ /** What envd calls need to reach one sandbox. */
77
+ interface Reach {
78
+ readonly sandboxId: string;
79
+ readonly accessToken: Redacted.Redacted<string> | undefined;
80
+ readonly trafficToken: Redacted.Redacted<string> | undefined;
81
+ readonly domain: string;
82
+ }
83
+
84
+ const reachOf = (
85
+ info: {
86
+ readonly sandboxID: string;
87
+ readonly envdAccessToken?: string | Redacted.Redacted<string>;
88
+ readonly trafficAccessToken?: string | Redacted.Redacted<string> | null;
89
+ readonly domain?: string | null;
90
+ },
91
+ domain: string,
92
+ trafficToken?: Redacted.Redacted<string>,
93
+ ): Reach => ({
94
+ sandboxId: info.sandboxID,
95
+ accessToken: redact(info.envdAccessToken),
96
+ trafficToken: redact(info.trafficAccessToken) ?? trafficToken,
97
+ domain: info.domain ?? domain,
98
+ });
99
+
100
+ const processConfig = (
101
+ command: Command,
102
+ cwd: string | undefined,
103
+ env: Readonly<Record<string, string>> | undefined,
104
+ ) => ({
105
+ ...(typeof command === "string"
106
+ ? { cmd: "/bin/bash", args: ["-l", "-c", command] }
107
+ : { cmd: command[0], args: command.slice(1) }),
108
+ ...(env ? { envs: env } : {}),
109
+ ...(cwd ? { cwd } : {}),
110
+ });
111
+
112
+ const signalOf = (signal: "SIGTERM" | "SIGKILL" | undefined) =>
113
+ signal === "SIGTERM" ? ("SIGNAL_SIGTERM" as const) : ("SIGNAL_SIGKILL" as const);
114
+
115
+ /** The entry envd reports for a filesystem call; envd always sends one on success. */
116
+ const entryOf = (entry: filesystem.EntryInfo | undefined, call: string) =>
117
+ entry === undefined
118
+ ? Effect.fail(new E2BUnexpectedResponse({ message: `envd sent no entry for ${call}` }))
119
+ : Effect.succeed(entry);
120
+
121
+ /**
122
+ * The operations every handle shares. `reach` resolves how to address the
123
+ * sandbox; `domainOf` is the domain its ports are served under; `resumed`
124
+ * records what a resume returned; `forked` builds a handle for a fork.
125
+ */
126
+ const makeOperations = (
127
+ auth: E2BAuth,
128
+ sandboxIdOf: Effect.Effect<string, never, RuntimeContext>,
129
+ reach: Effect.Effect<Reach, E2BError, RuntimeContext>,
130
+ domainOf: Effect.Effect<string, never, RuntimeContext>,
131
+ resumed: (info: sandboxes.Sandbox) => void,
132
+ forked: (info: sandboxes.Sandbox, domain: string) => E2BSandbox,
133
+ ): SandboxOperations => {
134
+ const connection = (r: Reach) =>
135
+ Layer.mergeAll(
136
+ sandboxConnection({
137
+ sandboxId: r.sandboxId,
138
+ domain: r.domain,
139
+ ...(r.accessToken === undefined ? {} : { accessToken: r.accessToken }),
140
+ }),
141
+ FetchHttpClient.layer,
142
+ );
143
+ type Envd = Layer.Success<ReturnType<typeof connection>>;
144
+ /** An envd call against this sandbox. */
145
+ const inSandbox = <A, E>(effect: Effect.Effect<A, E, Envd>) =>
146
+ Effect.flatMap(reach, (r) => effect.pipe(Effect.provide(connection(r))));
147
+ const streamInSandbox = <A, E>(stream: Stream.Stream<A, E, Envd>) =>
148
+ Stream.unwrap(Effect.map(reach, (r) => stream.pipe(Stream.provide(connection(r)))));
149
+ /** A control-plane call on this sandbox. */
150
+ const onSandbox = <A, E>(
151
+ call: (sandboxID: string) => Effect.Effect<A, E, sandboxes.E2BOpContext>,
152
+ ) => Effect.flatMap(sandboxIdOf, (id) => auth.authorize(call(id)));
153
+
154
+ /**
155
+ * Runs a process's event stream in the current scope and hands back its
156
+ * controls once envd reports its pid. Closing the scope detaches.
157
+ */
158
+ const attachTo = (
159
+ events: Stream.Stream<process.ProcessEvent, E2BError, RuntimeContext>,
160
+ knownPid?: number,
161
+ ): Effect.Effect<Process, E2BError, RuntimeContext | Scope.Scope> =>
162
+ Effect.gen(function* () {
163
+ const queue = yield* Queue.unbounded<ProcessOutput, E2BError | Cause.Done>();
164
+ const started = yield* Deferred.make<number, E2BError>();
165
+ const exited = yield* Deferred.make<ExecResult, E2BError>();
166
+ if (knownPid !== undefined) yield* Deferred.succeed(started, knownPid);
167
+ const stdout = new TextDecoder();
168
+ const stderr = new TextDecoder();
169
+ let pid = knownPid ?? 0;
170
+ let out = "";
171
+ let err = "";
172
+ const fail = (error: E2BError) =>
173
+ Effect.all([
174
+ Deferred.fail(started, error),
175
+ Deferred.fail(exited, error),
176
+ Queue.fail(queue, error),
177
+ ]);
178
+ yield* events.pipe(
179
+ Stream.runForEach((event) =>
180
+ Effect.gen(function* () {
181
+ switch (event._tag) {
182
+ case "Start":
183
+ pid = event.pid;
184
+ yield* Deferred.succeed(started, event.pid);
185
+ return;
186
+ case "Stdout":
187
+ case "Pty":
188
+ out += stdout.decode(event.data, { stream: true });
189
+ yield* Queue.offer(queue, { fd: "stdout", data: event.data });
190
+ return;
191
+ case "Stderr":
192
+ err += stderr.decode(event.data, { stream: true });
193
+ yield* Queue.offer(queue, { fd: "stderr", data: event.data });
194
+ return;
195
+ case "End":
196
+ yield* Deferred.succeed(exited, {
197
+ pid,
198
+ exitCode: event.exitCode,
199
+ stdout: out + stdout.decode(),
200
+ stderr: err + stderr.decode(),
201
+ error: event.error,
202
+ });
203
+ yield* Queue.end(queue);
204
+ return;
205
+ }
206
+ }),
207
+ ),
208
+ // A stream that ends without an End event: envd dropped it.
209
+ Effect.andThen(
210
+ Effect.suspend(() =>
211
+ Deferred.isDone(exited).pipe(
212
+ Effect.flatMap((done) =>
213
+ done
214
+ ? Effect.void
215
+ : fail(
216
+ new E2BUnexpectedResponse({
217
+ message: `envd closed process ${pid}'s stream before it exited`,
218
+ }),
219
+ ),
220
+ ),
221
+ ),
222
+ ),
223
+ ),
224
+ Effect.catch(fail),
225
+ Effect.forkScoped,
226
+ );
227
+ const processPid = yield* Deferred.await(started);
228
+ const selector: ProcessSelector = { pid: processPid };
229
+ return {
230
+ id: String(processPid),
231
+ pid: processPid,
232
+ output: Stream.fromQueue(queue),
233
+ write: (data) =>
234
+ inSandbox(
235
+ process.sendProcessInput({ process: selector, input: { stdin: toBase64(data) } }),
236
+ ).pipe(Effect.asVoid),
237
+ closeStdin: () =>
238
+ inSandbox(process.closeProcessStdin({ process: selector })).pipe(Effect.asVoid),
239
+ kill: (signal) =>
240
+ inSandbox(
241
+ process.sendProcessSignal({ process: selector, signal: signalOf(signal) }),
242
+ ).pipe(Effect.asVoid),
243
+ wait: Deferred.await(exited),
244
+ } satisfies Process;
245
+ });
246
+
247
+ const start = (command: Command, options?: StartOptions) =>
248
+ process
249
+ .startProcess({
250
+ process: processConfig(command, options?.cwd, options?.env),
251
+ ...(options?.tag ? { tag: options.tag } : {}),
252
+ ...(options?.stdin ? { stdin: true } : {}),
253
+ })
254
+ .pipe(streamInSandbox, (events) => attachTo(events));
255
+
256
+ return {
257
+ exec: (command, options) =>
258
+ Effect.scoped(
259
+ Effect.gen(function* () {
260
+ const proc = yield* start(command, {
261
+ ...(options?.cwd ? { cwd: options.cwd } : {}),
262
+ ...(options?.env ? { env: options.env } : {}),
263
+ ...(options?.stdin === undefined ? {} : { stdin: true }),
264
+ });
265
+ if (options?.stdin !== undefined) {
266
+ yield* proc.write(options.stdin);
267
+ yield* proc.closeStdin();
268
+ }
269
+ if (options?.timeout === undefined) return yield* proc.wait;
270
+ const timeoutMs = Duration.toMillis(Duration.fromInputUnsafe(options.timeout));
271
+ return yield* proc.wait.pipe(
272
+ Effect.timeoutOrElse({
273
+ duration: options.timeout,
274
+ orElse: () =>
275
+ proc
276
+ .kill()
277
+ .pipe(
278
+ Effect.ignore,
279
+ Effect.andThen(Effect.fail(new ExecTimedOut({ pid: proc.pid, timeoutMs }))),
280
+ ),
281
+ }),
282
+ );
283
+ }),
284
+ ),
285
+ start,
286
+ processes: {
287
+ list: () =>
288
+ inSandbox(process.listProcesses({})).pipe(Effect.map((res) => res.processes ?? [])),
289
+ attach: (selector) =>
290
+ attachTo(
291
+ streamInSandbox(process.connectProcess({ process: selector })),
292
+ "pid" in selector ? selector.pid : undefined,
293
+ ),
294
+ },
295
+ pty: {
296
+ open: (options) =>
297
+ Effect.gen(function* () {
298
+ const proc = yield* process
299
+ .startProcess({
300
+ process: {
301
+ cmd: "/bin/bash",
302
+ args: options.command ? ["-l", "-c", options.command] : ["-i", "-l"],
303
+ envs: { TERM: "xterm-256color", ...options.env },
304
+ ...(options.cwd ? { cwd: options.cwd } : {}),
305
+ },
306
+ pty: { size: { cols: options.cols, rows: options.rows } },
307
+ ...(options.tag ? { tag: options.tag } : {}),
308
+ })
309
+ .pipe(streamInSandbox, (events) => attachTo(events));
310
+ const selector: ProcessSelector = { pid: proc.pid };
311
+ return {
312
+ id: proc.id,
313
+ pid: proc.pid,
314
+ output: proc.output.pipe(Stream.map((chunk) => chunk.data)),
315
+ write: (data: string | Uint8Array) =>
316
+ inSandbox(
317
+ process.sendProcessInput({ process: selector, input: { pty: toBase64(data) } }),
318
+ ).pipe(Effect.asVoid),
319
+ resize: (size: { readonly cols: number; readonly rows: number }) =>
320
+ inSandbox(process.updateProcess({ process: selector, pty: { size } })).pipe(
321
+ Effect.asVoid,
322
+ ),
323
+ kill: () => proc.kill(),
324
+ wait: Effect.map(proc.wait, (result) => ({ exitCode: result.exitCode })),
325
+ };
326
+ }),
327
+ },
328
+ files: {
329
+ read: (path) => inSandbox(envd.readFile({ path })),
330
+ readText: (path) =>
331
+ inSandbox(envd.readFile({ path })).pipe(
332
+ Effect.map((bytes) => new TextDecoder().decode(bytes)),
333
+ ),
334
+ write: (path, data) =>
335
+ inSandbox(
336
+ envd.writeFiles({
337
+ path,
338
+ file: new Blob([typeof data === "string" ? data : new Uint8Array(data)]),
339
+ }),
340
+ ).pipe(Effect.asVoid),
341
+ list: (path, options) =>
342
+ inSandbox(filesystem.listDir({ path, depth: options?.depth ?? 1 })).pipe(
343
+ Effect.map((res) => res.entries ?? []),
344
+ ),
345
+ stat: (path) =>
346
+ inSandbox(filesystem.statPath({ path })).pipe(
347
+ Effect.flatMap((res) => entryOf(res.entry, `stat ${path}`)),
348
+ ),
349
+ exists: (path) =>
350
+ inSandbox(filesystem.statPath({ path })).pipe(
351
+ Effect.as(true),
352
+ Effect.catchTag("NotFound", () => Effect.succeed(false)),
353
+ ),
354
+ makeDir: (path) => inSandbox(filesystem.makeDir({ path })).pipe(Effect.asVoid),
355
+ move: (source, destination) =>
356
+ inSandbox(filesystem.movePath({ source, destination })).pipe(
357
+ Effect.flatMap((res) => entryOf(res.entry, `move to ${destination}`)),
358
+ ),
359
+ remove: (path) => inSandbox(filesystem.removePath({ path })).pipe(Effect.asVoid),
360
+ compose: (sources, destination) =>
361
+ inSandbox(envd.composeFiles({ source_paths: [...sources], destination })).pipe(
362
+ Effect.asVoid,
363
+ ),
364
+ watch: (path, options) =>
365
+ streamInSandbox(
366
+ filesystem.watchDir({
367
+ path,
368
+ recursive: options?.recursive ?? false,
369
+ includeEntry: options?.includeEntry ?? false,
370
+ }),
371
+ ),
372
+ },
373
+ getTcpPort: (port) =>
374
+ Effect.gen(function* () {
375
+ const sandboxId = yield* sandboxIdOf;
376
+ const domain = yield* domainOf;
377
+ const host = `${port}-${sandboxId}.${domain}`;
378
+ const url = `https://${host}`;
379
+ // Raw HTTP to the user's own server inside the sandbox, at the URL
380
+ // E2B serves its ports under. A proxied server request carries the
381
+ // caller's origin and `host`; only its path and query are kept, since
382
+ // E2B routes on the host.
383
+ const send = (request: HttpClientRequest.HttpClientRequest) =>
384
+ Effect.gen(function* () {
385
+ const r = yield* reach;
386
+ const client = yield* HttpClient.HttpClient;
387
+ const target = yield* Effect.sync(() => new URL(request.url, url));
388
+ return yield* client.execute(
389
+ request.pipe(
390
+ HttpClientRequest.setUrl(`${url}${target.pathname}${target.search}`),
391
+ HttpClientRequest.removeHeader("host"),
392
+ r.trafficToken === undefined
393
+ ? (req) => req
394
+ : HttpClientRequest.setHeader(
395
+ "e2b-traffic-access-token",
396
+ Redacted.value(r.trafficToken),
397
+ ),
398
+ ),
399
+ );
400
+ }).pipe(Effect.provide(FetchHttpClient.layer));
401
+ return { host, url, fetch: Sandbox.portFetch(send) } satisfies Port;
402
+ }),
403
+ describe: () => onSandbox((sandboxID) => sandboxes.getSandbox({ sandboxID })),
404
+ pause: () =>
405
+ onSandbox((sandboxID) => sandboxes.pauseSandbox({ sandboxID })).pipe(Effect.asVoid),
406
+ resume: (options) =>
407
+ onSandbox((sandboxID) =>
408
+ sandboxes.connectSandbox({
409
+ sandboxID,
410
+ ...(options?.timeout === undefined ? {} : { timeout: seconds(options.timeout) }),
411
+ }),
412
+ ).pipe(
413
+ Effect.tap((info) => Effect.sync(() => resumed(info))),
414
+ Effect.asVoid,
415
+ ),
416
+ kill: () =>
417
+ onSandbox((sandboxID) => sandboxes.deleteSandbox({ sandboxID })).pipe(
418
+ Effect.asVoid,
419
+ Effect.catchTag("NotFound", () => Effect.void),
420
+ ),
421
+ setTimeout: (timeout) =>
422
+ onSandbox((sandboxID) =>
423
+ sandboxes.setSandboxTimeout({ sandboxID, timeout: seconds(timeout) }),
424
+ ).pipe(Effect.asVoid),
425
+ refresh: (duration) =>
426
+ onSandbox((sandboxID) =>
427
+ sandboxes.refreshSandbox({
428
+ sandboxID,
429
+ ...(duration === undefined ? {} : { duration: seconds(duration) }),
430
+ }),
431
+ ).pipe(Effect.asVoid),
432
+ updateNetwork: (network) =>
433
+ onSandbox((sandboxID) =>
434
+ sandboxes.updateSandboxNetwork({ ...denyUnlisted(network), sandboxID }),
435
+ ).pipe(Effect.asVoid),
436
+ snapshot: (options) =>
437
+ onSandbox((sandboxID) => sandboxes.createSandboxSnapshot({ sandboxID, ...options })),
438
+ fork: (options) =>
439
+ onSandbox((sandboxID) =>
440
+ sandboxes.forkSandbox({
441
+ sandboxID,
442
+ ...(options?.count === undefined ? {} : { count: options.count }),
443
+ ...(options?.timeout === undefined ? {} : { timeout: seconds(options.timeout) }),
444
+ }),
445
+ ).pipe(
446
+ Effect.flatMap((results) => Effect.map(auth.domain, (domain) => ({ results, domain }))),
447
+ Effect.map(({ results, domain }) =>
448
+ results.map((result): ForkResult =>
449
+ result.sandbox !== undefined
450
+ ? { _tag: "Forked", sandbox: forked(result.sandbox, domain) }
451
+ : {
452
+ _tag: "ForkFailed",
453
+ code: result.error?.code,
454
+ message: result.error?.message ?? "fork failed",
455
+ },
456
+ ),
457
+ ),
458
+ ),
459
+ metrics: () => onSandbox((sandboxID) => sandboxes.listSandboxMetrics({ sandboxID })),
460
+ logs: (options) =>
461
+ onSandbox((sandboxID) => sandboxes.listSandboxLogs({ ...options, sandboxID })),
462
+ };
463
+ };
464
+
465
+ /** A handle to a sandbox created or connected at runtime, from what E2B returned. */
466
+ export const makeSandboxHandle = (
467
+ auth: E2BAuth,
468
+ info: sandboxes.Sandbox,
469
+ domain: string,
470
+ ): E2BSandbox => {
471
+ let current = reachOf(info, domain);
472
+ const operations = makeOperations(
473
+ auth,
474
+ Effect.succeed(info.sandboxID),
475
+ Effect.sync(() => current),
476
+ Effect.sync(() => current.domain),
477
+ (resumed) => {
478
+ current = reachOf(resumed, current.domain, current.trafficToken);
479
+ },
480
+ (fork, domain) => makeSandboxHandle(auth, fork, domain),
481
+ );
482
+ return {
483
+ ...operations,
484
+ sandboxId: info.sandboxID,
485
+ templateId: info.templateID,
486
+ domain: current.domain,
487
+ info,
488
+ };
489
+ };
490
+
491
+ /**
492
+ * A handle to a sandbox declared in the stack. Its envd token is read from
493
+ * E2B on first use (without resuming it) and kept; `trafficTokenOf` comes
494
+ * from the sandbox's attributes, since E2B returns the traffic token only
495
+ * on create and connect.
496
+ */
497
+ export const makeConnectedSandbox = (
498
+ auth: E2BAuth,
499
+ sandboxIdOf: Effect.Effect<string, never, RuntimeContext>,
500
+ trafficTokenOf: Effect.Effect<Redacted.Redacted<string> | undefined, never, RuntimeContext>,
501
+ ): ConnectedSandbox => {
502
+ let cached: Reach | undefined;
503
+ const reach = Effect.suspend(() =>
504
+ cached !== undefined
505
+ ? Effect.succeed(cached)
506
+ : Effect.gen(function* () {
507
+ const sandboxID = yield* sandboxIdOf;
508
+ const detail = yield* auth.authorize(sandboxes.getSandbox({ sandboxID }));
509
+ const domain = yield* auth.domain;
510
+ const trafficToken = yield* trafficTokenOf;
511
+ const resolved = reachOf(detail, domain, trafficToken);
512
+ cached = resolved;
513
+ return resolved;
514
+ }),
515
+ );
516
+ const domainOf = Effect.suspend(() =>
517
+ cached !== undefined ? Effect.succeed(cached.domain) : auth.domain,
518
+ );
519
+ const operations = makeOperations(
520
+ auth,
521
+ sandboxIdOf,
522
+ reach,
523
+ domainOf,
524
+ // The next call re-reads the sandbox's envd token.
525
+ () => {
526
+ cached = undefined;
527
+ },
528
+ (fork, domain) => makeSandboxHandle(auth, fork, domain),
529
+ );
530
+ return {
531
+ ...operations,
532
+ sandboxId: sandboxIdOf,
533
+ };
534
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Takes a snapshot: boots a template, runs setup commands in it, snapshots
3
+ * it, and always kills the setup sandbox. Shared by the `E2B.Snapshot`
4
+ * resource and the runtime `E2B.CreateSnapshot` client.
5
+ */
6
+ import { sandboxConnectionFor } from "@distilled.cloud/e2b";
7
+ import * as process from "@distilled.cloud/e2b/process";
8
+ import * as sandboxes from "@distilled.cloud/e2b/sandboxes";
9
+ import * as Effect from "effect/Effect";
10
+ import * as Stream from "effect/Stream";
11
+ import { SnapshotSetupFailed } from "../Errors.ts";
12
+
13
+ export interface SnapshotSetup {
14
+ /** The template to boot: an id or name. */
15
+ readonly template: string;
16
+ /** Shell commands run in order before the snapshot is taken. */
17
+ readonly run: ReadonlyArray<string>;
18
+ readonly envs?: Readonly<Record<string, string>>;
19
+ readonly cwd?: string;
20
+ /** Capture memory as well as the filesystem. @default true */
21
+ readonly memory?: boolean;
22
+ /** How long the setup sandbox may run, in seconds. @default 900 */
23
+ readonly timeout?: number;
24
+ }
25
+
26
+ /** Boots the template, runs the setup, snapshots it, and always kills the setup sandbox. */
27
+ export const takeSnapshot = Effect.fn(function* (name: string, props: SnapshotSetup) {
28
+ return yield* Effect.acquireUseRelease(
29
+ sandboxes.createSandbox({ templateID: props.template, timeout: props.timeout ?? 900 }),
30
+ (sandbox) =>
31
+ Effect.gen(function* () {
32
+ for (const command of props.run) {
33
+ const result = yield* process
34
+ .startProcess({
35
+ process: {
36
+ cmd: "/bin/bash",
37
+ args: ["-l", "-c", command],
38
+ ...(props.envs ? { envs: props.envs } : {}),
39
+ ...(props.cwd ? { cwd: props.cwd } : {}),
40
+ },
41
+ })
42
+ .pipe(
43
+ Stream.runFold(
44
+ () => ({ exitCode: -1, stderr: "" }),
45
+ (acc, event) =>
46
+ event._tag === "End"
47
+ ? { ...acc, exitCode: event.exitCode }
48
+ : event._tag === "Stderr"
49
+ ? {
50
+ ...acc,
51
+ stderr: (acc.stderr + new TextDecoder().decode(event.data)).slice(-4000),
52
+ }
53
+ : acc,
54
+ ),
55
+ Effect.provide(sandboxConnectionFor(sandbox)),
56
+ );
57
+ if (result.exitCode !== 0) {
58
+ return yield* new SnapshotSetupFailed({
59
+ message: `snapshot ${name}: \`${command}\` exited with ${result.exitCode}`,
60
+ command,
61
+ exitCode: result.exitCode,
62
+ stderr: result.stderr,
63
+ });
64
+ }
65
+ }
66
+ return yield* sandboxes.createSandboxSnapshot({
67
+ sandboxID: sandbox.sandboxID,
68
+ name,
69
+ memory: props.memory ?? true,
70
+ });
71
+ }),
72
+ (sandbox) => Effect.ignore(sandboxes.deleteSandbox({ sandboxID: sandbox.sandboxID })),
73
+ );
74
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The gzipped tar a template build's `COPY` step uploads: the step's files
3
+ * at their archive paths, as E2B's builder extracts them.
4
+ */
5
+ import * as zlib from "node:zlib";
6
+ import * as Effect from "effect/Effect";
7
+
8
+ export interface TarFile {
9
+ /** Path inside the archive, `/`-separated. */
10
+ readonly path: string;
11
+ readonly mode: number;
12
+ readonly contents: Uint8Array;
13
+ }
14
+
15
+ const encoder = new TextEncoder();
16
+
17
+ // ---- tar (ustar + pax for long names) ---------------------------------------
18
+
19
+ const octal = (value: number, length: number) => value.toString(8).padStart(length - 1, "0") + "\0";
20
+
21
+ const header = (name: string, size: number, mode: number, type: "0" | "x"): Uint8Array => {
22
+ const block = new Uint8Array(512);
23
+ const write = (text: string, offset: number) => block.set(encoder.encode(text), offset);
24
+ write(name.slice(0, 100), 0);
25
+ write(octal(mode, 8), 100);
26
+ write(octal(0, 8), 108);
27
+ write(octal(0, 8), 116);
28
+ write(octal(size, 12), 124);
29
+ write(octal(0, 12), 136);
30
+ write(" ", 148);
31
+ write(type, 156);
32
+ write("ustar\0", 257);
33
+ write("00", 263);
34
+ let checksum = 0;
35
+ for (const byte of block) checksum += byte;
36
+ write(octal(checksum, 7) + " ", 148);
37
+ return block;
38
+ };
39
+
40
+ const padded = (bytes: Uint8Array): Uint8Array[] => {
41
+ const pad = (512 - (bytes.length % 512)) % 512;
42
+ return pad === 0 ? [bytes] : [bytes, new Uint8Array(pad)];
43
+ };
44
+
45
+ const paxRecord = (key: string, value: string) => {
46
+ // "<len> <key>=<value>\n", where <len> counts itself.
47
+ const body = ` ${key}=${value}\n`;
48
+ let length = body.length + 1;
49
+ while (String(length).length + body.length !== length)
50
+ length = String(length).length + body.length;
51
+ return `${length}${body}`;
52
+ };
53
+
54
+ /** A gzipped tar of the files, as E2B's builder extracts it. */
55
+ export const tarGzip = (files: ReadonlyArray<TarFile>) =>
56
+ Effect.sync(() => {
57
+ const parts: Uint8Array[] = [];
58
+ for (const file of files) {
59
+ if (encoder.encode(file.path).length > 100) {
60
+ const pax = encoder.encode(paxRecord("path", file.path));
61
+ parts.push(header("PaxHeader", pax.length, 0o644, "x"), ...padded(pax));
62
+ }
63
+ parts.push(header(file.path, file.contents.length, file.mode, "0"), ...padded(file.contents));
64
+ }
65
+ parts.push(new Uint8Array(1024));
66
+ const size = parts.reduce((n, p) => n + p.length, 0);
67
+ const tar = new Uint8Array(size);
68
+ let offset = 0;
69
+ for (const part of parts) {
70
+ tar.set(part, offset);
71
+ offset += part.length;
72
+ }
73
+ return new Uint8Array(zlib.gzipSync(tar));
74
+ });