feat(isolate): V8-isolate runtime (workerd) — Phases 1–3, off by default#302
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
microvm | 918cf14 | Commit Preview URL Branch Preview URL |
Jul 18 2026, 03:24 AM |
Design & implementation plan for a fifth runtime — V8 isolates (the Deno/Workers model) — as the fast-JS, high-density tier complementing the Firecracker microVM tier. Mirrors the wasm-runtime.md structure: reuse surface, workerd engine choice, the isolate-group blast-radius decision (P0 cross-tenant isolation gate), host-mediated networking, warm pool, statekv durability, phasing, non-negotiables, and open questions. Grounded in the existing runtime.Runtime / ContainerRuntime split, the WASM driver seams, service dispatch, and the vmm/wasm warm-pool pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sumansaurabh
force-pushed
the
plans/isolate-runtime
branch
from
July 17, 2026 05:22
b001a6c to
45094e1
Compare
Reviewed 2026-07-17 (/office-hours design doc, 3 adversarial rounds 9/10, + /plan-eng-review 4 sections + Codex outside voice). Key amendments: - Security re-scope (2.1): per-tenant OS process is the REQUIRED cross-tenant boundary (upstream: OSS workerd alone lacks hostile-code defense-in-depth); P0 gate reworded to drop cross-tenant memory-safety claims, executes end of Phase 3 as a tag-gated integration CI job - TenantID group key: server-authorized only (forced co-residency attack) - Group router: per-tenant single-flight; empty-group synchronous teardown - Phase 1 gains: bundle-injection spike (hard exit criteria), jail workstream (+ --jitless eval), workerd binary distribution, tenant_id schema - Demand checkpoint (10.1): pitch during Phase 2 gates Phase 4 density work - /v1/js-bundles catalogue (experimental until checkpoint) + abuse controls - poolcore extraction (rule of three); vmm/wasm migration deferred to Phase 4+ - Per-sandbox egress attribution + observability pulled into core scope - Full 15-test map + GSTACK review report appended Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…runtime.md) Fifth runtime skeleton — "isolate" (V8 isolates, Workers model), a peer to docker/gvisor/firecracker/wasm: - models: RuntimeIsolate enum accepted by ValidRuntime ahead of impl; durability set (ephemeral default, passivatable rejected, durable gated to Phase 5); server-authorized TenantID on CreateSandboxRequest + Sandbox - store: tenant_id column (empty = null tenant, owner_ref convention) wired through Create/Upsert/Get/List/ListByOwner/ListByRuntime + scanner, with round-trip + null-tenant back-compat regression test - config: SB_ENABLE_ISOLATE, SB_ISOLATE_WORKERD_PATH, SB_ISOLATE_RUN_DIR, SB_ISOLATE_GROUP_GRANULARITY (per-tenant default / per-sandbox), SB_ISOLATE_USE_JAIL + chroot/uid/gid knobs, SB_ISOLATE_JITLESS; isolate rejected as host-default runtime - internal/runtime/isolate: Runtime-only driver (AsContainerRuntime false) whose methods return ErrRuntimeNotImplemented; Ping stats workerd; jail spec (chroot + cgroup + drop-priv + seccomp allowlist with JIT/jitless variants, strict group-key sanitizer) with profile-invariant tests; 97.2% package coverage - service: isIsolateSandbox dispatch in runtimeForSandbox/runtimeRef; gated createIsolateSandbox (flag + driver-registration gates, isolate option validation, §2.1 tenant authorization with forced-co-residency regression tests); platform volumes reject isolate - daemon: wireIsolateRuntime behind cfg.EnableIsolate on worker nodes; SupportedRuntimes advertises "isolate" (cluster placement parity) - distribution: install.sh --with-isolate (workerd v1.20260717.1 pinned, SHA-256 verified), Terraform with_isolate passthrough, upgrade runbook - feasibility gates: P0 hostile-isolate gate specified as a tag-gated ratchet test (fails once the create path lands until implemented); §2.2 injection-spike harness measuring the cold-boot baseline Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…l workerd Ran the dynamic worker-loading path against workerd v1.20260717.1: a controller worker with a workerLoader binding loads a distinct isolate per name and invokes it over getEntrypoint().fetch(). Measured, one process, no restarts: fresh inject (new isolate/req): p50 1.2ms (target: ≤10ms) cached warm hit (same name): p50 0.32ms (provider does not re-run) cold spawn→first-200 baseline: 38ms (injection beats it ~23×) Decision: the injection path wins — Phase 2 builds the controller-worker + WorkerLoader architecture; the spawn-per-process and config-regen fallbacks are shelved. Encoded as TestInjectionSpikeDynamicLoad (tag-gated); plan §2.2 marked GREEN. Requires compatibilityFlags=["experimental"] + --experimental. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ate Phase 2) pkg/jsbundle is the isolate runtime's bundle layer — the analog of pkg/wasmmod for .wasm (plans/isolate-runtime.md §7). A bundle is the code a workerd isolate runs: an ES module map + entry module + pinned compatibility date, identified by the sha256 over a canonical (order-insensitive) serialization so create is idempotent under retry and a failover peer resolves the exact same bytes. - resolve.go: ref → Bundle. Digest (sha256:<hex> or bare 64-hex) loads from the store; file://path / *.js|.mjs|.ts builds a one-module bundle; any other token is an uploaded name looked up tenant-scoped (the "no image, no registry" path). - store.go: content-addressed blobs + tenant-scoped name pointers + a restart- durable index. Ships the plan's day-one abuse controls: per-bundle size cap and per-tenant bundle-count quota (null/operator tenant exempt); Delete for reference-counted GC. - build.go: single-file entrypoint → bundle (Phase-2 hot-path default; multi- module esbuild is a later seam). 85.3% coverage. No workerd dependency — pure Go, fully offline-tested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hase 2) pkg/isolate is the workerd engine wrapper (plans/isolate-runtime.md §7). One Host owns one workerd process per isolate group, implementing the injection architecture the §2.2 spike validated: - config.go generates the group's capnp config + a tiny CONTROLLER worker with a workerLoader binding. Sandbox traffic arrives keyed by x-sb-id (driver-set, unforgeable); the controller resolves the bundle from the HOST service binding (a Go bundle-server over a unix socket), then env.LOADER.get(id, …) loads/caches the isolate and invokes it. Egress is bound to EGRESS, a fail-closed deny-all service in Phase 2 (Phase 3 swaps in the per-sandbox attributed proxy) — an isolate cannot reach the network before policy exists. - host.go: Start (gen config, serve bundle+egress sockets, spawn workerd, wait ready), Load/Unload (pin a sandbox's bundle; Unload returns remaining count for last-member teardown), Invoke (proxy an HTTP request to the controller), Stop. "No such sandbox" is a clean 404 (bundle probe), distinct from a 502 bundle-server error and the isolate's own fetch response. Offline tests cover every pure path at 100% (config gen, loader-id validation, Load/Unload counting, bundle-server, fail-closed egress, config write, Stop idempotency). The workerd-spawn paths (Start/waitReady/Invoke proxy) are integration-only — proven by TestHostEndToEnd (tag-gated), which loads two distinct isolates through the full stack against a real workerd and asserts each serves its own bundle with correct warm-hit routing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wires the isolate driver to actually create sandboxes (plans/isolate-runtime.md Phase 2), on the injection architecture the §2.2 spike validated. - seams.go finalized post-spike: BundleResolver(tenant,ref)→*jsbundle.Bundle, GroupHost (Load/Unload/Invoke/Stop, satisfied by *pkg/isolate.Host), and HostSupervisor.SpawnGroup(spec)→GroupHost. - grouprouter.go: per-tenant (or per-sandbox) group key → one workerd process, with PER-KEY SINGLE-FLIGHT so N concurrent first-creates for a tenant spawn ONE process (§11), and last-member teardown that removes the group before stopping it so a racing create spawns fresh. - create.go: resolve bundle → acquire group (spawn under single-flight) → load. A load failure reaps a just-spawned empty group (§11 empty-group rule). - driver.go: Start/Stop/Destroy/Inspect/ListManaged operate on the group router; Destroy routes the unload + triggers teardown. Snapshots stay unsupported (V8 has no serialize-running-isolate API). - bundleresolver.go / hostsupervisor.go: production adapters over pkg/jsbundle and pkg/isolate. - service/isolate.go: real boot path — durability normalize (isolate rejects passivatable, durable is Phase 5), admission (conservative: each sandbox at its declared footprint; per-group base RAM surfaced via expvar, not modeled), driver.Create, store.Create with LIFO unwind (Destroy + release on failure). No caddy on the boot path — external inbound routing is Phase 3, so a created isolate is invocable by the driver but not yet publicly routed (private-by- default). - daemon: wireIsolateRuntime now builds the jsbundle store + resolver + workerd supervisor and injects them. Boot-path impact: isolate creates now do bundle resolve (local, content- addressed) + group-router lookup (in-memory) + first-create-per-tenant workerd spawn. Other runtimes unaffected (the isolate branch is after the wasm branch, one string compare). Idempotency: content-addressed bundle digest + single- flight group spawn; retries join the existing group. Concurrent first-create single-flight has a regression test. Tests: driver 91% offline (group router, single-flight, per-tenant vs per-sandbox, last-member teardown, load-failure reap, start/stop, adapters) + TestDriverCreateEndToEnd (tag-gated) proving driver.Create → real workerd → serve → teardown. Full offline make test green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mains Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…code
The isolate runtime's "no image, no registry" path (plans/isolate-runtime.md
§8): upload a JS/TS bundle over HTTP, reference it by name or digest on create.
Mirrors /v1/wasm-modules.
- pkg/models: CreateJSBundleRequest (source or modules map) + JSBundle
catalogue DTO.
- pkg/jsbundle store: ListDigests / TenantOwns / NamesForTenant for the
catalogue; null/operator tenant now tracked in the ownership index too (still
quota-exempt) so its bundles list.
- service/isolate_bundles.go: CreateJSBundle / ListJSBundles / GetJSBundle /
DeleteJSBundle, all OWNER-SCOPED via ownerRefForCreate/ownerScope — a
user-scoped token only sees/resolves/deletes its own bundles (cross-tenant
get/delete is a 404, not 403, so digests can't be probed); operator = global.
Delete refuses a digest a live isolate sandbox still pins (store.ErrJSBundleInUse
→ 409), mirroring DeleteWasmModule.
- v1 routes + handlers: POST/GET/GET{id}/DELETE{id} under /v1/js-bundles,
behind d.Auth. New route only — v1 soft-freeze respected (pr-review.md §6).
- service/isolate.go create path: owner-scoped name→digest resolution + stage,
pinning "sha256:<digest>" onto module_ref before the driver runs, so an
uploaded name a user references actually resolves under that user and the
driver/failover resolve the exact bytes by digest.
pr-review.md call-outs:
- Idempotency (§1): content-addressed — re-uploading identical bytes yields the
same digest with no error; concurrent duplicate uploads are safe under the
store mutex; re-using a name deterministically repoints it; delete of an
absent digest is a no-op.
- Boot-path (§2): createIsolateSandbox now does an owner-scoped bundle resolve
+ store stage before the driver — ISOLATE PATH ONLY (runtime=isolate, and
only when the bundle store is wired). Local store read / file build + one
content-addressed file write; no network, no lock beyond the store mutex.
Other runtimes untouched.
- Failure-path (§4): handlers write only to the bundle store (no caddy). A
bundle staged before a later create failure stays in the store — harmless
(content-addressed, reusable, unreferenced-GC candidate); nothing to unwind.
- Abuse controls exist (per-bundle size cap, per-tenant quota) but default to
unlimited for self-host; the managed control plane sets them.
Tests: v1 handler tests (create/list/get/delete/404/400), service tests
(owner scoping, idempotency, modules-form, in-use delete refusal, name→digest
create resolution), jsbundle store list/ownership tests. jsbundle 86.5%; new
service methods 82-93%. Full offline make test green (62 pkgs); isolate
integration tests still pass vs real workerd.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduces a new integration test for measuring the create latency of sandboxes in the isolate runtime. The test sets up a real HTTP API server and benchmarks the performance of sandbox creation, reporting cold and warm create times. It includes setup for the necessary environment and configurations, ensuring that the test runs against a real workerd binary. This addition enhances the testing coverage for the isolate runtime's performance metrics.
- Introduced a new link in the documentation sidebar for the Isolate Sandbox, providing a clear entry point for users. - Added a comprehensive documentation file for the Isolate Sandbox, detailing its functionality, setup, and usage examples in TypeScript, Python, Go, Rust, and Java. - Enhanced the configuration for the isolate runtime, including parameters for idle time-to-live, pool settings, and garbage collection intervals for unreferenced bundles. - Implemented a warm pool for blank workerd hosts to optimize the creation of isolate sandboxes, improving performance and resource management. This update enhances the usability and configurability of the isolate runtime, aligning with ongoing development efforts for the V8 isolate feature set.
- Changed variable names in the isolate sandbox code for consistency and clarity, including `module_ref` to `moduleRef` and `CreateSandboxRequest` to `CreateSandboxOptions`. - Updated comments and documentation to reflect changes in the API and clarify the functionality of the isolate runtime. - Enhanced the handling of group membership in the `acquireGroup` function to prevent race conditions during sandbox creation and destruction. - Added tests to ensure the stability of the group management under concurrent operations, addressing potential issues with last-member teardown. These changes improve code readability and maintainability while ensuring the robustness of the isolate runtime's functionality.
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.
Summary
runtime: "isolate"), the 5th AerolVM runtime, backed by Cloudflare workerd (Workers model). Host-mediated like WASM: no IP/TAP/iptables — one workerd OS process per isolate group, sandboxes loaded dynamically by id via a controller worker +workerLoader.tenant_idschema + jail spec (P1); content-addressed JS/TS bundle store +POST /v1/js-bundlesupload catalogue + cold create path (P2); group router, inbound HTTP fetch-proxy, exec=invoke, idle-TTL reaper, warm pool, bundle GC, egress boundary (P3). Plus theisolateconstant +tenant_idacross all 5 SDKs and a docs page.SB_ENABLE_ISOLATE=false; Terraform/install.shWITH_ISOLATE=false) — merging activates nothing until an operator opts in.Review fixes in this PR (pre-merge
/reviewpass)A full review + real-workerd integration run found and fixed several issues, including one that made the feature non-functional as committed:
globalOutbound: <dynamically-loaded worker stub>, which real workerd rejects (not a Fetcher;getEntrypoint()→ "entrypoints of dynamically-loaded workers cannot be transferred"). Fixed toglobalOutbound: env.EGRESS(static service Fetcher). Consequence: egress is fail-closed deny-all (see Known limitations).SB_ISOLATE_USE_JAIL=trueclaimed confinement → now fail-closed (refuse to spawn when a required jail can't be realized) + honestjail_realizableboot log + Linux realization seam (priv-drop/chroot). Emptytenant_idcollapsed all scoped tenants into one"default"process → now a stable per-owner group key.file://refs operator-only; isolate health surfaced in/health; warm-pool refill on daemon ctx; 4 non-compiling SDK doc examples.Code-path diagram
flowchart TB C["POST /v1/sandboxes runtime=isolate"] --> R{"file:// ref & scoped caller?"} R -->|yes| Rej["reject: operator-only ref"] R -->|no| Res["resolve + stage bundle (Put, GC-pinned)"] Res --> Auth["authorizeIsolateTenantID<br/>empty+scoped -> per-owner key"] Auth --> Adm["admit"] --> Drv["driver.Create"] Drv --> AG["acquireGroup: add id to member set UNDER groupsMu"] AG -->|new key| Spawn["spawn workerd (jail: apply or FAIL CLOSED)"] AG -->|existing| Join["join group"] Spawn --> Load["host.Load (outside lock)"] Join --> Load Load -->|ok| Store["store.Create"] Load -->|fail| Rel["releaseFromGroup: member-set--, teardown iff empty"] Store -->|ErrSandboxExists| Idem["idempotent: re-Get, NO rollback of winner"] Store -->|other err| Unwind["Destroy + releaseAdmission"] Store -->|ok| OK["started"] subgraph egress["isolate outbound fetch"] EG["globalOutbound = static EGRESS service"] --> Proxy["Go egress proxy"] Proxy -->|no x-sb-id| Deny["403 deny-all (Phase-2 posture)"] Proxy -->|blocked IP range| Deny endSandbox boot impact
Isolate-only path (does not touch docker/fc/wasm boot). Per-create work, all called out:
Putthat is a fast no-op when the digest is already staged (the common case); first stage of a digest writes the index once. No network.Idempotency
store.CreatePK conflict returnsErrSandboxExists, handled as idempotent success (re-Get; never rolls back the winner's shared driver state/admission). Group membership,host.Load, and admission are all keyed by id and idempotent.POST /v1/js-bundles: content-addressed; re-uploading identical bytes → same digest, no error; wholePutunder one lock.EnsureHTTPListenerreturns the existing loopback listener; caddy upsert +store.UpsertPortare idempotent; never walks the TCP host-port pool (isolate rejects tcp/tls).Failure-path consistency
host.Loadfail →releaseFromGroup(member-set decremented under lock, group torn down iff it was the last member);store.Createfail (non-conflict) →driver.Destroy+releaseAdmission.L4 / host-port-pool changes
N/A — neither touched. Isolate is HTTP-only and host-mediated; it rejects tcp/tls and never calls
TryReserveHostPort/allocateHostPort/EnsureLayer4. The store change is an additivetenant_id TEXT NOT NULL DEFAULT ''column (idempotent migration, provably balanced INSERT/SELECT/scan at 56 columns — verified), not the host-port region.Known limitations (deliberate, safe-by-default)
globalOutboundis rejected by workerd) — tracked as a §4 follow-up. Until then every isolate outbound fetch is denied (safe).SB_ISOLATE_USE_JAIL=truerefuses to spawn where it can't realize the jail, so there is no silent-unconfined state.Test plan
make testgreen (62 packages);gofmtclean.-raceconcurrent churn, owner-key derivation, bundle ref-count delete.TestHostEndToEnd,TestDriverCreateEndToEnd,TestP0HostileIsolateContainment, allTestPhase3*) — including the ones that were red at HEAD before this pass.pkg/daemon/isolate_bench_integration_test.go): warm p50 ~0.7ms / cold ~28ms on darwin (not t3-comparable; live t3 bench pending).