wasi sandbox · capability broker · wasm guards · fail-closed

Run code you didn't write.
Sleep anyway.

Agents generate code now. Somebody has to run it. riz runs it behind three independent walls — a WASI capability sandbox, an OS process boundary with an always-on safety profile, and a host-side resource broker — so "the model wrote it" stops being a production risk. And it all holds for agents too: MCP tool calls and the built-in A2A agent's tool executions dispatch through the same guards and sandbox — no side door.

wall one · the sandbox

Deny by default.
Grant by config.

runtime = "wasm" runs a wasm32-wasip1 module under wasmtime with no filesystem and no network unless you grant them — and it's real compute, not a toy: the shipped example validates and prices an order entirely inside the sandbox, with sub-millisecond cold starts.

Beneath the WASI wall, every handler process — every runtime, not just WASM — gets the always-on safety profile: RLIMIT_CORE=0, file-descriptor and process caps, dies-with-host, no privilege escalation. Two walls before your code even matters.

proof wasm_orders_prices_a_valid_order · child_with_allowlist_can_read_allowed_path_only · child_inherits_always_on_caps
the wallsper request
requestWASI capability check     deny-by-default
  fs: no grant → denied
  net: no grant → denied
  ↓
your .wasm runs           sub-ms spawnhost safety profile       always-on
  rlimits · PDEATHSIG · no-new-privs
  ↓
response
wall two · the broker

Sandboxed code that still
reaches your database.

The classic sandbox trade-off is "safe but useless." The riz broker deletes it: a [function.x.capabilities] grant lets the guest run parameterized Postgres queries through the host — Neon, Supabase, RDS, or any PG is one config row.

  • The guest never opens a socket; credentials never cross the WASI boundary — it names a grant, it gets rows
  • The host owns the blast radius: deny-by-default, per-call deadlines, concurrency caps, rate limits, payload caps, audit
  • A stalled or hostile query is cut at the grant's deadline — the host is structurally unaffected
  • Read-only grants run inside genuine READ ONLY transactions — Postgres refuses writes, not string inspection
proof wasm_guest_brokers_pg_through_capability_grants — the full chain, riz → wasm child → guest → broker → PG wire, in CI
riz.tomlvalidated by the real config code in CI
# credentials live HERE, host-side — never in guest memory
[resources.pg.main]
dsn_env = "RIZ_PG_MAIN_DSN"   # Neon / Supabase / RDS / any PG

[function.orders]
runtime = "wasm"
handler = "./orders.wasm"
timeout_ms = 5000

# the grant — the guest can do THIS and nothing else
[function.orders.capabilities.db]
type = "pg"
resource = "pg.main"
mode = "read-only"
call_timeout_ms = 1500
rate_per_sec = 50
wall three · the guards

One .wasm policy.
Every runtime obeys it.

guard_in screens every event before the handler: allow it, scrub it, or deny with your status — the handler never runs on deny. guard_out sweeps every response before bytes leave: redact PII, replace the envelope, or block it.

The property nobody else has: the same guard module wraps Bun, Node.js, Python, Rust, Go, and WASM handlers identically — write your policy once, in any language that compiles to WASM, and every runtime obeys it. And it fails closed: a policy that can't run never silently allows traffic.

proof one_wasm_guard_protects_every_runtime — allow, deny, mutate, SSN redaction, and fail-closed, across runtimes, in CI
the verdict contractyour guard writes one line
# riz.toml
guard_in  = "./guards/validate.wasm"
guard_out = "./guards/redact.wasm"

# verdicts
{"action":"allow"}
{"action":"allow", "event":{ …scrubbed… }}
{"action":"deny", "statusCode":451, "body":"…"}

# guard_out, seen in CI:
"ssn":"123-45-6789""ssn":"***-**-****"