feat(sandbox): forkd Firecracker microVM provider - #517
Conversation
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.
|
@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. |
|
Thanks for the update, feel free to share your architectural concerns perhaps we can contribute with ideas! |
|
Superseded by #567 — we rebased this provider onto current |
|
Closing this one as it is superseded by #567 |
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:
step a real VM boundary (vs Docker's shared host kernel), closer to Daytona's
isolation but without a hosted control plane.
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.
isolation with container-like start times, so per-step VMs stay practical.
It is purely additive: a new
SandboxProviderselected via config, mirroring theexisting daytona provider's structure. Local/Docker/Daytona are unchanged.
What
SandboxCreateSpec::Forkd+SandboxProviderKind::ForkdForkdConfig/ForkdSettings(controller URL, token, snapshot/image) in configForkdSandboxProvider(implSandboxProvider: create/list/get/delete)ForkdSandbox(impl the fullSandboxtrait over a microVM: exec, streaming,file up/down, lifecycle, git)
from_environmentwiring; tests mirroring the sandbox round-tripsCredentials (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.