Repository navigation
feat(computer): the disk form on Modal VM sandboxes - #468
Open
earakely-scale wants to merge 15 commits into
Open
earakely-scale wants to merge 15 commits into
earakely-scale wants to merge 15 commits into
Conversation
A Modal VM sandbox boots a computer's disk under nested virtualization, its own image the QEMU launcher, the disk exported once per composed disk on this machine and mounted at create. The launcher serves each port through socat in front of slirp's forward, whose listen backlog is 1, and boots its disk afresh in place on a reset. Gate 3 reruns a probe that ended on a transport error. A run's and a deploy's composes build on this machine, the sandbox picking the forms. modal_vm takes an optional region and idle timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er runs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ontainer in this machine's registry A composed container is in the env's image store, which a registry on this machine's loopback is only for a sandbox on this machine; a disk the provider carries itself. So a run or deploy on modal_vm with the default stores picks the disk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y and reset on local Docker Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nce driver's click lands first (#464) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ich may reuse a disk by its key Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…build, never the amd64 disk Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…made under the same key Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tarted, the export signs in, a local reset has its minute bash's `wait -n` skips a job that ended before it was called, so a proxy that couldn't bind its port while the disk was expanded went unnoticed; the launcher now watches QEMU and its proxies every 0.2 s. The export signs in to the image store before `docker create` pulls the disk, as a boot's pull does. A local reset waits 90 s, longer than the minute `launch reset` gives the computer to start again, where `_docker`'s 30 s cut it short. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… stand-in had logged anything Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h a CI runner takes within a minute On a GitHub runner one of 64 new connections at once waited past 30 s: socat took every connection, and one of its own connections to QEMU's forward, whose backlog is 1, was left in its SYN's later retries. 64 at once take at most 1.9 s on EC2 and Modal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…EMU's forward itself libslirp's forward listens with a backlog of 1, so in a burst it drops socat's connections, and each then waits out the kernel's SYN backoff (1, 3, 7, 15, 31 s): on a starved host, past a client's timeout. socat now times its connect out at half a second and retries every 50 ms. Measured on a c8i box with its CPUs saturated, bursts of 128 new connections: the slowest took 54-56 s, with 10 of 128 failing, and now takes 3-3.9 s with none failing; medians are unchanged. The CI burst goes back to 64, which passes idle and saturated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ployment's rule; --keep names it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ploys materialize checked a deploy step's own refusal on the provider the step names, the config's default, though the run deploys it on --sandbox. On a host whose Docker boots no disk (no KVM: an arm64 Linux box, found on Graviton), a run of a disk-form task with --sandbox modal_vm was refused before it started, saying local Docker boots none of its arches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FD-4371 (M4.2): a computer's Linux disk form boots on a Modal VM sandbox under nested virtualization, carried from this machine's local stores, as M4.5's exit (FD-4379) needs. It implements the seven decisions in the design doc.
What changes
image,modal_vm.create_vmmakes a VM sandbox (runtime="vm",enable_nested_virt) whose own image is the QEMU launcher, ENTRYPOINT cleared, with no Docker in it. It's sized at the VM's CPUs and its memory plus 2 GiB, so the default 2 CPUs and 4096 MB give a 2-CPU, 6 GiB sandbox.disk_image_platforms()is{linux/amd64}.VM_REQUIRE_KVM=1, so it fails clearly without/dev/kvmrather than emulate. Local Docker sets it too. The release smoke leaves it unset, so arm64 still boots there under TCG.local_docker.disk.exported_diskexportsdisk.imgfrom the image store with this machine's Docker, once per composed disk ref (whose tag is the composer's key). It's zstd-compressed with the launcher'szstdand kept underconfig.paths.cache_root()/disks.add_local_file(..., copy=False). Modal uploads each content once.VM_DISK=/disk/disk.img.zst) expands it onto the sandbox's disk.write_file_from_objectare involved.VM_PORTSport withsocat TCP-LISTEN:<port>,fork,reuseaddr,backlog=1024in front of slirp'shostfwdon127.0.0.1:<port+10000>. socat is in the launcher image.wait -nskips a job that ended before it was called (found in review).[sandbox.providers.modal_vm.config] region(unset by default) applies to every sandbox the provider makes: the computer's, the agent's and the gateway's.launch reset(the new environment asVM_ENV_*lines on stdin) stops QEMU and returns once its successor has started on a fresh overlay. The ports stay up, so the URL is the same.reset_vmruns it in the sandbox hosting the launcher: Modal's exec, anddocker exec -ion local Docker, which no longer recreates its container./run/vmto/var/lib/agentenv-vm.attempts). A failed check, a boot that never got ready, and the probe's own timeout are recorded at once.Composing.for_run(provider)picks the forms on the run's sandbox (deploys_on) and builds them on the env's default provider, which is this machine unless the config names another.ComputerEnv.compose_unstartedbuilds there too, never on the deploy's sandbox, and says where ("composing disk/amd64 before this deploy on LocalSandboxProvider (no compose built it for ModalVmSandboxProvider yet)").put/env build --sandbox) is unchanged.Three additions beyond the seven decisions, for review
boot.native_links/first_nativetake the env's store id;boot.pulls_containers).--sandbox modal_vmpicks the container form for a bench task, because modal_vm runs amd64 containers natively and the container comes first in the base's order. A Modal VM then can't pull the container from this machine'slocalhost:5000.deploy_refusalallows the disk form on such a sandbox. For a container-only base it now says why: "…can't pull its containers from the image store's registry on this machine".[sandbox.providers.modal_vm.config] idle_timeout(seconds, unset by default) for every sandbox the provider makes.agent-env run --keep's help now says a provider's idle timeout still ends a sandbox nothing is connected to (e45cf46).--sandbox(33507b5,bundle/materialize.py).--sandbox. The run-wide preflight (preflight_run) already used--sandbox.--sandbox modal_vmwas refused before it started: "LocalSandboxProvider runs none of its arches (amd64, arm64) natively as disk". The amd64 EC2 boxes have KVM, which hid it. The arm64 check below found it.sandbox_typeandjudge_sandbox_typeare checked as the run's sandbox, assandbox_overridesdeploys them.Smaller changes:
composer.REPOSITORYmoved tocomposed.py, sobootcan read it without an import cycle.socatandzstd.computers/README.md,sandbox_providers/AGENTS.md,agent-env run --sandbox's help andrun_bundle's docstring say all of the above.regionandidle_timeoutsit in the provider's free-formconfigtable, whichconfig/describe.pydoesn't descend into (as for vercel'sregion), so it needs no new row.SandboxProvidergains nothing, and modal_vm's constructor only gains optional keys.The end-to-end bar (Edgar's condition for D3)
Run on EC2 (c8i.4xlarge with nested KVM,
earakelyan-m42-e2e), which acted as the laptop: default local stores, this branch merged with main at 0.9.1426 (with #464),[sandbox.providers.modal_vm.config] region = "us-west", and every deploy through the framework (ComputerEnv.deploy(sandbox_type=…, env_params={"form": "disk"})).Probes at
full, 20 boots each. Every boot was a fresh deploy and teardown.desktop-s02(cua-driver, cdb-s02, bench-sentinel)There were no transport errors (dropped or refused connections, timeouts) and no failed checks, P4/P6 included. Before merging #464 the same 80 boots passed too.
Latency in ms (p50 / p95 / p99). New connections are each on a fresh HTTPS (Modal) or HTTP (local) connection to the driver's card, 40 a boot one at a time and 40 a boot in bursts of 8; tool calls are 20 a boot over an MCP session. n is 800 a cell for connections and 400 for tool calls.
computer_screenshotcomputer_clickLarger bursts: one desktop boot each, 3 rounds, every connection with a 150 s timeout. Each cell gives p50 / p95 / p99 / max in ms for the worst round.
No connection was dropped anywhere. But on a GitHub runner (4 vCPUs), one of 64 at once waited past 30 s in
launcher_test.py.The cause: socat takes the burst, but each of its forked children then connects to slirp's forward inside the container, whose listen backlog is 1. A connect that overflows it waited for the kernel's SYN retry, at 1, 3, 7, 15 and 31 s, so the wait grew with the burst and the host's load. That was the 1 s tail on local Docker in the bursts of 8 above (about 1 in 20 connections), and how a starved runner reached 30 s. Modal's tunnel spreads the arrivals, so its tail stayed under 0.7 s at 8 at once.
The fix, 8ea287c: socat's onward connect is
TCP:127.0.0.1:<port+10000>,connect-timeout=0.5,retry=600,interval=0.05, so socat retries a dropped connect itself every 50 ms. Measured onearakelyan-m42-e2ewith the desktop disk on local Docker: 2 rounds of each burst, idle and with all 16 CPUs busy, a 60 s client timeout. Each cell gives p50 / p95 / max in seconds for the worst round.Every connection passed after the fix.
launcher_test.pyis back at a burst of 64. On the same VM it passed in 88 s idle and in 87 s with 16 CPU hogs. The bursts-of-8 and 16/64 tables above were measured before 8ea287c. Raising libslirp's backlog upstream would still remove the retries.A real G02 task end to end:
agent-env run ~/bench --task cdb-g02-foreground --sandbox modal_vm --model anthropic/claude-haiku-5-5, in 321 s.fail_task_on_error: false).workbook_agreementfailed), the sentinel check passed, and GUI-only scored 1.0.The agent's own tool calls, timed from its trajectory (tool_use event to tool_result event), include each tool's work.
run_stepsbatches actions and waits, andget_window_statereads the accessibility tree.run_stepsget_window_stateAfter the review fixes (9c61d1d, the launcher's watch loop), the recheck on Modal passed:
tst/integration/computer/disk--task probe(probe, a reset in 8.3 s, probe), and 5 of 5 G02 probes atfull, with the same latency.Reset in place, in a run:
tst/integration/computer/disk--task probeon modal_vm: probe passed, thenreset_envtook 7.6 s, then the probe passed again at the same URL.desktop-s02, probe,reset_env, probe, then the bench'scheck_app): the reset took 12.5 s and both probes passed.check_appfound every app pristine, placed and relaunched after the reset.Rechecked on Modal after the socat fix (8ea287c), on a fresh
earakelyan-m42-recheck(c8i.4xlarge) with this machine's stores,us-west:disk_modal_vm_test.py: passed (286 s). It exports once, boots, probes, resets in place and probes, and a second run exports nothing.agent-env run ~/bench --task app-s02 --sandbox modal_vm: it composed the three computers here, deployeddesktop-s02's disk on modal_vm in 44 s (the export and first upload included), andcheck_apppassed.full: 5/5 passed. Latency matched the 20-boot run above:computer_screenshotcomputer_clickA connection that arrives while QEMU's forward is down now waits in socat, retrying every 50 ms for up to 30 s, and is served once the computer is back. Before 8ea287c it was refused at once.
From an arm64 host, as an Apple-silicon laptop runs it: a fresh
earakelyan-m42-arm(Graviton c7g.4xlarge, Docker 29.9.3 with the containerd image store, no KVM, amd64 under binfmt emulation).agent-env run tst/integration/computer/disk --task probe --sandbox modal_vmpassed in 120 s.unverified, since no link here boots it.--platform.Tests
--sandbox(preflight_test.py);launchscript);launch reset, and the export once for each disk;disk_image_platforms, the launcher image, and every create argument bound to the installed Modal's signature;tst/integration/computer/disk_modal_vm_test.py, gated onremote_sandbox: the desktop disk carried, booted, probed, reset and probed on Modal, and a second run that exports nothing.launcher_test.py, gated onkvm: a burst of 64 new connections through the proxy, and a reset in place on local Docker.disk_boot_test.py, updated for the deploy's compose line.earakelyan-m42-tests(c8i.2xlarge), without Modal keys, as CI runs:make unit-testat the head: 12,796 passed, 7 skipped.make int-test-fastat 9c61d1d: 180 passed.make int-test-slow, serially at the merge with main (34b02c7): 172 passed and 825 skipped, the skips being capability gaps (remote providers, model endpoint). One failed:disk_boot_testexpected "making the disk", while an earlier module in the same serial run had already made that disk under its key. Its run itself passed (probe, a reset in 6.3 s, probe). 97a18c6 accepts a reused disk.disk_boot_test.pyandlauncher_test.pyat 9c61d1d: 2 passed.earakelyan-m42-e2e(c8i.4xlarge), with the CI project's Modal keys:disk_modal_vm_test.pypassed (2 runs, 142 s).Not in this PR
For the website docs:
--sandbox modal_vmfor computer envs; the disk export in~/.cache/agentenv/disks(prunable by hand until FD-5163);regionandidle_timeout; and that the idle timeout ends a computer with no connection open.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; no blocking issue remains.Summary
Adds disk boots on Modal VM sandboxes, compressed disk exports, and resets that keep the same URLs.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Compose disk on default provider] --> B[Export and compress disk locally] B --> C[Mount export on Modal VM sandbox] C --> D[Launcher expands disk] D --> E[QEMU boots fresh overlay] F[Client] --> G[socat proxy] G --> E H[Reset with new environment] --> I[Replace QEMU and overlay] I --> EReviews (6) · Last reviewed commit: "fix(bundle): a run preflights each step ..." · Reviewed by Greptile