VibeAI Virtual Hosts

A Wasm runtime platform where the sandbox never touches the host

wasm-box runs WebAssembly modules the way Docker runs containers — daemon, registry, CLI, dashboard — but isolation comes from the Wasm memory model and a capability firewall instead of kernel namespaces. Every number below is from a real run on this machine.

~120 ms
cold start incl. JIT
µs warm
cached instantiation
0 grants
default capability set
100%
host calls audited or denied

The capability pipeline

The load-bearing idea, formalized from the original sketch: a module sees only its Context; every request crosses the Firewall; only the Host Machine Interface touches anything real. Reading right to left is the guest's view of the world.

WASM module

sandboxed guest — deny by default
  • linear memory only, hard-capped
  • fuel-metered CPU, per-message refill
  • imports: WASI + vibeai ABI
  • no sockets, no host fs, no env
host_call

Context

this instance's declared grants
  • data: fs preopens, db, cache buckets
  • network: policy + allowlist
  • argv, env, stdin — explicit only
  • declared in vhost.toml / flags
check

Firewall

verdict per call, no state of its own
  • data layer registration check
  • network layer registration check
  • allow -> forward to HMI
  • deny -> reason back to guest
  • JSONL audit log of every attempt
execute

Host Machine Interface

the only code that touches resources
  • data: per-module SQLite, cache, folders
  • network: HTTP client, size-capped
  • planned: APIs, models, DNS, services
io

Host machine

filesystem, sqlite files, network
  • never sees the guest
  • daemon socket chmod 600
verified allowed in tests verified denied in tests policy checkpoint

System overview

Three tiers. Clients speak JSON to the daemon; the daemon owns policy and state; the engine executes. The daemon runs as a standalone service — no frontend needs to exist for it to work.

clients
wasm-box CLINDJSON over unix socket
web dashboardHTTP :7070, live refresh
SwiftUI appplanned — same /api/* bridge
daemon
DaemonStaterun history, instances, supervision, router
module registrysha256 blobs + mutable tags
firewall + HMIverdicts, audit, sqlite / cache / http
engine
WasmRunnerwasmtime JIT + WASI + vibeai linker, module cache
Store: instance Aown memory, fuel, WASI ctx, inbox
Store: instance Bnothing shared with A

Sandbox guarantees

Each threat maps to a specific enforcement mechanism, and each one has been exercised with a hostile test module.

host filesystem access
WASI preopens only; /etc returns ENOENT inside the guest. Read-only unless :rw.
memory exhaustion
StoreLimits cap — a 1 GiB allocation bomb under a 16 MiB cap OOMs inside the guest; the daemon never grows.
infinite loops / CPU DoS
Fuel metering — deterministic trap at the budget. Per-message refill means one poisoned request kills only itself.
blocked host calls
Wall-clock timeout; fuel-yield every 100k units keeps the scheduler live so it actually fires.
unauthorized data / network
Firewall verdicts from declared grants; undeclared bucket, un-granted db, non-allowlisted host — all denied with reasons, all audited.
output flooding
Ring-buffered capture — writes never fail, oldest bytes evict, daemon memory stays flat.

Message flows

Three ways data moves at runtime. The daemon routes every hop, so inter-module traffic stays observable and policeable in one place.

request / response — wasm-box send
CLI->daemonsend(id, payload)
daemon->instance Ainbox envelope: payload + reply channel
instance A->instance Anext_msg_len / read_msg (fuel refilled here)
instance A->daemonsend_reply(bytes)
daemon->CLIreply (30s timeout; instance keeps running)
guest to guest — vibeai.post, fire and forget
instance A->daemonpost("worker", payload)
daemon->instance Binbox envelope, no reply channel
verified: instance 1 posted to echo2; the message appeared in echo2's live logs
capability call — vibeai.host_call
instance A->firewall{"call":"db_query","sql":...}
firewall->HMIverdict: allow -> execute (sqlite / cache / http)
HMI->instance A{"ok":true,"data":{"rows":[...]}}
deny path returns {"ok":false,"error":"denied by firewall: ..."} and lands in firewall.log

Service lifecycle & supervision

A service instance is owned by a supervisor task for its whole life. Restart policy decides what happens when it stops; explicit stop always wins.

running exited failed backoff stopped
transitiontriggerwhat happens
running -> exitedguest returns / proc_exitoutcome + log tail recorded; on-failure with exit 0 rests, always restarts
running -> failedtrap, fuel exhaustederror recorded; on-failure and always go to backoff
failed -> backoff -> runningrestart policy1s doubling to 30s; a healthy 60s resets the clock; restart count on the record
running -> stoppedwasm-box stopstop flag set, inbox closes, guest's next_msg_len returns -1, loop exits clean — never restarted, desired state cleared
daemon restartcrash or upgraderuns.json survives; anything running is marked failed with a reason; always-services relaunch from services.json

vhost.toml — the virtual host definition

One file declares a named set of modules with their capabilities and budgets — the platform's Dockerfile and compose in one. wasm-box up makes it real.

[host]
name = "my-app"

[modules.api]
module  = "api"          # registry tag or ./path.wasm
service = true           # long-running, message inbox
restart = "always"       # survives crashes AND daemon restarts

[modules.api.data]
db    = true             # private sqlite, module-scoped
cache = ["session"]      # named buckets, per-module

[[modules.api.dirs]]
host = "./data"
guest = "/data"
writable = true

[modules.api.network]
policy = "allowlist"
allow  = ["api.example.com"]

[modules.api.limits]
memory_mb        = 64
fuel_per_message = 5000000
module — resolves through the content-addressed registry; same bytes under two tags share one blob.
service + restart — a supervisor owns the instance; "always" also relaunches it when the daemon boots.
data / network — this block is the Context. Anything not listed here does not exist from inside the sandbox.
limits — memory is a hard wall; fuel_per_message means a poisoned request burns its own budget, not the instance's future.

On-disk layout

Everything the platform knows lives under one data directory (default ~/.wasm-box) — trivially backed up, inspected, or wiped.

~/.wasm-box/
|- blobs/<sha256>.wasm      immutable module bytes, deduplicated by digest
|- index.json               tag -> digest, size, created (the "images" list)
|- runs.json                run history — survives daemon restarts
|- services.json            desired state: always-restart services to relaunch
|- firewall.log             JSONL audit: every network attempt, every denial
`- moduledb/<module>.sqlite3 private per-module databases, isolation by construction

CLI surface

# modules (the "images" workflow)
wasm-box load app.wasm --name app      wasm-box modules      wasm-box rm app

# batch runs
echo '{"q":"hi"}' | wasm-box run app --stdin --db --allow-net api.example.com

# services
wasm-box start app --restart always --fuel-per-msg 5000000 --cache session
wasm-box send 3 "payload"              wasm-box logs 3       wasm-box stop 3

# fleet
wasm-box up            # reads vhost.toml
wasm-box ps            # status, restarts, fuel, duration

Full specs live in the repo: ARCHITECTURE.md, docs/COMMUNICATION.md, docs/DIAGRAMS.md (Mermaid sources for every diagram here).