Skip to main content
A RuntimeDriver is a pluggable execution engine. The kernel routes commands to drivers based on the command registry. You can write a custom driver for any language or tool.

The RuntimeDriver interface

interface RuntimeDriver {
  /** Unique name for this driver (e.g., "ruby", "deno"). */
  name: string;

  /** Commands this driver handles (e.g., ["ruby", "irb", "gem"]). */
  commands: string[];

  /** Called once when mounting. Receive the KernelInterface for syscalls. */
  init(kernel: KernelInterface): Promise<void>;

  /** Spawn a process. Must return synchronously. */
  spawn(command: string, args: string[], ctx: ProcessContext): DriverProcess;

  /** Clean up all resources. Called during kernel.dispose(). */
  dispose(): Promise<void>;
}

Minimal example: echo driver

A driver that handles one command (myecho) and writes args to stdout:
import type {
  RuntimeDriver,
  KernelInterface,
  ProcessContext,
  DriverProcess,
} from "@secure-exec/kernel";

function createEchoDriver(): RuntimeDriver {
  let kernel: KernelInterface | null = null;

  return {
    name: "echo-driver",
    commands: ["myecho"],

    async init(ki: KernelInterface) {
      kernel = ki;
    },

    spawn(command: string, args: string[], ctx: ProcessContext): DriverProcess {
      if (!kernel) throw new Error("Driver not initialized");

      const output = args.join(" ") + "\n";
      const encoded = new TextEncoder().encode(output);

      // Resolve exit asynchronously (kernel expects this)
      let exitResolve: (code: number) => void;
      const exitPromise = new Promise<number>((r) => { exitResolve = r; });

      const driverProcess: DriverProcess = {
        writeStdin(_data: Uint8Array) { /* ignore stdin */ },
        closeStdin() { /* no-op */ },
        kill(_signal: number) { exitResolve!(128 + _signal); },
        wait() { return exitPromise; },
        onExit: null,
        onStdout: null,
        onStderr: null,
      };

      // Emit output on next microtask (after kernel wires callbacks)
      queueMicrotask(() => {
        driverProcess.onStdout?.(encoded);
        exitResolve!(0);
        driverProcess.onExit?.(0);
      });

      return driverProcess;
    },

    async dispose() {
      kernel = null;
    },
  };
}
Mount and use it:
import { createKernel } from "@secure-exec/kernel";
import { createInMemoryFileSystem } from "@secure-exec/os-browser";

const kernel = createKernel({ filesystem: createInMemoryFileSystem() });
await kernel.mount(createEchoDriver());

const result = await kernel.exec("myecho hello world");
console.log(result.stdout); // "hello world\n"

await kernel.dispose();

DriverProcess lifecycle

spawn() must return a DriverProcess synchronously. The process runs asynchronously.
spawn(command, args, ctx): DriverProcess {
  let exitResolve: (code: number) => void;
  const exitPromise = new Promise<number>((r) => { exitResolve = r; });

  const proc: DriverProcess = {
    writeStdin(data) { /* deliver to process */ },
    closeStdin()     { /* signal EOF */ },
    kill(signal)     { exitResolve(128 + signal); },
    wait()           { return exitPromise; },
    onExit: null,    // kernel sets this after spawn returns
    onStdout: null,  // kernel sets this after spawn returns
    onStderr: null,  // kernel sets this after spawn returns
  };

  // Start async work — emit output via callbacks
  queueMicrotask(() => {
    proc.onStdout?.(new TextEncoder().encode("output\n"));
    exitResolve(0);
    proc.onExit?.(0);
  });

  return proc;
}
The kernel attaches onStdout, onStderr, and onExit callbacks after spawn() returns. If you emit data synchronously during spawn(), use the ctx.onStdout/ctx.onStderr callbacks from ProcessContext instead — the kernel buffers data from both paths.

Callback timing

WhenUseWhy
During spawn() (sync)ctx.onStdout(data)DriverProcess.onStdout isn’t set yet
After spawn() returns (async)proc.onStdout?.(data)Kernel has wired callbacks
Both paths work. The kernel buffers and replays data if callbacks are attached late.

Using KernelInterface syscalls

Your driver receives a KernelInterface during init(). Use it for filesystem access, process spawning, and pipes.

Read/write files

async init(ki: KernelInterface) {
  this.kernel = ki;
}

spawn(command, args, ctx) {
  // Read a file from the shared VFS
  const content = await this.kernel.vfs.readFile("/app/config.json");

  // Write a file
  await this.kernel.vfs.writeFile("/tmp/output.txt", "result");
}

Spawn child processes (cross-runtime)

This is the critical integration point. When your runtime needs to run a command handled by another driver, call kernel.spawn():
spawn(command, args, ctx) {
  // Your runtime wants to run "grep" — routes to WasmVM driver
  const child = this.kernel.spawn("grep", ["-r", "TODO", "/app"], {
    ppid: ctx.pid,
    env: ctx.env,
    cwd: ctx.cwd,
    onStdout: (data) => { /* forward to parent */ },
  });

  const exitCode = await child.wait();
}

Create pipes

spawn(command, args, ctx) {
  const { readFd, writeFd } = this.kernel.pipe(ctx.pid);

  // Spawn writer with stdout → pipe
  const writer = this.kernel.spawn("echo", ["hello"], {
    ppid: ctx.pid,
    stdoutFd: writeFd,
  });

  // Read from pipe
  const data = await this.kernel.fdRead(ctx.pid, readFd, 4096);

  // Clean up parent's copies
  this.kernel.fdClose(ctx.pid, readFd);
  this.kernel.fdClose(ctx.pid, writeFd);
}

FD operations

// Open file → FD
const fd = this.kernel.fdOpen(ctx.pid, "/tmp/file.txt", O_RDONLY, 0);

// Read
const bytes = await this.kernel.fdRead(ctx.pid, fd, 1024);

// Write
const written = this.kernel.fdWrite(ctx.pid, fd, encoded);

// Seek
const newPos = this.kernel.fdSeek(ctx.pid, fd, 0n, SEEK_SET);

// Close
this.kernel.fdClose(ctx.pid, fd);

Testing your driver

Use createKernel directly in tests with your driver:
import { describe, it, expect, afterEach } from "vitest";
import { createKernel } from "@secure-exec/kernel";
import { createInMemoryFileSystem } from "@secure-exec/os-browser";

describe("MyDriver", () => {
  let kernel;

  afterEach(async () => {
    await kernel?.dispose();
  });

  it("handles myecho command", async () => {
    const vfs = createInMemoryFileSystem();
    kernel = createKernel({ filesystem: vfs });
    await kernel.mount(createEchoDriver());

    const result = await kernel.exec("myecho hello");
    expect(result.exitCode).toBe(0);
    expect(result.stdout).toBe("hello\n");
  });

  it("coexists with WasmVM", async () => {
    const vfs = createInMemoryFileSystem();
    kernel = createKernel({ filesystem: vfs });
    await kernel.mount(createWasmVmRuntime({ wasmBinaryPath: "..." }));
    await kernel.mount(createEchoDriver());

    // myecho routes to your driver
    const r1 = await kernel.exec("myecho test");
    expect(r1.stdout).toBe("test\n");

    // cat still routes to WasmVM
    await kernel.writeFile("/tmp/f.txt", "data");
    const r2 = await kernel.exec("cat /tmp/f.txt");
    expect(r2.stdout).toBe("data");
  });
});

Checklist

Before shipping your driver:
  • name is unique and descriptive
  • commands lists every command you handle
  • spawn() returns synchronously (async work on microtask/setTimeout)
  • dispose() cleans up all workers/processes/resources
  • init() throws if called twice without dispose
  • spawn() throws if called before init()
  • kill(signal) resolves the exit promise with 128 + signal
  • onExit callback is invoked exactly once
  • Tested with and without other drivers mounted