Mawu · Architecture

Ixchel: The Modding Runtime & WASM Sandbox

A focused page within the Mawu Architecture documentation. The full map and every sibling page live in the Architecture hub.

7sections12 minread1diagram

On this page

Ixchel is the part of Mawu that makes "creator republic" more than a slogan: it is the runtime that lets a community author executable content — entity scripts, behaviours, economy rules, whole world genomes — and run it on the authoritative realm server without that code being able to harm the realm, the players, or the platform. This is the differentiator V7 stakes its identity on. Roblox lets you run code but owns every server; FiveM lets you own the server but a malicious resource has near-total power over its host and the highest-risk path to players. V7's answer is to let creators ship real code and run it only as capability-sandboxed WebAssembly behind a hardened client the operator cannot patch. The whole bet rests on one claim being literally, mechanically true: untrusted creator WASM cannot reach a file, a socket, a process, another plugin's memory, the platform economy, or the realm clock unless a tier and a manifest explicitly grant it — and even then it traps at a deterministic fuel budget rather than hanging the world.

Ixchel productizes the Maya forge-* modding framework into the V7 realm runtime, and it is three things wearing one name: a resource model and lifecycle (load, hot-reload, fault-isolate creator code), a WASM sandbox safety model (tiers, capabilities, fuel/epoch budgets, store limits, host hardening), and a dependency-resolution + content-addressed storage layer (a PubGrub solver and a Nix-style content store that pins exactly which bytes a realm runs). This page is the deep companion to the "Ixchel — The Modding Runtime", "The WASM Sandbox", and "Dependency Resolution and Content-Addressed Storage" sections of the orientation hub ../V7_ARCHITECTURE.md.

What ships, honestly#

The sandbox is real, not a contract — and that is the distinction this page exists to be precise about. There are two Ixchel codebases in the tree, and they play different roles:

  • The real runtime ships in apps/v7/moremi-realm-server (a single ~14,900-line Rust crate) on a genuine Wasmtime 45.0.0 with the Component Model, Cranelift, fuel, epoch, and async features enabled (Cargo.toml:17), plus PubGrub 0.3.0 and SHA-256. It carries eleven Ixchel/forge eval functions and 69 tests; the fifteen ixchel_* sandbox tests compile and pass (cargo test … ixchel15 passed), exercising real WAT modules against the real engine — runaway loops that actually trap on fuel exhaustion, oversized memories that are actually refused at instantiation, hostile capability requests actually denied before a component instantiates. This is wired to CI: scripts/v7/verify-adversarial-eval-gates.mjs registers a sandbox-escape gate whose workflow command is cargo test … -p moremi-realm-server ixchel_sandbox_escape_eval and whose evidence needle is literally escaped_fixtures == 0.

  • The libs/maya/forge-* crates are framework scaffolds, deliberately API-first. forge-sandbox defines the tier/manifest/budget contract that upload and scanner code compile against, and says so in its own doc comment — "the full implementation will host Wasmtime component-model modules" (forge-sandbox/src/lib.rs:5); it has no Wasmtime dependency. forge- resolver is a deterministic exact-manifest resolver explicitly preceding the "full V7 resolver [that] will use PubGrub" (forge-resolver/src/lib.rs:6). moremi-realm-server does not import these crates — it reimplements their surface natively against the real engine. Two of the four are nonetheless real working code today: forge-compositor (835 lines) genuinely composes layers with per-field provenance and implements the Maya Loom genome/runtime- expression system, and @maya/forge-assist (TypeScript) is a real AI co-creator that enforces the trust boundary in code before any artifact reaches the sandbox.

  • The honest seam is the live intake-and-serve daemon. The sandbox, the resolver, and the content store ship as verified libraries and eval gates, not as an assembled service that today accepts an uploaded creator module over the network, scans it, and streams it to clients at scale. The hostile-module corpus is hand-built WAT fixtures, not a stream of real uploads; the upload → malware-scan → AOT → CDN pipeline is described in the monolith and partially realised (the AOT step is real; the scanner and CDN are the seam). Treat the byte-level mechanics below as proven and the production wiring as the integration target.

The modding runtime: resources, tiers, lifecycle#

A realm's behaviour is built from resources — the FiveM unit, hardened. A resource is an Ixchel layer that bundles assets, data blobs, and WASM scripts, declares its dependencies and the sandbox tier it needs, and names the events and callbacks it exports and consumes. That is exactly the shape of MoremiResourceManifest (moremi-realm-server/src/lib.rs:741): a resource_id, version, kind (BaseFramework or Content), a sandbox_tier, a hot_reload flag, and vectors of dependencies, assets, data_blobs, wasm_scripts, exported_events, exported_callbacks, consumed_exports, and capability_grants. Assets and data blobs carry a content_hash (:660, :680); a WASM script carries a module_hash and an entry_callback (:695). The reference RP framework is itself a BaseFramework resource that exports inventory/job/economy primitives, and content resources depend on it — the manifest's with_dependency/consuming_callback builders model precisely that.

The six tiers are a total order (MoremiResourceSandboxTier, :583): DataOnly < Scripted < Extended < System < Native < Trusted. The lattice is not decorative — allows_code_execution is tier > DataOnly (:617), and grants_capability is tier >= capability.minimum_sandbox_tier() (:621). Capabilities are typed (MoremiIxchelWitCapability, :542) and each names a WIT interface and the minimum tier that may hold it (:567): RealmEmitEvent needs Extended, EconomyGrantCurrency needs System, and PlatformSecretRead needs Trusted. A realm declares a maximum tier it will host; the tier-gate eval (run_moremi_ixchel_tier_gate_eval, :7893) proves that a System-tier resource loaded into an Extended-max realm is refused with a precise SandboxTierExceeded { required: System, allowed: Extended } error, while a lower-tier guest is never even handed the host import it didn't earn.

The lifecycle manager (MoremiIxchelLifecycleManager, :7599) hot-loads and unloads resources without restarting the realm. hot_reload_resource (:7673) swaps a resource generation while preserving the prior generation's state hash and emitting a cleanup receipt that releases the old callbacks and state handles. The decisive safety property is fault isolation: isolate_faulting_resource (:7702) sets a faulting resource to Disabled, records the fault reason, and — critically — leaves realm_running true and the connected-player count unchanged. The lifecycle smoke test (run_moremi_ixchel_lifecycle_smoke, :7757) asserts that a deliberately faulting resource is disabled while healthy resources stay active, no player is disconnected, and the realm does not crash. A bad mod takes itself down, not the world.

The WASM sandbox safety model#

The sandbox is the security keystone, and it is enforced at four layers that compose into one gate.

Capability-typed host interface (Component Model + WIT)#

A plugin receives only the host imports its tier and manifest grant — no ambient authority. The capability-deny eval (run_moremi_ixchel_capability_deny_eval, :8660) builds a guest that declares RealmEmitEvent, instantiates it as a real Wasmtime component, and confirms it can make exactly one host call. It then runs a hostile corpus — a guest reaching for EconomyGrantCurrency and one reaching for PlatformSecretRead without declaring them — and asserts both are denied before instantiation (denied_hostile_attempts == hostile.len()), never instantiated, never called, with zero host calls recorded. This is the UEFN-Verse "no filesystem, no sockets, no system resources" guarantee made language-agnostic: capability absence is structural, expressed through which WIT imports the linker actually provides.

CPU budgeting: fuel for gameplay, epoch for cosmetics#

Gameplay-critical plugins run under fuel — deterministic, instruction-counted metering — so they trap at the same instruction on every replay; cosmetic/UI work runs under cheaper, non-deterministic epoch interruption. The budget eval (run_moremi_ixchel_budget_eval, :7958) compiles two real runaway-loop WAT modules and runs them. For the deterministic case it sets config.consume_fuel(true) (:8030), store.set_fuel(budget) (:8047), calls the export, and observes a real trap with get_fuel() == 0, classified as FuelExhausted (:8076). For the cosmetic case it sets config.epoch_interruption(true) (:8033), set_epoch_deadline + increment_epoch (:8052), and observes an EpochDeadline trap. Either way the attempt is recorded as trapped, throttled, and flagged — overrun is contained and surfaced, never allowed to hang the realm tick.

Memory and stack limits via StoreLimits#

The Wasmtime Config caps the WASM stack, sizes and zeroes the async stack, and installs memory guard pages (moremi_ixchel_budget_wasmtime_config, :8151); the per-store StoreLimitsBuilder caps linear-memory size, table elements, and instance count and sets trap_on_grow_failure(true) (:8165). The probe proves three things against the real engine: an oversized linear-memory module is refused at instantiation (:8177), an unbounded recursive-stack module traps (:8190), and — the data-leak guard — two instances of the same module are isolated, so the second instance reads 0 for a secret the first wrote (173), giving no_cross_instance_memory_leak (:8146). Async-stack zeroing exists precisely so a freed stack can't leak one plugin's data into the next.

Host hardening and AOT#

Borrowing Luau's posture, the host surface is frozen. The host-tamper eval (run_moremi_ixchel_host_tamper_eval, :8517) runs five attacks — mutate a host global, mutate a host metatable, write another plugin's environment, disable the interrupt callback, and load untrusted bytecode — under a luau_hardened_default policy, and asserts all five are blocked and the shared host-state hash is byte-identical before and after (:8557). Separately, vetted plugins are AOT-compiled at upload: run_moremi_ixchel_aot_audit_eval (:8278) calls engine.precompile_component() to emit .cwasm bytes, confirms them with WasmtimeEngine::detect_precompiled (:8380), and runs them on a production engine built with the compiler disabled (:8384) — the same speed-and-attack- surface reduction the monolith specifies.

The escape gate#

All of this rolls up into one adversarial gate. run_moremi_ixchel_sandbox_escape_eval (:8726) aggregates the capability, budget, and host-tamper evidence into a single fixture corpus — undeclared-economy, undeclared-platform-secret, runaway- fuel-loop, oversized-linear-memory, frozen-host-global-tamper, untrusted-bytecode- load — marking each blocked / detected / escaped. The report passed() (:7559) is true only when the corpus is non-empty, every fixture is blocked and detected, and escaped_fixtures == 0. The pinned test ixchel_sandbox_escape_eval_blocks_all_hostile_modules (:14717) asserts report.passed() over ≥ 6 fixtures. That zero-escape bar is the launch gate the monolith names, and it is green today.

Dependency resolution and content-addressed storage#

A realm is reproducible only if "which exact bytes run" is pinned, and that is two subsystems.

Resolution is PubGrub. run_moremi_forge_resolver_eval (:9934) drives a real pubgrub::resolve over a package registry, with SemVer ranges parsed by the semver crate (VersionReq::parse / Version::parse, :10088), optional dependencies, feature gates (moremi_forge_dependency_active, :10072), and capability alternatives that expand to a cartesian product of providers satisfying an abstract capability id (moremi_forge_expand_capability_alternatives, :10019). The eval proves three behaviours: a satisfiable graph locks the expected versions and resolves render.surface to mesh-surface-pack over two decoy providers; disabling the weather feature drops the optional weather-pack from the lock; and an unsatisfiable graph fails with a precise root cause, not silently — the MoremiForgeFailureReport carries plain_english, root_causes, and a pubgrub_report from PubGrub's DefaultStringReporter. The solution freezes to a MoremiForgeLockFile (:1590) carrying the resolver id, enabled features, locked packages, capability selections, and a lock_hash.

Storage is content-addressed, Nix-model. run_moremi_forge_content_store_eval (:11145) builds a content manifest where every artifact's id is a SHA-256 hash of its content and its full dependency closure. moremi_forge_content_artifact_for (:11249) recurses dependencies, computes a content_hash over the package's blobs (:11342, Sha256), a closure_hash over the sorted dependency artifact hashes (:11359), and combines them into the artifact_hash (:11375) that becomes the store path. This buys, for free, the four properties the monolith claims: deduplication (identical content + closure ⇒ identical path), tamper-evidence (the eval swaps one dependency and the artifact hash changes), atomic install/rollback (MoremiForgeContentStore, :2078, keeps prior generations — a failed activation preserves the previous active state hash via try_install_manifest_with_activation_fault, :2126, and rollback_to, :2134, restores it), and side-by-side versions. The lock's hash is the supply-chain integrity primitive and the AOT .cwasm cache key.

The pinned lock is what crosses to the client. V7/ue/Config/RealmLocks/dedicated-smoke.lock.json is a real realm lock whose artifactClosure lists each artifact with a sha256: contentHash, its dependencyRefs, and a payloadHash; the UE module MawuRealmLockTypes.cpp parses and validates it, and AMawuComposedRealmActor::ApplyLockFile composes the realm geometry from that content-addressed closure. Server resolves and pins; client renders exactly the pinned composition.

Composition and the Loom#

Once resolved, layers compose in fixed priority. forge-compositor (forge-compositor/src/lib.rs:271) applies layers in the band order Engine < BaseGame < ContentPack < ServerRealm < CommunityVariant < PersonalOverride, breaking ties by layer id, and records the source_layer_id for every composed field — so a realm operator can see which layer supplied each value. On top of that sits the Maya Loom: compile_world_genome_layer (:300) turns a shareable world genome (physics constants, biome/dimension/narrative rules) into a deterministic Ixchel layer, and express_world_genome (:424) applies it as a non-destructive layer — fork_created: false — reporting only the changed paths, the §15 "Variants without forking" capability. compose_runtime_expression_view (:480) then layers per-player runtime expressions with visibility scopes, its test proving one player's "eternal winter" does not bleed into another's view and that a realm-wide expression requires explicit owner consent. (Loom's signatures use an FNV-1a digest for change-detection, distinct from the SHA-256 the content store uses for integrity — a deliberate, documented split.)

The pipeline end to end#

flowchart TD BRIEF[Creator brief] --> FA["@maya/forge-assist<br/>LLM proposal + CapabilityPolicy"] FA -- "platform.* or undeclared invoke" --> REJECT[[rejected: named violations<br/>never reaches sandbox]] FA -- "realm.* only, declared" --> MAN[MoremiResourceManifest<br/>tier + capabilities + WASM scripts] MAN --> RES["PubGrub resolve<br/>semver + features + capability alts"] RES -- unsat --> FAIL[[plain-English root cause]] RES -- locked --> CAS["SHA-256 content store<br/>artifact_hash = content + closure"] CAS --> LOCK[MoremiForgeLockFile<br/>lock_hash] LOCK --> AOT["AOT precompile_component → .cwasm<br/>prod engine: compiler disabled"] AOT --> SB{{Wasmtime sandbox}} subgraph SB_INNER [tiered enforcement] TIER[tier gate: tier ≥ capability min] --> CAP[capability deny:<br/>undeclared imports refused] CAP --> FUEL[fuel / epoch budget:<br/>runaway → trap + flag] FUEL --> LIM[StoreLimits:<br/>memory / stack / no cross-instance leak] LIM --> HARD[Luau host hardening:<br/>frozen globals, no untrusted bytecode] end SB --> SB_INNER SB_INNER --> ESCAPE{{sandbox-escape gate<br/>escaped_fixtures == 0}} ESCAPE --> COMPOSE[forge-compositor<br/>priority-ordered, provenance-stamped] COMPOSE --> CLIENT["UE client: ApplyLockFile<br/>renders pinned closure"] LOCK -.->|pinned lock| CLIENT

Edge cases and malicious UGC#

  • A mod reaches for the economy. A guest invoking EconomyGrantCurrency without the System tier and a declared grant is denied before instantiation — no host call ever happens (:8660). Currency lives in Aje behind the trust boundary, never in realm WASM, so even a granted realm script mutates only realm-local, non-fungible play-currency.
  • A mod tries to read a platform secret. PlatformSecretRead is Trusted-tier, first-party only; an uploaded community resource cannot declare it, and the capability is refused at link time. The substrate-bridge V7IdentityFirewall independently guarantees realm code only ever sees an opaque per-realm handle, never a platform account id.
  • An infinite loop. A runaway gameplay plugin exhausts its fuel and traps at a deterministic instruction (FuelExhausted, :8076); it is throttled and flagged, the tick proceeds, and a golden replay still reproduces the exact state hash because the trap is deterministic.
  • A memory bomb / data exfiltration. Oversized linear memory is refused at instantiation and a recursive stack traps (:8177, :8190); async-stack zeroing plus per-instance isolation mean a freed plugin can't leak bytes into the next (:8146).
  • Tampering with the host or another plugin. Frozen globals/metatables, a per-plugin environment boundary, a non-disableable interrupt callback, and a refusal to load untrusted bytecode block all five host-tamper vectors with the shared host hash unchanged (:8517).
  • A swapped dependency. Because an artifact id hashes its content and its dependency closure, substituting a dependency changes the artifact hash; the lock no longer verifies, and the realm refuses to compose the altered closure.
  • A forged client event. Orthogonal but adjacent: the realm protocol treats every cross-trust message as hostile — rejects_forged_payload_even_with_valid_token and rejects_forged_security_token_even_when_envelope_is_resigned (realm-protocol/src/lib.rs:3788, :3799) prove a tampered intent is dropped before it can mutate authoritative state.
  • A malicious proposal from the AI co-creator. @maya/forge-assist's CapabilityPolicy (capability-policy.ts:75) scans generated source for oshun.invoke("…") and rejects any artifact that touches a platform.* namespace or invokes a capability it didn't declare — there is "no declaration that makes a realm script mint currency", and the assistant has no path around the check.