Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Proposal: WASI Host Adapter

How capOS should host WebAssembly modules through the WebAssembly System Interface, without recreating ambient authority and without committing to a runtime that the userspace baseline cannot support today.

Problem

WASI is the natural sandboxed-execution path for capOS:

  • It is already designed to remove ambient authority. Preview 1 requires preopens — every file descriptor a module sees was granted by the host at startup. Preview 2 makes typed handles first-class through the Component Model.
  • A single host adapter unlocks every language with a useful WASI target: Rust, C/C++, Go (GOOS=wasip1), TinyGo, Python, Zig, AssemblyScript, any interpreter compiled to wasm.
  • Wasm linear-memory bounds checks plus capability scoping give defence in depth for untrusted plugins and third-party code without weakening the capOS isolation model.

The risk pattern is the same as POSIX: a host adapter that grants ambient authority would erase the property that makes WASI worth doing. Every WASI import must be backed by a typed capability the host process already holds. If the host does not hold the cap, the module cannot reach it.

WASI is not a substitute for native ports of languages that need real OS threads, full asynchronous I/O, signals, or large POSIX surfaces. Those remain the native runtime tracks. WASI is the right tool for sandboxing untrusted plugins, third-party scripts, isolated workloads, CPU-bound portable tools, and language ecosystems whose native capOS port has not yet been built.

Scope

In scope:

  • A capos-wasm userspace host adapter built on capos-rt.
  • A WASI Preview 1 surface whose imports map 1:1 to typed capOS capabilities.
  • Per-instance CapSet projection: each module sees only the caps the host grants for that instance.
  • Phase decomposition that picks one runtime for v0, lets later phases migrate to the Component Model and richer runtimes, and stays explicitly outside ambient authority.
  • Validation through QEMU smokes that prove granted and ungranted paths.

Out of scope for the first implementation:

  • wasi-threads (requires shared-memory + atomics + bulk-memory).
  • fork()-shaped semantics. Cannot clone wasm linear memory; same constraint as the browser-wasm proposal.
  • Synchronous signal delivery inside a wasm module. Fuel exhaustion plus host-driven termination are the only deterministic interruptions.
  • File-backed MAP_SHARED mmap.
  • Treating the wasm sandbox as the only isolation boundary for hostile modules — the capOS process boundary remains the primary boundary.
  • A custom non-portable WIT dialect with externref-typed cap handles. This proposal explicitly defers richer cap handles to Component Model resources (Phase W.7).

Current Manual Pages

  • Programming Languages summarizes WASI’s current status relative to Rust, Python, Go, C/C++, Lua, and POSIX adapter tracks.
  • Userspace Binaries Part 5 sketches the WASI host adapter at a higher level. This proposal supersedes that sketch with a full design surface; the userspace-binaries proposal continues to own the broader native-binary, language, and POSIX-adapter roadmap.
  • Userspace Runtime documents the implemented capos-rt surface that the host adapter consumes.
  • Browser/WASM covers the separate browser-hosted wasm experiment. The two proposals share wasm-runtime insight but target different substrates: WASI host adapter runs on capOS hardware; the browser proposal runs capOS concepts in a browser tab.
  • Lua Scripting covers a similar capability-scoped script runner shape; the WASI track is the untrusted / portable counterpart to that proposal’s trusted native runner.
  • Go Runtime covers the native GOOS=capos alternative to Go-on-WASI.

Research Grounding

Relevant research and external references:

In-tree references: this proposal lifts the capability-mapping table from docs/proposals/userspace-binaries-proposal.md Part 5 and the runtime survey/phase decomposition shape from comparable language-runtime planning work; concrete repo evidence appears inline below.

Design Principles

  1. WASI is not a kernel feature. The kernel sees a normal userspace process with a CapSet and a capability ring. The host adapter is one of many capos-rt-based binaries.
  2. The host adapter’s CapSet is the authority. WASI module bytes are data. They cannot create authority. Every import is satisfied by a cap the host already holds; absent caps are refused, not synthesised.
  3. Per-instance CapSets are subsets, not supersets. Each loaded module gets only the caps the manifest grants for that instance. The host’s own CapSet may be larger; the module never sees the parent.
  4. The wasm sandbox is defence in depth, not the isolation boundary. The capOS process boundary remains primary. Wasm bounds checking and immutable Module validation add a second software-enforced boundary inside the host process so an entire untrusted module image can be confined.
  5. Schema-first capability mapping. Each WASI function is backed by a typed capability, not by emulated POSIX semantics. POSIX-shaped integer fds in Preview 1 are a Preview 1 ABI requirement, not a capability model concession.
  6. Pick portable WASI, skip non-portable extensions. Custom imports with externref-typed cap handles would lock capOS into a non-portable WIT dialect that no other host implements. The Component Model’s typed resources are the right answer for first-class typed cap handles in wasm; defer to that path rather than inventing a one-vendor dialect.
  7. Fail closed. Any unimplemented WASI call returns ERRNO_NOSYS. Any cap lookup that fails returns the appropriate Preview 1 errno (ERRNO_BADF, ERRNO_ACCES, ERRNO_NOSYS). Modules cannot probe absent caps for ambient behavior.

Architecture

flowchart TD
    Manifest[boot manifest:<br/>system-wasm-host.cue] --> Host[capos-wasm process]
    Host --> Runtime[wasm runtime<br/>wasmi v0]
    Host --> Rt[capos-rt typed clients]
    Rt --> Ring[capability ring]
    Ring --> Kernel[kernel CapObject dispatch]
    Ring --> Services[userspace services]

    Runtime --> Module[wasm module instance]
    Module --> Imports{WASI imports}
    Imports --> FdTable[per-instance fd table /<br/>Preview 2 resource handles]
    FdTable --> Caps[granted typed caps]
    Caps --> Rt

capos-wasm is one userspace process. It hosts one or more wasm module instances. The runtime engine (wasmi for v0; see Runtime Selection below) is linked into that process. WASI imports are resolved by the host adapter’s import-resolver module against typed capOS clients. Each instance has its own per-instance fd table (Preview 1) or resource bundle (Preview 2) populated from the manifest grants for that instance.

The runtime exposes only what the host process can fulfil. If the host does not hold an EntropySource cap, random_get returns ERRNO_NOSYS. If the manifest did not grant a home namespace, the module’s preopen table does not contain it and path_open("/home/...") resolves to nothing.

Runtime Selection

For v0 (Phases W.1 through W.6), use wasmi. For W.7+, evaluate migration to wasmtime when capOS userspace gains std support and a futures executor, or to WAMR if minimal footprint becomes the dominant constraint and the C build path lands.

ConstraintwasmiWAMRwasm3wasmtime
Pure Rust, drops into capOS workspaceyesC (needs cc/build glue, no libcapos yet)C (same problem)yes
no_std + allocyes, advertised explicitlypartial (embedded, libc-shaped)yes (bare metal)no (needs std and a futures executor)
LicenseApache-2.0 / MITApache-2.0 with LLVM exceptionMITApache-2.0
Footprintsmall register-based bytecode (v0.32 5x speedup)~29 KB AOT, ~58 KB interpreter~64 KB code, ~10 KB RAMlarge (Cranelift JIT)
Sandboxingwasm spec + execution-engine isolationwasm spec + AOT validationwasm specwasm spec + Cranelift verifier
Fuel/gas meteringyes, built-innot advertisedyesyes
Capability transferexternref since 0.24; component model on roadmapreference types yes; component model partialpartial reference typesfull component model (best-in-class)
WASI versionspreview1 stable; preview2 on roadmappreview1 stable; preview2 partialpreview1 partialpreview1 + preview2 + components
Host function interfacemirrors wasmtime APIC API; Rust through wamr-rust-sdkC APIRust + C
Maintenancewasmi-labs, two security audits (2023, 2024)Bytecode Alliance, TSC-governedmaintainer in minimal-maintenance phaseBytecode Alliance flagship
Threadingnot in current scopeyes (wasi-threads)noyes

Why wasmi for v0:

  • Pure Rust drops directly into the capOS workspace. No C build chain required — the same chain libcapos does not yet provide.
  • Genuine no_std + alloc support means no host-side OS abstraction is required for the runtime itself; it sits cleanly on capos-rt.
  • Built-in fuel metering matches capOS’s preference for explicit resource accounting.
  • externref support is sufficient for any future v1 capability-handle experiment that does not block on the Component Model.
  • Mirroring the wasmtime API means that migrating to wasmtime in W.7 is rewiring imports, not rewriting host calls.

Not chosen for v0:

  • wasmtime needs std userspace and a futures executor. capOS userspace is no_std + alloc today; this is the same blocker that keeps the Rust capnp-rpc crate (v0.25) off capos-rt and queues the remote-session-client capnp-rpc rewrite behind an async runtime decision.
  • wasm3 is in maintainer-declared minimal-maintenance phase; not a good fit for a long-horizon capOS substrate.
  • wasmer has similar weight to wasmtime and does not align as cleanly with the Bytecode Alliance Preview 2 trajectory.
  • WAMR is a strong candidate when a C toolchain and libcapos exist and minimal footprint is the goal. It is the migration target for high-density wasm hosting later, but it is not the v0 baseline because the C substrate is not in tree.

WASI Version Stance

  • Preview 1 for v0 (Phases W.1 through W.6). POSIX-shaped, file-descriptor-based, C-friendly. Tier 2 in upstream Rust since 1.78 (May 2024); supported by Go 1.21+ (GOOS=wasip1 GOARCH=wasm), TinyGo, Clang --target=wasm32-wasi, Zig. This is the immediate unlock.
  • Preview 2 / Component Model for W.7+. Resources are first-class typed handles. They are the natural mapping for capOS capabilities — closer in shape to OwnedCapability<T> than to integer fds. WIT interfaces let cap-aware Rust crates export typed APIs that a wasm component on capOS or a native capOS service can consume the same way it consumes a capnp interface.

Skipping Preview 1 entirely and starting at Preview 2 is possible with wasmtime today, but harder with wasmi; doing so would push the entire v0 unlock behind the std-userspace decision. The Preview 1 first / Preview 2 later sequencing is the smaller-step path to running C, Rust, Go, Python, TinyGo on capOS.

Capability Mapping Surface

Preview 1: per-import mapping

Each Preview 1 import is backed by a typed capOS capability the host adapter already holds. POSIX inherits ambient authority through global path namespaces, integer fds, and a process credential table; WASI removes that by requiring preopens, and capOS pushes it further by requiring an explicit per-import cap mapping in the host adapter.

WASI preview1 importcapOS host-adapter implementation
args_get / args_sizes_getRead from a future capOS LaunchParameters cap or per-instance arena. Empty by default until that surface lands.
environ_get / environ_sizes_getRead from a KeyValueScope / ConfigOverlay cap when one exists; empty by default. Open question §6.
clock_time_get(MONOTONIC)Timer.now() over the host’s TimerClient.
clock_time_get(REALTIME)Future wall-clock cap; until then return ERRNO_NOSYS or ERRNO_INVAL.
proc_exit(code)Map to a host-internal “instance exited with code” status. The host process does not exit; the wasm instance does.
random_getThe kernel EntropySource cap (the in-tree CSPRNG capability; see schema/capos.capnp interface EntropySource and KernelCapSource::EntropySource). Refuse with ERRNO_NOSYS when the host adapter was not granted entropy authority.
fd_write(1, ...) / fd_write(2, ...)Pre-opened fd 1 to host’s Console / TerminalSession write path; fd 2 to same or a separate log cap if granted.
fd_read(0, ...)Pre-opened fd 0 to a granted TerminalSession or future StdIO input cap if available; else ERRNO_BADF. No bare in-tree StdinReader cap exists today; non-terminal stdin requires a future input cap.
path_open(preopened_dir_fd, path, ...)Resolve path inside the Namespace cap mounted as that preopen, then open through the namespace’s Store / File capability.
fd_read / fd_write on opened filesTranslate to the typed File capability behind the host-side fd table entry.
fd_closeDrop the typed cap handle (release-on-drop in capos-rt).
fd_seek / fd_tell / fd_filestat_getMethods on the File cap.
fd_prestat_get / fd_prestat_dir_nameEnumerate the host adapter’s preopened-directory table built from manifest grants.
sock_send / sock_recv / sock_shutdownTranslate to typed TcpSocket / UdpSocket cap calls.
poll_oneoffMultiplex over the host’s capability ring; CQEs are the event source. Open question §3.
fd_advise / fd_allocate / fd_renumberStub or ERRNO_NOSYS until needed.
sched_yieldNo-op or single-tick yield through the runtime’s scheduler.

Preview 2: WIT-resource mapping

When the host adapter migrates to Preview 2 (Phase W.7+), the imports become typed capOS capabilities directly through WIT resources:

WIT package / interfacecapOS host-side cap
wasi:io/streams (input-stream, output-stream resources)Wrap one capOS cap per stream (Console / TerminalSession / File / TcpSocket). The resource handle in wasm corresponds 1:1 to a host-side OwnedCapability<T>.
wasi:filesystem/types (descriptor resource)One OwnedCapability<File> or OwnedCapability<Directory> per descriptor. Preopened dirs become resource handles passed at instantiation.
wasi:clocks/{monotonic-clock,wall-clock}Timer / future wall-clock cap.
wasi:random/{random,insecure}EntropySource cap.
wasi:sockets/tcp (tcp-socket resource)TcpSocket cap.
wasi:cli/{stdin,stdout,stderr,environment,exit}Per-instance CapSet projection.
wasi:http/incoming-handler / outgoing-handlerMatch capOS HttpEndpoint / Fetch (drafted in service-architecture-proposal.md).

Components in the same store can pass resources to other components; the host mediates the move. This maps directly to capOS capability transfer semantics — the same shape as the kernel’s result-cap insertion for typed cap returns from a CALL.

Capability Handle Path in the Module

How a wasm module receives and refers to a capOS capability is one of the load-bearing design questions. Three options:

  1. Preview 1 + integer fds, host-side fd table only (recommended for v0). All caps live in the host process. The module sees integer fds. The host adapter maps fds to OwnedCapability<T> slots in its own per- instance table. Works with every existing wasip1 binary unchanged. A wasm module cannot pass a typed cap to another wasm module without going through the host.
  2. Custom externref import (alternative; not recommended). Requires the reference-types proposal (supported by wasmi >=0.24, wasmtime, wasmer; partial in wasm3). The host adapter exports custom imports like cap_call_ref that take an externref typed handle. This is non-standard and locks capOS into a one-vendor WIT dialect that no other host implements; it would also delay Preview 2 adoption because the dialect would need its own mapping policy.
  3. Preview 2 / Component Model resources (target for W.7+). Resources in the Component Model are unforgeable typed handles. Components that import wasi:filesystem/types.descriptor receive a handle that is the host-side OwnedCapability<File>. Components can pass resources to other components in the same store; the host mediates. Direct match to capOS capability transfer semantics.

Recommendation: ship Preview 1 + integer fds for v0; defer rich typed-cap-in-module support to Preview 2 in W.7. Skip the externref custom-import path entirely.

Per-Instance vs Per-Process Model

Two reasonable shapes:

  1. One wasm instance per capos-wasm process (recommended for v0). Faults are isolated at the capOS process boundary. Fuel and budget enforcement are per-process and use the existing capOS resource accounting. Manifest-grant shape stays simple: each manifest entry names one binary and one cap bundle.
  2. Many instances per capos-wasm process (alternative). Better density. Suits hosting many small modules (plugin systems, embedded scripts). Adds host-side scheduling concerns: a runaway instance can starve siblings; fuel/budget enforcement now has to demultiplex; the poll_oneoff reactor question becomes load-bearing.

Recommendation: one instance per process for v0. Revisit when instance count actually matters. The capOS process boundary is already a strong isolation primitive; trading it away for density before density is needed adds complexity for no v0 unlock.

Per-Instance CapSet Plumbing

Each loaded module gets a per-instance capability bundle. The host adapter receives manifest grants and projects them onto WASI imports.

The shape needs to land alongside argv/env passing — argv for wasm modules has the same lifecycle question as argv for native processes. When a future capOS LaunchParameters surface lands it becomes the canonical source for both argv and env. Until then, a small bounded text grant in the host adapter manifest is acceptable for v0 (Open Question §6 / §7).

Sketch of the manifest shape (pre-LaunchParameters):

wasm_host: {
    binary: "thing.wasm"
    args: ["--input", "data"]
    caps: {
        console:   @console
        timer:     @timer
        random:    @random
        // preopen 3 → home namespace; preopen 4 → tmp namespace, etc.
        preopens: [
            { fd: 3, namespace: @home_namespace, name: "/home" }
            { fd: 4, namespace: @tmp_namespace,  name: "/tmp" }
        ]
    }
}

Same authority model the rest of capOS uses: every cap the module sees is named in the manifest and granted by the parent. The wasm sandbox is defence in depth on top of capability scoping, not a replacement.

Trust Boundaries

BoundaryNative capOS serviceWASI host adapter + module
Authority sourceProcess CapSetHost CapSet then per-instance subset
Memory isolationPage tablesWasm linear-memory bounds-check plus page tables (host process)
Code integrityW^X + NXWasm module validation plus immutable WebAssembly.Module
Cap forgeryKernel-owned CapTableHost-owned per-instance fd table or resource-handle table; module sees opaque ints/handles only
Resource limitsKernel quotasWasm fuel + memory cap + host-side per-instance time/byte budgets
Side channelsHardware-level (Spectre etc.)Same hardware level, plus wasm-specific (e.g. timer resolution)

Wasm does not weaken capOS isolation; it adds a second software-enforced boundary that contains an entire untrusted module image. This is exactly the property that makes WASI a good fit for plugin and script loading.

What WASI Does Not Solve

  • fork(): cannot clone wasm linear memory mid-execution. Same reason the browser-wasm proposal documents. POSIX programs that fork-then-exec must use posix_spawn-shaped equivalents, or the host adapter must spawn a new wasm instance.
  • Synchronous signals: no preemption inside a wasm module without cooperative yield points or interrupted execution. Fuel exhaustion is the only deterministic interruption; gross preemption is “host kills the instance”. Acceptable for plugins.
  • Threads without wasm-threads: requires shared-memory + atomics + bulk-memory features and a runtime that supports them. Out of scope for v0.
  • Live mmap of files: wasm linear memory is not file-backed. Workable only for small read-or-write cycles.

Phase Decomposition

Smallest reviewable slices ordered by dependency. Each phase is independently demoable and gates the next.

Phase W.0 — Decision and host runtime selection (planning)

  • Decide runtime: wasmi vs WAMR (recommendation above).
  • Land this proposal and the matching docs/plans/wasi-host-adapter.md ralphex plan.
  • Resolve cross-cutting open questions §1, §3, §6, §7, and §8 below (the §8 vendoring posture decision gates the W.1 scaffold layout).

Deliverable: agreed proposal + plan file. No code.

Phase W.1 — capos-wasm host process scaffold (no WASI yet)

  • New crate capos-wasm/ — userspace process built on capos-rt.
  • Vendor the chosen runtime (wasmi recommended; one local cargo dep patched for no_std + alloc if needed).
  • Host process can WebAssembly.compile(bytes) then instantiate(no imports) then run an empty _start. No imports resolved yet.
  • Manifest: new system-wasm-host.cue boots one host process with one embedded .wasm blob (the smoke binary).
  • Smoke: make run-wasm-host boots, host loads the empty blob, prints [wasm-host] empty module instantiated and exited, host exits cleanly.

Deliverable: a wasm runtime runs as a capOS process on top of capos-rt. No imports, no host functions, no WASI. Validates the runtime crate works in no_std + alloc userspace.

Phase W.2 — WASI Preview 1 stdout-only

  • Implement args_get, args_sizes_get, environ_get, environ_sizes_get (all return empty), clock_time_get(MONOTONIC)Timer.now(), proc_exit, random_get (gated on a granted EntropySource cap; refuse with ERRNO_NOSYS if absent), fd_write(1, …) → host’s Console.write_line, fd_write(2, …) → same.
  • Stub everything else as ERRNO_NOSYS.
  • Build a “hello, wasi” smoke binary in two languages: Rust cargo build --target wasm32-wasip1 and C clang --target=wasm32-wasi. Both print Hello from WASI on capOS, exit cleanly.
  • Smokes: make run-wasi-hello-rust, make run-wasi-hello-c.

Deliverable: the first non-Rust-native userspace code paths land on capOS. C runs on capOS without any libcapos/POSIX work in tree.

Phase W.3 — Per-instance CapSet plumbing + LaunchParameters

  • Define how the host adapter receives per-instance grants from the manifest. Likely shape: extend system.cue so a wasm host entry lists { binary = "thing.wasm", caps = [...] }. Each cap grant becomes an entry in the per-instance preopen / argv table.
  • Cross-cutting dependency on a future capOS LaunchParameters surface — argv passing for wasm needs the same plumbing as native argv. Until that surface exists, ship a smaller workaround: wasm modules read argv from a bounded text grant in the host adapter manifest.
  • Smoke: a wasm module reads argv[1] and prints it back.

Deliverable: per-instance CapSet selection works.

Phase W.4 — WASI Preview 1 random + clocks production-ready

  • Wire the kernel EntropySource cap (the in-tree CSPRNG capability; see EntropySourceClient and KernelCapSource::EntropySource) through the host adapter as the backing for random_get. The same cap is the natural future analogue of the browser’s crypto.getRandomValues surface.
  • Wall-clock support deferred until capOS has a wall-clock cap.

Deliverable: every Preview 1 call that does not need filesystem or sockets is implemented honestly.

Phase W.5 — WASI Preview 1 filesystem (gated on Namespace/File caps)

  • Map preopened-dir fds to Namespace caps from the manifest.
  • Implement path_open, fd_read, fd_write, fd_seek, fd_close, fd_filestat_get, fd_prestat_get, fd_prestat_dir_name against whatever Namespace / File / Store cap surface exists at the time.
  • Blocked on the missing storage caps: Namespace, File, Store, and Directory do not yet exist in the in-tree capability set, so there is no host-side authority to translate a preopen fd to. Capabilities the host adapter can already use for stdio, time, entropy, terminal, and memory (Console, TerminalSession, Timer, EntropySource, VirtualMemory) are sufficient for Phases W.2–W.4 but do not cover filesystem semantics. This phase waits on storage proposals (docs/proposals/storage-and-naming-proposal.md).

Deliverable: a wasm module can read and write files inside a preopened capOS namespace.

Phase W.6 — WASI Preview 1 sockets (gated on userspace network stack)

  • sock_send, sock_recv, etc. against TcpSocket / UdpSocket caps when the userspace network stack lands.
  • Until then, an HTTP client over Fetch / HttpEndpoint is a reasonable shim for HTTP-only use.

Deliverable: a wasm module can serve HTTP requests inside a capOS process.

Phase W.7 — Move to wasmtime or migrate to WASI Preview 2 / Component Model

  • If the runtime selected in W.0 was wasmi, decide whether to swap to wasmtime once std/futures runtime is available in capOS userspace.
  • Or instead promote wasmi to wasip2 / Component Model support (wasmi roadmap covers components, but maturity is behind wasmtime).
  • Map WIT resources to typed OwnedCapability<T> slots. This is the natural place to bridge capOS capabilities into wasm as first-class typed handles. Capability transfer between wasm components becomes a host-mediated resource handoff.
  • Component-Model support enables cap-aware Rust crates to export their typed interfaces as WIT, which a Rust capOS service can consume the same way it consumes a capnp interface.
  • Schema serial-surface coordination: this phase will likely add new variants under schema/capos.capnp for component-model resource bridging. Serialise with other schema-touching plans (docs/plans/README.md Concurrency Notes).

Deliverable: a wasm component on capOS exports a typed interface that a native capOS process can call.

Phase W.8 — TinyGo / Go-on-WASI integration for CUE

  • Build a CUE evaluator binary against TinyGo or upstream Go’s GOOS=wasip1. Run it in the host adapter against a CUE source blob granted as a ScriptPackage (future package-cap surface, same shape as the planned LaunchParameters work).
  • Reuses existing CUE workflows; capOS just hosts the evaluator.

Deliverable: capOS can evaluate CUE manifests at runtime without the host toolchain. Bridges to the eventual native Go track (go-runtime-proposal.md).

Languages Targeting WASI

What capOS gets “for free” once the host adapter exists, ranked by how mature each language’s WASI target is. This is the leverage argument: one host adapter unlocks every row at once.

LanguageWASI statusToolchainNative capOS alternativeWhen WASI wins
Rustwasm32-wasip1 Tier 2 since 1.78; wasm32-wasip2 Tier 2 since 1.82cargo build --target wasm32-wasip2targets/x86_64-unknown-capos.json (implemented)Untrusted Rust plugins. Cross-compiled tools.
C / C++wasi-libc + Clang --target=wasm32-wasi; wasi-sdk packagedclang --target=wasm32-wasifuture libcaposAny C/C++ tool needing portability before libcapos lands. CPython-on-WASI today is the canonical example.
Go (upstream)GOOS=wasip1 since Go 1.21 (Aug 2023). Single-thread, blocking I/O, no goroutine parallelism.GOOS=wasip1 GOARCH=wasm go buildfuture GOOS=capos (go-runtime-proposal.md)CUE evaluation, go run style tools, single-goroutine compute.
TinyGowasip1 supported; wasip2 supported in dev branchtinygo build -target=wasip2n/aSmaller Go binaries; Component Model export of typed interfaces.
Python (CPython)wasm32-unknown-wasip1 Tier 2 (PEP 11)Upstream CPython buildfuture native CPython through POSIX adapterSandboxed Python plugins, configuration scripts.
AssemblyScriptDesigned for wasm; WASI host integration via runtimeascn/aLightweight typed scripting. Less interesting on capOS than Lua.
ZigNative wasm32-wasi target; no runtime overheadzig build-exe -target wasm32-wasin/aZig systems code in a sandbox.
Lua / interpreters in generalA Lua interpreter compiled to wasi runs Lua scripts in a wasm sandboxCompile any C interpreter to wasm32-wasiLua piccolo runner (lua-scripting-proposal.md)When Lua scripts are untrusted. The piccolo native-Rust runner remains the right answer for trusted capOS scripting.
JavaScriptQuickJS-on-wasi works todayCompile QuickJS to wasm32-wasiQuickJS native runner (future)Untrusted JS plugins; portable JS without writing a native QuickJS runtime.
.NET (mono-wasi)Experimentaldotnet wasi-experimentaln/aIf a port of a .NET tool is required. Low priority.

When WASI vs Native

These are complementary tracks, not competitors.

  • Native wins for foundational services, performance-critical code, anything calling typed capOS caps directly, anything needing real threads, full async I/O, or first-class participation in the cap graph.
  • WASI wins for portability or untrusted code execution, for any existing C/C++ program with wasi-libc support that cannot wait for libcapos, for CPU-bound CUE evaluation before native Go lands, and for sandboxed user-submitted scripts.

The browser-wasm proposal captures the same intuition: the cap-ring layer is the only stable interface that survives substrate swaps. The WASI host adapter is another substrate swap, this time at the language level instead of the hardware level.

Validation

The first implementation is not complete until it has QEMU evidence:

  • A wasm module prints through a granted Console / TerminalSession.
  • The same module cannot use fd_write to a fd it was not granted, cannot open a path outside its preopened namespaces, and cannot call an unimplemented WASI function without receiving ERRNO_NOSYS.
  • A missing or wrong-interface cap lookup returns the appropriate WASI errno (not a host-side panic, not silent success).
  • An owned result cap is released deterministically when the instance exits.
  • The host adapter exits cleanly and does not wedge the kernel.

Host tests should cover WASI value conversion and import-resolver generation once those pieces are pure enough to test outside QEMU. Do not claim “WASI works” from host tests alone; the useful behavior is authority-shaped wasm execution in capOS.

Open Questions

  1. Per-instance vs per-process. One wasm instance per capos-wasm process (recommended) or many? Affects fuel/budget enforcement and the manifest shape. Working answer: one per process for v0; revisit when instance count matters.

  2. Capability handle path: extension import or pure WASI-only? Custom externref imports lock capOS into a non-portable WIT dialect. Working answer: skip the custom-import path entirely; jump straight to Preview 2 / Component Model in Phase W.7.

  3. poll_oneoff semantics over the capOS ring. Block the host process’s cap_enter (simple, scales to one instance per process), or run a single-thread reactor that drives multiple instances in round-robin (scales to many instances per process)? Coupled to Q1. Working answer: block synchronously in v0. Revisit reactors when multi-instance hosting becomes a goal.

  4. Fuel budget defaults and exhaustion semantics. wasmi exposes fuel; what is the default budget per instance, and what is the exhaustion behaviour (instance traps and exits, or instance pauses pending refill from a FuelGrant cap)? Affects the cap surface. Working answer: trap-and-exit default; defer the FuelGrant cap until long-running plugins exist.

  5. Typed result-cap from a host call into a wasm module. Preview 1 has no externref. How does the host hand a typed cap back to the instance after a CALL that returns a transferred result cap? Working answer: v0 reifies result caps as integer fds in the per-instance fd table; the host returns fd numbers from capability-issuing imports. Defer typed caps in wasm imports to Preview 2 / Component Model in Phase W.7, where WIT resources match the shape directly.

  6. environ_get source. Empty-by-default, or backed by a KeyValueScope / ConfigOverlay cap? Working answer: empty for v0; bind to whatever environment cap a future capOS LaunchParameters surface produces (no in-tree plan owns this yet; the shell proposal sketches the broader launch-args/environment discussion).

  7. args_get source. Reuse a future capOS LaunchParameters surface (not yet in tree), or ship a wasm-host-specific text grant in the manifest until that surface lands? Working answer: ship a small bounded text grant for v0; migrate to the future LaunchParameters surface once it exists.

  8. Vendoring posture for wasmi. vendor/wasmi-no_std/ (forked, patched) or a cargo-vendor-style mirror of upstream default-features = false? Same question as the piccolo Lua track. Working answer: match whatever the Lua track picks; the chosen posture must remain correct (no_std + alloc userspace, no host side-effects), and consistency across the two tracks is preferred over a WASI-only bespoke layout.

  9. WASI module distribution and versioning. Shipped inline in a manifest blob (today), or via a future Store/Namespace? Working answer: inline blobs for v0; revisit after the storage proposals land.

  10. Component-Model adoption timeline. Skip Preview 1 entirely and target Preview 2 from day one? Possible with wasmtime, harder with wasmi today. Working answer: ship Preview 1 first because it unlocks Rust, C, Go, Python, TinyGo immediately; layer Preview 2 on once wasmi’s component support hardens or migrate to wasmtime.

  11. Out-of-tree wasm packaging. Will capOS ship pre-built .wasm binaries from the boot manifest only, or will operators bring their own? Same scoping question as the future LaunchParameters / package-cap surfaces. Working answer: in-tree only for v0–v6; out-of-tree once a Store cap can hold blobs.

  12. Audit cap shape for wasm instance lifecycle events. Same open question as Lua scripting Phase 4. Component-Model paths benefit from per-instance audit because resource handoffs are interesting events to record. Working answer: defer until the userspace audit cap surface exists.

Relationship to Other Proposals

  • Userspace Binaries owns the broader native-binary, language, and POSIX-adapter roadmap. This proposal supersedes Part 5 of that proposal with the full WASI host adapter design.
  • Programming Languages is the reader-facing summary of language support; the WASI row points at this proposal.
  • Browser/WASM is the separate browser-hosted wasm experiment. Both proposals share wasm-runtime insight but target different substrates.
  • Lua Scripting is the trusted capability-scoped script runner using a native (likely piccolo) Lua VM. WASI-hosted Lua is the untrusted alternative.
  • Go Runtime is the native GOOS=capos alternative to Go-on-WASI. Go-on-WASI is the v0 path for CUE evaluation; native Go is the path for full Go runtime semantics.
  • Storage and Naming defines the Directory / File / Store / Namespace surfaces that Phase W.5 consumes.
  • Networking defines the TcpSocket / UdpSocket surfaces that Phase W.6 consumes.
  • Service Architecture defines Fetch / HttpEndpoint, useful as the v0 networking shim before the full userspace network stack lands.