# Writing a third-party kit — a kit logic module of your own, step by step A **third-party kit** is a kit of game logic that is not one of GripForge's: you write it in Rust, the **build tool** compiles it on the base GripForge fixes, and a game plays it together with GripForge's kits. Neither the core nor any other kit is recompiled. This page is the whole path — write, build, declare, verify, export, play — and can be followed without reading the code of the core. Readable offline: `gripforge docs kit-third-party`, or the MCP tool `gripforge_docs` (topic `kit-third-party`). The commands of this page — `gripforge kit build|socle|add|verify|list| remove` and the MCP tools `gripforge_third_party_kit_*` — are those of CLI 0.3.44 / MCP 0.1.61 or later (`gripforge --version`; `gripforge update`). The model behind it (the core, the interfaces, the costs): topic `kit-modules`; every structure shared in memory: topic `kit-structures`. **Use the existing kits first.** A game is made by picking GripForge's kits and adapting them (config, mods, hooks of `game.engine`): MCP `gripforge_gamekit_search`, `gripforge kit api `, and the topic `third-party` (mods, presentation modules). Write a kit of your own only for a rule no kit has and no mod of `game.engine` can express — and make it small, on top of the others (listen to their events, call their services), as the example does. The worked example of this page is in the repository: **`examples/kit-third-party`** (`acme.streak`). It listens to an event of `collect.tokens`, reads and writes a component on the player, and calls a service of `economy.money`. | Step | What | Command | |---|---|---| | 1 | Write the kit | two files: `kit.json`, `src/lib.rs` | | 2 | Build it on the build base | `gripforge kit build ` (MCP: `gripforge_third_party_kit_build`) | | 3 | Declare it in the game, and say how far you trust it | `gripforge kit add --kit [--trust common]` (MCP: `gripforge_third_party_kit_add`) | | 4 | Verify, list, remove | `gripforge kit verify …`, `kit list`, `kit remove` (MCP: `…_verify`, `…_verify_build`, `…_list`, `…_remove`) | | 5 | Check what the game declares before it is played | `gripforge kit verify `, `gripforge kit list ` | | 6 | Export the game and play it in a page | `gripforge game-export `, `gripforge kit add …`, then `npm run dev` | **Two rules.** 1. **A third-party module is loaded only when the game declares it.** The declaration names where the module comes from and its SHA-256; the lock of the game freezes the version, that fingerprint and the trust; the export and the page refuse a binary that is not the declared one. 2. **A third-party kit is isolated unless the maker of the game says otherwise, kit by kit.** Isolated, it keeps a memory of its own: it sees the shared zone and the interfaces of the core, nothing else, and cannot corrupt the game. Trusted (`--trust common`), it is placed in the memory of the core and of the other kits, as a kit of GripForge is — faster, and no longer isolated. §2 has the two side by side. ## 0. What you need - The GripForge CLI (`gripforge`, 0.3.44 or later). It ships the **kit SDK** — the Rust side of the contract a kit is compiled against — in its `kit-sdk` folder: no GripForge checkout is needed to build a kit. (The MCP client `@gripforgeai/mcp` 0.1.61 or later has the same tools; it uses the kit SDK of the CLI found on `PATH`, or the one you name: `sdk`, `GRIPFORGE_KIT_SDK`. A GripForge checkout is a kit SDK too: `--sdk `.) None of these commands needs a login: they work on the files of the kit and of the game. - **rustup** (https://rustup.rs). The build tool uses the Rust toolchain the base pins, with its components and its target; it does not use your default toolchain and never changes it. What is missing is named with the one command that installs it — or installed for you with `--install`: ``` gripforge kit socle # the build base, and whether this machine is ready for it gripforge kit socle --install # installs the pinned toolchain beside yours (rustup toolchain install …) ``` - Node 22.19 or later (the CLI's own requirement). - A **game**: a file `game.json` (`{ "project": …, "lock": … }`), or the folder that holds it. An exported web game (`gripforge game-export `) is such a folder; `examples/kit-third-party/game.json` is such a file. - A **folder of kit modules** to play the game: the core and the GripForge kits your game uses, each as a `.wasm`, with `kit-modules.json`. An exported web game has one when its player package has them (`runtime-rust/kit-modules/`), a player package too (`kit-modules/`). The core and GripForge's kits come compiled: adding your kit recompiles none of them. ## 1. Write the kit ``` my-kit/ kit.json what the kit is: its id, its version, what it serves and what it needs src/lib.rs the kit ``` No `Cargo.toml`, no export macro, no flags: the build tool writes the crate around your source, once per mode (§2), so the same source gives both builds. **`kit.json`** ```json { "schema": "gripforge.kit-module/1", "id": "acme.streak", "version": "1.0.0", "description": "Rewards collecting streaks.", "services": ["acme.streak@1.0.0"], "requires": ["collect.tokens@^1.1"], "optional": ["economy.money@^1"] } ``` - `id`: dotted, lowercase, **under a prefix of your own** (`acme.…`); never the id of a GripForge kit. It is the id `fn id()` returns and the one the game declares. - `version`: `major.minor.patch` of the kit. It is the version the binary carries and the lock freezes. - `services`: the capabilities of `fn services()`, with the version of their API. That version is the one of the service, not of the kit: it stays `1.0.0` while you only fix the kit, gets a new minor when you add a method. - `requires`: the kits your kit cannot work without (it listens to them, calls them without a test). `optional`: the kits it uses only when the game has them (`kx.host.has_service(…)` first). The requirement is matched against the **kit** version of `kit-modules.json` (`economy.money` 1.3.0), not the version of the service API that `kit api` prints. - Optional: `"source"` (the file of the kit, `src/lib.rs` by default), `"module"` (the function that gives the kit, `module` by default), `"crates": { "smallvec": "1" }` (crates of crates.io your kit needs; the crates of the base keep the versions of the base), `"events"` and `"hooks"` (what the kit declares for the game schema: §7b). - **Not written by you**: `provides`, `consumes`, `memory`, `structs`, `sha256`. The build tool reads them from the binary it made and writes them in the manifest that goes with it. **`src/lib.rs`** — a kit is a type that implements `KitModule`, given to the build tool by a public function. A module is stateless: what it keeps between hooks goes in `kx.put(…)` and is read back with `kx.data::()`. ```rust use gripforge_kit_guest::bus::GameEvent; use gripforge_kit_guest::host::{KitModule, Kx}; use gripforge_kit_guest::shared::current_player; use serde_json::{json, Value as Json}; use std::rc::Rc; struct Streak; struct Settings { every: u64, bonus: f64 } impl KitModule for Streak { fn id(&self) -> &'static str { "acme.streak" } // the id of kit.json fn services(&self) -> &'static [&'static str] { &["acme.streak"] } // what it serves to the other kits fn setup(&self, kx: &mut Kx) { let cfg = kx.config().clone(); // its `config` in the project kx.put(Settings { every: cfg["every"].as_u64().unwrap_or(3), bonus: cfg["bonus"].as_f64().unwrap_or(25.0) }); kx.define_var("streak.count", "number", "Streak", None, Some(json!(0))); kx.on("tokens.collected", "collected"); // an event of collect.tokens } fn on_event(&self, kx: &mut Kx, tag: &str, ev: &GameEvent) { if tag != "collected" { return; } let Some(player) = current_player(kx.host) else { return }; // Read a component, write it back. let count = kx.host.entities.component(&player, "streak").and_then(|c| c["count"].as_u64()).unwrap_or(0) + 1; kx.host.entities.set(&player, "streak", json!({ "count": count, "kind": ev.payload["kind"] })); kx.set_num("streak.count", count as f64); let (every, bonus) = { let s = kx.data::(); (s.every, s.bonus) }; if count % every == 0 && kx.host.has_service("economy.money") { // A service of another kit. let balance = kx.call("economy.money", "add", &[json!(bonus), json!("streak")]).ok(); kx.emit("streak.bonus", json!({ "count": count, "bonus": bonus, "balance": balance })); } } fn call(&self, kx: &mut Kx, _capability: &str, method: &str, _args: &[Json]) -> Result { match method { "count" => Ok(json!(current_player(kx.host) .and_then(|p| kx.host.entities.component(&p, "streak").and_then(|c| c["count"].as_u64())) .unwrap_or(0))), _ => Err(format!("acme.streak has no method {method}")), } } } /// What the build tool takes. pub fn module() -> Rc { Rc::new(Streak) } ``` The complete file with comments: `examples/kit-third-party/src/lib.rs`. The two crates a kit always has are `gripforge-kit-guest` (the kit side of the interfaces) and `serde_json` (the value model of the core: `3` and `3.0` stay distinct, the keys keep their order). ### The API of a kit (`Kx`) Everything goes through the `kx: &mut Kx` each hook receives. The ones you need most: | You want to | Call | |---|---| | read your config, a data collection of the project | `kx.config()`, `kx.data_collection("token_kinds")`, `kx.project()` | | keep state between hooks | `kx.put(value)` once in `setup`, then `kx.data::()` — a `&mut T`: `kx.data::().ticks += 1` | | the clock, random numbers | `kx.elapsed()` (seconds of game time), `kx.tick()`, `kx.dt()` (a fixed tick: 1/60 s), `kx.rng()` (deterministic, forked per kit) | | listen to an event | `kx.on("topic", "tag")` in `setup` (`"family.*"` and `"*"` work) → `fn on_event(&self, kx: &mut Kx, tag: &str, ev: &GameEvent)`; `ev.topic`, `ev.payload` (a `serde_json::Value`: `ev.payload["amount"].as_f64()`, `ev.payload["kind"].as_str()`), `ev.entity`, `ev.source` | | emit an event | `kx.emit("topic", json!({ … }))` | | a var (shown by HUDs, saved) | `kx.define_var(path, "number" \| "string" \| "boolean", label, format, Some(default))` (`format`: `None`, or a display hint such as `Some("money")`), `kx.get_var(path)`, `kx.set_var(path, value)`, `kx.set_num(path, n)`, `kx.bind_var(path, "tag")` → `fn on_var(&self, kx: &mut Kx, tag: &str, path: &str, value: Option<&Json>, prev: Option<&Json>)` | | the player in scope | `current_player(kx.host)` → `Option` (its entity id) | | read an entity | `kx.host.entities.get(id)` (`.kind`, `.tags`, `.has_tag(t)`, `.component(name)`), `kx.host.entities.component(id, name)`, `kx.host.entities.transform(id)` (`Pos { x, y, z, yaw }`) | | find entities | `kx.host.entities.by_tag("enemy")`, `kx.host.entities.query(&Query { kind, all, any, none })`, `kx.host.entities.player()` | | write an entity | `kx.host.entities.set(id, component, value)`, `kx.host.entities.set_transform(id, &json!({ "x": 1.0 }))`, `kx.host.entities.tag(id, &["hidden"])`, `untag` | | create, destroy | `kx.create_entity(kind, &tags, Some(&components))` → id, `kx.host.destroy_entity(id)` | | call another kit | `kx.host.has_service("economy.money")`, `kx.call("economy.money", "add", &[json!(25)])` → `Result` | | serve the others | `fn services()` + `fn call(&self, kx: &mut Kx, capability: &str, method: &str, args: &[Json]) -> Result` | | a timer | `kx.after(0.5, "tag", Json::Null)`, `kx.every(0.5, "tag", json!({ … }))` (seconds of game time; the data is a `serde_json::Value`) → `fn on_timer(&self, kx: &mut Kx, tag: &str, data: &Json)` | | a trigger verb usable from the project's data | `kx.register_action("verb")` → `fn action(&self, kx: &mut Kx, verb: &str, args: &Json, ev: Option<&GameEvent>)`, `kx.register_condition("verb")` → `fn condition(…same…) -> bool` | | every tick | `fn update(&self, kx: &mut Kx, dt: f64)`, `fn late_update(&self, kx: &mut Kx, dt: f64)`, `fn update_idle(…)` — the build tool finds which ones your source implements and tells the core: a kit without them is never called on a tick | | input, physics | `kx.input().pressed("jump")`, `kx.input().axis("move_x")`, `kx.physics().raycast(…)` | | save and load | `fn save(&self, kx: &mut Kx) -> Option`, `fn load(&self, kx: &mut Kx, data: &Json)` (components and vars are already saved by the core) | | log | `kx.log("warn", "message")` | The other hooks (`ready`, `teardown`, `spawn`, `scene_enter`, `component`, mission steps…) and the cost of each call are in the topic `kit-modules` §3. **Which events a kit emits, which services it serves, with which arguments**: `gripforge kit api --section events,services`, or the MCP tool `gripforge_gamekit_api`. Rules that save time: - A read returns a **copy** (`component`, `get`, `get_var`): change it, then write it back with `set` / `set_var`. - Do not hold a reference read from the world across a call that writes (`emit`, `set`, `call`): Rust refuses it; clone what you need first (`let cfg = kx.config().clone();`). - An event emitted in a hook is delivered **after** the hook, in the next round of the same tick. - An isolated kit that panics is left out (its hooks are logged as failed); the game goes on without it. A kit in the common memory that panics stops the game (§2). ## 2. Build it on the build base ``` gripforge kit build my-kit # both builds, in my-kit/build/ gripforge kit build my-kit --out --mode isolated|common|both --install --jobs 4 --json ``` (MCP: `gripforge_third_party_kit_build { kit, out?, mode?, install? }`; `gripforge_third_party_kit_socle` for the base.) ``` Built acme.streak@1.0.0 on the build base kithost.kit@1.1.0 (structures 1.0, rustc 1.99.0 (…)) isolated my-kit/build/acme.streak-1.0.0.isolated.wasm sha256 0730139b… (168099 bytes) manifest my-kit/build/acme.streak-1.0.0.isolated.kit.json common my-kit/build/acme.streak-1.0.0.common.wasm sha256 6a5a9843… (179749 bytes) manifest my-kit/build/acme.streak-1.0.0.common.kit.json ``` **The build base** is what makes a binary built by anyone fit the contract. It is fixed by GripForge, in the kit SDK (`socle.json`; `gripforge kit socle` prints it), and it is not a choice of the kit: | | | |---|---| | the Rust toolchain | one pinned version, with the component `rust-src` and the target `wasm32-unknown-unknown`; run through rustup beside your own toolchains | | the crates | the lock of the base (`serde_json`, `indexmap`, … at the versions the core computes with); the build is `--locked`. A kit that names crates of its own resolves them on top, and the build fails — expected / found — if one of them moved a crate of the base | | the options | the profile (size first, whole-program LTO, `panic = "abort"`), and for the common build the recipe of a relocatable module (position-independent code, the standard library rebuilt once) | | the contract | `kithost.kit@1.1.0`, the interfaces a kit consumes, and the **structures** of the contract with the size and the offset every field must have | After the build the tool **checks the binary**, and stops with what was expected and what was found: - it is the build of the mode asked (isolated, or relocatable for the common memory); - its **structures table** (the custom section `gripforge.structs`, written by the compiler from the types of the contract) is the one of the base — the version of the contract, then each structure, field by field; - its contracts section is the one of the mode; an isolated build links only the functions of the core that carry numbers; nothing asks the system (file, clock, random: a crate that does is named). Then it writes, per build, the `.wasm` and its **manifest** `.kit.json` — your `kit.json` completed with what the binary says: `provides`, `consumes`, `memory` (the mode), `structs`, `sha256`, `bytes`, `built` (the base, the toolchain) — and `build.json`, which names the two. **The two builds, the two ways a game loads your kit:** | | `isolated` (the default) | `common` (trusted) | |---|---|---| | its memory | its own | the memory of the core and of the other kits: the host places it there | | what it can reach | the interfaces of the core, the shared zone (read; it writes only the event queue and the regions it asked) | everything in that memory | | a fault in it | stays in it: the kit is left out, the game goes on | can corrupt the core and the other kits; a trap stops the game | | how a call crosses | the host copies the message **once** into the other memory; lists and dictionaries cross as fixed-layout blocks, read where they land | by address, nothing is copied | | who decides | nobody has to: it is what a game gets | the maker of the game, for this kit, by name (`--trust common`) | | cost | one copy per call, and a function of the host on each crossing | the cost of a GripForge kit | Both speak **the same contract**: the functions of the interfaces (`kithost.world@1` …) by their numbers, and in memory the structures of `gripforge docs kit-structures` — `FixedValue`, `FixedPair`, `FixedBlock` for any list or dictionary, the records of the shared zone, the event queue. Nothing of a compiler's own layout crosses, which is why a kit built by another Rust than the core's loads. These two builds are the only ways a kit module is loaded: a binary that is neither (not relocatable, no entry across memories — a kit module with a memory of its own reached by the calls bridge, as they were once built) is refused by every tool and by the page, by name (`the binary is not a kit module a game can declare: it is neither relocatable … nor isolated … build the kit from its source with "gripforge kit build "`). Build both and let each game choose: the source is the same. | What the build says | Cause | |---|---| | `the build base needs the component rust-src of the toolchain 1.99.0. Install it …: rustup toolchain install …` | run that command, or the build again with `--install` | | `gripforge kit build needs rustup …` | install rustup first | | `…/src/lib.rs: no "pub fn module()"` | the function that gives the kit must be public (or name another with `"module"`) | | `the source invokes an export macro (export_kit!, …) — remove it` | a source written for an older guide: the tool writes the exports | | `the build failed (cargo exit 101) — the compiler's errors are above` | a Rust error in the kit; with `--json` the last lines are in the message | | `(the crates are locked to the build base: a crate the kit needs goes in "crates" of kit.json)` | your source uses a crate the base does not have | | `crate serde_json: expected 1.0.151 (the build base), found 1.0.160` | a crate of yours asks for another version of a crate of the base: pin yours lower | | `it imports env.…, which no host gives a kit module` | a crate of the kit asks the system for something (a clock, a file, random numbers): a kit takes them from `kx` | | `the kit SDK was not found` | the CLI is older than 0.3.44, or the MCP client finds no CLI on `PATH`: update, or pass `--sdk` | ## 3. Declare it in the game — and say how far you trust it ``` gripforge kit add --kit my-kit/build # isolated (the default) gripforge kit add --kit my-kit/build --trust common # in the common memory: only a kit you trust gripforge kit add --wasm --manifest <.kit.json|url> [--trust common] [--config '{"every":3}'] [--modules ] [--check] ``` (MCP: `gripforge_third_party_kit_add { game, kit | wasm + manifest, trust?, config?, modules?, check? }`.) `` is a `game.json` or the folder that holds it. `--kit` takes the output folder of `kit build` and picks the build of the trust you give; `--wasm` + `--manifest` name one build yourself (files, or `http(s)` URLs: a module declared by URL is not copied, its SHA-256 is what holds it). The command reads the module and its manifest, checks them, and — unless `--check` — writes: ```jsonc // project.thirdPartyKits[] { "id": "acme.streak", "version": "1.0.0", "source": "./third-party-kits/acme.streak-1.0.0.wasm", "sha256": "0730139b…", "bytes": 168099, "trust": "isolated", "manifest": { … } } // project.installed[] { "id": "acme.streak", "version": "1.0.0", "enabled": true, "config": { "every": 3 } } // lock.order […, "acme.streak"] (a new kit mounts last: after the kits it calls) // lock.kits["acme.streak"] { "version": "1.0.0", "enabled": true } // lock.thirdParty["acme.streak"] { "version": "1.0.0", "sha256": "0730139b…", "bytes": 168099, "trust": "isolated" } ``` and copies the module beside the game (`third-party-kits/`) and, when the game has kit modules, into them (`kit-modules.json` then says `"memory": "isolated"` or `"common"` for it: how the page loads it). **`trust`** is the word of the maker of the game, for this kit: - absent or `"isolated"`: the kit keeps a memory of its own. Use it for any kit you did not write. - `"common"`: the kit enters the memory of the game. The command warns you each time (`… is NOT isolated — a fault in it can corrupt the game`). The lock freezes it like the version and the SHA-256: a trust edited by hand is refused by `verify`, the export and the page; it is changed by declaring the kit again. - The build declared must be the one of the trust: an isolated binary is never placed in the common memory, a common binary never run isolated. There is no fallback from one to the other. What is checked, each with a message that says what to change: | Message | Cause | |---|---| | `the binary of acme.streak is built for the common memory, where it can read and corrupt the game and the other kits, and the game does not trust it with it: declare its isolated build …, or say you trust this kit (--trust common)` | you gave the `.common.wasm` without `--trust common` | | `the game trusts acme.streak with the common memory ("trust": "common") but the binary is its isolated build` | `--trust common` with the `.isolated.wasm`: give the common build (`--kit` picks it) | | `the binary is not a kit module a game can declare: it is neither relocatable (the build for the common memory) nor isolated (it does not export gfx_iso_enter) — build the kit from its source with "gripforge kit build "` | a binary that is neither build (made before the build tool, or by hand) | | `structures contract: expected 2.0 (the core kithost.core@1.0.0), found 1.0 — another major …` | the kit and the core of the game are of two structures contracts: build the kit with the CLI of that core | | `structure FixedValue@1.0: field data expected at 8 (8 bytes), found at 12 (8 bytes)` | the binary does not lay a structure out as the contract says (a hand-made or damaged binary) | | `the kit consumes kithost.world@^1.3, the core provides kithost.world@2.0.0: another major version — rebuild the kit for this core` | the core of the game is of another major of an interface | | `the kit consumes kithost.iso@^1.0, which the core kithost.core@1.0.0 does not provide` | the kit modules of the game are older than the isolated mode: export the game again | | `the core … of these kit modules owns its memory (an older build)` | the same: the kit modules of the game must be rebuilt | | `kit.json "consumes" is not what the binary declares` / `kit.json says memory "common", the binary is the isolated build` | the manifest is not the one of this binary: take the `.kit.json` written beside it | | `acme.streak: requires collect.tokens@^1.1, which the game does not install` | add that kit to the game first | | `"economy.money" is the id of a GripForge kit: choose an id under a prefix of your own` | the id | | `this game has no kit modules beside it … the kit was checked against its manifest only` (a warning) | pass `--modules ` to check against a core now; the export checks in any case | Its config is the `config` of its entry in `project.installed` (`--config`, kept when the kit is added again). ## 4. Verify, list, remove ``` gripforge kit verify --kit my-kit/build [--trust common] --modules # one build against a core, no game gripforge kit verify [kit-id] [--modules ] # what a game declares gripforge kit list gripforge kit remove ``` (MCP: `gripforge_third_party_kit_verify_build`, `…_verify`, `…_list`, `…_remove`.) **One build against a core** — before you hand a kit to someone, or when a core changes: ``` ok acme.streak@1.0.0 isolated sha256 0730139b43cd5fcc… (168099 bytes) contract expected kithost.kit@1.1.0, found kithost.kit@1.1.0 structures expected 1.0, found 1.0 ``` It compares the version of the contract and of every interface the kit consumes with what the core provides, then the structures of the contract one by one with the core's (`kit-modules.json` carries them). A refusal names the structure or the interface, what was expected and what was found; the exit code is 2. **What a game declares**: each module is read again from its source and must have the SHA-256 the game declares and its lock froze; its manifest must still be what the binary says; the binary must be the build of the trust declared; the declaration, `installed`, the lock and the kit modules of the game must agree on the version, the fingerprint and the trust. Exit code 2 when a kit does not verify. `list` shows, per kit, `isolated` or `COMMON MEMORY (trusted)`, its place in the mount order, its config, what it requires and serves. `remove` takes the declaration, the `installed` entry, the lock lines, the copy of the module and its kit module away. ## 5. Check what the game declares before it is played ``` gripforge kit verify gripforge kit list ``` `verify` reads every declared module again and compares it with the declaration and the lock (§4); `list` shows the mount order, the trust of each kit, what it requires and serves. The game is then played in a page (§6): the page loads the kit modules of the game exactly as declared and, when something does not fit, names the kit and the cause instead of starting. The services of a kit are called by the names of its API (`gripforge kit api `), its events are the topics that API lists. | What you see | Cause | | `the kit modules did not link: … was compiled against kithost.world@^1.3; the game provides kithost.world@2.0.0` | the binary and the core are of different majors: build the kit again with the CLI of that core | | `the kit modules did not link: … (memory "isolated"): structures contract 2.0, the owner (kithost.core) provides 1.0 …` | the kit and the core are not of the same structures contract: build the kit again (§2), check with `gripforge kit verify --kit … --modules …` | | `… (memory "common"): declared memory "common" but the binary is not relocatable` / `… the binary is relocatable … but it is declared memory "isolated"` | the folder holds the build of the other mode: `gripforge kit add` again with the trust you mean (§3) | | `… is in project.installed but not in lock.order` | add the id to `lock.order` | | `game.engine is in lock.order but not in project.installed` | add its entry to `installed`: every id of `lock.order` has one, `game.engine` included | | your kit is mounted but nothing happens | the event has another topic or payload than you think (`gripforge kit api `; the line `events` lists every topic that was emitted), or the service is not served (`has_service`) because that kit is missing or mounted later | | `. is not served by ` / `acme.streak has no method …` | the method name (they are the names of the TypeScript API: `canAfford`, not `can_afford`) | Both builds of the example (isolated and common) give the same game: same results, events, vars and components. ## 6. Export the game and play it in a page **A game folder that already has its kit modules** (an exported web game with `runtime-rust/kit-modules/`): after `gripforge kit add …` it is ready — `npm run dev` in the folder, open the address it prints. **A game of the site**: `gripforge game-export ` downloads the game with its player and its kit modules; `gripforge kit add --kit ` then declares your kit in that folder (§3: the module and its manifest are read and checked, the declaration and the lock are written, the module goes with the kit modules of the game) and `gripforge kit verify ` checks the result. A third-party kit has no TypeScript runtime and no view: nothing else is built for it. Both modes take the same path. **The page plays it like any kit.** The kit modules are the Rust logic of every game (`?logic=wasm` and `?logic=kits` mean the same): the page loads the core and the kits of the game's lock — yours with them — and nothing else. The console says it: ``` [gripforge] kit logic modules: the core and 5 kits (third-party: acme.streak) ``` Before a module is compiled the page compares it with the declaration: the version of `kit-modules.json` with the declared and locked one, the SHA-256 of the bytes with the declared one, the way the package loads it with the trust declared and locked. When it links the modules, the loader compares the structures table of your kit with the core's. There is no other wasm behind the kit modules: when something does not fit, **the game does not start** and the page shows the error, naming the kit, its version, its mode and the cause (topic `kit-modules` §1). It never plays the game without your kit in silence, and never loads it another way than declared. | What the page says | Cause | |---|---| | `acme.streak: … its module has sha256 …, the game declares … (not the binary that was declared)` | the module served is not the declared one: export again, or `gripforge kit add` then `kit verify` | | `acme.streak: the kit module is version 1.1.0, the game declares 1.0.0` | `kit-modules.json` and `game.json` are of two different builds | | `acme.streak: … was compiled against kithost.world@^2.0; the game provides kithost.world@1.0.0` | the kit and the core are of different majors: build the kit again, export again | | `acme.streak: the kit modules load it with memory "common", the game declares trust "isolated"` | the package would let the kit into the common memory and the game did not say so: export again, or `gripforge kit add` with the trust you mean | | `acme.streak: the lock froze trust "isolated", the project declares "common"` | the trust was edited by hand: it is changed by declaring the kit again (`gripforge kit add --trust …`) | | `acme.streak@1.0.0 (memory "isolated"): structures contract 2.0, the owner (kithost.core) provides 1.0 — …` | the kit was built on another structures contract than this core: build it again with the CLI of this core | | `kit-modules/kit-modules.json: the manifest of the kit modules was not read (HTTP 404)` | the folder served has no kit modules: export the game again (`gripforge game-export`) | | `acme.streak@1.0.0: no module in the kits package` | the kit modules do not hold the kit: `gripforge kit add` on this folder, or export again | | `This game runs on the TypeScript KitHost (logic ts), WITHOUT its third-party kit …` | the page was opened with `?logic=ts`, or the project asks for `logic: "ts"` (a red line at the top of the page) | The export refuses, with the reason, a game it could only ship broken: | Message of the export | What to do | |---|---| | `acme.streak: installed, but it is neither a GripForge kit nor a declared third-party kit` | `gripforge kit add` (the kit was written in `installed` by hand) | ## 7. Versions: what stays compatible - **The interfaces.** Your binary names what it was built against: `consumes kithost.world@^1.3`. A core that provides `world@1.4.0` accepts it; a core at `2.0.0` refuses it at load with a message. **Within a major an interface only grows at the end**: a function keeps its number, its name and its types, new ones get new numbers. The schemas of the interfaces are compared with their 1.0 snapshot in GripForge's CI. - **The structures.** What is shared in memory is versioned apart (`structures 1.0`). A structure added, or a field added in reserved bytes, is a minor: a kit built on 1.0 loads in a core at 1.1. A size, an offset or a meaning changed is a major: the two do not load together, and every tool says so before the page does (`structures contract: expected 2.0 …, found 1.0`). Nothing about the compiler is compared. - **The build base moves with the CLI.** A new CLI may pin another Rust or other crate versions: a kit built on the older base still loads while the contract and the structures are the same majors. Build again to follow. - **Your own versions.** Change `version` in `kit.json`, build, then `gripforge kit add` again: it moves the version and the SHA-256 in the declaration, `installed` and the lock. The lock is what a game is checked against: the export and the page refuse a module of another version or with other bytes than the lock froze. The services you serve to other kits follow the rule of the interfaces: add methods, never change one. - **A SHA-256 names a binary, not a source.** Two builds of the same source by the same CLI installed at the same place are the same bytes; built from an SDK at another path they differ (the compiler's symbol names depend on it). Ship the binary you verified. ## 7b. Changing and stopping what another kit decides: interceptable hooks **An event cannot be modified or cancelled.** The bus notifies after the fact, every subscriber in a fixed order. What a kit lets others change or stop is an **interceptable hook** of game.engine (`engine.hooks`, topic `mods-engine`): a decision the kit asks **before** it acts, and waits for. If you want to change a decision of another kit, tap its hook; if you want your own decisions to be changeable, declare a hook and run it before you act (then emit your event). Worked example: `examples/kit-third-party-hooks` (`acme.tax`). **Tap the hook of another kit, in code.** The hooks of a kit are in its API (`gripforge kit api economy.money`): name, payload, which fields are `mutable`, whether it is `cancellable`. ```rust fn setup(&self, kx: &mut Kx) { // priority: higher runs first, among the taps in code and the taps in data of the game and its mods. let opts = json!({ "priority": 0, "owner": "acme.tax", "id": "acme.tax.earn" }); let _ = kx.call("engine.hooks", "tap", &[json!("money.earn"), kx.callback("earn"), opts]); } /// Called synchronously each time economy.money is about to credit: args[0] is the payload as the taps before /// left it. Answer nothing (Json::Null), `{ set }` on mutable fields, or `{ cancel }` on a cancellable hook. fn callback(&self, kx: &mut Kx, name: &str, args: &[Json]) -> Result { let amount = args[0]["amount"].as_f64().unwrap_or(0.0); if amount > 500.0 { return Ok(json!({ "cancel": true, "reason": "over_cap" })); } Ok(json!({ "set": { "amount": amount * 0.8 }, "reason": "tax" })) } ``` A field that is not `mutable`, a value its schema refuses or a cancel of a hook that is not `cancellable` is refused and announced (`engine.hook.refused`); the game can also refuse a hook to mods (`mods.deny.hooks`). **Declare a hook of your own**, in `kit.json` — the game schema then carries it, and a game, a mod or another module taps it like any hook: ```json "hooks": { "tax.rate": { "mutable": ["rate"], "cancellable": false, "fields": { "rate": { "type": "number", "minimum": 0, "maximum": 1 } } } } ``` ```rust let asked = kx.call("engine.hooks", "run", &[json!("tax.rate"), json!({ "amount": amount, "rate": rate })])?; if asked["cancelled"] == json!(true) { return Ok(Json::Null); } // only when the hook is cancellable let rate = asked["payload"]["rate"].as_f64().unwrap_or(rate); // what the taps left // … act, THEN notify: kx.emit("tax.taken", json!({ "amount": amount, "rate": rate })); ``` **See who did what**: `logic.hooks.trace()` in the page or in a test lists every run — the taps in order, the fields each one set, who cancelled and why. Both builds tap and declare hooks the same way (the test plays `acme.tax` isolated and in the common memory and compares the decisions). ## 8. Limits today - **Kit modules only.** A third-party kit exists only as its own module, which is how every game is played in Rust logic; the TypeScript KitHost cannot play it. What it means for you: the game needs kit modules for every kit it installs, so every GripForge kit of the game must have a Rust module (`gripforge game-logic ` says when one has not). - **Isolation is of the memory.** An isolated kit cannot read or write the memory of the game, cannot take a link that hands an address, and writes in the shared zone only the event queue and its own regions. It is still code the game runs: it can emit events, set components and vars, call services, spend time. Read the source of a kit before you install it, isolated or not; `common` is for a kit you would have written yourself. - **No shared-memory build.** The build tool does not make the build for a memory shared between threads: a game with a trusted third-party kit runs on the plain build of the kit modules (the page picks it by itself). - **Hosted creation.** Create game can generate and retain project-local isolated kits after compilation and integration checks. The global search/pickers still list stock kits; arbitrary third-party uploads use an exported game folder (§3). Native exports of generated kits are not yet supported. - **The binary carries neither its id nor its version as a field the tools compare.** What is compared is the manifest written by the build, the declaration, the lock and `kit-modules.json`; the SHA-256 ties them to the bytes. Keep `fn id()` and `kit.json` in step yourself; play the game (§6) after each add — the line of the console (`kit logic modules: … (third-party: …)`) must name your kit. - **Updating an exported game from the site** (`gripforge game-export ` again) knows GripForge's kits only: it reports `game.json` and the kit modules as edited locally. After it, `gripforge kit verify `, and `gripforge kit add` again if the kit no longer verifies (its module is still in `third-party-kits/`). - **Logic only.** A kit module computes; what is drawn comes from the presentation modules of the player (`gripforge docs porting`). Your kit shows itself through vars (HUD), components and events that existing views already read. - **Rust only.** The kit side of the contract is the Rust crate of the kit SDK. The contract itself is not Rust's — numbered functions and structures with a declared layout — but there is no binding for another language yet. - **Cost.** A kit module pays for each call to the core; an isolated one also pays one copy of the message each way. Read once per hook, prefer one `query` to many `get`, keep per-tick hooks for what needs them (topic `kit-modules` §3 and §6). Hosted Create game now supports project-local, isolated generated Rust kits with bounded compilation, integration tests and admin history. Local CLI/MCP commands remain available for exported games.