Archil.Sandbox reference
ConnectSandbox
Section titled “ConnectSandbox”Source:
packages/archil/src/ConnectSandbox.ts
Act on a sandbox declared at deploy time from a Worker, Lambda, or any
other host: run commands, start background processes and terminals,
move files, pause and resume, fork it into a new sandbox, open ports,
and change its network policy. A program sandbox (Archil.Sandbox with
main) is a sandbox too.
ConnectSandbox: Driving a Dev Box
Section titled “ConnectSandbox: Driving a Dev Box”export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url }, Effect.gen(function* () { const devbox = yield* Archil.ConnectSandbox(DevBox); return { fetch: Effect.gen(function* () { yield* devbox.exec("git pull && pnpm build", { cwd: "/workspace" }); yield* devbox.files.write("/workspace/.env", "PREVIEW=1\n"); const preview = yield* devbox.fork({ name: "pr-42" }); const port = yield* preview.exposePort(3000); return HttpServerResponse.text(`https://${port.hostname}`); }), }; }).pipe(Effect.provide(Archil.ConnectSandboxHttp)),) {}ConnectSandbox: Proxying to a Port
Section titled “ConnectSandbox: Proxying to a Port”export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url }, Effect.gen(function* () { const devbox = yield* Archil.ConnectSandbox(DevBox); return { fetch: Effect.gen(function* () { const port = yield* devbox.getTcpPort(3000); return yield* port.fetch(yield* HttpServerRequest); }), }; }).pipe(Effect.provide(Archil.ConnectSandboxHttp)),) {}CreateSandbox
Section titled “CreateSandbox”Source:
packages/archil/src/CreateSandbox.ts
Create sandboxes at run time, one per user, session, or task: Firecracker
microVMs with disks mounted, public ports, and an outbound network policy
that can inject credentials at the boundary. Bind an Image to make
it the default image; a run-time create takes the same props as a
deploy-time Sandbox.
Objects created at run time are tagged with the stack, stage and binding
(alchemy::stack, alchemy::stage, alchemy::id) and are not deleted
when the stack is destroyed. List them by those tags to clean them up.
Archil has no tags, labels or descriptions, so the tags are a name
prefix, alc-<id>-<hash of stack/stage/id>: every sandbox (and fork)
this client creates is <prefix>-<name>, and prefix on the client is the value.
CreateSandbox: Sandboxes per Task
Section titled “CreateSandbox: Sandboxes per Task”export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.url }, Effect.gen(function* () { const disks = yield* Archil.CreateDisk(); const sandboxes = yield* Archil.CreateSandbox(Agent); return { fetch: Effect.gen(function* () { const home = yield* disks.create("user-1"); const sandbox = yield* sandboxes.create({ name: "task-1", mounts: [{ disk: home, path: "/home/user" }], network: { egress: { default: "deny", allow: ["registry.npmjs.org"] } }, idleTtlSeconds: 300, }); const result = yield* sandbox.exec("npm test", { cwd: "/home/user/app" }); yield* sandbox.pause(); return HttpServerResponse.text(result.stdout); }), }; }).pipe(Effect.provide(Layer.mergeAll(Archil.CreateDiskHttp, Archil.CreateSandboxHttp))),) {}Sandbox
Section titled “Sandbox”Source:
packages/archil/src/Sandbox.ts
An Archil sandbox: a Firecracker microVM with disks mounted, public ports, and an outbound network policy that can inject credentials at the boundary. It pauses when idle and resumes on the next connection, keeping its memory.
Declare one as a box (a dev box or a preview environment that outlives
deploys), or give it a main and it runs your program: Alchemy bundles
it, uploads it, and registers it with Archil’s service manager, which
restarts it when it crashes and starts it whenever the sandbox boots. Its
port gets a public https:// URL, and a request to a stopped sandbox
starts it again, typically in a few seconds. A code change uploads the
program and restarts it in place.
Short-lived sandboxes for one user or task are created at run time with
CreateSandbox; act on a declared one with ConnectSandbox.
Sandbox: Creating a Sandbox
Section titled “Sandbox: Creating a Sandbox”A dev box with a disk mounted
export const Workspace = Archil.Disk("Workspace", {});export const DevBox = Archil.Sandbox("DevBox", { baseImage: "node:24-bookworm", vcpuCount: 2, memSizeMiB: 4096, mounts: [{ disk: Workspace, path: "/workspace" }], ports: [3000], idleTtlSeconds: 600,});A bigger sandbox that never pauses while idle
export const Box = Archil.Sandbox("Box", { vcpuCount: 4, memSizeMiB: 8192, idleTtlSeconds: 0,});Boot a private image
export const AgentImage = Archil.Image("AgentImage", { image: "ghcr.io/acme/agent:1", registry: { server: "ghcr.io", username: "acme", password: Config.Redacted("GHCR_TOKEN") },});export const Agent = Effect.gen(function* () { const image = yield* AgentImage; return yield* Archil.Sandbox("Agent", { image: image.imageId });});Sandbox: Running a Program
Section titled “Sandbox: Running a Program”An Effect program that reads a mounted disk
import * as Archil from "@alchemy.run/archil";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as HttpServerResponse from "effect/http/HttpServerResponse";
export const Dataset = Archil.Disk("Dataset", {});
export default class Api extends Archil.Sandbox<Api>()( "Api", { main: import.meta.url, port: 3000 }, Effect.gen(function* () { const dataset = yield* Archil.MountDisk(Dataset, { path: "/data", mode: "read-only" }); const fs = yield* FileSystem.FileSystem; return { fetch: Effect.gen(function* () { return yield* HttpServerResponse.json(yield* fs.readDirectory(dataset.path)); }).pipe(Effect.orDie), }; }).pipe(Effect.provide(Archil.MountDiskLive)),) {}A plain async handler with typed env
export const Hello = Archil.Sandbox("Hello", { main: "./src/hello.ts", env: { GREETING: "hello" },});export type HelloEnv = Archil.InferEnv<typeof Hello>;import type { HelloEnv } from "./infra.ts";
export default { async fetch(request: Request, env: HelloEnv) { return new Response(`${env.GREETING} from ${new URL(request.url).pathname}`); },};Secrets in the program’s environment
export const Billing = Archil.Sandbox("Billing", { main: "./src/billing.ts", env: { STRIPE_KEY: Config.Redacted("STRIPE_KEY") },});Sandbox: Mounting Disks
Section titled “Sandbox: Mounting Disks”Archil sandboxes mount disks natively. Each entry names the disk and a
path, with Archil’s own options: mode (exclusive, conditional
or read-only), subdirectory, and queueMs.
Mount two disks, one read-only
export const Box = Archil.Sandbox("Box", { mounts: [ { disk: Workspace, path: "/workspace" }, { disk: Dataset, path: "/data", mode: "read-only" }, ],});Mount from the program with MountDisk
const uploads = yield* Archil.MountDisk(Uploads, { path: "/uploads", mode: "conditional" });// runtime: the disk is already mounted at uploads.pathyield* fs.writeFileString(`${uploads.path}/${id}.txt`, body);Sandbox: Mount Clients From Other Bindings
Section titled “Sandbox: Mount Clients From Other Bindings”A binding that installs a filesystem client (an alchemy/Mount
capability) into a program’s image makes Alchemy build one: the
sandbox’s base plus the client’s install lines, built with your local
Docker and pushed to push.repository for Archil to pull. A different
set of clients replaces the sandbox.
export default class Indexer extends Archil.Sandbox<Indexer>()( "Indexer", { main: import.meta.url, push: { repository: "ghcr.io/acme/indexer", credentials: { server: "ghcr.io", username: "acme-bot", password: Alchemy.Secret("GHCR_TOKEN") }, }, }, Effect.gen(function* () { const media = yield* Cloud.MountBucket(Media, { path: "/media" }); // installs a FUSE client return { fetch: Effect.succeed(HttpServerResponse.text(media.path)) }; }),) {}Sandbox: Packaging
Section titled “Sandbox: Packaging”Alchemy bundles main with rolldown into one Node program. Packages
that can’t be bundled (native modules, or ones that read their own files
at run time) go in build.install and are installed next to it with
npm install.
Install a native package
export const Thumbs = Archil.Sandbox("Thumbs", { main: "./src/thumbs.ts", build: { install: ["sharp"] },});Upload static files next to the program
export const Site = Archil.Sandbox("Site", { main: "./dist/server.mjs", extraFiles: [{ source: "./dist/client", destination: "client" }],});Sandbox: Ports
Section titled “Sandbox: Ports”export const Preview = Archil.Sandbox("Preview", { ports: [8080] });export const PreviewToken = Effect.gen(function* () { const preview = yield* Preview; return yield* Archil.SandboxPortToken("PreviewToken", { sandbox: preview.sandboxId, port: 8080, ttl: "24h", });});Sandbox: Network Policy
Section titled “Sandbox: Network Policy”export const Agent = Archil.Sandbox("Agent", { network: { egress: { default: "deny", allow: [ "registry.npmjs.org", { target: "api.github.com", headers: { Authorization: Alchemy.Secret("GITHUB_AUTH") } }, ], }, },});Sandbox: Websites
Section titled “Sandbox: Websites”const site = yield* Archil.Website.Astro("Site", { sandbox: { vcpuCount: 2 } });Sandbox: Using a Sandbox at Run Time
Section titled “Sandbox: Using a Sandbox at Run Time”const preview = yield* Archil.ConnectSandbox(Preview);const result = yield* preview.exec("pnpm build", { cwd: "/workspace" });const fork = yield* preview.fork({ name: "pr-42" });SandboxPortToken
Section titled “SandboxPortToken”Source:
packages/archil/src/SandboxPortToken.ts
A token that opens one exposed port of one sandbox, for a client that
shouldn’t hold your API key. Archil only shows the token once, so Alchemy
stores it in state as a Redacted value; destroying the resource revokes
it.
SandboxPortToken: Creating a Port Token
Section titled “SandboxPortToken: Creating a Port Token”const token = yield* Archil.SandboxPortToken("PreviewToken", { sandbox: preview.sandboxId, port: 3000, ttl: "24h",});