# Ixchel: The Modding Runtime & WASM Sandbox

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](../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 … ixchel` → `15 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

```mermaid
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.

## Related

- [Moremi: Realm Server & Netcode](./moremi-realm-server-and-netcode.md) — the
  authoritative Rust server that _hosts_ this sandbox, validates client intent,
  and replicates the composed realm.
- [Thesis, Trust Boundary, and Topology](./thesis-trust-boundary-and-topology.md)
  — why a realm is assumed hostile and exactly which concerns are never
  delegated to one (the boundary the sandbox enforces).
- [Sekhmet: Safety & Anti-Cheat](./sekhmet-safety-and-anti-cheat.md) — the
  platform-central malware scan, integrity, and child-safety floor that gate an
  artifact _before_ Ixchel ever resolves or runs it (planned sibling).
- The orientation hub [../V7_ARCHITECTURE.md](../V7_ARCHITECTURE.md).
