Skip to content

Runtime Architecture

Overview

The Runtime is the layer between Image and Instance. It owns the boot procedure — assembling the namespace, creating the kernel, and constructing the running instance. The Runtime knows what platform it’s on and provides platform-appropriate capabilities.

UnixBuilder --> UnixImage --> Runtime --> UnixInstance
(build) (data) (boot) (running)

Design Principles

  1. The Runtime is a namespace assembler (Plan 9). Its job is to construct the right mount table for each instance — the right fileservers at the right paths. Everything else (process lifecycle, I/O routing, file access) is handled by the kernel and existing fileservers.

  2. PlatformCapabilities are internal. Processes never see platform capabilities. They use the namespace. If a process needs to load bytes, it reads a file. If it needs to fetch a URL, it uses httpFS. The Runtime wires platform-appropriate implementations behind the fileservers.

  3. Factory injection. The Runtime constructs bin factories (createWasiBin, createWasmBin) with platform capabilities closed over. The resulting BinFunction is opaque — the process calls it without knowing what platform it’s on.

  4. RuntimeConfig vs BootOpts. Platform-level config (capabilities, adapter options) is set once at Runtime construction. Per-instance config (tty, cwd, volumes) is provided at boot time.

  5. The kernel routes bytes. Nothing more. The Runtime is a userspace component — it does not extend the Kernel interface or ProcContext. It consumes the kernel, not modifies it.

Architecture

Type Hierarchy

PlatformCapabilities — what the host provides (loadAsset, importEsm, fetch, compileWasm)
|
RuntimeConfig — platform-level config (capability overrides)
|
Runtime — boot(image, opts) -> UnixInstance
|
BootContext — data contract: Image exposes frozen config to Runtime

Boot Sequence

The Runtime’s boot(image, opts) method executes these steps (extracted from the former UnixImageImpl.boot()):

1. Read BootContext from image (rootFs, mounts, bins, env, services, catalogEntries)
2. Create writable upper layer (memoryFS)
3. Create overlay root: overlayFS(upper, rootFs)
4. Mount devFS at /dev
5. Create processTable + KernelLog
6. Mount procFS at /proc (with signal + getNs callbacks — signal callback captures `kernel` via late-binding closure; kernel is created in step 14)
7. Mount consFS at /dev/cons (if tty provided) via mountConsTerminal helper
8. Mount userFS at /dev/user
9. Mount volumes from BootOpts
10. Mount srvFS at /srv
11. Write catalog metadata to /pkg/.catalog/
12. Build environment (DEFAULT_ENV + image env + runtime env)
13. Build catalogInstall callback (closes over upper layer + capabilities)
14. Create kernel with mount table + callbacks
15. Determine PID 1 (init with services, or shell)
16. Construct UnixInstance (boot, spawn, wait, shutdown)

Steps 1-16 are in bootFromImage() in src/runtime/platform/boot.ts.

Platform Capabilities

Each platform provides four capabilities:

CapabilityNode.jsBrowserTest
loadAsset(url, relativeTo?)node:fs/promises readFilefetch() -> arrayBufferthrows
importEsm(source)Buffer -> base64 data URI -> import()Blob -> createObjectURL -> import()throws
fetch(url, init?)globalThis.fetchglobalThis.fetchthrows
compileWasm(binary)WebAssembly.compileWebAssembly.compileWebAssembly.compile

Capabilities flow into the boot sequence via closure injection:

  • WASM bin factories receive caps.loadAsset and caps.compileWasm at construction time
  • The package evaluator receives caps.importEsm via the fsResolver parameter
  • The package registry fetcher uses caps.fetch

Module Structure

src/runtime/platform/
types.ts — Runtime, PlatformCapabilities, RuntimeConfig, BootContext interfaces
boot.ts — bootFromImage(), createRuntime() — the core boot procedure
node.ts — nodeRuntime() factory + Node capabilities + nodeStdio adapter
browser.ts — browserRuntime() factory + Browser capabilities + xtermStdio adapter
test.ts — testRuntime() factory + Test capabilities (throws for most)

Entry Points (package.json exports)

@fishnet/core — core: Unix(), stdSystem(), types
@fishnet/core/node — nodeRuntime(), nodeStdio
@fishnet/core/browser — browserRuntime(), xtermStdio
@fishnet/core/test — testRuntime()

The wildcard subpath export "./*" also supports deep imports like @fishnet/core/kernel/types. All imports use the @fishnet/core package specifier with no .js extensions (moduleResolution: “bundler”).

Node.js code (node:fs, node:path, node:url) is isolated to @fishnet/core/node. Browser bundles never see it.

Image / Runtime Boundary

Image (pure data)

interface UnixImage {
createBootContext(): BootContext // read-only view of image contents
extend(): UnixBuilder // create a new builder with this image as base
}

The Image holds: frozen rootFS (stacked overlay layers from .build() and .run() steps — each .run() step produces a frozen upper layer overlaid on the previous), merged config (bins, env, mounts, services, catalogs). The rootFs in BootContext is this frozen stack. bootFromImage creates another overlay on top — the writable upper layer for runtime mutations.

Runtime (boot procedure)

interface Runtime {
boot(image: UnixImage, opts?: BootOpts): Promise<UnixInstance>
readonly capabilities: Readonly<PlatformCapabilities>
}

The Runtime holds: platform capabilities (fixed at construction). Each boot() call creates a fresh instance with its own overlay, kernel, process table, and namespace.

BootContext (the handoff)

interface BootContext {
readonly rootFs: Fileserver & ExecCapable
readonly mounts: Readonly<Record<string, Fileserver>>
readonly bins: Readonly<Record<string, BinFunction>>
readonly env: Readonly<Record<string, string>>
readonly services: readonly ServiceDef[]
readonly catalogEntries: Readonly<Record<string, () => Extension>>
}

All fields are Readonly — the Image is frozen, the Runtime must not mutate it. Multiple boots from the same image create independent instances.

Containerd Mapping

For readers familiar with container runtimes:

containerd/runcfishbowl
OCI ImageUnixImage (frozen rootFS + config)
OCI Runtime SpecBootOpts (mounts, env, cwd)
containerd runtime shimRuntime (platform capabilities)
runc create + runc startruntime.boot(image, opts)
rootfs overlayoverlayFS(upperFs, image.rootFs)
/proc mountprocFS(processTable, log, signal, getNs)
/dev mountdevFS()
PID 1 / initmakeInit(services) or shell
SIGTERM -> grace -> SIGKILLinstance.shutdown()

Lifecycle

Construction: nodeRuntime({ prompt: '$ ' })
|
v
Boot: runtime.boot(image, { tty, cwd })
|
v
Running: instance.boot() → shell/init as PID 1
instance.spawn() → additional processes
instance.wait() → wait for PID 1 exit
|
v
Shutdown: instance.shutdown()
SIGTERM all → 5s grace → SIGKILL survivors
→ allSettled → kernelLog.dispose()

What the Runtime Does NOT Do

  • Extend the kernel. The kernel interface is unchanged. The Runtime consumes createKernel().
  • Modify ProcContext. No new fields on ProcContext. Capabilities flow through closures, not process context.
  • Own the build path. UnixBuilderImpl.build() and .run() stay in builder.ts. Build-time kernels are separate from boot-time.
  • Provide security boundaries. Namespace isolation is a convention. Real sandboxing is a host-level concern (Phase C).
  • Auto-detect platform. Consumers explicitly choose nodeRuntime(), browserRuntime(), or testRuntime().