diff --git a/README.md b/README.md index 97bfea1..ca0baae 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ box down devbox ```yaml base: docker.io/library/alpine:latest -backend: podman # podman, krun or firecracker +backend: auto # firecracker when it fits, else podman; or name one isolation: strict # the VMM runs as a UID that is not yours network: bridge # or none, for no network at all warm: 2 # keep VMs booted so runs start in ms diff --git a/docs/docs.html b/docs/docs.html index 71e4747..e896d5d 100644 --- a/docs/docs.html +++ b/docs/docs.html @@ -77,7 +77,7 @@

Boxfile

One YAML file per sandbox at ~/.box/sandboxes/<name>/Boxfile. Unknown keys are rejected, so a typo fails loudly.

- +
FieldValuesDefaultMeaning
baseimage referencealpine:latestThe image every VM starts from.
backendpodman, krun, firecrackerpodmanWhat boots the VM. See Architecture.
isolationstandard, strictstandardstrict runs the VMM as a subordinate UID of the sandbox's own.
cpus1 to 162vCPUs.
ram_mib128 and up2048Guest RAM in MiB.
networkbridge, nonebridgeInternet access, or none at all.
readonlyboolfalseRead-only root; /tmp and /data stay writable.
timeout_secondsint0Per-command limit; exits 124. 0 is unlimited.
warm0 to 80VMs kept booted for run (podman, krun).
warmupshell linesRun once in each warm VM before it serves a run.
packagesnamesInstalled with the image's package manager.
runshell linesExtra build steps.
envmapEnvironment for every command.
mountshost, guest, modeHost directories shared in (podman, krun); ro by default.
blueprintusers, write_files, runcmdProvisioning applied at build time.
pkgmgrapk, apt, dnfinferredOverride the package manager.
passtboolfalseA real NIC with a default route (podman, krun).
seccomppathA seccomp profile for the VMM.
baseimage referencealpine:latestThe image every VM starts from.
backendauto, podman, krun, firecrackerpodman (new Boxfiles: auto)What boots the VM. auto picks firecracker when the sandbox is offline, has no host mounts, no warm pool and no forks, and the host can run it; podman otherwise. The pick is made once and shown by box inspect. See Architecture.
isolationstandard, strictstandardstrict runs the VMM as a subordinate UID of the sandbox's own.
cpus1 to 162vCPUs.
ram_mib128 and up2048Guest RAM in MiB.
networkbridge, nonebridgeInternet access, or none at all.
readonlyboolfalseRead-only root; /tmp and /data stay writable.
timeout_secondsint0Per-command limit; exits 124. 0 is unlimited.
warm0 to 80VMs kept booted for run (podman, krun).
warmupshell linesRun once in each warm VM before it serves a run.
packagesnamesInstalled with the image's package manager.
runshell linesExtra build steps.
envmapEnvironment for every command.
mountshost, guest, modeHost directories shared in (podman, krun); ro by default.
blueprintusers, write_files, runcmdProvisioning applied at build time.
pkgmgrapk, apt, dnfinferredOverride the package manager.
passtboolfalseA real NIC with a default route (podman, krun).
seccomppathA seccomp profile for the VMM.

CLI

diff --git a/docs/index.html b/docs/index.html index b21e964..c4d8fbf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -124,7 +124,7 @@

The whole sandbox is one file.

box builds the image, checks that the sandbox really has its own kernel, and refuses to run it if not. Hover over a line to see what it does.

-
# agent: everything this sandbox isbase: docker.io/library/alpine:latestbackend: firecrackerisolation: strictcpus: 2ram_mib: 1024network: nonereadonly: truetimeout_seconds: 60packages: [python3, py3-pip]env: {PYTHONUNBUFFERED: "1"}
+
# agent: everything this sandbox isbase: docker.io/library/alpine:latestbackend: firecrackerisolation: strictcpus: 2ram_mib: 1024network: nonereadonly: truetimeout_seconds: 60packages: [python3, py3-pip]env: {PYTHONUNBUFFERED: "1"}

@@ -229,7 +229,7 @@

Not sure it fits? Ask your AI.

-
you5.2 KB · plain text
+
you5.5 KB · plain text
I'm evaluating box (https://github.com/chxperiments/box) for something I'm building. Help me decide whether it fits, and if it does, how to set it up correctly. Use only the facts below about box; if something isn't covered here, say so instead of guessing.
 
 ## What box is
@@ -244,7 +244,8 @@ 

Not sure it fits? Ask your AI.

- `box data import|export <name> <dir>` copies files into or out of `/data`. Exported files are treated as hostile (symlinks never followed, setuid dropped). ## Backends (`backend:` in the Boxfile) -- podman (default): podman with the krun runtime (libkrun). Supports everything. Cold run ~1.3–1.5 s. +- auto (what new Boxfiles say): box picks once per sandbox and records why: firecracker when the Boxfile is offline, has no host mounts, no warm pool and no forks, and the host can run it; podman otherwise. It never moves a sandbox whose /data is on the other engine; it stops and says how to move the data. `box inspect` shows the choice. +- podman (a Boxfile without the key): podman with the krun runtime (libkrun). Supports everything. Cold run ~1.3–1.5 s. - krun: drives crun's libkrun handler directly. Cold run ~0.75–1.1 s. `isolation: standard` only for now. - firecracker: restores a snapshot per run, ~0.3 s, no pool needed. x86_64 Linux only; no networking, no host mounts, no forks yet. Measured: a warm `run` is ~20–45 ms; `exec` in a running sandbox is ~15 ms for a no-op. @@ -273,8 +274,8 @@

Not sure it fits? Ask your AI.

diff --git a/internal/boxfile/boxfile.go b/internal/boxfile/boxfile.go index b467ae5..8bfa735 100644 --- a/internal/boxfile/boxfile.go +++ b/internal/boxfile/boxfile.go @@ -107,8 +107,30 @@ var Default = Spec{ // Backends are the values `backend:` accepts. podman drives crun+libkrun // through podman; krun drives crun+libkrun directly, skipping podman's -// per-boot cost; firecracker uses the Firecracker VMM. -var Backends = []string{"podman", "krun", "firecracker"} +// per-boot cost; firecracker uses the Firecracker VMM. auto lets box pick, +// once per sandbox: firecracker when it fits the Boxfile and the host, +// podman otherwise. A Boxfile without the key keeps podman, so existing +// sandboxes never move; new Boxfiles say auto. +var Backends = []string{"auto", "podman", "krun", "firecracker"} + +// FirecrackerFits says what in a Boxfile the firecracker backend cannot do, +// or nil. backend: firecracker refuses such a Boxfile; backend: auto picks +// podman for it instead. +func FirecrackerFits(s Spec) error { + // /data is a disk there, not a shared directory, and a restore from a + // snapshot already starts in tens of milliseconds. + switch { + case len(s.Mounts) > 0: + return errors.New("backend: firecracker has no host mounts; move files with `box data import`") + case s.Warm > 0: + return errors.New("backend: firecracker restores a snapshot per run; drop warm") + case s.Network != "none": + return errors.New("backend: firecracker supports network: none for now") + case s.Passt: + return errors.New("backend: firecracker does not use passt") + } + return nil +} // BackendNames lists the accepted backends for error messages. func BackendNames() string { return strings.Join(Backends, ", ") } @@ -125,7 +147,7 @@ network: bridge # bridge = internet access, none = offline passt: false # real NIC + default route in the guest (needed for k3s) readonly: false # read-only guest root (/tmp and /data stay writable) isolation: standard # strict: VMM runs as a UID that is not yours (use for agents) -backend: podman # podman (default) | krun (direct, faster boots) | firecracker (snapshot per run, no network yet) +backend: auto # auto: firecracker when it fits (offline, no mounts, no warm pool), else podman | podman | krun | firecracker timeout_seconds: 0 # per-run wall clock limit, 0 = unlimited warm: 0 # VMs kept booted so run starts in ms (each holds its RAM) @@ -219,17 +241,8 @@ func (s Spec) validate() error { return fmt.Errorf("backend must be one of %s, got %q", BackendNames(), s.Backend) } if s.Backend == "firecracker" { - // /data is a disk there, not a shared directory, and a restore from - // a snapshot already starts in tens of milliseconds. - switch { - case len(s.Mounts) > 0: - return errors.New("backend: firecracker has no host mounts; move files with `box data import`") - case s.Warm > 0: - return errors.New("backend: firecracker restores a snapshot per run; drop warm") - case s.Network != "none": - return errors.New("backend: firecracker supports network: none for now") - case s.Passt: - return errors.New("backend: firecracker does not use passt") + if err := FirecrackerFits(s); err != nil { + return err } } if s.Isolation != "standard" && s.Isolation != "strict" { diff --git a/internal/cli/commands.go b/internal/cli/commands.go index dde28ae..ff64768 100644 --- a/internal/cli/commands.go +++ b/internal/cli/commands.go @@ -219,11 +219,19 @@ func resetCmd() *cobra.Command { if err := refuseWithForks(name, "reset it"); err != nil { return err } + s, err := loadSpec(name) + if err != nil { + return err + } + fc := s.Backend == "firecracker" // /data is a disk there // Only ask when there is actually something to lose. empty, err := sandbox.DataEmpty(name) if err != nil { return err } + if fc { + empty = !runtime.FirecrackerDataExists(name) + } if !empty { if err := confirm(fmt.Sprintf("Delete everything in %s's /data?", name), yes); err != nil { return err @@ -236,6 +244,11 @@ func resetCmd() *cobra.Command { if err := sandbox.ResetData(name); err != nil { return err } + if fc { + if err := runtime.ResetFirecrackerData(name); err != nil { + return err + } + } runtime.SpawnTender(name) fmt.Printf("reset %s\n", name) return nil diff --git a/internal/cli/fork.go b/internal/cli/fork.go index 85eb74e..0dc75c2 100644 --- a/internal/cli/fork.go +++ b/internal/cli/fork.go @@ -32,6 +32,15 @@ func forkCmd() *cobra.Command { if err := sandbox.ValidName(name); err != nil { return err } + if ps, err := loadSpec(parent); err == nil && ps.Backend == "firecracker" { + why := "its Boxfile says backend: firecracker" + if c, ok := runtime.ReadAutoChoice(parent); ok { + why = "backend: auto picked it: " + c.Reason + } + return fmt.Errorf("%s runs on firecracker (%s), and forks run on podman for now. "+ + "Set backend: podman in %s's Boxfile while its /data is empty, or move the data first "+ + "(box data export, box reset, box build, box data import)", parent, why, parent) + } if err := sandbox.CreateFork(parent, name); err != nil { return err } diff --git a/internal/cli/inspect.go b/internal/cli/inspect.go index ace0abe..fde9ca0 100644 --- a/internal/cli/inspect.go +++ b/internal/cli/inspect.go @@ -60,6 +60,7 @@ func inspectCmd() *cobra.Command { func printReport(r runtime.Report) { fmt.Printf("%s (backend %s, isolation %s)\n", r.Sandbox, r.Backend, r.Isolation) + fmt.Printf("backend chosen by %s\n", r.ChosenBy) fmt.Printf("read from %s\n\n", r.ReadFrom) v := r.VMM diff --git a/internal/runtime/auto.go b/internal/runtime/auto.go new file mode 100644 index 0000000..5a82cdd --- /dev/null +++ b/internal/runtime/auto.go @@ -0,0 +1,147 @@ +package runtime + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "box/internal/boxfile" + "box/internal/sandbox" +) + +// backend: auto lets box pick the engine. It picks firecracker when that +// fits -- the Boxfile asks for nothing firecracker cannot do, the host can +// run it, and the sandbox has no forks -- because its VMM holds no +// capabilities and has the smallest device model; podman otherwise, which +// does everything. +// +// The pick is made once and recorded with the sandbox, because the two keep +// /data differently (a disk on firecracker, a directory on podman), and a +// sandbox that silently changed engine would lose sight of its data. A later +// Boxfile change that the recorded engine cannot serve switches engines only +// while /data is empty; otherwise it stops and says how to move the data. + +// AutoChoice is what backend: auto picked for a sandbox, and why. +type AutoChoice struct { + Backend string `json:"backend"` + Reason string `json:"reason"` + At string `json:"at"` +} + +func autoPath(name string) (string, error) { + dir, err := sandbox.Dir(name) + if err != nil { + return "", err + } + return filepath.Join(dir, "backend.json"), nil +} + +// ReadAutoChoice returns the recorded choice, if backend: auto made one. +func ReadAutoChoice(name string) (AutoChoice, bool) { + p, err := autoPath(name) + if err != nil { + return AutoChoice{}, false + } + b, err := os.ReadFile(p) + if err != nil { + return AutoChoice{}, false + } + var c AutoChoice + if json.Unmarshal(b, &c) != nil || c.Backend == "" { + return AutoChoice{}, false + } + return c, true +} + +func writeAutoChoice(name string, c AutoChoice) error { + p, err := autoPath(name) + if err != nil { + return err + } + c.At = time.Now().UTC().Format(time.RFC3339) + b, err := json.MarshalIndent(c, "", " ") + if err != nil { + return err + } + tmp := p + ".tmp" + if err := os.WriteFile(tmp, append(b, '\n'), 0o644); err != nil { + return err + } + return os.Rename(tmp, p) +} + +// firecrackerFits says why firecracker cannot run this sandbox here, or nil. +func firecrackerFits(name string, s boxfile.Spec) error { + if err := boxfile.FirecrackerFits(s); err != nil { + return err + } + if sandbox.IsFork(name) { + return fmt.Errorf("forks run on podman") + } + if forks, _ := sandbox.Forks(name); len(forks) > 0 { + return fmt.Errorf("it has forks, which run on podman") + } + return fcHostCheck() +} + +// fcHostCheck is firecracker's preflight, replaceable in tests. +var fcHostCheck = func() error { return backends["firecracker"].Preflight() } + +// HostFirecracker says whether this host can run firecracker at all, for +// box doctor. +func HostFirecracker() error { return fcHostCheck() } + +// autoPick is the engine backend: auto would choose now, and why. +func autoPick(name string, s boxfile.Spec) (string, string) { + if err := firecrackerFits(name, s); err != nil { + return "podman", "this sandbox needs podman: " + strings.TrimPrefix(err.Error(), "backend: ") + } + return "firecracker", "offline, no host mounts, no warm pool, and the host can run it: firecracker's VMM holds no capabilities" +} + +func backendFits(backend, name string, s boxfile.Spec) bool { + if backend == "firecracker" { + return firecrackerFits(name, s) == nil + } + return true // podman runs anything a Boxfile can ask for +} + +// dataInUse reports whether a sandbox has data where backend keeps it. +func dataInUse(name, backend string) bool { + if backend == "firecracker" { + return FirecrackerDataExists(name) + } + empty, err := sandbox.DataEmpty(name) + return err == nil && !empty +} + +// resolveAuto turns backend: auto into the sandbox's engine. +func resolveAuto(name string, s boxfile.Spec) (boxfile.Spec, error) { + pick, why := autoPick(name, s) + rec, ok := ReadAutoChoice(name) + switch { + case !ok: + if err := writeAutoChoice(name, AutoChoice{Backend: pick, Reason: why}); err != nil { + return s, err + } + s.Backend = pick + case rec.Backend == pick || backendFits(rec.Backend, name, s): + s.Backend = rec.Backend // stay where the data is + case dataInUse(name, rec.Backend): + return s, fmt.Errorf("%s runs on %s (backend: auto picked it: %s), which cannot serve its Boxfile any more, "+ + "and its /data is there. To move it, set the Boxfile back, then:\n"+ + " box data export %s ./%s-data && box reset %s -y\n"+ + "change the Boxfile again, and:\n"+ + " box build %s && box data import %s ./%s-data", + name, rec.Backend, rec.Reason, name, name, name, name, name, name) + default: + if err := writeAutoChoice(name, AutoChoice{Backend: pick, Reason: why}); err != nil { + return s, err + } + s.Backend = pick + } + return s, nil +} diff --git a/internal/runtime/auto_test.go b/internal/runtime/auto_test.go new file mode 100644 index 0000000..a18cc44 --- /dev/null +++ b/internal/runtime/auto_test.go @@ -0,0 +1,126 @@ +package runtime + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "box/internal/sandbox" +) + +// autoSandbox writes a Boxfile and points the firecracker host check at +// hostOK, so the tests do not depend on the machine running them. +func autoSandbox(t *testing.T, name, boxfile string, hostOK bool) { + t.Helper() + old := fcHostCheck + fcHostCheck = func() error { + if hostOK { + return nil + } + return errors.New("no KVM here") + } + t.Cleanup(func() { fcHostCheck = old }) + dir, err := sandbox.Dir(name) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "Boxfile"), []byte(boxfile), 0o644); err != nil { + t.Fatal(err) + } + if data, err := sandbox.DataDir(name); err == nil { + os.MkdirAll(data, 0o755) + } +} + +const offline = "base: alpine\nnetwork: none\nbackend: auto\n" +const online = "base: alpine\nnetwork: bridge\nbackend: auto\n" + +func resolvedBackend(t *testing.T, name string) string { + t.Helper() + s, err := parseSpec(name) + if err != nil { + t.Fatal(err) + } + return s.Backend +} + +func TestAutoPicksFirecrackerWhenItFits(t *testing.T) { + t.Setenv("BOX_HOME", t.TempDir()) + autoSandbox(t, "off", offline, true) + if got := resolvedBackend(t, "off"); got != "firecracker" { + t.Fatalf("offline sandbox on a capable host: %s", got) + } + if c, ok := ReadAutoChoice("off"); !ok || c.Backend != "firecracker" || c.Reason == "" { + t.Errorf("choice not recorded: %+v %v", c, ok) + } +} + +func TestAutoPicksPodmanOtherwise(t *testing.T) { + t.Setenv("BOX_HOME", t.TempDir()) + autoSandbox(t, "on", online, true) + if got := resolvedBackend(t, "on"); got != "podman" { + t.Errorf("networked sandbox: %s", got) + } + c, _ := ReadAutoChoice("on") + if !strings.Contains(c.Reason, "network: none") { + t.Errorf("reason does not say why: %q", c.Reason) + } + autoSandbox(t, "nokvm", offline, false) + if got := resolvedBackend(t, "nokvm"); got != "podman" { + t.Errorf("host without firecracker: %s", got) + } +} + +func TestAutoStaysWhereTheDataIs(t *testing.T) { + t.Setenv("BOX_HOME", t.TempDir()) + // Picked podman while online; going offline would now fit firecracker, + // but the sandbox stays on podman. + autoSandbox(t, "a", online, true) + resolvedBackend(t, "a") + autoSandbox(t, "a", offline, true) + if got := resolvedBackend(t, "a"); got != "podman" { + t.Errorf("moved off podman: %s", got) + } +} + +func TestAutoSwitchesOnlyWithoutData(t *testing.T) { + t.Setenv("BOX_HOME", t.TempDir()) + autoSandbox(t, "b", offline, true) + if resolvedBackend(t, "b") != "firecracker" { + t.Fatal("setup") + } + // It goes online, which firecracker cannot do, and has no data: switch. + autoSandbox(t, "b", online, true) + if got := resolvedBackend(t, "b"); got != "podman" { + t.Errorf("empty sandbox did not switch: %s", got) + } + + autoSandbox(t, "c", offline, true) + resolvedBackend(t, "c") + disk, _ := fcDataDiskPath("c") + os.MkdirAll(filepath.Dir(disk), 0o700) + os.WriteFile(disk, []byte("data"), 0o600) + autoSandbox(t, "c", online, true) + if _, err := parseSpec("c"); err == nil || !strings.Contains(err.Error(), "box data export c") { + t.Errorf("a sandbox with data on firecracker switched or said nothing useful: %v", err) + } + if c, _ := ReadAutoChoice("c"); c.Backend != "firecracker" { + t.Errorf("the record moved anyway: %s", c.Backend) + } +} + +func TestNoBackendKeyStaysPodman(t *testing.T) { + t.Setenv("BOX_HOME", t.TempDir()) + autoSandbox(t, "old", "base: alpine\nnetwork: none\n", true) + if got := resolvedBackend(t, "old"); got != "podman" { + t.Errorf("an existing Boxfile without backend: moved to %s", got) + } + if _, ok := ReadAutoChoice("old"); ok { + t.Error("recorded an auto choice for a sandbox that did not ask for one") + } +} diff --git a/internal/runtime/doctor.go b/internal/runtime/doctor.go index cbdd26a..c79377d 100644 --- a/internal/runtime/doctor.go +++ b/internal/runtime/doctor.go @@ -96,6 +96,16 @@ func Doctor() []Check { add(check) } + // What backend: auto can choose here. Not a failure: podman runs + // everything, firecracker is the stronger choice when it fits. + if err := HostFirecracker(); err != nil { + add(Check{Name: "backend: auto", OK: true, + Detail: "picks podman here; firecracker is not available (" + err.Error() + ")"}) + } else { + add(Check{Name: "backend: auto", OK: true, + Detail: "picks firecracker for offline sandboxes without host mounts or a warm pool, podman otherwise"}) + } + if exe, err := os.Executable(); err == nil { if dyn, err := dynamicallyLinked(exe); err == nil && dyn { add(Check{Name: "static binary", Warn: true, diff --git a/internal/runtime/fc_image.go b/internal/runtime/fc_image.go index 8442632..9632bf2 100644 --- a/internal/runtime/fc_image.go +++ b/internal/runtime/fc_image.go @@ -168,7 +168,8 @@ mv /out/rootfs.ext4.partial /out/rootfs.ext4` // fcDataDisk returns the sandbox's /data disk, creating an empty ext4 // filesystem on first use. It is sparse: its size is a ceiling, not a cost. -func fcDataDisk(name string) (string, error) { +// fcDataDiskPath is where a sandbox's /data disk lives, without creating it. +func fcDataDiskPath(name string) (string, error) { h, err := sandbox.Home() if err != nil { return "", err @@ -176,7 +177,37 @@ func fcDataDisk(name string) (string, error) { if err := sandbox.ValidName(name); err != nil { return "", err } - disk := filepath.Join(h, "disks", name+".ext4") + return filepath.Join(h, "disks", name+".ext4"), nil +} + +// ResetFirecrackerData deletes a sandbox's /data disk; the next boot makes +// an empty one. +func ResetFirecrackerData(name string) error { + disk, err := fcDataDiskPath(name) + if err != nil { + return err + } + if err := os.Remove(disk); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +// FirecrackerDataExists reports whether a sandbox has a /data disk. +func FirecrackerDataExists(name string) bool { + disk, err := fcDataDiskPath(name) + if err != nil { + return false + } + _, err = os.Stat(disk) + return err == nil +} + +func fcDataDisk(name string) (string, error) { + disk, err := fcDataDiskPath(name) + if err != nil { + return "", err + } if _, err := os.Stat(disk); err == nil { return disk, nil } diff --git a/internal/runtime/pool.go b/internal/runtime/pool.go index 5c33485..b2755e7 100644 --- a/internal/runtime/pool.go +++ b/internal/runtime/pool.go @@ -337,7 +337,12 @@ func parseSpec(name string) (boxfile.Spec, error) { if err != nil { return boxfile.Spec{}, err } - return boxfile.Parse(p) + s, err := boxfile.Parse(p) + if err != nil || s.Backend != "auto" { + return s, err + } + // Everything past here sees the engine auto picked, never "auto". + return resolveAuto(name, s) } // TendLog records a background tender's failure where `box logs` shows diff --git a/internal/runtime/report.go b/internal/runtime/report.go index 458cf48..6b51f50 100644 --- a/internal/runtime/report.go +++ b/internal/runtime/report.go @@ -27,6 +27,7 @@ import ( type Report struct { Sandbox string `json:"sandbox"` Backend string `json:"backend"` + ChosenBy string `json:"chosen_by"` // the Boxfile, or backend: auto and why Isolation string `json:"isolation"` ReadFrom string `json:"read_from"` VMM VMMReport `json:"vmm"` @@ -119,7 +120,13 @@ func SecurityReport(name string, s boxfile.Spec) (Report, error) { if err != nil { return Report{}, err } - r := Report{Sandbox: name, Backend: b.Name(), Isolation: s.Isolation} + r := Report{Sandbox: name, Backend: b.Name(), Isolation: s.Isolation, ChosenBy: "the Boxfile"} + if c, ok := ReadAutoChoice(name); ok && c.Backend == b.Name() { + r.ChosenBy = "backend: auto (" + c.Reason + ")" + } + if os.Getenv("BOX_BACKEND") != "" { + r.ChosenBy = "BOX_BACKEND, overriding the Boxfile" + } if r.Isolation == "" { r.Isolation = "standard" } @@ -328,7 +335,7 @@ func parseVolume(v string) (MountReport, bool) { func dataReport(name string, s boxfile.Spec) DataReport { if s.Backend == "firecracker" { - d, _ := fcDataDisk(name) + d, _ := fcDataDiskPath(name) // a report never creates the disk dr := DataReport{Host: d, Kind: "disk"} statInto(&dr, d) return dr diff --git a/tools/site/build.py b/tools/site/build.py index 6c6fac9..2141ec0 100644 --- a/tools/site/build.py +++ b/tools/site/build.py @@ -194,7 +194,7 @@ def bars(): "The image every VM of this sandbox starts from. The package manager is inferred from it.", "FROM docker.io/library/alpine:latest"), ("backend", "backend: firecracker", - "What boots the VM: podman (default), krun (crun with libkrun, no podman), or firecracker (a snapshot restored per run).", + "What boots the VM: auto (box picks firecracker when it fits, else podman, once per sandbox), podman, krun (crun with libkrun, no podman), or firecracker (a snapshot restored per run).", "a fresh VM in about 0.3 s, restored from a snapshot"), ("isolation", "isolation: strict", "The VMM process runs as a subordinate host UID that owns nothing of yours. Recommended for agents.", @@ -691,7 +691,7 @@ def api_code(name, py, ts, go, rs): BOXFILE_FIELDS = [ ("base", "image reference", "alpine:latest", "The image every VM starts from."), - ("backend", "podman, krun, firecracker", "podman", "What boots the VM. See Architecture."), + ("backend", "auto, podman, krun, firecracker", "podman (new Boxfiles: auto)", "What boots the VM. auto picks firecracker when the sandbox is offline, has no host mounts, no warm pool and no forks, and the host can run it; podman otherwise. The pick is made once and shown by box inspect. See Architecture."), ("isolation", "standard, strict", "standard", "strict runs the VMM as a subordinate UID of the sandbox's own."), ("cpus", "1 to 16", "2", "vCPUs."), ("ram_mib", "128 and up", "2048", "Guest RAM in MiB."), diff --git a/tools/site/fit_prompt.txt b/tools/site/fit_prompt.txt index cc92f56..11e4258 100644 --- a/tools/site/fit_prompt.txt +++ b/tools/site/fit_prompt.txt @@ -12,7 +12,8 @@ An open-source CLI (Go, MIT) that runs code in disposable microVMs on your own m - `box data import|export ` copies files into or out of `/data`. Exported files are treated as hostile (symlinks never followed, setuid dropped). ## Backends (`backend:` in the Boxfile) -- podman (default): podman with the krun runtime (libkrun). Supports everything. Cold run ~1.3–1.5 s. +- auto (what new Boxfiles say): box picks once per sandbox and records why: firecracker when the Boxfile is offline, has no host mounts, no warm pool and no forks, and the host can run it; podman otherwise. It never moves a sandbox whose /data is on the other engine; it stops and says how to move the data. `box inspect` shows the choice. +- podman (a Boxfile without the key): podman with the krun runtime (libkrun). Supports everything. Cold run ~1.3–1.5 s. - krun: drives crun's libkrun handler directly. Cold run ~0.75–1.1 s. `isolation: standard` only for now. - firecracker: restores a snapshot per run, ~0.3 s, no pool needed. x86_64 Linux only; no networking, no host mounts, no forks yet. Measured: a warm `run` is ~20–45 ms; `exec` in a running sandbox is ~15 ms for a no-op.