@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
@@ -0,0 +1,91 @@
1
+ import * as Binding from "alchemy/Binding";
2
+ import type { RuntimeContext } from "alchemy/RuntimeContext";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+ import { ApiAccess } from "./ArchilBindings.ts";
6
+ import { makeSandboxOps, type SandboxOps } from "./Clients.ts";
7
+ import type { Sandbox } from "./Sandbox.ts";
8
+
9
+ /**
10
+ * The handle on a deploy-time sandbox: a {@link SandboxHandle} whose id and
11
+ * name resolve at run time.
12
+ */
13
+ export interface ConnectSandboxClient extends SandboxOps {
14
+ readonly sandboxId: Effect.Effect<string, never, RuntimeContext>;
15
+ readonly name: Effect.Effect<string, never, RuntimeContext>;
16
+ }
17
+
18
+ /**
19
+ * Act on a sandbox declared at deploy time from a Worker, Lambda, or any
20
+ * other host: run commands, start background processes and terminals,
21
+ * move files, pause and resume, fork it into a new sandbox, open ports,
22
+ * and change its network policy. A program sandbox (`Archil.Sandbox` with
23
+ * `main`) is a sandbox too.
24
+ *
25
+ * ### Driving a Dev Box
26
+ * **Example:** Build on the dev box, fork a preview per pull request
27
+ * ```typescript
28
+ * export default class Api extends Cloudflare.Worker<Api>()(
29
+ * "Api",
30
+ * { main: import.meta.url },
31
+ * Effect.gen(function* () {
32
+ * const devbox = yield* Archil.ConnectSandbox(DevBox);
33
+ * return {
34
+ * fetch: Effect.gen(function* () {
35
+ * yield* devbox.exec("git pull && pnpm build", { cwd: "/workspace" });
36
+ * yield* devbox.files.write("/workspace/.env", "PREVIEW=1\n");
37
+ * const preview = yield* devbox.fork({ name: "pr-42" });
38
+ * const port = yield* preview.exposePort(3000);
39
+ * return HttpServerResponse.text(`https://${port.hostname}`);
40
+ * }),
41
+ * };
42
+ * }).pipe(Effect.provide(Archil.ConnectSandboxHttp)),
43
+ * ) {}
44
+ * ```
45
+ *
46
+ * ### Proxying to a Port
47
+ * **Example:** A Worker in front of the dev box's server
48
+ * ```typescript
49
+ * export default class Api extends Cloudflare.Worker<Api>()(
50
+ * "Api",
51
+ * { main: import.meta.url },
52
+ * Effect.gen(function* () {
53
+ * const devbox = yield* Archil.ConnectSandbox(DevBox);
54
+ * return {
55
+ * fetch: Effect.gen(function* () {
56
+ * const port = yield* devbox.getTcpPort(3000);
57
+ * return yield* port.fetch(yield* HttpServerRequest);
58
+ * }),
59
+ * };
60
+ * }).pipe(Effect.provide(Archil.ConnectSandboxHttp)),
61
+ * ) {}
62
+ * ```
63
+ *
64
+ * @binding
65
+ * @product Sandbox
66
+ */
67
+ export interface ConnectSandbox extends Binding.Service<
68
+ ConnectSandbox,
69
+ "Archil.ConnectSandbox",
70
+ (sandbox: Sandbox) => Effect.Effect<ConnectSandboxClient>
71
+ > {}
72
+
73
+ export const ConnectSandbox = Binding.Service<ConnectSandbox>("Archil.ConnectSandbox");
74
+
75
+ /** {@link ConnectSandbox} over Archil's HTTP API, usable from any host. */
76
+ export const ConnectSandboxHttp = Layer.effect(
77
+ ConnectSandbox,
78
+ Effect.gen(function* () {
79
+ const access = yield* ApiAccess;
80
+ return Effect.fn(function* (sandbox: Sandbox) {
81
+ const authorize = yield* access();
82
+ const sandboxId = yield* sandbox.sandboxId;
83
+ const name = yield* sandbox.name;
84
+ return {
85
+ sandboxId,
86
+ name,
87
+ ...makeSandboxOps(sandboxId, authorize),
88
+ } satisfies ConnectSandboxClient;
89
+ });
90
+ }),
91
+ );
@@ -0,0 +1,9 @@
1
+ import * as Contributions from "alchemy/Contributions";
2
+
3
+ export default Contributions.make({
4
+ name: "archil",
5
+ auth: Contributions.lazy(
6
+ () => import("./AuthProvider.ts"),
7
+ (module) => module.ArchilAuth,
8
+ ),
9
+ });
@@ -0,0 +1,165 @@
1
+ import * as archil from "@distilled.cloud/archil";
2
+ import * as Binding from "alchemy/Binding";
3
+ import type { RuntimeContext } from "alchemy/RuntimeContext";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import * as Stream from "effect/Stream";
7
+ import { ApiAccess, runtimeName, runtimePrefix } from "./ArchilBindings.ts";
8
+ import { makeDiskHandle, type DiskHandle } from "./Clients.ts";
9
+ import { deleteDisk, ensureDisk, type DiskStorage } from "./Disk.ts";
10
+ import type { ArchilFailed, ArchilNotReady } from "./internal/archil.ts";
11
+ import { findDiskByName, listAllDisks } from "./internal/archil.ts";
12
+
13
+ export interface CreateDiskOptions {
14
+ /**
15
+ * Where disks created by this client keep their files. Omit for
16
+ * Archil-managed storage. A run-time `create` can override it.
17
+ */
18
+ storage?: DiskStorage;
19
+ }
20
+
21
+ export type CreateDiskError =
22
+ | archil.CreateDiskError
23
+ | archil.GetDiskError
24
+ | archil.ListDisksError
25
+ | ArchilFailed
26
+ | ArchilNotReady;
27
+
28
+ export interface CreateDiskClient {
29
+ /**
30
+ * The name prefix this client puts on everything it creates,
31
+ * `alc-<id>-<hash of stack/stage/id>`.
32
+ */
33
+ readonly prefix: string;
34
+ /**
35
+ * Create a disk (or return the one that already has this name). Names
36
+ * are stamped with this binding's prefix, so `create({ name: "user-1" })`
37
+ * is the same disk on every call.
38
+ */
39
+ create(
40
+ name: string,
41
+ options?: { readonly storage?: DiskStorage },
42
+ ): Effect.Effect<DiskHandle, CreateDiskError, RuntimeContext>;
43
+ /** A disk this client created, or `undefined`. */
44
+ get(name: string): Effect.Effect<DiskHandle | undefined, archil.ListDisksError, RuntimeContext>;
45
+ /** Every disk this client created. */
46
+ list(): Effect.Effect<DiskHandle[], archil.ListDisksError, RuntimeContext>;
47
+ /** Delete a disk this client created. Deleting a missing disk succeeds. */
48
+ delete(
49
+ name: string,
50
+ ): Effect.Effect<
51
+ void,
52
+ archil.ListDisksError | Effect.Error<ReturnType<typeof deleteDisk>>,
53
+ RuntimeContext
54
+ >;
55
+ }
56
+
57
+ /**
58
+ * Create disks at run time, one per user, session, or task, by name. This
59
+ * is how most apps use Archil: a disk mounts in about 50 ms, so it can
60
+ * belong to a single request. The client returns full disk handles (exec,
61
+ * grep, objects, branches, disk tokens), the same shape the deploy-time
62
+ * bindings return.
63
+ *
64
+ * Objects created at run time are tagged with the stack, stage and binding
65
+ * (`alchemy::stack`, `alchemy::stage`, `alchemy::id`) and are not deleted
66
+ * when the stack is destroyed. List them by those tags to clean them up.
67
+ * Archil has no tags, labels or descriptions, so the tags are a name
68
+ * prefix, `alc-<id>-<hash of stack/stage/id>`: every name this client
69
+ * creates is `<prefix>-<name>`, and `prefix` on the client is the value.
70
+ *
71
+ * ### Disks per User
72
+ * **Example:** A home disk for each user
73
+ * ```typescript
74
+ * export default class Api extends Cloudflare.Worker<Api>()(
75
+ * "Api",
76
+ * { main: import.meta.url },
77
+ * Effect.gen(function* () {
78
+ * const disks = yield* Archil.CreateDisk();
79
+ * return {
80
+ * fetch: Effect.gen(function* () {
81
+ * const request = yield* HttpServerRequest;
82
+ * const user = request.headers["x-user"] ?? "anonymous";
83
+ * const home = yield* disks.create(`user-${user}`);
84
+ * const result = yield* home.exec("ls -la /mnt/archil");
85
+ * return HttpServerResponse.text(result.stdout);
86
+ * }),
87
+ * };
88
+ * }).pipe(Effect.provide(Archil.CreateDiskHttp)),
89
+ * ) {}
90
+ * ```
91
+ *
92
+ * ### Cleaning Up
93
+ * **Example:** Delete the disks one binding created
94
+ * ```typescript
95
+ * const disks = yield* Archil.CreateDisk();
96
+ * for (const disk of yield* disks.list()) {
97
+ * yield* disks.delete(disk.name.slice(disks.prefix.length + 1));
98
+ * }
99
+ * ```
100
+ *
101
+ * @binding
102
+ * @product Disk
103
+ */
104
+ export interface CreateDisk extends Binding.Service<
105
+ CreateDisk,
106
+ "Archil.CreateDisk",
107
+ (options?: CreateDiskOptions) => Effect.Effect<CreateDiskClient>
108
+ > {}
109
+
110
+ export const CreateDisk = Binding.Service<CreateDisk>("Archil.CreateDisk");
111
+
112
+ /** {@link CreateDisk} over Archil's HTTP API, usable from any host. */
113
+ export const CreateDiskHttp = Layer.effect(
114
+ CreateDisk,
115
+ Effect.gen(function* () {
116
+ const access = yield* ApiAccess;
117
+ return Effect.fn(function* (options?: CreateDiskOptions) {
118
+ const authorize = yield* access();
119
+ const { prefix: owner } = yield* runtimePrefix("Disks");
120
+ const prefix = Effect.succeed(owner);
121
+ const fullName = (name: string) => Effect.succeed(runtimeName(owner, name));
122
+ const handle = (disk: { id: string; name: string }) =>
123
+ makeDiskHandle({ diskId: disk.id, name: disk.name }, authorize);
124
+ return {
125
+ prefix: owner,
126
+ create: (name, createOptions) =>
127
+ authorize(
128
+ Effect.gen(function* () {
129
+ const disk = yield* ensureDisk(
130
+ yield* fullName(name),
131
+ createOptions?.storage ?? options?.storage,
132
+ );
133
+ return handle(disk);
134
+ }),
135
+ ),
136
+ get: (name) =>
137
+ authorize(
138
+ Effect.gen(function* () {
139
+ const diskName = yield* fullName(name);
140
+ const disk = yield* findDiskByName(diskName);
141
+ return disk && disk.status !== "deleted" ? handle(disk) : undefined;
142
+ }),
143
+ ),
144
+ list: () =>
145
+ authorize(
146
+ Effect.gen(function* () {
147
+ const p = yield* prefix;
148
+ const disks = yield* Stream.runCollect(listAllDisks());
149
+ return disks
150
+ .filter((d) => d.name.startsWith(`${p}-`) && d.status !== "deleted")
151
+ .map(handle);
152
+ }),
153
+ ),
154
+ delete: (name) =>
155
+ authorize(
156
+ Effect.gen(function* () {
157
+ const diskName = yield* fullName(name);
158
+ const disk = yield* findDiskByName(diskName);
159
+ if (disk) yield* deleteDisk(disk.id);
160
+ }),
161
+ ),
162
+ } satisfies CreateDiskClient;
163
+ });
164
+ }),
165
+ );
@@ -0,0 +1,73 @@
1
+ import type { ImageRegistry } from "@alchemy.run/docker/Registry";
2
+ import * as archil from "@distilled.cloud/archil";
3
+ import * as Binding from "alchemy/Binding";
4
+ import type { RuntimeContext } from "alchemy/RuntimeContext";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Layer from "effect/Layer";
7
+ import { ApiAccess } from "./ArchilBindings.ts";
8
+ import { pullImage, toImageAttributes, type ImageAttributes } from "./Image.ts";
9
+ import type { ArchilFailed, ArchilNotReady } from "./internal/archil.ts";
10
+
11
+ export interface CreateImageClient {
12
+ /**
13
+ * Pull an OCI reference into an Archil image and wait until it is ready.
14
+ * Takes the same `image` and `registry` props as {@link Image}.
15
+ */
16
+ build(props: {
17
+ readonly image: string;
18
+ readonly registry?: ImageRegistry;
19
+ }): Effect.Effect<
20
+ ImageAttributes,
21
+ archil.CreateImageError | archil.GetImageError | ArchilFailed | ArchilNotReady,
22
+ RuntimeContext
23
+ >;
24
+ /** An image by id, or `undefined` when it doesn't exist. */
25
+ get(
26
+ imageId: string,
27
+ ): Effect.Effect<ImageAttributes | undefined, archil.GetImageError, RuntimeContext>;
28
+ }
29
+
30
+ /**
31
+ * Build sandbox images at run time, e.g. one per customer repository, with
32
+ * the same props as the deploy-time {@link Image}. Pass the result's
33
+ * `imageId` to `CreateSandbox`.
34
+ *
35
+ * ### Building Images
36
+ * **Example:** An image per tenant
37
+ * ```typescript
38
+ * const images = yield* Archil.CreateImage();
39
+ * const image = yield* images.build({ image: `ghcr.io/acme/${tenant}:latest` });
40
+ * const sandbox = yield* sandboxes.create({ name: tenant, image: image.imageId });
41
+ * ```
42
+ *
43
+ * @binding
44
+ * @product Image
45
+ */
46
+ export interface CreateImage extends Binding.Service<
47
+ CreateImage,
48
+ "Archil.CreateImage",
49
+ () => Effect.Effect<CreateImageClient>
50
+ > {}
51
+
52
+ export const CreateImage = Binding.Service<CreateImage>("Archil.CreateImage");
53
+
54
+ /** {@link CreateImage} over Archil's HTTP API, usable from any host. */
55
+ export const CreateImageHttp = Layer.effect(
56
+ CreateImage,
57
+ Effect.gen(function* () {
58
+ const access = yield* ApiAccess;
59
+ return Effect.fn(function* () {
60
+ const authorize = yield* access();
61
+ return {
62
+ build: (props) => authorize(pullImage(props.image, props.registry, props.image)),
63
+ get: (imageId) =>
64
+ authorize(
65
+ archil.getImage({ image_id: imageId }).pipe(
66
+ Effect.map(({ data }) => toImageAttributes(data, data.source)),
67
+ Effect.catchTag("NotFound", () => Effect.undefined),
68
+ ),
69
+ ),
70
+ } satisfies CreateImageClient;
71
+ });
72
+ }),
73
+ );
@@ -0,0 +1,258 @@
1
+ import * as archil from "@distilled.cloud/archil";
2
+ import * as Binding from "alchemy/Binding";
3
+ import type { RuntimeContext } from "alchemy/RuntimeContext";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import * as Random from "effect/Random";
7
+ import { ApiAccess, runtimeName, runtimePrefix } from "./ArchilBindings.ts";
8
+ import { makeSandboxHandle, type SandboxHandle } from "./Clients.ts";
9
+ import type { Image } from "./Image.ts";
10
+ import type { ArchilFailed, ArchilNotReady } from "./internal/archil.ts";
11
+ import { findSandboxByName, listSandboxes, waitForSandbox } from "./internal/archil.ts";
12
+ import {
13
+ deleteSandbox,
14
+ toCreateSandboxRequest,
15
+ type Sandbox,
16
+ type SandboxProps,
17
+ } from "./Sandbox.ts";
18
+ import {
19
+ MountDiskUnresolved,
20
+ mountDiskId,
21
+ toSandboxMount,
22
+ type DiskRef,
23
+ type MountableDisk,
24
+ type MountOptionUnsupported,
25
+ } from "./SandboxOptions.ts";
26
+
27
+ /**
28
+ * A run-time create: a box {@link Sandbox}'s props (no program), with a
29
+ * required name. A mount's disk is a `DiskHandle` (e.g. from
30
+ * `CreateDisk`), or a `MountDisk` reference this host bound at deploy.
31
+ */
32
+ export interface CreateSandboxRequest extends Omit<
33
+ SandboxProps,
34
+ "name" | "main" | "handler" | "port" | "build" | "extraFiles" | "isExternal"
35
+ > {
36
+ /** Name, stamped with this binding's prefix. Lowercase letters, digits and `-`. */
37
+ readonly name: string;
38
+ }
39
+
40
+ export type CreateSandboxError =
41
+ | archil.CreateSandboxError
42
+ | archil.ForkSandboxError
43
+ | archil.GetSandboxError
44
+ | archil.ListSandboxesError
45
+ | ArchilFailed
46
+ | ArchilNotReady
47
+ | MountOptionUnsupported
48
+ | MountDiskUnresolved;
49
+
50
+ /**
51
+ * A mount's disk id at run time: a handle carries it; a declared disk
52
+ * reads the id `MountDisk` bound into this host at deploy.
53
+ */
54
+ const runtimeDiskId = Effect.fn(function* (disk: MountableDisk, path: string) {
55
+ if (isDiskRef(disk)) return disk.diskId;
56
+ if (Effect.isEffect(disk)) {
57
+ return yield* new MountDiskUnresolved({
58
+ path,
59
+ message: `The disk mounted at ${path} is a declaration: bind it with Archil.MountDisk(disk) and pass that reference, or pass a DiskHandle.`,
60
+ });
61
+ }
62
+ const diskId = yield* disk.diskId;
63
+ return yield* diskId;
64
+ });
65
+
66
+ const isDiskRef = (disk: MountableDisk): disk is DiskRef => mountDiskId(disk) !== undefined;
67
+
68
+ export interface CreateSandboxClient {
69
+ /**
70
+ * The name prefix this client puts on everything it creates,
71
+ * `alc-<id>-<hash of stack/stage/id>`.
72
+ */
73
+ readonly prefix: string;
74
+ /**
75
+ * Create a sandbox (or return the one that already has this name) and
76
+ * wait until it is running. Takes a {@link Sandbox}'s props; the bound
77
+ * image is the default.
78
+ */
79
+ create(
80
+ request: CreateSandboxRequest,
81
+ ): Effect.Effect<SandboxHandle, CreateSandboxError, RuntimeContext>;
82
+ /** A sandbox this client created, or `undefined`. */
83
+ get(
84
+ name: string,
85
+ ): Effect.Effect<SandboxHandle | undefined, archil.ListSandboxesError, RuntimeContext>;
86
+ /** Every sandbox this client created. */
87
+ list(): Effect.Effect<SandboxHandle[], archil.ListSandboxesError, RuntimeContext>;
88
+ /** Delete a sandbox this client created. Deleting a missing sandbox succeeds. */
89
+ delete(
90
+ name: string,
91
+ ): Effect.Effect<
92
+ void,
93
+ archil.ListSandboxesError | Effect.Error<ReturnType<typeof deleteSandbox>>,
94
+ RuntimeContext
95
+ >;
96
+ }
97
+
98
+ /**
99
+ * Create sandboxes at run time, one per user, session, or task: Firecracker
100
+ * microVMs with disks mounted, public ports, and an outbound network policy
101
+ * that can inject credentials at the boundary. Bind an {@link Image} to make
102
+ * it the default image; a run-time `create` takes the same props as a
103
+ * deploy-time {@link Sandbox}. Bind a {@link Sandbox} instead and `create`
104
+ * forks it: each sandbox starts from its memory and disk, with whatever its
105
+ * `run` commands set up, and changes only its own copy.
106
+ *
107
+ * Objects created at run time are tagged with the stack, stage and binding
108
+ * (`alchemy::stack`, `alchemy::stage`, `alchemy::id`) and are not deleted
109
+ * when the stack is destroyed. List them by those tags to clean them up.
110
+ * Archil has no tags, labels or descriptions, so the tags are a name
111
+ * prefix, `alc-<id>-<hash of stack/stage/id>`: every sandbox (and fork)
112
+ * this client creates is `<prefix>-<name>`, and `prefix` on the client is the value.
113
+ *
114
+ * ### Sandboxes per Task
115
+ * **Example:** Run a task in a sandbox with the user's disk mounted
116
+ * ```typescript
117
+ * export default class Api extends Cloudflare.Worker<Api>()(
118
+ * "Api",
119
+ * { main: import.meta.url },
120
+ * Effect.gen(function* () {
121
+ * const disks = yield* Archil.CreateDisk();
122
+ * const sandboxes = yield* Archil.CreateSandbox(Agent);
123
+ * return {
124
+ * fetch: Effect.gen(function* () {
125
+ * const home = yield* disks.create("user-1");
126
+ * const sandbox = yield* sandboxes.create({
127
+ * name: "task-1",
128
+ * mounts: [{ disk: home, path: "/home/user" }],
129
+ * network: { egress: { default: "deny", allow: ["registry.npmjs.org"] } },
130
+ * idleTtlSeconds: 300,
131
+ * });
132
+ * const result = yield* sandbox.exec("npm test", { cwd: "/home/user/app" });
133
+ * yield* sandbox.pause();
134
+ * return HttpServerResponse.text(result.stdout);
135
+ * }),
136
+ * };
137
+ * }).pipe(Effect.provide(Layer.mergeAll(Archil.CreateDiskHttp, Archil.CreateSandboxHttp))),
138
+ * ) {}
139
+ * ```
140
+ *
141
+ * ### Forking a Sandbox Built at Deploy
142
+ * **Example:** A fork per user from a sandbox set up at deploy
143
+ * ```typescript
144
+ * export const Golden = Archil.Sandbox("Golden", {
145
+ * run: ["git clone --depth 1 https://github.com/sindresorhus/is ~/is"],
146
+ * });
147
+ *
148
+ * // in a Worker or program:
149
+ * const sandboxes = yield* Archil.CreateSandbox(Golden);
150
+ * const sandbox = (yield* sandboxes.get(`user-${user}`)) ?? (yield* sandboxes.create({ name: `user-${user}` }));
151
+ * const result = yield* sandbox.exec("git log -1", { cwd: "~/is" });
152
+ * ```
153
+ *
154
+ * @binding
155
+ * @product Sandbox
156
+ */
157
+ export interface CreateSandbox extends Binding.Service<
158
+ CreateSandbox,
159
+ "Archil.CreateSandbox",
160
+ (base?: Image | Sandbox) => Effect.Effect<CreateSandboxClient>
161
+ > {}
162
+
163
+ export const CreateSandbox = Binding.Service<CreateSandbox>("Archil.CreateSandbox");
164
+
165
+ /** {@link CreateSandbox} over Archil's HTTP API, usable from any host. */
166
+ export const CreateSandboxHttp = Layer.effect(
167
+ CreateSandbox,
168
+ Effect.gen(function* () {
169
+ const access = yield* ApiAccess;
170
+ return Effect.fn(function* (base?: Image | Sandbox) {
171
+ const authorize = yield* access();
172
+ const { prefix: owner } = yield* runtimePrefix("Sandboxes");
173
+ const prefix = Effect.succeed(owner);
174
+ // Resources are proxies (every property reads as an Output), so tell a
175
+ // bound sandbox from a bound image by its type, not by `in`.
176
+ const isSandbox = (b: Image | Sandbox): b is Sandbox => b.Type === "Archil.Sandbox";
177
+ const source = base !== undefined && isSandbox(base) ? yield* base.sandboxId : undefined;
178
+ const imageId = base !== undefined && !isSandbox(base) ? yield* base.imageId : undefined;
179
+ const fullName = (name: string) => Effect.succeed(runtimeName(owner, name));
180
+ // Forks carry the same prefix as the sandboxes they come from; an
181
+ // unnamed fork gets a random name.
182
+ const forkName = Effect.fn(function* (name: string | undefined) {
183
+ const id = (yield* Random.nextIntBetween(0, 2 ** 31)).toString(36);
184
+ return yield* fullName(name ?? `fork-${id}`);
185
+ });
186
+ const handle = (s: { sandbox_id: string; name: string }) =>
187
+ makeSandboxHandle({ sandboxId: s.sandbox_id, name: s.name }, authorize, forkName);
188
+ return {
189
+ prefix: owner,
190
+ create: (request) =>
191
+ authorize(
192
+ Effect.gen(function* () {
193
+ const name = yield* fullName(request.name);
194
+ const existing = yield* findSandboxByName(name);
195
+ if (existing) return handle(existing);
196
+ if (source !== undefined) {
197
+ // A bound sandbox is the base: fork its memory and disk.
198
+ const { data } = yield* archil.forkSandbox({
199
+ sid: yield* source,
200
+ wait: true,
201
+ name,
202
+ });
203
+ return handle(yield* waitForSandbox(data.sandbox_id));
204
+ }
205
+ const defaultImage =
206
+ request.image === undefined && request.baseImage === undefined && imageId
207
+ ? yield* imageId
208
+ : request.image;
209
+ const mounts: archil.SandboxMount[] = [];
210
+ for (const mount of request.mounts ?? []) {
211
+ const diskId = yield* runtimeDiskId(mount.disk, mount.path);
212
+ mounts.push(yield* toSandboxMount(diskId, mount));
213
+ }
214
+ const { data } = yield* archil
215
+ .createSandbox(
216
+ toCreateSandboxRequest(name, { ...request, image: defaultImage }, mounts),
217
+ )
218
+ .pipe(
219
+ Effect.catchTag("SandboxNameConflict", (conflict) =>
220
+ findSandboxByName(name).pipe(
221
+ Effect.flatMap((found) =>
222
+ found ? Effect.succeed({ data: found }) : Effect.fail(conflict),
223
+ ),
224
+ ),
225
+ ),
226
+ );
227
+ return handle(yield* waitForSandbox(data.sandbox_id));
228
+ }),
229
+ ),
230
+ get: (name) =>
231
+ authorize(
232
+ Effect.gen(function* () {
233
+ const sandboxName = yield* fullName(name);
234
+ const found = yield* findSandboxByName(sandboxName);
235
+ return found ? handle(found) : undefined;
236
+ }),
237
+ ),
238
+ list: () =>
239
+ authorize(
240
+ Effect.gen(function* () {
241
+ const p = yield* prefix;
242
+ return (yield* listSandboxes())
243
+ .filter((s) => s.name.startsWith(`${p}-`) && s.status !== "deleted")
244
+ .map(handle);
245
+ }),
246
+ ),
247
+ delete: (name) =>
248
+ authorize(
249
+ Effect.gen(function* () {
250
+ const sandboxName = yield* fullName(name);
251
+ const found = yield* findSandboxByName(sandboxName);
252
+ if (found) yield* deleteSandbox(found.sandbox_id);
253
+ }),
254
+ ),
255
+ } satisfies CreateSandboxClient;
256
+ });
257
+ }),
258
+ );