Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
514ec50
feat(docker): BuildPlan lowers image sources for builders that are no…
sam-goodwin Nov 2, 2026
2e30c22
Merge remote-tracking branch 'origin/main' into feat/docker-build-plan
sam-goodwin Nov 4, 2026
070a4df
feat(core): shared Mount, Sandbox and RuntimeObjects bases; BuildPlan…
sam-goodwin Nov 4, 2026
beed767
fix(core): RuntimeObjects.lifecycle fits Provider.effect (mutable sta…
sam-goodwin Nov 4, 2026
017eeed
fix(docker): iterate code points without spreading a string (no-misus…
sam-goodwin Nov 5, 2026
d0d6e82
Merge remote-tracking branch 'origin/main' into feat/docker-build-plan
sam-goodwin Nov 6, 2026
35d212a
feat(core): sandbox ports via getTcpPort; fetch proxies server requests
sam-goodwin Nov 6, 2026
042f2b6
fix(docker): parse Dockerfiles as BuildKit does, with a conformance s…
sam-goodwin Nov 6, 2026
8606656
test(docker): keep CRLF conformance cases byte-exact; sync exports
sam-goodwin Nov 6, 2026
1506764
test(docker): BuildKit lexer tables, build-time checks and coverage f…
sam-goodwin Nov 6, 2026
8123374
Merge remote-tracking branch 'origin/main' into feat/docker-build-plan
sam-goodwin Nov 6, 2026
57b61a2
refactor(core): drop alchemy/RuntimeObjects; providers tag runtime ob…
sam-goodwin Nov 6, 2026
3438df3
feat(e2b): add the E2B provider
sam-goodwin Nov 6, 2026
ea0daa5
Merge remote-tracking branch 'origin/main' into feat/e2b-provider
sam-goodwin Nov 6, 2026
a10573e
Merge remote-tracking branch 'origin/main' into feat/e2b-provider
sam-goodwin Nov 6, 2026
174eb82
Merge remote-tracking branch 'origin/main' into feat/e2b-provider
sam-goodwin Nov 6, 2026
166b497
docs(website): own Sandboxes dropdown
sam-goodwin Nov 6, 2026
51c662a
Merge remote-tracking branch 'origin/main' into feat/e2b-provider
sam-goodwin Nov 6, 2026
910d2e6
docs(e2b): What is E2B, access first, diagrams per chapter
sam-goodwin Nov 7, 2026
0cabab6
Merge remote-tracking branch 'origin/main' into feat/e2b-provider
sam-goodwin Nov 7, 2026
d8eb8a5
docs(e2b): regenerate llms.txt
sam-goodwin Nov 7, 2026
c6539f4
docs(e2b): the Worker creates and mounts the volume in the What is di…
sam-goodwin Nov 7, 2026
498eecb
docs(e2b): compact the sandbox state machine
sam-goodwin Nov 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pkg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
${{ contains(github.event.pull_request.labels.*.name, 'force-ci') && '--all' || '' }}
--rebuild-all-path 'scripts/package-build.ts'
--group 'alchemy=./packages/alchemy'
--group '@alchemy.run[Collapsed]=./packages/{better-auth,git,drizzle,sqlite,acme,archil,axiom,doppler,infisical,turso,daytona,planetscale,neon,prisma,hetzner,railway,fly,github,stripe,cloudflare,gcp,aws,domain,kubernetes,docker,redis,sql,cloudflare-runtime,frontend-frameworks,node-utils,pkg}'
--group '@alchemy.run[Collapsed]=./packages/{better-auth,git,drizzle,sqlite,acme,archil,axiom,doppler,infisical,turso,e2b,daytona,planetscale,neon,prisma,hetzner,railway,fly,github,stripe,cloudflare,gcp,aws,domain,kubernetes,docker,redis,sql,cloudflare-runtime,frontend-frameworks,node-utils,pkg}'

- name: Vouch for the manifest
if: steps.pack.outputs.package-count != '0'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ examples/cloudflare-worker/src/scratch.ts
!packages/aws/src/globals.d.ts
!packages/kubernetes/src/globals.d.ts
!packages/fly/src/globals.d.ts
!packages/e2b/src/globals.d.ts
!packages/github/src/globals.d.ts
!packages/stripe/src/globals.d.ts
!packages/hetzner/src/globals.d.ts
Expand Down
21 changes: 21 additions & 0 deletions examples/e2b-agent-sandbox/alchemy.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// alchemy.run.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as E2B from "@alchemy.run/e2b";
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import Api from "./src/Api.ts";
import { Agent } from "./src/Sandbox.ts";

export default Alchemy.Stack(
"E2BAgentSandboxExample",
{
providers: Layer.mergeAll(Cloudflare.providers(), E2B.providers()),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const agent = yield* Agent;
const api = yield* Api;
return { url: api.url.as<string>(), templateId: agent.templateId };
}),
);
27 changes: 27 additions & 0 deletions examples/e2b-agent-sandbox/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "e2b-agent-sandbox",
"version": "0.0.0",
"private": true,
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/alchemy-run/alchemy.git",
"directory": "examples/e2b-agent-sandbox"
},
"type": "module",
"scripts": {
"deploy": "alchemy deploy",
"destroy": "alchemy destroy",
"test": "bun test"
},
"dependencies": {
"@alchemy.run/cloudflare": "workspace:*",
"@alchemy.run/docker": "workspace:*",
"@alchemy.run/e2b": "workspace:*",
"alchemy": "workspace:*",
"effect": "catalog:"
},
"devDependencies": {
"@alchemy.run/test": "workspace:*"
}
}
87 changes: 87 additions & 0 deletions examples/e2b-agent-sandbox/src/Api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// src/Api.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as E2B from "@alchemy.run/e2b";
import * as Effect from "effect/Effect";
import { HttpServerRequest } from "effect/http/HttpServerRequest";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
import * as Layer from "effect/Layer";
import { Agent } from "./Sandbox.ts";

export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
const sandboxes = yield* E2B.CreateSandbox(Agent);
const volumes = yield* E2B.CreateVolume();

/** Runs a script in a fresh sandbox that is killed when the request ends. */
const run = Effect.fn("run")(function* (code: string) {
const sandbox = yield* Effect.acquireRelease(
sandboxes.create({ timeout: 120, network: { allowOut: [] } }),
(sandbox) => Effect.ignore(sandbox.kill()),
);
yield* sandbox.files.write("main.mjs", code);
return yield* sandbox.exec("node main.mjs");
});

/** The user's sandbox: found by metadata, resumed if paused, created with their volume. */
const userSandbox = Effect.fn("userSandbox")(function* (user: string) {
const home = yield* volumes.getOrCreate(`home-${user}`);
const [existing] = yield* sandboxes.list({
metadata: { user },
state: ["running", "paused"],
});
return existing !== undefined
? yield* sandboxes.connect(existing.sandboxID, { timeout: 600 })
: yield* sandboxes.create({
metadata: { user },
timeout: 600,
autoPause: true,
mounts: [{ volume: home, path: "/home/user/work" }],
});
});

return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const url = new URL(request.url, "http://api");

if (request.method === "POST" && url.pathname === "/run") {
const code = yield* request.text;
const result = yield* run(code).pipe(Effect.scoped);
return yield* HttpServerResponse.json(result);
}

const user = /^\/users\/([a-z0-9-]+)$/.exec(url.pathname)?.[1];
if (request.method === "DELETE" && user !== undefined) {
const sandboxesOfUser = yield* sandboxes.list({ metadata: { user } });
yield* Effect.forEach(sandboxesOfUser, (s) => sandboxes.kill(s.sandboxID));
const home = yield* volumes.get(`home-${user}`);
if (home !== undefined) yield* volumes.delete(home.volumeId);
return HttpServerResponse.empty({ status: 204 });
}

const match = /^\/users\/([a-z0-9-]+)\/files\/(.+)$/.exec(url.pathname);
if (match !== null) {
const [, user, file] = match as unknown as [string, string, string];
const sandbox = yield* userSandbox(user);
if (request.method === "PUT") {
const content = yield* request.text;
yield* sandbox.files.write(`/home/user/work/${file}`, content);
}
const listing = yield* sandbox.exec(["ls", "-1", "/home/user/work"]);
const content = yield* sandbox.files.readText(`/home/user/work/${file}`);
yield* sandbox.pause();
return yield* HttpServerResponse.json({
sandboxId: sandbox.sandboxId,
files: listing.stdout.split("\n").filter(Boolean),
content,
});
}

if (url.pathname === "/") return HttpServerResponse.text("ok");
return HttpServerResponse.text("not found", { status: 404 });
}).pipe(Effect.orDie),
};
}).pipe(Effect.provide(Layer.mergeAll(E2B.CreateSandboxHttp, E2B.CreateVolumeHttp))),
) {}
13 changes: 13 additions & 0 deletions examples/e2b-agent-sandbox/src/Sandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// src/Sandbox.ts
import * as Dockerfile from "@alchemy.run/docker/Dockerfile";
import * as E2B from "@alchemy.run/e2b";

/** Every sandbox boots from this image: Node 22 with git and ripgrep. */
export const Agent = E2B.Template("Agent", {
dockerfile: Dockerfile.inline`
FROM node:22-bookworm
RUN apt-get update && apt-get install -y git ripgrep
`,
cpuCount: 2,
memoryMB: 2048,
});
63 changes: 63 additions & 0 deletions examples/e2b-agent-sandbox/test/integ.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { expect } from "bun:test";
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as E2B from "@alchemy.run/e2b";
import * as Test from "@alchemy.run/test/Bun";
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
import * as HttpClient from "effect/http/HttpClient";
import * as HttpClientRequest from "effect/http/HttpClientRequest";
import * as Layer from "effect/Layer";
import Stack from "../alchemy.run.ts";

const providers = Layer.mergeAll(Cloudflare.providers(), E2B.providers());
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
providers,
state: Alchemy.localState(),
});

const stack = beforeAll(deploy(Stack));
afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));

const { executeWhenReady } = Test;
const user = `user-${Test.defaultStage()}`.toLowerCase().replace(/[^a-z0-9-]/g, "-");

test(
"runs code in a one-shot sandbox",
Effect.gen(function* () {
const { url } = yield* stack;
const res = yield* executeWhenReady(
HttpClientRequest.post(`${url}/run`).pipe(HttpClientRequest.bodyText("console.log(6 * 7)")),
);
expect(res.status).toBe(200);
const body = (yield* res.json) as { stdout: string; exitCode: number };
expect(body).toMatchObject({ stdout: "42\n", exitCode: 0 });
}),
{ timeout: 180_000 },
);

// E2B enables volumes per team; set E2B_TEST_VOLUMES=1 on an enabled one.
test.skipIf(!process.env.E2B_TEST_VOLUMES)(
"keeps a user's files in their sandbox and volume across requests",
Effect.gen(function* () {
const { url } = yield* stack;
const put = yield* executeWhenReady(
HttpClientRequest.put(`${url}/users/${user}/files/notes.md`).pipe(
HttpClientRequest.bodyText("hello"),
),
);
const first = (yield* put.json) as { sandboxId: string; files: string[]; content: string };
expect(first.content).toBe("hello");

const get = yield* HttpClient.execute(
HttpClientRequest.get(`${url}/users/${user}/files/notes.md`),
);
const second = (yield* get.json) as { sandboxId: string; files: string[]; content: string };
expect(second.sandboxId).toBe(first.sandboxId);
expect(second.files).toContain("notes.md");

// The user's sandbox and volume were created at runtime: delete them through the API.
const removed = yield* HttpClient.execute(HttpClientRequest.delete(`${url}/users/${user}`));
expect(removed.status).toBe(204);
}),
{ timeout: 300_000 },
);
25 changes: 25 additions & 0 deletions examples/e2b-agent-sandbox/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.base.json",
"include": ["alchemy.run.ts", "src/**/*.ts", "test/**/*.ts"],
"compilerOptions": {
"noEmit": true,
"rootDir": ".",
"module": "Preserve",
"moduleResolution": "Bundler",
"target": "ESNext"
},
"references": [
{
"path": "../../packages/alchemy/tsconfig.json"
},
{
"path": "../../packages/cloudflare/tsconfig.json"
},
{
"path": "../../packages/e2b/tsconfig.json"
},
{
"path": "../../packages/docker/tsconfig.json"
}
]
}
16 changes: 16 additions & 0 deletions examples/e2b-program-async/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# E2B async program example

A plain Node handler, with no Effect runtime, baked into an E2B template and served from one long-lived sandbox.

- [`src/Greeter.ts`](./src/Greeter.ts) — `E2B.Template` called as a function with `main` pointing at the handler file, and `E2B.InferEnv` typing its `env`.
- [`src/server.ts`](./src/server.ts) — the handler, reading `GREETING` from its environment.
- [`alchemy.run.ts`](./alchemy.run.ts) — builds the template and declares the `E2B.Sandbox` that serves it.

## Commands

```sh
pnpm alchemy profile edit --add E2B # once: log Alchemy in to E2B
pnpm --filter e2b-program-async deploy
pnpm --filter e2b-program-async test
pnpm --filter e2b-program-async destroy
```
20 changes: 20 additions & 0 deletions examples/e2b-program-async/alchemy.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// alchemy.run.ts
import * as E2B from "@alchemy.run/e2b";
import * as Alchemy from "alchemy";
import * as Output from "alchemy/Output";
import * as Effect from "effect/Effect";
import { Greeter } from "./src/Greeter.ts";

export default Alchemy.Stack(
"E2BProgramAsyncExample",
{ providers: E2B.providers(), state: Alchemy.localState() },
Effect.gen(function* () {
const sandbox = yield* E2B.Sandbox("GreeterSandbox", {
template: Greeter,
network: { allowPublicTraffic: true },
});
return {
url: Output.interpolate`https://3000-${sandbox.sandboxId}.${sandbox.domain}`,
};
}),
);
26 changes: 26 additions & 0 deletions examples/e2b-program-async/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "e2b-program-async",
"version": "0.0.0",
"private": true,
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/alchemy-run/alchemy.git",
"directory": "examples/e2b-program-async"
},
"type": "module",
"scripts": {
"deploy": "alchemy deploy",
"destroy": "alchemy destroy",
"test": "bun test"
},
"dependencies": {
"@alchemy.run/e2b": "workspace:*",
"@distilled.cloud/e2b": "workspace:*",
"alchemy": "workspace:*",
"effect": "catalog:"
},
"devDependencies": {
"@alchemy.run/test": "workspace:*"
}
}
14 changes: 14 additions & 0 deletions examples/e2b-program-async/src/Greeter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// src/Greeter.ts
import * as E2B from "@alchemy.run/e2b";

/**
* A plain handler file baked into an E2B template, run as-is with no
* Effect runtime. `env` becomes its environment.
*/
export const Greeter = E2B.Template("Greeter", {
main: "./src/server.ts",
port: 3000,
env: { GREETING: "hello" },
});

export type GreeterEnv = E2B.InferEnv<typeof Greeter>;
11 changes: 11 additions & 0 deletions examples/e2b-program-async/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// src/server.ts
import { createServer } from "node:http";
import type { GreeterEnv } from "./Greeter.ts";

const env = process.env as GreeterEnv & { PORT?: string };

createServer((request, response) => {
const name = new URL(request.url ?? "/", "http://sandbox").searchParams.get("name") ?? "world";
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ message: `${env.GREETING}, ${name}` }));
}).listen(Number(env.PORT ?? 3000), "0.0.0.0");
26 changes: 26 additions & 0 deletions examples/e2b-program-async/test/integ.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect } from "bun:test";
import * as E2B from "@alchemy.run/e2b";
import * as Test from "@alchemy.run/test/Bun";
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
import * as HttpClientRequest from "effect/http/HttpClientRequest";
import Stack from "../alchemy.run.ts";

const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
providers: E2B.providers(),
state: Alchemy.localState(),
});

const stack = beforeAll(deploy(Stack));
afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));

test(
"the plain handler serves with its env from the declared sandbox",
Effect.gen(function* () {
const { url } = yield* stack;
const res = yield* Test.executeWhenReady(HttpClientRequest.get(`${url}/?name=e2b`));
expect(res.status).toBe(200);
expect(yield* res.json).toEqual({ message: "hello, e2b" });
}),
{ timeout: 180_000 },
);
Loading
Loading