@alchemy.run/archil 0.0.0 → 2.0.0-beta.87

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 (184) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +57 -4
  3. package/lib/AWS/S3Disk.d.ts +68 -0
  4. package/lib/AWS/S3Disk.d.ts.map +1 -0
  5. package/lib/AWS/S3Disk.js +100 -0
  6. package/lib/AWS/S3Disk.js.map +1 -0
  7. package/lib/AWS/index.d.ts +2 -0
  8. package/lib/AWS/index.d.ts.map +1 -0
  9. package/lib/AWS/index.js +2 -0
  10. package/lib/AWS/index.js.map +1 -0
  11. package/lib/ApiToken.d.ts +63 -0
  12. package/lib/ApiToken.d.ts.map +1 -0
  13. package/lib/ApiToken.js +121 -0
  14. package/lib/ApiToken.js.map +1 -0
  15. package/lib/ArchilBindings.d.ts +65 -0
  16. package/lib/ArchilBindings.d.ts.map +1 -0
  17. package/lib/ArchilBindings.js +128 -0
  18. package/lib/ArchilBindings.js.map +1 -0
  19. package/lib/AuthProvider.d.ts +40 -0
  20. package/lib/AuthProvider.d.ts.map +1 -0
  21. package/lib/AuthProvider.js +87 -0
  22. package/lib/AuthProvider.js.map +1 -0
  23. package/lib/Bootstrap.d.ts +8 -0
  24. package/lib/Bootstrap.d.ts.map +1 -0
  25. package/lib/Bootstrap.js +27 -0
  26. package/lib/Bootstrap.js.map +1 -0
  27. package/lib/Branch.d.ts +101 -0
  28. package/lib/Branch.d.ts.map +1 -0
  29. package/lib/Branch.js +117 -0
  30. package/lib/Branch.js.map +1 -0
  31. package/lib/BranchDisk.d.ts +42 -0
  32. package/lib/BranchDisk.d.ts.map +1 -0
  33. package/lib/BranchDisk.js +14 -0
  34. package/lib/BranchDisk.js.map +1 -0
  35. package/lib/Checkpoint.d.ts +78 -0
  36. package/lib/Checkpoint.d.ts.map +1 -0
  37. package/lib/Checkpoint.js +95 -0
  38. package/lib/Checkpoint.js.map +1 -0
  39. package/lib/Clients.d.ts +369 -0
  40. package/lib/Clients.d.ts.map +1 -0
  41. package/lib/Clients.js +334 -0
  42. package/lib/Clients.js.map +1 -0
  43. package/lib/ConnectSandbox.d.ts +69 -0
  44. package/lib/ConnectSandbox.d.ts.map +1 -0
  45. package/lib/ConnectSandbox.js +21 -0
  46. package/lib/ConnectSandbox.js.map +1 -0
  47. package/lib/Contributions.d.ts +7 -0
  48. package/lib/Contributions.d.ts.map +1 -0
  49. package/lib/Contributions.js +6 -0
  50. package/lib/Contributions.js.map +1 -0
  51. package/lib/CreateDisk.d.ts +90 -0
  52. package/lib/CreateDisk.d.ts.map +1 -0
  53. package/lib/CreateDisk.js +47 -0
  54. package/lib/CreateDisk.js.map +1 -0
  55. package/lib/CreateImage.d.ts +42 -0
  56. package/lib/CreateImage.d.ts.map +1 -0
  57. package/lib/CreateImage.js +19 -0
  58. package/lib/CreateImage.js.map +1 -0
  59. package/lib/CreateSandbox.d.ts +104 -0
  60. package/lib/CreateSandbox.d.ts.map +1 -0
  61. package/lib/CreateSandbox.js +98 -0
  62. package/lib/CreateSandbox.js.map +1 -0
  63. package/lib/Credentials.d.ts +44 -0
  64. package/lib/Credentials.d.ts.map +1 -0
  65. package/lib/Credentials.js +57 -0
  66. package/lib/Credentials.js.map +1 -0
  67. package/lib/Disk.d.ts +283 -0
  68. package/lib/Disk.d.ts.map +1 -0
  69. package/lib/Disk.js +294 -0
  70. package/lib/Disk.js.map +1 -0
  71. package/lib/DiskObjectsHttp.d.ts +20 -0
  72. package/lib/DiskObjectsHttp.d.ts.map +1 -0
  73. package/lib/DiskObjectsHttp.js +51 -0
  74. package/lib/DiskObjectsHttp.js.map +1 -0
  75. package/lib/DiskUser.d.ts +189 -0
  76. package/lib/DiskUser.d.ts.map +1 -0
  77. package/lib/DiskUser.js +243 -0
  78. package/lib/DiskUser.js.map +1 -0
  79. package/lib/ExecDisk.d.ts +57 -0
  80. package/lib/ExecDisk.d.ts.map +1 -0
  81. package/lib/ExecDisk.js +3 -0
  82. package/lib/ExecDisk.js.map +1 -0
  83. package/lib/ExecDiskHttp.d.ts +5 -0
  84. package/lib/ExecDiskHttp.d.ts.map +1 -0
  85. package/lib/ExecDiskHttp.js +18 -0
  86. package/lib/ExecDiskHttp.js.map +1 -0
  87. package/lib/ExecDisks.d.ts +54 -0
  88. package/lib/ExecDisks.d.ts.map +1 -0
  89. package/lib/ExecDisks.js +37 -0
  90. package/lib/ExecDisks.js.map +1 -0
  91. package/lib/Image.d.ts +188 -0
  92. package/lib/Image.d.ts.map +1 -0
  93. package/lib/Image.js +191 -0
  94. package/lib/Image.js.map +1 -0
  95. package/lib/InferEnv.d.ts +26 -0
  96. package/lib/InferEnv.d.ts.map +1 -0
  97. package/lib/InferEnv.js +1 -0
  98. package/lib/InferEnv.js.map +1 -0
  99. package/lib/MountDisk.d.ts +190 -0
  100. package/lib/MountDisk.d.ts.map +1 -0
  101. package/lib/MountDisk.js +227 -0
  102. package/lib/MountDisk.js.map +1 -0
  103. package/lib/Providers.d.ts +25 -0
  104. package/lib/Providers.d.ts.map +1 -0
  105. package/lib/Providers.js +48 -0
  106. package/lib/Providers.js.map +1 -0
  107. package/lib/ReadDisk.d.ts +55 -0
  108. package/lib/ReadDisk.d.ts.map +1 -0
  109. package/lib/ReadDisk.js +3 -0
  110. package/lib/ReadDisk.js.map +1 -0
  111. package/lib/ReadWriteDisk.d.ts +26 -0
  112. package/lib/ReadWriteDisk.d.ts.map +1 -0
  113. package/lib/ReadWriteDisk.js +3 -0
  114. package/lib/ReadWriteDisk.js.map +1 -0
  115. package/lib/Sandbox.d.ts +507 -0
  116. package/lib/Sandbox.d.ts.map +1 -0
  117. package/lib/Sandbox.js +729 -0
  118. package/lib/Sandbox.js.map +1 -0
  119. package/lib/SandboxOptions.d.ts +185 -0
  120. package/lib/SandboxOptions.d.ts.map +1 -0
  121. package/lib/SandboxOptions.js +102 -0
  122. package/lib/SandboxOptions.js.map +1 -0
  123. package/lib/SandboxPortToken.d.ts +64 -0
  124. package/lib/SandboxPortToken.d.ts.map +1 -0
  125. package/lib/SandboxPortToken.js +77 -0
  126. package/lib/SandboxPortToken.js.map +1 -0
  127. package/lib/WriteDisk.d.ts +37 -0
  128. package/lib/WriteDisk.d.ts.map +1 -0
  129. package/lib/WriteDisk.js +3 -0
  130. package/lib/WriteDisk.js.map +1 -0
  131. package/lib/index.d.ts +29 -0
  132. package/lib/index.d.ts.map +1 -0
  133. package/lib/index.js +28 -0
  134. package/lib/index.js.map +1 -0
  135. package/lib/internal/DeployerKey.d.ts +21 -0
  136. package/lib/internal/DeployerKey.d.ts.map +1 -0
  137. package/lib/internal/DeployerKey.js +34 -0
  138. package/lib/internal/DeployerKey.js.map +1 -0
  139. package/lib/internal/archil.d.ts +87 -0
  140. package/lib/internal/archil.d.ts.map +1 -0
  141. package/lib/internal/archil.js +154 -0
  142. package/lib/internal/archil.js.map +1 -0
  143. package/lib/internal/hosted.d.ts +90 -0
  144. package/lib/internal/hosted.d.ts.map +1 -0
  145. package/lib/internal/hosted.js +291 -0
  146. package/lib/internal/hosted.js.map +1 -0
  147. package/package.json +79 -3
  148. package/src/AWS/S3Disk.ts +125 -0
  149. package/src/AWS/index.ts +1 -0
  150. package/src/ApiToken.ts +168 -0
  151. package/src/ArchilBindings.ts +179 -0
  152. package/src/AuthProvider.ts +122 -0
  153. package/src/Bootstrap.ts +54 -0
  154. package/src/Branch.ts +199 -0
  155. package/src/BranchDisk.ts +57 -0
  156. package/src/Checkpoint.ts +145 -0
  157. package/src/Clients.ts +954 -0
  158. package/src/ConnectSandbox.ts +91 -0
  159. package/src/Contributions.ts +9 -0
  160. package/src/CreateDisk.ts +165 -0
  161. package/src/CreateImage.ts +73 -0
  162. package/src/CreateSandbox.ts +258 -0
  163. package/src/Credentials.ts +101 -0
  164. package/src/Disk.ts +468 -0
  165. package/src/DiskObjectsHttp.ts +64 -0
  166. package/src/DiskUser.ts +369 -0
  167. package/src/ExecDisk.ts +62 -0
  168. package/src/ExecDiskHttp.ts +25 -0
  169. package/src/ExecDisks.ts +100 -0
  170. package/src/Image.ts +316 -0
  171. package/src/InferEnv.ts +27 -0
  172. package/src/MountDisk.ts +466 -0
  173. package/src/Providers.ts +75 -0
  174. package/src/ReadDisk.ts +61 -0
  175. package/src/ReadWriteDisk.ts +31 -0
  176. package/src/Sandbox.ts +1064 -0
  177. package/src/SandboxOptions.ts +230 -0
  178. package/src/SandboxPortToken.ts +125 -0
  179. package/src/WriteDisk.ts +42 -0
  180. package/src/globals.d.ts +2 -0
  181. package/src/index.ts +59 -0
  182. package/src/internal/DeployerKey.ts +63 -0
  183. package/src/internal/archil.ts +273 -0
  184. package/src/internal/hosted.ts +402 -0
package/src/Clients.ts ADDED
@@ -0,0 +1,954 @@
1
+ /**
2
+ * The run-time handles Archil bindings return. A disk or sandbox handle is
3
+ * the same shape whether it came from a deploy-time resource
4
+ * (`ExecDisk(Dataset)`, `ConnectSandbox(Preview)`) or was created at run
5
+ * time (`CreateDisk`, `CreateSandbox`), and takes Archil's own parameters.
6
+ */
7
+ import * as archil from "@distilled.cloud/archil";
8
+ import * as Objects from "@distilled.cloud/archil/Objects";
9
+ import * as Process from "@distilled.cloud/archil/Process";
10
+ import type { RuntimeContext } from "alchemy/RuntimeContext";
11
+ import * as SandboxBase from "alchemy/Sandbox";
12
+ import * as Data from "effect/Data";
13
+ import * as Duration from "effect/Duration";
14
+ import * as Effect from "effect/Effect";
15
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
16
+ import * as HttpClient from "effect/http/HttpClient";
17
+ import * as HttpClientRequest from "effect/http/HttpClientRequest";
18
+ import * as Option from "effect/Option";
19
+ import * as Redacted from "effect/Redacted";
20
+ import type * as Scope from "effect/Scope";
21
+ import * as Stream from "effect/Stream";
22
+ import type { Authorize, ObjectsRunner } from "./ArchilBindings.ts";
23
+ import type { BranchAttributes } from "./Branch.ts";
24
+ import { ensureBranch, toBranchAttributes } from "./Branch.ts";
25
+ import type { DiskAttributes } from "./Disk.ts";
26
+ import type { DiskUserAttributes } from "./DiskUser.ts";
27
+ import {
28
+ addDiskUser,
29
+ removeDiskUser,
30
+ type DiskUserSpec,
31
+ type DiskUserUnexpectedResponse,
32
+ } from "./DiskUser.ts";
33
+ import { deleteSandbox, observeSandbox, type SandboxAttributes } from "./Sandbox.ts";
34
+ import { toEgressPolicy, type SandboxEgress } from "./SandboxOptions.ts";
35
+ import type { SandboxPortTokenAttributes } from "./SandboxPortToken.ts";
36
+ import { createPortToken, revokePortToken } from "./SandboxPortToken.ts";
37
+
38
+ type R = RuntimeContext;
39
+
40
+ /** What a serverless exec printed, and its exit code. */
41
+ export interface ExecResult {
42
+ readonly exitCode: number;
43
+ readonly stdout: string;
44
+ readonly stderr: string;
45
+ /** Where the time went, as Archil reports it. */
46
+ readonly timing: archil.ExecTiming;
47
+ }
48
+
49
+ /** Search options for `grep`. */
50
+ export interface GrepOptions {
51
+ /** Directory on the disk to search. @default "/" */
52
+ readonly directory?: string;
53
+ /** Regular expression. */
54
+ readonly pattern: string;
55
+ /** Search subdirectories. @default false */
56
+ readonly recursive?: boolean;
57
+ /** Stop after this many seconds. @default 30 */
58
+ readonly maxDurationSeconds?: number;
59
+ /** Workers to fan out to. @default 50 */
60
+ readonly concurrency?: number;
61
+ /** Stop after this many matches. @default 1000 */
62
+ readonly maxResults?: number;
63
+ }
64
+
65
+ /** Serverless commands and search against one disk. */
66
+ export interface ExecDiskClient {
67
+ /**
68
+ * Run a shell command in a fresh microVM with the disk mounted at
69
+ * `/mnt/archil`. Blocks until it exits (up to five minutes).
70
+ */
71
+ exec(command: string): Effect.Effect<ExecResult, archil.ExecDiskError, R>;
72
+ /** Constant-time parallel grep across the disk's files. */
73
+ grep(options: GrepOptions): Effect.Effect<archil.GrepDiskResult, archil.GrepDiskError, R>;
74
+ }
75
+
76
+ /** Read the disk's files as objects. */
77
+ export interface ReadDiskClient {
78
+ /** A file's bytes, optionally a byte range (`bytes=0-1023`). */
79
+ get(
80
+ key: string,
81
+ options?: { readonly range?: string },
82
+ ): Effect.Effect<Uint8Array, Objects.ObjectsFailure | ObjectsTransportError, R>;
83
+ /** A file as UTF-8 text. */
84
+ getText(key: string): Effect.Effect<string, Objects.ObjectsFailure | ObjectsTransportError, R>;
85
+ /** A file's bytes as a stream, for files too big to hold in memory. */
86
+ getStream(
87
+ key: string,
88
+ ): Stream.Stream<Uint8Array, Objects.ObjectsFailure | ObjectsTransportError, R>;
89
+ /** A file's metadata, or `undefined` when it doesn't exist. */
90
+ head(
91
+ key: string,
92
+ ): Effect.Effect<
93
+ Objects.ObjectMetadata | undefined,
94
+ Objects.ObjectsFailure | ObjectsTransportError,
95
+ R
96
+ >;
97
+ /** Every file under a prefix, lazily paginated. */
98
+ list(
99
+ prefix?: string,
100
+ options?: { readonly recursive?: boolean },
101
+ ): Stream.Stream<Objects.ObjectSummary, Objects.ObjectsFailure | ObjectsTransportError, R>;
102
+ /**
103
+ * A signed URL anyone can download one file from, valid for
104
+ * `expiresInSeconds` (1 to 604800; default one hour).
105
+ */
106
+ share(
107
+ key: string,
108
+ expiresInSeconds?: number,
109
+ ): Effect.Effect<Redacted.Redacted<string>, ShareError, R>;
110
+ }
111
+
112
+ /** What creating a share URL can fail with. */
113
+ export type ShareError =
114
+ | archil.CreateShareUrlError
115
+ | Effect.Error<ReturnType<typeof Objects.presignGetObject>>;
116
+
117
+ /** Write the disk's files as objects. */
118
+ export interface WriteDiskClient {
119
+ /** Write a file (≤ 5 GiB). */
120
+ put(
121
+ key: string,
122
+ body: Uint8Array | string,
123
+ options?: { readonly contentType?: string } & Objects.PosixAttributes,
124
+ ): Effect.Effect<{ etag: string | undefined }, Objects.ObjectsFailure | ObjectsTransportError, R>;
125
+ /** Append to a file, creating it when missing (≤ 1 MiB per call). */
126
+ append(
127
+ key: string,
128
+ body: Uint8Array | string,
129
+ ): Effect.Effect<{ etag: string | undefined }, Objects.ObjectsFailure | ObjectsTransportError, R>;
130
+ /** Delete a file. Deleting a missing file succeeds. */
131
+ delete(key: string): Effect.Effect<void, Objects.ObjectsFailure | ObjectsTransportError, R>;
132
+ /** Delete many files; per-file failures are returned. */
133
+ deleteMany(keys: ReadonlyArray<string>): Effect.Effect<
134
+ {
135
+ deleted: string[];
136
+ errors: Array<{ readonly key: string; readonly code?: string; readonly message?: string }>;
137
+ },
138
+ Objects.ObjectsFailure | ObjectsTransportError,
139
+ R
140
+ >;
141
+ /** Copy a file within the disk. */
142
+ copy(
143
+ sourceKey: string,
144
+ key: string,
145
+ ): Effect.Effect<{ etag: string | undefined }, Objects.ObjectsFailure | ObjectsTransportError, R>;
146
+ /**
147
+ * Write a file of any size from a stream, as a multipart upload in
148
+ * 8 MiB parts. A failed upload is aborted, so no parts are left behind.
149
+ */
150
+ upload<E>(
151
+ key: string,
152
+ body: Stream.Stream<Uint8Array, E>,
153
+ options?: { readonly contentType?: string } & Objects.PosixAttributes,
154
+ ): Effect.Effect<
155
+ { etag: string | undefined },
156
+ Objects.ObjectsFailure | ObjectsTransportError | E,
157
+ R
158
+ >;
159
+ }
160
+
161
+ /** Write delegations on a disk: the paths a mounted client holds for writing. */
162
+ export interface DiskDelegationsClient {
163
+ /** Write delegations held on the disk, e.g. by a crashed mount. */
164
+ delegations(): Effect.Effect<archil.DelegationEntry[], archil.ListDelegationsError, R>;
165
+ /** Reclaim write access from a client that is gone. */
166
+ revokeDelegation(
167
+ delegation: Pick<archil.DelegationEntry, "clientId" | "inodeId">,
168
+ ): Effect.Effect<void, archil.RevokeDelegationError, R>;
169
+ }
170
+
171
+ /** Both {@link ReadDiskClient} and {@link WriteDiskClient}. */
172
+ export interface ReadWriteDiskClient extends ReadDiskClient, WriteDiskClient {}
173
+
174
+ /** Errors from the HTTP transport underneath the objects API. */
175
+ export type ObjectsTransportError = Effect.Error<ReturnType<typeof Objects.headObject>>;
176
+
177
+ /** Branches of one disk. */
178
+ export interface BranchDiskClient {
179
+ /**
180
+ * Create a writable branch from a committed checkpoint, or return the
181
+ * branch with that name.
182
+ */
183
+ branch(options: {
184
+ readonly name: string;
185
+ readonly fromCheckpoint: string;
186
+ readonly fromBranch?: string;
187
+ }): Effect.Effect<BranchAttributes, archil.CreateBranchError | archil.GetBranchError, R>;
188
+ /** A branch, or `undefined` when it doesn't exist. */
189
+ getBranch(name: string): Effect.Effect<BranchAttributes | undefined, archil.GetBranchError, R>;
190
+ /** Every branch of the disk, including branches of branches. */
191
+ listBranches(): Effect.Effect<BranchAttributes[], archil.ListBranchesError, R>;
192
+ }
193
+
194
+ /** One disk, with everything a run-time client can do to it. */
195
+ export interface DiskHandle
196
+ extends ExecDiskClient, ReadWriteDiskClient, BranchDiskClient, DiskDelegationsClient {
197
+ readonly diskId: string;
198
+ readonly name: string;
199
+ /**
200
+ * A disk token for mounting this disk only, e.g. to hand to a machine
201
+ * that mounts with `MountDisk()`. `readOnly`, `expiresAt` and `oneUse`
202
+ * narrow it. Revoke it with `removeToken`.
203
+ */
204
+ createToken(
205
+ nickname: string,
206
+ options?: Pick<DiskUserSpec, "readOnly" | "expiresAt" | "oneUse">,
207
+ ): Effect.Effect<DiskUserAttributes, archil.AddDiskUserError | DiskUserUnexpectedResponse, R>;
208
+ /** Revoke a disk token. */
209
+ removeToken(identifier: string): Effect.Effect<void, archil.RemoveDiskUserError, R>;
210
+ }
211
+
212
+ /** A command: one shell line, or an argument vector (quoted for you). */
213
+ export type Command = SandboxBase.Command;
214
+
215
+ /** Options for a command run in a sandbox (`timeout` rounds up to whole seconds). */
216
+ export type ExecOptions = SandboxBase.ExecOptions;
217
+
218
+ /** Options for a process started in the background. */
219
+ export type StartOptions = Omit<ExecOptions, "stdin">;
220
+
221
+ /**
222
+ * What a process call on a sandbox handle can fail with. A paused sandbox
223
+ * is resumed first, so resuming can fail too.
224
+ */
225
+ export type ProcessCallError =
226
+ | Process.ProcessError
227
+ | archil.CreateSandboxConnectionError
228
+ | archil.GetSandboxError
229
+ | archil.ResumeSandboxError;
230
+
231
+ /** A chunk of process output, with its offset in the combined output (for `attach`). */
232
+ export interface SandboxProcessOutput extends SandboxBase.ProcessOutput {
233
+ readonly offset: number;
234
+ }
235
+
236
+ /** How an Archil process ended. */
237
+ export interface SandboxProcessExit {
238
+ /**
239
+ * The exit code. Archil reports none for a process it killed (a timeout,
240
+ * or `kill`); that is `-1` here, and `status` / `exitReason` say why.
241
+ */
242
+ readonly exitCode: number;
243
+ readonly status: string;
244
+ readonly exitReason: string | undefined;
245
+ /** Output offset at exit, for a later `attach`. */
246
+ readonly cursor: number;
247
+ }
248
+
249
+ /**
250
+ * A process running in a sandbox, attached over one connection. Closing
251
+ * the scope that started it detaches; the process keeps running and can be
252
+ * re-attached by `id`.
253
+ */
254
+ export interface SandboxProcess extends SandboxBase.Process<ProcessCallError> {
255
+ readonly output: Stream.Stream<SandboxProcessOutput, ProcessCallError, R>;
256
+ /**
257
+ * Kill the process. Archil's process API kills without a choice of
258
+ * signal, so `signal` is ignored.
259
+ */
260
+ kill(signal?: "SIGTERM" | "SIGKILL"): Effect.Effect<void, ProcessCallError, R>;
261
+ readonly wait: Effect.Effect<SandboxProcessExit, ProcessCallError, R>;
262
+ /** Close stdin. */
263
+ readonly closeStdin: Effect.Effect<void, ProcessCallError, R>;
264
+ }
265
+
266
+ /** A shell (or command) on a pseudo-terminal. */
267
+ export interface PtyConnection extends SandboxBase.PtyConnection<ProcessCallError> {
268
+ /** Kill the terminal's process. */
269
+ kill(): Effect.Effect<void, ProcessCallError, R>;
270
+ /** Wait for the terminal's process to exit. */
271
+ readonly wait: Effect.Effect<SandboxProcessExit, ProcessCallError, R>;
272
+ }
273
+
274
+ /** Options for a pseudo-terminal. `command` may also be an argument vector. */
275
+ export interface PtyOptions extends Omit<SandboxBase.PtyOptions, "command"> {
276
+ /** @default "bash" */
277
+ readonly command?: Command;
278
+ }
279
+
280
+ /**
281
+ * Files in the sandbox. Archil's process protocol has no file calls of its
282
+ * own: these run small POSIX shell commands over it (as Archil's own SDK
283
+ * does for uploads and downloads), so they work on any image with `sh`,
284
+ * `cat`, `stat`, `rm` and `mkdir`.
285
+ */
286
+ export interface SandboxFiles extends SandboxBase.Files<ProcessCallError> {
287
+ /** Write a file (atomically: a temp file, then a rename), creating its directory. */
288
+ write(
289
+ path: string,
290
+ data: string | Uint8Array,
291
+ options?: { readonly mode?: number },
292
+ ): Effect.Effect<void, ProcessCallError, R>;
293
+ }
294
+
295
+ /** The sandbox doesn't serve this port publicly (`exposePort` it first). */
296
+ export class SandboxPortNotExposed extends Data.TaggedError("Archil.SandboxPortNotExposed")<{
297
+ readonly port: number;
298
+ readonly message: string;
299
+ }> {}
300
+
301
+ /** Why `getTcpPort` can fail. */
302
+ export type SandboxPortError = SandboxPortNotExposed | archil.ListSandboxPortsError;
303
+
304
+ /**
305
+ * A port the sandbox serves publicly, at its own `archil.app` hostname.
306
+ * `fetch` sends a client request there, or proxies the server request a
307
+ * handler received; either way the method, path, query, headers and body
308
+ * are kept. Anyone with the URL can call a public port, so hand browsers a
309
+ * port token (`createPortToken`) rather than your key.
310
+ */
311
+ export interface SandboxPort extends SandboxBase.SandboxPort<SandboxPortError> {
312
+ /** The port inside the sandbox. */
313
+ readonly port: number;
314
+ /** Archil's public hostname for this port, e.g. `3000-<route>.<zone>.archil.app`. */
315
+ readonly hostname: string;
316
+ /** `https://` + `hostname`. */
317
+ readonly url: string;
318
+ }
319
+
320
+ /** Everything a run-time client can do to one sandbox. */
321
+ export interface SandboxOps
322
+ extends
323
+ SandboxBase.SandboxExec<ProcessCallError, Process.ProcessResult, ExecOptions>,
324
+ SandboxBase.SandboxProcesses<ProcessCallError, StartOptions>,
325
+ SandboxBase.SandboxFiles<ProcessCallError>,
326
+ SandboxBase.SandboxPty<ProcessCallError, PtyOptions>,
327
+ SandboxBase.SandboxPorts<SandboxPortError, SandboxPort> {
328
+ /** The sandbox's current attributes. */
329
+ refresh(): Effect.Effect<SandboxAttributes, archil.GetSandboxError, R>;
330
+ /**
331
+ * Run a command to completion and collect its output. A non-zero exit is
332
+ * returned in `exitCode`, not failed.
333
+ */
334
+ exec(
335
+ command: Command,
336
+ options?: ExecOptions,
337
+ ): Effect.Effect<Process.ProcessResult, ProcessCallError, R>;
338
+ /**
339
+ * Start a command in the background and attach to it. Closing the scope
340
+ * detaches; the process keeps running and can be re-attached by id.
341
+ */
342
+ start(
343
+ command: Command,
344
+ options?: StartOptions,
345
+ ): Effect.Effect<SandboxProcess, ProcessCallError, R | Scope.Scope>;
346
+ /** Re-attach to a running process, resuming its output at `offset`. */
347
+ attach(
348
+ processId: string,
349
+ offset?: number,
350
+ ): Effect.Effect<SandboxProcess, ProcessCallError, R | Scope.Scope>;
351
+ /** Files in the sandbox. */
352
+ readonly files: SandboxFiles;
353
+ /** Pseudo-terminals. */
354
+ readonly pty: {
355
+ /** Open a shell (or `command`) on a terminal of `cols` × `rows`. */
356
+ open(options: PtyOptions): Effect.Effect<PtyConnection, ProcessCallError, R | Scope.Scope>;
357
+ };
358
+ /**
359
+ * A port the sandbox serves publicly. Fails with `SandboxPortNotExposed`
360
+ * until the port is exposed (`ports` on the resource, or `exposePort`).
361
+ */
362
+ getTcpPort(port: number): Effect.Effect<SandboxPort, SandboxPortError, R>;
363
+ /** Start a stopped sandbox (Archil's `start`). */
364
+ boot(): Effect.Effect<SandboxAttributes, archil.StartSandboxError, R>;
365
+ /** Stop the sandbox (its disk survives; memory doesn't). */
366
+ stop(): Effect.Effect<SandboxAttributes, archil.StopSandboxError, R>;
367
+ /** Pause the sandbox, keeping its memory. */
368
+ pause(): Effect.Effect<SandboxAttributes, archil.PauseSandboxError, R>;
369
+ /** Resume a paused sandbox. */
370
+ resume(): Effect.Effect<SandboxAttributes, archil.ResumeSandboxError, R>;
371
+ /**
372
+ * Fork the sandbox (memory and disk) into a new one. Archil only forks a
373
+ * sandbox with mounts while it is stopped.
374
+ */
375
+ fork(options?: {
376
+ readonly name?: string;
377
+ readonly checkpoint?: string;
378
+ }): Effect.Effect<SandboxHandle, archil.ForkSandboxError, R>;
379
+ /**
380
+ * Pause after `timeoutSeconds` from now, and/or after `idleTtlSeconds`
381
+ * without a connection (`0` disables idle expiry).
382
+ */
383
+ setTimeout(options: {
384
+ readonly timeoutSeconds?: number;
385
+ readonly idleTtlSeconds?: number;
386
+ }): Effect.Effect<SandboxAttributes, archil.SetSandboxTimeoutError, R>;
387
+ /** Serve a port publicly. */
388
+ exposePort(
389
+ port: number,
390
+ ): Effect.Effect<{ port: number; hostname: string }, archil.ExposeSandboxPortError, R>;
391
+ /** Stop serving a port. */
392
+ unexposePort(port: number): Effect.Effect<void, archil.DeleteSandboxPortError, R>;
393
+ /** Ports the sandbox serves. */
394
+ ports(): Effect.Effect<
395
+ Array<{ port: number; hostname: string }>,
396
+ archil.ListSandboxPortsError,
397
+ R
398
+ >;
399
+ /** A token that opens one port for a client that shouldn't hold an API key. */
400
+ createPortToken(
401
+ port: number,
402
+ ttl?: string,
403
+ ): Effect.Effect<SandboxPortTokenAttributes, archil.CreateSandboxPortTokenError, R>;
404
+ /** Revoke a port token. */
405
+ revokePortToken(tokenId: string): Effect.Effect<void, archil.DeleteSandboxPortTokenError, R>;
406
+ /**
407
+ * The sandbox's port tokens, without their secret values (Archil only
408
+ * shows a token when it's created).
409
+ */
410
+ listPortTokens(): Stream.Stream<archil.SandboxPortToken, archil.ListSandboxPortTokensError, R>;
411
+ /** Replace the outbound network policy. Applies at once. */
412
+ setEgress(egress: SandboxEgress): Effect.Effect<void, archil.UpdateSandboxNetworkError, R>;
413
+ /**
414
+ * Stop (if it's live) and delete the sandbox. Deleting a missing sandbox
415
+ * succeeds.
416
+ */
417
+ delete(): Effect.Effect<void, Effect.Error<ReturnType<typeof deleteSandbox>>, R>;
418
+ }
419
+
420
+ /** One sandbox known by id and name, with everything a run-time client can do to it. */
421
+ export interface SandboxHandle extends SandboxOps {
422
+ readonly sandboxId: string;
423
+ readonly name: string;
424
+ }
425
+
426
+ const toExecResult = (data: archil.ExecDiskResult): ExecResult => ({
427
+ exitCode: data.exitCode,
428
+ stdout: data.stdout,
429
+ stderr: data.stderr,
430
+ timing: data.timing,
431
+ });
432
+
433
+ const unwrapToken = (value: string | Redacted.Redacted<string>) =>
434
+ typeof value === "string" ? Redacted.make(value) : value;
435
+
436
+ /** Serverless exec + grep on a disk whose id resolves at run time. */
437
+ export const makeExecDiskClient = (
438
+ diskId: Effect.Effect<string, never, R>,
439
+ authorize: Authorize,
440
+ ): ExecDiskClient => ({
441
+ exec: (command) =>
442
+ authorize(
443
+ Effect.gen(function* () {
444
+ const id = yield* diskId;
445
+ const { data } = yield* archil.execDisk({ id, command });
446
+ return toExecResult(data);
447
+ }),
448
+ ),
449
+ grep: (options) =>
450
+ authorize(
451
+ Effect.gen(function* () {
452
+ const { data } = yield* archil.grepDisk({
453
+ id: yield* diskId,
454
+ directory: options.directory ?? "/",
455
+ pattern: options.pattern,
456
+ recursive: options.recursive,
457
+ maxDurationSeconds: options.maxDurationSeconds,
458
+ concurrency: options.concurrency,
459
+ maxResults: options.maxResults,
460
+ });
461
+ return data;
462
+ }),
463
+ ),
464
+ });
465
+
466
+ /** How a read client makes share URLs. */
467
+ export type ShareStrategy = (
468
+ ref: Objects.DiskRef,
469
+ key: string,
470
+ expiresInSeconds: number,
471
+ ) => Effect.Effect<Redacted.Redacted<string>, ShareError, R>;
472
+
473
+ /** Share URLs presigned with the client's own S3 user keys. */
474
+ export const presignedShare =
475
+ (run: ObjectsRunner): ShareStrategy =>
476
+ (ref, key, expiresInSeconds) =>
477
+ run(Objects.presignGetObject(ref, key, expiresInSeconds)).pipe(Effect.map(Redacted.make));
478
+
479
+ /** Share URLs from Archil's control plane (needs the API key). */
480
+ export const controlPlaneShare =
481
+ (authorize: Authorize): ShareStrategy =>
482
+ (ref, key, expiresInSeconds) =>
483
+ authorize(
484
+ Effect.gen(function* () {
485
+ const { data } = yield* archil.createShareUrl({
486
+ id: ref.disk,
487
+ key,
488
+ expiresIn: expiresInSeconds,
489
+ });
490
+ return unwrapToken(data.url);
491
+ }),
492
+ );
493
+
494
+ /** Object reads on a disk (or branch) whose id resolves at run time. */
495
+ export const makeReadDiskClient = (
496
+ ref: Effect.Effect<Objects.DiskRef, never, R>,
497
+ run: ObjectsRunner,
498
+ share: ShareStrategy,
499
+ ): ReadDiskClient => ({
500
+ get: (key, options) => run(Effect.flatMap(ref, (r) => Objects.getObject(r, key, options))),
501
+ getText: (key) => run(Effect.flatMap(ref, (r) => Objects.getObjectText(r, key))),
502
+ getStream: (key) =>
503
+ run.stream(Stream.unwrap(Effect.map(ref, (r) => Objects.getObjectStream(r, key)))),
504
+ head: (key) => run(Effect.flatMap(ref, (r) => Objects.headObject(r, key))),
505
+ list: (prefix, options) =>
506
+ run.stream(
507
+ Stream.unwrap(
508
+ Effect.map(ref, (r) => Objects.listObjects(r, prefix, { recursive: options?.recursive })),
509
+ ),
510
+ ),
511
+ share: (key, expiresInSeconds = 3600) =>
512
+ Effect.flatMap(ref, (r) => share(r, key, expiresInSeconds)),
513
+ });
514
+
515
+ const PART_SIZE = 8 * 1024 * 1024;
516
+
517
+ const concatBytes = (chunks: ReadonlyArray<Uint8Array>) => {
518
+ const out = new Uint8Array(chunks.reduce((n, c) => n + c.byteLength, 0));
519
+ let offset = 0;
520
+ for (const chunk of chunks) {
521
+ out.set(chunk, offset);
522
+ offset += chunk.byteLength;
523
+ }
524
+ return out;
525
+ };
526
+
527
+ /** Object writes on a disk (or branch) whose id resolves at run time. */
528
+ export const makeWriteDiskClient = (
529
+ ref: Effect.Effect<Objects.DiskRef, never, R>,
530
+ run: ObjectsRunner,
531
+ ): WriteDiskClient => ({
532
+ put: (key, body, options) =>
533
+ run(Effect.flatMap(ref, (r) => Objects.putObject(r, key, body, options))),
534
+ append: (key, body) => run(Effect.flatMap(ref, (r) => Objects.appendObject(r, key, body))),
535
+ delete: (key) => run(Effect.flatMap(ref, (r) => Objects.deleteObject(r, key))),
536
+ deleteMany: (keys) => run(Effect.flatMap(ref, (r) => Objects.deleteObjects(r, keys))),
537
+ copy: (sourceKey, key) =>
538
+ run(
539
+ Effect.flatMap(ref, (r) => Objects.copyObject(r, sourceKey, key)).pipe(
540
+ Effect.map(({ etag }) => ({ etag })),
541
+ ),
542
+ ),
543
+ upload: (key, body, options) =>
544
+ run(
545
+ Effect.gen(function* () {
546
+ const r = yield* ref;
547
+ const { uploadId } = yield* Objects.createMultipartUpload(r, key, options);
548
+ const parts: Array<{ readonly partNumber: number; readonly etag: string }> = [];
549
+ const sendPart = (bytes: Uint8Array) =>
550
+ Objects.uploadPart(r, key, uploadId, parts.length + 1, bytes).pipe(
551
+ Effect.map((part) => void parts.push(part)),
552
+ );
553
+ return yield* Effect.gen(function* () {
554
+ const rest = yield* Stream.runFoldEffect(
555
+ body,
556
+ (): ReadonlyArray<Uint8Array> => [],
557
+ (pending, chunk) => {
558
+ const next = [...pending, chunk];
559
+ const size = next.reduce((n, c) => n + c.byteLength, 0);
560
+ if (size < PART_SIZE) return Effect.succeed(next);
561
+ const bytes = concatBytes(next);
562
+ return Effect.as(sendPart(bytes.subarray(0, PART_SIZE)), [bytes.subarray(PART_SIZE)]);
563
+ },
564
+ );
565
+ const last = concatBytes(rest);
566
+ // S3 needs at least one part; an empty body is one empty part.
567
+ if (last.byteLength > 0 || parts.length === 0) yield* sendPart(last);
568
+ return yield* Objects.completeMultipartUpload(r, key, uploadId, parts);
569
+ }).pipe(
570
+ Effect.onError(() => Objects.abortMultipartUpload(r, key, uploadId).pipe(Effect.ignore)),
571
+ Effect.map(({ etag }) => ({ etag })),
572
+ );
573
+ }),
574
+ ),
575
+ });
576
+
577
+ /** Write delegations on a disk whose id resolves at run time (needs the API key). */
578
+ export const makeDiskDelegationsClient = (
579
+ ref: Effect.Effect<Objects.DiskRef, never, R>,
580
+ authorize: Authorize,
581
+ ): DiskDelegationsClient => ({
582
+ delegations: () =>
583
+ authorize(
584
+ Effect.gen(function* () {
585
+ const { disk } = yield* ref;
586
+ const { data } = yield* archil.listDelegations({ id: disk });
587
+ return data.delegations;
588
+ }),
589
+ ),
590
+ revokeDelegation: (delegation) =>
591
+ authorize(
592
+ Effect.gen(function* () {
593
+ const { disk } = yield* ref;
594
+ yield* archil.revokeDelegation({
595
+ id: disk,
596
+ clientId: delegation.clientId,
597
+ inodeId: delegation.inodeId,
598
+ });
599
+ }),
600
+ ),
601
+ });
602
+
603
+ /** Branches of a disk whose id resolves at run time. */
604
+ export const makeBranchDiskClient = (
605
+ diskId: Effect.Effect<string, never, R>,
606
+ authorize: Authorize,
607
+ ): BranchDiskClient => ({
608
+ branch: (options) =>
609
+ authorize(
610
+ Effect.gen(function* () {
611
+ const id = yield* diskId;
612
+ return yield* ensureBranch({
613
+ disk: id,
614
+ name: options.name,
615
+ fromCheckpoint: options.fromCheckpoint,
616
+ fromBranch: options.fromBranch,
617
+ });
618
+ }),
619
+ ),
620
+ getBranch: (name) =>
621
+ authorize(
622
+ Effect.gen(function* () {
623
+ const id = yield* diskId;
624
+ return yield* archil.getBranch({ id, name }).pipe(
625
+ Effect.map(({ data }) => toBranchAttributes(data)),
626
+ Effect.catchTag("NotFound", () => Effect.undefined),
627
+ );
628
+ }),
629
+ ),
630
+ listBranches: () =>
631
+ authorize(
632
+ Effect.gen(function* () {
633
+ const id = yield* diskId;
634
+ const { data } = yield* archil.listBranches({ id });
635
+ return data.map(toBranchAttributes);
636
+ }),
637
+ ),
638
+ });
639
+
640
+ /** A full handle on a disk known by id and name. */
641
+ export const makeDiskHandle = (
642
+ disk: Pick<DiskAttributes, "diskId" | "name">,
643
+ authorize: Authorize,
644
+ ): DiskHandle => {
645
+ const diskId = Effect.succeed(disk.diskId);
646
+ const ref = Effect.succeed({ disk: disk.diskId } satisfies Objects.DiskRef);
647
+ return {
648
+ diskId: disk.diskId,
649
+ name: disk.name,
650
+ ...makeExecDiskClient(diskId, authorize),
651
+ ...makeReadDiskClient(ref, authorize.objects, controlPlaneShare(authorize)),
652
+ ...makeWriteDiskClient(ref, authorize.objects),
653
+ ...makeDiskDelegationsClient(ref, authorize),
654
+ ...makeBranchDiskClient(diskId, authorize),
655
+ createToken: (nickname, options) =>
656
+ authorize(addDiskUser(disk.diskId, { nickname, ...options })),
657
+ removeToken: (identifier) =>
658
+ authorize(removeDiskUser({ diskId: disk.diskId, type: "token", identifier })),
659
+ };
660
+ };
661
+
662
+ /** Single-quote a value for `sh`. */
663
+ const shellQuote = (value: string) => `'${value.replaceAll("'", `'"'"'`)}'`;
664
+
665
+ const commandLine = (command: Command) =>
666
+ typeof command === "string" ? command : command.map(shellQuote).join(" ");
667
+
668
+ const runOptions = (options: StartOptions | undefined): Process.RunOptions => ({
669
+ cwd: options?.cwd,
670
+ env: options?.env === undefined ? undefined : { ...options.env },
671
+ timeoutSeconds:
672
+ options?.timeout === undefined ? undefined : Math.ceil(Duration.toSeconds(options.timeout)),
673
+ });
674
+
675
+ const utf8 = new TextDecoder();
676
+
677
+ const toExit = (exit: Process.ProcessExit): SandboxProcessExit => ({
678
+ exitCode: exit.exitCode ?? -1,
679
+ status: exit.status,
680
+ exitReason: exit.exitReason,
681
+ cursor: exit.cursor,
682
+ });
683
+
684
+ const entryType = (kind: string): SandboxBase.FileEntry["type"] =>
685
+ kind.includes("directory")
686
+ ? "directory"
687
+ : kind.includes("symbolic link")
688
+ ? "symlink"
689
+ : kind.includes("regular")
690
+ ? "file"
691
+ : "other";
692
+
693
+ /** Parse `stat -c '%F\t%s\t%n'` lines; names are joined to `dir` when given. */
694
+ const parseEntries = (out: string, dir: string | undefined): SandboxBase.FileEntry[] =>
695
+ out
696
+ .split("\n")
697
+ .filter((line) => line.length > 0)
698
+ .map((line) => {
699
+ const [kind = "", size = "0", ...name] = line.split("\t");
700
+ const base = name.join("\t");
701
+ return {
702
+ path: dir === undefined ? base : `${dir.replace(/\/+$/, "")}/${base}`,
703
+ type: entryType(kind),
704
+ size: Number(size),
705
+ };
706
+ });
707
+
708
+ /**
709
+ * Everything a run-time client can do to the sandbox `sid` resolves to.
710
+ * `forkName` maps a fork's name (a binding puts its owner prefix on it,
711
+ * like on the sandboxes it creates).
712
+ */
713
+ export const makeSandboxOps = (
714
+ sid: Effect.Effect<string, never, R>,
715
+ authorize: Authorize,
716
+ forkName: (name: string | undefined) => Effect.Effect<string | undefined> = Effect.succeed,
717
+ ): SandboxOps => {
718
+ // A paused sandbox (idle TTL) refuses process connections with
719
+ // `sandbox_terminated`; resume it and try once more.
720
+ const awake = <A, E extends { readonly _tag: string }, R2>(
721
+ use: (sid: string) => Effect.Effect<A, E, R2>,
722
+ ) =>
723
+ authorize(
724
+ Effect.flatMap(sid, (id) =>
725
+ use(id).pipe(
726
+ Effect.catchIf(
727
+ (e) => e._tag === "SandboxTerminated",
728
+ (e) =>
729
+ archil
730
+ .getSandbox({ sid: id })
731
+ .pipe(
732
+ Effect.flatMap(({ data }) =>
733
+ data.status === "paused"
734
+ ? archil.resumeSandbox({ sid: id, wait: true }).pipe(Effect.andThen(use(id)))
735
+ : Effect.fail(e),
736
+ ),
737
+ ),
738
+ ),
739
+ ),
740
+ ),
741
+ );
742
+ const call = <A, E, R2>(use: (sid: string) => Effect.Effect<A, E, R2>) =>
743
+ authorize(Effect.flatMap(sid, use));
744
+ const attrs = <E, R2>(use: (sid: string) => Effect.Effect<{ data: archil.Sandbox }, E, R2>) =>
745
+ call((id) => use(id).pipe(Effect.flatMap(({ data }) => observeSandbox(data))));
746
+ const toProcess = (process: Process.Process): SandboxProcess => ({
747
+ id: process.id,
748
+ output: process.output.pipe(
749
+ Stream.map((chunk) => ({ fd: chunk.stream, data: chunk.data, offset: chunk.offset })),
750
+ ),
751
+ write: (data) => process.write(data),
752
+ closeStdin: process.closeStdin,
753
+ kill: () => awake((id) => Process.kill(id, process.id)),
754
+ wait: Effect.map(process.exit, toExit),
755
+ });
756
+ // A shell command whose non-zero exit is a failure with its stderr.
757
+ const sh = (command: string, what: string) =>
758
+ awake((id) => Process.exec(id, command)).pipe(
759
+ Effect.flatMap((result) =>
760
+ result.exitCode === 0
761
+ ? Effect.succeed(result.stdout)
762
+ : Effect.fail(
763
+ Process.ProcessError.make({
764
+ message: `${what} failed (exit ${result.exitCode ?? result.status}): ${result.stderr.trim()}`,
765
+ }),
766
+ ),
767
+ ),
768
+ );
769
+ const files: SandboxFiles = {
770
+ read: (path) => awake((id) => Process.downloadFile(id, path)),
771
+ readText: (path) =>
772
+ awake((id) => Process.downloadFile(id, path)).pipe(Effect.map((bytes) => utf8.decode(bytes))),
773
+ write: (path, data, options) => awake((id) => Process.uploadFile(id, path, data, options)),
774
+ list: (path) =>
775
+ sh(
776
+ `cd ${shellQuote(path)} && for f in * .[!.]* ..?*; do [ -e "$f" ] || [ -L "$f" ] || continue; stat -c '%F\t%s\t%n' -- "$f"; done`,
777
+ `list ${path}`,
778
+ ).pipe(Effect.map((out) => parseEntries(out, path))),
779
+ stat: (path) =>
780
+ sh(`stat -c '%F\t%s\t%n' -- ${shellQuote(path)}`, `stat ${path}`).pipe(
781
+ Effect.map((out) => ({ ...parseEntries(out, undefined)[0]!, path })),
782
+ ),
783
+ remove: (path) => sh(`rm -rf -- ${shellQuote(path)}`, `remove ${path}`).pipe(Effect.asVoid),
784
+ makeDir: (path) => sh(`mkdir -p -- ${shellQuote(path)}`, `makeDir ${path}`).pipe(Effect.asVoid),
785
+ };
786
+ const ops: SandboxOps = {
787
+ refresh: () => attrs((id) => archil.getSandbox({ sid: id })),
788
+ exec: (command, options) =>
789
+ options?.stdin === undefined
790
+ ? awake((id) => Process.exec(id, commandLine(command), runOptions(options)))
791
+ : awake((id) =>
792
+ Effect.scoped(
793
+ Effect.gen(function* () {
794
+ const process = yield* Process.run(id, commandLine(command), runOptions(options));
795
+ yield* process.write(options.stdin!);
796
+ yield* process.closeStdin;
797
+ const out = new TextDecoder();
798
+ const err = new TextDecoder();
799
+ let stdout = "";
800
+ let stderr = "";
801
+ yield* process.output.pipe(
802
+ Stream.runForEach((chunk) =>
803
+ Effect.sync(() => {
804
+ if (chunk.stream === "stdout")
805
+ stdout += out.decode(chunk.data, { stream: true });
806
+ else stderr += err.decode(chunk.data, { stream: true });
807
+ }),
808
+ ),
809
+ );
810
+ const exit = yield* process.exit;
811
+ return {
812
+ ...exit,
813
+ stdout: stdout + out.decode(),
814
+ stderr: stderr + err.decode(),
815
+ } satisfies Process.ProcessResult;
816
+ }),
817
+ ),
818
+ ),
819
+ start: (command, options) =>
820
+ awake((id) => Process.run(id, commandLine(command), runOptions(options))).pipe(
821
+ Effect.map(toProcess),
822
+ ),
823
+ attach: (processId, offset) =>
824
+ awake((id) => Process.attach(id, processId, offset)).pipe(Effect.map(toProcess)),
825
+ files,
826
+ pty: {
827
+ open: (options) =>
828
+ awake((id) =>
829
+ Process.run(id, commandLine(options.command ?? "bash"), {
830
+ cwd: options.cwd,
831
+ env: options.env === undefined ? undefined : { ...options.env },
832
+ terminal: { cols: options.cols, rows: options.rows },
833
+ }),
834
+ ).pipe(
835
+ Effect.map((process): PtyConnection => ({
836
+ id: process.id,
837
+ output: process.output.pipe(Stream.map((chunk) => chunk.data)),
838
+ write: (data) => process.write(data),
839
+ resize: (size) => awake((id) => Process.resize(id, process.id, size)),
840
+ kill: () => awake((id) => Process.kill(id, process.id)),
841
+ wait: Effect.map(process.exit, toExit),
842
+ })),
843
+ ),
844
+ },
845
+ getTcpPort: (port) =>
846
+ call((id) =>
847
+ Effect.gen(function* () {
848
+ const { data } = yield* archil.listSandboxPorts({ sid: id });
849
+ const endpoint = (data.ports ?? []).find((p) => p.port === port);
850
+ if (endpoint === undefined) {
851
+ return yield* new SandboxPortNotExposed({
852
+ port,
853
+ message: `Port ${port} isn't served publicly; call exposePort(${port}) first.`,
854
+ });
855
+ }
856
+ const url = `https://${endpoint.hostname}`;
857
+ // Archil's edge routes by hostname, so the caller's Host header
858
+ // (a Worker's own, when proxying) is replaced by the port's.
859
+ const send = (request: HttpClientRequest.HttpClientRequest) => {
860
+ const source = new URL(request.url, "http://sandbox");
861
+ return HttpClient.HttpClient.pipe(
862
+ Effect.flatMap((client) =>
863
+ client.execute(
864
+ request.pipe(
865
+ HttpClientRequest.setUrl(`${url}${source.pathname}${source.search}`),
866
+ HttpClientRequest.removeHeader("host"),
867
+ ),
868
+ ),
869
+ ),
870
+ Effect.provide(FetchHttpClient.layer),
871
+ );
872
+ };
873
+ return {
874
+ port,
875
+ hostname: endpoint.hostname,
876
+ url,
877
+ fetch: SandboxBase.portFetch<SandboxPortError>(send),
878
+ } satisfies SandboxPort;
879
+ }),
880
+ ),
881
+ boot: () => attrs((id) => archil.startSandbox({ sid: id, wait: true })),
882
+ stop: () => attrs((id) => archil.stopSandbox({ sid: id })),
883
+ pause: () => attrs((id) => archil.pauseSandbox({ sid: id })),
884
+ resume: () => attrs((id) => archil.resumeSandbox({ sid: id })),
885
+ fork: (options) =>
886
+ call((id) =>
887
+ forkName(options?.name).pipe(
888
+ Effect.flatMap((name) =>
889
+ archil.forkSandbox({ sid: id, wait: true, name, checkpoint: options?.checkpoint }),
890
+ ),
891
+ ),
892
+ ).pipe(
893
+ Effect.map(({ data }) =>
894
+ makeSandboxHandle({ sandboxId: data.sandbox_id, name: data.name }, authorize, forkName),
895
+ ),
896
+ ),
897
+ setTimeout: (options) =>
898
+ attrs((id) =>
899
+ archil.setSandboxTimeout({
900
+ sid: id,
901
+ timeout: options.timeoutSeconds,
902
+ idle_ttl_seconds: options.idleTtlSeconds,
903
+ }),
904
+ ),
905
+ exposePort: (port) =>
906
+ call((id) => archil.exposeSandboxPort({ sid: id, port })).pipe(
907
+ Effect.map(({ data }) => ({ port: data.port, hostname: data.hostname })),
908
+ ),
909
+ unexposePort: (port) =>
910
+ call((id) => archil.deleteSandboxPort({ sid: id, port })).pipe(Effect.asVoid),
911
+ ports: () =>
912
+ call((id) => archil.listSandboxPorts({ sid: id })).pipe(
913
+ Effect.map(({ data }) =>
914
+ (data.ports ?? []).map((p) => ({ port: p.port, hostname: p.hostname })),
915
+ ),
916
+ ),
917
+ createPortToken: (port, ttl) => call((id) => createPortToken(id, port, ttl)),
918
+ revokePortToken: (tokenId) => call((id) => revokePortToken(id, tokenId)),
919
+ listPortTokens: () =>
920
+ authorize.stream(
921
+ Stream.unwrap(
922
+ Effect.map(sid, (id) =>
923
+ Stream.paginate(undefined as string | undefined, (cursor) =>
924
+ archil
925
+ .listSandboxPortTokens({ sid: id, cursor })
926
+ .pipe(
927
+ Effect.map(
928
+ ({ data, nextCursor }) =>
929
+ [data.tokens, Option.fromNullishOr(nextCursor)] as const,
930
+ ),
931
+ ),
932
+ ),
933
+ ),
934
+ ),
935
+ ),
936
+ setEgress: (egress) =>
937
+ call((id) => archil.updateSandboxNetwork({ sid: id, egress: toEgressPolicy(egress) })).pipe(
938
+ Effect.asVoid,
939
+ ),
940
+ delete: () => call((id) => deleteSandbox(id)),
941
+ };
942
+ return ops;
943
+ };
944
+
945
+ /** A full handle on a sandbox known by id and name. */
946
+ export const makeSandboxHandle = (
947
+ sandbox: { readonly sandboxId: string; readonly name: string },
948
+ authorize: Authorize,
949
+ forkName?: (name: string | undefined) => Effect.Effect<string | undefined>,
950
+ ): SandboxHandle => ({
951
+ sandboxId: sandbox.sandboxId,
952
+ name: sandbox.name,
953
+ ...makeSandboxOps(Effect.succeed(sandbox.sandboxId), authorize, forkName),
954
+ });