WASMruntime

Why WASM for Sandboxed Extensibility

User-defined business rules at native speed, with deterministic isolation

v1.0·13 min read·Kenneth Pernyér
wasmwasmtimesandboxextensibilitydeterminismcapability-securityconverge

The Problem

Converge has a hard requirement that most frameworks avoid: end-user extensibility inside a safety-critical loop.

The framework author can define the convergence engine, merge rules, and invariants. But the customer knows their business:

  • "No strategy may contain brand-unsafe terms"
  • "Invoices above €50k require dual approval"
  • "Every rollout proposal must reference a compliance framework"

The usual options all break down:

  • Config files are safe but not expressive enough
  • Plugin SDKs are expressive but expand the trust boundary
  • Embedded scripting is flexible but hard to constrain deterministically

We needed an extension model that preserves Converge's core axioms:

  • Agents Suggest, Engine Decides (guests can read, not mutate)
  • Safety by Construction (budgeted, capability-gated execution)
  • Transparent Determinism (replayable outcomes with full traces)

The requirement was not "run user code somehow."

It was: run tenant-defined logic in the convergence path without surrendering control of correctness, authority, or auditability.

That pushed us to WebAssembly (WASM).

Current Options

OptionProsCons
Configuration Rules (YAML / JSON)Declarative configuration with schema validation and limited operators.
  • Easy to validate at input boundaries
  • Operationally safe and familiar
  • Simple to diff and review
  • Low runtime complexity
  • Poor expressiveness for non-trivial predicates
  • Complex logic becomes unreadable quickly
  • Inevitable pressure toward ad hoc mini-languages
  • Hard to model reusable business semantics cleanly
Embedded Scripting (Lua / JavaScript Isolates)Run user-authored scripts inside an embedded VM or isolate.
  • Flexible and easy to author
  • Large talent pool (especially JavaScript)
  • Fast iteration for prototyping rules
  • Mature language tooling
  • Harder to guarantee deterministic behavior
  • Ambient runtime features must be aggressively stripped
  • Resource limits often rely on timers/watchdogs
  • Auditable capability boundaries are harder to make explicit
Containerized PluginsRun extensions as separate processes/containers with RPC boundaries.
  • Strong isolation at OS/process level
  • Language-agnostic plugin implementation
  • Operational familiarity (Docker/Kubernetes)
  • Crash containment is straightforward
  • High latency for per-merge invariant checks
  • Heavy operational overhead for small predicates
  • Coarse-grained permissions and networking concerns
  • Difficult to make replay truly deterministic
WASM (Wasmtime)Compiled sandboxed modules with explicit imports, fuel metering, and near-native speed.
  • Deterministic bytecode execution with explicit host interface
  • Fuel metering enables guaranteed termination
  • Zero ambient authority by default
  • Fast enough for convergence-loop invariants
  • ABI and host contract design is non-trivial
  • Debugging guest modules is less ergonomic than native code
  • WASI/component standards are still evolving
  • Serialization boundaries add overhead if designed poorly

Future Outlook

WASM is moving from "browser technology" to the default extensibility sandbox for safety-critical systems.

The shift is structural:

  1. AI systems need safe user extensibility. Prompts are not enough for enforceable policy.
  2. Governance requires deterministic control surfaces. "Probably safe" is not a control model.
  3. Capability-based execution beats ambient runtime permissions. Explicit imports are easier to audit than hidden runtime affordances.

We're seeing the same pattern across the industry:

  • Commerce platforms use WASM for user-defined business logic in checkout paths
  • Agent platforms use WASM to sandbox tool execution
  • Edge runtimes use WASM where cold-start and isolation both matter

For Converge specifically, the long-term opportunity is bigger than "plugins":

  • Compile tenant '.truth' / Gherkin-like invariants into WASM
  • Run them with fuel quotas inside the convergence loop
  • Record module hash + input hash + execution trace for deterministic replay

WASM does not replace native Rust.

Native Rust remains the right choice for the engine, networking stack, storage adapters, and core invariants. WASM is the extension boundary: the place where we want power for the tenant and control for the host at the same time.

As WASI Preview 2 and the Component Model mature, interoperability improves. The contract gets cleaner. The deployment story gets better. The governance story stays intact.

Our Decision

Why we chose this

  • Deterministic execution modelGiven the same module bytes and the same input bytes, guest execution is replayable. This aligns with Converge's traceability and deterministic replay requirements.
  • Fuel metering for bounded executionWasmtime fuel budgets let us stop runaway guest code deterministically. No watchdog threads, no best-effort timeouts, no infinite loops holding the convergence engine hostage.
  • Zero ambient authorityWASM modules can only do what the host explicitly imports. If a module is not given a capability, it cannot access it. This is explicit authority, not convention.
  • Near-native performanceStructural invariants may run on every merge cycle. WASM via Wasmtime is fast enough to live on the hot path without turning policy checks into a latency tax.
  • Content-addressed reproducibilityHashing module bytes gives stable identity. Same module hash + same input = reproducible execution, cacheability, and precise audit references.

×Trade-offs we accept

  • Host ABI design is a product decisionThe hard part is not "running WASM." It is designing a minimal, stable, auditable contract between host and guest. A sloppy ABI recreates the same risk you were trying to eliminate.
  • Guest debugging ergonomicsNative Rust debugging is still easier than debugging tenant-provided WASM modules, especially when failures happen at the serialization or ABI boundary.
  • Standards are improving, not finishedWASI and the Component Model are moving fast. This is good for the future but requires version discipline and conservative adoption in production systems.
  • Boundary overhead is realEvery host↔guest call and serialization step costs time. The fix is careful contract design: coarse enough calls, explicit projections, and no chatty interfaces.

Motivation

Converge's core principle is simple: the engine must remain authoritative even when the system becomes extensible.

That rules out "just let users run code" approaches.

We want customers to express business invariants in a human-readable format (Gherkin / '.truth' files), but we do not want those invariants implemented as:

  • prompt instructions mixed with untrusted inputs
  • unmetered scripts with hidden runtime capabilities
  • external services that break determinism and observability

WASM gives us the right shape of boundary.

The guest sees a projection, not the engine.

The module receives a read-only 'GuestContext' projection (facts + version + cycle metadata). It does not receive host internals, proposal mutation rights, or opaque handles to engine state.

The host controls capability linkage.

Need logging? Expose 'host_log'. Need context reads? Expose 'host_read_context'. No capability granted? No access. There is nothing to "forget to secure" because there is no ambient access to begin with.

The engine controls termination.

Fuel budgets map cleanly onto our existing budget model ('CycleBudget', 'ExecutionBudget', etc.). If a tenant module exceeds its fuel allotment, the engine traps and records the outcome. Deterministic stop, deterministic trace.

Every invocation is observable.

Module hash, fuel consumed, host calls, duration, outcome. We do not treat extension execution as a black box. We treat it as a first-class governance event.

This is why WASM matters for Converge: not because it is fashionable, but because it lets us add user-defined logic without weakening the engine's authority model.

Recommendation

Use WASM when all of the following are true:

  • You need tenant-defined logic or third-party extensibility
  • The code runs in a governance-critical or safety-critical path
  • You need deterministic replay / auditability
  • You need explicit capability boundaries

Keep native Rust for:

  • Core engine logic
  • Network-facing services
  • Storage adapters
  • Performance-critical subsystems with full host trust

Design rules for a production WASM extension boundary:

  1. Minimize the ABI - small, explicit host imports
  2. Project data, don't expose internals - guests get a read-only view tailored to the task
  3. Budget everything - fuel, memory, payload size, host calls
  4. Hash everything - content-addressed identity for replay and audit
  5. Trace everything - invocation metadata is part of the product, not debug scaffolding

For Converge, WASM is not the runtime. Rust is the runtime. WASM is the constitutional sandbox where end-user policy becomes executable without becoming dangerous.

Examples

converge-core/src/invariant.rsrust
pub trait Invariant: Send + Sync {
    fn name(&self) -> &str;
    fn class(&self) -> InvariantClass; // Structural, Semantic, Acceptance
    fn check(&self, ctx: &Context) -> InvariantResult;
}

Converge invariants are intentionally pure: read context, return a verdict. That purity makes a WASM guest contract practical and auditable.

converge-runtime/src/wasm_engine.rsrust
let initial_fuel = 1_000_000;
store.set_fuel(initial_fuel)?;

let result = check_invariant.call(&mut store, (ctx_ptr, ctx_len))?;

let remaining = store.get_fuel()?;
let consumed = initial_fuel - remaining;

trace.record_fuel(consumed);
trace.record_outcome(&result);

Fuel metering gives deterministic bounded execution. Exhaust fuel and the guest traps immediately; the engine records the exact outcome.

module-manifest.jsonjson
{
  "name": "brand-safety-invariant",
  "version": "1.2.0",
  "kind": "Invariant",
  "invariant_class": "Structural",
  "capabilities": ["ReadContext", "Log"],
  "requires_human_approval": false,
  "jtbd": {
    "truth_id": "growth_strategy.truth",
    "actor": "Growth Lead",
    "job_functional": "Prevent unsafe strategy outputs"
  }
}

The guest self-declares identity and required capabilities. The host validates the manifest before activation and records the module hash for replay.

Related Articles

Stockholm, Sweden

Version 1.0

Kenneth Pernyér signature