@alchemy.run/archil 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.
- package/LICENSE +201 -0
- package/README.md +57 -4
- package/lib/AWS/S3Disk.d.ts +68 -0
- package/lib/AWS/S3Disk.d.ts.map +1 -0
- package/lib/AWS/S3Disk.js +100 -0
- package/lib/AWS/S3Disk.js.map +1 -0
- package/lib/AWS/index.d.ts +2 -0
- package/lib/AWS/index.d.ts.map +1 -0
- package/lib/AWS/index.js +2 -0
- package/lib/AWS/index.js.map +1 -0
- package/lib/ApiToken.d.ts +63 -0
- package/lib/ApiToken.d.ts.map +1 -0
- package/lib/ApiToken.js +121 -0
- package/lib/ApiToken.js.map +1 -0
- package/lib/ArchilBindings.d.ts +65 -0
- package/lib/ArchilBindings.d.ts.map +1 -0
- package/lib/ArchilBindings.js +128 -0
- package/lib/ArchilBindings.js.map +1 -0
- package/lib/AuthProvider.d.ts +40 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +87 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Bootstrap.d.ts +8 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +27 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/Branch.d.ts +101 -0
- package/lib/Branch.d.ts.map +1 -0
- package/lib/Branch.js +117 -0
- package/lib/Branch.js.map +1 -0
- package/lib/BranchDisk.d.ts +42 -0
- package/lib/BranchDisk.d.ts.map +1 -0
- package/lib/BranchDisk.js +14 -0
- package/lib/BranchDisk.js.map +1 -0
- package/lib/Checkpoint.d.ts +78 -0
- package/lib/Checkpoint.d.ts.map +1 -0
- package/lib/Checkpoint.js +95 -0
- package/lib/Checkpoint.js.map +1 -0
- package/lib/Clients.d.ts +369 -0
- package/lib/Clients.d.ts.map +1 -0
- package/lib/Clients.js +334 -0
- package/lib/Clients.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +69 -0
- package/lib/ConnectSandbox.d.ts.map +1 -0
- package/lib/ConnectSandbox.js +21 -0
- package/lib/ConnectSandbox.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/CreateDisk.d.ts +90 -0
- package/lib/CreateDisk.d.ts.map +1 -0
- package/lib/CreateDisk.js +47 -0
- package/lib/CreateDisk.js.map +1 -0
- package/lib/CreateImage.d.ts +42 -0
- package/lib/CreateImage.d.ts.map +1 -0
- package/lib/CreateImage.js +19 -0
- package/lib/CreateImage.js.map +1 -0
- package/lib/CreateSandbox.d.ts +104 -0
- package/lib/CreateSandbox.d.ts.map +1 -0
- package/lib/CreateSandbox.js +98 -0
- package/lib/CreateSandbox.js.map +1 -0
- package/lib/Credentials.d.ts +44 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +57 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Disk.d.ts +283 -0
- package/lib/Disk.d.ts.map +1 -0
- package/lib/Disk.js +294 -0
- package/lib/Disk.js.map +1 -0
- package/lib/DiskObjectsHttp.d.ts +20 -0
- package/lib/DiskObjectsHttp.d.ts.map +1 -0
- package/lib/DiskObjectsHttp.js +51 -0
- package/lib/DiskObjectsHttp.js.map +1 -0
- package/lib/DiskUser.d.ts +189 -0
- package/lib/DiskUser.d.ts.map +1 -0
- package/lib/DiskUser.js +243 -0
- package/lib/DiskUser.js.map +1 -0
- package/lib/ExecDisk.d.ts +57 -0
- package/lib/ExecDisk.d.ts.map +1 -0
- package/lib/ExecDisk.js +3 -0
- package/lib/ExecDisk.js.map +1 -0
- package/lib/ExecDiskHttp.d.ts +5 -0
- package/lib/ExecDiskHttp.d.ts.map +1 -0
- package/lib/ExecDiskHttp.js +18 -0
- package/lib/ExecDiskHttp.js.map +1 -0
- package/lib/ExecDisks.d.ts +54 -0
- package/lib/ExecDisks.d.ts.map +1 -0
- package/lib/ExecDisks.js +37 -0
- package/lib/ExecDisks.js.map +1 -0
- package/lib/Image.d.ts +188 -0
- package/lib/Image.d.ts.map +1 -0
- package/lib/Image.js +191 -0
- package/lib/Image.js.map +1 -0
- package/lib/InferEnv.d.ts +26 -0
- package/lib/InferEnv.d.ts.map +1 -0
- package/lib/InferEnv.js +1 -0
- package/lib/InferEnv.js.map +1 -0
- package/lib/MountDisk.d.ts +190 -0
- package/lib/MountDisk.d.ts.map +1 -0
- package/lib/MountDisk.js +227 -0
- package/lib/MountDisk.js.map +1 -0
- package/lib/Providers.d.ts +25 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +48 -0
- package/lib/Providers.js.map +1 -0
- package/lib/ReadDisk.d.ts +55 -0
- package/lib/ReadDisk.d.ts.map +1 -0
- package/lib/ReadDisk.js +3 -0
- package/lib/ReadDisk.js.map +1 -0
- package/lib/ReadWriteDisk.d.ts +26 -0
- package/lib/ReadWriteDisk.d.ts.map +1 -0
- package/lib/ReadWriteDisk.js +3 -0
- package/lib/ReadWriteDisk.js.map +1 -0
- package/lib/Sandbox.d.ts +507 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +729 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxOptions.d.ts +185 -0
- package/lib/SandboxOptions.d.ts.map +1 -0
- package/lib/SandboxOptions.js +102 -0
- package/lib/SandboxOptions.js.map +1 -0
- package/lib/SandboxPortToken.d.ts +64 -0
- package/lib/SandboxPortToken.d.ts.map +1 -0
- package/lib/SandboxPortToken.js +77 -0
- package/lib/SandboxPortToken.js.map +1 -0
- package/lib/WriteDisk.d.ts +37 -0
- package/lib/WriteDisk.d.ts.map +1 -0
- package/lib/WriteDisk.js +3 -0
- package/lib/WriteDisk.js.map +1 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +28 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/DeployerKey.d.ts +21 -0
- package/lib/internal/DeployerKey.d.ts.map +1 -0
- package/lib/internal/DeployerKey.js +34 -0
- package/lib/internal/DeployerKey.js.map +1 -0
- package/lib/internal/archil.d.ts +87 -0
- package/lib/internal/archil.d.ts.map +1 -0
- package/lib/internal/archil.js +154 -0
- package/lib/internal/archil.js.map +1 -0
- package/lib/internal/hosted.d.ts +90 -0
- package/lib/internal/hosted.d.ts.map +1 -0
- package/lib/internal/hosted.js +291 -0
- package/lib/internal/hosted.js.map +1 -0
- package/package.json +79 -3
- package/src/AWS/S3Disk.ts +125 -0
- package/src/AWS/index.ts +1 -0
- package/src/ApiToken.ts +168 -0
- package/src/ArchilBindings.ts +179 -0
- package/src/AuthProvider.ts +122 -0
- package/src/Bootstrap.ts +54 -0
- package/src/Branch.ts +199 -0
- package/src/BranchDisk.ts +57 -0
- package/src/Checkpoint.ts +145 -0
- package/src/Clients.ts +954 -0
- package/src/ConnectSandbox.ts +91 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateDisk.ts +165 -0
- package/src/CreateImage.ts +73 -0
- package/src/CreateSandbox.ts +258 -0
- package/src/Credentials.ts +101 -0
- package/src/Disk.ts +468 -0
- package/src/DiskObjectsHttp.ts +64 -0
- package/src/DiskUser.ts +369 -0
- package/src/ExecDisk.ts +62 -0
- package/src/ExecDiskHttp.ts +25 -0
- package/src/ExecDisks.ts +100 -0
- package/src/Image.ts +316 -0
- package/src/InferEnv.ts +27 -0
- package/src/MountDisk.ts +466 -0
- package/src/Providers.ts +75 -0
- package/src/ReadDisk.ts +61 -0
- package/src/ReadWriteDisk.ts +31 -0
- package/src/Sandbox.ts +1064 -0
- package/src/SandboxOptions.ts +230 -0
- package/src/SandboxPortToken.ts +125 -0
- package/src/WriteDisk.ts +42 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +59 -0
- package/src/internal/DeployerKey.ts +63 -0
- package/src/internal/archil.ts +273 -0
- package/src/internal/hosted.ts +402 -0
package/src/MountDisk.ts
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { DEFAULT_REGION } from "@distilled.cloud/archil/Credentials";
|
|
2
|
+
import * as Binding from "alchemy/Binding";
|
|
3
|
+
import type { MountBinding, MountCapability } from "alchemy/Mount";
|
|
4
|
+
import type { RuntimeContext } from "alchemy/RuntimeContext";
|
|
5
|
+
import * as Data from "effect/Data";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Fiber from "effect/Fiber";
|
|
8
|
+
import * as Layer from "effect/Layer";
|
|
9
|
+
import * as Option from "effect/Option";
|
|
10
|
+
import { ChildProcess } from "effect/process";
|
|
11
|
+
import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner";
|
|
12
|
+
import * as Redacted from "effect/Redacted";
|
|
13
|
+
import * as Schedule from "effect/Schedule";
|
|
14
|
+
import type * as Scope from "effect/Scope";
|
|
15
|
+
import * as Semaphore from "effect/Semaphore";
|
|
16
|
+
import * as Stream from "effect/Stream";
|
|
17
|
+
import type { Disk } from "./Disk.ts";
|
|
18
|
+
import { DiskUser } from "./DiskUser.ts";
|
|
19
|
+
import type { DiskMount, DiskMountOptions, DiskRef } from "./SandboxOptions.ts";
|
|
20
|
+
|
|
21
|
+
/** Where a disk mounts when no `path` is given (Archil's own default). */
|
|
22
|
+
export const DEFAULT_MOUNT_PATH = "/mnt/archil";
|
|
23
|
+
|
|
24
|
+
/** A run-time mount of a disk named at call time (`MountDisk()`). */
|
|
25
|
+
export interface MountAnyOptions extends Partial<DiskMountOptions> {
|
|
26
|
+
/** The disk: a `DiskHandle`, or its id (`dsk-…`) as a job received it. */
|
|
27
|
+
readonly disk: DiskRef | string;
|
|
28
|
+
/** A disk token for that disk, e.g. from `DiskHandle.createToken`. */
|
|
29
|
+
readonly token: string | Redacted.Redacted<string>;
|
|
30
|
+
/** Region the disk lives in. @default the region bound at deploy */
|
|
31
|
+
readonly region?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `archil mount` or `archil unmount` failed. */
|
|
35
|
+
export class MountFailed extends Data.TaggedError("Archil.MountFailed")<{
|
|
36
|
+
readonly disk: string;
|
|
37
|
+
readonly path: string;
|
|
38
|
+
readonly exitCode: number;
|
|
39
|
+
readonly stderr: string;
|
|
40
|
+
}> {
|
|
41
|
+
override get message() {
|
|
42
|
+
return `archil mount ${this.disk} at ${this.path} exited ${this.exitCode}: ${this.stderr.trim()}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The host can't run the Archil client (no process spawner, e.g. a Worker). */
|
|
47
|
+
export class MountNotSupported extends Data.TaggedError("Archil.MountNotSupported")<{
|
|
48
|
+
readonly message: string;
|
|
49
|
+
}> {}
|
|
50
|
+
|
|
51
|
+
/** The path is already mounted in this process with different options. */
|
|
52
|
+
export class MountConflict extends Data.TaggedError("Archil.MountConflict")<{
|
|
53
|
+
readonly path: string;
|
|
54
|
+
readonly message: string;
|
|
55
|
+
}> {}
|
|
56
|
+
|
|
57
|
+
export type MountError = MountFailed | MountNotSupported | MountConflict;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* One deploy-time disk, mounted at `path`. The same fields as a `mounts`
|
|
61
|
+
* entry, so it can go straight into `Archil.Sandbox({ mounts })` or
|
|
62
|
+
* `sandboxes.create({ mounts })`.
|
|
63
|
+
*/
|
|
64
|
+
export interface DiskMountRef extends DiskMount {
|
|
65
|
+
readonly disk: Disk;
|
|
66
|
+
readonly path: string;
|
|
67
|
+
/**
|
|
68
|
+
* Mount the disk for the life of the current scope and return its path.
|
|
69
|
+
* In an `Archil.Sandbox` program the sandbox already mounts it, so this
|
|
70
|
+
* returns the path at once. On other hosts it runs the FUSE client;
|
|
71
|
+
* concurrent scopes that mount the same path share one mount, and the
|
|
72
|
+
* last one to close flushes and unmounts, releasing any delegations the
|
|
73
|
+
* mount held.
|
|
74
|
+
*/
|
|
75
|
+
mount(): Effect.Effect<string, MountError, RuntimeContext | Scope.Scope>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Mounts any disk the program names at run time. */
|
|
79
|
+
export interface MountAnyDiskClient {
|
|
80
|
+
/**
|
|
81
|
+
* Mount a disk with its token for the life of the current scope and
|
|
82
|
+
* return the mount path.
|
|
83
|
+
*/
|
|
84
|
+
mount(options: MountAnyOptions): Effect.Effect<string, MountError, RuntimeContext | Scope.Scope>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The Archil FUSE client, installed into a host's image. `archil mount`
|
|
89
|
+
* needs `/dev/fuse` and `CAP_SYS_ADMIN`.
|
|
90
|
+
*/
|
|
91
|
+
export const archilClientCapability: MountCapability = {
|
|
92
|
+
id: "archil",
|
|
93
|
+
install: [
|
|
94
|
+
"RUN (command -v apt-get >/dev/null && apt-get update && apt-get install -y --no-install-recommends ca-certificates curl fuse3 && rm -rf /var/lib/apt/lists/*) || (command -v apk >/dev/null && apk add --no-cache ca-certificates curl fuse3)",
|
|
95
|
+
"RUN curl -fsSL https://archil.com/install | sh",
|
|
96
|
+
],
|
|
97
|
+
requires: { fuse: true },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Bind the Archil client capability onto the current host. A mount is a
|
|
102
|
+
* host-only attachment (an image layer and a kernel feature), so this is
|
|
103
|
+
* the one place Archil binds directly onto its host.
|
|
104
|
+
*/
|
|
105
|
+
const bindClientCapability = Effect.gen(function* () {
|
|
106
|
+
if (globalThis.__ALCHEMY_RUNTIME__) return;
|
|
107
|
+
const host = yield* Binding.Host;
|
|
108
|
+
if (!host) return;
|
|
109
|
+
// `Binding.Host` is typed as a bare `ResourceLike` (no `bind`): a host's
|
|
110
|
+
// binding contract is only known per host type, and `MountBinding` is the
|
|
111
|
+
// slice every mount-capable host accepts.
|
|
112
|
+
yield* (
|
|
113
|
+
host as unknown as { bind: (sid: string, data: MountBinding) => Effect.Effect<void> }
|
|
114
|
+
).bind("Archil.MountDisk", { mounts: [archilClientCapability] });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const modeFlags = (mode: DiskMountOptions["mode"]) =>
|
|
118
|
+
mode === "shared"
|
|
119
|
+
? ["--shared"]
|
|
120
|
+
: mode === "conditional"
|
|
121
|
+
? ["--conditional"]
|
|
122
|
+
: mode === "read-only"
|
|
123
|
+
? ["--read-only"]
|
|
124
|
+
: [];
|
|
125
|
+
|
|
126
|
+
const run = (
|
|
127
|
+
spawner: ChildProcessSpawner["Service"],
|
|
128
|
+
command: string,
|
|
129
|
+
args: ReadonlyArray<string>,
|
|
130
|
+
env: Record<string, string | Redacted.Redacted<string>> = {},
|
|
131
|
+
) =>
|
|
132
|
+
Effect.gen(function* () {
|
|
133
|
+
const handle = yield* ChildProcess.make(command, args, {
|
|
134
|
+
env,
|
|
135
|
+
extendEnv: true,
|
|
136
|
+
shell: false,
|
|
137
|
+
// A client stuck on the network may ignore SIGTERM; don't wait on it.
|
|
138
|
+
forceKillAfter: "3 seconds",
|
|
139
|
+
});
|
|
140
|
+
let stderr = "";
|
|
141
|
+
const reader = yield* handle.stderr.pipe(
|
|
142
|
+
Stream.decodeText,
|
|
143
|
+
Stream.runForEach((chunk) => Effect.sync(() => void (stderr += chunk))),
|
|
144
|
+
Effect.forkScoped,
|
|
145
|
+
);
|
|
146
|
+
// A client that never finishes attaching (no FUSE, no route to Archil)
|
|
147
|
+
// is killed, and reported with what it printed so far.
|
|
148
|
+
const exitCode = yield* handle.exitCode.pipe(
|
|
149
|
+
Effect.timeoutOption("60 seconds"),
|
|
150
|
+
Effect.flatMap((code) =>
|
|
151
|
+
Option.isSome(code)
|
|
152
|
+
? Effect.succeed(Number(code.value))
|
|
153
|
+
: handle.kill().pipe(
|
|
154
|
+
Effect.ignore,
|
|
155
|
+
Effect.as(124),
|
|
156
|
+
Effect.tap(() =>
|
|
157
|
+
Effect.sync(
|
|
158
|
+
() =>
|
|
159
|
+
void (stderr +=
|
|
160
|
+
"\n(timed out after 60s: check that the host can reach Archil; a Cloudflare Container needs `start({ enableInternet: true })`)"),
|
|
161
|
+
),
|
|
162
|
+
),
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
// Let the stderr reader drain what's left.
|
|
167
|
+
yield* Fiber.join(reader).pipe(Effect.timeoutOption("2 seconds"), Effect.ignore);
|
|
168
|
+
return { exitCode, stderr };
|
|
169
|
+
}).pipe(
|
|
170
|
+
Effect.scoped,
|
|
171
|
+
Effect.provideService(ChildProcessSpawner, spawner),
|
|
172
|
+
// The binary is missing or can't start: report it like a failed command.
|
|
173
|
+
Effect.catch((cause) => Effect.succeed({ exitCode: 127, stderr: String(cause) })),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
/** The mounts this process holds, by path. */
|
|
177
|
+
interface Mounts {
|
|
178
|
+
readonly lock: Semaphore.Semaphore;
|
|
179
|
+
readonly byPath: Map<string, { readonly key: string; users: number }>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const makeMounts = Effect.gen(function* () {
|
|
183
|
+
return {
|
|
184
|
+
lock: yield* Semaphore.make(1),
|
|
185
|
+
byPath: new Map(),
|
|
186
|
+
} satisfies Mounts;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// `archil unmount` returns before the client's daemon exits, and a mount at
|
|
190
|
+
// the same path fails until it has.
|
|
191
|
+
const isStillUnmounting = (e: MountError) =>
|
|
192
|
+
e._tag === "Archil.MountFailed" && e.stderr.includes("older Archil process is still running");
|
|
193
|
+
|
|
194
|
+
/** Mount a disk for the life of the ambient scope. */
|
|
195
|
+
const mountFor = (
|
|
196
|
+
spawner: Option.Option<ChildProcessSpawner["Service"]>,
|
|
197
|
+
mounts: Mounts,
|
|
198
|
+
input: {
|
|
199
|
+
readonly disk: string;
|
|
200
|
+
readonly token: Redacted.Redacted<string> | undefined;
|
|
201
|
+
readonly region: string;
|
|
202
|
+
readonly options: Partial<DiskMountOptions>;
|
|
203
|
+
},
|
|
204
|
+
) =>
|
|
205
|
+
Effect.gen(function* () {
|
|
206
|
+
if (Option.isNone(spawner)) {
|
|
207
|
+
return yield* new MountNotSupported({
|
|
208
|
+
message:
|
|
209
|
+
"Archil.MountDisk needs a host that can run processes (an Archil sandbox, a Container, Fly Machine, ECS task, or VM).",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const path = input.options.path ?? DEFAULT_MOUNT_PATH;
|
|
213
|
+
const target = input.options.subdirectory
|
|
214
|
+
? `${input.disk}:/${input.options.subdirectory.replace(/^\//, "")}`
|
|
215
|
+
: input.disk;
|
|
216
|
+
const args = [
|
|
217
|
+
"mount",
|
|
218
|
+
target,
|
|
219
|
+
path,
|
|
220
|
+
"--region",
|
|
221
|
+
input.region,
|
|
222
|
+
...modeFlags(input.options.mode),
|
|
223
|
+
...(input.options.branch ? ["--branch", input.options.branch] : []),
|
|
224
|
+
...(input.options.force ? ["--force"] : []),
|
|
225
|
+
];
|
|
226
|
+
const key = args.join(" ");
|
|
227
|
+
const env: Record<string, string | Redacted.Redacted<string>> = input.token
|
|
228
|
+
? { ARCHIL_MOUNT_TOKEN: input.token }
|
|
229
|
+
: {};
|
|
230
|
+
const mount = Effect.gen(function* () {
|
|
231
|
+
const result = yield* run(spawner.value, "archil", args, env);
|
|
232
|
+
if (result.exitCode !== 0) {
|
|
233
|
+
return yield* new MountFailed({ disk: input.disk, path, ...result });
|
|
234
|
+
}
|
|
235
|
+
}).pipe(
|
|
236
|
+
Effect.retry({
|
|
237
|
+
while: isStillUnmounting,
|
|
238
|
+
schedule: Schedule.spaced("1 second"),
|
|
239
|
+
times: 15,
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
yield* Effect.acquireRelease(
|
|
243
|
+
Effect.gen(function* () {
|
|
244
|
+
const held = mounts.byPath.get(path);
|
|
245
|
+
if (held !== undefined) {
|
|
246
|
+
if (held.key !== key) {
|
|
247
|
+
return yield* new MountConflict({
|
|
248
|
+
path,
|
|
249
|
+
message: `${path} is already mounted with \`archil ${held.key}\``,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
held.users++;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
yield* run(spawner.value, "mkdir", ["-p", path]);
|
|
256
|
+
yield* mount;
|
|
257
|
+
mounts.byPath.set(path, { key, users: 1 });
|
|
258
|
+
}).pipe(mounts.lock.withPermits(1)),
|
|
259
|
+
() =>
|
|
260
|
+
Effect.gen(function* () {
|
|
261
|
+
const held = mounts.byPath.get(path);
|
|
262
|
+
if (held === undefined || --held.users > 0) return;
|
|
263
|
+
mounts.byPath.delete(path);
|
|
264
|
+
yield* run(spawner.value, "archil", ["unmount", path], env);
|
|
265
|
+
}).pipe(mounts.lock.withPermits(1)),
|
|
266
|
+
);
|
|
267
|
+
return path;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
/** How `MountDisk(disk)` mounts its disk: a `mounts` entry's options. */
|
|
271
|
+
export interface MountDiskOptions extends Partial<DiskMountOptions> {}
|
|
272
|
+
|
|
273
|
+
/** Options for `MountDisk()`, which mounts disks named at run time. */
|
|
274
|
+
export interface MountAnyDiskOptions {
|
|
275
|
+
/**
|
|
276
|
+
* Region of the disks the program mounts.
|
|
277
|
+
* @default "aws-us-east-1"
|
|
278
|
+
*/
|
|
279
|
+
region?: string;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Mount an Archil disk where your program runs. `MountDisk(disk, options)`
|
|
284
|
+
* returns a mount reference: the fields of a `mounts` entry (`disk`,
|
|
285
|
+
* `path`, `mode`, …) plus `mount()`.
|
|
286
|
+
*
|
|
287
|
+
* - In an `Archil.Sandbox` program, the sandbox mounts the disk natively:
|
|
288
|
+
* the binding adds the entry to the sandbox's `mounts`, and `mount()`
|
|
289
|
+
* returns the path at once.
|
|
290
|
+
* - On your own machines (a Cloudflare Container, an ECS task, a Fly
|
|
291
|
+
* Machine, or a VM), the binding installs the Archil client into the
|
|
292
|
+
* host's image and checks at plan time that the host provides FUSE.
|
|
293
|
+
* `mount()` runs the client for the life of the scope that called it.
|
|
294
|
+
* The host gets its own disk user (read-only for `mode: "read-only"`),
|
|
295
|
+
* so Archil refuses it any other disk and it never holds an API key.
|
|
296
|
+
*
|
|
297
|
+
* `MountDisk()` mounts whatever disk the program names, with a disk token
|
|
298
|
+
* it receives at run time (e.g. from a control Worker's
|
|
299
|
+
* `DiskHandle.createToken`).
|
|
300
|
+
*
|
|
301
|
+
* The client reaches Archil over the network, so start a Cloudflare
|
|
302
|
+
* Container with `container.start({ enableInternet: true })`.
|
|
303
|
+
*
|
|
304
|
+
* ### Mounting in a Sandbox Program
|
|
305
|
+
* **Example:** Serve a mounted disk from an Archil sandbox
|
|
306
|
+
* ```typescript
|
|
307
|
+
* export default class Api extends Archil.Sandbox<Api>()(
|
|
308
|
+
* "Api",
|
|
309
|
+
* { main: import.meta.url },
|
|
310
|
+
* Effect.gen(function* () {
|
|
311
|
+
* const dataset = yield* Archil.MountDisk(Dataset, { path: "/data", mode: "read-only" });
|
|
312
|
+
* const fs = yield* FileSystem.FileSystem;
|
|
313
|
+
* return {
|
|
314
|
+
* fetch: Effect.gen(function* () {
|
|
315
|
+
* return yield* HttpServerResponse.json(yield* fs.readDirectory(dataset.path));
|
|
316
|
+
* }).pipe(Effect.orDie),
|
|
317
|
+
* };
|
|
318
|
+
* }).pipe(Effect.provide(Archil.MountDiskLive)),
|
|
319
|
+
* ) {}
|
|
320
|
+
* ```
|
|
321
|
+
*
|
|
322
|
+
* ### Mounting on Your Own Hosts
|
|
323
|
+
* **Example:** Read a dataset in a Cloudflare Container
|
|
324
|
+
* ```typescript
|
|
325
|
+
* export default class Indexer extends Cloudflare.Container<Indexer>()(
|
|
326
|
+
* "Indexer",
|
|
327
|
+
* { main: import.meta.url },
|
|
328
|
+
* Effect.gen(function* () {
|
|
329
|
+
* const dataset = yield* Archil.MountDisk(Dataset, { path: "/data", mode: "read-only" });
|
|
330
|
+
* const fs = yield* FileSystem.FileSystem;
|
|
331
|
+
* return {
|
|
332
|
+
* fetch: Effect.scoped(
|
|
333
|
+
* Effect.gen(function* () {
|
|
334
|
+
* const path = yield* dataset.mount();
|
|
335
|
+
* return yield* HttpServerResponse.json(yield* fs.readDirectory(path));
|
|
336
|
+
* }),
|
|
337
|
+
* ),
|
|
338
|
+
* };
|
|
339
|
+
* }).pipe(Effect.provide(Archil.MountDiskLive)),
|
|
340
|
+
* ) {}
|
|
341
|
+
* ```
|
|
342
|
+
*
|
|
343
|
+
* **Example:** Write through a mount in an ECS task on EC2
|
|
344
|
+
* ```typescript
|
|
345
|
+
* const scratch = yield* Archil.MountDisk(Scratch, { path: "/scratch" });
|
|
346
|
+
* // runtime
|
|
347
|
+
* const path = yield* scratch.mount();
|
|
348
|
+
* yield* fs.writeFileString(`${path}/result.json`, JSON.stringify(result));
|
|
349
|
+
* ```
|
|
350
|
+
*
|
|
351
|
+
* ### Passing a Mount to a Sandbox
|
|
352
|
+
* **Example:** Mount the same disk into a sandbox created per task
|
|
353
|
+
* ```typescript
|
|
354
|
+
* const workspace = yield* Archil.MountDisk(Workspace, { path: "/workspace", mode: "conditional" });
|
|
355
|
+
* const sandboxes = yield* Archil.CreateSandbox();
|
|
356
|
+
* // runtime
|
|
357
|
+
* const sandbox = yield* sandboxes.create({ name: `task-${id}`, mounts: [workspace] });
|
|
358
|
+
* ```
|
|
359
|
+
*
|
|
360
|
+
* ### Mounting Disks Named at Run Time
|
|
361
|
+
* **Example:** Mount the disk a job names, with its token
|
|
362
|
+
* ```typescript
|
|
363
|
+
* const archil = yield* Archil.MountDisk();
|
|
364
|
+
* const path = yield* archil.mount({ disk: job.diskId, token: job.token, path: "/work" });
|
|
365
|
+
* ```
|
|
366
|
+
*
|
|
367
|
+
* @binding
|
|
368
|
+
* @product Disk
|
|
369
|
+
*/
|
|
370
|
+
export interface MountDisk extends Binding.Service<
|
|
371
|
+
MountDisk,
|
|
372
|
+
"Archil.MountDisk",
|
|
373
|
+
{
|
|
374
|
+
(disk: Disk, options?: MountDiskOptions): Effect.Effect<DiskMountRef>;
|
|
375
|
+
(disk?: undefined, options?: MountAnyDiskOptions): Effect.Effect<MountAnyDiskClient>;
|
|
376
|
+
} & ((
|
|
377
|
+
disk?: Disk,
|
|
378
|
+
options?: MountDiskOptions & MountAnyDiskOptions,
|
|
379
|
+
) => Effect.Effect<DiskMountRef & MountAnyDiskClient>)
|
|
380
|
+
> {}
|
|
381
|
+
|
|
382
|
+
export const MountDisk = Binding.Service<MountDisk>("Archil.MountDisk");
|
|
383
|
+
|
|
384
|
+
type BindableHost = {
|
|
385
|
+
readonly Type: string;
|
|
386
|
+
readonly LogicalId: string;
|
|
387
|
+
bind: (sid: string, data: object) => Effect.Effect<void>;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
/** {@link MountDisk} over native sandbox mounts and the Archil FUSE client. */
|
|
391
|
+
export const MountDiskLive = Layer.effect(
|
|
392
|
+
MountDisk,
|
|
393
|
+
Effect.gen(function* () {
|
|
394
|
+
const Users = yield* DiskUser;
|
|
395
|
+
const spawner = yield* Effect.serviceOption(ChildProcessSpawner);
|
|
396
|
+
const mounts = yield* makeMounts;
|
|
397
|
+
return Effect.fn(function* (disk?: Disk, options?: MountDiskOptions & MountAnyDiskOptions) {
|
|
398
|
+
// `Binding.Host` is typed as a bare `ResourceLike` (no `bind`): a
|
|
399
|
+
// host's binding contract is only known per host type.
|
|
400
|
+
const host = (yield* Binding.Host) as BindableHost | undefined;
|
|
401
|
+
if (disk === undefined) {
|
|
402
|
+
yield* bindClientCapability;
|
|
403
|
+
const client: MountAnyDiskClient = {
|
|
404
|
+
mount: (input) =>
|
|
405
|
+
mountFor(spawner, mounts, {
|
|
406
|
+
disk: typeof input.disk === "string" ? input.disk : input.disk.diskId,
|
|
407
|
+
token: typeof input.token === "string" ? Redacted.make(input.token) : input.token,
|
|
408
|
+
region: input.region ?? options?.region ?? DEFAULT_REGION,
|
|
409
|
+
options: input,
|
|
410
|
+
}),
|
|
411
|
+
};
|
|
412
|
+
return client as DiskMountRef & MountAnyDiskClient;
|
|
413
|
+
}
|
|
414
|
+
const entry = {
|
|
415
|
+
disk,
|
|
416
|
+
path: options?.path ?? DEFAULT_MOUNT_PATH,
|
|
417
|
+
mode: options?.mode,
|
|
418
|
+
subdirectory: options?.subdirectory,
|
|
419
|
+
queueMs: options?.queueMs,
|
|
420
|
+
branch: options?.branch,
|
|
421
|
+
force: options?.force,
|
|
422
|
+
} satisfies DiskMount;
|
|
423
|
+
if (host?.Type === "Archil.Sandbox") {
|
|
424
|
+
// The sandbox mounts the disk itself: add it to the sandbox's mounts.
|
|
425
|
+
if (!globalThis.__ALCHEMY_RUNTIME__) {
|
|
426
|
+
yield* host.bind(`Archil.MountDisk(${disk.LogicalId})`, {
|
|
427
|
+
mounts: [
|
|
428
|
+
{
|
|
429
|
+
diskId: disk.diskId,
|
|
430
|
+
path: entry.path,
|
|
431
|
+
mode: entry.mode,
|
|
432
|
+
subdirectory: entry.subdirectory,
|
|
433
|
+
queueMs: entry.queueMs,
|
|
434
|
+
branch: entry.branch,
|
|
435
|
+
force: entry.force,
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const ref: DiskMountRef = { ...entry, mount: () => Effect.succeed(entry.path) };
|
|
441
|
+
return ref as DiskMountRef & MountAnyDiskClient;
|
|
442
|
+
}
|
|
443
|
+
yield* bindClientCapability;
|
|
444
|
+
const user = yield* Users(`${host?.LogicalId ?? "Script"}${disk.LogicalId}DiskUser`, {
|
|
445
|
+
disk: disk.diskId,
|
|
446
|
+
readOnly: options?.mode === "read-only" ? true : undefined,
|
|
447
|
+
});
|
|
448
|
+
const token = yield* user.token;
|
|
449
|
+
const diskId = yield* disk.diskId;
|
|
450
|
+
const region = yield* disk.region;
|
|
451
|
+
const ref: DiskMountRef = {
|
|
452
|
+
...entry,
|
|
453
|
+
mount: () =>
|
|
454
|
+
Effect.gen(function* () {
|
|
455
|
+
return yield* mountFor(spawner, mounts, {
|
|
456
|
+
disk: yield* diskId,
|
|
457
|
+
token: yield* token,
|
|
458
|
+
region: yield* region,
|
|
459
|
+
options: entry,
|
|
460
|
+
});
|
|
461
|
+
}),
|
|
462
|
+
};
|
|
463
|
+
return ref as DiskMountRef & MountAnyDiskClient;
|
|
464
|
+
});
|
|
465
|
+
}),
|
|
466
|
+
);
|
package/src/Providers.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { DockerLive } from "@alchemy.run/docker/Docker";
|
|
2
|
+
import { CredentialsStoreLive } from "alchemy/Auth/Credentials";
|
|
3
|
+
import { ProfileStoreLive } from "alchemy/Auth/Profile";
|
|
4
|
+
import * as Command from "alchemy/Command";
|
|
5
|
+
import * as Provider from "alchemy/Provider";
|
|
6
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
7
|
+
import * as Layer from "effect/Layer";
|
|
8
|
+
import { ApiToken, ApiTokenProvider } from "./ApiToken.ts";
|
|
9
|
+
import { ArchilAuth } from "./AuthProvider.ts";
|
|
10
|
+
import { Branch, BranchProvider } from "./Branch.ts";
|
|
11
|
+
import { Checkpoint, CheckpointProvider } from "./Checkpoint.ts";
|
|
12
|
+
import * as Credentials from "./Credentials.ts";
|
|
13
|
+
import { Disk, DiskProvider } from "./Disk.ts";
|
|
14
|
+
import { DiskUser, DiskUserProvider } from "./DiskUser.ts";
|
|
15
|
+
import { Image, ImageProvider } from "./Image.ts";
|
|
16
|
+
import { DeployerKey, DeployerKeyProvider } from "./internal/DeployerKey.ts";
|
|
17
|
+
import { Sandbox, SandboxProvider } from "./Sandbox.ts";
|
|
18
|
+
import { SandboxPortToken, SandboxPortTokenProvider } from "./SandboxPortToken.ts";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Service tag bundling all Archil providers + auth + credentials. Use
|
|
22
|
+
* `Archil.providers()` to materialize the full layer.
|
|
23
|
+
*/
|
|
24
|
+
export class Providers extends Provider.ProviderCollection<Providers>()("Archil") {}
|
|
25
|
+
|
|
26
|
+
export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the complete Archil providers Layer: every Archil resource, the
|
|
30
|
+
* Archil AuthProvider registration (so `alchemy profile edit` can configure
|
|
31
|
+
* it), and credential resolution.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* Alchemy.Stack("App", { providers: Archil.providers() }, ...)
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export const providers = () =>
|
|
39
|
+
Layer.effect(
|
|
40
|
+
Providers,
|
|
41
|
+
Provider.collection([
|
|
42
|
+
ApiToken,
|
|
43
|
+
Branch,
|
|
44
|
+
Checkpoint,
|
|
45
|
+
DeployerKey,
|
|
46
|
+
Disk,
|
|
47
|
+
DiskUser,
|
|
48
|
+
Image,
|
|
49
|
+
Sandbox,
|
|
50
|
+
SandboxPortToken,
|
|
51
|
+
]),
|
|
52
|
+
).pipe(
|
|
53
|
+
Layer.provide(
|
|
54
|
+
Layer.mergeAll(
|
|
55
|
+
ApiTokenProvider(),
|
|
56
|
+
BranchProvider(),
|
|
57
|
+
CheckpointProvider(),
|
|
58
|
+
DeployerKeyProvider(),
|
|
59
|
+
DiskProvider(),
|
|
60
|
+
DiskUserProvider(),
|
|
61
|
+
ImageProvider(),
|
|
62
|
+
SandboxProvider(),
|
|
63
|
+
SandboxPortTokenProvider(),
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
Layer.provideMerge(Command.providers()),
|
|
67
|
+
// `BuildPlan.push` builds Dockerfile images locally for Archil to pull.
|
|
68
|
+
Layer.provideMerge(DockerLive),
|
|
69
|
+
Layer.provideMerge(Credentials.fromAuthProvider()),
|
|
70
|
+
Layer.provideMerge(FetchHttpClient.layer),
|
|
71
|
+
Layer.provideMerge(ArchilAuth),
|
|
72
|
+
Layer.provideMerge(ProfileStoreLive),
|
|
73
|
+
Layer.provideMerge(CredentialsStoreLive),
|
|
74
|
+
Layer.orDie,
|
|
75
|
+
);
|
package/src/ReadDisk.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as Binding from "alchemy/Binding";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
import type { ReadDiskClient } from "./Clients.ts";
|
|
4
|
+
import type { Disk } from "./Disk.ts";
|
|
5
|
+
|
|
6
|
+
export type { ReadDiskClient } from "./Clients.ts";
|
|
7
|
+
|
|
8
|
+
/** Options for the object bindings. */
|
|
9
|
+
export interface DiskObjectsOptions {
|
|
10
|
+
/** Read or write a branch of the disk instead of its root. */
|
|
11
|
+
branch?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read a disk's files as objects over Archil's S3-compatible API, with no
|
|
16
|
+
* machine and no mount: get, stream, head, list, and presigned share URLs.
|
|
17
|
+
* Reads see what mounted clients have written.
|
|
18
|
+
*
|
|
19
|
+
* The host gets its own read-only S3 user on this disk: its keys can only
|
|
20
|
+
* `GET` and `HEAD` this disk's files, and the host never holds an API key.
|
|
21
|
+
* Destroying the stack revokes the user.
|
|
22
|
+
*
|
|
23
|
+
* ### Reading Files
|
|
24
|
+
* **Example:** Serve files from a disk in a Worker
|
|
25
|
+
* ```typescript
|
|
26
|
+
* export default class Api extends Cloudflare.Worker<Api>()(
|
|
27
|
+
* "Api",
|
|
28
|
+
* { main: import.meta.url },
|
|
29
|
+
* Effect.gen(function* () {
|
|
30
|
+
* const files = yield* Archil.ReadDisk(Dataset);
|
|
31
|
+
* return {
|
|
32
|
+
* fetch: Effect.gen(function* () {
|
|
33
|
+
* const readme = yield* files.getText("README.md");
|
|
34
|
+
* return HttpServerResponse.text(readme);
|
|
35
|
+
* }),
|
|
36
|
+
* };
|
|
37
|
+
* }).pipe(Effect.provide(Archil.ReadDiskHttp)),
|
|
38
|
+
* ) {}
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* **Example:** Stream a large file to the response
|
|
42
|
+
* ```typescript
|
|
43
|
+
* return HttpServerResponse.stream(files.getStream("exports/data.parquet"));
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* ### Sharing Files
|
|
47
|
+
* **Example:** A one-hour download link
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const url = yield* files.share("reports/2026-11.pdf", 3600);
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @binding
|
|
53
|
+
* @product Disk
|
|
54
|
+
*/
|
|
55
|
+
export interface ReadDisk extends Binding.Service<
|
|
56
|
+
ReadDisk,
|
|
57
|
+
"Archil.ReadDisk",
|
|
58
|
+
(disk: Disk, options?: DiskObjectsOptions) => Effect.Effect<ReadDiskClient>
|
|
59
|
+
> {}
|
|
60
|
+
|
|
61
|
+
export const ReadDisk = Binding.Service<ReadDisk>("Archil.ReadDisk");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Binding from "alchemy/Binding";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
import type { ReadWriteDiskClient } from "./Clients.ts";
|
|
4
|
+
import type { Disk } from "./Disk.ts";
|
|
5
|
+
import type { DiskObjectsOptions } from "./ReadDisk.ts";
|
|
6
|
+
|
|
7
|
+
export type { ReadWriteDiskClient } from "./Clients.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read and write a disk's files as objects: everything {@link ReadDisk}
|
|
11
|
+
* and {@link WriteDisk} do, in one client. The host gets its own
|
|
12
|
+
* read-write S3 user on this disk, and never holds an API key.
|
|
13
|
+
*
|
|
14
|
+
* ### Reading and Writing
|
|
15
|
+
* **Example:** Upload, then hand out a link
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const files = yield* Archil.ReadWriteDisk(Dataset);
|
|
18
|
+
* yield* files.put("uploads/photo.jpg", bytes, { contentType: "image/jpeg" });
|
|
19
|
+
* const url = yield* files.share("uploads/photo.jpg", 600);
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @binding
|
|
23
|
+
* @product Disk
|
|
24
|
+
*/
|
|
25
|
+
export interface ReadWriteDisk extends Binding.Service<
|
|
26
|
+
ReadWriteDisk,
|
|
27
|
+
"Archil.ReadWriteDisk",
|
|
28
|
+
(disk: Disk, options?: DiskObjectsOptions) => Effect.Effect<ReadWriteDiskClient>
|
|
29
|
+
> {}
|
|
30
|
+
|
|
31
|
+
export const ReadWriteDisk = Binding.Service<ReadWriteDisk>("Archil.ReadWriteDisk");
|