Skip to content

WASI shell commands cannot read from host-dir-backend mounts #1439

@atsushi-ishibashi

Description

@atsushi-ishibashi

Summary

WASM shell commands (cat, ls, etc.) fail when reading from createHostDirBackend mounts. The kernel API (vm.readFile() / vm.writeFile()) and shell writes work correctly -- only WASI reads on mounted paths are broken.

The root cause: WASM binaries use Node.js native WASI whose preopens only maps /workspace to process.cwd(). Kernel mount points (e.g., /mnt/session) are invisible to the WASI runtime because it bypasses the kernel VFS entirely.

Reproduction

import { AgentOs, createHostDirBackend } from "@rivet-dev/agent-os-core";
import common from "@rivet-dev/agent-os-common";
import fs from "node:fs";

fs.mkdirSync("/tmp/test-host/workspace", { recursive: true });

const vm = await AgentOs.create({
  software: [common],
  mounts: [
    {
      path: "/mnt/session",
      driver: createHostDirBackend({ hostPath: "/tmp/test-host", readOnly: false }),
    },
  ],
});

// Kernel API -- works
await vm.writeFile("/mnt/session/workspace/hello.txt", "hello world");
const content = new TextDecoder().decode(
  await vm.readFile("/mnt/session/workspace/hello.txt")
);
console.log(content); // "hello world"

// Shell write -- works
await vm.exec('echo "from shell" > /mnt/session/workspace/shell.txt');
// Host file /tmp/test-host/workspace/shell.txt is created correctly

// Shell read -- FAILS
const catResult = await vm.exec("cat /mnt/session/workspace/hello.txt");
console.log(catResult.stdout);   // ""
console.log(catResult.exitCode); // 1
console.log(catResult.stderr);
// "WARN could not retrieve pid for child process"
// "The number ... cannot be converted to a BigInt because it is not an integer"

// Shell ls -- FAILS
const lsResult = await vm.exec("ls /mnt/session/workspace/");
console.log(lsResult.stdout);   // ""
console.log(lsResult.exitCode); // 1

// Control: same commands on in-memory VFS work fine
await vm.writeFile("/tmp/control.txt", "ok");
const ctrl = await vm.exec("cat /tmp/control.txt");
console.log(ctrl.stdout); // "ok", exitCode=0

await vm.dispose();

Observed behavior

Operation Result
vm.writeFile() on mount OK
vm.readFile() on mount OK
vm.exec('echo > file') on mount OK
vm.exec('cat file') on mount Empty stdout, exitCode=1, BigInt error
vm.exec('ls dir/') on mount Empty stdout, exitCode=1
vm.exec('cat file') on in-memory VFS OK

Where the bug is

buildPreopens() in crates/execution/src/node_import_cache.rs (line 7748) hardcodes a single WASI preopen:

function buildPreopens() {
  // ...
  return { '/workspace': process.cwd() };
}

This is the only filesystem mapping the WASI runtime receives (line 7895):

const wasi = new WASI({
  version: 'preview1',
  preopens: buildPreopens(), // only '/workspace'
  // ...
});

Kernel mount points like /mnt/session are never passed to the WASI preopens. The kernel API works because Node.js fs calls are intercepted by ESM loader hooks and routed through the kernel VFS via sync RPC (service.rs:8172), which correctly resolves mounts. But WASM binaries issue native WASI syscalls that bypass the kernel entirely.

Additionally, configure_wasm_node_sandbox() in crates/execution/src/wasm.rs (line 598) only adds host filesystem paths (sandbox root, cache, module path) to --allow-fs-read/--allow-fs-write -- mount host paths are not included.

Test coverage gap

No existing test combines vm.exec() with createHostDirBackend mounts:

  • host-dir-backend.test.ts -- kernel API only
  • execute.test.ts -- shell commands on in-memory VFS only
  • native-sidecar-process.test.ts -- mounts via client.readFile() only

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions