Repository navigation
feat(sandbox-adapter): add boxd provider - #388
MichielMAnalytics wants to merge 1 commit into
Conversation
|
ayyy!! 🔥 love to see it. from quick skim of things, looks like this is targeting sandbox-agent. I recommend targeting It's a lot leaner to implement. I recommend pointing an agent to how we implemented e2b.ts and add a
no rush! tag me any time if you have questions or want a review :)) |
|
docs on sandbox-adapter https://rivet.dev/agents/docs/sandbox-adapter/ |
a87c2b5 to
039b8f8
Compare
Preview approval requiredThis pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit |
Add @rivet-dev/sandbox-adapter/boxd. Each Actor gets one boxd machine, hibernated while the Actor sleeps and deleted when it is destroyed. Commands stream through boxd's exec call, and a timeout or an abort closes the call, which kills the command's process group. Files go through the machine filesystem API. @boxd-sh/sdk is an optional peer dependency. The Sandboxes, Sandbox Adapter, and quickstart pages list boxd, with the example in examples/docs/sandboxes/boxd.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
039b8f8 to
4fd17ef
Compare
Note
draft, please don't merge yet. i'm going through this by hand first and will mark it ready for review myself
Summary
I work on boxd. boxd runs full Linux microVMs (KVM) with a persistent disk that start in about a second, and can be hibernated, snapshotted and forked. This PR adds
@rivet-dev/sandbox-adapter/boxd, shaped likee2b.tsandfreestyle.ts. Each Actor gets one boxd machine. It is hibernated while the Actor sleeps and deleted when the Actor is destroyed. It was built with AI assistance and I tested it against production boxd.What changed:
packages/sandbox-adapter/src/boxd.ts: the provider.packages/sandbox-adapter/package.json: the./boxdexport, and@boxd-sh/sdk(^0.3.0) as an optional peer dependency and a dev dependency.packages/sandbox-adapter/tests/boxd.test.ts: 12 unit tests with a fake client, which always run, and 8 live tests shaped likedocker.test.ts, which run only whenBOXD_API_KEYis set.examples/docs/sandboxes/boxd.ts.scripts/test-packed.mjschecks thatdist/boxd.jsis packed.pnpm-lock.yaml: only the new@boxd-sh/sdkentries (15 lines added).The repo has no changesets, so there is none here.
The earlier Sandbox Agent provider from this PR is kept on the branch
feat/boxd-provider-sandbox-agentin my fork, in case you want it too.How it maps onto
SandboxProviderSandboxProvider/Sandboxnameboxdcwd/home/boxd(optioncwd), created on connectcreate(c)machines.create(create).createcan be a function of the Actor context. Returns the machine id.connect(c, id)machines.get. A missing or destroyed machine returnsundefined. Asuspended,hibernatedorstoppedmachine is resumed, woken or started, thenwaitUntilReadyandfs.mkdir(cwd).suspend(c, id)machines.hibernate. A machine that is gone or already stopped is left as it is.destroy(c, id)machines.delete. A machine that is gone is fine.execmachines.streamExecwith stdin closed, running/bin/sh -cincwdwithenvset for the whole command. stdout and stderr stream toonDataas they arrive. A timeout or an abort closes the call, which ends the command's process group.readFile,writeFile,mkdir,stat,readdir,existsmachines.fs.read,write,mkdir,stat(isDir),list,existsThe client is created on first use, like
daytonaProviderandfreestyleProvider.new Boxd()readsBOXD_API_KEY.Where boxd does not map cleanly
kill -9 $$) reports128 + signal(137), notnull. The Docker provider does the same. A timeout givesexitCode: nullandtimedOut: true, and an abort rejects withError("aborted"), as the interface says.stdoutandstderr. E2B and Daytona return them empty. I think this is more useful, and it matches whatonDataalready got./bin/shand the user isboxd. That matches the Docker and Freestyle providers'/bin/sh -c. Theboxduser has passwordlesssudo. Files written through the file API are owned byboxd.pause(memory kept in RAM) because an Actor can sleep for a long time.egressallowlist ascreate.spec.new Boxd()withoutBOXD_API_KEYuses that machine's own sign-in. Everywhere else it throwsAuthenticationError(missing_credentials). Passclient: new Boxd({ apiKey })to be explicit.Test plan
All live runs used production boxd (region
fra), at most two machines at a time, each named, labelled and set to delete itself after an hour. Every machine was deleted at the end.vitest runinpackages/sandbox-adapterwithBOXD_API_KEYset: 3 files, 49 passed (boxd 20: 12 unit and 8 live; plus Docker andsandboxEnv). Without a key: 40 passed, 8 skipped. The live boxd tests take about 6 s. They cover separate stdout and stderr, exit code,cwdandenvwith a quote in the value; output streaming before the command ends and an abort that kills it (sleep 300gone fromps); a timeout that kills the command and keeps its output; a 1 ms timeout; file round trip,readdir,stat,exists, missing files throwing, file owner; host env not visible in the machine; suspend, reconnect with files intact, destroy,connectreturningundefinedafter destroy, a second destroy; and a wrong API key throwingAuthenticationErrorwith no machine created.packagesjob, locally:pnpm install --frozen-lockfile,pnpm build,pnpm check-types(includingexamples/docs),pnpm test(sandbox adapter 40 passed and 8 skipped, pi 118 of 118),pnpm check-boundaries,pnpm lint(no errors, the 5 existing warnings inpackages/pi),npm pack --dry-runin both packages (dist/boxd.*included), andpnpm test:packed. All pass. The formatter step (lefthook run pre-commit --fail-on-changeson the changed files) passes.npmnext to@boxd-sh/sdk,rivetkitand@earendil-works/pi-durable, and ranexamples/docs/sandboxes/boxd.tswithtsx. I only added a machine name, a label anddeleteAftertocreate, so I could find and delete the machine. The client was the quickstart client. With a real Anthropic key (anthropic/claude-opus-5-5, as in the example):uname -a && whoami && pwdwithbash, writenotes/result.txtand read it back. It reported the boxd kernel, userboxd,/home/boxdand the machine's hostname from the file. 9.5 s.hibernated.cat notes/result.txt; uptime. The machine woke and the file was there. 4.5 s.c.destroy()) deleted the machine.sleep 1000still running after reconnect, reconnect in 195 ms);suspendtwice in a row and on an already hibernated machine;suspendon a stopped machine leaves it stopped; a machine spec withegress("example.com")passed ascreate.specreached that host and could not reachgithub.com; no API key in a plainnode:22container throwsAuthenticationError(missing_credentials) with no machine created.Diagramcomponent's source, which sizes the SVG from the nodes.Links