Architecture
NanoVM is a shared kernel (bus, virtual filesystem, process router, networking, capabilities, services) with four peer runners that each execute one kind of program on a different engine, plus apps delivered through a signed catalog.
┌──────────────────────────────┐
│ kernel/ │
│ bus · VFS · router · net · │
│ caps · services · platform │
└──────────────┬───────────────┘
register delegates via │ router (runners never import each other)
┌───────────────┬──────────────┼───────────────┐
▼ ▼ ▼ ▼
runners/riscv runners/node runners/wasm runners/boa
RV64 ELF emu Node.js on wasm32-wasip1 sandboxed JS
(Rust→wasm) host JS commands (Boa, Rust→wasm)
▲
│ apps (ripgrep, node, …) target a runner ABI, delivered via the catalogThe kernel
kernel/vfs/memfs.mjs) every runner reads and writes/dev/__net__)The runners
The RISC-V runner is the original NanoVM and the oracle the faster runners are tested against: differential tests run the same program on nodert and on the real Node.js ELF in the VM and fail on any difference not recorded in the divergence registry (runners/node/DIVERGENCES.md; see Limitations).
The dependency rule
- A runner imports only from
kernel/(pluskernel/platform.mjsfor host infrastructure). - No runner imports another runner. Cross-runner work goes through
router.route(), the bus and the shared VFS. - Apps target a runner ABI (
riscv64gc-linux-musl,wasm32-wasip1) and import nothing; the catalog delivers them. - Known exception:
runners/riscv/host/nanovm.mjslazily importsrunners/boa/host/boa.mjsfor scripting.
Three runners, one filesystem
Node.js (host engine) writes a file, BusyBox (RISC-V) transforms it, and a wasm app reads the image next to it, all through the shared VFS.
await nano.node(["-e", "require('fs').writeFileSync('/work/msg.txt', 'written by node\\n')"]);
await nano.shExec("tr a-z A-Z < /work/msg.txt > /work/MSG.txt");
print(nano.fs.readText("/work/MSG.txt").trim());
await nano.registerWasmApp("photon", new Uint8Array(await (await fetch("/nano/photon.wasm")).arrayBuffer()));
nano.fs.writeFile("/work/in.jpg", new Uint8Array(await (await fetch("/demo-files/photon/sample.jpg")).arrayBuffer()));
print((await nano.execWasmApp(["photon", "in.jpg", "out.png", "--filter", "grayscale"], { cwd: "/work" })).stdout.trim());
print((await nano.shExec("ls /work | tr '\\n' ' '")).stdout.trim());WRITTEN BY NODE photon: grayscale 360x240 -> out.png (360x240 PNG) MSG.txt in.jpg msg.txt out.png
The SDK on top
@userland-run/nano-sdk vendors the kernel and runners and adds the typed API: createNano, the Shell, serve mode, the worker transport, the catalog client and provision(). The terminal UI (terminal repository) ships as the SDK's /terminal subpath.
Terminal: model and renderer
The terminal's model lives in the guest: term.rs (an ANSI/vte parser and a fixed cell grid) and tty.rs (line discipline and stdin ring) are compiled into nano.wasm. The <nano-terminal> element reads the grid out of linear memory and renders it with WebGPU or a 2D canvas. See Embedded terminal.
Product-level tours of each runner: RISC-V emulator, WASI apps, JS sandbox; and the cross-tier example.
Repositories
@userland-run/nano-sdk<nano-terminal> (re-exported by the SDK)