# 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](../programming-languages.md) summarizes WASI's
  current status relative to Rust, Python, Go, C/C++, Lua, and POSIX adapter
  tracks.
- [Userspace Binaries](userspace-binaries-proposal.md) 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](../architecture/userspace-runtime.md) documents the
  implemented `capos-rt` surface that the host adapter consumes.
- [Browser/WASM](browser-wasm-proposal.md) 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](lua-scripting-proposal.md) 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](go-runtime-proposal.md) covers the native `GOOS=capos`
  alternative to Go-on-WASI.

## Research Grounding

Relevant research and external references:

- WASI Preview 2 launch — Bytecode Alliance,
  ["WASI 0.2 Launched"](https://bytecodealliance.org/articles/WASI-0.2).
- Component Model status — eunomia,
  ["WASI and the WebAssembly Component Model: Current Status"](https://eunomia.dev/blog/2025/02/16/wasi-and-the-webassembly-component-model-current-status/).
- WIT resources / portable plugins — Medium,
  ["WASI 2.0 Components: Portable, Fast Plugins"](https://medium.com/@hadiyolworld007/wasi-2-0-components-portable-fast-plugins-58c24d891584).
- Externref design — Bytecode Alliance,
  ["WebAssembly Reference Types in Wasmtime"](https://bytecodealliance.org/articles/reference-types-in-wasmtime).
- Rust target stabilization — Rust Blog,
  ["Changes to Rust's WASI targets"](https://blog.rust-lang.org/2024/04/09/updates-to-rusts-wasi-targets/)
  and ["wasm32-wasip2 Tier 2"](https://blog.rust-lang.org/2024/11/26/wasip2-tier-2/).
- TinyGo WASI — [TinyGo WASI guide](https://tinygo.org/docs/guides/webassembly/wasi/),
  wasmCloud,
  ["Compile Go directly to WebAssembly components with TinyGo and WASI P2"](https://wasmcloud.com/blog/compile-go-directly-to-webassembly-components-with-tinygo-and-wasi-p2/).
- Runtime survey —
  [Wasmi v0.32 release notes](https://wasmi-labs.github.io/blog/posts/wasmi-v0.32/),
  arXiv 2404.12621
  ["Research on WebAssembly Runtimes"](https://arxiv.org/html/2404.12621v1),
  Colin Breck,
  ["Choosing a WebAssembly Run-Time"](https://blog.colinbreck.com/choosing-a-webassembly-run-time/).
- Runtime repos — [wasmi](https://github.com/wasmi-labs/wasmi),
  [WAMR](https://github.com/bytecodealliance/wasm-micro-runtime),
  [wasmtime](https://github.com/bytecodealliance/wasmtime),
  [wasm3](https://github.com/wasm3/wasm3),
  [wasmer](https://github.com/wasmerio/wasmer).

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

```mermaid
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.

| Constraint | wasmi | WAMR | wasm3 | wasmtime |
|---|---|---|---|---|
| Pure Rust, drops into capOS workspace | yes | C (needs `cc`/build glue, no `libcapos` yet) | C (same problem) | yes |
| `no_std + alloc` | yes, advertised explicitly | partial (embedded, libc-shaped) | yes (bare metal) | no (needs `std` and a futures executor) |
| License | Apache-2.0 / MIT | Apache-2.0 with LLVM exception | MIT | Apache-2.0 |
| Footprint | small register-based bytecode (v0.32 5x speedup) | ~29 KB AOT, ~58 KB interpreter | ~64 KB code, ~10 KB RAM | large (Cranelift JIT) |
| Sandboxing | wasm spec + execution-engine isolation | wasm spec + AOT validation | wasm spec | wasm spec + Cranelift verifier |
| Fuel/gas metering | yes, built-in | not advertised | yes | yes |
| Capability transfer | externref since 0.24; component model on roadmap | reference types yes; component model partial | partial reference types | full component model (best-in-class) |
| WASI versions | preview1 stable; preview2 on roadmap | preview1 stable; preview2 partial | preview1 partial | preview1 + preview2 + components |
| Host function interface | mirrors wasmtime API | C API; Rust through `wamr-rust-sdk` | C API | Rust + C |
| Maintenance | wasmi-labs, two security audits (2023, 2024) | Bytecode Alliance, TSC-governed | maintainer in minimal-maintenance phase | Bytecode Alliance flagship |
| Threading | not in current scope | yes (wasi-threads) | no | yes |

**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 import | capOS host-adapter implementation |
|---|---|
| `args_get` / `args_sizes_get` | Read from a future capOS `LaunchParameters` cap or per-instance arena. Empty by default until that surface lands. |
| `environ_get` / `environ_sizes_get` | Read 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_get` | The 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 files | Translate to the typed `File` capability behind the host-side fd table entry. |
| `fd_close` | Drop the typed cap handle (release-on-drop in capos-rt). |
| `fd_seek` / `fd_tell` / `fd_filestat_get` | Methods on the `File` cap. |
| `fd_prestat_get` / `fd_prestat_dir_name` | Enumerate the host adapter's preopened-directory table built from manifest grants. |
| `sock_send` / `sock_recv` / `sock_shutdown` | Translate to typed `TcpSocket` / `UdpSocket` cap calls. |
| `poll_oneoff` | Multiplex over the host's capability ring; CQEs are the event source. Open question §3. |
| `fd_advise` / `fd_allocate` / `fd_renumber` | Stub or `ERRNO_NOSYS` until needed. |
| `sched_yield` | No-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 / interface | capOS 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-handler` | Match 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):

```text
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

| Boundary | Native capOS service | WASI host adapter + module |
|---|---|---|
| Authority source | Process CapSet | Host CapSet then per-instance subset |
| Memory isolation | Page tables | Wasm linear-memory bounds-check plus page tables (host process) |
| Code integrity | W^X + NX | Wasm module validation plus immutable `WebAssembly.Module` |
| Cap forgery | Kernel-owned `CapTable` | Host-owned per-instance fd table or resource-handle table; module sees opaque ints/handles only |
| Resource limits | Kernel quotas | Wasm fuel + memory cap + host-side per-instance time/byte budgets |
| Side channels | Hardware-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/tasks/` task record for the first
  WASI host-adapter slice.
- 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 plus dispatchable task record. No code.

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

**Status:** host-runtime scaffold landed 2026-05-05 19:12 UTC. Manifest
and `make run-wasm-host` smoke moved into Phase W.2 (see Status note
below).

- 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.

**Status note (revised 2026-05-06 20:19 UTC):** the v0 W.1 slice landed
only the host-runtime *substrate* — the `capos-wasm/` standalone crate,
the vendored `vendor/wasmi-no_std/wasmi-1.0.9/` snapshot, and the `make
capos-wasm-build` target — without a wasm-host binary,
`system-wasm-host.cue` manifest, or `make run-wasm-host` smoke. The
binary/manifest/smoke trio was rolled into Phase W.2 and landed there
in W.2 sub-slice 1 (2026-05-06 20:19 UTC) using an inline 8-byte empty
wasm module as the payload. Earlier drafts of this status note worried
about re-cutting the same host binary twice (once empty, once with a
Preview 1 surface) and proposed deferring the empty-module smoke until
"hello, wasi" was ready; the actual outcome went the other way: the
empty-module regression is its own slice that exercises wasmi's
`Module::new` + `Linker::instantiate_and_start` end-to-end on capOS,
and later W.2 sub-slices extend the same binary in place with the
Preview 1 import resolver and language-level smokes.

**Deliverable:** a wasm runtime crate compiles and links inside the
capOS userspace `no_std + alloc` build. No imports, no host functions,
no WASI. Validates the runtime crate works in `no_std + alloc`
userspace and that the vendored `wasmi` snapshot exposes `Engine` and
`Store<HostState>` to a future host binary.

**Validation:** `make capos-wasm-build` succeeds against
`targets/x86_64-unknown-capos.json` with `no_std + alloc`; `make
fmt-check` and the host test gates remain green; the kernel and other
userspace crates are untouched (no kernel surface, no
`schema/capos.capnp` change, no `init/` change).

### Phase W.2 — WASI Preview 1 stdout-only

**Inherits from W.1:** the wasm-host binary, `system-wasm-host.cue`
manifest, and `make run-wasm-host` smoke originally listed under W.1
land here in sub-slice 1, so the same binary that future sub-slices
extend with the Preview 1 import surface also provides the
empty-instantiation smoke.

The phase is landing in four sub-slices, not one big drop, to keep
each diff reviewable. `random_get` production wiring stays owned by
Phase W.4 (entropy + clocks production-ready); W.2 leaves it stubbed
as `ERRNO_NOSYS`:

- W.2 sub-slice 1 (landed): wasm-host binary, `system-wasm-host.cue`
  empty-instantiation manifest, `make run-wasm-host` smoke, and the
  one-time userspace ABI bump (USER_STACK_BASE etc.) that wasmi's
  ~3 MiB BSS forced.
- W.2 sub-slice 2 (landed 2026-05-07 08:03 UTC): Preview 1 stdout-only
  imports (args/environ as empty, `clock_time_get(MONOTONIC)`,
  `proc_exit`, `fd_write(1,…)`/`fd_write(2,…)`); everything else stubs
  as `ERRNO_NOSYS` including `random_get` (Phase W.4 promotes that to
  production). The wasm-host smoke now drives a 114-byte hand-encoded
  probe module that calls `random_get`, stores the returned errno in
  an exported global, and refuses to print the `nosys=52` proof line
  unless it equals `ERRNO_NOSYS`.
- W.2 sub-slice 3 (landed 2026-05-07 09:36 UTC): Rust `hello, wasi`
  smoke (`demos/wasi-hello-rust/`, `system-wasi-hello-rust.cue`,
  `make run-wasi-hello-rust`). The wasm-host binary now optionally
  reads a `BootPackage` cap, walks the manifest's `binaries[]` for
  the `wasi-payload` entry, instantiates it through the same
  Preview 1 linker, and explicitly invokes the `_start` export
  (wasmi's `instantiate_and_start` runs the WebAssembly `start`
  section, NOT WASI's `_start`). The sub-slice 1+2 regression keeps
  running first; the existing `make run-wasm-host` smoke continues to
  pass because it does not grant `boot`.
- W.2 sub-slice 4 (landed 2026-05-07 10:53 UTC): C `hello, wasi` smoke
  (`demos/wasi-hello-c/`, `system-wasi-hello-c.cue`,
  `make run-wasi-hello-c`). The wasm-host payload-load path landed in
  sub-slice 3 carries the C `.wasm` payload too — sub-slice 4 only
  added the C toolchain wiring (system clang-18 with
  `--target=wasm32-wasi --sysroot=/usr` against the Ubuntu wasi-libc +
  `libclang-rt-18-dev-wasm32` packages), the second manifest, the
  matching smoke harness, and these closeout stamps. **Phase W.2 is
  done.**

- **W.2 sub-slice 1 (landed 2026-05-06 20:19 UTC):** the wasm-host userspace binary,
  `system-wasm-host.cue` empty-instantiation manifest,
  `tools/qemu-wasm-host-smoke.sh` assertion harness, and the
  userspace-image budget bump that wasmi's ~3 MiB BSS requires.
  USER_STACK_BASE moved from 0x60_0000 to 0x100_0000 in
  `capos-config/src/process_layout.rs`; RING_VADDR
  (`capos-config/src/ring.rs`) and CAPSET_VADDR
  (`capos-config/src/capset.rs`) shifted in lockstep, and every
  linker.ld assertion (`init/`, `capos-rt/`, `demos/`, `shell/`,
  `capos-wasm/`) and the `system-spawn.cue` stack-overlap-elf
  fixture were updated to match. No Preview 1 imports yet — the
  binary instantiates the inline 8-byte empty wasm module and
  exits cleanly through the existing capos-rt entrypoint.
- **W.2 sub-slice 3 (landed 2026-05-07 09:36 UTC) and W.2 sub-slice 4
  (landed 2026-05-07 10:53 UTC):** language-level Rust + C
  `hello, wasi` smokes plus the manifest-payload load path on the
  wasm-host binary. Phase W.2 is closed by sub-slice 4.

Sub-slice 1 (landed) delivered:

- The `wasm-host` userspace binary built on the W.1 scaffold,
  instantiating an inline 8-byte empty wasm module through
  `wasmi::Linker::instantiate_and_start`.
- Manifest `system-wasm-host.cue` (empty-instantiation regression).
- Smoke `make run-wasm-host` (asserted by
  `tools/qemu-wasm-host-smoke.sh`).

Sub-slice 2 (landed) delivered:

- `capos-wasm/src/wasi/preview1.rs` Preview 1 import resolver on top
  of the existing wasm-host binary, registering 46 `wasi_snapshot_preview1`
  imports against a fixed-arity `wasmi::Linker<HostState>`.
- Implemented surface: `args_get`, `args_sizes_get`, `environ_get`,
  `environ_sizes_get` (all return zero counts / empty buffers);
  `clock_time_get(CLOCKID_MONOTONIC)` via the host's `TimerClient`
  (`CLOCKID_REALTIME` returns `ERRNO_NOSYS` until a wall-clock cap
  exists); `proc_exit` via `capos_rt::syscall::exit`; `fd_write(1, …)`
  and `fd_write(2, …)` via the host's `Console.write` byte path with
  a fixed 4 KiB scratch ceiling (oversize total → `ERRNO_INVAL`); all
  other Preview 1 imports stubbed as `ERRNO_NOSYS` (including
  `random_get`, which Phase W.4 promotes against `EntropySource`).
- Manifest update (`system-wasm-host.cue` now grants Console + Timer)
  and smoke harness update (`tools/qemu-wasm-host-smoke.sh` asserts
  the new `[wasm-host] preview1 imports linked: ...; nosys=52` proof
  line in addition to the empty-instantiation regression).
- Probe-driven evidence: a 114-byte hand-encoded probe module imports
  `random_get`, calls it once at instantiation, stores the returned
  errno in an exported global, and the host refuses to print the
  proof line unless the global reads back as `ERRNO_NOSYS = 52`.

Sub-slice 3 (landed 2026-05-07 09:36 UTC) delivered:

- `demos/wasi-hello-rust/` standalone crate built against the upstream
  `wasm32-wasip1` target. Source is a single `println!`; the produced
  `hello.wasm` (~40 KiB) imports `environ_get`, `environ_sizes_get`,
  `fd_write`, and `proc_exit` from `wasi_snapshot_preview1`, all of
  which the sub-slice 2 resolver already implements.
- `capos_wasm::payload` helper module: streams the capnp-encoded
  `SystemManifest` blob through `BootPackage.readManifestChunk`
  (4 KiB chunks) and walks `binaries[]` via raw capnp readers to
  return the bytes for a named entry. The wasm-host binary calls
  this only when the manifest grants the optional `boot`
  (`BootPackage`) cap, so the sub-slice 1+2 `make run-wasm-host`
  smoke -- which does not grant `boot` -- keeps passing unchanged.
- `system-wasi-hello-rust.cue` manifest: lists the wasm-host ELF and
  the `wasi-payload` blob, grants Console + Timer + BootPackage to
  the wasm-host, and reuses the shared `cue/defaults` package.
- `tools/qemu-wasi-hello-rust-smoke.sh` smoke harness: asserts the
  existing sub-slice 1 + 2 proof lines, the new
  `Hello from WASI on capOS` payload stdout (the load-bearing
  evidence), and the clean process/scheduler exit pair. The
  wasm-host payload-stage proof line is *not* asserted because
  wasi-libc's `_start` is allowed to terminate via `proc_exit` from
  inside the Preview 1 import handler, in which case the host
  process exits before the wasm-host can print its post-_start
  proof line.
- `make wasi-hello-rust-build` cargo wrapper that clears
  `RUSTFLAGS`/`CARGO_ENCODED_RUSTFLAGS` so the kernel-target
  rustflags pinned in the repo `.cargo/config.toml` do not leak into
  the wasm build.
- capos-rt re-export additions: `capos_capnp` and
  `default_reader_options` are now reachable from `capos_rt::*` so
  `capos-wasm` keeps a single direct path-dep on capos-rt and the
  vendored wasmi tree (adding `capos-config` directly to capos-wasm
  triggered an unrelated cargo workspace-inheritance error against
  the vendored wasmi at `vendor/wasmi-no_std/wasmi-1.0.9/`).

Sub-slice 4 (landed 2026-05-07 10:53 UTC) delivered:

- `demos/wasi-hello-c/` standalone C smoke (NOT a Cargo crate; built
  directly with system clang-18 + lld via the Makefile
  `wasi-hello-c-build` target). Source is a single
  `printf("Hello, wasi from capOS C\n")` `main()` compiled with
  `--target=wasm32-wasi --sysroot=/usr` against the Ubuntu wasi-libc +
  `libclang-rt-18-dev-wasm32` apt packages; the produced
  `hello-c.wasm` (~46 KiB) imports five functions from
  `wasi_snapshot_preview1`: `fd_close`, `fd_fdstat_get`, `fd_seek`,
  `fd_write`, and `proc_exit`. `fd_write` and `proc_exit` reach the
  host's granted Console cap and the clean capos-rt exit path
  implemented in sub-slice 2; `fd_close`, `fd_fdstat_get`, and
  `fd_seek` return `ERRNO_NOSYS = 52` from the same sub-slice 2 stub
  surface, which is sufficient for wasi-libc's stdout-only path.
- `system-wasi-hello-c.cue` manifest: same shape as
  `system-wasi-hello-rust.cue`, lists the wasm-host ELF and the
  `wasi-payload` blob, grants Console + Timer + BootPackage to the
  wasm-host, and reuses the shared `cue/defaults` package.
- `tools/qemu-wasi-hello-c-smoke.sh` smoke harness: asserts the
  existing sub-slice 1 + 2 proof lines, the new
  `Hello, wasi from capOS C` payload stdout (the load-bearing
  evidence), and the clean process/scheduler exit pair.
- `make wasi-hello-c-build` target that runs system clang with
  `RUSTFLAGS`/`CARGO_ENCODED_RUSTFLAGS` cleared (matching the
  `wasi-hello-rust-build` shape so the two flows stay symmetric).
- No host-side change to `capos-wasm/`: the manifest-payload load
  path landed in sub-slice 3 carries the C `.wasm` payload through
  the same wasm-host binary unchanged.

**Deliverable:** the first WASI-hosted, sandboxed portable-payload
language path lands on capOS. Both Rust (`wasm32-wasip1`) and C
(`wasm32-wasi`) `hello, wasi` payloads run inside the wasmi
interpreter under the `wasm-host` capOS process and reach the host's
granted Console cap through Preview 1 `fd_write`. Native C already
boots through the libcapos C-substrate (`make run-c-hello`) and the
POSIX adapter (`make run-posix-dns-smoke`); this phase specifically
adds the WASI-hosted path -- in particular, C runs on capOS through
the WASI surface without requiring any `libcapos`/POSIX work in
tree, because the wasm-host's host-side imports cover everything
the wasi-libc stdout-only path needs.

**Phase W.2 closed 2026-05-07 10:53 UTC.** Phase W.3 closed
2026-05-07 18:25 UTC. Phase W.4 closed 2026-05-07 20:09 UTC.

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

**Status:** landed 2026-05-07 18:25 UTC. Per-instance CapSet selection
keeps using the existing manifest cap-grant block on
`initConfig.init.caps` (no new cap needed for the v0 argv path); the
new surface is the bounded-text argv grant on
`initConfig.init.wasiArgs`. The wasm-host pulls it out of the
manifest blob through its already-granted `BootPackage` cap, validates
it against the bounds in `capos-wasm/src/payload.rs`
(`WASI_ARGS_MAX_COUNT = 32`, `WASI_ARGS_MAX_ARG_BYTES = 4096`,
`WASI_ARGS_MAX_TOTAL_BYTES = 8192`), packs it into a per-instance
`HostState` argv buffer, and reflects it back through Preview 1
`args_get` / `args_sizes_get`. A 2026-05-13 successor mirrors the
same bounded-text pattern for environment variables through
`initConfig.init.wasiEnv`, validated against
`WASI_ENV_MAX_COUNT = 32`, `WASI_ENV_MAX_ENTRY_BYTES = 4096`, and
`WASI_ENV_MAX_TOTAL_BYTES = 8192`, with interior NULs rejected before
the payload instantiates. Open Question §5 / §6 / §7 status is
recorded in the section below; a future capOS `LaunchParameters` cap
is still the migration path for argv and environment together.

- Per-instance CapSet selection: keeps using the manifest-defined
  cap-grant block (`initConfig.init.caps`) the W.2 sub-slice 3 / 4
  smokes already exercised. Phase W.3 does not add a new cap; it adds
  the `wasiArgs` bounded-text grant alongside the cap list. Future
  phases (W.4 entropy, W.5 namespaces, W.6 sockets) will extend the
  same `caps` block with their respective surfaces.
- Bounded-text argv grant: `initConfig.init.wasiArgs` is a CUE text
  list. Schema/`schema/capos.capnp` is unchanged because `initConfig`
  is already `CueValue` and unknown sub-fields under
  `initConfig.init` are ignored by the existing manifest decoder.
  The wasm-host walks the field directly through raw capnp readers in
  `capos-wasm/src/payload.rs::read_wasi_args`. An absent or empty
  `wasiArgs` keeps the W.2 "no argv" behaviour (`args_sizes_get`
  reports zero, `args_get` writes nothing) so the existing
  `make run-wasm-host`, `make run-wasi-hello-rust`, and
  `make run-wasi-hello-c` smokes stay unchanged.
- Bounded-text environment grant: `initConfig.init.wasiEnv` is a CUE
  text list of entries such as `KEY=value`. It uses the same raw
  capnp reader path as `wasiArgs`, the same no-schema-change
  `initConfig` `CueValue` extension point, and the same empty-by-
  default behavior: absent or empty `wasiEnv` makes
  `environ_sizes_get` report zero and `environ_get` write nothing.
  Oversized entry count, oversized individual entries, oversized
  packed total bytes, and interior NUL bytes make wasm-host abort with
  stable exit codes rather than truncating or corrupting the WASI
  Preview 1 NUL-terminated layout.
- Migration to a future `LaunchParameters` cap: when capOS gains a
  capability-shaped `LaunchParameters` surface (the same one
  envisioned by `docs/proposals/userspace-binaries-proposal.md` Part
  5 and the future shell launch flow), the wasm-host will swap
  `read_wasi_args` for a typed `LaunchParametersClient` lookup and
  the manifest-side `wasiArgs` field becomes redundant. The
  bounds constants stay relevant either way (a typed
  `LaunchParameters` cap will still need byte ceilings before it
  ships argv into wasm linear memory).
- Smoke: `demos/wasi-cli-args/` (Rust, `wasm32-wasip1`) reads
  `argv[1]` and prints it through `println!` ->
  `fd_write(1, …)` -> the host's `Console` cap. The harness
  (`tools/qemu-wasi-cli-args-smoke.sh`) asserts the existing
  sub-slice 1 + 2 regression lines plus the load-bearing
  `capos-wasi-cli-args-sentinel` line.

**Deliverable:** per-instance CapSet selection works (commit landed
2026-05-07 18:25 UTC; smoke `make run-wasi-cli-args`).

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

**Status:** landed 2026-05-07 20:09 UTC. The wasm-host looks up an
optional per-instance `EntropySource` cap from the CapSet under the
well-known name `random`. When the manifest grants it, the typed
`EntropySourceClient` is installed on `HostState` after the W.2
sub-slice 2 probe regression runs (so the probe's
`random_get(0, 0)` call still observes the closed-fail
`ERRNO_NOSYS = 52` path byte-identically with the W.2/W.3 proof
line). Preview 1 `random_get` then drains arbitrary wasm-supplied
byte ranges into the manifest-granted entropy stream by chunking
against the kernel cap's per-call `MAX_ENTROPY_FILL_BYTES = 64`
ceiling and walking up to `RANDOM_GET_MAX_BYTES = 65_536` total
bytes per Preview 1 invocation. Truncated kernel responses, RDRAND
unavailable status, and any transport-level error surface as
`ERRNO_IO`; out-of-bounds wasm pointer writes surface as
`ERRNO_FAULT`; oversized requests surface as `ERRNO_INVAL`. The
ungranted-variant manifest still routes Preview 1 `random_get`
through the no-grant refusal branch which never enters the kernel,
so an instance without an `EntropySource` grant cannot leak
entropy.

- 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 stays deferred until capOS has a typed
  `WallClock` / `RealTimeClock` cap. `clock_time_get(CLOCKID_REALTIME)`
  keeps returning the W.2 sub-slice 2 sentinel `ERRNO_NOSYS` so a
  Preview 1 guest can distinguish "host refused" from a kernel /
  transport failure; future phases promote it once the wall-clock cap
  lands. The monotonic clock keeps using the manifest-granted `Timer`
  cap unchanged.
- Smoke: `demos/wasi-random/` (Rust, `wasm32-wasip1`) reads N=64
  bytes via a raw Preview 1 import binding (avoiding wasi-libc's
  panic-on-errno wrapper so the ungranted-variant payload can print a
  refusal sentinel and exit with code 52 rather than aborting). The
  granted-variant smoke (`make run-wasi-random` /
  `tools/qemu-wasi-random-smoke.sh`) asserts the W.2 sub-slice 1 + 2
  regression proof lines, the load-bearing
  `[wasi-random] entropy_bytes=64 entropy_bound_ok=true` line, and a
  clean exit; the ungranted-variant smoke (`make
  run-wasi-random-ungranted` /
  `tools/qemu-wasi-random-ungranted-smoke.sh`) asserts the same
  regression lines plus the load-bearing
  `[wasi-random] random_get returned errno=52 (ENOSYS)` refusal
  sentinel and refuses the granted-variant entropy line.

**Deliverable:** Preview 1 `random_get` is wired to the kernel
`EntropySource` cap with the closed-fail refusal contract, the
`clock_time_get(REALTIME)` deferral is documented, and the
ungranted-variant smoke proves both. A 2026-05-13 compatibility slice
also promotes authority-free Preview 1 imports that need no new cap:
`clock_res_get(CLOCKID_MONOTONIC)` returns the monotonic nanosecond
resolution, `sched_yield` returns success as a no-op, `fd_fdstat_get`
for stdout/stderr returns character-device write metadata, and
`fd_seek` for stdout/stderr returns `ERRNO_SPIPE`. The direct-import
`make run-wasi-stdio-fd` smoke requires all promoted imports to return
non-`ERRNO_NOSYS` results. The remaining non-filesystem / non-socket
Preview 1 imports that still return `ERRNO_NOSYS` -- `poll_oneoff`,
`proc_raise`, fd operations that need file or close-state authority,
and the `path_*` paths -- stay future work; promoting each to
"honest" needs either the typed capability it would route through
(for example a `WallClock` / `RealTimeClock` cap for `REALTIME` or
namespace/file caps for storage fds and paths) or an explicit decision
to keep the NOSYS refusal as the v0 honest behaviour. Phase W.4 closed
2026-05-07 20:09 UTC.

Harness-hardening landed on 2026-05-13: `make
run-wasi-preview1-refusals` boots a direct-import payload that calls
representative blocked filesystem/socket imports with no
Namespace/File/Store/socket authority in the manifest and requires each
return to equal `ERRNO_NOSYS = 52`. The initial slice (2026-05-13
08:50 UTC) covered `path_open`, `fd_prestat_get`, `fd_read`,
`sock_send`, `sock_recv`; a follow-up (2026-05-13 21:15 UTC) extended
the harness to also cover `fd_pread`, `fd_pwrite`,
`path_create_directory`, and `sock_shutdown`, bringing the total to nine
covered imports. As each filesystem import gains a real implementation its
no-preopen errno migrates from `ERRNO_NOSYS = 52` to `ERRNO_BADF = 8`
(`path_open` / `fd_prestat_get` / `fd_read` with Phase W.5;
`path_create_directory` on 2026-05-24 10:09 UTC; `fd_pread` / `fd_pwrite`
when positional I/O landed -- see below); the harness asserts the
current errno per import rather than a blanket NOSYS. Only the socket
imports (`sock_send` / `sock_recv` / `sock_shutdown`) still return
`ERRNO_NOSYS = 52`. This records fail-closed evidence for the current
surface only; it does not implement W.6 behavior.

### Phase W.5 — WASI Preview 1 filesystem (landed 2026-05-17 05:42 UTC)

- Map preopened-dir fds to a manifest-granted root `Directory` cap from
  the per-instance CapSet. The v0 surface ships a single preopen at
  fd 3 named `/preopen-0`; the manifest CapSet slot name is `root`
  (matching the POSIX adapter P1.4 Slice 4 bootstrap). `Namespace` /
  `Store` integration is deferred until a use case requires the
  content-addressed pseudo-fs shape -- the kernel caps remain
  available for a future slice (storage Phase 3 slice 3 landed them).
- Implement `path_open`, `fd_read`, `fd_write`, `fd_seek`, `fd_close`,
  `fd_filestat_get`, `fd_prestat_get`, and `fd_prestat_dir_name`
  against the kernel `Directory` / `File` cap interface in
  `capos-wasm/src/wasi/fs.rs`. The resolver mirrors POSIX P1.4
  Slice 4 (`libcapos-posix/src/path.rs`): non-leaf segments walk
  `Directory.sub`; the leaf mints either an existing or freshly
  created `File` via `Directory.open(flags=CREATE|TRUNCATE)`.
- Preview 1 base and inheriting rights are stored in the host fd table.
  The single preopen advertises only implemented directory/path rights and
  inheritable File rights; `path_open` refuses requested base or inheriting
  rights outside the preopen's inheriting set, and opened File fds retain
  exactly the requested rights. `fd_fdstat_get` reports those stored rights,
  and `fd_fdstat_set_rights` can only attenuate them. `fd_read`, `fd_write`,
  `fd_pread`, `fd_pwrite`, `fd_seek`, `fd_tell`, `fd_filestat_get`, and
  `fd_filestat_set_size` check the stored File rights before constructing a
  `FileClient`; `path_create_directory`, `path_remove_directory`,
  `path_unlink_file`, `path_filestat_get`, `fd_readdir`, and preopen
  `fd_filestat_get` check the preopen rights before constructing a
  `DirectoryClient` or resolving the path.
- WASI `fd_close` only releases the local cap-table slot. The
  kernel-side `File.close()` would invalidate the `Arc<FileCap>` that
  the parent `Directory` holds keyed by entry name, breaking re-open
  of the same path; WASI semantics expect `fd_close` to release the
  per-process fd without deleting the underlying file. New
  `path_open` calls for the same path mint a fresh local handle
  against the same kernel-side entry.
- Preopen sandbox: the resolver refuses absolute paths (leading `/`)
  and parent-escape segments (`..`, `.`) with `ERRNO_NOTCAPABLE = 76`.
  The single preopen has no parent reachable through any path syntax.
- The `make run-wasi-fs` smoke (`system-wasi-fs.cue`,
  `demos/wasi-fs/`, `tools/qemu-wasi-fs-smoke.sh`) completes a full
  `path_open(CREAT+TRUNC)` / `fd_write` / `fd_close` / re-open /
  `fd_filestat_get` / `fd_seek` / `fd_read` round trip, asserts
  both the absolute-path refusal and the parent-escape refusal, and proves
  narrowed File/preopen rights fail closed with `ERRNO_NOTCAPABLE` before
  the underlying File/Directory client call. The
  `make run-wasi-preview1-refusals` smoke continues to prove the
  fail-closed contract for an ungranted manifest: `path_open(3, ...)`,
  `fd_prestat_get(3)`, and `fd_read(3, ...)` now return
  `ERRNO_BADF = 8` (no preopen) instead of the pre-W.5 stub
  `ERRNO_NOSYS = 52` (`path_create_directory` joined this BADF group
  2026-05-24 10:09 UTC, and `fd_pread` / `fd_pwrite` joined when
  positional I/O landed -- see below); only the socket imports continue
  to return `ERRNO_NOSYS`.
- Kernel authority surface landed 2026-05-14 (RAM-backed `File`,
  `Directory`, `Store`, and `Namespace` kernel caps with QEMU smokes
  `run-file-server-smoke`, `run-directory-server-smoke`,
  `run-store-namespace-smoke`). W.5 wires the wasm-host adapter to
  the `Directory` / `File` subset of that authority; `Store` /
  `Namespace` integration is deferred until a use case requires it.
- `fd_readdir` landed 2026-05-24 08:44 UTC over the existing preopen
  `Directory` cap (`DirectoryClient::list` -- no schema or
  generated-bindings change). `fs::fd_readdir_impl` enumerates the
  preopen, rejecting open file fds with `ERRNO_NOTDIR = 54` and unknown
  fds with `ERRNO_BADF = 8`; `preview1::fd_readdir` serializes the
  fixed 24-byte little-endian Preview 1 `dirent` records (`d_next`,
  zero `d_ino`, `d_namlen`, `d_type` from `DirEntry.is_dir`) followed
  by name bytes, with cookie-based resume and a short-buffer truncation
  contract that never writes past `buf_len`. The `make run-wasi-fs`
  smoke now also enumerates the `smoke.txt` it created
  (`readdir_found_smoke=true`) and proves the short-buffer truncation.
- `fd_tell` and `fd_filestat_set_size` landed 2026-05-24 09:34 UTC,
  completing the File-cap method triad (no schema or generated-bindings
  change -- `File.truncate` already shipped). `fs::fd_tell_impl` is a
  pure host-side read of the maintained `FileEntry::position` (symmetric
  with `fd_seek`'s SET/CUR branches); `fs::fd_filestat_set_size_impl`
  calls `FileClient::truncate_wait` and leaves the file offset unchanged
  per the WASI contract. `preview1::fd_tell` returns `ERRNO_SPIPE = 70`
  on a stdio fd (mirroring `fd_seek`) and writes the position as LE-u64;
  `preview1::fd_filestat_set_size` rejects a negative `size` with
  `ERRNO_INVAL = 28` and maps non-file fds to `ERRNO_BADF = 8`. The
  `make run-wasi-fs` smoke now asserts `fd_tell` reports the post-write
  position (`tell_ok=true`) and `fd_filestat_set_size` shrinks the file
  (`truncate_size=4`), plus the stdio refusals for both imports.
- `path_create_directory` and `path_remove_directory` landed
  2026-05-24 10:09 UTC over the preopen `Directory` cap
  (`DirectoryClient::mkdir` / `remove` -- no schema or generated-bindings
  change; `Directory.mkdir`/`remove` already shipped).
  `fs::path_create_directory_impl` / `path_remove_directory_impl` reuse the
  `path_open` resolve-parent-and-leaf path and the same preopen sandbox, so
  absolute paths and `..` segments are refused with `ERRNO_NOTCAPABLE = 76`
  before any kernel call; the `mkdir` result-cap (a fresh `Directory` handle
  the WASI layer does not retain) is released immediately to avoid leaking a
  cap-table slot. The `make run-wasi-fs` smoke now creates `subdir`, confirms
  it via `fd_readdir` (directory `d_type`), removes it, confirms it is gone,
  and asserts the directory-write sandbox refusals
  (`mkdir_ok=true rmdir_ok=true dir_escape_refused=true`). Implementing
  `path_create_directory` moves its no-preopen errno from `ERRNO_NOSYS = 52`
  to `ERRNO_BADF = 8` (the base-fd preopen lookup precedes the path), so the
  `make run-wasi-preview1-refusals` harness now asserts it in the BADF group.
- `fd_pread` and `fd_pwrite` landed 2026-05-30 14:49 UTC as positional I/O
  over the host `File` cap (no schema or generated-bindings change -- the
  kernel `File.read` / `File.write` methods already carry an explicit byte
  offset, and `fd_read` / `fd_write` already drive them). `fs::fd_pread_impl`
  / `fs::fd_pwrite_impl` mirror `fd_read_impl` / `fd_write_file_impl` but use
  the WASI-supplied `offset` and, per the WASI Preview 1 contract, leave
  `FileEntry::position` untouched -- the defining positional-I/O invariant.
  `preview1::fd_pread` / `fd_pwrite` reuse the same guest-memory iovec
  gather/scatter helpers `fd_read` / `fd_write` were refactored onto (one
  walker, not two), reject a negative `offset` with `ERRNO_INVAL = 28`, and
  return `ERRNO_SPIPE = 70` on a stdio fd (mirroring `fd_seek` / `fd_tell`).
  The `make run-wasi-fs` smoke now writes "ABCD" at offset 2, reads it back
  at offset 2, and asserts the fd's stream position is unchanged
  (`pwrite_pread_ok=true pos_unchanged=true`), that a negative offset is
  refused (`pread_neg_offset_inval=true`), and that a stdio fd surfaces a
  non-`ERRNO_NOSYS` error (`ppos_stdio_refused=true`). The
  `make run-wasi-preview1-refusals` harness moves both imports into the BADF
  group (fd 3 is a bad descriptor against an absent preopen).
- `path_filestat_get` and `path_unlink_file` landed 2026-05-30 as
  path-resolved metadata/removal over the host `File` / `Directory` caps (no
  schema / generated-bindings change). `fs::path_filestat_get_impl` resolves the
  leaf under the preopen, opens a transient read-only `File` (`flags = 0`), runs
  `File.stat`, and releases the transient cap before returning the size;
  `fs::path_unlink_file_impl` deletes the named entry through `Directory.remove`
  (the same void-result op `path_remove_directory` uses, which removes file
  leaves). Both enforce the absolute/`..` `ERRNO_NOTCAPABLE` sandbox in
  `resolve_parent_and_leaf` before any kernel call; `preview1::path_filestat_get`
  accepts and ignores the `lookupflags` symlink-follow bit (no symlinks in v0)
  and writes the 64-byte filestat via `write_filestat`. The
  `make run-wasi-fs` smoke stats `smoke.txt` by path (size 4, regular-file type)
  and unlinks it, and `make run-wasi-preview1-refusals` moves both imports into
  the BADF group. The remaining `ERRNO_NOSYS` returns are the deliberately
  deferred surfaces (`fd_advise`, `fd_allocate`, the sync family, the path
  timestamp/symlink/link family (`path_filestat_set_times`, `path_symlink`,
  `path_readlink`, `path_link`, `path_rename`), `poll_oneoff`, `proc_raise`,
  and the W.6-blocked socket family).

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

### 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.
- `make run-wasi-preview1-refusals` proves representative socket
  imports (`sock_send`, `sock_recv`, `sock_shutdown`) fail closed with
  `ERRNO_NOSYS = 52` when no socket cap is present. This is current
  refusal evidence only; W.6 remains blocked until the networking
  authority exists.

**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/backlog/index.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.

| Language | WASI status | Toolchain | Native capOS alternative | When WASI wins |
|---|---|---|---|---|
| **Rust** | `wasm32-wasip1` Tier 2 since 1.78; `wasm32-wasip2` Tier 2 since 1.82 | `cargo build --target wasm32-wasip2` | `targets/x86_64-unknown-capos.json` (implemented) | Untrusted Rust plugins. Cross-compiled tools. |
| **C / C++** | wasi-libc + Clang `--target=wasm32-wasi`; wasi-sdk packaged | `clang --target=wasm32-wasi` | future `libcapos` | Any 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 build` | future `GOOS=capos` (`go-runtime-proposal.md`) | CUE evaluation, `go run` style tools, single-goroutine compute. |
| **TinyGo** | wasip1 supported; wasip2 supported in dev branch | `tinygo build -target=wasip2` | n/a | Smaller Go binaries; Component Model export of typed interfaces. |
| **Python (CPython)** | `wasm32-unknown-wasip1` Tier 2 (PEP 11) | Upstream CPython build | future native CPython through POSIX adapter | Sandboxed Python plugins, configuration scripts. |
| **AssemblyScript** | Designed for wasm; WASI host integration via runtime | `asc` | n/a | Lightweight typed scripting. Less interesting on capOS than Lua. |
| **Zig** | Native wasm32-wasi target; no runtime overhead | `zig build-exe -target wasm32-wasi` | n/a | Zig systems code in a sandbox. |
| **Lua / interpreters in general** | A Lua interpreter compiled to wasi runs Lua scripts in a wasm sandbox | Compile any C interpreter to `wasm32-wasi` | Lua piccolo runner (`lua-scripting-proposal.md`) | When Lua scripts are *untrusted*. The piccolo native-Rust runner remains the right answer for *trusted* capOS scripting. |
| **JavaScript** | QuickJS-on-wasi works today | Compile QuickJS to `wasm32-wasi` | QuickJS native runner (future) | Untrusted JS plugins; portable JS without writing a native QuickJS runtime. |
| **.NET (mono-wasi)** | Experimental | dotnet wasi-experimental | n/a | If 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. **Resolved 2026-05-13 16:46 UTC — one wasm instance
   per `capos-wasm` process.** Phases W.2–W.4 shipped on top of this
   shape: `capos_wasm::Runtime` owns exactly one `wasmi::Engine` and one
   `Store<HostState>`, and `HostState` aggregates the per-instance
   `Console` / `Timer` / `RingClient` / optional `EntropySource` /
   optional `BootPackage` clients plus the per-instance `WasiArgs` /
   `WasiEnv` bundles. That host state IS the per-instance state; there
   is no second instance to demultiplex against. The decision aligns
   with capOS capability discipline: the per-process CapSet is the
   authority boundary, manifest grants are scoped one binary at a time
   (`docs/capability-model.md`), and the capOS process boundary already
   provides the fault, fuel/budget, and audit isolation a multi-tenant
   wasm host would otherwise need to rebuild inside the runtime.
   Preview 2 / Component Model migration in Phase W.7 inherits the same
   per-process shape — one `capos-wasm` process per top-level component
   — and gains nothing from packing many components into one process
   while the OS-level isolation is free. A future multi-instance host
   (plugin sandboxes, embedded scripts) is allowed but must come back
   as a separate proposal that names the density target, the fuel and
   `poll_oneoff` reactor design, and the audit/observability shape; it
   does not block any current phase.

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.
   **Resolved 2026-05-13 16:46 UTC — blocking `cap_enter` against the
   single per-process instance, with the surface expanded one
   subscription kind at a time as the underlying caps land.** v0 keeps
   the W.2 sub-slice 2 `ERRNO_NOSYS` stub already in
   `capos-wasm/src/wasi/preview1.rs`: there is no portable subset of
   `poll_oneoff` we can answer correctly without `Namespace` / `File` /
   `TcpSocket` / `UdpSocket` caps, and the existing
   `make run-wasi-preview1-refusals` harness proves the refusal closes
   cleanly. Phase W.5 (filesystem) is the first phase that consumes a
   real subscription kind — `eventtype_clock` against monotonic time
   plus `eventtype_fd_read` / `eventtype_fd_write` against preopen-fd
   `File` handles — and will implement those subscription kinds by
   walking the subscription array, demultiplexing each subscription
   onto a single blocking `cap_enter` over the per-process ring, and
   returning the events the kernel completes. Phase W.6 adds the
   socket subscription kinds against `TcpSocket` / `UdpSocket` once
   the userspace network stack lands. A multi-instance reactor stays
   out of scope: §1 resolves to one wasm instance per `capos-wasm`
   process, so `poll_oneoff` only ever has to demultiplex one
   instance's subscription set, and the kernel ring is already a
   completion-queue primitive that fits that shape directly. Realtime
   clock subscriptions remain `ERRNO_NOSYS` until a typed `WallClock`
   cap exists (same ceiling as `clock_time_get(CLOCKID_REALTIME)`).

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. **Phase W.3 status (2026-05-07 18:25 UTC):**
   unchanged. W.3 does not introduce any capability-issuing import,
   so no result-cap reification path landed; the working answer
   carries forward into W.5 (filesystem) / W.6 (sockets), which are
   the first phases that will exercise it.

6. **`environ_get` source.** Empty-by-default, or backed by a
   `KeyValueScope` / `ConfigOverlay` cap? **Resolved by Phase W.3
   (2026-05-07 18:25 UTC) and the 2026-05-13 follow-up — bounded
   manifest-provided text grant, empty when absent.** Migration to a
   future `LaunchParameters` cap remains the open path. **Original
   working answer:** empty for
   v0 unless the manifest supplies a bounded text environment grant;
   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). **Phase
   W.3 decision (2026-05-07 18:25 UTC):** kept empty-by-default and
   shipped the argv text grant only. **2026-05-13 update:** the same
   bounded manifest-text pattern now exists as
   `initConfig.init.wasiEnv`, a CUE text list under the existing
   `initConfig` `CueValue` field (no `schema/capos.capnp` change).
   Capacity bounds in `capos-wasm/src/payload.rs`:
   - `WASI_ENV_MAX_COUNT = 32` environment entries.
   - `WASI_ENV_MAX_ENTRY_BYTES = 4096` per entry (NUL terminator
     not included).
   - `WASI_ENV_MAX_TOTAL_BYTES = 8192` for the packed environment
     buffer including per-entry NUL terminators.
   Interior NUL bytes inside an entry are rejected. The decoder
   tolerates an absent or empty `wasiEnv`, in which case Preview 1
   `environ_get` / `environ_sizes_get` report zero entries (the W.2
   behavior). A future `LaunchParameters` cap remains the migration
   path for argv and environ together.

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? **Resolved by Phase W.3
   (2026-05-07 18:25 UTC) — bounded manifest-provided argv text grant
   on `initConfig.init.wasiArgs`, migrating to the future
   `LaunchParameters` cap once it exists.** **Original working
   answer:** ship a
   small bounded text grant for v0; migrate to the future
   `LaunchParameters` surface once it exists. **Phase W.3 decision
   (2026-05-07 18:25 UTC):** shipped as
   `initConfig.init.wasiArgs`, a CUE text list under the existing
   `initConfig` `CueValue` field (no `schema/capos.capnp` change).
   Capacity bounds in `capos-wasm/src/payload.rs`:
   - `WASI_ARGS_MAX_COUNT = 32` argv entries.
   - `WASI_ARGS_MAX_ARG_BYTES = 4096` per entry (NUL terminator
     not included).
   - `WASI_ARGS_MAX_TOTAL_BYTES = 8192` for the packed argv buffer
     including per-entry NUL terminators.
   Interior NUL bytes inside an argv entry are rejected (would
   corrupt the WASI Preview 1 NUL-terminated layout). Each violation
   surfaces through a stable wasm-host exit code so harnesses can
   distinguish them from generic decode failures. The decoder
   tolerates an absent or empty `wasiArgs`, in which case Preview 1
   `args_get` / `args_sizes_get` report zero entries (W.2 behaviour).
   Migration to the future `LaunchParameters` cap stays the open path
   per the original working answer.

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.
   **Resolved 2026-05-05 19:12 UTC:** mirror-as-is. The vendored
   snapshot at `vendor/wasmi-no_std/wasmi-1.0.9/` is a static-pinned
   copy of upstream `v1.0.9` with no source patches; cargo
   `default-features = false` strips `std`/`wat` cleanly out of the
   box. Provenance and refresh procedure are recorded in
   `vendor/wasmi-no_std/VENDORED_FROM.md`. This posture is
   independent of what the Lua track chooses; if the two tracks
   diverge, document the divergence in each track's `VENDORED_FROM.md`.

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.

Progress 2026-05-13 16:46 UTC: §1 (per-instance vs per-process) and §3
(`poll_oneoff` semantics) resolved. §1 is locked at one wasm instance
per `capos-wasm` process, matching the per-process `Runtime` +
`Store<HostState>` shape shipped through Phases W.2–W.4 and the
per-process CapSet authority boundary; future multi-instance hosting
must come back as a separate proposal. §3 keeps the W.2 sub-slice 2
`ERRNO_NOSYS` `poll_oneoff` stub for v0 and pre-commits Phases W.5 / W.6
to extend it one subscription kind at a time (monotonic clock + fd
read/write in W.5 against `Namespace`/`File` caps, sockets in W.6
against `TcpSocket`/`UdpSocket` caps), demultiplexed onto a single
blocking `cap_enter` over the per-process ring; multi-instance reactors
remain out of scope. §6 (`environ_get`) and §7 (`args_get`) reclassified
as resolved by Phase W.3 (2026-05-07 18:25 UTC) with the bounded
manifest-text grants on `initConfig.init.wasiEnv` /
`initConfig.init.wasiArgs`; the migration path to a future
`LaunchParameters` cap is preserved.

## Relationship to Other Proposals

- **[Userspace Binaries](userspace-binaries-proposal.md)** 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](../programming-languages.md)** is the
  reader-facing summary of language support; the WASI row points at
  this proposal.
- **[Browser/WASM](browser-wasm-proposal.md)** is the separate
  browser-hosted wasm experiment. Both proposals share wasm-runtime
  insight but target different substrates.
- **[Lua Scripting](lua-scripting-proposal.md)** is the trusted
  capability-scoped script runner using a native (likely piccolo) Lua
  VM. WASI-hosted Lua is the untrusted alternative.
- **[Go Runtime](go-runtime-proposal.md)** 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](storage-and-naming-proposal.md)** defines the
  `Directory` / `File` / `Store` / `Namespace` surfaces that Phase W.5
  consumes.
- **[Networking](networking-proposal.md)** defines the
  `TcpSocket` / `UdpSocket` surfaces that Phase W.6 consumes.
- **[Service Architecture](service-architecture-proposal.md)** defines
  `Fetch` / `HttpEndpoint`, useful as the v0 networking shim before the
  full userspace network stack lands.
