userland.run70Try the demo
Example · cross-tier

Three runners, one filesystem: a cross-tier handoff

node · host JS engineriscv · RISC-V VMboa · sandboxed JS

NanoVM's runners are peers over one kernel: they never call each other directly, but they share the kernel's in-memory filesystem, process table and router. This example makes that visible.

Step 1 runs a Node.js program on the host JS engine that writes report.json. Step 2 reads the same file with BusyBox cat on the RISC-V VM and prints uname -m from the emulated CPU. Step 3 hands the file to a Boa sandbox granted read-only file access. Three engines, one file.

Run it

SDK · JavaScript

Run the handoff

An async function body with nano and print() in scope. Every step reads the file the previous one wrote.

javascript
// Cross-tier handoff over one shared filesystem. This is an SDK snippet: an
// async function body with `nano` (a booted Nano, engines.node = "host",
// scripting enabled) and `print` in scope.
const dir = "/work/handoff";

// 1 · Node.js on the host JS engine (nodert) writes a file…
nano.fs.writeFile(
  `${dir}/producer.js`,
  `const fs = require("fs");
fs.writeFileSync("${dir}/report.json", JSON.stringify({ from: "node " + process.version + " " + process.arch }));
console.log("wrote report.json");`,
);
const a = await nano.node([`${dir}/producer.js`]);
print("[node    · host engine]", a.stdout.trim());

// 2 · …BusyBox on the RISC-V VM reads the same file…
const b = await nano.run(`cat ${dir}/report.json`);
print("[busybox · riscv VM   ]", b.stdout.trim());
const m = await nano.run("uname -m");
print("[busybox · riscv VM   ] uname -m:", m.stdout.trim());

// 3 · …and a sandboxed Boa script reads it too (read-only fs, nothing else).
const from = await nano.script(`JSON.parse(nano.fs.readText("${dir}/report.json")).from`, {
  expose: { fs: "readonly" },
});
print("[boa     · sandbox    ]", from);
loading…
Expected output
[node    · host engine] wrote report.json
[busybox · riscv VM   ] {"from":"node v25.4.0 x64"}
[busybox · riscv VM   ] uname -m: riscv64
[boa     · sandbox    ] node v25.4.0 x64

How it runs

  1. nano.node() runs producer.js on the host-engine Node tier (it reports x64, see the limitations).
  2. nano.run("cat …") runs the BusyBox applet on the RISC-V VM against the same VFS.
  3. nano.script(…, { expose: { fs: "readonly" } }) reads it once more from the Boa sandbox.

Source

From demos/cross-tier/.

handoff.js
javascript
// Cross-tier handoff over one shared filesystem. This is an SDK snippet: an
// async function body with `nano` (a booted Nano, engines.node = "host",
// scripting enabled) and `print` in scope.
const dir = "/work/handoff";

// 1 · Node.js on the host JS engine (nodert) writes a file…
nano.fs.writeFile(
  `${dir}/producer.js`,
  `const fs = require("fs");
fs.writeFileSync("${dir}/report.json", JSON.stringify({ from: "node " + process.version + " " + process.arch }));
console.log("wrote report.json");`,
);
const a = await nano.node([`${dir}/producer.js`]);
print("[node    · host engine]", a.stdout.trim());

// 2 · …BusyBox on the RISC-V VM reads the same file…
const b = await nano.run(`cat ${dir}/report.json`);
print("[busybox · riscv VM   ]", b.stdout.trim());
const m = await nano.run("uname -m");
print("[busybox · riscv VM   ] uname -m:", m.stdout.trim());

// 3 · …and a sandboxed Boa script reads it too (read-only fs, nothing else).
const from = await nano.script(`JSON.parse(nano.fs.readText("${dir}/report.json")).from`, {
  expose: { fs: "readonly" },
});
print("[boa     · sandbox    ]", from);

Further reading

Other examples