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-wasmuserspace host adapter built oncapos-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(requiresshared-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_SHAREDmmap. - 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-rtsurface 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=caposalternative to Go-on-WASI.
Research Grounding
Relevant research and external references:
- WASI Preview 2 launch — Bytecode Alliance, “WASI 0.2 Launched”.
- Component Model status — eunomia, “WASI and the WebAssembly Component Model: Current Status”.
- WIT resources / portable plugins — Medium, “WASI 2.0 Components: Portable, Fast Plugins”.
- Externref design — Bytecode Alliance, “WebAssembly Reference Types in Wasmtime”.
- Rust target stabilization — Rust Blog, “Changes to Rust’s WASI targets” and “wasm32-wasip2 Tier 2”.
- TinyGo WASI — TinyGo WASI guide, wasmCloud, “Compile Go directly to WebAssembly components with TinyGo and WASI P2”.
- Runtime survey — Wasmi v0.32 release notes, arXiv 2404.12621 “Research on WebAssembly Runtimes”, Colin Breck, “Choosing a WebAssembly Run-Time”.
- Runtime repos — wasmi, WAMR, wasmtime, wasm3, 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
- 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. - 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.
- 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.
- The wasm sandbox is defence in depth, not the isolation boundary.
The capOS process boundary remains primary. Wasm bounds checking and
immutable
Modulevalidation add a second software-enforced boundary inside the host process so an entire untrusted module image can be confined. - 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.
- 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. - 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.
| 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
libcaposdoes not yet provide. - Genuine
no_std + allocsupport means no host-side OS abstraction is required for the runtime itself; it sits cleanly oncapos-rt. - Built-in fuel metering matches capOS’s preference for explicit resource accounting.
externrefsupport 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
stduserspace and a futures executor. capOS userspace isno_std + alloctoday; this is the same blocker that keeps the Rustcapnp-rpccrate (v0.25) offcapos-rtand 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
libcaposexist 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:
- 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 existingwasip1binary unchanged. A wasm module cannot pass a typed cap to another wasm module without going through the host. - Custom
externrefimport (alternative; not recommended). Requires thereference-typesproposal (supported by wasmi >=0.24, wasmtime, wasmer; partial in wasm3). The host adapter exports custom imports likecap_call_refthat take anexternreftyped 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. - Preview 2 / Component Model resources (target for W.7+).
Resources in the Component Model are unforgeable typed handles.
Components that import
wasi:filesystem/types.descriptorreceive a handle that is the host-sideOwnedCapability<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:
- One wasm instance per
capos-wasmprocess (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. - Many instances per
capos-wasmprocess (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; thepoll_oneoffreactor 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
| 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 useposix_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: requiresshared-memory+atomics+bulk-memoryfeatures and a runtime that supports them. Out of scope for v0. - Live
mmapof 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.mdralphex 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 oncapos-rt. - Vendor the chosen runtime (wasmi recommended; one local cargo dep
patched for
no_std + allocif needed). - Host process can
WebAssembly.compile(bytes)theninstantiate(no imports)then run an empty_start. No imports resolved yet. - Manifest: new
system-wasm-host.cueboots one host process with one embedded.wasmblob (the smoke binary). - Smoke:
make run-wasm-hostboots, 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 grantedEntropySourcecap; refuse withERRNO_NOSYSif absent),fd_write(1, …)→ host’sConsole.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-wasip1and Cclang --target=wasm32-wasi. Both printHello 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.cueso 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
LaunchParameterssurface — 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
EntropySourcecap (the in-tree CSPRNG capability; seeEntropySourceClientandKernelCapSource::EntropySource) through the host adapter as the backing forrandom_get. The same cap is the natural future analogue of the browser’scrypto.getRandomValuessurface. - 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
Namespacecaps from the manifest. - Implement
path_open,fd_read,fd_write,fd_seek,fd_close,fd_filestat_get,fd_prestat_get,fd_prestat_dir_nameagainst whateverNamespace/File/Storecap surface exists at the time. - Blocked on the missing storage caps:
Namespace,File,Store, andDirectorydo 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. againstTcpSocket/UdpSocketcaps when the userspace network stack lands.- Until then, an HTTP client over
Fetch/HttpEndpointis 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.capnpfor component-model resource bridging. Serialise with other schema-touching plans (docs/plans/README.mdConcurrency 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 aScriptPackage(future package-cap surface, same shape as the plannedLaunchParameterswork). - 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_writeto a fd it was not granted, cannot open a path outside its preopened namespaces, and cannot call an unimplemented WASI function without receivingERRNO_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
-
Per-instance vs per-process. One wasm instance per
capos-wasmprocess (recommended) or many? Affects fuel/budget enforcement and the manifest shape. Working answer: one per process for v0; revisit when instance count matters. -
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.
-
poll_oneoffsemantics over the capOS ring. Block the host process’scap_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. -
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
FuelGrantcap)? Affects the cap surface. Working answer: trap-and-exit default; defer theFuelGrantcap until long-running plugins exist. -
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. -
environ_getsource. Empty-by-default, or backed by aKeyValueScope/ConfigOverlaycap? Working answer: empty for v0; bind to whatever environment cap a future capOSLaunchParameterssurface produces (no in-tree plan owns this yet; the shell proposal sketches the broader launch-args/environment discussion). -
args_getsource. Reuse a future capOSLaunchParameterssurface (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 futureLaunchParameterssurface once it exists. -
Vendoring posture for wasmi.
vendor/wasmi-no_std/(forked, patched) or acargo-vendor-style mirror of upstreamdefault-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 + allocuserspace, no host side-effects), and consistency across the two tracks is preferred over a WASI-only bespoke layout. -
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. -
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.
-
Out-of-tree wasm packaging. Will capOS ship pre-built
.wasmbinaries from the boot manifest only, or will operators bring their own? Same scoping question as the futureLaunchParameters/ package-cap surfaces. Working answer: in-tree only for v0–v6; out-of-tree once aStorecap can hold blobs. -
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=caposalternative 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/Namespacesurfaces that Phase W.5 consumes. - Networking defines the
TcpSocket/UdpSocketsurfaces that Phase W.6 consumes. - Service Architecture defines
Fetch/HttpEndpoint, useful as the v0 networking shim before the full userspace network stack lands.