sandy runs a job in an ephemeral NixOS microVM and returns a locked result
envelope. One VmBackend seam launches the guest (qemu on Linux, vfkit on
macOS); inputs, outputs, secrets, and a scoped forge credential travel in over
virtio-fs shares; an egress allow-list wraps the boot.
A job boots a real microVM and runs end to end on both backends, verified by host-gated live tests on Linux/KVM and Apple-silicon hosts.
Every hard boundary behind the library is a trait with one implementation per target — the extension points a new backend, transport, credential provider, or egress applier plugs into.
flowchart LR
subgraph seams["Seams (sandy library)"]
VmBackend
GuestShareTransport
CredentialProvider
EgressApplier
end
VmBackend --> QemuBackend
VmBackend --> VfkitBackend
VmBackend --> FakeBackend["FakeBackend (tests)"]
GuestShareTransport --> VirtiofsdTransport["VirtiofsdTransport (qemu)"]
GuestShareTransport --> VirtioFsTransport["VirtioFsTransport (vfkit)"]
CredentialProvider --> GithubAdapter
CredentialProvider --> FakeProvider["FakeProvider (tests)"]
EgressApplier --> NftablesApplier["NftablesApplier (Linux)"]
EgressApplier --> FakeApplier["FakeApplier (tests)"]
The run pipeline, each subsystem's design, and the live acceptance matrix are
in docs/architecture/.
sandy doctor # verify host prerequisites
sandy run <template> [flags] -- <cmd...><template> is a guest flake attr (e.g. topologies.aarch64-linux.vfkit). The
job command runs inside the guest; its exit code and the guest console come back in
the result envelope. Flags (repeatable where it makes sense):
| Flag | Effect |
|---|---|
--input HOST:NAME |
mount the host file/dir read-only at /run/sandy/inputs/NAME (write → EROFS) |
--out DEST / --out NAME:DEST |
collect everything under /run/sandy/outputs (or just /run/sandy/outputs/NAME) to the host DEST at teardown |
--secret NAME=@PATH |
inject the file's bytes as $NAME in-guest (never on argv/env) |
--env KEY=VALUE |
set an environment variable in-guest |
--credential <name> |
mint a scoped token from a sandy cred github trust root, inject it as $GITHUB_TOKEN, revoke at teardown |
--allow HOST:PORT |
egress allow-list entry (enforced on Linux; macOS is unfiltered) |
--cpu N / --mem MiB / --timeout SECS |
resource knobs |
-o plain (default) / -o json |
human summary, or the full structured envelope for agents |
# read an input, write a collected output, with a scoped GitHub token
sandy run topologies.aarch64-linux.vfkit \
--input ./data:data --out result:./out --credential sandy-runner \
-- sh -c 'process /run/sandy/inputs/data > /run/sandy/outputs/result/report'sandy ls # list every job record
sandy status <job-id> # reconcile + print one job's current state
sandy wait <job-id> # block until terminal, then print it
sandy kill <job-id> # cancel a running job and tear its box down
sandy gc [--keep-last N] # evict terminal records, keep the newest N (default 20)Credential trust roots are managed with
sandy cred github register | import | verify | deregister (see
docs/providers/github).
sandy daemon submit <manifest|-> --manifest-encoding <ENCODING>
sandy daemon status|attach|cancel <job-id>
sandy daemon listsandy daemon drives sandyd, an evolving control-plane daemon with its own
job model (see docs/decisions/sandyd.md).
Build, test, and contribution workflow are in AGENTS.md — the
canonical source Claude Code and GitHub Copilot both read natively. Full
architecture, design decisions, and subsystem specs are under
docs/.
Sandy is dual-licensed:
- Open source: GNU AGPL-3.0-only. Free to use, modify, and distribute under the AGPL's terms, including its network-use copyleft (a service built on Sandy must offer its complete corresponding source).
- Commercial: a separate commercial license is available for use that cannot meet the AGPL's obligations, such as proprietary or closed-source products and services. See COMMERCIAL.md.
Contributions are accepted under the Contributor License Agreement in CONTRIBUTING.md, which lets the project offer the commercial license above.