# xript — Full Documentation
> xript (eXtensible Runtime Interface Protocol Tooling) is a platform specification for making any application moddable through sandboxed JavaScript. A single manifest declares the bindings, capabilities, hooks, types, and slots a host exposes; everything else — TypeScript definitions, docs, validation — derives from it.
Source: https://xript.dev
----------------------------------------
# xript
Source: https://xript.dev/
export const manifestSample = `{
"xript": "0.6",
"name": "my-game",
"bindings": {
"player": {
"description": "The player character.",
"members": {
"getHealth": {
"description": "Current health points.",
"returns": "number"
},
"setHealth": {
"description": "Set the player's health.",
"params": [{ "name": "value", "type": "number" }],
"capability": "modify-player"
}
}
}
}
}`;
export const modSample = `const hp = player.getHealth()
const max = player.getMaxHealth()
log("Current HP: " + hp + "/" + max)
player.setHealth(max)
log("Healed to full! HP: " + player.getHealth())`;
One manifest. Users write mods against the bindings you declare.
Write in
JavaScript
TypeScript
Integrate with
Rust
C# / .NET
Node.js
Run on
Browser
Node.js
Deno
Bun
Workers
Unity
Godot
Users write JavaScript, the most widely known programming language on the planet. No proprietary syntax, no specialized tools, no compilation step. If a user has ever opened a browser console, they already know enough to write their first mod.
Scripts execute inside a sandboxed QuickJS WASM engine with no access to the host filesystem, network, or process. Capabilities are opt-in and default-deny; a mod can only call what you explicitly allow. `eval()` and `new Function()` are blocked at the engine level. Running someone else's mod should never be a question of trust.
A single JSON manifest declares every binding, capability, type, and example your application exposes. From it, the toolchain generates TypeScript definitions, markdown documentation, and validation, all of it automatic. Change the manifest and everything downstream updates. No hand-written docs to forget, no types to keep in sync.
Start with safe expression evaluation in five minutes — no bindings, no capabilities, just math and string processing. Add simple host bindings in an afternoon for read-only data access. Graduate to advanced scripting with namespaces, capabilities, hooks, and persistent storage when you're ready. Go all the way to full-feature modding with UI fragments, state binding, and event handling. Each tier stands on its own; you never need the next one.
The JS/WASM runtime compiles QuickJS to WebAssembly, giving you one sandbox that runs identically in browsers, Node.js, Deno, Bun, and Cloudflare Workers. For native performance, drop in the Rust runtime (QuickJS via rquickjs) or the C# runtime (Jint) for Unity and Godot. Same manifest, same scripts, every platform.
JavaScript is the most-represented language in LLM training data. Your users can ask an AI to write their mods, and the manifest gives the AI everything it needs: every binding, parameter, type, and capability documented in one machine-readable file. No guessing, no hallucinated APIs. The CLI also runs as a Model Context Protocol server (`xript mcp`), so an agent can validate, score, lint, and scaffold against your manifest with the same tools you run at the terminal.
----------------------------------------
# Getting Started
Source: https://xript.dev/getting-started/
This guide walks through adding xript to an application from scratch. By the end you'll have a working sandboxed expression evaluator that users can safely extend.
## Install the Runtime
The universal runtime uses QuickJS compiled to WebAssembly: it works in browsers, Node.js, Deno, and more.
```sh
npm install @xriptjs/runtime
```
## Write a Manifest
Create a `manifest.json` describing what your application exposes to scripts. Start with a few safe bindings:
```json
{
"$schema": "https://xript.dev/schema/manifest/v0.6.json",
"xript": "0.6",
"name": "my-app",
"version": "1.0.0",
"bindings": {
"greet": {
"description": "Returns a greeting for the given name.",
"params": [{ "name": "name", "type": "string" }],
"returns": "string"
},
"add": {
"description": "Adds two numbers.",
"params": [
{ "name": "a", "type": "number" },
{ "name": "b", "type": "number" }
],
"returns": "number"
}
}
}
```
Only `xript` and `name` are required. Everything else is optional and layered on as needed.
## Provide Host Bindings
Each binding in the manifest needs a host-side implementation. These are regular JavaScript functions:
```javascript
const hostBindings = {
greet: (name) => `Hello, ${name}!`,
add: (a, b) => a + b,
};
```
## Create a Runtime
Initialize the WASM sandbox, then wire the manifest and bindings together:
```javascript
const xript = await initXript();
const runtime = xript.createRuntime(manifest, {
hostBindings,
console: { log: console.log, warn: console.warn, error: console.error },
});
```
`initXript()` loads the QuickJS WASM module once. After that, `createRuntime()` is synchronous: create as many runtimes as you need.
## Execute Scripts
Now you can safely evaluate user expressions:
```javascript
runtime.execute('greet("World")'); // { value: "Hello, World!", duration_ms: ... }
runtime.execute('add(2, 3)'); // { value: 5, duration_ms: ... }
runtime.execute('add(1, add(2, 3))'); // { value: 6, duration_ms: ... }
```
Scripts can compose your bindings with standard JavaScript:
```javascript
runtime.execute('[1, 2, 3].map(n => add(n, 10))'); // { value: [11, 12, 13], ... }
```
## Clean Up
When you're done with a runtime, free its WASM resources:
```javascript
runtime.dispose();
```
## See the Sandbox in Action
Anything not declared in the manifest is inaccessible:
```javascript
runtime.execute('process.exit(1)'); // Error: process is not defined
runtime.execute('require("fs")'); // Error: require is not defined
runtime.execute('eval("1 + 1")'); // Error: eval() is not permitted
runtime.execute('fetch("https://x")'); // Error: fetch is not defined
```
The sandbox guarantees that user scripts cannot escape the boundaries you define.
## Next Steps
- **Scaffold a new project** with `npx xript init`. See [Init CLI](/tools/cli#init).
- **Add capabilities** to gate sensitive operations. See the [Capabilities](/spec/capabilities) spec.
- **Add namespaces** to organize related bindings. See the [Manifest](/spec/manifest) spec.
- **Expose extension points** by declaring typed slots that mods fill, and broadcast named events the host emits. Bindings are what mods call, slots are what they fill, events are what the host emits. See the [Manifest](/spec/manifest) and [Mod Manifest](/spec/mod-manifest) specs.
- **Inherit from a base manifest** with `extends`; fill a base's abstract holes, refine its concrete pieces. See the [Manifest](/spec/manifest) spec.
- **Measure moddability** with `xript score`, which rates how much extension surface your host exposes. See [Extensibility Score](/tools/score).
- **Drive the toolchain from an agent** by running the CLI as an MCP server with `xript mcp`. See [MCP Server](/tools/mcp).
- **Read the doctrine** behind xript's open-by-default posture with `xript guide`. See ["More extensible, not less"](/guidance/openness).
- **Generate TypeScript definitions** from your manifest with `xript typegen`. See [Type Generator](/tools/cli#typegen).
- **Generate documentation** from your manifest with `xript docgen`. See [Doc Generator](/tools/cli#docgen).
- **Use the Node.js runtime** for file-based workflows and native V8 performance. See [Node.js Runtime](/runtimes/node).
- **Explore the runtime API** in depth. See [JS/WASM Runtime](/runtimes/js-wasm).
- **Run the full example** in the repository: `examples/expression-evaluator/`.
----------------------------------------
# Vision
Source: https://xript.dev/vision/
## The Problem
Software, games, the web, and embedded processes are closed by default.
Applications are isolated. Games reinvent modding from scratch, or skip it. Tools lock people into whatever the original authors imagined. When extensibility exists, it's bespoke: a unique API, a unique sandbox (or none at all), a unique set of conventions extenders have to learn from zero.
The result: users have no voice in how the software they run actually works.
The Elder Scrolls series proved decades ago that when you hand a community the tools to extend your work, they sustain it. Skyrim still thrives a decade-plus later because Bethesda let their community finish what they started. That principle shouldn't be the privilege of a handful of franchises.
---
## The Three Roles
xript centers on three roles:
- **Authors** build applications and define their extensibility surface. They write the manifest.
- **Extenders** write scripts and mods. They consume the manifest and add functionality.
- **Users** run the application, with or without mods. They benefit from both.
---
## The Vision
**Every application should be moddable. xript exists to make that practical.**
xript is not a programming language. It is a *platform specification*: a standard for how software exposes functionality in a safe, consistent, and well-documented way.
Extenders write JavaScript. They already know it, their tools already support it, and LLMs already speak it fluently. xript doesn't reinvent the language. It standardizes everything else: the bindings, the capability model, the sandboxing guarantees, the documentation, and the tooling.
When an application is xript-enabled, extenders get:
- A familiar language with nothing to install
- Type-safe bindings with editor support
- Generated documentation
- A sandbox they can't escape and don't need to fear
- Confidence that their work won't break the host or anyone using it
When authors integrate xript, they get:
- A declarative manifest that *is* the documentation
- Sandboxed execution with fine-grained capability gating
- Generated types, docs, and validation from a single source of truth
- A growing community of extenders who already know the system
---
## Guiding Principles
### 1. The Extender Is the Customer's Customer
Every decision flows through one question: *How does this affect the person writing the script?*
Authors adopt xript to access a community of extenders. Extenders stay because the experience respects their time. That experience drives the whole adoption loop.
### 2. Safety Is Not Optional
Extensibility without safety is a liability. xript-enabled applications guarantee:
- **No escape from the sandbox.** Scripts cannot access anything the host hasn't explicitly exposed.
- **No denial of service.** Execution limits prevent runaway scripts.
- **No implicit trust.** Capabilities are denied by default and granted deliberately.
- **No eval.** Ever.
A user running someone else's mod should never have to wonder if it's safe. It is safe. That's the contract.
### 3. The Manifest Is the Product
The xript manifest is not configuration. It *is* the API. It defines bindings, capabilities, types, descriptions, and examples in one place. From it, everything else is derived:
- Documentation sites
- TypeScript definitions
- Validation rules
- Interactive playgrounds
If it's not in the manifest, it doesn't exist. If it is, it's documented, typed, and enforceable.
### 4. Incremental Adoption, Always
No application should need to go all-in. xript is useful at every level of commitment:
- **Expressions only** — Safe eval replacement.
- **Simple bindings** — Expose a few functions with capability gating.
- **Advanced scripting** — Namespaces, capabilities, types, async.
- **Full feature** — Mods contribute UI, bind to state, handle events.
Each level stands on its own. Each level is a reason to adopt.
### 5. The Language Is Commodity
JavaScript is the runtime language, not because it's perfect but because it's *known*. Extenders don't want to learn a new syntax to add a feature. They want to open an editor and start writing.
xript's value is never in the syntax. It is in the bindings, the safety model, the tooling, and the ecosystem.
### 6. Standards Outlive Implementations
The xript specification is more important than any single runtime. Runtimes will come and go. QuickJS today, something else tomorrow. The spec endures.
A manifest written for xript-spec v1.0 should be implementable in any language, on any platform, for decades. That's the bar.
### 7. Documentation Is Not an Afterthought
If an extender can't find how to use a binding, it doesn't matter that it exists. xript treats documentation as a first-class output: generated, versioned, and always in sync with the manifest.
The quality of xript.dev and every generated doc site is as much a part of the product as the runtime itself.
### 8. More Extensible, Not Less
xript is an extensibility substrate, so its defaults lean the way the project does. When a design choice could go either way (expose it or hide it, accept the unknown shape or reject it, allow the reach or wall it off), the open option is the one that matches what xript is for.
A restriction is permitted only when it genuinely buys convenience or security the framework couldn't otherwise provide, and it has to justify itself plainly. The capability model is the real security wall; default-deny, explicit grants, gated surfaces. Restrictions that *are* that boundary are the product, not lockdown. Schema validation is not a security boundary, so tightening a schema is rarely a security argument.
Where a feature could be open or guarded, it ships open and lets the host opt *out*. You opt out of openness, not into it. And an open default fails soft where failing hard would buy nothing: when an optional reach can't complete, fall back to what's bundled and surface a warning instead of taking the whole operation down.
This doctrine is authored as first-class guidance, surfaced through the `xript guide` command, the `xript_guide` MCP tool, and the docs [Doctrine](/guidance/openness/) section, so the same words steer the framework, the tooling, and the people building on it.
---
## The Analogy
**xript is the USB of software extensibility.**
Before USB, every device had its own connector, its own driver model, its own limitations. After USB, you plug things in and they work.
Before xript, applications reinvent extensibility from scratch, or ship without it. After xript, authors declare a manifest and their software becomes a platform. Extenders learn one system and can extend anything.
---
## What xript Is
- A specification for declaring extensibility manifests
- A capability-based security model for sandboxed scripting
- A toolchain for generating documentation, types, and interactive demos
- A set of runtime implementations for major platforms
- A community standard for moddable software
## What xript Is Not
- A programming language
- A general-purpose application framework
- A replacement for WebAssembly components
- A build system, database, or deployment platform
---
## The Measure of Success
xript succeeds when:
- An extender can look at any xript-enabled application and immediately know how to extend it.
- An author can make their application moddable in an afternoon.
- A community can sustain and transform a product beyond what its authors imagined.
- The question changes from *"Can users extend this?"* to *"Why can't they?"*
---
*xript.dev — mod the it*
---
And before anyone asks, yes, it was backronymed: **eXtensible Runtime Interface Protocol Tooling**, but the 'xr' is real.
----------------------------------------
# Adoption Tiers
Source: https://xript.dev/adoption-tiers/
No application has to go all-in on xript. The four adoption tiers let you start simple and add complexity only when you need it. Each tier stands on its own as a valid integration point.
## The Four Tiers
| | Tier 1 | Tier 2 | Tier 3 | Tier 4 |
|---|---|---|---|---|
| **Name** | Expressions Only | Simple Bindings | Advanced Scripting | Full Feature |
| **Bindings** | None or flat functions | Flat functions + namespaces | Rich namespaces | Rich namespaces |
| **Capabilities** | None | Optional | Required | Required |
| **Custom types** | None | Optional | Yes | Yes |
| **Execution limits** | Optional | Optional | Yes | Yes |
| **Inline examples** | No | No | Yes | Yes |
| **Async bindings** | No | Optional | Yes | Yes |
| **Slots** | No | No | No | Yes |
| **Mod manifests** | No | No | No | Yes |
| **Fills (fragments, roles, hook handlers)** | No | No | No | Yes |
| **Example** | [Expression Evaluator](/examples/expression-evaluator) | [Plugin System](/examples/plugin-system) | [Game Mod System](/examples/game-mod-system) | [UI Dashboard](/examples/ui-dashboard) |
## Tier 1: Expressions Only
**The safe eval replacement.** Your application needs to evaluate user-provided expressions: formula fields, template logic, calculated columns. You want a sandbox that guarantees safety.
The manifest is minimal:
```json
{
"xript": "0.1",
"name": "calculator"
}
```
No bindings, no capabilities, no types. Users get the JavaScript language itself inside a sandbox. They can write `2 + 2`, `[1,2,3].map(x => x * 2)`, or any pure expression. They cannot access `process`, `eval`, `fetch`, or anything outside standard JavaScript.
You can optionally expose flat helper functions (like `abs`, `round`, `upper`) to make expressions more useful. These are declared as bindings with no capability gates, so every function is always available.
**Choose tier 1 when:**
- You want a drop-in replacement for `eval()` that is actually safe
- All exposed functions are read-only with no side effects
- You do not need to gate any functionality behind permissions
- You want the smallest possible integration surface
**See it in action:** [Expression Evaluator example](/examples/expression-evaluator)
## Tier 2: Simple Bindings
**The plugin system.** Your application exposes a handful of functions organized into namespaces. Some operations are sensitive and need permission gating.
The manifest adds bindings, capabilities, and custom types:
```json
{
"xript": "0.1",
"name": "task-manager",
"version": "1.0.0",
"bindings": {
"tasks": {
"description": "Read and manage tasks.",
"members": {
"list": { "description": "Returns all tasks.", "returns": { "array": "Task" } },
"add": { "description": "Creates a new task.", "params": [...], "capability": "manage-tasks" },
"remove": { "description": "Removes a task.", "params": [...], "capability": "admin" }
}
}
},
"capabilities": {
"manage-tasks": { "description": "Create and complete tasks.", "risk": "medium" },
"admin": { "description": "Delete tasks and admin operations.", "risk": "high" }
},
"types": {
"Task": { "description": "A task.", "fields": { "id": { "type": "string" }, ... } }
}
}
```
Namespaces group related functions (`tasks.list()`, `tasks.add()`). Capabilities create a permission hierarchy: read-only operations are always available, writes require `manage-tasks`, destructive operations require `admin`. Custom types document the data structures extenders will work with.
**Choose tier 2 when:**
- You need to organize bindings into logical groups
- Some operations are destructive or sensitive and need permission gating
- You want to document data structures for script authors
- Different scripts need different permission levels
**See it in action:** [Plugin System example](/examples/plugin-system)
## Tier 3: Advanced Scripting
**The complete scripting system.** Your application exposes a rich API with multiple namespaces, fine-grained capabilities, complex types, inline code examples, async operations, and execution limits.
A tier 3 manifest uses the full scripting surface of the spec:
- **Multiple namespaces** organized by domain (`player`, `world`, `data`)
- **Capability tiers** from low-risk (`storage`) through medium (`modify-player`) to high (`modify-world`)
- **Object and enum types** that describe the full data model (`Position`, `Item`, `Enemy`, `ItemType`)
- **Async bindings** for I/O-bound operations (`world.getEnemies()`, `data.get()`)
- **Inline examples** showing extenders how to use each binding
- **Execution limits** tuned for the application's performance requirements
The manifest becomes the complete contract between your application and its scripting community. From it, the toolchain generates TypeScript definitions, API documentation, and validation rules.
**Choose tier 3 when:**
- You are building a scripting system or extensibility platform
- Your API surface is large enough to need careful organization
- You want generated docs and types that are always in sync with the API
- Extenders will write multi-line scripts, not just expressions
- You need async operations (database access, network calls, file I/O)
**See it in action:** [Game Mod System example](/examples/game-mod-system)
## Tier 4: Full Feature
**The modding platform.** Mods stop being invisible background logic and start having a visual presence in your application. Authors declare typed **slots**, named plug-points in their host, and mods **fill** them. A slot's `accepts` type governs what a valid fill looks like and what the host does with it: mount a fragment, call a renderer, resolve a provider role, or fire an event handler. Everything a mod contributes is a fill.
A tier 4 manifest builds on everything in tier 3 and adds `slots`:
```json
{
"slots": [
{
"id": "sidebar.left",
"accepts": ["text/html"],
"capability": "ui-mount",
"multiple": true,
"style": "isolated"
},
{
"id": "header.status",
"accepts": ["text/html"],
"style": "inherit"
}
]
}
```
Mods declare themselves in a [mod manifest](/spec/mod-manifest/) and contribute through a single `fills` object, keyed by the host slot id. A fragment-format slot takes a fragment fill:
```json
{
"xript": "0.6",
"name": "health-panel",
"version": "1.0.0",
"capabilities": ["ui-mount"],
"fills": {
"sidebar.left": [
{
"format": "text/html",
"source": "fragments/panel.html",
"bindings": [{ "name": "health", "path": "player.health.val" }],
"handlers": [{ "selector": "[data-action='heal']", "on": "click", "handler": "onHealClicked" }]
}
]
}
}
```
`fills` is the canonical contribution surface. A UI fragment, a provider role, and a lifecycle hook handler are not separate top-level concepts; each is a fill of a slot of a particular type. The value under each slot id is always an array, so a `multiple: true` slot can take more than one fill. (The earlier top-level `fragments[]` array and `contributions.provides` still validate but emit a deprecation warning; fold them into `fills`.)
Fragment markup uses `data-bind` for value binding and `data-if` for conditional visibility. DOM event handlers go in the `handlers` array (entries shaped `{ selector, on, handler }`; the old `events` key is a deprecated alias). The sandbox fragment API gives mods programmatic control through a command buffer: `toggle`, `addClass`, `setText`, `replaceChildren`, and more. Every fragment is sanitized before it reaches the host.
Scaffold a new mod project with `xript init --mod` to get a working template with a mod manifest, fragment HTML, and entry script.
**Choose tier 4 when:**
- You want mods to contribute visible UI, not just background logic
- Your application has natural extension points in its interface (sidebars, panels, overlays, status bars)
- You want mods to react to state changes and render live data
- You are building a platform where the community shapes the user experience
**See it in action:** [UI Dashboard example](/examples/ui-dashboard)
## Progressing Between Tiers
The tiers are not walls; they are waypoints. Moving from one tier to the next is additive:
**Tier 1 → Tier 2:** Add a `bindings` section with namespaces. Add `capabilities` for anything sensitive. Optionally add `types` to document your data structures. Your existing flat bindings (if any) continue to work unchanged.
**Tier 2 → Tier 3:** Add `examples` to bindings so extenders can see usage patterns. Add `async: true` to bindings that need it. Add `limits` tuned for your use case. Expand your type definitions to cover the full data model. The structure you already built in tier 2 is the foundation.
**Tier 3 → Tier 4:** Add `slots` to your app manifest to define where mods can plug in. Each slot declares an `accepts` type and an optional `payload` JSON Schema describing a valid fill. Mods create their own [mod manifests](/spec/mod-manifest/) and contribute through `fills` keyed by your slot ids. The runtime handles sanitization, data binding, and event routing. Your existing scripting API becomes the data layer that fragments bind to.
Nothing breaks when you add complexity. A tier 1 manifest is a valid tier 4 manifest: it just uses fewer features.
## The Manifest Drives Everything
Regardless of tier, the manifest is the single source of truth. The toolchain reads it and generates:
- **TypeScript definitions** via `xript typegen`: editor autocomplete and type checking for extenders
- **API documentation** via `xript docgen`: always in sync, always accurate
- **Validation** via `xript validate`: catch manifest errors before runtime
- **Moddability scoring** via `xript score`: rate how much extension surface your host exposes, with `xript score-diff` to track the delta against a baseline
- **Lint findings** via `xript lint`: the actionable list behind the score; dead slots, undeclared capabilities, legacy-shape mods
- **A plain-English summary** via `xript describe`: what bindings, hooks, slots, and capabilities a host manifest exposes
The same toolchain runs as a Model Context Protocol server via `xript mcp`, exposing every command one-to-one (`xript_validate`, `xript_typegen`, `xript_score`, and the rest) so an agent can read and reason about your manifest over stdio.
A tier 1 manifest generates simpler output. A tier 4 manifest generates richer output. But the workflow is the same at every level: declare your API in JSON, and let the tools do the rest.
----------------------------------------
# When to reach for xript
Source: https://xript.dev/guidance/when-to-use/
xript exists so that a user-facing surface can be **composed from a manifest, scripts, and fragments** instead of baked into the host. The default stance is simple: when something in an application could be contributed from outside rather than hardcoded inside, reach for xript first.
## The default
The framework provides primitives. The application's own content is its first mod. If the host cannot be replaced by a mod doing the same thing through the same surface, the surface is not yet extensible — it is hardcoded with a manifest sitting next to it. Proximity to a manifest is not the same as being manifest-driven.
## Three questions for any surface
Run every surface a host is about to build through these, in order:
1. **Could this live outside the host as a manifest + script + fragment instead of inside it as host code?** If yes, that is the default. The host implements primitives; behavior and presentation come from data and script.
2. **Is there already a canonical shape for this?** Slots and fragments compose UI. Bindings expose host calls. Hooks fire on lifecycle events. Capabilities gate access. Commands name invocable actions. When a surface fits one of these cleanly, use that name — do not invent new vocabulary.
3. **What does the manifest look like first?** The manifest is the contract. Sketch it before the implementation. Types, documentation, and validation all derive from it.
## Signals you should be using xript
- "We'll probably want users to customize this later."
- A renderer, editor, viewer, or panel that someone might want to replace or extend.
- A growing `switch` or `if/else` ladder over a closed set of kinds, where new kinds keep getting added by editing the host.
- A registry of behaviors that only the host can populate.
- Content the host ships that looks exactly like content a third party would contribute.
When any of these appear, the manifest-driven shape is almost always the better one. Name it explicitly so the trade-off is a decision rather than a default.
## When the hardcoded shape is genuinely right
xript is a compass, not a gate. A real constraint can rule out the extensible shape: a hot inner loop where the runtime boundary costs too much, a surface with exactly one possible implementation forever, a security boundary that must stay in host code. Name the constraint, name the fork, and choose deliberately. The goal is a visible decision, not a forced one.
----------------------------------------
# Choosing an extensibility surface
Source: https://xript.dev/guidance/surfaces/
xript has a small, fixed vocabulary of surfaces. Most "how should we make this extensible" questions resolve to picking the right one. Use the canonical name; do not coin a synonym.
The host offers exactly two surfaces. **Bindings** are points the mod *calls*. **Slots** are typed points the mod *fills*. Everything a mod contributes — a fragment, a role, a lifecycle handler — is a fill of a slot of a particular type. There is no separate top-level "fragment" or "hook" or "provides" primitive; each is just a fill, and the slot's `accepts` type governs what a valid fill looks like and what the host does with it.
## The vocabulary
- **Binding** — a function or namespace the host exposes for a mod to call. Use when a mod needs the host to *do* something: read state, perform an action, reach a host capability. Bindings are the mod-to-host direction. The mod calls; the host implements.
- **Slot** — a named, typed plug-point the host declares for a mod to fill. Use whenever a mod should *contribute* something the host then mounts, calls, resolves, or fires. A slot declares what it `accepts` (one or more format/kind names), whether it allows `multiple` fills, and an optional gating `capability`. The `accepts` type is the whole contract: it decides what a fill must look like and what the host does with it.
- **Fill** — the mod's contribution into a host slot, keyed by the slot's `id`. The host declares the slot; the mod declares the fill. The fill's inner shape is governed by the target slot's `accepts` type — the host owns that shape, and validation does not police it beyond "the slot exists and you hold its capability."
- **Capability** — a named permission that gates a binding or a slot. Default-deny. Use to make access explicit and grantable rather than ambient. A mod that fills a gated slot must hold the slot's capability.
- **Command** — a named, invocable action with typed inputs and outputs. Use when an action should be discoverable and callable by name, by a user or another mod.
## Slot types you will meet
A slot's `accepts` names the kind of fill it takes. The common kinds:
- **Fragment-format slot** — accepts an inert template (`text/html+jsml`, `application/jsml+json`, or another registered format). The fill names the `format`, a `source`, and its `bindings` / `handlers` (DOM event handlers; `events` is a deprecated alias). The host mounts it. Fragments carry no logic of their own: values flow through `data-bind`, visibility through `data-if`, and everything else through the sandbox fragment API.
- **Role slot** — accepts a set of functions the mod exports to satisfy a named role (`application/x-xript-role`). The fill maps role function names to the mod's exports; the host resolves and calls them.
- **Event slot** — accepts a lifecycle handler (`application/x-xript-hook`). The fill names the handler export; the host fires the slot, which calls every fill. This is what a "hook" is now: an event-typed slot, fired by calling its fills.
- **Code / data slots** — accept a registered renderer kind, a JSON payload, or another host-defined shape. The fill matches whatever the slot's `accepts` declares.
## How to pick
- Mod needs to call the host → **binding**.
- Mod contributes anything the host mounts, calls, resolves, or fires → **slot** (host side) + **fill** (mod side). Pick the slot whose `accepts` type matches the contribution: a fragment goes into a fragment-format slot, a role into a role slot, a lifecycle handler into an event slot.
- Access must be gated → **capability**.
- Action should be named and invocable → **command**.
## Anti-patterns
- **Reaching for a separate "fragment" or "hook" primitive.** There is one contribution surface: fills into slots. A fragment is a fill of a fragment-format slot; a lifecycle handler is a fill of an event slot. Don't model them as their own top-level things.
- **Inventing a manifest schema** where the existing manifest already has a place for this. Check the schema before defining new JSON. The answer is almost always a new slot, not a new concept.
- **A host-only registry** that mods can't populate, when a slot would let them fill it.
- **Vocabulary drift** — mixing *extension / plugin / add-on / mod* within one application. Pick one noun and hold it.
- **Modeling renderers as slots.** A format renderer (a DOM fragment processor, a terminal widget renderer, a future native renderer) is runtime infrastructure, not a manifest concept. It paints a fragment of format F onto a target; the slot's `accepts` type names the format the runtime must be able to render. Don't put renderers in the manifest.
- **Logic in fragments** — a fragment that tries to compute or branch beyond `data-bind` / `data-if`. That logic belongs in the sandbox, reached through the fragment API.
----------------------------------------
# Mod zero
Source: https://xript.dev/guidance/mod-zero/
The strongest test of an extensibility surface is whether the host's *own* features go through it. If the application's built-in content is authored as a mod against the same surface a third party would use, the surface is real. If the built-in content takes a private path the host keeps for itself, the surface is decoration.
## The principle
Build the framework in host code. Author the behavior and content in data and script. The first mod — "mod zero" — is the application itself, loaded through the public surface. Third-party mods are then not a special case; they are more of the same.
## Why it holds the line
- **It proves the surface.** A slot that only the host can fill is untested as an extensibility point. A slot the host fills *as a mod* is exercised every time the app runs.
- **It prevents private back doors.** When the host's own features must go through bindings, hooks, slots, and capabilities, those surfaces stay complete. Gaps surface immediately, because the host hits them first.
- **It keeps the manifest honest.** If the built-in content is manifest-driven, the manifest stays the source of truth. Types, docs, and validation derived from it describe reality, not a subset of it.
## The failure mode it guards against
A renderer, panel, or behavior written directly in host code, with a manifest placed beside it, described as extensible. It is not. The manifest is documentation of a closed implementation. The test: delete the host code and reimplement that feature as an external mod through the declared surface. If that is impossible, the surface is not yet what it claims to be.
## Applying it
When adding a host feature, ask whether it *could* be authored as a mod against the existing surface. If yes, author it that way even though it ships with the app. If no, that gap is the signal — the surface is missing a binding, a slot, a hook, or a capability. Close the gap rather than routing around it with private host code.
----------------------------------------
# The host/mod boundary
Source: https://xript.dev/guidance/boundary/
The hardest recurring question in an extensible app is where the line sits: what belongs *in the host* versus what belongs *in a mod*. The rule is short — **the host provides mechanism; mods provide policy and content. The host declares the surface; mods fill it.** Everything else is applying that rule to a specific case.
## What belongs in the host
- **The surface itself** — the manifest, and the bindings, slots, and capabilities it declares. A mod cannot declare the host's own surface; that is the host's job.
- **Mechanism** — the implementation behind each binding, and the host-side handling of each slot. The *how* of reaching real state, performing a real action, mounting a fragment, or firing an event lives in the host; mods call bindings and fill slots, they don't reimplement the host's side.
- **Security-critical enforcement** — capability gating, the sandbox boundary, anything a mod must not be able to reach around. If correctness depends on a mod *not* being able to bypass it, it is host code.
- **Genuinely hot paths** — a tight inner loop where crossing the runtime boundary per iteration costs too much. A real performance constraint is a legitimate reason to keep something in the host; name the constraint when you invoke it.
## What belongs in a mod
- **Policy and behavior** — the decisions, the rules, the *what to do*. The host exposes the levers; mods decide how to pull them.
- **Presentation, behavior, and reactions** — what the user sees, the roles the host needs satisfied, the moments mods react to: all contributed as fills into declared slots.
- **The app's own content** — built-in features authored as mod zero, through the same surface a third party would use.
- **Anything a third party could plausibly replace or extend** — if an outside author could reasonably want to do this differently, it is mod territory, even when the host ships the default.
## The deciding question
> Could a third party do this as a mod, through the declared surface?
- **Yes** → it is mod territory. Build it as mod zero even though it ships with the app. If the host *can't* currently let a mod do it, that gap is the signal: the surface is missing a binding, a slot, or a capability. Close the gap rather than keeping the feature as private host code.
- **No** — because it *is* the surface, or a mechanism nothing can reach around, or a security boundary — → it is host code.
## The drift to watch, both directions
- **Policy creeping into the host**: the host grows a behavior that should have been a mod. The tell is a `switch` over named kinds, or a default that hardcodes one opinion where a slot would let mods fill their own.
- **A mod reaching for mechanism**: a mod reimplementing something the host should own, or wanting access the surface deliberately withholds. The tell is a mod that only works by reaching around the declared surface.
Both are the line moving the wrong way. Name which side a thing belongs on, and why, when it isn't obvious.
## Prefer the check to the reminder
This is doctrine, and doctrine is advisory — easy to forget mid-task. Where the boundary can be made *checkable*, prefer the check: a slot only the host can declare, a capability that gates access, a cross-validation that fails loudly when a mod's fills or requests don't match the host contract. A validator that fails on every commit holds the line in a way a remembered principle cannot. Use the doctrine to decide where the line is; use the contract to keep it there.
----------------------------------------
# More extensible, not less
Source: https://xript.dev/guidance/openness/
xript is an extensibility substrate. Its whole reason to exist is to let things be reached, replaced, and extended from outside the host. So its default has to lean the same way the project does: **more extensible, not less.** When a design choice could go either way — expose it or hide it, accept the unknown shape or reject it, allow the reach or wall it off — the open option is the one that matches what xript is for. Closing a door is the exception, and an exception has to argue for itself.
## The rule
The framework defaults toward openness. A restriction is permitted only when it genuinely buys convenience or security the framework could not otherwise provide — and the restriction has to justify itself plainly, in the moment it's added, in terms a reader can check. "It felt safer" is not a justification. "This is the security boundary, and here is what it stops" is.
Reflexive lockdown is off-brand for an extensibility substrate. A framework whose first instinct is to forbid is a framework working against its own grain. The instinct to add a guard, narrow an input, or reject an unfamiliar shape is worth having — but it has to clear a bar, not ride in for free.
## When a restriction earns its place
A restriction belongs when it buys something the open shape can't:
- **It is the security boundary.** The capability model is xript's real wall: default-deny, explicit grants, gated surfaces. Restrictions that *are* that boundary — or that close a genuine hole in it — are not lockdown; they're the product. Schema validation, by contrast, is not a security boundary, so tightening a schema is rarely a security argument.
- **It buys real convenience or a real guarantee.** A constraint that makes the common case simpler, the error clearer, or a result reproducible can pay for itself. Name the payoff.
- **The alternative is genuinely unsafe or unworkable**, not merely unfamiliar. An unrecognized shape is not automatically a threat.
When none of these holds, the open shape wins by default. The burden is on the restriction, never on the openness.
## Opt out of openness, not into it
Where a feature could be open or guarded, ship it open and let the host *opt out*. Remote schema resolution is the worked example: it is allowed by default, and a host that wants a tighter posture sets an explicit restriction (an allowlist, or disabling remote resolution outright). The dial exists; it just starts at open. Inverting that — making every host opt *in* to a capability that's safe by default — taxes the common case to soothe an instinct, and that is exactly the reflexive lockdown this doctrine exists to resist.
## Openness over brittleness
An open default also means failing soft where failing hard would buy nothing. When an optional reach can't complete — a remote schema is unreachable, an uncached fetch fails offline — fall back to what's bundled and surface a warning, rather than hard-failing the whole operation. A brittle "all or nothing" path is a quiet form of lockdown: it turns a recoverable gap into a wall. Degrade, warn, and keep going.
## The drift to watch
- **A guard with no stated cost it prevents.** If a restriction can't name what it buys, it's reflex, not design. Strip it or justify it.
- **Opt-in where opt-out would do.** A safe-by-default capability hidden behind a flag the host must find and enable. Flip the default.
- **Hard-fail where fallback would do.** An optional path that takes the whole operation down with it when it can't complete.
- **A schema treated as a security wall.** Tightening validation to "lock something down" confuses the schema with the capability model. The schema describes shape; the capability model holds the line.
Use this doctrine the way you'd use the boundary doctrine: to decide which way a close call leans. When in doubt, lean open, and make any door you close explain itself.
----------------------------------------
# Authoring a mod against a host
Source: https://xript.dev/guidance/authoring/
A mod is a manifest plus the scripts it declares. The host's manifest tells you exactly what you can call and what you can fill. Read it first; do not guess.
## The loop
1. **Read the host's surface.** Get the host manifest and have it described: what bindings exist, what slots the host declares, what each slot `accepts`, and what capabilities gate them. This is the whole contract — there is nothing to call and nothing to fill that is not declared here.
2. **Declare the mod manifest.** Name the mod, declare the capabilities it requests, and declare its entry script and exports. Requested capabilities are explicit; default-deny means anything not requested is denied.
3. **Write the entry script.** Implement the exports the host will call: fragment event handlers, role functions, lifecycle handlers. Call host bindings. Keep logic here, in the sandbox — never in fragments.
4. **Fill the host's slots.** In the mod manifest's `fills`, key each entry by a host slot `id` and provide the fill the slot's `accepts` type calls for: a fragment into a fragment-format slot, a function map into a role slot, a handler export into an event slot. A fragment, a role, and a lifecycle handler are all just fills — pick the slot whose type matches.
5. **Validate.** Run the mod manifest through validation, and cross-validate it against the host manifest — requested capabilities must be grantable, and every slot you fill must exist in the host and not require a capability you don't hold. Validation checks that the slot exists and that you hold its gate; the host owns the inner shape of each fill.
6. **Run it.** Load the mod in a runtime and exercise its exports, its fills, and the events the host fires before shipping.
## Everything you contribute is a fill
There is one contribution surface: `fills`, keyed by host slot `id`. A panel of UI is a fill of a fragment-format slot. Satisfying a host role — a transcriber, a formatter, a provider — is a fill of a role slot. Reacting to startup or to a state change is a fill of an event slot. Don't reach for a separate `fragments` or `provides` or `hooks` field; the slot's `accepts` type already says what the fill must look like.
## Fragments carry no logic
A fragment is an inert template you fill into a fragment-format slot. Bind a value with `data-bind`. Toggle visibility with `data-if`. For anything else — events, mutations, computed content — route through the sandbox fragment API. A fragment that tries to branch or compute on its own is the most common authoring mistake; move that logic into the entry script.
## Capabilities are requested, not assumed
If a binding call or a gated slot needs a capability, the mod must request it in its manifest, and the host must be willing to grant it. Validation catches a fill into a gated slot the manifest never requested the capability for, and an export that uses a capability the manifest never requested. Request the narrowest set that makes the mod work.
## Keep types in the loop
Generate TypeScript definitions from the host manifest and author against them. The types describe the real surface — the bindings you can call and the slots you can fill; if a call or a fill does not typecheck, it is not something the host declares. This closes the gap between what an author assumes exists and what the manifest actually declares.
----------------------------------------
# Hosting xript in an application
Source: https://xript.dev/guidance/hosting/
A host embeds a runtime, loads mods through it, and drives what they contribute. The split is the whole job: **the host provides primitives and decides policy; the runtime owns the sandbox, capability enforcement, sanitization, and resolution.** Mods are loaded *through* the runtime, never reached around it. This record is the umbrella; each hosting concept below has its own.
## The host / runtime split
- **The host provides** the bindings a mod may call, the grant decision (which capabilities are honored), and the places contributions mount. It renders inert output and routes interaction back into the sandbox.
- **The runtime owns** the sandbox, default-deny capability enforcement, fragment sanitization, hook firing, and slot/role resolution. It is the only thing that executes mod code.
The boundary is one-directional: a host drives the runtime and consumes what it returns. It never imports the runtime's internals to do the runtime's job — if a host finds itself wanting to, it is hosting the wrong unit (see [rendering fragments](/guidance/host-fragments/) for the canonical case).
## The lifecycle
1. **Initialize the factory.** `const xript = await initXript()` (or `initXriptAsync()`). The runtime factory is the only import a host needs from `@xriptjs/runtime`.
2. **Create a runtime per app manifest.** `xript.createRuntime(manifest, options)`.
3. **Load mods into it.** `runtime.loadMod(modManifest, { fragmentSources })` for each mod, returning a `ModInstance`.
4. **Drive it.** `invokeExport`, `fireHook`, `fireFragmentHook`, `resolveSlot`, `resolveRole` — the verbs each concept record covers.
5. **Dispose.** `runtime.dispose()` tears down the sandbox. A runtime is per app manifest; create one, load many mods, dispose when done.
## RuntimeOptions at a glance
`createRuntime(manifest, options)` takes:
- `hostBindings` — the functions and namespaces mods may call. The mod-to-host direction.
- `capabilities?` — the allow-list of capabilities this runtime grants. Default-deny: omitted means nothing. See [granting capabilities](/guidance/host-capabilities/).
- `console?` — where sandbox console output is routed.
- `audit?` — a callback fired on every gated binding call. See [limits, cancellation & audit](/guidance/host-safety/).
- `hardLimits?` — `timeout_ms`, `memory_mb`, `max_stack_depth`. The runtime enforces them.
- `cancellation?` — a `CancellationToken` for cooperative cancellation.
- `rolePreferences?` — preferred provider addon per role. See [resolving roles](/guidance/host-roles/).
- `debug?` — debug-protocol options.
## The host-side records
- [Hosting: rendering fragments](/guidance/host-fragments/) — the inert-output seam, and why you never call the processor directly.
- [Hosting: granting capabilities](/guidance/host-capabilities/) — default-deny, what granting means, and why the grant decision is the host's.
- [Hosting: mounting slots](/guidance/host-slots/) — `resolveSlot`, the `SlotContribution` shape, and honoring `priority` and `multiple`.
- [Hosting: resolving roles](/guidance/host-roles/) — `resolveRole`, the `RoleResolution` shape, and picking among providers.
- [Hosting: firing hooks & events](/guidance/host-hooks/) — `fireHook`, event-typed slots, and how they differ from the `events` catalog.
- [Hosting: limits, cancellation & audit](/guidance/host-safety/) — the caps the runtime enforces and the signals it hands back.
----------------------------------------
# Hosting: rendering fragments
Source: https://xript.dev/guidance/host-fragments/
A host renders a mod's UI by driving the runtime and applying what it returns. The split is the whole job: **the runtime does the processing; the host renders the runtime's inert output and routes interaction back in.** This is the host side of the [authoring](/guidance/authoring/) topic, and the canonical case of the [host/runtime boundary](/guidance/hosting/).
## The seam
There is one public entry point: the runtime factory. A host never reaches into the runtime's internals; it drives the runtime and renders what comes back.
1. **Initialize the factory.** `const xript = await initXript()` (or `initXriptAsync()` for the async sandbox). This is the only import a host needs from `@xriptjs/runtime`.
2. **Create a runtime per app manifest.** `xript.createRuntime(manifest, { hostBindings, capabilities, console })`. The runtime is the unit of hosting — it owns sanitization, binding resolution, conditional visibility, hooks, capability enforcement, and the sandbox.
3. **Load mods into it.** `runtime.loadMod(modManifest, { fragmentSources })` returns a `ModInstance` describing the fragments and exports the mod contributed.
4. **Render inert output.** Push host data in with `modInstance.updateBindings({ ... })`, which returns `{ fragmentId, html, visibility }` per fragment; fire lifecycle and update points with `runtime.fireFragmentHook(fragmentId, lifecycle, bindings)`, which returns a `FragmentOp[]` command buffer. The host applies that html, visibility, and op buffer to its UI — and nothing more.
5. **Route interaction back in.** When a rendered element fires a DOM event, the host hands the matching handler *declaration* (`{ selector, on, handler }`) to a dispatch callback that calls `runtime.invokeExport(handler, args)`. The fragment author's code runs in the sandbox, never in the page.
## The two directions
- **In (host → fragment):** `updateBindings` resolves `data-bind` values and `data-if` visibility; `fireFragmentHook` returns command-buffer ops. Both hand back inert data for the host to apply.
- **Out (fragment → host):** the renderer knows only a handler *name*. The host decides that name maps to a sandbox export and calls `invokeExport`. No authored logic executes in the page; that is the inert-fragment guarantee, enforced at the boundary.
## What you get is inert by contract
The runtime hands the host three things, all data: resolved `{ html, visibility }`, a `FragmentOp[]` command buffer, and handler *declarations*. The host applies the html, toggles visibility, runs the ops in order, and wires each declared handler to its dispatch callback. It honors the fill's [styling mode](/spec/fragments/#styling) (`inherit` / `isolated` / `scoped`) when it mounts. It does not branch on, compute from, or execute fragment content — that all already happened inside the sandbox.
## Do not reach past the boundary
The runtime's fragment processor and its helpers (`processFragment`, `createFragmentInstance`, `resolveBindings`) are **internal and deliberately not exported.** The sealed export surface is the design, not a gap. If you find yourself wanting to import the processor — or asking for it to be exported — you are trying to host the *processor* when the unit of hosting is the *runtime*. Load the mod and render `fireFragmentHook`'s ops instead; that is the supported seam, and it is the only one that keeps sanitization and the sandbox guarantee intact.
The reference host glue lives at `examples/svelte-fragment-renderer/` — `src/host/` drives the runtime, `src/lib/applyFragment.js` applies the inert output, and `src/host/dispatch.js` is the out-seam. Mirror it.
## Common mistakes
- **Vendoring a copy of the fragment processor into the host.** A copy is the host *reimplementing* the runtime, not hosting it. It drifts from the real sanitizer, binding, and visibility semantics, and it loses the sandbox guarantee entirely. Load the runtime; render its output.
- **Importing — or lobbying to export — an internal like `processFragment`.** The entry point is the runtime factory. The fragment seam is `fireFragmentHook` → `FragmentOp[]`. There is nothing else to import.
- **Mounting raw fragment html without applying the runtime's ops and visibility.** Raw markup is unresolved: you lose `data-bind` values, `data-if` toggles, and every command-buffer mutation. Apply the inert output the runtime returns.
- **Executing fragment markup or its handlers in the page.** The renderer only ever knows a handler *name*; the host maps it to `invokeExport`. Fragment content carries no logic of its own and must never run in the host context.
----------------------------------------
# Hosting: granting capabilities
Source: https://xript.dev/guidance/host-capabilities/
Capabilities are default-deny. A mod requests the ones it needs in its manifest; the host decides which to honor. **Granting is host policy; enforcement is runtime mechanism.** The runtime never grants on its own and never prompts — it enforces exactly the allow-list the host hands it.
## How a host grants
Pass the allow-list to the runtime: `createRuntime(manifest, { capabilities: ["clipboard.read", "net"] })`. That array is the complete set of capabilities this runtime honors. Omit it and the runtime grants nothing — a default-deny floor, not an oversight.
A mod's requested capabilities (from its manifest) are a *request*, not a grant. The host reads what the mod asks for, decides what it is willing to honor, and grants the intersection it chooses. Granting more than the mod requested is pointless; granting less than it requested means the mod's gated calls will fail loudly.
## What the runtime enforces
A binding gated by a capability the runtime was not granted throws `CapabilityDeniedError` when the mod calls it. A gated slot a mod fills without holding the gate fails [cross-validation](/guidance/authoring/) at load. The runtime is the only thing that checks; the host is the only thing that decides.
Every gated binding call is observable through the `audit` callback — see [limits, cancellation & audit](/guidance/host-safety/). Wire it if you want a record of which capabilities a mod actually exercised.
## Grant UX is host-side
The spec ships capability-grant *data shapes* only — `capability-prompt`, `install-descriptor`, `discovery-result` — and no prompt implementation. Whether to show a consent dialog, remember a decision, or grant silently from a trusted manifest is entirely the host's call. The runtime takes a finished allow-list; how the host arrived at it is out of scope. See the [security model](/spec/security/) and [capability reference](/spec/capabilities/).
## Common mistakes
- **Granting the union of everything every mod requests.** Grant the narrowest set you intend to honor. A blanket grant defeats default-deny.
- **Treating capabilities as mod-side enforcement.** The mod *declares* what it needs; the runtime *enforces*; the host *decides*. A mod cannot grant itself anything.
- **Expecting the runtime to prompt.** It will not. If a host wants consent UX, the host builds it and hands the runtime the resulting allow-list.
----------------------------------------
# Hosting: mounting slots
Source: https://xript.dev/guidance/host-slots/
A slot is a named, typed plug-point the host declares; a mod fills it. After loading mods, the host asks the runtime what filled each slot and mounts the result. **The host owns where and how a slot mounts; the runtime owns what is allowed to fill it.**
## Resolving fills
- `runtime.resolveSlot(slotId)` returns every `SlotContribution` filling that slot, ordered.
- `runtime.resolveSlotSingle(slotId)` returns the highest-priority contribution, or `null`.
A `SlotContribution` is `{ modName, fragmentId, slot, format, priority }`. The host reads it and mounts according to the slot's declared `accepts` type — a fragment-format fill gets rendered ([rendering fragments](/guidance/host-fragments/)), a role fill gets resolved ([resolving roles](/guidance/host-roles/)), an event fill gets fired ([firing hooks & events](/guidance/host-hooks/)). The `accepts` type is the whole contract for what the host does with the fill.
## Honor multiple and priority
A slot declares whether it allows `multiple` fills. For a single-fill slot, take `resolveSlotSingle`. For a multi-fill slot, take `resolveSlot` and mount each contribution in `priority` order. Dropping priority — or mounting only the first of many — is a host bug, not a runtime one; the runtime hands you the full ordered set and trusts you to honor it.
## Mod zero applies to the host's own UI
The strongest slot is one the host fills *as a mod*. If the application's own panels mount through `resolveSlot` like any third-party fill, the slot is exercised every run and stays honest. A slot only the host can fill privately is decoration. See [mod zero](/guidance/mod-zero/).
## Common mistakes
- **Hardcoding the host's own UI beside a slot instead of filling the slot.** That is the private-back-door failure mod zero exists to catch. Fill your own slots.
- **Ignoring `priority` or `multiple`.** Mount the full ordered set a multi-fill slot returns; do not assume one fill.
- **Branching on `format` the host never declared `accepts` for.** The slot's `accepts` type is the contract; a fill outside it would have failed validation at load.
----------------------------------------
# Hosting: resolving roles
Source: https://xript.dev/guidance/host-roles/
A role is a named set of functions a mod promises to provide — a transcriber, a formatter, a data provider. Cross-mod collaboration goes through roles, not hardcoded globals. The host resolves the active provider and calls its functions. **Declaring a role grants nothing; each named function stays gated by its own capability.**
## Resolving a provider
- `runtime.resolveRole(role)` returns the active `RoleResolution`, or `null` when no mod provides it.
- `runtime.resolveRoleAll(role)` returns every provider, for when the host wants to fan out rather than pick one.
A `RoleResolution` is `{ addon, role, fns }`, where `fns` maps each role-function name to the provider mod's export name. The host calls them through `runtime.invokeExport(fns[name], args)` — it never assumes a global of that name exists.
## Picking among providers
When more than one mod provides a role, the host chooses. Set `rolePreferences` on `createRuntime` (`{ "transcriber": "my-whisper-addon" }`) to prefer a named provider per role; `resolveRole` honors it, falling back to the highest-priority provider otherwise. Use `resolveRoleAll` when every provider should run.
## Roles grant nothing on their own
Providing a role is not a capability. A role function that reaches a gated binding still needs that capability granted to its mod ([granting capabilities](/guidance/host-capabilities/)). Resolving a role tells the host *who* provides it and *what to call*; it never widens what those functions may do.
## Common mistakes
- **Assuming a provider exists.** `resolveRole` returns `null` when nothing provides the role. Handle absence; do not call into a null resolution.
- **Calling role functions by their role name.** Call the *export* name from `fns`, through `invokeExport`. The role name is a label, not a global.
- **Treating a role as a grant.** A provider's functions are still gated by the capabilities its mod was granted.
----------------------------------------
# Hosting: firing hooks & events
Source: https://xript.dev/guidance/host-hooks/
A hook is an extension point the host *fires*; every mod that filled it runs. This is how a host lets mods react to lifecycle moments and state changes. **Firing a hook calls its fills; it is not the same as the `events` catalog, which only declares what the host broadcasts.**
## Firing a hook
`runtime.fireHook(hookName, { phase, data })` fires the named hook and returns the array of fill return values. `phase` and `data` are both optional — `data` is the payload handlers receive, `phase` names a sub-stage when a hook has more than one.
Hooks are modeled as **event-typed slots**: the host declares a slot whose `accepts` type is `application/x-xript-hook`, a mod fills it with a handler export, and firing the slot calls every fill. Resolving and firing go together — see [mounting slots](/guidance/host-slots/). There is no separate top-level "hook" primitive; a hook is an event slot the host fires.
## Hooks vs the events catalog
Two surfaces share the word "event" and point opposite directions:
- **An event-typed slot** is a plug-point a mod *fills* with a handler; the host *fires* it with `fireHook`, and the fills run. Use it when you want mods to *respond*.
- **The `events` catalog** (top-level `events` in the host manifest) declares *what the host emits* — a discovery list of named broadcasts and their payload types. Declaring an event wires up no listener and grants nothing; it only tells observers what the application broadcasts. See [choosing a surface](/guidance/surfaces/).
If you want mods to react to something, you need a slot to fire. If you only want to publish that something happened, audience open, declare it in the catalog. The two often pair.
## Fragment lifecycle is its own hook
Fragments have a dedicated firing path: `runtime.fireFragmentHook(fragmentId, lifecycle, bindings)` returns a `FragmentOp[]` command buffer the host applies. See [rendering fragments](/guidance/host-fragments/).
## Common mistakes
- **Conflating the `events` catalog with an event slot.** The catalog announces; a slot is fired. Listing an event grants no listener.
- **Expecting `fireHook` to do something with no fills.** It returns an empty array. A hook does nothing until a mod fills its slot.
- **Reaching for a top-level `hooks` field.** A standalone `hooks` block is deprecated; model a hook as an event-typed slot.
----------------------------------------
# Hosting: limits, cancellation & audit
Source: https://xript.dev/guidance/host-safety/
A host runs untrusted code. The runtime gives it the caps to bound that code and the signals to watch it. **The runtime provides the mechanism; the host sets the policy** — what the limits are, when to cancel, what to do with an audit trail.
## Hard limits
Set `hardLimits` on `createRuntime`:
- `timeout_ms` — wall-clock ceiling for a single execution.
- `memory_mb` — sandbox memory ceiling.
- `max_stack_depth` — recursion ceiling.
Exceeding any of them throws `ExecutionLimitError`. Run without a timeout and a mod can hang the host; set one. Limits are per runtime, enforced by the runtime — the host does not police them by hand.
## Cooperative cancellation
Pass a `CancellationToken` as `cancellation`. Call `token.cancel()` to request a stop; long-running sandbox work observes the flag and throws `CancellationError`. Cancellation is *cooperative*, not preemptive — it unwinds at the runtime's check points, not instantly. Some engines require the async sandbox (`initXriptAsync`) for cancellation to bite mid-execution; see the [runtime overview](/runtimes/overview/) for per-engine fidelity.
## Audit
Pass an `audit` callback: `(event: AuditEvent) => void`. The runtime fires it on every gated binding call, with `{ binding, capability, at }` — which binding ran, which capability gated it, and when. Wire it to the host's logging to keep a record of what a mod actually exercised. Pair it with [granting capabilities](/guidance/host-capabilities/): grants are what you *allowed*, audit is what was *used*.
## Console
Route sandbox console output through the `console` handler — `log`, `info`, `warn`, `error`, `debug`, `trace`, or a single `onLog(severity, ...args)`. Without it, a mod's console output goes nowhere.
## Common mistakes
- **No timeout.** A mod with an infinite loop hangs the host. Always set `timeout_ms`.
- **Assuming cancellation is preemptive.** It is cooperative; it unwinds at check points. For mid-execution cancellation on some engines, use the async sandbox.
- **Ignoring the audit channel.** Without it you have grants but no record of use — half the security story.
----------------------------------------
# Adoption tiers
Source: https://xript.dev/guidance/tiers/
xript is adopted incrementally. A host does not have to expose everything at once; it picks the tier that matches what it needs today and grows later. Each tier is a superset of the one before.
## Tier 1 — expressions
The host evaluates user-supplied expressions in a sandbox. No bindings, no host calls — just safe evaluation of values. Use when the extensibility you need is "let users write a formula" and nothing more.
## Tier 2 — simple bindings
The host exposes a set of bindings and lets mods call them. Mods are scripts that read host state and invoke host actions through the declared surface. Use when mods need to *do* things in the host but do not yet contribute UI.
## Tier 3 — advanced scripting
Full scripting with hooks, capabilities, and lifecycle. Mods react to host events, request gated capabilities, and carry real behavior. Use when mods are first-class participants in how the application behaves.
## Tier 4 — full feature
Everything, including UI contribution: slots, fragments, contributions, and the fragment protocol. Mods add and replace presentation, not just behavior. Use when the application is fully moddable and its own content is authored as mod zero.
## Choosing a tier
Pick the lowest tier that covers what mods genuinely need now. Adding a higher tier later is additive — new bindings, hooks, slots, and capabilities extend the manifest without breaking existing mods. Do not expose tier 4 surfaces for a host whose mods only need tier 2; do not cap a host at tier 2 when its mods clearly want to contribute UI. The manifest grows with the need.
----------------------------------------
# Manifest Specification
Source: https://xript.dev/spec/manifest/
The xript manifest is the single source of truth for an application's scripting API. It declares what functionality is exposed to scripts, how it is organized, what capabilities gate access, and what types are involved. From the manifest, everything else is derived: documentation, TypeScript definitions, validation, and interactive playgrounds.
## Overview
A manifest is a JSON file conforming to the [manifest JSON Schema](https://github.com/nekoyoubi/xript/blob/main/spec/manifest.schema.json). At minimum, a manifest declares a spec version and a name:
```json
{
"xript": "0.1",
"name": "my-app"
}
```
Complexity layers on only as needed. Every field beyond `xript` and `name` is optional, and each section you add enables more functionality.
## Top-Level Fields
### `$schema`
The schema this manifest conforms to. Optional; when present, tooling validates against it rather than always assuming bundled core, which lets a domain extend the vocabulary with an overlay. See [Schema and domain overlays](#schema-and-domain-overlays).
### `xript` (required)
The specification version this manifest conforms to. This is not the application's version; it's the version of the xript spec the manifest was written against.
Format: `major.minor` (e.g., `"0.1"`).
### `name` (required)
A machine-readable identifier for the application. Used in generated package names, documentation URLs, and tooling output.
Constraints: lowercase letters, numbers, and hyphens. Must start with a letter. Maximum 64 characters.
### `version`
The version of the application's scripting API, following semver. Tracks how the exposed bindings evolve over time.
### `title`
A human-readable display name. Used in documentation headers and UI.
### `description`
A brief description aimed at extenders. Used in documentation landing pages and registry listings.
### `extends`
One or more base manifests to inherit from. The bases get resolved and deep-merged before validation, so a host can build on a shared foundation and add, fill, or refine only what differs:
```json
{
"xript": "0.6",
"name": "extended-host",
"extends": "./base.manifest.json"
}
```
`extends` takes a single path or an array of paths, resolved before schema validation. Maps (`bindings`, `capabilities`, `hooks`, `types`) key-merge, `slots` append keyed by `id`, and scalars are child-wins. Paths are filesystem-relative to the manifest, and resolution is transitive with cycle detection. A name that appears in both base and child resolves by one of three moves (**add**, **fill**, or **refine**); an un-opted concrete-name collision is an error. See [Manifest inheritance](#manifest-inheritance-extends).
### `events`
A catalog of the named events the host broadcasts and their payload types. Optional. See [Events](#events).
## Bindings
Bindings define the functions and namespaces that scripts can call.
### Function Bindings
A function binding declares a callable function:
```json
{
"bindings": {
"getHealth": {
"description": "Returns the player's current health points.",
"returns": "number"
}
}
}
```
Every function binding requires a `description`. Optional fields include `params`, `returns`, `async`, `capability`, `examples`, and `deprecated`.
### Namespace Bindings
Namespaces group related functions using the `members` field:
```json
{
"bindings": {
"player": {
"description": "Functions related to the player character.",
"members": {
"getHealth": {
"description": "Returns the player's current health points.",
"returns": "number"
},
"setHealth": {
"description": "Sets the player's health points.",
"params": [
{ "name": "value", "type": "number", "description": "The new health value." }
]
}
}
}
}
}
```
Namespaces can nest, but deep nesting is discouraged. Two levels is usually plenty.
## Slots
A host declares a surface of named, typed plug-points. Bindings are callables the host implements and the mod *calls*; slots are typed points the host declares and the mod *fills*. Everything a mod contributes is a slot fill — a fragment, a provider role, a lifecycle-event handler are all fills of slots of a particular type.
```json
{
"slots": [
{
"id": "sidebar.left",
"accepts": ["text/html+jsml"],
"capability": "ui-mount",
"multiple": true,
"style": "isolated"
}
]
}
```
A slot's `accepts` type names the format(s) or kind the slot takes and governs what a valid fill looks like and what the host does with it: mount it, call it, resolve it, or fire it. Representative `accepts` values: `"text/html+jsml"` (an inert fragment), `"application/javascript+esm"` (a code-backed renderer), `"application/json"`, `"application/x-xript-role"` (a provider role), `"application/x-xript-hook"` (an event handler).
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `id` | string | yes | — | Unique slot identifier (`^[a-z][a-z0-9.-]*$`) |
| `accepts` | string[] | yes | — | The format(s)/kind this slot takes |
| `description` | string | no | — | What the slot is for; surfaced in docs |
| `capability` | string | no | — | Capability a mod must hold to fill it |
| `multiple` | boolean | no | false | Allow more than one fill |
| `payload` | object | no | — | A JSON Schema each fill's payload must satisfy |
| `reserved` | boolean | no | false | Aspirational slot; never flagged unfilled, excluded from coverage |
| `refines` | boolean | no | false | Deep-merge onto a base slot of the same `id` (see [inheritance](#manifest-inheritance-extends)) |
| `style` | enum | no | `"inherit"` | Styling mode (fragment-format slots) |
A slot's `payload` carries a full JSON Schema (draft 2020-12), not a flat field list — the host validates each fill's payload against it. `cross-validate` checks fills against the target slot's payload schema by default (`--no-fill-payloads` / `checkFillPayloads` flexes it off; extras pass unless the slot closes its payload). A `reserved` slot is aspirational surface: declared without a current filler, never reported as dead, and excluded from coverage denominators.
Mods engage slots through the `fills` surface in their [mod manifest](/spec/mod-manifest/). See [Fragments](/spec/fragments/) for the fragment-format slot type in depth.
### Role slots and resolution
A role slot (`accepts: ["application/x-xript-role"]`) is a host-declared plug-point any mod can fill. Rather than core UI hardcoding a mod-specific global function name, the host declares the role slot, a mod fills it with a logical-to-concrete `fns` map (in its `fills` surface), and the host asks the runtime to resolve it via `resolve_role` / `resolveRole` / `ResolveRole` (and the `*_all` variants). Resolution is pure data lookup over loaded mods in load order; it returns `{ addon, role, fns }`, never calls the resolved functions, and grants no capability. The functions stay gated by their own capabilities, and an unfilled role slot resolves cleanly to `null`/`None`. See the [mod manifest](/spec/mod-manifest/) for the fill shape.
## Capabilities
Capabilities implement the default-deny security model. Every capability is a named permission that must be explicitly granted before scripts can use the functionality it protects.
```json
{
"capabilities": {
"filesystem": {
"description": "Read and write files in the mod's data directory.",
"risk": "medium"
}
}
}
```
Functions reference capabilities via the `capability` field. Functions without a `capability` are always available.
The `risk` field (`low`, `medium`, `high`) is advisory; it gives users a signal when deciding what to grant.
A capability may carry `"reserved": true` to mark it as aspirational: declared for canon parity or a future surface without yet gating anything. Tooling treats a reserved capability as intentional rather than vestigial and suppresses the unreferenced-capability warning. A capability that gates a binding or hook counts as used.
## Events
The `events` array is a discovery declaration of the named events the **host emits** and the shape of each one's payload. It is consumer-agnostic: it says what the host broadcasts, not who listens. Sandbox scripts, the host's own UI, and external subscribers are all equally valid audiences — the catalog presupposes none of them.
```json
{
"events": [
{
"id": "player.died",
"description": "Fired when the player's health reaches zero.",
"payload": "DeathContext"
},
{
"id": "level.loaded",
"description": "Fired after a new level finishes loading."
}
]
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | yes | The event name the host broadcasts under |
| `description` | string | yes | What the event means and when it fires |
| `payload` | type reference | no | The shape of the data delivered with the event |
`typegen` emits a typed event catalog from this array, and `docgen` renders an events section, so an author knows exactly what a host emits and what each payload carries.
### Events versus its neighbors
Three surfaces sit close enough to confuse. The line between them:
- **Event-typed slots** (a slot whose `accepts` is `application/x-xript-hook`) are extension *points* — places a mod fills with a handler the host calls.
- **Fragment `handlers`** are DOM responses wired on a fragment fill — what runs when the user clicks something in mounted UI.
- **`events`** (this surface) is what the *host emits* — a declaration of broadcasts, with no consumer presupposed.
One line: bindings are *what you can call*, slots and handlers are *what handles*, and `events` is *what the host emits*.
## Hooks
:::caution[Deprecated as a standalone concept]
A lifecycle hook is a slot whose `accepts` is the event-handler type (`application/x-xript-hook`), and firing it means calling that slot's fills. The standalone `hooks` field remains allowed for back-compat, but new hosts should declare event-typed slots instead. Host-side hook firing is unchanged. See [Hooks](/spec/hooks/).
:::
## Types
Custom types describe complex data structures used in bindings.
### Object Types
```json
{
"types": {
"Position": {
"description": "A 2D position in world coordinates.",
"fields": {
"x": { "type": "number", "description": "Horizontal position." },
"y": { "type": "number", "description": "Vertical position." }
}
}
}
}
```
An object type can be marked `"abstract": true` to declare a contract hole: described but unpopulated, with no `fields` of its own, left for an extending manifest to fill. See [Manifest inheritance](#manifest-inheritance-extends).
### Enum Types
```json
{
"types": {
"Direction": {
"description": "A cardinal direction.",
"values": ["north", "south", "east", "west"]
}
}
}
```
#### Open Enums
Adding `"open": true` to a type's `values` (or to a field's inline `enum`) declares an *open* enum — the listed values are the known set, but any other string is also valid:
```json
{
"types": {
"Severity": {
"description": "A diagnostic severity. Hosts and addons may contribute their own.",
"values": ["info", "warn", "error"],
"open": true
}
}
}
```
`typegen` emits `... | (string & {})` so unlisted (for example, addon-contributed) values still type-check, and `docgen` marks the type as extensible. xript does not enforce the closed set at runtime regardless of `open`.
An enum type may also be abstract: a `description` with `"abstract": true` and no `values`, leaving the concrete `values` for an extending manifest to fill.
### Record Fields
Object-type fields can carry a `default` value and an inline `enum` of allowed values. That's enough for a mod to describe an owned record type, a structured value it manages, entirely through the `types` surface; no new persistence concept required. `typegen` emits typed accessors for these, and the runtimes stay persistence-agnostic: xript describes the shape, the host decides where it lives.
```json
{
"types": {
"QuestState": {
"description": "A tracked quest.",
"fields": {
"title": { "type": "string" },
"stage": { "type": "number", "default": 0 },
"status": { "type": "string", "enum": ["active", "done", "failed"], "default": "active" }
}
}
}
}
```
### Type References
Anywhere a type is expected, you can use:
- **Primitives**: `"string"`, `"number"`, `"boolean"`, `"void"`, `"null"`
- **Custom types**: `"Position"`, `"Direction"`
- **Array shorthand**: `"string[]"`, `"Position[]"`
- **Complex expressions**: `{ "array": "Position" }`, `{ "union": ["string", "number"] }`, `{ "map": "number" }`, `{ "optional": "string" }`
## Execution Limits
Default bounds for script execution:
```json
{
"limits": {
"timeout_ms": 5000,
"memory_mb": 64,
"max_stack_depth": 256
}
}
```
These are defaults that runtimes enforce unless the host application overrides them.
## Adoption Tiers
The manifest supports four adoption tiers through progressive complexity.
### Tier 1: Expressions Only
```json
{
"xript": "0.1",
"name": "calculator"
}
```
Safe eval replacement. No bindings, no capabilities.
### Tier 2: Simple Bindings
```json
{
"xript": "0.1",
"name": "my-game",
"version": "1.0.0",
"bindings": {
"getPlayerName": {
"description": "Returns the current player's display name.",
"returns": "string"
},
"getHealth": {
"description": "Returns the player's current health (0-100).",
"returns": "number"
}
}
}
```
A few functions, no capabilities needed.
### Tier 3: Advanced Scripting
Namespaces, capabilities, custom types, examples, and execution limits. See the [Game Mod System](/examples/game-mod-system) example walkthrough for a complete tier 3 manifest.
### Tier 4: Full Feature
Slots, mod manifests, and fills. The host declares typed plug-points; mods fill them with fragments that bind to host state and handle interaction, with provider roles, and with lifecycle-event handlers. See the [UI Dashboard](/examples/ui-dashboard) example for a complete tier 4 integration.
## Manifest inheritance (`extends`)
A manifest may build on one or more base manifests via the top-level [`extends`](#extends) field. A **base manifest** (sometimes called *canon*) declares the bindings, capabilities, slots, and types a family of hosts holds in common; each extending manifest builds on that floor. Resolution happens before schema validation, flattening base-then-child into a single schema-valid manifest the runtime never sees `extends` on.
### Abstract types
A type carrying `"abstract": true` is declared and described but unpopulated; it supplies neither `fields` nor `values`, standing as a typed contract hole. The base may reference it from concrete surface (a binding return type, a slot's payload schema, another type's field) without committing to its shape, and each extending manifest decides what fills it.
### The three moves: add, fill, refine
When an extending manifest declares a name, exactly one of three moves applies:
- **Add** — a name the base never declared. Purely additive, no marker. Canon is a shared floor, never a cage.
- **Fill** — redeclaring an *abstract* base type with concrete `fields` and/or `values`. Allowed without a marker; the base being abstract is the opt-in signal. The concrete definition replaces the abstract stub.
- **Refine** — redeclaring a *concrete* base type (or slot) with `"refines": true`. The child deep-merges onto the base: child members win key-by-key, and base members the child omits are retained. Slots refine the same way, including their `payload` JSON Schema.
```json
{
"extends": "./base.json",
"name": "consuming-host",
"types": {
"StatusCode": { "description": "Codes this host recognizes.", "values": ["ok", "retry", "error"] },
"Envelope": {
"refines": true,
"fields": { "traceId": { "type": "string", "description": "Correlation id." } }
}
}
}
```
Any other concrete-name collision is a resolution error: redeclaring a concrete type, slot `id`, binding, capability, or hook without the right move (or a cross-base collision in an `extends` array) fails at resolution time, before validation, and cannot be suppressed. An inherited abstract type left unfilled is an `abstract-type-unfilled` error; a locally-declared abstract type (for one's own extenders to fill) is not flagged. Filling or refining inherited surface counts as legitimate use; it never trips dead-slot or vestigial-capability findings. The model is at parity across all four runtimes. See [`spec/extends.md`](https://github.com/nekoyoubi/xript/blob/main/spec/extends.md) for the normative reference.
## Schema and domain overlays
The full JSON Schema is available at [`spec/manifest.schema.json`](https://github.com/nekoyoubi/xript/blob/main/spec/manifest.schema.json). Core xript defines a fixed top-level vocabulary, but a domain can extend it.
### `$schema`
A manifest may name the schema it conforms to with a top-level `$schema` field, the way any JSON document does:
```json
{
"$schema": "https://xript.dev/schema/manifest/v0.6.json",
"xript": "0.6",
"name": "my-app"
}
```
When `$schema` is present, tooling validates against the schema it names rather than always assuming bundled core. Resolution leans open by default: a recognized schema id resolves to its bundled local copy (core's own URI resolves to bundled core); a local path resolves relative to the manifest, the same way `extends` does; and an `http(s)` URL is fetched and cached, keyed by URL, so a repeat validation reuses the cached copy and a run pins the schema it resolved for reproducibility. If the schema can't be reached (offline, or an uncached remote), tooling falls back to bundled core and surfaces a warning rather than hard-failing. Remote resolution is allowed unless a host explicitly restricts it (an allowlist, or disabling remote schemas); you opt out of openness, not into it.
Honoring a declared schema grants no new power. Schema validation is not xript's security boundary; the [capability model](/spec/capabilities/) is. A manifest naming its own schema can describe a richer vocabulary, but it can't reach past its capabilities. The real concerns are staying usable offline, keeping a validation reproducible, and fetching safely; the cache, the pin, the bundled fallback, and the optional restriction cover those.
### Extending the vocabulary with an overlay
A domain can add its own top-level manifest properties by layering a schema *overlay* on top of core:
```json
{
"allOf": [
{ "$ref": "https://xript.dev/schema/manifest/v0.6.json" },
{
"type": "object",
"properties": {
"myDomain": { "type": "object" }
}
}
]
}
```
Core's top-level object is open to this: it constrains the properties it knows but does not reject unknown top-level properties an overlay introduces, so a manifest validated against the overlay above can carry both core surfaces and `myDomain` and still pass. The openness stops at the top level by design; nested objects (bindings, slots, types, and the rest) stay closed, so a typo inside a known surface is still caught. A domain that needs more vocabulary adds it at the top with an overlay; it does not fork core.
----------------------------------------
# Mod Manifest
Source: https://xript.dev/spec/mod-manifest/
The mod manifest is a JSON file that declares what a mod provides and what it needs from the host application. It is distinct from the [app manifest](/spec/manifest/). The app manifest declares a surface of typed [slots](/spec/manifest/#slots); the mod manifest declares the **fills** that engage them.
## Overview
A mod manifest declares metadata, required capabilities, a script entry point, and the slot fills it contributes. At minimum, it requires a spec version, name, and version:
```json
{
"xript": "0.6",
"name": "health-panel",
"version": "1.0.0"
}
```
The full schema lives at [`spec/mod-manifest.schema.json`](https://github.com/nekoyoubi/xript/blob/main/spec/mod-manifest.schema.json).
## Required Fields
### `xript`
The xript specification version this mod targets. Format: `major.minor` (e.g., `"0.6"`).
### `name`
Machine-readable mod identifier. Same constraints as the app manifest: lowercase letters, numbers, and hyphens; starts with a letter; max 64 characters.
### `version`
The mod's version, following [semver](https://semver.org/) (e.g., `"1.0.0"`, `"0.3.0-beta.1"`).
## Optional Fields
### `title`
Human-readable display name (max 128 characters).
### `description`
Brief description of what the mod does (max 1024 characters).
### `author`
The mod author's name or handle (max 128 characters).
### `license`
The mod's license: an SPDX identifier or a short label (max 128 characters).
```json
{ "license": "MIT" }
```
### `extends`
One or more base mod manifests to inherit from. Resolved and deep-merged base-then-child before validation, transitively, with cycle detection. Paths are filesystem-relative to this manifest.
```json
{ "extends": "./base.mod.json" }
{ "extends": ["./base.mod.json", "./theme.mod.json"] }
```
Merge rules: maps key-merge, arrays append, scalars are child-wins, and duplicate ids are an error. This is the same inheritance model the [app manifest](/spec/manifest/#extends) uses.
### `capabilities`
An array of capability names the mod requires from the host:
```json
{
"capabilities": ["ui-mount", "modify-player"]
}
```
The host grants or denies these when loading the mod. If a required capability isn't granted, gated operations fail with `CapabilityDeniedError`.
### `entry`
Script entry point(s) relative to the mod root. The simple form is a single string or an array:
```json
{ "entry": "src/mod.js" }
{ "entry": ["src/setup.js", "src/handlers.js"] }
```
The richer object form names a primary `script`, an execution `format`, and the named `exports` the host can invoke:
```json
{
"entry": {
"script": "src/mod.js",
"format": "module",
"exports": {
"transcribe": { "description": "Transcribe a value.", "params": [{ "name": "value", "type": "string" }], "returns": "string" }
}
}
}
```
With `format: "module"` the entry evaluates as an ES module and its top-level named exports become host-invokable automatically; see [Module-Format Mods](/spec/modules/) for the full rules. Entry scripts run in the host's sandbox when the mod loads: they register fill handlers, register fragment lifecycle callbacks, and set up mod state.
### `family`
An optional grouping family for host-side organization, like collecting related mods into one navigation rail. When absent, hosts fall back to name-prefix heuristics.
```json
{ "family": "inventory-tools" }
```
### `fills`
The canonical contribution surface. `fills` is an object keyed by host slot id; each value is an array of fill entries that engage that slot:
```json
{
"fills": {
"sidebar.left": [
{
"format": "text/html+jsml",
"source": "fragments/panel.html",
"bindings": [
{ "name": "health", "path": "player.health.val" }
],
"handlers": [
{ "selector": "[data-action='heal']", "on": "click", "handler": "onHealClicked" }
]
}
]
}
}
```
A fill's inner shape is governed by the target slot's `accepts` type; the host owns that contract. Representative shapes:
| Slot kind (`accepts`) | Fill shape | The host does |
|-----------------------|------------|----------------|
| fragment format (`text/html+jsml`) | `{ "format", "source", "bindings", "handlers" }` | mounts the inert fragment |
| code renderer (`application/javascript+esm`) | `{ "kind", "entry", "label", "icon" }` | invokes the entry to paint |
| role (`application/x-xript-role`) | `{ "fns": { "transcribe": "transcribeAudio" } }` | resolves a logical role to concrete exports |
| event/hook (`application/x-xript-hook`) | `{ "handler": "onStartup" }` | fires the slot, calling the handler |
A **fragment** is a fill of a fragment-format slot; the [fragment protocol](/spec/fragments/) governs that slot type in full. A **provider role** is a fill of a role-type slot. The host calls `resolve_role(role)` to map a logical role to a providing mod and its `fns` (first-installed-wins, settings-overridable), or `resolve_role_all` to build its own picker; declaring a role grants nothing, and the named functions stay gated by their own capabilities. A **lifecycle hook handler** is a fill of an event-typed slot; firing the slot calls the handler.
A fill into a slot the host never declared, or into a gated slot the mod lacks the capability for, is a validation error. The inner fill shape is not policed by the validator; that contract belongs to the slot's `accepts` type.
:::note[Legacy `fragments` and `contributions`]
The older top-level `fragments` array and `contributions` object (`provides` + `slots`) are retired in favor of `fills`. A `fragments[]` entry is a fill of a fragment-format slot; `contributions.provides` is a fill of a role-type slot. The validator still accepts both for migration smoothness and emits a deprecation warning. New mods should write only `fills`.
:::
:::note[A fragment fill's `handlers` field, and the deprecated `events` alias]
A fragment fill's DOM event handler array is named `handlers`. It was previously called `events`, which misnamed handlers as events; a reader still accepts `events` as a deprecated alias (if both are present, `handlers` wins) and warns on it. Rename the key to migrate; the entry shape (`selector`, `on`, `handler`) is unchanged. Not to be confused with the host manifest's top-level [`events` catalog](/spec/manifest/#events), which declares what the host broadcasts.
:::
## Example
```json
{
"$schema": "https://xript.dev/schema/mod-manifest/v0.6.json",
"xript": "0.6",
"name": "health-panel",
"version": "1.0.0",
"title": "Health Panel",
"description": "Displays a health bar with low-health warnings.",
"author": "modder",
"license": "MIT",
"capabilities": ["ui-mount"],
"entry": "src/mod.js",
"fills": {
"sidebar.left": [
{
"format": "text/html+jsml",
"source": "fragments/panel.html",
"bindings": [
{ "name": "health", "path": "player.health.val" },
{ "name": "maxHealth", "path": "player.health.max" }
],
"handlers": [
{ "selector": "[data-action='heal']", "on": "click", "handler": "onHealClicked" }
],
"priority": 10
}
]
}
}
```
## Validation
Use the validator to check mod manifests:
```bash
npx xript validate mod-manifest.json
```
The validator auto-detects whether a file is an app manifest or a mod manifest based on the presence of `fills`, `entry`, or the legacy `fragments` field.
For cross-validation (checking that a mod's fills target valid slots and hold the capabilities those slots gate):
```bash
npx xript validate --cross manifest.json mod-manifest.json
```
Cross-validation also checks each fill's payload against the target slot's `payload` schema (on by default; pass `--no-fill-payloads` to flex it off). A fill carrying more than the payload declares still passes unless the slot explicitly closes its payload.
For a heuristic review of the same host/mod pair (dead slots, vestigial capabilities, ungated surfaces), reach for [`xript lint`](/tools/lint/).
----------------------------------------
# Fragment Protocol
Source: https://xript.dev/spec/fragments/
The fragment protocol governs the **fragment-format slot** — a [slot](/spec/manifest/#slots) whose `accepts` type is a fragment format like `text/html+jsml`. The host declares the slot; a mod fills it with inert UI. The runtime sanitizes fragment content, resolves data bindings, evaluates conditional visibility, and routes events through the sandbox. A fragment is a fill of this slot type; everything below describes that fill's shape and lifecycle.
## Slots
Hosts declare slots in their app manifest. A fragment-format slot is a named mounting point whose `accepts` type names a fragment format, with optional capability gating.
```json
{
"slots": [
{
"id": "sidebar.left",
"accepts": ["text/html+jsml"],
"capability": "ui-mount",
"multiple": true,
"style": "isolated"
}
]
}
```
### Slot Fields
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `id` | string | yes | — | Unique slot identifier (e.g. `sidebar.left`) |
| `accepts` | string[] | yes | — | Fragment format(s) this slot accepts |
| `capability` | string | no | — | Capability required to mount here |
| `multiple` | boolean | no | false | Allow multiple mod fragments |
| `style` | enum | no | `"inherit"` | Styling mode |
### Styling Modes
- **`inherit`** — fragment inherits host styles; good for inline UI like status bars
- **`isolated`** — no host styles bleed in; good for panels and overlays
- **`scoped`** — host exposes CSS custom properties; the fragment uses them
## Fragment Fills
Mods fill a fragment-format slot through the `fills` surface of their [mod manifest](/spec/mod-manifest/), keyed by the slot id. Each fill provides markup and optionally declares data bindings and event handlers.
```json
{
"fills": {
"sidebar.left": [
{
"format": "text/html+jsml",
"source": "fragments/panel.html",
"bindings": [
{ "name": "health", "path": "player.health.val" },
{ "name": "maxHealth", "path": "player.health.max" }
],
"handlers": [
{ "selector": "[data-action='heal']", "on": "click", "handler": "onHealClicked" }
],
"priority": 10
}
]
}
}
```
### Inline Fragments (JSML)
For simple fragments, inline the markup:
```json
{
"fills": {
"header.status": [
{
"format": "text/html+jsml",
"source": "0 / 0",
"inline": true,
"bindings": [
{ "name": "health", "path": "player.health.val" },
{ "name": "maxHealth", "path": "player.health.max" }
]
}
]
}
}
```
## Data Binding: `data-bind`
The `data-bind` attribute wires host data into fragment markup. The runtime finds elements with `data-bind=""` and sets their content to the resolved binding value.
```html
Health: 0/0
```
Attributes persist in the DOM, so updates stay O(1). That holds 60fps game-loop speed without re-parsing the template every frame.
For text elements: sets `textContent`. For input elements: sets `value`.
## Conditional Visibility: `data-if`
The `data-if` attribute evaluates an expression against the binding context to control element visibility.
```html
You're hurting!
Get to a healer!
```
Expressions use the same safe evaluator that powers tier 1. Truthy = visible, falsy = hidden. Re-evaluates on binding change, skips when the boolean result hasn't changed.
### The Hard Wall
`data-bind` and `data-if` are the only two "smart" attributes the spec defines. No `data-each`, no `data-else`, no template language. Everything beyond binding and conditional visibility goes through the sandbox fragment API.
## Event Handlers
DOM event handlers are declared in the manifest, not the markup. The runtime attaches listeners and delegates to sandbox functions. Each entry pairs a `selector`, the DOM event to listen for (`on`), and the sandbox `handler` to call.
```json
"handlers": [
{ "selector": "[data-action='heal']", "on": "click", "handler": "onHealClicked" }
]
```
Multi-match is intentional: three `[data-action='heal']` buttons all wire to the same handler.
:::note[The field is `handlers` — `events` is a deprecated alias]
This array used to be called `events`, but its entries are event *handlers*, not events; the old name was simply wrong. The field is now `handlers`. Readers still accept `events` as a deprecated alias for back-compat, mirroring the standalone-`hooks` to event-slot precedent. If a fill carries both, `handlers` wins; if it carries only `events`, that is honored with a deprecation warning. New fills should use `handlers`. Migrate by renaming the key; the entry shape is unchanged.
This is distinct from the top-level [`events` catalog](/spec/manifest/#events), which declares what the *host* broadcasts. A fragment's `handlers` are DOM responses wired on a fill; the catalog is a discovery declaration of host-emitted events. Same word, opposite directions.
:::
## Sandbox Fragment API
For logic beyond `data-bind` and `data-if`, mods use the sandbox fragment API with the command buffer pattern:
```javascript
hooks.fragment.update("health-panel", function (bindings, fragment) {
fragment.toggle(".warning", bindings.health < 50);
fragment.addClass(".bar", bindings.health < 20 ? "critical" : "normal");
fragment.replaceChildren(".inventory-list",
bindings.inventory.map(item => "" + item.name + "")
);
});
```
The `fragment` proxy accumulates operations; the host applies them after the callback returns. No direct DOM access.
### Available Operations
| Method | Effect |
|--------|--------|
| `toggle(selector, condition)` | Show/hide matching elements |
| `addClass(selector, className)` | Add class to matching elements |
| `removeClass(selector, className)` | Remove class |
| `setText(selector, text)` | Set text content |
| `setAttr(selector, attr, value)` | Set attribute |
| `replaceChildren(selector, html)` | Replace children |
### Lifecycle Hooks
```javascript
hooks.fragment.mount("panel", (fragment) => { /* inserted into slot */ });
hooks.fragment.unmount("panel", (fragment) => { /* removed from slot */ });
hooks.fragment.update("panel", (bindings, fragment) => { /* data changed */ });
hooks.fragment.suspend("panel", (fragment) => { /* temporarily inactive */ });
hooks.fragment.resume("panel", (fragment) => { /* reactivated */ });
```
## HTML Sanitization
For `text/html` fragments, the runtime sanitizes content before the host sees it. The guarantee: what you're mounting is inert.
**Preserved:** structural/presentational elements, `class`, `id`, `data-*`, `aria-*`, `role`, scoped `