Skip to content

feat(sandbox): forkd Firecracker microVM provider - #517

Closed
zenprocess wants to merge 9 commits into
fabro-sh:mainfrom
zenprocess:feat/forkd-microvm-sandbox
Closed

feat(sandbox): forkd Firecracker microVM provider#517
zenprocess wants to merge 9 commits into
fabro-sh:mainfrom
zenprocess:feat/forkd-microvm-sandbox

Conversation

@zenprocess

@zenprocess zenprocess commented Jun 23, 2026

Copy link
Copy Markdown

feat(sandbox): forkd Firecracker microVM provider

Why

fabro already supports Local, Docker, and Daytona sandboxes. Daytona is a great
fit for managed/cloud remote VMs. This PR adds a complementary option for teams
that need execution to stay fully self-contained and on-premise:

  • Strong isolation without a shared kernel. Firecracker microVMs give each
    step a real VM boundary (vs Docker's shared host kernel), closer to Daytona's
    isolation but without a hosted control plane.
  • Self-hosted, zero external dependency. forkd runs on your own hardware via
    a small controller API. No SaaS account, no per-VM cloud billing, no data
    leaving the network. Ideal for regulated/air-gapped/on-prem deployments.
  • Fast clone-from-snapshot. forkd's snapshot+clone model gives microVM-grade
    isolation with container-like start times, so per-step VMs stay practical.

It is purely additive: a new SandboxProvider selected via config, mirroring the
existing daytona provider's structure. Local/Docker/Daytona are unchanged.

What

  • SandboxCreateSpec::Forkd + SandboxProviderKind::Forkd
  • ForkdConfig/ForkdSettings (controller URL, token, snapshot/image) in config
  • A thin forkd controller HTTP client (clone snapshot, exec, delete; token auth)
  • ForkdSandboxProvider (impl SandboxProvider: create/list/get/delete)
  • ForkdSandbox (impl the full Sandbox trait over a microVM: exec, streaming,
    file up/down, lifecycle, git)
  • Registry + from_environment wiring; tests mirroring the sandbox round-trips

Credentials (forkd token) come from config/env, never hardcoded.

Status

First cut. Behavior contract: 27 BDD scenarios across the provider + Sandbox trait
surface. Compile/CI verification pending on a Rust-capable runner.

Valentin Vladescu and others added 9 commits June 23, 2026 12:56
Add a `forkd` feature-gated sandbox provider that runs code inside
Firecracker microVMs managed by a forkd controller.

Changes:
- `fabro-types`: add `SandboxProviderKind::Forkd`; widen `is_clone_based()`
- `fabro-sandbox/Cargo.toml`: add `forkd` feature wiring `reqwest` + `rand`
- `config.rs`: add `ForkdSnapshotSettings`, `ForkdNetwork`, `ForkdSettings`
- `provider.rs`: add `SandboxCreateSpec::Forkd` variant + `pub mod forkd`
- `from_environment.rs`: add `forkd_config_from_environment()`; widen
  `duration_to_minutes_i32` gate to include forkd
- `sandbox.rs`: widen `resolve_path` gate to include forkd
- `details.rs`: add `forkd::forkd_info_from_name` + `forkd::forkd_details`;
  add exhaustive `Forkd` arm in `sandbox_details()` match
- `lib.rs`: widen `from_environment` gate; add `pub mod forkd` re-export;
  re-export `ForkdConfig`, `ForkdSandbox`, `ForkdSandboxProvider`
- `src/forkd/mod.rs` (new): `ForkdConfig`, `ForkdSandbox` implementing the
  full `Sandbox` trait — one long-lived VM per sandbox; all file I/O via exec
  with base64 round-trips for binary safety
- `src/provider/forkd.rs` (new): `ForkdSandboxProvider` implementing
  `SandboxProvider` — list/get/create/delete over forkd REST API

FORKD_URL (default http://127.0.0.1:8889) and FORKD_TOKEN (default
forkd-local-token) are resolved from environment variables at provider
construction time; they are never hardcoded.

Cargo unavailable in this environment; code is syntactically careful,
mirrors daytona idioms exactly (OnceCell, shell_quote, rand 0.9 API,
fabro error types, SandboxEvent variants).
…ermination, hardening

Finding 1 (HIGH) — provider/forkd.rs create(): call sandbox.initialize().await? after
constructing ForkdSandbox so the microVM is actually provisioned (POST /vms + optional
git clone) before SandboxInfo is returned.  Previously the VM was never created.

Finding 2 (HIGH) — forkd/mod.rs upload_file_from_local(): replace String::from_utf8_lossy
(corrupts non-UTF-8 bytes) with a binary-safe path: base64-encode raw bytes via the
BASE64 engine and decode inside the VM via `echo <b64> | base64 -d > path`, mirroring
how read_file_bytes works in reverse.

Finding 3 (MEDIUM) — forkd/mod.rs exec_command(): CommandTermination is now TimedOut
when forkd returns no exit_code (killed/OOM/timeout), not the incorrect Exited.

Finding 4 (MEDIUM) — forkd/mod.rs glob(): replace the broken `*/pattern` prefix (which
requires >=1 intermediate dir and mangles patterns like *.rs) with `-name` for
patterns without `/` and `-path <base>/<pattern>` for path-qualified patterns.

Hardening:
- HTTP client timeout (120 s) + connect_timeout (15 s) on ForkdSandbox; 30 s / 15 s
  on ForkdSandboxProvider.
- Bounded exponential-backoff retry (up to 3 attempts, 250 ms initial, max 10 s) on
  5xx responses and connect errors in exec_in_vm, create_vm, delete_vm (mod.rs) and
  list/get/delete (provider/forkd.rs).
- ForkdConfig::Debug is now a manual impl that prints forkd_token as "[redacted]"
  so the bearer token never appears in tracing output or panic messages.
- All error paths use crate::Error::context (no bare unwrap on IO/HTTP).
- No hardcoded internal hostnames, IPs, or tokens introduced.
…ovider

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… literals

Add the missing `forkd` field to both the `ServerSandboxProvidersLayer`
struct (layers/server.rs) and the `ServerSandboxProvidersSettings` literal
in `resolve_sandbox` (resolve/server.rs).

The `forkd` field was added to `ServerSandboxProvidersSettings` in
fabro-types but the config-layer structs and the resolver literal were not
updated, causing E0063 (missing field) when building with --features forkd.

Both sites now mirror the `daytona` field pattern exactly.
Add missing SandboxProviderKind::Forkd => arms to every match block that
covered Daytona but not Forkd, fixing non-exhaustive match errors when
building with --features forkd.

Files changed:
- fabro-install/src/lib.rs: Forkd => false (install wizard does not offer forkd; configured via settings/env)
- fabro-sandbox/src/sandbox_spec.rs: add SandboxSpec::Forkd variant + provider()/provider_name()/build()/to_run_sandbox_instance() arms
- fabro-sandbox/src/reconnect.rs: #[cfg(feature="forkd")] Forkd reconnect arm + not-enabled fallback
- fabro-sandbox/src/terminal.rs: Forkd => error (terminals not supported)
- fabro-server/src/server/handler/sandbox.rs: Forkd => conflict (SSH access not supported)
- fabro-server/src/run_manifest.rs: Forkd in clone_disabled_for_provider + preflight_sandbox_spec
- fabro-workflow/src/operations/start.rs: Forkd => SandboxSpec::Forkd arm
provider/forkd.rs called sandbox.initialize() on a ForkdSandbox value but
did not import the Sandbox trait, causing E0599. Mirror daytona.rs's idiom:
  use crate::{Sandbox, details};

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
forkd was registered as a SandboxProviderKind but EnvironmentProvider (the
run-level environment selector) had no Forkd variant, so resolve_sandbox_provider()
could never route a run to forkd and the SandboxProviderKind::Forkd => SandboxSpec::Forkd
arm in start.rs was unreachable. Add the Forkd variant + From<EnvironmentProvider>
mapping + is_clone_based, plus the matching arms in the exhaustive matches:
default environment TOML (store.rs), capability validation (environment.rs), and
capability warnings (run_manifest.rs). Verified: cargo zigbuild --release
--target x86_64-unknown-linux-musl -p fabro-cli --features forkd compiles green;
binary --version rc=0.
…wire feature end-to-end

Rewrites the forkd client to the actual controller API and makes forkd usable in
a real workflow run (validated live: fabro provisioned a microVM "Sandbox: forkd
ready in 74ms" and executed inside it against a zen-gate-base snapshot).

- forkd/mod.rs: /v1/sandboxes snapshot API — create POST {snapshot_tag} -> id from
  response[0] (array), exec POST /{id}/exec {args,timeout_secs} (argv-only; cwd/env
  folded into sh -lc), delete /{id}; token-redacted Debug; retry/backoff preserved.
- provider/forkd.rs: list/get/delete -> /v1/sandboxes; create uses server-assigned
  sandbox_id() (drops caller-chosen vm_name).
- config.rs/from_environment.rs: snapshot_tag (FORKD_SNAPSHOT_TAG, default zen-gate-base).
- store.rs: seed forkd into seeded_catalog_layer so --environment forkd resolves.
- Cargo: fabro-workflow gains a forkd feature; fabro-cli + fabro-server pass
  fabro-workflow/forkd through (previously the worker compiled the disabled arm ->
  "Forkd sandbox support is not enabled" at run time).
- specs/forkd-e2e/behavior.feature: BDD contract for create/exec/delete + real run.

Reviewed: 3 adversarial workflow reviewers (8 issues fixed) + ocr (no CRITICAL/HIGH).
Follow-ups (LOW, ocr): FORKD_ENVIRONMENT_ID const; Ready event memory field; install wizard forkd.
@brynary

brynary commented Jun 26, 2026

Copy link
Copy Markdown
Member

@zenprocess thanks for this PR. I've begun a manual review. Primarily my review is bringing to light some pre-existing architectural design issues that we may want to resolve before adding the next sandbox provider. More soon.

@zenprocess

Copy link
Copy Markdown
Author

Thanks for the update, feel free to share your architectural concerns perhaps we can contribute with ideas!

@zenprocess

Copy link
Copy Markdown
Author

Superseded by #567 — we rebased this provider onto current main (this branch had fallen ~50 commits behind, which left it in a diverged/unknown-mergeable state). #567 is the same forkd provider plus feature-gating fixes, reconnect env resolution, a DB migration for the environments provider CHECK, and a docker-build --features passthrough. @brynary — your note here about pre-existing architectural design issues to resolve before adding the next sandbox provider never got the follow-up detail; we'd genuinely like to align on it. #567 deliberately conforms to the existing Sandbox trait / provider-registry / clone-based contracts rather than changing them, so if there's a refactor you want as a precondition, name it and we'll take it on. Happy to move that to an Issue/Discussion if that's the better venue per CONTRIBUTING.

@swerner

swerner commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closing this one as it is superseded by #567

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants