# Proposal: Running capOS in the Browser (WebAssembly, Worker-per-Process)

How capOS goes from "boots in QEMU" to "boots in a browser tab," with each
capOS process executing in its own Web Worker and the kernel acting as the
scheduler/dispatcher across them.

This proposal is the inverse of the
[Browser Capability and Agent Web Sessions](browser-capability-proposal.md)
direction: that one is about *capOS exposing browsers to users and agents*
as capability-scoped services; this one is about *running capOS itself inside
a browser tab* as a teaching and demo substrate. It is also adjacent to but
distinct from the
[WASI Host Adapter](wasi-host-adapter-proposal.md): WASI hosts third-party
wasm modules inside a capOS userspace process under explicit per-instance cap
grants, while the browser port is capOS itself rebuilt for
`wasm32-unknown-unknown` and run inside Workers. Both share the constraint
that authority must be ABI-typed and per-instance, never ambient.

The goal is a teaching and demo target, not a production runtime. It should
preserve the *capability model* — typed endpoints, ring-based IPC, no ambient
authority — while replacing the hardware substrate (page tables, IDT,
preemptive timer, privilege rings) with browser primitives (Worker
boundaries, `SharedArrayBuffer`, `Atomics.wait/notify`).


**Depends on:** Stage 5 (Scheduling), Stage 6 (IPC) — the capability ring is
the only kernel/user interface we want to port. Anything still sitting behind
the transitional `write`/`exit` syscalls must migrate to ring opcodes first.

**Complements:** `userspace-binaries-proposal.md` and
`../programming-languages.md` (language/runtime story),
`service-architecture-proposal.md` (process lifecycle). A browser port
stresses both: the runtime must build for `wasm32-unknown-unknown`, and
process spawn becomes "instantiate a Worker" rather than "map an ELF."

**Non-goals:**

- Running the *existing* x86_64 kernel unmodified in the browser. That's a
  separate question (QEMU-WASM / v86) and is a simulator, not a port.
- Emulating the MMU, IDT, or PIT in WASM. The whole point is to replace them
  with primitives the browser already gives us for free.
- Any persistence, networking, or storage beyond what a hosted demo needs.

---

## Current State

capOS is x86_64-only. Arch-specific code lives under `kernel/src/arch/x86_64/`
and relies on:

| Mechanism | File | Browser equivalent |
|---|---|---|
| Page tables, W^X, user/kernel split | `mem/paging.rs`, `arch/x86_64/smap.rs` | Worker + linear-memory isolation (structural) |
| Preemptive timer (PIT @ 100 Hz) | `arch/x86_64/pit.rs`, `idt.rs` | `setTimeout`/`MessageChannel` + cooperative yield |
| Syscall entry (SYSCALL/SYSRET) | `arch/x86_64/syscall.rs` | Direct `Atomics.notify` on ring doorbell |
| Context switch | `arch/x86_64/context.rs` | None — each process is its own Worker, OS schedules |
| ELF loading | `elf.rs`, `main.rs` | `WebAssembly.instantiate` from module bytes |
| Frame allocator | `mem/frame.rs` | `memory.grow` inside each instance |
| Capability ring | `capos-config/src/ring.rs`, `cap/ring.rs` | **Reused unchanged** — shared via `SharedArrayBuffer` |
| CapTable, CapObject | `capos-lib/src/cap_table.rs` | **Reused unchanged** in kernel Worker |

The capability-ring layer is the only stable interface that survives the port
intact. Everything below `cap/ring.rs` is arch work; everything above is
schema-driven capnp dispatch that doesn't care about the substrate.

---

## Architecture

```mermaid
flowchart LR
    subgraph Tab[Browser Tab / Origin]
        direction LR
        Main[Main thread<br/>xterm.js, UI, loader]
        subgraph KW[Kernel Worker]
            Kernel[capOS kernel<br/>CapTable, scheduler,<br/>ring dispatch]
        end
        subgraph P1[Process Worker #1<br/>init]
            RT1[capos-rt] --> App1[init binary]
        end
        subgraph P2[Process Worker #2<br/>service<br/>spawned by init]
            RT2[capos-rt] --> App2[service binary]
        end
        SAB1[(SharedArrayBuffer<br/>ring #1)]
        SAB2[(SharedArrayBuffer<br/>ring #2)]
        Main <-->|postMessage| KW
        KW <-->|SAB + Atomics| SAB1
        KW <-->|SAB + Atomics| SAB2
        P1 <-->|SAB + Atomics| SAB1
        P2 <-->|SAB + Atomics| SAB2
        P1 -.spawn.-> KW
        KW -.new Worker.-> P2
    end
```

**One Worker per capOS process.** Each process is a WASM instance in its own
Worker, with its own linear memory. Cross-process access is structurally
impossible — `postMessage` and shared ring buffers are the *only* channels.

**Kernel in a dedicated Worker.** Not on the main thread: the main thread is
reserved for UI (terminal, loader, error display). The kernel Worker owns
the `CapTable`, holds the `Arc<dyn CapObject>` registry, dispatches SQEs,
and maintains one `SharedArrayBuffer` per process for that process's
ring. It directly spawns init; all further processes are created via the
`ProcessSpawner` cap it serves.

**Capability ring over `SharedArrayBuffer`.** The existing
`CapRingHeader`/`CapSqe`/`CapCqe` layout in `capos-config/src/ring.rs` already
uses volatile access helpers for cross-agent visibility. Mapping it onto a
`SharedArrayBuffer` is a change of backing store, not of protocol. Both sides
see the same bytes; `Atomics.load`/`Atomics.store` replace the volatile reads
on the host side; on the Rust/WASM side the existing `read_volatile`/
`write_volatile` lower to plain atomic loads/stores under
`wasm32-unknown-unknown` with the `atomics` feature enabled.

**`cap_enter` becomes `Atomics.wait`.** The process Worker calls
`Atomics.wait` on a doorbell word in the SAB after publishing SQEs. The
kernel Worker (or its scheduler tick) calls `Atomics.notify` after producing
completions. That is exactly the io_uring-inspired "syscall-free submit,
blocking wait on completion" the ring was designed around — the browser
happens to give us the primitive for free.

**No preemption inside a process.** A Worker runs to completion on its event
loop turn; the kernel can't interrupt it. This is fine: each process is
single-threaded in its own isolate, and the scheduler only needs to wake the
*next* process after `Atomics.wait`, not forcibly remove the running one.
This is closer to a cooperative capnp-rpc vat model than to the current
timer-preempted kernel, and matches what the capability ring already assumes.

---

## Mapping capOS Concepts to WASM/Browser

### Process isolation

The Worker boundary replaces the page table. Two capOS processes cannot
observe each other's linear memory, cannot jump into each other's code (code
is out-of-band in WASM — not addressable as data), and cannot share globals.
The `SharedArrayBuffer` containing the ring is the *only* intentional shared
region, and it is created by the kernel Worker and transferred to the process
Worker at spawn time.

No W^X enforcement is needed *within* a Worker because WASM has no writable
code region to begin with — `WebAssembly.Module` is validated and immutable.
The MMU's job is done by the WASM type system and validator.

### Address space / memory

Each Worker's WASM instance has one linear memory. `capos-rt`'s fixed heap
initialization uses `memory.grow` instead of `VirtualMemory::map`. The
`VirtualMemory` capability still exists in the schema, but its
implementation in the browser port is a thin wrapper over `memory.grow` with
bookkeeping for "logical unmap" (zeroing + tracking a free list — WASM
doesn't return pages to the host).

Protection flags (`PROT_READ`/`PROT_WRITE`/`PROT_EXEC`) become no-ops with a
documented caveat in the proposal: the browser port does not enforce
intra-process protection. Cross-process protection is structural and
stronger than the native build.

### Syscalls

The three transitional syscalls (`write`, `exit`, `cap_enter`) collapse to:

- `write` — already slated for removal once init is cap-native. In the
  browser port, do not implement it at all. Force the port to drive the
  existing cap-native Console ring path, which forces the rest of the tree
  to be cap-native too. A forcing function, not a cost.
- `exit` — `postMessage({type: 'exit', code})` to the kernel Worker, which
  terminates the Worker via `worker.terminate()` and reaps the process entry.
- `cap_enter` — `Atomics.wait` on the ring doorbell after publishing SQEs,
  with a `waitAsync` variant for cooperative mode if we ever want to avoid
  blocking the Worker's event loop.

### Scheduler

Round-robin is gone; the browser scheduler is the OS scheduler. The kernel
Worker's "scheduler" is reduced to:

1. A poll loop that drains each process's SQ (the existing
   `cap/ring.rs::process_sqes` logic, called on every `notify` or on a
   `setTimeout(0)` tick).
2. A completion-fanout step that pushes CQEs and `Atomics.notify`s the
   target Worker.

No context switch, no run queue, no per-process kernel stack. The code
deleted here is exactly the code that `smp-proposal.md` says needs per-CPU
structures — an orthogonal win: the browser port has no SMP problem because
each process is structurally on its own agent.

### Process spawning

The kernel Worker spawns exactly one process Worker directly — init —
with a fixed cap bundle: Console, `ProcessSpawner`, `FrameAllocator`,
`VirtualMemory`, `BootPackage`, and any host-backed caps (`Fetch`,
etc.) granted to it.

```js
// Kernel Worker bootstrap
const initMod = await WebAssembly.compileStreaming(fetch('/init.wasm'));
const initRing = new SharedArrayBuffer(RING_SIZE);
const initWorker = new Worker('process-worker.js', {type: 'module'});
kernel.registerProcess(initWorker, initRing, buildInitCapBundle());
initWorker.postMessage(
    {type: 'boot', mod: initMod, ring: initRing, capSet: initCapSet,
     bootPackage: manifestBytes},
    [/* transfer */]);
```

All further processes come from init invoking `ProcessSpawner.spawn`.
`ProcessSpawner` is served by the kernel Worker; each invocation:

1. Compiles the referenced binary bytes (`WebAssembly.compile` over the
   `NamedBlob` from `BootPackage`).
2. Creates a `new Worker` and a `SharedArrayBuffer` for its ring.
3. Builds the child's `CapTable` from the `ProcessSpec` the caller
   passed, applying move/copy semantics to caps transferred from the
   caller's table.
4. Returns a `ProcessHandle` cap.

Init composes service caps in userspace: hold `Fetch`, attenuate to
per-origin `HttpEndpoint`, hand each child only the caps its
`ProcessSpec` names. Same shape as native after Stage 6.

### Host-backed capability services

Some capabilities in the browser port are implemented by talking to the
browser rather than to hardware. `Fetch` and `HttpEndpoint` — drafted in
[service-architecture-proposal.md](service-architecture-proposal.md) —
are the canonical example. On native capOS they run over a userspace
TCP/IP stack on virtio-net/ENA/gVNIC. In the browser port, the service
process is replaced by a thin implementation living in the kernel Worker
(or a dedicated "host bridge" Worker) that dispatches each capnp call
by calling `fetch` / `new WebSocket` and returning the response as a
CQE. The attenuation story is unchanged: `Fetch` can reach any URL,
`HttpEndpoint` is bound to one origin at mint time, derived from
`Fetch` by a policy process.

This is not a back door. The capability is granted through the manifest
exactly as on native. Processes without the cap cannot reach the host's
network, cannot discover it, and cannot forge one. The only difference
from native is the implementation of the service behind the `CapObject`
trait — same schema, same `TYPE_ID`, same error model.

The same authority-boundary rule the trusted local
[Remote Session UI Security Proposal](remote-session-ui-security-proposal.md)
enforces between a loopback browser bridge and the upstream capOS gateway
applies inside the browser port: browser JavaScript on the main thread is
*untrusted UI*, the kernel Worker holds the `CapTable`, and the JS layer
receives view models / call results, not raw `CapId`s. Any path that lets
main-thread JS originate a SQE without going through the kernel Worker's
validated `postMessage` surface is the same class of bug the remote-session-ui
bridge calls out — a loopback or in-tab listener inheriting operator
authority because it skipped the typed boundary.

The same pattern applies to anything else the browser provides natively.
Candidate future interfaces (no schema yet, mentioned so the port is
considered when they are designed):

- `Clipboard` over `navigator.clipboard`
- `LocalStorage` / `KvStore` over IndexedDB (natural `Store` backend for
  the storage proposal in the browser)
- `Display` / `Canvas` over an `OffscreenCanvas` posted back to the main
  thread
- `RandomSource` over `crypto.getRandomValues` — trivial but needs a cap
  rather than a syscall

Other drafted network interfaces — `TcpSocket`, `TcpListener`,
`UdpSocket`, `NetworkManager` from
[networking-proposal.md](networking-proposal.md) — do not have a clean
browser mapping. The browser exposes no raw-socket primitives, so these
caps cannot be served in the browser port at all. Applications that need
networking in the browser must go through `Fetch`/`HttpEndpoint`, and the
POSIX compatibility adapter's socket path must detect the absence of
`NetworkManager` and route `connect("http://...")` through `Fetch` instead
(or fail closed for other schemes). `CloudMetadata` from
[cloud-metadata-proposal.md](cloud-metadata-proposal.md) is simply not
granted in the browser; there is no cloud instance to describe.

Each host-backed cap is opt-in per-process via the manifest; each has a
native counterpart that the schema is already the contract for. This is
a substantial point in favor of the port: host-provided services slot
into the existing capability model without widening it.

### CapSet bootstrap

The read-only CapSet page at `CAPSET_VADDR` is replaced by a structured-clone
payload in the initial `postMessage`. `capos-rt::capset::find` still parses
the same `CapSetHeader`/`CapSetEntry` layout, just out of a `Uint8Array`
placed at a known offset in the process's linear memory by the boot shim.

---

## Binary Portability

Source-portable, not binary-portable. An ELF built for `x86_64-unknown-capos`
does not run; the same source rebuilt for `wasm32-unknown-unknown` (with the
`atomics` target feature) does, provided it stays inside the supported API
surface.

### Rust binaries on capos-rt

Port cleanly:

- Any binary that uses only `capos-rt`'s public API — typed cap clients
  (`ConsoleClient`, future `FileClient`, etc.), ring submission/completion,
  `CapSet::find`, `exit`, `cap_enter`, `alloc::*`.
- Pure computation, `core`/`alloc` containers, serde/capnp message building.

Do **not** port:

1. Anything that uses `core::arch::x86_64`, inline `asm!`, or `global_asm!`.
2. Binaries with a custom `_start` or a linker script baking in `0x200000`.
   capos-rt owns the entry shape; the wasm entry is set by the host
   (`WebAssembly.instantiate` + an exported init), so the prologue differs.
3. `#[thread_local]` relying on FS base until the wasm TLS story is
   decided (per-Worker globals, or the wasm threads proposal's TLS).
4. Code that assumes a fixed-size static heap region and reaches it with
   raw pointers. The wasm arch uses `memory.grow`; `alloc::*` hides this,
   `unsafe { &mut HEAP[..] }` does not.
5. Anything that still calls the transitional `write` syscall shim —
   the browser build deliberately omits it.

Binaries mixing target features across the workspace produce silently-
broken atomics. A single `rustflags` set for the browser build is required.

### POSIX binaries (when the adapter lands)

The POSIX compatibility adapter described in
[userspace-binaries-proposal.md](userspace-binaries-proposal.md) Part 4
sits on top of capos-rt. If capos-rt builds for wasm, the adapter builds for
wasm, and well-behaved POSIX code rebuilt for a wasm-targeted
`libcapos` (clang `--target=wasm32-unknown-unknown` + our libc) ports too.

Ports cleanly:

- Pure computation, string/number handling, data-structure libraries.
- `stdio` over Console / future File caps.
- `malloc`/`free`, C++ `new`/`delete`, static constructors.
- `select`/`poll`/`epoll` implemented over the ring (ring CQEs are exactly
  the event source these APIs want).
- `posix_spawn` over `ProcessSpawner` — spawning a new process becomes
  "instantiate a new Worker," which is the native shape of the browser
  anyway.
- Networking via `Fetch`/`HttpEndpoint` (drafted in
  [service-architecture-proposal.md](service-architecture-proposal.md))
  if the manifest grants the cap. The browser port serves these against
  the host's `fetch`/WebSocket — not ambient authority, because only
  processes granted the cap can invoke it. Raw `AF_INET`/`AF_INET6`
  sockets via the `TcpSocket`/`NetworkManager` interfaces in
  [networking-proposal.md](networking-proposal.md) are not available in
  the browser (no raw-socket primitive); POSIX networking code wants
  URLs in practice, and a libc shim can map
  `getaddrinfo`+`connect`+`write` over `Fetch`/`HttpEndpoint` for the
  HTTP(S) case, failing closed otherwise.

Does **not** port without new work, possibly ever:

1. **`fork`.** Cannot clone a Worker's linear memory into a new Worker and
   resume at the `fork` call site — there is no COW, no MMU, no way to
   duplicate an opaque WASM module's mid-execution state. This is the
   same reason Emscripten/WASI don't support `fork`. POSIX programs that
   fork-then-exec can be rewritten to `posix_spawn`; programs that
   fork-for-concurrency (Apache prefork, some Redis paths) cannot.
2. **Signals.** No preemption inside a Worker means no asynchronous signal
   delivery. `SIGALRM`, `SIGINT`, `SIGSEGV` all need cooperative polling
   at best; `kill(pid, SIGKILL)` maps to `worker.terminate()` and nothing
   finer. `setjmp`/`longjmp` works within a function call tree;
   `siglongjmp` out of a signal handler does not exist.
3. **`mmap` of files with `MAP_SHARED`.** WASM linear memory is not
   file-backed and cannot be. `MAP_PRIVATE | MAP_ANONYMOUS` works
   trivially (it's just `memory.grow` + a free list). File-backed
   mappings require a userspace emulation that reads on fault and writes
   back on unmap — workable for small files, a lie for the memory-
   mapped-database case.
4. **Threads without the wasm threads proposal.** pthreads over Workers
   sharing a memory is the only implementation strategy, and it requires
   the wasm `atomics`/`bulk-memory`/`shared-memory` feature set plus
   careful runtime support. Single-threaded POSIX code works now;
   multithreaded POSIX code needs the in-process-threading track from
   the native roadmap *and* its wasm counterpart.
5. **Address-arithmetic tricks.** Wasm validates loads/stores against the
   linear-memory bounds. Code that relies on unmapped trap pages (guard
   pages, end-of-allocation sentinels) or on specific virtual addresses
   fails.
6. **`dlopen`.** A wasm module is immutable after instantiation. Dynamic
   loading requires loading a *second* module and linking via exported
   tables — possible with the component model, nowhere near drop-in
   `dlopen`. Static linking is the pragmatic answer.

Rough guide: if a POSIX program compiles cleanly under WASI and uses only
WASI-supported syscalls, it will almost certainly port to capOS-on-wasm
with the adapter, because the constraints overlap. If it needs features WASI
doesn't support (fork, signals, shared mmap), the capOS browser port will
not magically fix that — the limitations come from the substrate, not from
the POSIX adapter's completeness.

---

## Build Path

Three new cargo targets, no workspace restructuring required:

1. **`capos-lib` on `wasm32-unknown-unknown`.** Already `no_std + alloc`, no
   arch-specific code. Should build as-is; verify under
   `cargo check --target wasm32-unknown-unknown -p capos-lib`.
2. **`capos-config` on `wasm32-unknown-unknown`.** Same — pure logic, the
   ring structs and volatile helpers are portable.
3. **`capos-rt` on `wasm32-unknown-unknown` with `atomics` feature.** The
   standalone userspace runtime currently hard-codes x86_64 syscall
   instructions. Introduce an `arch` module split:
   - `arch/x86_64.rs` (existing `syscall.rs` contents)
   - `arch/wasm.rs` (new — `Atomics.wait` via `core::arch::wasm32::memory_atomic_wait32`, `exit` via host import)

   Gate at the `syscall` boundary, not deeper; the ring client above it is
   arch-agnostic.
4. **Demos on `wasm32-unknown-unknown`.** Same arch split applied via
   `capos-rt`. No per-demo changes expected if the split is clean.

The **kernel** does not build for wasm. Instead, a new crate
`capos-kernel-wasm/` (peer to `kernel/`) reuses `capos-lib`'s `CapTable` and
`capos-config`'s ring structs and implements the dispatch loop against JS
host imports for Worker management. It is, deliberately, not the same kernel
binary. Trying to build `kernel/` for wasm would pull in IDT/GDT/paging code
that has no meaning in the browser.

---

## Phased Plan

### Phase A: Port the pure crates

- Verify `capos-lib`, `capos-config` build clean on
  `wasm32-unknown-unknown`. CI job: `cargo check --target
  wasm32-unknown-unknown -p capos-lib -p capos-config`.
- Add a host-side `ring-tests-js` harness that exercises the same invariants
  as `tests/ring_loom.rs` but with a real JS producer and a Rust/wasm
  consumer, both sharing a `SharedArrayBuffer`. Proves the volatile access
  helpers are portable before anything else depends on them.

### Phase B: capos-rt arch split

- Introduce `capos-rt/src/arch/{x86_64,wasm}.rs` behind a `#[cfg(target_arch)]`.
- Rewire `syscall`/`ring`/`client` to call through the arch module.
- Add `make capos-rt-wasm-check` target. Existing `make capos-rt-check`
  stays for x86_64.

### Phase C: Kernel Worker + init

- `capos-kernel-wasm/` with a Console capability that renders to xterm.js
  via `postMessage` back to the main thread.
- Kernel Worker spawns init. Init prints "hello" through Console and
  exits.

### Phase D: ProcessSpawner + Endpoint

- `ProcessSpawner` served by the kernel Worker, granted to init.
- Init parses its `BootPackage` and spawns the `endpoint-roundtrip` and
  `ipc-server`/`ipc-client` demos via `ProcessSpawner.spawn`. These
  stress capability transfer across Workers: does a cap handed from A to
  B via the ring land correctly in B's ring, and does B's subsequent
  invocation route back to the right holder?
- This phase turns the port into a validation surface for the
  capability-transfer and badge-propagation invariants in
  `docs/authority-accounting-transfer-design.md`, and a second implementation
  of the Stage 6 spawn primitive.

### Phase E: Integration with demos page

- Hosted page at a project URL; xterm.js terminal; selector for which
  demo manifest to boot.
- Serve `.wasm` artifacts as static assets.

---

## Security Boundary Analysis

The browser port changes what is *trusted* and what is *verified*. Summary:

| Boundary | Native (x86_64) | Browser (WASM-Workers) |
|---|---|---|
| Process ↔ process | Page tables + rings | Worker agents + SAB (structural) |
| Process ↔ kernel | Syscall MSRs + SMEP/SMAP | `postMessage` + validated host imports |
| Code integrity | W^X + NX | WASM validator + immutable `Module` |
| Capability forgery | Kernel-owned `CapTable` | Kernel-Worker-owned `CapTable` |
| Capability transfer | Ring SQE validated in kernel | Ring SQE validated in kernel Worker — **same code path** |

The capability-forgery story is the same in both: an unforgeable 64-bit
`CapId` is assigned by the kernel and can only be resolved through the
kernel's `CapTable`. A process Worker cannot synthesize a valid `CapId`
because it never sees the `CapTable`; it only sees SQEs it submits and CQEs
it receives. This property is what makes the port worth doing — the
capability model is preserved exactly.

What weakens: no SMAP/SMEP equivalent, but also no corresponding attack
surface (the "kernel" Worker has no pointer into process memory; it can only
copy bytes out of the shared ring). No DMA problem. No side-channel parity
with `docs/dma-isolation-design.md` — Spectre/meltdown in the browser is the
browser's problem, mitigated by site isolation and COOP/COEP.

Required headers: `Cross-Origin-Opener-Policy: same-origin` and
`Cross-Origin-Embedder-Policy: require-corp` — `SharedArrayBuffer` is gated
on these. A hosted demo page must set them.

---

## What This Port Buys Us

1. **Shareable demos.** A URL that boots capOS in ~1s, with no QEMU, no
   local install. Valuable for documentation and recruiting.
2. **A second substrate for the capability model.** If the cap-transfer
   protocol has a bug, reproducing it under Workers (single-threaded,
   deterministic scheduling) is much easier than under SMP x86_64. A second
   implementation of the dispatch surface is a correctness asset.
3. **Forcing function for `write` syscall removal.** The browser port
   cannot support the transitional `write` path without importing host
   I/O as a back door, which is exactly the ambient authority we want to
   avoid. Shipping a browser demo at all requires finishing the migration
   to the Console capability over the ring.
4. **Teaching surface.** Workers give a much clearer visual of "one
   process, one memory, one cap table" than a bare-metal kernel ever will.
   The isolation story renders in the DevTools panel.

## What It Does *Not* Buy Us

1. **Not a validation surface for the x86_64 kernel.** Page tables, IDT,
   context switch, SMP — none of that runs. Bugs in those subsystems will
   not appear in the browser build.
2. **Not a performance story.** WASM + Workers + SAB is slower than native
   QEMU-KVM for the parts it does overlap on, and does not exercise the
   hardware features capOS eventually cares about (IOMMU, NVMe, virtio-net).
3. **Not a path to "capOS on Cloudflare Workers" or similar.** Cloudflare's
   runtime is a single isolate per request, no SAB, no threads — a different
   environment that would need its own proposal.

---

## Open Questions

1. **Do we ship one `capos-kernel-wasm` crate, or does the kernel Worker
   run plain JS that imports a thin `capos-dispatch` wasm?** JS-hosted
   kernel is simpler (no second wasm toolchain for the kernel side) but
   duplicates cap-dispatch logic. Preferred: Rust/wasm kernel Worker reusing
   `capos-lib` — dispatch code stays single-sourced.
2. **How do we surface kernel panics in the browser?** Native capOS halts
   the CPU; the browser equivalent is posting an error to the main thread
   and tearing down all Workers. Should match the `panic = "abort"`
   contract — no recovery attempted.
3. **Do we implement `VirtualMemory` as a no-op or as a real allocator?**
   No-op is faster to ship; a real allocator over `memory.grow` exercises
   more of the capability surface. Lean toward real, gated behind a
   `browser-shim` flag so the demo doesn't silently diverge from the
   native semantics.
4. **Manifest format: keep capnp, or add JSON for hand-authored demo
   configs?** Keep capnp. The manifest is already the contract; adding a
   parallel format is exactly the drift the project has been careful to
   avoid.

---

## Relationship to Other Proposals

- **[`userspace-binaries-proposal.md`](userspace-binaries-proposal.md)** —
  the wasm32 runtime story lives there eventually. This proposal is
  narrower: just enough runtime to boot the existing demo set in a browser.
  If the userspace proposal lands a richer runtime first, this one adopts
  it.
- **[`wasi-host-adapter-proposal.md`](wasi-host-adapter-proposal.md)** —
  the WASI host adapter (capos-wasm) already exercises the inverse
  direction: hosting third-party `wasm32-wasip1`/`wasm32-wasi` modules
  inside a capOS userspace process whose Preview 1 imports are backed by
  typed capabilities (Console, Timer, EntropySource, bounded argv/env
  text grants). The browser port consumes that experience in three ways:
  it reuses the per-instance cap-grant pattern (no ambient host imports,
  every authority surfaced through the CapSet); it inherits the lesson
  that host-backed imports must refuse closed when the cap is not granted
  (W.4's `ERRNO_NOSYS = 52` refusal sentinel); and it specifically rejects
  pulling the kernel itself into a hosted wasm-runtime substrate — the
  browser kernel Worker is a Rust/wasm port of `capos-lib`'s `CapTable`
  and `capos-config`'s ring dispatch, not a wasmi-style interpreter over
  another guest. If a future browser-port phase wants to host *third-party*
  wasm modules inside a capOS-on-wasm userspace process, that work belongs
  to the WASI adapter direction, not here.
- **[`browser-capability-proposal.md`](browser-capability-proposal.md)** —
  the opposite direction: capOS exposing browsers as capability-scoped
  services (`BrowserSession`, `BrowserProfile`, `BrowserContext`) to
  users, shells, and agents. The two proposals share design principles
  (browser state is authority; the interface is the permission; agents
  receive tools, not admin ports) but do not overlap in implementation —
  one is a userspace browser service driven over CDP/WebDriver BiDi from a
  capOS host; this one is capOS rebuilt for wasm and run inside Workers
  with no browser engine of its own.
- **[`remote-session-ui-security-proposal.md`](remote-session-ui-security-proposal.md)** —
  the trusted local web bridge that owns the TCP connection and upstream
  capOS session while browser JavaScript receives only DTOs. The browser
  port faces the same boundary inside one tab: the kernel Worker holds the
  `CapTable` and serves typed CQEs back to process Workers, and any UI
  surface on the main thread is untrusted glue, not a cap holder. The
  CSRF/CSP/cookie/cookie-isolation posture documented there is the
  reference the browser port adopts before serving any host-backed
  capability (Fetch, Clipboard, storage) to a process Worker; relaxing it
  for "just a demo" is exactly the ambient-authority drift the proposal
  warns against.
- **[`smp-proposal.md`](smp-proposal.md)** — structurally irrelevant to
  the browser port (each Worker is its own agent). The browser port
  *does* inform SMP testing, because the cap-transfer protocol under
  Workers is a cleaner model of "messages cross agents asynchronously"
  than single-CPU preempted kernels.
- **[`service-architecture-proposal.md`](service-architecture-proposal.md)** —
  process spawn in the browser becomes Worker instantiation. The
  lifecycle primitives (supervise, restart, retarget) map naturally. Live
  upgrade ([`live-upgrade-proposal.md`](live-upgrade-proposal.md)) is even
  more natural under Workers than under in-kernel retargeting — swap the
  `WebAssembly.Module` behind a Worker while the ring stays live.
- **[`security-and-verification-proposal.md`](security-and-verification-proposal.md)** —
  the browser port adds a CI job (wasm builds + JS-side ring tests) but
  does not change the verification story for the native kernel.
