Skip to content

Mounting

Archil.MountDisk(disk, { path, mode }) returns a mount reference: the fields of a sandbox mounts entry, plus mount(). Where it mounts depends on the host it’s bound to.

An Archil.Sandbox with main mounts the disk itself. The binding adds it to the sandbox’s mounts, and the disk is there when the program starts:

const dataset = yield* Archil.MountDisk(Dataset, { path: "/data", mode: "read-only" });
const names = yield* fs.readDirectory(dataset.path);

On a Cloudflare Container or an ECS task on EC2, the binding installs the Archil client into the host’s image, and the plan fails if the host can’t provide FUSE. mount() runs the client:

export default class Indexer extends Cloudflare.Container<Indexer>()(
"Indexer",
{ main: import.meta.url },
Effect.gen(function* () {
const dataset = yield* Archil.MountDisk(Dataset, { path: "/data", mode: "read-only" });
const fs = yield* FileSystem.FileSystem;
return {
fetch: Effect.scoped(
Effect.gen(function* () {
const path = yield* dataset.mount();
const entries = yield* fs.readDirectory(path);
return yield* HttpServerResponse.json(entries);
}),
),
};
}).pipe(Effect.provide(Archil.MountDiskLive)),
) {}

The mount lasts as long as its scope. Closing the scope flushes writes and unmounts.

The host gets a disk token for Dataset only, so it can’t mount any other disk and never holds an API key. With mode: "read-only" the token is read-only too.

The Archil client talks to Archil over the network, and a Cloudflare Container has no outbound network unless the Durable Object that starts it asks for one:

export class IndexerObject extends Cloudflare.DurableObject<IndexerObject>()(
"IndexerObject",
Effect.gen(function* () {
const indexer = yield* Indexer;
return Effect.gen(function* () {
const { fetch } = yield* indexer.getTcpPort(3000);
return {
list: () =>
Effect.gen(function* () {
yield* indexer.start({ enableInternet: true });
const response = yield* fetch(HttpClientRequest.get("http://container/"));
return yield* response.text;
}),
};
});
}),
) {}

Without enableInternet: true, mount fails after 60 seconds with the client’s output.

MountDisk() takes the disk and its token per call, e.g. from a Worker that created the disk:

const archil = yield* Archil.MountDisk();
const path = yield* archil.mount({ disk: job.diskId, token: job.token, path: "/work" });

The Worker makes the token with home.createToken(name) and revokes it with home.removeToken(id).

mode picks how this mount shares the disk. An exclusive mount, the default, locks the whole disk for one writer, and every other client can only mount it read-only:

DatasetdiskwritesreadsIndexerexclusiveJobexclusiveApiread-only/locked by Indexer
The Indexer holds the disk's write lock, so the Api only reads it and the Job can't write until the Indexer unmounts. Mount both writers conditional and both can write.
const dataset = yield* Archil.MountDisk(Dataset, { path: "/data", mode: "shared" });

exclusive (the default) owns writes; shared lets many clients mount and write only paths they check out; conditional lets many clients write anywhere; read-only never writes. Mount a branch with branch: "session-42". shared, branch, and force are FUSE-client options: a sandbox mounts with exclusive, conditional, or read-only, and takes queueMs.

A mount reference has the same fields as a sandbox mounts entry, so it goes straight into a sandbox created at run time:

const workspace = yield* Archil.MountDisk(Workspace, { path: "/workspace", mode: "conditional" });
const sandboxes = yield* Archil.CreateSandbox();
const sandbox = yield* sandboxes.create({ name: `task-${id}`, mounts: [workspace] });

ECS tasks need the EC2 launch type, since Fargate has no FUSE:

export default class Reader extends AWS.ECS.Task<Reader>()(
"Reader",
{ main: import.meta.url, port: 3000, requiresCompatibilities: ["EC2"], networkMode: "bridge" },
Effect.gen(function* () {
const dataset = yield* Archil.MountDisk(Dataset, { path: "/data", mode: "read-only" });
const fs = yield* FileSystem.FileSystem;
return {
fetch: Effect.scoped(
Effect.gen(function* () {
const path = yield* dataset.mount();
const entries = yield* fs.readDirectory(path);
return yield* HttpServerResponse.json(entries);
}),
),
};
}).pipe(Effect.provide(Archil.MountDiskLive)),
) {}

Alchemy adds /dev/fuse and SYS_ADMIN to the task’s container.