# 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.7", "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. :::tip[Hosting or modding?] xript has two sides. **Hosting** means your application embeds xript so others can extend it. That's this page. **Modding** means writing an extension for an app that already speaks xript; start at [Your first mod](/mods/first-mod/) instead. ::: ## Before You Start - **Node.js 20 or newer** (or a browser project; the universal runtime runs anywhere JavaScript does). Check with `node --version`; an error instead of a version number means [install Node](https://nodejs.org) first. - **A JavaScript project to add xript to.** No project yet? `mkdir my-app && cd my-app && npm init -y` gives you one. - **The CLI, for the tooling steps.** The runtime below is a library you `npm install`; the `xript` *command* (validate, typegen, init, and friends) is a separate global install: ```sh npm install -g @xriptjs/cli xript --version ``` If `xript --version` prints a version, you're set. If it prints `command not found`, the CLI isn't installed yet. Anywhere this site says `xript `, `npx xript ` works without the global install. ## Install the Runtime The universal runtime uses QuickJS compiled to WebAssembly: it works in browsers, Node.js, Deno, and more. From your project folder: ```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.7.json", "xript": "0.7", "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 ``` User scripts cannot escape the boundaries you define. That's the whole point. ## 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 on because Bethesda let their community finish what they started. That 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 place to stop. ## 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.7", "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.7", "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.7", "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. ---------------------------------------- # Changelog Source: https://xript.dev/changelog/ ## v0.7.0 — Capability Hierarchy & Live Events The security-and-reactivity chapter, and then some. v0.7.0 started as two pillars and finished as five: **hierarchical capabilities** (grant broad or narrow without a flat-cap explosion), **live events** (the `events` catalog actually reaches subscribing mods now), **xript libs** (whole approved libraries, imported in-sandbox), the **host harness** (test a mod end-to-end with no application running), and **`fills` landing in every runtime** (the canonical contribution surface finally loads as authored). Around the pillars: the static tooling reasons about capabilities the same way the runtimes do, the docs reorganized around who's reading them, and the site now serves every schema and spec page it used to 404 on. ### Hierarchical capabilities - reshaped capability matching from flat string equality to **prefix subsumption with a read/write mode axis**: a capability reference is `[:]`, where the scope is a dotted tree matched on whole segments (`run` covers `run.command`, never `runner`) and the mode is a two-point lattice (`write` covers `read`; a bare reference means `write`) - declared capability keys stay scope-only; the mode axis lives on references and grants, so every existing flat capability keeps its meaning with zero migration - a granted set satisfies a requirement when **any single grant** covers both axes; there is no cross-grant composition - defined the `capabilityRef` grammar in the schema (`^(read:|write:)?[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$`) and applied it to every `capability` reference field, where declarations were previously unvalidated free text - pinned the model with a shared conformance corpus (`spec/capability-tests.json`, 33 cases) that all four runtimes load and assert, the same cross-runtime oracle pattern as the extends and sanitizer corpora - wrote the **monotonic-privilege invariant** into `spec/capabilities.md` as a normative MUST, with the honest caveat that it cannot be statically proven; `xript lint` carries a heuristic `capability-escalation` warning for escalation-named children under broad grantable ancestors - documented the **two-grammars rule** in `spec/bindings.md`: binding and member names are JavaScript identifiers (deliberately unconstrained, since kebab would parse as subtraction), capability scopes are kebab dotted paths, and tooling constrains only the capability side ### Live events & hook dispatch - made the `events` catalog deliverable: a sandbox script subscribes with `events.on(id, handler)` (alias `events.subscribe`), the host broadcasts with `emit(id, payload)`, and delivery rides the same keyed-registry fan-out engine hooks already use rather than a parallel subsystem - an event's optional `capability` gates subscription at registration time, reusing the hook gate model under subsumption; events without a gate stay open to any script - the fan-out contract is byte-for-byte `fireHook`'s: object payloads spread positionally, handlers run in registration order, per-handler errors are swallowed - closed #112: event-typed slots (`accepts: application/x-xript-hook`) now register the `hooks` global and fire through `fireHook` across all four runtimes, so a manifest that declares its hooks as slots no longer silently no-ops; an explicit `hooks` entry still wins over a same-id slot ### Adoption gaps — the asks that came back from real hosts - gave xript.dev a `/changelog/` page, synced from the repo `CHANGELOG.md` at build time and linked from Start Here, so release notes live at the conventional URL instead of taking version spelunking to find - served every schema at its `$id` URL: `/schema/manifest/v0.7.json`, `/schema/mod-manifest/v0.7.json`, the harness pair, and the four host-side data shapes now resolve on xript.dev, with prior version ids (v0.6/v0.3/v0.1) answering as aliases; previously only a single stale manifest version was served, so the `$schema` URL every manifest declares was a 404 and editor autocomplete had nothing to fetch - mapped the `/spec/` URLs readers guess at: a `/spec/` overview landing (documents + schema directory), plus `/spec/extends/` and `/spec/harness/` pages that existed in the repo but never made it onto the site; internal spec cross-links now resolve on-site instead of pointing at the repository - added a minimal end-to-end example to the "Rendering fragments" guidance: a host manifest with one fragment slot, an inert two-binding template, and the ~10-line host loop, every command verified against the real runtime, declared `bindings` and all - blessed **host-side role resolution across isolated runtimes** as canon in the "Resolving roles" guidance: a host that gives each mod its own runtime for grant isolation implements `resolveRole` semantics natively over its own registry; the load-bearing invariant is the `{ addon, role, fns }` fill contract and invoke-by-name, not which code path selects the provider - fixed `score`'s last string-equality capability checks: its integrity and utilization figures compared slot gates and capability references literally, spuriously flagging mode-prefixed or child-scope gates; they reason by subsumption now, same as `lint` and `crossValidate` - blessed **data fills** in the mod-manifest spec: a slot whose `accepts` names a data format takes pure-metadata fills validated by the slot's `payload` schema, the canonical shape for grouping/pack/curation surfaces, with the discovery policy staying host-side; no new primitive needed ### `fills` lands in the runtimes — the canonical surface finally loads - implemented `fills` consumption in all four runtimes: `loadMod` now resolves the canonical contribution surface against the host's slot types, closing the gap where the validator, lint, spec, and docs all pushed mods toward `fills` while every runtime silently ignored it - a fragment-format fill becomes a fragment declaration (an id-less fill gets a stable synthesized id), a role fill (`application/x-xript-role`) becomes a provider role, and an event/hook fill (`application/x-xript-hook`) registers its named `handler` export so `fireHook` invokes it after in-sandbox-registered handlers, same swallow-errors contract - fills are capability-gated at load against the slot's declared gate (subsumption applies), a fill targeting an undeclared slot fails loudly, and a mod mixing `fills` with the deprecated `fragments`/`contributions` surfaces is rejected rather than silently double-contributing - the `ui-dashboard` and `svelte-fragment-renderer` examples dropped their hand-rolled fills→legacy conversion shims; mods now load as authored - made the static capability checks subsumption-aware, matching the runtimes: `crossValidate` accepts a mod requesting a child scope of a declared capability (`fs.addon` under a declared `fs`) or holding a broader grant than a slot's gate, and `lint`'s undeclared/vestigial checks reason over the scope tree instead of string equality - the `satisfies` / `grantedSatisfies` predicate now lives in `@xriptjs/validate` (exported for host import), bound to the shared `spec/capability-tests.json` corpus, and `docgen` reuses it instead of carrying a third copy - folded event-typed slots into the tooling's hook surfaces, mirroring the runtimes' dispatch: `typegen` emits a `hooks` registration function for each hook slot (with a bracket-access note for non-identifier ids) and `docgen` lists hook slots in the Hooks section; an explicit hook still wins over a same-id slot - modernized the `xript init` scaffolds end to end - manifests author the current shape: `fills` keyed by slot id, `xript: "0.7"`, v0.7 schema ids, slots with explicit `accepts`, the lifecycle hook declared as an event-typed slot - the tier-4 demo now actually runs; it loaded mods without fragment sources and called a `runtime.processFragment` method that doesn't exist, and now loads through `loadMod` + `fragmentSources` and renders via `updateBindings` - the mod scaffold gained a runnable harness-powered demo (`demo/host-manifest.json` + `demo/steps.json`, wired to `npm run demo`), which also exposed and fixed a wrong `hooks.fragment.update` signature the entry script taught - dependencies reference the current release line instead of `^0.2.0` - grew the harness steps format a `sources` map on `load-mod`, so a mod whose fills reference file-sourced fragments can load through a steps file or `xript_host_step` - advanced the manifest and mod-manifest schema `$id`s to the v0.7 line, with the v0.6 and v0.3 ids kept as resolvable aliases; spec prose, docs examples, and the bundled example manifests now author `xript: "0.7"` against the new ids ### Docs restructure — sections by reader, entry pages for everyone - reorganized the docs sidebar by reader role: the seven host-implementation pages moved out of Doctrine into their own **Hosting xript** section (the "Hosting:" title prefixes dropped, since the section says it now), a new **Authoring Mods** section pairs the authoring doctrine with a walkthrough, and Doctrine keeps the actual philosophy - the retitles happened at the source, the `xript guide` topic catalog, so the CLI and the docs site stay one set of content - added **Your first mod**: zero to a running, sandboxed mod in two files and three commands, no host application required, with every command's expected output shown - geared the entry pages for less-technical readers: Getting Started and the CLI page now open with explicit prerequisites (Node check, CLI install, a verify step), a hosting-vs-modding orientation callout, and a `command not found` rescue note; the runtime-vs-CLI install split is now stated instead of assumed ### xript libs — approved in-sandbox libraries - added the `libraries` manifest surface: a curated allow-list of whole libraries mod code may `import`, the capability model applied to modules - imports stay default-deny; an allow-list entry is the only thing that lifts it, and only for mods whose grants satisfy the entry's capability under subsumption (`lib` ⊇ `lib.doc`); an ungated entry is importable by every mod - the host supplies each library's pre-bundled ES module source at runtime construction; an approved library links **inside the sandbox at the importing mod's own privilege**: full-fidelity calls, no JSON boundary, no new power granted - registration is guarded: a source for an undeclared specifier, a library carrying CommonJS artifacts, or one that is not **import-clean** (it has imports of its own) fails loudly at construction; a declared-but-unregistered library fails a mod's import as a named host bug - dynamic `import()` stays dead: literal dynamic forms are rejected by the static scan, and loaders only answer during entry-module linking - implemented the loader in all four runtimes at parity: a QuickJS-WASM module loader (`@xriptjs/runtime`), a `SourceTextModule` linker (`@xriptjs/runtime-node`), an rquickjs resolver/loader pair with a load-phase gate (`xript-runtime`), and Jint module registration (`Xript.Runtime`), all with matching error identities (`ImportDeniedError`, `CapabilityDeniedError`, `LibraryUnavailableError`, `LibraryRegistrationError`) and ~11 tests each - taught the toolchain the new surface: `validate` checks the schema shape, `lint` errors on a library gating an undeclared capability and counts library gates as capability use, `score` counts `libraries` as a fifth capacity surface, `describe` lists libraries (and events), `docgen` renders a Libraries table, and `typegen --ambient` emits `declare module` declarations so a TypeScript mod can import approved libraries without type errors - documented the model in `spec/manifest.md` (Libraries section) and `spec/modules.md` (Approved Libraries: resolution order, in-sandbox execution semantics, the import-clean rule, and the pure-compute-vs-host-binding line) ### Host harness - added the host harness: a host manifest executed with stub bindings instead of a live application, so mods, fills, events, and hooks are testable end-to-end with no app running - two spec data shapes define the whole contract: `spec/harness.schema.json` (binding stubs `returns` / `throws` / `sequence` / `script` / `record`, plus capability grants) and `spec/harness-steps.schema.json` (an ordered, replayable scenario: load mods, invoke exports, emit events, fire hooks, resolve slots and roles, read the journal) - every stubbed binding call is journaled in order alongside capability audit events and sandbox console logs; the journal is the scenario's assertion surface - when no grants are listed, every capability scope the host declares is granted in full; capability-denial testing sets the list explicitly - taught `xript run` batch harnessing: `xript run --app host.json --harness harness.json --steps steps.json` runs a scenario file against a synthetic host and exits non-zero if any step fails - gave the MCP server persistent harnessed sessions: `xript_host_load` holds a live runtime across tool calls, `xript_host_step` speaks the same step vocabulary as the steps file, and `xript_host_journal` / `xript_host_list` / `xript_host_unload` round out the family; an interactive session transcribes directly to a replayable steps file, so nothing is expressible over MCP that the CLI can't replay - harness descriptors carry library sources too: `libraries` entries (inline `source` or `path` relative to the harness file) stand in for the host's registration step, so a scenario can exercise a mod that imports an approved library with no app running; the session summary reports each declared library's registration state - exposed the harness spec over MCP resources (`xript://spec/harness` and both schemas) and exported the session API (`createHarnessSession`, `runSteps`, `runSessionStep`, `loadStepsFile`) from `@xriptjs/cli` for host import ### Test counts | Package | v0.6.0 | v0.7.0 | |---------|-------:|-------:| | `@xriptjs/runtime` (js) | 187 | 256 | | `@xriptjs/runtime-node` | 185 | 252 | | `xript-runtime` (rust) | 150 | 185 | | `Xript.Runtime` (csharp) | 229 | 298 | | `@xriptjs/sanitize` | 93 | 93 | | `@xriptjs/validate` | 155 | 169 | | `@xriptjs/typegen` | 64 | 82 | | `@xriptjs/docgen` | 42 | 61 | | `@xriptjs/init` | 41 | 44 | | `@xriptjs/cli` | 60 | 76 | | `xript-ratatui` | 58 | 58 | | `xript-wiz` | 35 | 38 | | **Total** | **1299** | **1612** | ## v0.6.0 — Manifest Inheritance & the Agent CLI Two stories in one release. Manifests learned to **inherit**: a manifest can `extends` a base, fill the abstract holes the base leaves open, refine the concrete pieces it declares, and the same resolution runs identically across all four runtimes. The CLI grew an **agent**: `@xriptjs/cli` now speaks Model Context Protocol, exposing every capability a human runs at the terminal to an agent over stdio. No separate package, no logic to drift. ### Manifest inheritance (`extends`) - added manifest inheritance: a manifest names one or more base manifests in `extends`, resolved and deep-merged base-then-child before validation, transitively, with cycle detection - three moves on a name that collides with the base; **add-new** introduces a name the base does not have (additive, no marker), **fill** redeclares an `abstract: true` base type with concrete fields or values (abstractness is the opt-in, so no marker), and **refine** redeclares a concrete base type or slot with `refines: true` to deep-merge (child wins per key, nested objects recurse, arrays and scalars replace wholesale) - any other collision is an error, so inheritance never silently clobbers; concrete-on-concrete without `refines`, a duplicate binding, a duplicate capability, or a duplicate hook all fail - an inherited abstract type left unfilled is an `abstract-type-unfilled` error, so a base can declare a typed hole a child is required to concretize - made a slot's `payload` carry a full JSON Schema, so a slot can describe exactly what a valid fill looks like (patterns, nested `required`, the lot) instead of a flat field list - added open enums: a type's `values` or a field's inline `enum` can set `open: true` to mean "these known values, plus any other string"; `typegen` emits `... | (string & {})` so the known values autocomplete while any string still type-checks, and `docgen` marks the type extensible - brought `extends` resolution to parity across all four runtimes (the universal QuickJS-WASM, Node, Rust, and C#) against an 18-case conformance corpus, so a manifest resolves identically wherever it loads - consolidated the resolver: `typegen` and `docgen` now reuse `@xriptjs/validate`'s resolver instead of carrying their own copies; one resolution implementation per language, not one per tool - taught the analyzers (`validate`, `score`, `cross-validate`) to resolve `extends` before they run, so inherited slots and capabilities are seen rather than reported missing ### Contribution model - redesigned the contribution surface around "host declares typed slots, mod fills them"; a host slot's `accepts` type governs what a valid fill looks like and what the host does with it (mount, call, resolve, or fire) - folded fragments, provider roles, and hook handlers into one concept: each is a fill of a slot of a particular type, not a separate top-level surface - mods now contribute through a single `fills` object keyed by host slot id; a fragment is a fill of a fragment-format slot, a provider role is a fill of a role-typed slot, a lifecycle hook handler is a fill of an event-typed slot - standalone `hooks` is deprecated in favor of event-typed slots; a hook is a slot whose `accepts` is the event-handler kind, and firing it calls that slot's fills, with host-side hook firing unchanged - validation stays tolerant of legacy `fragments[]` and `contributions` for smooth migration (still validated, now with a deprecation warning); the fill contract checks that a filled slot exists and the mod holds its capability, and leaves the inner fill shape to the slot's type - clarified that format renderers (`xript-ratatui`, the DOM fragment processor) are runtime infrastructure, not manifest concepts; a slot's `accepts` names the format the runtime must be able to paint ### Manifest surfaces - renamed a fragment fill's DOM event handler array from `events` to `handlers`; the entries are event _handlers_, not events, and the old name said the wrong thing - `events` stays accepted as a deprecated alias for back-compat (mirroring the standalone-`hooks` to event-slot precedent): a reader takes `handlers` or `events`, `handlers` wins if both are present, and `events` warns; the entry shape (`selector`, `on`, `handler`) is unchanged, so migration is a key rename - added a top-level `events` catalog: an optional array declaring the named events a host broadcasts and each one's payload type - it is a consumer-agnostic discovery declaration (what the host emits, with no listener presupposed) and is deliberately distinct from event-typed slots (extension points a mod fills) and fragment `handlers` (DOM responses on a fill); one line: bindings are what you can call, slots and handlers are what handles, `events` is what the host emits - `typegen` emits a typed event catalog and `docgen` renders an events section - let a domain extend the top-level manifest vocabulary with a schema overlay, and taught the validator to honor a manifest's declared `$schema` - the core manifest's top level no longer rejects unknown top-level properties, so an `allOf` overlay can add domain surfaces and still validate; deeper objects stay closed, so typos inside known surfaces are still caught - schema resolution leans open: a known schema id resolves to bundled core, a local path resolves relative to the manifest the way `extends` does, and a remote `http(s)` URL is fetched and cached (keyed by URL, pinned per run); offline or uncached-remote falls back to bundled core with a surfaced warning rather than hard-failing - remote resolution is allowed unless a host opts out (allowlist or disable-remote); you opt out of openness, not into it, and honoring a declared schema grants no power, since the capability model, not schema validation, is the security boundary - bumped the manifest schema `$id` from the v0.3 line to v0.6, with a legacy-id alias so a manifest or overlay still pinning the old id resolves - added an optional `license` field to the mod manifest (an SPDX id or short label); forbidding it bought nothing under the openness doctrine ### Extensibility scoring & lint - reshaped `xript score` to measure **moddability capacity**: how much of the extension surface a host exposes (bindings to call, slots to fill, events to observe, a capability model to gate them), against a ceiling of exposing all of it, rather than how much a supplied mod set happens to exercise it - exposing a slot the host does not fill itself now reads as moddability, not waste, and resolving `extends` can only raise the score, never drag it down; "find the unused surface" stays `lint`'s job - slot and capability utilization survive as informational mod-coverage, now excluding `reserved` and inherited surface from their denominators - `score-diff` diffs capacity too, and its regression gate keys off the capacity headline - taught `cross-validate` to check each fill's payload against the target slot's `payload` schema, closing the gap where a fill could name a real slot, hold its capability, and still carry a payload the slot forbids - the schema is applied as authored: a fill carrying more than the payload declares still passes unless the slot explicitly closes its payload; only declared shape is enforced, extras are not policed - on by default; `--no-fill-payloads` on the CLI and `checkFillPayloads` in the library and MCP tool flex it off - added `xript lint`, a findings-based reviewer that complements `score`: where score is the number, lint is the actionable list behind it - checks are set arithmetic over manifest fields; filled-but-undeclared slots and undeclared capabilities are errors, dead slots and vestigial capabilities are warnings, ungated and undescribed surfaces are info - each finding carries a severity, a stable code, a message, and a suggestion; `--strict` promotes warnings to failures for CI, and the exit code gates accordingly - a `legacy-shape` finding flags a mod still on the deprecated `fragments` / `contributions` shape, so migration progress is visible in the linter instead of by grep - added `xript score-diff`: it compares a current run against a saved baseline and reports whether the surface moved toward or away from xript, naming the capacity delta, the slots and capabilities gained or lost, and the integrity violations introduced or fixed; `--min-delta N` is the regression gate - added a `reserved` flag to slots and capabilities, so a surface declared ahead of a filler (for forward-compat or inherited parity) is treated as aspirational, never flagged dead or vestigial, and is excluded from coverage - counted capabilities that gate bindings and hooks (not just slots and mod requests) toward "used," so a capability doing real gating work is never called vestigial - moved the analyzers (`scoreManifests`, `diffScores`, `lintManifests`) out of the CLI into `@xriptjs/validate`, so a host application can surface a modder's problems in its own UI by importing the validation library it already depends on; the CLI commands and MCP tools are thin front-ends over them ### Agent tooling - taught `@xriptjs/cli` to run as a Model Context Protocol server via `xript mcp` - tools mirror the CLI one-to-one (`xript_validate`, `xript_cross_validate`, `xript_typegen`, `xript_docgen`, `xript_sanitize`, `xript_scaffold`, `xript_scan`, `xript_manifest_describe`, `xript_run`, `xript_score`, `xript_score_diff`, `xript_lint`, and `xript_guide`), each calling the same core its matching command does - resources serve the spec straight from source (`xript://spec/*`) alongside authoring guidance (`xript://guidance/*`), and prompts (`adopt-xript`, `is-this-xript-native`, `choose-a-surface`, `author-a-mod`) carry the doctrine as reusable templates - manifest-taking tools accept a file path or inline JSON, so a large host manifest needn't ride through the tool-call tokens; relative paths resolve against the client's workspace root - added `xript_server_info`, reporting the server's name, version, build timestamp, and runtime; the timestamp comes from the running module's own file mtime, so a stale server process whose binary predates a repo change is detectable rather than silently serving old results - added four commands so the human gets every capability too, not just the agent - `xript run` loads a mod into the QuickJS-WASM sandbox and optionally invokes an export - `xript describe` summarizes what a host manifest exposes: bindings, hooks, slots, capabilities - `xript score` rates a host's moddability capacity, with a `--min` gate for CI - `xript guide` prints xript's authoring doctrine by topic - authored the doctrine as markdown content rather than code; one source of truth behind the `xript guide` command, the `xript_guide` tool, and the `xript://guidance/*` resources ### Doctrine - added xript's "More extensible, not less" doctrine: the framework defaults toward openness, and a restriction is permitted only when it genuinely buys convenience or security the framework couldn't otherwise provide, and must justify itself plainly - authored as guidance content like the other doctrine topics, so it surfaces through the `xript guide` command, the `xript_guide` MCP tool, the `xript://guidance/*` resources, and a Doctrine page on the site from one source ### Docs - surfaced three subsystems that lived in the spec but never reached the site: Hooks, Module-Format Mods (the TypeScript authoring guide v0.5 promised), and the DAP-shaped Debugging protocol - documented the v0.5 manifest surfaces the site had missed: provider roles, owned record types, manifest `extends`, the mod `family` field, and the `entry` module form - added an `extends` / inheritance page, MCP server, Extensibility Score, and Lint pages, surfaced the authoring doctrine as a Doctrine section (derived from one source), and expanded the CLI reference with the new commands - reframed the manifest, mod-manifest, fragments, and hooks spec pages around the host-slots / mod-fills model; fragments and provider roles and hooks are now documented as typed slot fills, with `fills` as the canonical contribution surface - generated `llms.txt` and `llms-full.txt` at build time: a curated index and a full-corpus one-pager for agents, linked from the home page - fixed the CommonJS error in `@xriptjs/validate` pointing at a guide URL that never existed; it now points at the published Module-Format Mods page ### Tests | Package | Before | After | |---------|--------|-------| | `@xriptjs/validate` | 68 | 155 | | `@xriptjs/typegen` | 52 | 64 | | `@xriptjs/docgen` | 35 | 42 | | `@xriptjs/cli` | 38 | 60 | | `@xriptjs/runtime` | 166 | 187 | | `@xriptjs/runtime-node` | 165 | 185 | | `xript-runtime` (Rust) | 125 | 150 | | `Xript.Runtime` (C#) | 201 | 229 | ## v0.5.0 — Hardening, Roles & a Debugger The biggest release since the fragment protocol: a security fix that touches every host, a full pass of runtime lifecycle controls, a clutch of new extensibility surfaces, a DAP-shaped debugger across all four runtimes, and first-class TypeScript authoring with real ES module evaluation. Every runtime kept in lockstep against a shared contract. ### Security - closed a `data:` URI XSS hole in the Rust sanitizer that any host embedding the runtime inherited - `xript_runtime::sanitize_html` registered `data:` as a blanket allowed scheme, so `data:text/html,` survived on `` and `` - added a subtype gate that keeps only `data:image/{png,jpeg,gif,svg+xml}` and strips everything else, matching the TS and C# runtimes that already conformed - brought the Rust serializer the rest of the way onto the canonical 56-case corpus: XHTML self-closing void elements and bare boolean attributes, byte-for-byte ### Runtime lifecycle - added host-driven cooperative cancellation: a `CancellationToken` on `RuntimeOptions` that interrupts in-flight execution at the next check point and surfaces a distinct cancellation error (not a timeout) - QuickJS, rquickjs, and Jint interrupt mid-run; Node's `vm` has no mid-run hook, so it checks the token at execute/invoke entry - added an opt-in per-capability audit channel: a fire-and-forget hook that reports every allowed binding invocation as `{ binding, capability, at }` - gave `ConsoleHandler` a severity enum (log/info/warn/error/debug) and a trace channel - finished the sandbox hard caps (memory, CPU time, and stack depth) and brought every runtime to parity ### Extensibility - added the host-invoke export seam: mods declare named exports the host can call and whose return value it honors (the non-streaming core; streaming is reserved) - gave slots runtime teeth (ordering by priority, single/multiple cardinality, and capability enforcement on contributions); they were advisory-only before - added provider-role resolution as a first-class mechanism, retiring the pattern of core UI hardcoding addon-specific globals - mods declare `contributions.provides: [{ role, fns }]` where `fns` maps logical names to concrete exports - the host calls `resolve_role(role) → { addon, fns }` (first-installed-wins, settings-overridable) or `resolve_role_all` to build its own picker - declaring a role grants nothing; the named fns stay gated by their own capabilities - let addons describe owned record types through the existing `types` surface rather than a new persistence concept - `fieldDefinition` gained `default` and inline `enum`; `typegen` emits typed accessors; the runtimes stay persistence-agnostic - added manifest `extends` with deep-merge so a manifest can inherit and override host bindings - added an optional top-level `family` field to the mod-manifest schema for addon grouping - added capability-grant data shapes (schemas only): a prompt payload (capability + description + risk + scope), an install descriptor, and a discovery result; grant policy and prompt UX stay host-side ### Debugging - added a DAP-shaped debug protocol the host can drive: set/clear breakpoints by source position, pause/resume/step in/over/out, and inspect scopes, locals, and stack frames - implemented across rquickjs (Rust), QuickJS-WASM (the async sandbox), Node's `vm` (AST instrumentation), and Jint (C#) using Debug Adapter Protocol vocabulary - engine fidelity differs and is documented per runtime; rquickjs 0.10 exposes no per-line hook, QuickJS-WASM debugging requires the async sandbox, Jint pauses synchronously on the engine thread ### TypeScript & ES modules - made `entry.format: "module"` real: the runtimes now evaluate a mod entry as an ES module instead of treating the value as a reserved no-op - implemented across rquickjs (Rust), QuickJS-WASM (async sandbox), Node's `vm` (`SourceTextModule`), and Jint (C#) - top-level named function exports become host-invokable exports automatically; `export function transcribe()` needs no `xript.exports.register` call, the two paths coexist, and an explicit `register` wins on a name collision - external imports stay denied (`import x from "fs"` fails at load); the sandbox's no-external-modules guarantee is unchanged - added a CommonJS guardrail: `require(`, `module.exports`, and top-level `exports.` in a mod entry now fail loudly with a fix-it message instead of breaking silently, so a mis-set `tsconfig` can't quietly produce unrunnable output - added first-class typed authoring for TypeScript mods - `@xriptjs/typegen --ambient` emits a `.d.ts` declaring the `xript` global (host bindings, `exports.register`, and the mod's own declared exports and types), so authors get real intellisense and typecheck - `xript init --mod --typescript` now scaffolds an ESM `tsconfig`, an `export`-based example, and the ambient types wired in - a new "Authoring Mods in TypeScript" guide documents the canon: compile to ESM, use top-level exports, no external imports, no CommonJS ### Tooling & ergonomics - added a reference Svelte fragment renderer under `examples/svelte-fragment-renderer/`: copy-adaptable host glue that renders inert fragment output (`html` + visibility + command-buffer dispatch) as Svelte, staying inside the inert-fragment wall (not a published package, not core-runtime code) - fixed `@xriptjs/validate` and the CLI failing to locate `manifest.schema.json` from the published package, with a packaging regression test - updated `@xriptjs/typegen` and `@xriptjs/docgen` for the new manifest surfaces (provider roles, record accessors, grant payloads) - added a `namespace_builder` combinator for async namespaces and `add_mixed_namespace` (property values alongside callable functions) to the Rust runtime - made the Rust runtime recurse into nested namespace members instead of silently dropping them - fixed the Rust runtime swallowing uncaught throws in async workflows; a rejected top-level promise read as a successful `undefined`, but now surfaces the real rejection ### Test counts | package | v0.4.2 | v0.5.0 | |---------|--------|--------| | `@xriptjs/sanitize` | 93 | 93 | | `@xriptjs/validate` | 25 | 68 | | `@xriptjs/typegen` | 31 | 52 | | `@xriptjs/docgen` | 28 | 35 | | `@xriptjs/init` | 34 | 41 | | `@xriptjs/cli` | 29 | 38 | | `@xriptjs/runtime` | 97 | 166 | | `@xriptjs/runtime-node` | 97 | 165 | | `xript-runtime` (Rust) | 48 | 125 | | `xript-ratatui` | 58 | 58 | | `xript-wiz` | 35 | 35 | | `Xript.Runtime` (C#) | 116 | 201 | | **total** | **691** | **1077** | ## v0.4.2 — Sanitizer + Rust Runtime Fixes - expanded the sanitizer's allowed element list across all four implementations (TypeScript, Rust/ammonia, C#) - added `button`, `progress`, `meter`, `output`, `fieldset`, and `legend`; `button` was the big miss since it's the primary element for `data-action` event handlers in fragments - added 14 SVG elements: `svg`, `g`, `defs`, `symbol`, `use`, `circle`, `ellipse`, `path`, `rect`, `line`, `polygon`, `polyline`, `text`, `tspan` for icons and data visualization in mod UIs - added `foreignObject`, `animate`, and `set` to the stripped elements list (dangerous SVG elements that shouldn't survive sanitization) - added missing attributes: `open` for `
`, `low`/`high`/`optimum` for ``, plus 18 SVG attributes covering geometry and presentation - fixed SVG attribute casing; `viewBox` and `preserveAspectRatio` were being lowercased by the tokenizer, which silently breaks SVG rendering in browsers - updated the fragment spec documentation in `fragments.md` with the new element and attribute lists - added 11 new conformance test cases to `spec/sanitizer-tests.json` and 11 new unit tests across the implementations - fixed a serialization bug in `xript-runtime` (Rust) where `js_value_to_json` silently returned `Null` for objects and arrays from `execute()` (#89) - the fallback code evaluated `((v) => JSON.stringify(v))` which returned the function's _string representation_ instead of actually calling it with the value - replaced the broken eval with a proper `Function::call` through rquickjs's API - added 3 new tests for object, array, and nested object serialization ### Test counts | package | v0.4.1 | v0.4.2 | |---------|--------|--------| | `@xriptjs/sanitize` | 71 | 93 | | `@xriptjs/validate` | 25 | 25 | | `@xriptjs/typegen` | 31 | 31 | | `@xriptjs/docgen` | 28 | 28 | | `@xriptjs/init` | 34 | 34 | | `@xriptjs/cli` | 29 | 29 | | `@xriptjs/runtime` | 97 | 97 | | `@xriptjs/runtime-node` | 97 | 97 | | `xript-runtime` (Rust) | 45 | 48 | | `xript-ratatui` | 58 | 58 | | `xript-wiz` | 35 | 35 | | `Xript.Runtime` (C#) | 116 | 116 | | **total** | **666** | **691** | ## v0.4.1 — npm housekeeping - added a README for `@xriptjs/cli` so the npm package page isn't a blank stare - bootstrapped `@xriptjs/cli` on the npm registry; it was built and published in CI but had never been seeded locally, so npm didn't know it existed - all eight `@xriptjs/*` packages now have READMEs on npmjs.com ## v0.4.0 — Unified CLI, Tier 4 & Rust Runtime - consolidated five separate CLI tools into `@xriptjs/cli`, published as the `xript` command - `xript validate`, `xript typegen`, `xript docgen`, `xript init`, `xript sanitize` all route to the existing library packages - individual tool packages (`@xriptjs/validate`, `@xriptjs/typegen`, etc.) dropped their `bin` entries but remain published as libraries - one command to remember instead of five separate `npx xript-*` invocations - added `xript scan`, a new subcommand that reads `@xript` and `@xript-cap` JSDoc tags from TypeScript source and generates manifest bindings and capabilities - spec document at `spec/annotations.md` defining the tag convention - scanner parses TypeScript ASTs via `ts-morph` (optional dependency; the CLI prompts if it's missing) - merge mode reads an existing manifest, adds new bindings, warns about removals, and auto-generates capability entries - outputs to stdout, to a file, or directly into an existing manifest with `--write` - `xript-runtime` (Rust) gained three headline features - `load_mod()` now executes mod entry scripts after fragment validation (#87) - async host bindings with `Promise`/`await` support via `pollster` (#86); host functions return real Promises, JS callers can `await` them, chained awaits work - `XriptHandle`, a `Send + Sync` wrapper that owns an `XriptRuntime` on a dedicated thread, communicates via `mpsc` channels, mirrors the full runtime API (#88) - introduced **tier 4 "Full Feature"** adoption tier covering slots, mod manifests, fragments, and the sandbox fragment API - updated adoption tiers docs, spec, vision, README, and CONTRIBUTING - `xript init` scaffolds tier 4 apps with slots, companion mod manifests, and fragment HTML - UI Dashboard example linked as the tier 4 reference implementation - improved `@xriptjs/docgen` with two new flags - `--link-format no-extension` strips `.md` from generated links for static site generators that don't want them - `--frontmatter` injects YAML frontmatter into all generated files - built the **Fragment Workbench** (#85), an interactive tool page on the docs site for building and testing xript UI fragments - tabbed workflow (Manifest, Author, Preview, Export) with collapsible inline guides - CodeJar syntax highlighting for manifest JSON and fragment HTML editors - slot contract panel, JSML toggle, validation-as-you-type, dynamic state simulation - Export tab with live mod manifest preview and one-click download - overhauled the **Fragment Builder** demo - RPG dungeon theme ("Realm of Xript") with ASCII roguelike map - radio-pill slot selection, individual fragment close buttons, CodeJar editor - added two new screens to `xript-wiz` - audit: capability coverage analysis showing ungated bindings, unused capabilities, capability gaps, and risk distribution - diff: compares the current manifest against the last git tag, surfacing added/removed bindings, capabilities, and slots - home menu expanded from 4 to 6 items - consolidated docs site tool pages from 6 separate pages to 3 (CLI, TUI Wizard, Fragment Workbench) - unified CLI reference page with all subcommands, flags, examples, and programmatic API links - new TUI Wizard page with `Terminal.astro` component mockups for home, audit, and diff screens - added Annotations spec page to the Specification sidebar section - updated all four runtime doc pages with `loadMod`, fragment hooks, async bindings, and `XriptHandle` - fixed stale tool references (`xript-validate` to `xript validate`, etc.) across the entire docs site - updated the publish pipeline for 8 npm packages (added `@xriptjs/cli`); `scripts/bump-version.mjs` now handles 14 files ### Test counts | package | v0.3.1 | v0.4.0 | |---------|--------|--------| | `@xriptjs/sanitize` | 71 | 71 | | `@xriptjs/validate` | 25 | 25 | | `@xriptjs/typegen` | 31 | 31 | | `@xriptjs/docgen` | 22 | 28 | | `@xriptjs/init` | 27 | 34 | | `@xriptjs/cli` | — | 29 | | `@xriptjs/runtime` | 97 | 97 | | `@xriptjs/runtime-node` | 97 | 97 | | `xript-runtime` (Rust) | 31 | 45 | | `xript-ratatui` | 58 | 58 | | `xript-wiz` | 33 | 35 | | `Xript.Runtime` (C#) | 116 | 116 | | **total** | **608** | **666** | ## v0.3.1 — Publishing & Release Tooling - fixed the docs deploy workflow; `@xriptjs/sanitize` wasn't being built before the runtime, so the docs site build was failing - switched all publish workflows to fire on GitHub Release creation (`release: published`) with `workflow_dispatch` as a manual fallback - `publish.yml` (npm), `publish-nuget.yml`, and `publish-crates.yml` all use the same trigger pattern now - previously npm and NuGet were manual-only; crates.io had no workflow at all - created `publish-crates.yml` for crates.io publishing - publishes `xript-runtime`, `xript-ratatui`, and `xript-wiz` in dependency order - unified all 11 published packages (7 npm, 3 Rust crates, 1 NuGet) to version `0.3.1` - internal dependency references updated to match - created `scripts/bump-version.mjs` (`npm run version:bump `) to sync versions across all 12 package files - covers `package.json`, `Cargo.toml`, and `.csproj` files plus their internal dependency references - created `scripts/release.mjs` (`npm run release`) to cut a GitHub Release from the current package version and matching `CHANGELOG.md` section - added `readme`, `keywords`, and `categories` to `xript-ratatui` and `xript-wiz` Cargo.toml files; added `version` fields to path dependencies so `cargo publish` works - wrote package READMEs for `@xriptjs/sanitize`, `xript-ratatui`, `xript-wiz`, and `Xript.Runtime` so they're not bare on their respective registries - wired `PackageReadmeFile` in the C# `.csproj` so the README shows on nuget.org - documented the full release process in `CLAUDE.md` ## v0.3.0 — Fragment Protocol - introduced **mod manifests**: mods declare themselves, their capabilities, entry scripts, and UI fragment contributions in a single JSON file (`spec/mod-manifest.schema.json`) - extended app manifests with **slots**: host-declared UI mounting points where mods contribute fragments - each slot declares accepted formats, capability gating, multiplicity, and styling mode (`inherit`, `isolated`, `scoped`) - added the **fragment protocol** to the spec (`spec/fragments.md`): the full lifecycle for host-declared slots, mod-contributed UI, sanitization, data binding, conditional visibility, event routing, and the sandbox fragment API - `data-bind` for value binding: attributes persist in the DOM for O(1) updates at game-loop speed - `data-if` for conditional visibility: expressions evaluated by the same tier 1 engine - only two "smart" attributes; everything else goes through the sandbox fragment API - built `@xriptjs/sanitize`: a pure string-based HTML sanitizer with no DOM dependency (`tools/sanitize/`) - works inside QuickJS WASM, Node, Deno, and browsers, anywhere - 45-case conformance test suite at `spec/sanitizer-tests.json` that all runtime implementations must pass - JSML support (`application/jsml+json`): JSON Markup Language as a native fragment format, no escaping needed - added `loadMod()` to all four runtimes - `@xriptjs/runtime`: JS/WASM via QuickJS, JSML support, sandbox fragment API with command buffer pattern - `@xriptjs/runtime-node`: Node.js vm-based, same API surface - `xript-runtime` (Rust): `load_mod()` with ammonia-based sanitization, cross-validation, fragment hooks - `Xript.Runtime` (C#): `LoadMod()` with regex-based sanitization, Jint fragment hooks - added the **sandbox fragment API** to the JS and Node runtimes: `hooks.fragment.update(id, callback)` with a command buffer proxy (`toggle`, `addClass`, `setText`, `setAttr`, `replaceChildren`) - `@xriptjs/validate` gained mod manifest validation, auto-detection (app vs mod), and `--cross` flag for cross-validation against app slots - `@xriptjs/typegen` now generates `FragmentProxy` interface, `hooks.fragment` namespace, and `XriptSlots` types - `@xriptjs/docgen` produces slot documentation tables and a Fragment API reference page - `@xriptjs/init` gained a `--mod` flag for mod project scaffolding: generates `mod-manifest.json`, fragment HTML, and entry script - built `xript-ratatui`: a fragment renderer for Ratatui terminal applications (`renderers/ratatui/`) - parses `application/x-ratatui+json` fragment trees into native Ratatui widgets - layout engine, style mapper, color/modifier support, `data-bind`/`data-if` processing - reusable logo module with ANSI art rendered via `ansi-to-tui` - built `xript-wiz`: an interactive TUI wizard for the xript toolchain (`tools/wiz/`) - dogfoods the xript ecosystem: app manifest with slots, fragments rendered by `xript-ratatui` - card-style menu with icons, tab-completion file input, scaffold form with toggle cards - validate, scaffold, and sanitize workflows - added `examples/ui-dashboard/`: a full fragment protocol demo with two mods (health panel, inventory panel) - demonstrates `data-bind`, `data-if`, sandbox fragment API iteration, cross-validation, and mod loading - added four new fragment format examples to the docs: HTML, JSML, Ratatui JSON, WinForms JSON - same health panel rendered in four formats showing the protocol is rendering-agnostic - added 6 new docs pages: mod manifest spec, fragment protocol spec, fragment formats, sanitizer tool, UI dashboard example, Fragment Builder interactive demo - updated all tool docs pages (validator, typegen, docgen, init) with v0.3 features - sidebar expanded to 30 pages - fixed a binding-name injection vulnerability in `evaluateCondition`: mod-authored binding names are now validated against a safe identifier pattern before interpolation - created tracking issues for future fragment renderer packages (#76 hub, #77 xript-ratatui, #78 xript-winforms) ### Test counts | package | v0.2 | v0.3 | |---------|------|------| | `@xriptjs/sanitize` | — | 71 | | `@xriptjs/runtime` | 69 | 97 | | `@xriptjs/runtime-node` | 71 | 97 | | `xript-runtime` (Rust) | 17 | 31 | | `xript-ratatui` | — | 58 | | `xript-wiz` | — | 33 | | `Xript.Runtime` (C#) | 72 | 116 | | `@xriptjs/validate` | 11 | 25 | | `@xriptjs/typegen` | 24 | 31 | | `@xriptjs/docgen` | 17 | 22 | | `@xriptjs/init` | 20 | 27 | | **total** | **301** | **608** | ---------------------------------------- # 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 is a fill of a slot of a particular type: a fragment, a role, a lifecycle handler. 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. This is the whole extensibility surface. ## 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** → it is host code, because it *is* the surface, or a mechanism nothing can reach around, or a security boundary. ## 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. ---------------------------------------- # 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. ---------------------------------------- # 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 - [Rendering fragments](/guidance/host-fragments/) — the inert-output seam, and why you never call the processor directly. - [Granting capabilities](/guidance/host-capabilities/) — default-deny, what granting means, and why the grant decision is the host's. - [Mounting slots](/guidance/host-slots/) — `resolveSlot`, the `SlotContribution` shape, and honoring `priority` and `multiple`. - [Resolving roles](/guidance/host-roles/) — `resolveRole`, the `RoleResolution` shape, and picking among providers. - [Firing hooks & events](/guidance/host-hooks/) — `fireHook`, event-typed slots, and how they differ from the `events` catalog. - [Limits, cancellation & audit](/guidance/host-safety/) — the caps the runtime enforces and the signals it hands back. ---------------------------------------- # 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. ## End to end, minimally Everything above, assembled — a host manifest declaring one fragment slot, a mod filling it with an inert template, and the host loop that renders it. **Host manifest** (the slot is the contract): ```json { "xript": "0.7", "name": "minimal-host", "slots": [ { "id": "panel", "accepts": ["text/html"], "description": "A status panel." } ] } ``` **Mod manifest + template** (`fills` keyed by slot id; the template is inert — `data-bind` for values, `data-if` for visibility, no script): ```json { "xript": "0.7", "name": "status-mod", "version": "1.0.0", "fills": { "panel": [ { "format": "text/html", "source": "panel.html", "bindings": [ { "name": "status", "path": "app.status" }, { "name": "warning", "path": "app.warning" } ] } ] } } ``` ```html

Status:

Check the logs.

``` **The host loop** (~10 lines: create, load, push data, apply what comes back): ```js const xript = await initXript(); const runtime = xript.createRuntime(hostManifest, { hostBindings: {} }); const mod = runtime.loadMod(modManifest, { fragmentSources: { "panel.html": await readFile("panel.html", "utf-8") }, }); for (const result of mod.updateBindings({ app: { status: "online", warning: false } })) { mount(result.fragmentId, result.html, result.visibility); // your render fn — apply, don't interpret } const ops = runtime.fireFragmentHook("panel-fill-0", "update", { status: "degraded" }); applyOps(ops); // walk the FragmentOp[] command buffer in order runtime.dispose(); ``` That's the whole seam: the fill's `bindings` map each `data-bind`/`data-if` name to a path in the data you push (`status` ← `app.status`), the runtime resolved them into inert html, visibility, and ops; `mount` and `applyOps` are host-owned and apply data without executing any of it. (An id-less fill gets a synthesized id, `-fill-`; declare an explicit `id` on the fill when you'd rather name it.) ## 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. ---------------------------------------- # 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. ---------------------------------------- # 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. ---------------------------------------- # 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. ## Roles across isolated runtimes Some hosts load every mod into one shared runtime; others give each mod its own runtime for per-mod grant isolation. Both postures are fully supported, and in the isolated case, **the host implements role resolution itself, natively, over its own mod registry.** That is canon, not a workaround. A role is defined by its fill contract, the `{ addon, role, fns }` resolution plus invoke-by-name through the provider's own runtime handle, not by which code path performs the selection. `resolveRole` / `resolveRoleAll` / `rolePreferences` are *semantics*; the single-shared-runtime methods are one implementation of them. A host with per-mod runtimes reproduces the same semantics host-side: iterate the loaded mods, match the role's fills, apply the preference policy, and call the chosen provider's exports via `invokeExport` on *that mod's* handle. Every invariant survives: the typed fill, the `fns` map, invoke-by-name, and per-function capability gating (each provider's functions stay gated by its own grants). Do not collapse per-mod isolation into one shared runtime just to call `runtime.resolveRole` literally. That trades a real security property for spec-literalism. High isolation is the stronger posture, and roles work there by design. ## 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. ---------------------------------------- # 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, and `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. ---------------------------------------- # 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. ---------------------------------------- # Your first mod Source: https://xript.dev/mods/first-mod/ This page takes you from nothing to a running mod: two files, three commands, and a sandbox doing the work. You don't need a host application yet; the CLI carries one for exactly this purpose. ## Before you start You need two things, and the second one is the step people skip: 1. **Node.js 20 or newer.** Check with `node --version`. If that prints an error instead of a version, install Node from [nodejs.org](https://nodejs.org) first. 2. **The xript CLI.** Install it globally: ```sh npm install -g @xriptjs/cli ``` Then confirm it answers: ```sh xript --version ``` :::note[Seeing `xript: command not found`?] The CLI isn't installed yet; run the install command above. If you'd rather not install anything globally, every command on this page also works prefixed with `npx`: where we write `xript validate`, you write `npx xript validate`. ::: ## Two files Make a folder anywhere and create these two files in it. **`mod-manifest.json`**, what your mod is and what it offers: ```json { "$schema": "https://xript.dev/schema/mod-manifest/v0.7.json", "xript": "0.7", "name": "hello-mod", "version": "1.0.0", "description": "My first mod.", "entry": { "script": "mod.js", "format": "module", "exports": { "greet": { "description": "Greets a name.", "params": [{ "name": "name", "type": "string" }], "returns": "string" } } } } ``` **`mod.js`**, the code: ```js export function greet(name) { return "hello, " + name + "!"; } ``` That's a complete mod. The manifest declares one invokable export; the script implements it as a plain ES module export. No build step, no dependencies, no config beyond what you see. ## Check it From the folder you just made: ```sh xript validate mod-manifest.json ``` Success looks like a single line: ``` ✓ mod-manifest.json ``` If something's off, a typo'd field or a missing required property, the validator names the exact path and what it expected. Fix and re-run. ## Run it ```sh xript run mod-manifest.json mod.js --export greet --args '["world"]' ``` This loads your mod into the real QuickJS sandbox, the same one a host application would use, invokes `greet`, and prints what happened: ```json { "loaded": true, "logs": [], "declaredExports": ["greet"], "fragments": [], "provides": [], "result": "hello, world!" } ``` `result` is your function's return value, round-tripped across the sandbox boundary. The mod ran with no filesystem, no network, and no host APIs beyond what a manifest grants — which here was nothing, because `greet` needed nothing. ## What just happened xript evaluated `mod.js` as an ES module inside a sandbox, harvested the top-level `greet` export, and made it invokable by name. In a real application, the host loads your mod the same way and calls your exports the same way; the only difference is that a real host also offers **bindings** (functions you call), **slots** (places your UI mounts), and **capabilities** (permissions gating both). The mod you just wrote is the seed all of that grows from. ## Where to go next - [Authoring against a host](/guidance/authoring/) — the full loop once a real host manifest enters the picture: read its surface, declare what you need, fill its slots - [Module-format mods](/spec/modules/) — the rules your entry module plays by, including authoring in TypeScript with generated types - `xript init --mod` — scaffolds a fuller mod project (TypeScript, fragments, a demo harness) when you outgrow two files ---------------------------------------- # 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 and mutations and 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. ---------------------------------------- # Specification Source: https://xript.dev/spec/index/ The specification is the contract every runtime, tool, and host implements. The manifest is the center of gravity; everything else derives from it. ## Documents - [Manifest](/spec/manifest/) — the app manifest: bindings, capabilities, types, slots, events, libraries - [Manifest Inheritance](/spec/extends/) — `extends`: add-new, fill, refine, and the collision rules - [Mod Manifest](/spec/mod-manifest/) — what a mod declares: capabilities, entry, and `fills` keyed by host slot id - [Fragments](/spec/fragments/) — the inert-template protocol: `data-bind`, `data-if`, handlers, and the command buffer - [Fragment Formats](/spec/fragment-formats/) — the format catalog a slot's `accepts` names - [Capabilities](/spec/capabilities/) — default-deny grants, prefix subsumption, and the read/write mode lattice - [Bindings](/spec/bindings/) — host functions and namespaces, error vocabulary, naming grammars - [Hooks](/spec/hooks/) — event-typed slots and the dispatch contract - [Module-Format Mods](/spec/modules/) — ES module entries, the import deny, and approved libraries - [Host Harness](/spec/harness/) — synthetic hosts: stub bindings, journals, and replayable step scenarios - [Debugging](/spec/debugging/) — the DAP-shaped debug protocol - [Security](/spec/security/) — the sandbox guarantees and threat model - [Annotations](/spec/annotations/) — `@xript` source annotations scanned into manifests ## Schemas Every schema is served at its `$id` URL, with prior version ids resolving as aliases: - [`/schema/manifest/v0.7.json`](/schema/manifest/v0.7.json) — the app manifest schema - [`/schema/mod-manifest/v0.7.json`](/schema/mod-manifest/v0.7.json) — the mod manifest schema - [`/schema/harness/v0.7.json`](/schema/harness/v0.7.json) and [`/schema/harness-steps/v0.7.json`](/schema/harness-steps/v0.7.json) — the harness descriptor and scenario shapes - [`/schema/capability-prompt/v0.5.json`](/schema/capability-prompt/v0.5.json), [`/schema/install-descriptor/v0.5.json`](/schema/install-descriptor/v0.5.json), [`/schema/discovery-result/v0.5.json`](/schema/discovery-result/v0.5.json), [`/schema/debug-messages/v0.5.json`](/schema/debug-messages/v0.5.json) — the host-side data shapes Point a manifest's `$schema` at the matching URL and editors pick up validation and autocomplete. ---------------------------------------- # 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, and validation. Interactive playgrounds are also supported as a toolchain output. This document explains the structure of the manifest, the rationale behind key decisions, and how the manifest supports xript's four adoption tiers. ## Overview A manifest is a JSON file conforming to the [manifest JSON Schema](/schema/manifest/v0.7.json). At minimum, a manifest declares a spec version and a name: ```json { "xript": "0.7", "name": "my-app" } ``` From there, complexity is layered on only as needed. The schema is designed so that every field beyond `xript` and `name` is optional, and each additional section enables more functionality. ## Top-Level Fields ### `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. Runtimes use this to determine which features and validation rules apply. Format: `major.minor` (e.g., `"0.1"`). Patch versions are intentionally excluded, since the spec level doesn't change for non-breaking fixes. ### `name` (required) A machine-readable identifier for the application. Used in generated package names (`@xriptjs/-types`), documentation URLs, and tooling output. Constraints: lowercase letters, numbers, and hyphens. Must start with a letter. Maximum 64 characters. This mirrors npm package naming conventions because the generated types will live in that ecosystem. ### `version` The version of the application's scripting API, following semver. This is distinct from `xript`; it tracks how the application's exposed bindings evolve over time. When a binding is added, the minor version increments. When a binding's signature changes in a breaking way, the major version increments. This versioning drives compatibility checks and generated type package versioning. ### `title` A human-readable display name. While `name` is `"skyrim-modkit"`, `title` might be `"Skyrim Mod Toolkit"`. Used in documentation headers and UI. ### `description` A brief description aimed at modders. Tells them what the application does and what they can extend. Used in documentation landing pages and registry listings. ## Bindings Bindings are the core of the manifest. They 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`. This is not optional because if a modder can't understand what a function does, it may as well not exist. The description appears in generated docs, TypeScript JSDoc comments, and editor tooltips. Optional fields on function bindings: - **`params`** — an ordered array of parameters, each with a `name`, `type`, and optional `description` and `default` - **`returns`** — the return type (omit for void functions) - **`async`** — whether the function returns a promise (defaults to `false`) - **`capability`** — the capability required to call this function - **`examples`** — usage examples for documentation - **`deprecated`** — marks the function as deprecated with a migration message ### Namespace Bindings Namespaces group related functions. They create the `namespace.function()` calling convention: ```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 arbitrarily (`game.world.weather.setRain()`), but deep nesting is discouraged; two levels is usually plenty. A binding is distinguished as a namespace by the presence of `members`. A binding with both `members` and `params` is invalid. ### Why Bindings Are a Flat Object, Not Nested by Default Bindings are declared as a flat key-value map at the top level (`"getHealth": {...}`) rather than being implicitly grouped. Namespaces exist as an explicit opt-in via `members`. This keeps the simple case simple (one function = one key) while allowing organization when needed. ## 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" }, "network": { "description": "Make HTTP requests to allowed domains.", "risk": "high" } } } ``` Capabilities are referenced by name in function bindings via the `capability` field. A function with `"capability": "filesystem"` is only callable if the script has been granted the `filesystem` capability. The `risk` field is advisory; it helps users make informed decisions about what to grant. It does not affect runtime behavior. Runtimes may use it to display warnings or require additional confirmation for `high` risk capabilities. Functions without a `capability` field are always available to any script. This is intentional: the common case is that most bindings are safe read-only operations that don't need gating. ## Types Custom types let the manifest 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 with no `fields` of its own, expecting an extending manifest to fill it. See [Abstract Types](/spec/extends/#abstract-types). ### Enum Types ```json { "types": { "Direction": { "description": "A cardinal direction.", "values": ["north", "south", "east", "west"] } } } ``` An enum type can also be abstract: a `description` with `"abstract": true` and no `values`, leaving the concrete `values` for an extending manifest to fill. See [Filling Abstract Types](/spec/extends/#the-three-moves-add-fill-refine). ### Field Defaults and Inline Enums Object-type fields carry optional `default` and inline `enum` metadata: ```json { "types": { "BrickFiles": { "description": "File-viewer configuration for a brick.", "fields": { "path": { "type": "string", "optional": true, "description": "Path to display." }, "pathStyle": { "type": "string", "enum": ["posix", "hybrid", "native"], "default": "posix" }, "viewingEnabled": { "type": "boolean", "default": true } } } } } ``` - `default` declares the value used when the field is absent. - `enum` declares the allowed literal values for the field. Both are **documentation and codegen hints only**. xript does not apply defaults, does not enforce enum membership, and reads neither at runtime. Codegen consumes them: a field with a `default` becomes non-optional in the generated interface (the host can rely on a value being present), and an inline `enum` becomes a literal union type. An enum field may instead reference a named `values`-based type definition (`{ "type": "PathStyle" }` where `PathStyle` declares `values`). The inline `enum` form and the named-enum form generate identical TypeScript. ### Record Schemas via Types Addon-owned record shapes are expressed as ordinary object type definitions — there is no separate records block, no key field, and no record vocabulary in the schema. A record type **is** a custom object type whose `fields` carry `type`, `optional`, `default`, and `enum`. xript stays persistence-agnostic. It owns no store, reads and writes nothing, validates no field at runtime, and has no migration story. The type definition is purely a source of truth for documentation and code generation. Strictness, cross-addon writes, and migration are host concerns: the type def supplies the shape, and the host decides enforcement. Schema evolution over time is narrated through the manifest's own semver. For codegen, typegen emits a companion `Accessor` interface alongside the plain interface, exposing typed get/set per field so a host that backs records with its own store gets typed access without xript ever seeing that store. ### Type References Anywhere a type is expected, you can use: - **Primitives**: `"string"`, `"number"`, `"boolean"`, `"void"`, `"null"` - **Custom types**: `"Position"`, `"Direction"` (references to the `types` section) - **Array shorthand**: `"string[]"`, `"Position[]"` - **Complex expressions**: `{ "array": "Position" }`, `{ "union": ["string", "number"] }`, `{ "map": "number" }`, `{ "optional": "string" }` The shorthand `"string[]"` is equivalent to `{ "array": "string" }`. Both are valid. The shorthand exists because array types are common and the verbose form is noisy for simple cases. ## Slots Slots are the host's typed fill surface, the counterpart to bindings. A binding is a callable the host implements and the mod *calls*. A slot is a typed plug-point the host declares and the mod *fills*. Everything a mod contributes is a fill of a slot; the slot's `accepts` type governs what a valid fill looks like and what the host does with it: mount it, call it, resolve it, fire it. ```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 (`^[a-z][a-z0-9.-]*$`) | | `accepts` | string[] | yes | — | Format(s)/kind(s) this slot takes (see slot types) | | `capability` | string | no | — | Capability required to fill this slot | | `multiple` | boolean | no | false | Whether more than one mod can fill this slot | | `refines` | boolean | no | false | When set, deep-merges onto a base slot of the same `id` (see below) | | `style` | enum | no | "inherit" | Styling mode, for fragment-format slots | A slot in an extending manifest can redeclare a base slot `id` with `"refines": true` to deep-merge onto it, including its payload member types. Without the marker, redeclaring a base slot `id` is a resolution error. See [Refining Concrete Types and Slots](/spec/extends/#the-three-moves-add-fill-refine). ### Slot Types A slot's `accepts` names the format(s) or kind(s) of fill it takes. The type determines what a fill looks like and what the host does with it: - **Fragment-format slots** — `accepts` names a fragment format (`text/html+jsml`, `application/jsml+json`, `text/html`). The host *mounts* the fill as an inert fragment. The [fragment protocol](/spec/fragments/) is the semantics of this slot type. - **Code-renderer slots** — `accepts` names an executable renderer kind (e.g. `application/javascript+esm`). The host *loads and runs* the fill's code to paint the slot. - **Role slots** — `accepts` is `application/x-xript-role`. The host *resolves* the fill's logical-to-concrete function map and calls the named functions itself. - **Event slots** — `accepts` is `application/x-xript-hook`; the host *fires* the slot, calling each fill's named handler when the event occurs. This is the modern replacement for the standalone `hooks` concept below. Mods fill slots through the `fills` surface in their mod manifest; see [mod-manifest.md](/spec/mod-manifest/). ### Styling Modes For fragment-format slots, `style` controls how host styles reach the fragment: - **`inherit`** — fragment inherits host styles. Suitable for inline UI like status bars. - **`isolated`** — no host styles bleed into the fragment. Suitable for panels and overlays. On the web, implemented via Shadow DOM or equivalent. - **`scoped`** — host exposes CSS custom properties / design tokens; the fragment uses them. ## Hooks (deprecated — use event-typed slots) > **Deprecated.** A lifecycle hook is an event-typed slot (`accepts: ["application/x-xript-hook"]`) whose fills are handlers the host calls when the event fires. Declare lifecycle events as slots and let mods fill them. The `hooks` field remains allowed for back-compat. Hosts still fire hooks and runtimes still dispatch them, but new manifests should model events as slots. See [hooks.md](/spec/hooks/). Hooks are the reverse of bindings. While bindings let scripts call the host, hooks let the host call scripts. They enable the event-driven programming model that real modding requires: "when the player takes damage," "before the game saves," "after a level loads." ### Simple Hooks A hook without lifecycle phases is a simple notification: ```json { "hooks": { "playerDamage": { "description": "Fired when the player takes damage.", "params": [ { "name": "amount", "type": "number", "description": "Damage amount." }, { "name": "source", "type": "string", "description": "What caused the damage." } ] } } } ``` Scripts register handlers: `hooks.playerDamage((amount, source) => { ... })`. The host fires the hook via `runtime.fireHook("playerDamage", { amount: 25, source: "trap" })`. ### Phased Hooks Hooks can declare lifecycle phases for structured interception: ```json { "hooks": { "save": { "description": "Fired during the save lifecycle.", "phases": ["pre", "post", "done", "error"], "capability": "persistence", "params": [ { "name": "data", "type": "SaveData" } ] } } } ``` Scripts register per-phase: `hooks.save.pre((data) => { ... })`. Multiple handlers per phase run in registration order. The four standard phases are `pre` (before execution), `post` (after execution, can modify), `done` (after all post-processing, sealed), and `error` (after failure). Hosts declare which phases apply and control firing order. ### Hook Properties Optional fields on hooks mirror bindings where appropriate: - **`phases`** — lifecycle phases (`pre`, `post`, `done`, `error`). Omit for simple hooks. - **`params`** — parameters passed to handlers when the hook fires - **`capability`** — capability required to register for this hook - **`async`** — whether handlers run asynchronously (host-controlled, defaults to `false`) - **`limits`** — per-handler execution limits, overriding manifest defaults - **`examples`** — usage examples for documentation - **`deprecated`** — marks the hook as deprecated with a migration message See [hooks.md](/spec/hooks/) for the full hook conventions, error handling, and TypeScript mapping. ## Events (Host Broadcast Catalog) The optional top-level `events` array is the named events a host **emits** and the payload each carries. It says "here is what this application broadcasts," and through the shared dispatch engine that hooks already use, it is *deliverable*: a sandbox script subscribes with `events.on("", handler)`, the host's own UI may react, an external subscriber may observe. The catalog names the signal; sandbox delivery rides the hook registry/fire engine rather than introducing a parallel event subsystem (see [Event Delivery](/spec/hooks/#event-delivery)). ```json { "events": [ { "id": "player.damaged", "description": "Broadcast after the player takes damage, once the new health is committed.", "payload": "DamageEvent" }, { "id": "level.loaded", "description": "Broadcast after a level finishes loading and is interactive.", "capability": "read:world" } ] } ``` ### Event Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `id` | string | yes | Event identifier, the name the host broadcasts under | | `description` | string | yes | What the event signals and when it fires | | `payload` | typeRef | no | The type of the data carried with the event (a [type reference](#type-references)) | | `capability` | capabilityRef | no | Capability required to subscribe via `events.on`. If omitted, any script may subscribe. Gated at registration time, reusing the hook gate model. | ### Three Surfaces, One Distinction `events`, event-typed slots, and fragment handlers all touch "events," but they answer different questions, and the manifest keeps the line clean: - **`events` (this surface)** — *what the host emits.* A catalog of broadcasts the application produces. A sandbox script subscribes with `events.on(id, fn)`; the host delivers with `emit(id, payload)`. (See [Event Delivery](/spec/hooks/#event-delivery).) - **Event-typed slots** (`accepts: ["application/x-xript-hook"]`) — *extension points addons fill.* The host declares the plug-point; a mod fills it with a handler the host calls when the event fires. (See [Slot Types](#slot-types).) - **Fragment `handlers`** — *DOM responses on a fragment fill.* A `{ selector, on, handler }` entry wiring a sandboxed function to a DOM event on mounted markup. (See the [fragment protocol](/spec/fragments/#event-routing).) In one line: bindings are *what you can call*, slots and fragment handlers are *what handles*, and `events` is *what the host emits and a mod subscribes to*. The catalog is a source of truth for documentation and code generation: typegen emits a typed event catalog, docgen renders an events section. Sandbox delivery and the event-typed-slot hook dispatch share one engine: a keyed handler registry plus a fire-from-registry pass. As with the rest of the manifest, xript declares the shape; the host owns dispatch. ## Libraries (Approved Import Allow-List) The optional top-level `libraries` map is the host's **curated allow-list of importable libraries**: whole pre-bundled modules (a markdown renderer, a date library) that mod code may pull in with a real static `import`. Imports stay default-deny; an entry here is the only thing that lifts the deny, and only for mods holding the entry's capability. This is the capability model applied to modules: the host curates *which* libraries exist, the capability gates *which mods* may import each. ```json { "libraries": { "@example/doc": { "description": "Shared markdown + doc rendering.", "capability": "lib.doc", "version": "^1.0.0" }, "luxon": { "description": "Date/time parsing, formatting, and arithmetic.", "capability": "lib.luxon" } } } ``` An approved library runs **inside the sandbox at the importing mod's own privilege**: full-fidelity calls, no JSON boundary, and no new power granted. The manifest declares the allow-list; the host supplies each library's source when it constructs the runtime. A library must be **import-clean** (a self-contained bundle with no imports of its own), and its `capability` scope must be declared in `capabilities` like any other gate. Contrast with bindings: a binding runs *host-side* with host privilege behind a JSON boundary; a library runs *sandbox-side* with mod privilege. Pure compute belongs in a library; privileged operations stay bindings. Full semantics, the resolution order, and the import-clean rule live in [Modules — Approved Libraries](/spec/modules/#approved-libraries). ### Library Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `description` | string | yes | What the library provides to mod code | | `capability` | capabilityRef | no | Capability a mod must hold to import it. If omitted, any mod may import it. Checked at link time under subsumption (`lib` ⊇ `lib.doc`) | | `version` | string | no | The version or semver range the host ships (the contract mod authors compile against) | | `deprecated` | string | no | If present, the library is deprecated; the value says what to use instead | ## Execution Limits The `limits` section sets 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 at runtime. They exist in the manifest so that the application author can declare sensible defaults for their use case: a game mod system might allow 100ms per frame tick, while a data processing tool might allow 30 seconds. ## Adoption Tiers The manifest supports xript's four adoption tiers through progressive complexity. ### Tier 1: Expressions Only The simplest manifest. No bindings, no capabilities. The application uses xript purely as a safe eval replacement for user-provided expressions. ```json { "xript": "0.7", "name": "calculator" } ``` The runtime provides only the JavaScript language itself — no host bindings. This is useful for formula fields, template expressions, and user-defined calculations. ### Tier 2: Simple Bindings The application exposes a few functions. No capabilities needed because everything exposed is inherently safe. ```json { "xript": "0.7", "name": "my-game", "version": "1.0.0", "title": "My Game", "bindings": { "getPlayerName": { "description": "Returns the current player's display name.", "returns": "string" }, "getHealth": { "description": "Returns the player's current health (0-100).", "returns": "number" }, "log": { "description": "Logs a message to the mod console.", "params": [ { "name": "message", "type": "string" } ] } } } ``` ### Tier 3: Advanced Scripting Namespaces organize a rich API. Capabilities gate sensitive operations. Custom types describe complex data. Examples document usage. See the [game mod system example](../examples/game-mod-system/) for a full tier 3 manifest and walkthrough. ### Tier 4: Full Feature Slots and mod manifests. The host declares typed slots; mods fill them: fragments that bind to host state and handle interaction, roles the host resolves, event handlers the host fires. See the [UI dashboard example](../examples/ui-dashboard/) for a full tier 4 integration. ## Manifest Inheritance (`extends`) A manifest may inherit from one or more base manifests via the optional top-level `extends` field (a path string or an array of path strings): ```jsonc { "xript": "0.7", "extends": "./host.json", "name": "my-workflow", "bindings": { /* only new bindings; base bindings are merged in */ } } ``` Resolution happens before schema validation, performed identically by loaders and tools: - **Maps merge**: `bindings`, `capabilities`, `hooks`, and `types` are key-merged; the child augments the base. - **Arrays append**: `slots` append, deduped by slot `id`. - **Scalars: child wins**: `name`, `version`, `title`, `description`, `xript`. - **Transitive with cycle detection**: a base may itself `extends` another; cycles error. - **Paths are relative** to the extending manifest's location. Remote and URL bases are not supported in this version. When `extends` is an array, bases merge left-to-right (the child applies last). The resolved manifest is a flat, schema-valid manifest; the runtime never sees `extends` after resolution. A name present in both base and child resolves by one of three moves: **add** a name the base never declared, **fill** an abstract base type, or **refine** a concrete one with `refines: true`. An un-opted concrete-name collision is an error, not a silent override. The full model (abstract types, the three moves, deep-merge semantics, and the `abstract-type-unfilled` lint) is documented in [Manifest Extension and Inheritance](/spec/extends/). ## Mod Manifest `family` The mod manifest carries an optional top-level `family` string (pattern `^[a-z][a-z0-9-]*$`) for host-side grouping of addons (e.g. a nav rail). When absent, hosts fall back to name-prefix heuristics. The runtime stores and round-trips `family` but does not branch on it; grouping is host policy. `display_name` is intentionally not added; the existing `title` field covers it. ## Host-Invokable Exports A mod's `entry` block may declare named exports the host can invoke and whose return value it honors: ```jsonc { "entry": { "script": "main.js", "format": "script", "exports": { "transcribe": { "description": "Transcribe an audio clip to text.", "params": [{ "name": "audioUrl", "type": "string" }], "returns": "string", "capability": "audio-read" } } } } ``` The bare `entry: "main.js"` / `entry: ["a.js", "b.js"]` forms remain valid (script mode, no exports). The entry script registers each declared export via the runtime-injected `xript.exports.register(name, fn)`; the host invokes by name with JSON-serializable args and receives a JSON-serializable result. Invoking an undeclared or unregistered export, or an export that throws, surfaces a typed invocation error. An export may declare a required `capability`; invoking it without the grant throws a capability-denied error. **Streaming (partial results) is not yet specified.** Only request → single-response is defined in this version. ## Role Slots and Resolution A role slot (`accepts: ["application/x-xript-role"]`) is a host-declared plug-point that any mod can fill. Instead of core UI hardcoding a mod-specific global function name, the host declares a role slot, mods fill it with a logical-to-concrete function map, and the host asks the runtime to resolve the slot, getting back the mod that fills it plus the map from logical method names to the concrete function names that mod registered. A mod fills a role slot through its `fills` surface (see [mod-manifest.md](/spec/mod-manifest/)). The fill is the `fns` map: ```json { "fills": { "clipboard-history": [ { "fns": { "query": "clipHistory_query", "restore": "clipHistory_restore", "togglePin": "clipHistory_togglePin", "setTags": "clipHistory_setTags", "delete": "clipHistory_delete", "clear": "clipHistory_clear", "getImage": "clipHistory_getImage" } } ] } } ``` - The slot id (`clipboard-history`) is a lowercase-hyphen identifier (`^[a-z][a-z0-9-]*$`), the same vocabulary discovery results use. - `fns` is an **object map** from logical method name to the concrete export or registered-global function name. The host calls `fns.query`; it is never a positional list. ### Resolution The host resolves a role slot through the runtime's resolver API (`resolve_role` / `resolveRole` / `ResolveRole` and the `*_all` variants). Resolution is pure data lookup over loaded mods: 1. Iterate loaded mods in **load order** (first-installed-wins). 2. Collect every mod that fills the requested role slot; that ordered list is the result of `resolve_role_all`. 3. For `resolve_role`: if the host supplied a preference (a flat `role → addon-name` map on `RuntimeOptions`) that names a candidate, return that candidate; otherwise return the first candidate; otherwise `null`/`None`. A resolution returns `{ addon, role, fns }` where `addon` is the filling mod's `name` and `fns` is the winning fill's declared map verbatim. ### Mechanism, not policy - **xript never calls the resolved fns.** It returns the name map; the host invokes the concrete functions through its existing export or binding path. - **Filling a role slot grants no capability.** The functions it points at remain ordinary exports/bindings gated by their own capabilities. Default-deny is preserved. - A role slot with no fill resolves cleanly to `null`/`None`, never an error. - xript stores no preference state and persists nothing; the preference map is host-supplied per run, driven from the host's own settings. - `resolve_role` returns only the winner; `resolve_role_all` exposes the full ordered candidate set so the host can build its own picker UI. ## Schema Evolution The manifest schema will evolve as xript matures. The `xript` field enables runtime compatibility: - **0.x** versions may introduce breaking changes between minors - **1.0** and beyond will follow semver: minors add, majors break Runtimes should validate the `xript` field first and reject manifests with unsupported spec versions with a clear error message. ## Domain Schema Extension xript is an extensibility substrate, and that posture extends to its own vocabulary. The core manifest schema is meant to be *extended*, not fenced off: a domain (a particular kind of host, a product family, a deployment context) can add its own top-level surfaces to the manifest and still validate cleanly. ### Extending the Core Vocabulary The top-level manifest object uses `unevaluatedProperties: false` rather than a closed `additionalProperties: false`. A domain overlay composes the core schema with its own surfaces: ```json { "$schema": "https://example.dev/schemas/my-domain-manifest.schema.json", "allOf": [ { "$ref": "https://xript.dev/schema/manifest/v0.7.json" }, { "properties": { "myDomainSurface": { "type": "object" } } } ] } ``` Because validation evaluates the composed branches together, properties the overlay introduces are recognized and the manifest still validates against core. Top-level extension is open in exactly this way; deeper objects (bindings, slots, types, and the rest) stay closed, so the loosening is scoped to where a domain legitimately needs room and nowhere else. ### Honoring the Declared `$schema` A manifest may name the schema it was written against via the standard `$schema` keyword. Validation honors that declaration rather than always validating against bundled core. Resolution proceeds in order: 1. **Known schema id/URI** → its bundled local copy. Core's own URI resolves to the bundled core schema; a domain schema the validator already knows resolves to its bundled copy. 2. **Local path**, relative to the manifest → the schema at that path, resolved the same way `extends` resolves a base path. 3. **Remote `http(s)` URL** → fetched, with a local cache keyed by URL. A cache hit uses the cached copy; the resolved schema is pinned so a given manifest validates reproducibly across runs. When the network is unavailable or a remote schema is uncached and unreachable, validation **falls back to bundled core with a surfaced warning**; it never hard-fails on a schema fetch. Openness beats brittleness: a host should be able to validate the parts of a manifest it understands even when a domain schema is momentarily out of reach. Remote resolution is **open by default.** A host opts *out* of openness by setting an allowlist of permitted schema origins, or by disabling remote resolution entirely, rather than opting in. Reflexive lockdown is off-brand for an extensibility substrate; a restriction is justified only where it buys real security or convenience the framework could not otherwise provide. This is safe to honor because **schema validation is not the security boundary; the capability model is.** A declared schema describes shape; it grants no capability and confers no power. Validating against a domain or remote schema cannot widen what a script may do. The real concerns a remote schema raises are operational (offline behavior, reproducibility, and fetch safety), and those are handled by the cache, the schema pin, the bundled-core fallback, and the optional origin restriction, not by refusing to look. ---------------------------------------- # Manifest Inheritance (extends) Source: https://xript.dev/spec/extends/ A manifest may build on one or more base manifests via the optional top-level `extends` field. Inheritance lets a family of manifests share a common floor, a **base manifest** that declares the surface every consuming host has in common, while each consuming host extends that floor with its own additions, fills, and refinements. This document is the normative reference for the `extends` model: the canon-as-base premise, resolution order, abstract types, the three inheritance moves, the `refines` marker, the collision guard, and the `abstract-type-unfilled` lint. ## Canon as a Base Manifest A base manifest is a plain data file: an ordinary, schema-valid manifest with no special status. It is sometimes called *canon*: a shared floor that a group of related manifests agree to build on. Canon declares the bindings, capabilities, slots, and types the group holds in common, and nothing in the base is a cage; a consuming host is always free to add surface the base never knew about. A consuming host (an **extending manifest**) opts in by naming the base: ```jsonc { "xript": "0.7", "extends": "./base.json", "name": "consuming-host", "types": { /* additions, fills, and refinements over the base */ } } ``` The base has no knowledge of its extenders. Inheritance flows one way: a base is authored standalone, and any number of siblings may extend the same base independently. ## Resolution `extends` is a path string or an array of path strings, resolved **before** schema validation, identically by loaders and tools. Resolution flattens base-then-child: - A base is resolved first (recursively, since a base may itself `extends` another). - The child is then merged on top. - When `extends` is an array, bases merge left-to-right; the child applies last. The result is a single flat, schema-valid manifest. The runtime never sees `extends` after resolution; inheritance is a build-time concern, not a runtime one. - **Maps** (`bindings`, `capabilities`, `hooks`, `types`) are key-merged. - **Slots** append, keyed by `id`. - **Scalars** (`name`, `version`, `title`, `description`, `xript`) are child-wins. - **Paths** are relative to the extending manifest's location. Remote and URL bases are not supported in this version. - **Cycles error.** A transitive `extends` chain that loops back on itself is a resolution error. How a name that appears in *both* base and child is resolved depends on which of the three moves applies. ## Abstract Types A type definition carrying `"abstract": true` is **declared, described, and contract-bearing, but unpopulated**. It supplies neither `fields` nor `values`; it is a typed hole the base leaves open for an extending manifest to fill. ```json { "types": { "StatusCode": { "description": "A host-defined code classifying the outcome of an operation.", "abstract": true } } } ``` An abstract type is a *contract*: 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. Each extending manifest decides what the contract is filled with. ```json { "types": { "StatusCode": { "description": "A host-defined code classifying the outcome of an operation.", "abstract": true }, "Envelope": { "description": "A response wrapper every operation returns.", "fields": { "status": { "type": "StatusCode", "description": "The outcome classification." }, "payload": { "type": "string", "optional": true, "description": "The result body, when present." } } } } } ``` Here the base declares an abstract `StatusCode` and a concrete `Envelope` whose `status` field references it. `Envelope` is complete; `StatusCode` is a hole. An extending manifest must fill `StatusCode` before the resolved manifest is sound; see [the lint](#linting). A slot's payload schema may likewise reference an abstract type by name; the reference resolves to the concrete fill once the extending manifest supplies it. ## The Three Moves: Add, Fill, Refine When an extending manifest declares a name, exactly one of three moves applies. The moves are distinguished by **intent**, signalled by what the base declared and by the markers the child carries. ### 1. Add New The child declares a type, slot, or capability name the **base never declared**. This is purely additive: no marker, no collision, no ceremony. A sibling is free to declare whatever the base does not know about; canon is a shared floor, never a cage. ```json { "types": { "RetryPolicy": { "description": "How a consuming host retries a failed operation.", "fields": { "maxAttempts": { "type": "number", "default": 3 }, "backoff": { "type": "string", "enum": ["fixed", "exponential"], "default": "exponential" } } } } } ``` `RetryPolicy` is unknown to the base, so it simply joins the resolved manifest. This is the existing, unchanged behavior. ### 2. Fill The child redeclares an **abstract** base type name, supplying concrete `fields` and/or `values`. This is **allowed without any marker**: the base being abstract *is* the opt-in signal. The concrete child definition replaces the abstract stub in the resolved manifest. ```json { "extends": "./base.json", "name": "consuming-host", "types": { "StatusCode": { "description": "The set of outcome codes this host recognizes.", "values": ["ok", "retry", "denied", "error"] } } } ``` After resolution, `StatusCode` is the concrete enum above, and the base's `Envelope.status` reference now resolves to it. Each sibling that extends the same base may fill `StatusCode` with a different concrete shape; the contract is shared, the fill is local. ### 3. Refine The child redeclares a **concrete** base type (or slot) name carrying `"refines": true`. The child **deep-merges onto the base**: child members win key-by-key, and base members the child omits are retained. ```json { "extends": "./base.json", "name": "consuming-host", "types": { "Envelope": { "refines": true, "fields": { "payload": { "type": "string", "description": "The result body, always present in this host." }, "traceId": { "type": "string", "description": "Correlation id for this host's tracing." } } } } } ``` After resolution, `Envelope` retains the base's `status` field, takes the child's overridden `payload` field, and gains the new `traceId` field. The child did not have to restate `status` to keep it. Without `refines: true`, redeclaring a concrete base name is an **error**; see [collisions](#collisions). The marker is a deliberate opt-in: refinement is intentional, so a refine must say so. ## Deep-Merge Semantics Refinement deep-merges recursively over `fields`: - For each field, a child field **replaces** the base field of the same key. - Base fields the child does not mention are **retained**. - A field whose value is itself an object is merged recursively by the same rule. - `values` (enum members) and any other array members are replaced **wholesale**; there is no element-wise array merge. The `refines` marker itself is consumed during resolution and does not appear in the resolved manifest. Slots refine by the same shape. A child slot redeclaring a base slot `id` is permitted only with `"refines": true`, and deep-merges onto the base slot, including its `payload` JSON Schema. The merged slot keeps the base's fields the child omits and takes the child's overrides; the `payload` contract itself deep-merges key-by-key like any other object. A base declares the payload its slot fills must satisfy, and an extender tightens it: ```json { "extends": "./base.json", "name": "consuming-host", "slots": [ { "id": "event.commit", "refines": true, "payload": { "required": ["id", "author"], "properties": { "author": { "type": "string", "description": "Who authored the commit." } } } } ] } ``` If the base slot's payload was `{ "type": "object", "required": ["id"], "properties": { "id": { "type": "string" } } }`, the resolved slot keeps `type` and the base `id` property, takes the child's `required` array wholesale, and gains the `author` property; the schema merges by the same per-key rules as any other object. ## Collisions A concrete-name collision that is **not** opted into is a hard guard against accidents: - A child redeclaring a **concrete** base type name **without** `refines: true` is a resolution error. - A child redeclaring a base **slot** `id` **without** `refines: true` is a resolution error. - A child redeclaring a base `binding`, `capability`, or `hook` name is a resolution error; these maps are collision-as-error throughout, and fill and refine are type and slot concerns. - A cross-base collision (two bases in an `extends` array declaring the same concrete name) is a resolution error. These errors are thrown at resolution time, before validation and before lint. They are not warnings and cannot be suppressed; silently overriding inherited surface is exactly the accident the guard exists to catch. To override deliberately, fill an abstract type or refine a concrete one. ## Linting Resolution tracks **provenance**: which surface in the resolved manifest is local to the child and which was inherited from a base. One lint draws on that provenance. - **`abstract-type-unfilled`** (severity: **error**) — a resolved host that inherits an abstract type and leaves it abstract (never fills it) is a defect. An abstract type is a contract hole; shipping a host with the hole still open means a referenced type has no concrete shape. Fill it, or stop inheriting it. A locally-declared abstract type is **not** flagged; declaring an abstract type for one's own extenders to fill is legitimate authorship, not a defect. The lint fires only when the abstract type was *inherited* and left unfilled. A filled or refined inherited surface is **legitimately used**. Filling an abstract type or refining a concrete one must not trip dead-slot or vestigial-capability findings; the inherited surface is in active use, not vestigial. ---------------------------------------- # Mod Manifest Source: https://xript.dev/spec/mod-manifest/ A mod manifest is a JSON file declaring what a mod needs from a host and what it contributes back. It is distinct from the app manifest: the app manifest describes the surface a host exposes; the mod manifest describes the code and contributions a mod plugs into that surface. The schema lives at [`mod-manifest.schema.json`](/schema/mod-manifest/v0.7.json). ## The Shape: Host Slots, Mod Fills A host declares a surface of named, typed plug-points called **slots**. A mod engages that surface two ways: - **bindings** — callables the host implements; the mod *calls* them. - **fills** — typed plug-points the host declares as slots; the mod *fills* them. Everything a mod contributes is a fill. 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 target slot's `accepts` type governs what a valid fill looks like and what the host does with it: mount it, call it, resolve it, or fire it. See [manifest.md](/spec/manifest/) for how a host declares slots. ## Required Fields | Field | Type | Description | |-------|------|-------------| | `xript` | string | Spec version this mod targets (e.g. `"0.3"`) | | `name` | string | Machine-readable identifier (`^[a-z][a-z0-9-]*$`, max 64 chars) | | `version` | string | Mod version (semver) | ## Optional Fields | Field | Type | Description | |-------|------|-------------| | `title` | string | Human-readable display name | | `description` | string | Brief description for users | | `author` | string | Author name or handle | | `family` | string | Host-side grouping key (`^[a-z][a-z0-9-]*$`) | | `capabilities` | string[] | Capabilities this mod requires from the host | | `entry` | object \| string \| string[] | The mod's code and its callable API | | `fills` | object | Contributions, keyed by host slot id (see below) | ## Capabilities `capabilities` is a flat array of capability names the mod needs, what it *takes*. It is the gate on every binding the mod calls and every slot it fills. A mod that fills a capability-gated slot, or calls a capability-gated binding, must list that capability here. ```json { "capabilities": ["ui-mount", "audio-read"] } ``` Declaring a capability does not grant it. The host decides what to grant at load time; an ungranted capability blocks the bindings and fills that depend on it. ## Entry The `entry` block declares the mod's code and the named API the host can invoke. ```jsonc { "entry": { "script": "main.js", "format": "script", "exports": { "transcribe": { "description": "Transcribe an audio clip to text.", "params": [{ "name": "audioUrl", "type": "string" }], "returns": "string", "capability": "audio-read" } } } } ``` The bare `entry: "main.js"` and `entry: ["a.js", "b.js"]` forms remain valid (script mode, no exports). The entry script registers each declared export via the runtime-injected `xript.exports.register(name, fn)`; the host invokes by name with JSON-serializable args and receives a JSON-serializable result. Invoking an undeclared or unregistered export, or an export that throws, surfaces a typed invocation error. An export may declare a required `capability`; invoking it without the grant throws a capability-denied error. **Streaming (partial results) is not yet specified.** Only request → single-response is defined in this version. Slot fills reference these exports by name (see the fill shapes below). ## Fills `fills` is the canonical contribution surface. It is an object keyed by host slot id; each value is an array of fill entries: ```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 engages exactly one slot: the key it lives under. The inner shape of a fill entry is governed by that slot's `accepts` type, which the host owns. The mod manifest does not redeclare the slot's type; it conforms to it. ### Representative Fill Shapes The `accepts` type a slot declares determines the fill's shape and what the host does with it. **Fragment-format slot** (`accepts` names a fragment format, e.g. `text/html+jsml`, `application/jsml+json`). The fill is an inert fragment the host mounts. The [fragment protocol](/spec/fragments/) governs this slot type — `data-bind`, `data-if`, the command buffer, and sanitization are its semantics. ```json { "format": "text/html+jsml", "source": "panel.html", "bindings": [{ "name": "health", "path": "player.health.val" }], "handlers": [{ "selector": "[data-action='heal']", "on": "click", "handler": "onHealClicked" }] } ``` **Code-renderer slot** (`accepts` names an executable renderer kind, e.g. `application/javascript+esm`). The fill points the host at code it loads and runs to paint the slot. ```json { "kind": "text", "entry": "dist/text.js", "label": "Plain Text", "icon": "file-text" } ``` **Role slot** (`accepts` is `application/x-xript-role`). The fill maps logical method names to the concrete `entry` exports that implement them. The host resolves the role and calls the named functions itself. ```json { "fns": { "transcribe": "transcribeAudio", "detectLanguage": "detectLang" } } ``` **Event/hook slot** (`accepts` is `application/x-xript-hook`). The fill names a handler export the host calls when the event fires. See [hooks.md](/spec/hooks/). ```json { "handler": "onStartup" } ``` **Data slot** (`accepts` names a data format the host reads, e.g. `application/json`). The fill is pure metadata — no fragment, no code, no handler. The host reads it and applies its own policy. This is the canonical shape for grouping and curation surfaces: a host that wants mods to declare collection membership, ordering, or pack-style bundles declares a data slot with a `payload` schema describing the metadata, and a "grouping mod" is simply a mod whose only contribution is a data fill. ```json { "members": ["mod-one", "mod-two", "mod-three"], "label": "Starter Pack" } ``` The slot's `payload` schema validates the fill's shape (`xript validate --cross` enforces it); what the host *does* with the metadata (discovery gating, ordering, bundling) is host policy, deliberately outside the spec. No dedicated grouping primitive exists because none is needed: a typed slot plus a payload-schema'd data fill already carries the whole pattern. ### Multiple Fills per Slot The value under each slot id is always an array. A slot the host declared with `"multiple": true` accepts more than one fill; a single-fill slot resolves a deterministic winner (the [fragment protocol](/spec/fragments/) defines ordering for fragment-format slots). Authoring a single fill still uses a one-element array. ## Validation Contract When a mod is loaded against a host, the runtime validates each slot id in `fills`: 1. The slot id must exist in the host's `slots` (matched by `id`). 2. If the slot declares a `capability`, the mod must list that capability in its `capabilities`. The runtime does **not** police the inner shape of a fill; that is the slot type's contract, enforced by whatever consumes the fill (the fragment processor, the renderer, the role resolver, the hook dispatcher). A fill into an undeclared slot, or into a capability-gated slot the mod lacks the capability for, is an error. ## Deprecated Aliases Two earlier top-level surfaces fold into `fills`. Validators still accept them and emit a deprecation warning so migration is smooth; new manifests should use `fills` only. ### `fragments` → fragment-format slot fills The former top-level `fragments` array is a list of fills of fragment-format slots. Each legacy fragment's `slot` field becomes the `fills` key; the rest of the entry is the fill. ```jsonc // deprecated { "fragments": [ { "id": "health-panel", "slot": "sidebar.left", "format": "text/html", "source": "panel.html" } ] } // equivalent { "fills": { "sidebar.left": [ { "format": "text/html", "source": "panel.html" } ] } } ``` ### `contributions.provides` → role slot fills The former `contributions.provides` array is a list of fills of role-type slots. Each entry's `role` becomes the `fills` key; its `fns` map becomes the fill. ```jsonc // deprecated { "contributions": { "provides": [ { "role": "clipboard-history", "fns": { "query": "clipHistory_query" } } ] } } // equivalent { "fills": { "clipboard-history": [ { "fns": { "query": "clipHistory_query" } } ] } } ``` `contributions.slots` was always just fills — its entries move under `fills` unchanged. ## Format Renderers Are Not Manifest Concepts A format renderer (a terminal-widget renderer, a DOM fragment processor, a future native-widget renderer) is runtime infrastructure, not a slot or a fill. It paints a fragment of format `F` onto a target. A slot's `accepts` type names the format the runtime must be able to render; the renderer that does the painting lives in the runtime, not the manifest. Do not model renderers as slots or fills. ---------------------------------------- # Fragment Protocol Source: https://xript.dev/spec/fragments/ The fragment protocol is the semantics of one slot type: the **fragment-format slot**. A host declares a slot whose `accepts` names a fragment format (`text/html+jsml`, `application/jsml+json`, `text/html`); a mod fills it with an inert fragment — markup plus declared data bindings and DOM event handlers. A fragment is a fill of a fragment-format slot; it is not a separate top-level mod concept. The runtime sanitizes fragment content, resolves data bindings, evaluates conditional visibility, and routes events through the sandbox. See [mod-manifest.md](/spec/mod-manifest/) for the `fills` surface and the other slot types (code-renderer, role, event), and [manifest.md](/spec/manifest/) for how a host declares slots and their `style` modes. ## Fragment-Format Slots A host declares a fragment-format slot in its app manifest `slots` array. The slot's `accepts` lists the fragment formats it takes; `style` controls how host styles reach the mounted fragment. ```json { "slots": [ { "id": "sidebar.left", "accepts": ["text/html+jsml"], "capability": "ui-mount", "multiple": true, "style": "isolated" } ] } ``` The full slot field reference and the `inherit` / `isolated` / `scoped` styling modes live in [manifest.md](/spec/manifest/). ## Fragment Fills A mod fills a fragment-format slot through its `fills` surface, keyed by the host slot id. The fill carries markup and optionally declares data bindings and DOM 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 } ] } } ``` ### Fill Fields | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `format` | string | yes | — | Fragment format of the content (must be in the slot's `accepts`) | | `source` | string | yes | — | File path (relative to mod root) or inline markup | | `inline` | boolean | no | false | When true, `source` is inline markup (JSML) | | `bindings` | Binding[] | no | — | Data bindings | | `handlers` | Handler[] | no | — | DOM event handlers (entries shaped `{ selector, on, handler }`) | | `events` | Handler[] | no | — | **Deprecated** alias for `handlers`. Accepted for back-compat; if both are present, `handlers` wins | | `id` | string | no | — | Optional fill identifier, used for ordering tie-breaks and the sandbox fragment API | | `priority` | integer | no | 0 | Ordering within the slot (higher = earlier) | > **`events` → `handlers` migration.** The handler array was renamed: its entries are event *handlers* (`{ selector, on, handler }`), not events, so `handlers` names what it carries. `events` stays accepted as a deprecated alias — a reader honors `handlers` or `events`, and when both appear `handlers` wins. Migrate by renaming the key; the entry shape is unchanged. This mirrors the `hooks` → event-typed-slots back-compat precedent: the old name keeps working, the new name is preferred. ### Inline Fills (JSML) For simple fragments, the source can be inline markup: ```json { "fills": { "header.status": [ { "id": "status-text", "format": "text/html", "source": "0 / 0", "inline": true, "bindings": [ { "name": "health", "path": "player.health.val" }, { "name": "maxHealth", "path": "player.health.max" } ] } ] } } ``` ### Fill Ordering When multiple fills target the same slot (requires `multiple: true`): 1. Sort by `priority` descending (higher values render first) 2. Break ties alphabetically by fill `id` Hosts can override this ordering via user preferences. > The former top-level `fragments[]` array is a deprecated alias for fragment-format slot fills: each legacy fragment's `slot` becomes the `fills` key and the rest of the entry becomes the fill. Validators still accept it with a deprecation warning. See [mod-manifest.md](/spec/mod-manifest/#deprecated-aliases). ## Data Binding: `data-bind` The `data-bind` attribute is the mechanism for wiring 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

``` ### Resolution 1. The fragment declares bindings mapping local names to host data paths 2. The runtime resolves each path against the host's data layer (e.g. `"player.health.val"` → traverse `data.player.health.val`) 3. For text elements (`span`, `div`, `p`, etc.): the runtime sets `textContent` 4. For input elements (`input`, `textarea`, `select`): the runtime sets `value` 5. On data change, the runtime re-resolves only changed bindings and patches the affected elements ### Performance `data-bind` attributes persist in the DOM. The runtime maintains a map of attribute → element references for O(1) updates. This supports 60fps update rates for game-loop-driven UI without template re-parsing or diffing. ### Two-Way Binding For input elements, the runtime can both push values (host → fragment) and listen for changes (fragment → host). Write-back is explicit: use the `handlers` array to declare handlers for `input` or `change` events on bound elements. ## 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!
``` ### Evaluation 1. The runtime extracts the expression string from `data-if` 2. The expression is evaluated using the same safe evaluator that powers tier 1 (no `eval`, no `Function`, no code generation — the sandboxed expression engine) 3. Binding values are injected as variables in the expression context 4. Truthy result → element is visible. Falsy → element is hidden (via `display: none` or DOM removal, host's choice) 5. On binding change, the runtime re-evaluates and toggles only if the boolean result changed ### 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 constructs. Everything beyond binding and conditional visibility goes through the sandbox fragment API. ## Event Routing Handlers are declared in the fragment manifest, not in the markup. The runtime attaches listeners to matching elements and delegates to sandbox functions. ```json "handlers": [ { "selector": "[data-action='heal']", "on": "click", "handler": "onHealClicked" } ] ``` The deprecated `events` key is accepted as an alias; see [Fill Fields](#fill-fields). ### How It Works 1. After mounting the fragment, the runtime queries elements matching each handler's `selector` 2. For each match, the runtime attaches a listener for the specified `on` event 3. When the event fires, the runtime calls the named `handler` function in the mod's sandboxed script 4. The handler receives event data (target element info, event type) 5. Multi-match is intentional: `[data-action='heal']` matching three buttons wires all three to the same handler ### Format-Specific Targeting For `text/html` fragments, `selector` is a CSS selector. For other formats, the targeting mechanism is defined by the format (e.g. widget IDs for terminal UI formats). ## Fragment Lifecycle Fragments have five lifecycle events, consistent with xript's existing hook vocabulary: | Lifecycle | When | Typical Use | |-----------|------|-------------| | `mount` | Fragment inserted into slot, bindings resolved | Initialize state, set up timers | | `unmount` | Fragment removed from slot | Cleanup, release resources | | `update` | Bound data changed | Reflect new state, complex updates | | `suspend` | Host context changed (e.g. scene transition) | Pause timers, reduce activity | | `resume` | Fragment reactivated after suspend | Resume timers, refresh state | The host fires lifecycle events via the runtime. Mods register handlers through the sandbox fragment API. ## Sandbox Fragment API For logic beyond `data-bind` and `data-if`, mods use the sandbox fragment API. This provides imperative fragment manipulation from within the sandboxed script. ```javascript hooks.fragment.update("health-panel", (bindings, fragment) => { fragment.toggle(".low-health-warning", bindings.health < 50); fragment.addClass(".health-bar", bindings.health < 20 ? "critical" : "normal"); fragment.replaceChildren(".inventory-list", bindings.inventory.map(item => `
  • ${item.name} (x${item.count})
  • `) ); }); ``` ### Command Buffer Pattern The `fragment` object passed to callbacks is a proxy, not a live DOM reference. Method calls accumulate an operation list (command buffer). After the callback returns, the runtime passes the operation list to the host, which applies the mutations. The sandbox never touches the real DOM. ### Available Operations | Method | Arguments | Effect | |--------|-----------|--------| | `toggle(selector, condition)` | CSS selector, boolean | Show/hide matching elements | | `addClass(selector, className)` | CSS selector, string | Add class to matching elements | | `removeClass(selector, className)` | CSS selector, string | Remove class from matching elements | | `setText(selector, text)` | CSS selector, string | Set text content of matching elements | | `setAttr(selector, attr, value)` | CSS selector, string, string | Set attribute on matching elements | | `replaceChildren(selector, html)` | CSS selector, string/string[] | Replace children of matching elements | ### Lifecycle Registration ```javascript hooks.fragment.mount("health-panel", (fragment) => { /* called on mount */ }); hooks.fragment.unmount("health-panel", (fragment) => { /* called on unmount */ }); hooks.fragment.update("health-panel", (bindings, fragment) => { /* called on data change */ }); hooks.fragment.suspend("health-panel", (fragment) => { /* called on suspend */ }); hooks.fragment.resume("health-panel", (fragment) => { /* called on resume */ }); ``` ## HTML Sanitization For `text/html` fragments, the runtime sanitizes content before the host ever sees it. The guarantee to hosts: what you're mounting is inert. ### Allowed Elements Structural and presentational elements: `div`, `span`, `p`, `h1`-`h6`, `ul`, `ol`, `li`, `dl`, `dt`, `dd`, `table`, `thead`, `tbody`, `tfoot`, `tr`, `td`, `th`, `caption`, `col`, `colgroup`, `figure`, `figcaption`, `blockquote`, `pre`, `code`, `em`, `strong`, `b`, `i`, `u`, `s`, `small`, `sub`, `sup`, `br`, `hr`, `img`, `picture`, `source`, `audio`, `video`, `track`, `details`, `summary`, `section`, `article`, `aside`, `nav`, `header`, `footer`, `main`, `a`, `abbr`, `mark`, `time`, `wbr`, `style` (scoped). Interactive and form elements: `button`, `input`, `textarea`, `select`, `option`, `label`, `fieldset`, `legend`, `progress`, `meter`, `output`. SVG elements: `svg`, `g`, `defs`, `symbol`, `use`, `circle`, `ellipse`, `path`, `rect`, `line`, `polygon`, `polyline`, `text`, `tspan`. ### Stripped Elements Removed entirely (element and all children): `script`, `iframe`, `object`, `embed`, `form`, `base`, `link`, `meta`, `title`, `html`, `head`, `body`, `noscript`, `applet`, `frame`, `frameset`, `foreignObject`, `animate`, `set`. ### Allowed Attributes `class`, `id`, `data-*`, `aria-*`, `role`, `style`, `src` (safe URIs only), `alt`, `width`, `height`, `href` (safe URIs only), `target`, `rel`, `colspan`, `rowspan`, `scope`, `headers`, `lang`, `dir`, `title`, `tabindex`, `hidden`. Form attributes: `type`, `value`, `placeholder`, `name`, `for`, `checked`, `disabled`, `readonly`, `required`, `rows`, `cols`, `maxlength`, `minlength`, `min`, `max`, `step`, `pattern`, `open`, `low`, `high`, `optimum`. SVG attributes: `cx`, `cy`, `r`, `x`, `y`, `x1`, `y1`, `x2`, `y2`, `points`, `d`, `fill`, `stroke`, `stroke-width`, `opacity`, `transform`, `viewBox`, `preserveAspectRatio`, `xmlns`. ### Stripped Attributes All `on*` event attributes (`onclick`, `onerror`, `onload`, etc.), `formaction`, `action`, `method`, `enctype`. ### URI Sanitization `javascript:`, `vbscript:`, and `data:` URIs are stripped from `href` and `src` attributes. Exception: `data:image/png`, `data:image/jpeg`, `data:image/gif`, and `data:image/svg+xml` are allowed in `src` attributes only. ### Style Sanitization Within `