--- url: /guides.md --- # Guides This section contains narrative documentation that explains how runblox-core works and how to use it. Guides are read in order when learning the runtime; each one builds on the ones above it. For the full surface of a specific module, see the reference page under [`std:*`](/std/), [`lib:*`](/lib/), or [`vnd:*`](/vnd/) instead. ## Getting oriented [Getting started](/guides/getting-started) : Install runblox-core, run a first script, and understand the basic invocation model. [The sandboxing model](/guides/sandboxing-model) : What the engine strips from Lua, why those parts are stripped, and what the curated module surface replaces them with. [The module system](/guides/module-system) : How `require("ns:name")` resolves, the three namespaces, and the rules each one follows. [Error conventions](/guides/error-conventions) : When functions return `(value, err)` tuples and when they raise. How error prefixes are formatted. ## See also * [`std:*`](/std/) — Standard-library mirrors. * [`lib:*`](/lib/) — Project-native modules. * [`vnd:*`](/vnd/) — Vendored crates. * [RFCs](/rfcs/) — Cross-cutting design proposals. --- --- url: /guides/getting-started.md --- # Getting started This guide walks through installing runblox-core and running a first Luau script. It assumes a working command line and basic familiarity with Lua syntax. ## Installation Pre-built binaries for Linux, macOS, and Windows are published on [GitHub Releases](https://github.com/flying-dice/runblox-core/releases). The install scripts download the appropriate binary for the host platform and place it on the path. ### On Linux or macOS ```bash curl -fsSL https://raw.githubusercontent.com/flying-dice/runblox-core/main/scripts/install.sh | bash ``` ### On Windows ```powershell irm https://raw.githubusercontent.com/flying-dice/runblox-core/main/scripts/install.ps1 | iex ``` ### Environment variables The install scripts respect the following environment variables. `RUNBLOX_INSTALL_DIR` : The directory to install the binary into. Defaults to `~/.runblox/bin`. `RUNBLOX_VERSION` : The release tag to install. Defaults to `latest`. ## Running a script A Luau script is a plain text file with a `.luau` extension. The runblox-core binary takes one or more file paths or glob patterns and runs each matching file as an independent script. ### A first script The following script prints a greeting using the standard input/output module. ```lua local io = require("std:io") io.println("hello, runblox") ``` Save it as `hello.luau` and run it with the binary. ```bash runblox-core hello.luau ``` The output is the literal string `hello, runblox` followed by a newline. ### Running multiple scripts The binary accepts multiple paths and glob patterns, expanding them before execution. ```bash runblox-core 'tests/**/*.test.luau' ``` This invocation runs every file matching the glob pattern as an independent script. The shell quoting prevents the shell from expanding the glob; runblox-core's internal expansion picks up the pattern instead. ## What runs where Each script executes inside its own Luau virtual machine. The runtime maintains a pool of these virtual machines as *workers* and dispatches scripts across them. Globals, locals, and userdata constructed inside a worker are private to that worker; cross-worker state is exposed only through the explicit `std:workers` surface. For an explanation of why the engine uses this model and what it implies for script authors, see [The sandboxing model](/guides/sandboxing-model). ## See also * [The sandboxing model](/guides/sandboxing-model) — How the curated module surface replaces stripped Lua functionality. * [The module system](/guides/module-system) — How `require("ns:name")` resolves. * [`std:io`](/std/reference/io) — Standard input and output. * [`std:workers`](/std/reference/workers) — The worker pool and cross-worker shared state. --- --- url: /guides/sandboxing-model.md --- # The sandboxing model This guide explains how runblox-core sandboxes the Luau engine: which parts of Lua's standard library are stripped, why those parts are removed, and what the curated module surface replaces them with. Familiarity with Lua and basic operating-system concepts (filesystems, processes, network sockets) is assumed. ## Why sandbox Luau is a sandbox-friendly dialect of Lua. The standard distribution still ships modules that expose unrestricted operating-system access — `io.open`, `os.execute`, `package.loadlib`, and the `debug` library, among others. A general-purpose host runtime that simply embeds Luau and exposes those modules inherits whatever the script chooses to do with them. runblox-core takes the opposite default. The unsafe halves of the standard library are stripped at engine construction, and every capability a script needs — file access, network I/O, subprocess control, third-party crate functionality — is reintroduced through *explicit modules* under the `std:*`, `lib:*`, and `vnd:*` namespaces. Scripts cannot reach a capability that the host has not registered. ## What is stripped The following standard Lua surface is unavailable inside runblox-core's engine. | Stripped surface | Reason | |---|---| | `io.*` | File and stream I/O is reintroduced through [`std:fs`](/std/reference/fs) and [`std:io`](/std/reference/io). | | `os.*` | Process and time control is reintroduced through `std:thread`, `std:net`, and the worker model. | | `package.*` | The `require` resolver is replaced; see [The module system](/guides/module-system). | | `debug.*` | Reflection over running coroutines, registries, and upvalues is unsafe in a multi-tenant runtime. | | `loadfile`, `dofile` | File-driven script loading is replaced by the binary's argument-driven model. | The remaining standard surface — `string`, `table`, `math`, `coroutine`, `bit32`, `utf8`, the basic `print` family — is available unchanged. ## What replaces it Every capability that scripts need is reintroduced through one of three namespaces, chosen by the kind of capability: `std:*` : Capabilities that mirror Rust's standard library. Filesystem, networking, threading, collections, I/O. The surface is shaped so a reader fluent in Rust's `std::*` recognises the methods and semantics. See [`std:*`](/std/). `lib:*` : Blessed gap-fillers and project-native modules. JSON document handles, base64 encoding, the test runner, the VCR fixture system. See [`lib:*`](/lib/). `vnd:*` : Individual vendored Rust crates exposed to Lua. The module name matches the upstream crate name verbatim — `vnd:hyper` is the `hyper` crate, `vnd:serde_json` is the `serde_json` crate. See [`vnd:*`](/vnd/). > **Note:** A script cannot invent its own access to a stripped capability. Reaching into the host through FFI, loading shared libraries, or executing subprocesses are not available. The host runtime is the only thing that can extend the script's capability surface, and it does so by registering modules at engine construction. ## The worker model Scripts execute inside a pool of independent Luau virtual machines called *workers*. Each worker is a self-contained interpreter: globals, locals, upvalues, and any userdata constructed inside a worker are private to that worker. The worker pool size is fixed at process startup, and dispatch across workers is transparent — a script does not pick which worker it runs on. Cross-worker state is the exception, and it is opt-in. Specific userdata types (such as collections from `std:collections`, the JSON Object and Array handles from `lib:json`, and the TCP listener from `std:net`) implement a sharing contract. A script publishes a value once with `workers.shared(key, value)` and any worker calling the same key receives a handle to the same shared state. Other userdata types — and all plain Lua tables — cannot cross worker boundaries. For the full surface, see [`std:workers`](/std/reference/workers). ## See also * [The module system](/guides/module-system) — How `require("ns:name")` resolves and the rules each namespace follows. * [Error conventions](/guides/error-conventions) — How modules signal failures. * [`std:workers`](/std/reference/workers) — Worker pool, cross-worker state, and the shutdown signal. * [`std:fs`](/std/reference/fs) — Filesystem reads, writes, and metadata. * [`std:net`](/std/reference/net) — TCP listeners and sockets. --- --- url: /guides/module-system.md --- # The module system This guide explains how runblox-core resolves `require` calls, the three namespaces a module may belong to, and the rules each namespace follows. Familiarity with Lua's standard `require` and basic notions of namespacing is assumed. ## How `require` resolves The standard Lua `require` is replaced inside the engine. The replacement accepts a single argument of the form `"namespace:name"` — a colon-separated pair where the namespace is one of `std`, `lib`, or `vnd`, and the name is the module's identifier within that namespace. ```lua local fs = require("std:fs") local test = require("lib:test") local json = require("vnd:serde_json") ``` The resolver is a flat lookup table built once at engine boot. There is no path search, no file I/O, no fallback to the standard `package.path`. A `require` call either finds a registered module by exact name or it fails. ### Failure mode A `require` call for an unregistered name raises an error that names the requested module and lists the three valid namespace prefixes: ```text require: unknown module "vnd:hyperz" — expected std:, lib:, or vnd: ``` The error surfaces at the point of the call, not later when a missing function is invoked. This makes typos in module names visible immediately. ### Identity of returned tables `require` returns the module table itself, not a copy. Multiple callers — and multiple workers — receive handles to the same table. Writing into a module table observably mutates it for every caller. This matches Lua's standard `require` contract, with the caveat that modules in runblox-core are typically frozen at registration time and writes are rare. ## The three namespaces Every module belongs to exactly one namespace, picked at registration. The choice encodes what kind of capability the module provides and what rules it follows. ### `std:*` — standard-library mirrors The `std:*` namespace contains modules that mirror Rust's standard library. The naming and shape of each module's surface is chosen so a reader fluent in Rust's `std::*` recognises it without explanation: `std:collections` mirrors `std::collections`, `std:fs` mirrors `std::fs`, `std:net` mirrors `std::net`, and so on. A small number of `std:*` modules cover capabilities that Rust's standard library does not — `std:workers`, the worker pool substrate, is the principal example. These are admitted to `std:*` only when they are broadly needed and have no clean home elsewhere. For the catalogue, see [`std:*`](/std/). ### `lib:*` — blessed gap-fillers and project-native modules The `lib:*` namespace contains modules that are not direct mirrors of a Rust crate or standard module. Two kinds qualify: *gap-fillers* that plug a hole the standard library does not cover (such as `lib:base64`), and *project-native* modules written specifically for runblox-core (such as `lib:json` and `lib:test`). A module qualifies for `lib:*` when its purpose is well-defined, its surface is stable, and it is genuinely useful to most scripts. Speculative or experimental functionality belongs elsewhere until it has earned a place here. For the catalogue, see [`lib:*`](/lib/). ### `vnd:*` — vendored crates The `vnd:*` namespace exposes individual Rust crates to Lua. Each module wraps exactly one upstream crate, and the module name matches the crate name verbatim. ```lua local hyper = require("vnd:hyper") -- the `hyper` crate local serde_json = require("vnd:serde_json") -- the `serde_json` crate ``` There is no renaming, no aliasing, and no merging of multiple crates into a composite module. This naming rule is non-negotiable: it is what lets scripts reach for an upstream crate by its real name and find its real documentation without indirection. For the catalogue, see [`vnd:*`](/vnd/). ## Module construction Internally, every module exports a single Rust function with the signature `pub fn module(lua: &Lua) -> mlua::Result`. The engine calls this function once at registration time and stores the resulting table in the resolver's lookup map under the module's qualified name. Scripts never see the construction step; they see only the resulting table when they call `require`. This structure is documented for contributors writing new modules. Scripts do not interact with it. ## See also * [The sandboxing model](/guides/sandboxing-model) — How the module system fits into the broader sandbox. * [Error conventions](/guides/error-conventions) — How modules signal failures. * [`std:*`](/std/) — Standard-library mirrors. * [`lib:*`](/lib/) — Project-native modules. * [`vnd:*`](/vnd/) — Vendored crates. --- --- url: /guides/error-conventions.md --- # Error conventions This guide describes how runblox-core modules signal failure to Lua scripts. It covers the two failure-signalling shapes the runtime uses, when each one applies, how error messages are formatted, and how scripts handle them. Familiarity with Lua's `error`, `pcall`, and multi-value return is assumed. ## Two shapes of failure A function in runblox-core signals failure in one of two ways. `(value, err)` tuple : The function returns two values. On success, `value` holds the result and `err` is `nil`. On failure, `value` is `nil` and `err` is a string describing the failure. This shape is used for *recoverable* failures — conditions a script is expected to inspect and react to. Raised error : The function calls `error(...)`, propagating up the stack until caught by `pcall` or `xpcall`. This shape is used for *programming errors* — conditions that indicate a script bug rather than a runtime condition the script should handle. The choice of shape is documented per function on its module's reference page. The two shapes are not interchangeable: a function either consistently uses `(value, err)` or it consistently raises. ## When each shape applies A function returns the `(value, err)` tuple when its failure mode is something the caller might reasonably want to inspect and recover from. Examples include I/O failures (`std:fs.read_to_string` on a missing file), parser failures (`vnd:serde_json.from_str` on malformed input), and network failures (`vnd:hyper.get` against an unreachable host). A function raises when its failure mode indicates the script has misused the surface — passing an argument of the wrong type, calling a method on a closed handle, or calling `workers.shared` with a userdata type that does not opt into sharing. These are bugs in the script, not runtime conditions, and pretending otherwise would mask the bug. ## Error message format Every error message — whether returned in the `err` slot of a tuple or raised — begins with a qualified prefix that names the module and function that produced it. The prefix has the form `"module.function:"` (no namespace prefix in the message itself). ```text fs.read_to_string: No such file or directory (os error 2) serde_json.from_str: expected value at line 1 column 1 hyper.get: connection refused workers.shared: not a sharable userdata; expected one of std:collections.map, ... ``` This format is consistent across every module. Scripts can match against the prefix programmatically, and readers of stack traces can locate the failing call site without context. ## Handling failures in scripts ### Inspecting a tuple return A script handles a recoverable failure by inspecting the second return value. ```lua local fs = require("std:fs") local body, err = fs.read_to_string("config.json") if err then -- handle the failure: log, retry, fall back, etc. return end -- use `body` ``` The pattern is identical across every function that returns the tuple shape. There is no separate status code, no exception object, and no wrapping type to unpack. ### Catching a raised error A script catches a raised error with `pcall` (protected call). The standard Lua idiom applies unchanged. ```lua local workers = require("std:workers") local json = require("lib:json") local ok, err = pcall(function() workers.shared("k", json.null) -- json.null is not sharable end) if not ok then -- err contains the qualified message end ``` Scripts should reach for `pcall` when they genuinely need to recover from a programming error — for example, in a test runner or in a top-level supervisor that should report the failure rather than crash the worker. ## See also * [The sandboxing model](/guides/sandboxing-model) — Where errors fit into the broader engine model. * [The module system](/guides/module-system) — How `require` itself signals failure. * [`std:fs`](/std/reference/fs) — Example of a tuple-returning module. * [`std:workers`](/std/reference/workers) — Example of a module that raises on misuse. --- --- url: /std.md --- # `std:*`: Standard-library modules The `std:*` namespace exposes modules that mirror the Rust standard library. Each module's public surface is shaped so a reader fluent in Rust's `std::*` recognises the methods, semantics, and error behaviour without explanation. This namespace also contains a small number of modules that have no `std::` equivalent but cover capabilities that scripts cannot reasonably do without — `std:workers`, the worker-pool substrate, is the principal example. New modules are added to `std:*` only when they are both broadly needed and cleanly mappable to a Rust standard concept. ## Reference * [`std:collections`](/std/reference/collections) — Concurrent and shareable collection types. * [`std:fs`](/std/reference/fs) — Filesystem reads, writes, and metadata. * [`std:io`](/std/reference/io) — Standard input and output handles. * [`std:net`](/std/reference/net) — TCP listeners and sockets. * [`std:thread`](/std/reference/thread) — Thread-local primitives and sleep. * [`std:workers`](/std/reference/workers) — The worker pool, cross-worker shared state, and the shutdown signal. ## See also * [The module system](/guides/module-system) — How `require("std:name")` resolves. * [The sandboxing model](/guides/sandboxing-model) — What the standard surface replaces from Lua's stripped `os`, `io`, and `package`. * [`lib:*`](/lib/) — Project-native and gap-filling modules. * [`vnd:*`](/vnd/) — Vendored crates. --- --- url: /std/reference/collections.md --- # `std:collections` module **Experimental** The **`std:collections`** module exposes concurrent and shareable collection types — `map`, `counter`, and `mutex`. | Property | Value | |---|---| | Namespace | `std` | | Source | [`src/lua/std/collections.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/std/collections.rs) | | Tests | [`tests/std/collections.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/std/collections.test.luau) | | Stability | Experimental | | Mirror | [`std::collections`](https://doc.rust-lang.org/std/collections/) | ## Syntax ```lua local collections = require("std:collections") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("std:collections")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `std:*` modules fit into the engine. * [`std:*`](/std/) — Other modules in this namespace. --- --- url: /std/reference/fs.md --- # `std:fs` module **Experimental** The **`std:fs`** module mirrors filesystem reads, writes, and metadata operations from Rust's standard library. | Property | Value | |---|---| | Namespace | `std` | | Source | [`src/lua/std/fs.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/std/fs.rs) | | Tests | [`tests/std/fs.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/std/fs.test.luau) | | Stability | Experimental | | Mirror | [`std::fs`](https://doc.rust-lang.org/std/fs/) | ## Syntax ```lua local fs = require("std:fs") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("std:fs")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `std:*` modules fit into the engine. * [`std:*`](/std/) — Other modules in this namespace. --- --- url: /std/reference/io.md --- # `std:io` module **Experimental** The **`std:io`** module exposes standard input and output handles — `print`, `println`, `eprintln`, and friends. | Property | Value | |---|---| | Namespace | `std` | | Source | [`src/lua/std/io.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/std/io.rs) | | Tests | [`tests/std/io.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/std/io.test.luau) | | Stability | Experimental | | Mirror | [`std::io`](https://doc.rust-lang.org/std/io/) | ## Syntax ```lua local io = require("std:io") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("std:io")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `std:*` modules fit into the engine. * [`std:*`](/std/) — Other modules in this namespace. --- --- url: /std/reference/net.md --- # `std:net` module **Experimental** The **`std:net`** module exposes TCP listeners and connected sockets, mirroring Rust's networking primitives. | Property | Value | |---|---| | Namespace | `std` | | Source | [`src/lua/std/net.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/std/net.rs) | | Tests | [`tests/std/net.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/std/net.test.luau) | | Stability | Experimental | | Mirror | [`std::net`](https://doc.rust-lang.org/std/net/) | ## Syntax ```lua local net = require("std:net") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("std:net")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `std:*` modules fit into the engine. * [`std:*`](/std/) — Other modules in this namespace. --- --- url: /std/reference/thread.md --- # `std:thread` module **Experimental** The **`std:thread`** module exposes thread-local primitives and the `sleep` function. | Property | Value | |---|---| | Namespace | `std` | | Source | [`src/lua/std/thread.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/std/thread.rs) | | Tests | [`tests/std/thread.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/std/thread.test.luau) | | Stability | Experimental | | Mirror | [`std::thread`](https://doc.rust-lang.org/std/thread/) | ## Syntax ```lua local thread = require("std:thread") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("std:thread")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `std:*` modules fit into the engine. * [`std:*`](/std/) — Other modules in this namespace. --- --- url: /std/reference/workers.md --- # `std:workers` module **Stable** The **`std:workers`** module exposes the worker-pool model to scripts. It provides cross-worker shared state through [`workers.shared`](#workers-shared) and an observable shutdown signal through [`workers.shutdown_signal`](#workers-shutdown-signal). The pool itself is sized at process startup; `std:workers` does not expose pool construction or worker lifecycle to scripts. | Property | Value | |---|---| | Namespace | `std` | | Source | [`src/lua/std/workers.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/std/workers.rs) | | Tests | [`tests/std/workers.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/std/workers.test.luau) | | Stability | Stable | | Mirror | tokio runtime — no `std::` equivalent | ## Syntax ```lua local workers = require("std:workers") ``` The returned table exposes the module functions described below. ## Description runblox-core runs scripts in a pool of independent Lua virtual machines — *workers*. Each worker is a self-contained interpreter: globals, locals, upvalues, and any userdata constructed inside it are private to that worker. The pool is sized at process startup and tasks dispatch across workers transparently. Script authors do not pick which worker they run on. `std:workers` is the surface that lets a script reason about that model. The contract is intentionally narrow: only userdata that *opts in* can cross worker boundaries. Plain Lua tables, functions, threads, and arbitrary userdata cannot — they are bound to the VM that created them. For background on the worker model, see [The sandboxing model](/guides/sandboxing-model#the-worker-model). ## Module functions #### workers.shared ```lua workers.shared(key: string, userdata: Sharable): Sharable ``` Process-global registry keyed by string. `key` : The string identifier under which the value is registered. Any worker calling with the same key receives a handle to the same shared state. `userdata` : On the first call for a given key, the userdata is registered and a handle to the shared state is returned. On subsequent calls, the userdata is treated as a placeholder used only to communicate "I'm expecting this type"; the stored handle is rewrapped for the calling worker and returned, and the placeholder is discarded. The returned handle is bound to the calling worker; the underlying state lives outside any single worker. See [Sharable types](#sharable-types) for the set of userdata types that opt into sharing. #### workers.shutdown\_signal ```lua workers.shutdown_signal(): ShutdownSignal ``` Returns a process-singleton signal that fires once when the process receives a shutdown request (Ctrl-C or SIGINT). Repeated calls return references to the same underlying signal. ## ShutdownSignal methods `ShutdownSignal:is_fired()` : Returns `true` once the signal has fired, `false` otherwise. Non-blocking. `ShutdownSignal:wait()` : Resumes when the signal fires. Returns immediately if the signal has already fired. Safe to call from multiple workers and multiple coroutines concurrently. ## Sharable types A userdata type is *sharable* when it implements the cross-worker contract. The current set is: | Type label | Constructor | |---|---| | `std:collections.map` | `(require("std:collections")).map()` | | `std:collections.counter` | `(require("std:collections")).counter()` | | `std:net.listener` | `(require("std:net")).listener(addr)` | | `lib:json.object` | [`(require("lib:json")).object()`](/lib/reference/json#json-object), and any Object returned from a JSON parse | | `lib:json.array` | [`(require("lib:json")).array()`](/lib/reference/json#json-array), and any Array returned from a JSON parse | Other userdata types — including `std:collections.mutex`, [`json.null`](/lib/reference/json#json-null), database handles, and server handles — are *not* sharable. Passing one to [`workers.shared`](#workers-shared) raises an error listing the currently sharable set, so scripts get a stable error catalogue rather than a silent failure. When a new sharable type is added to the runtime, it appears in this catalogue automatically. Scripts can rely on the error message to discover the live set. ## Errors All errors from [`workers.shared`](#workers-shared) are *raised*, not returned via the `(value, err)` tuple. Misuse of this surface is a programming error rather than a recoverable condition. Wrap calls in `pcall` if a script needs to observe them. | Trigger | Message contains | |---|---| | Plain table or scalar passed | `"userdata"` | | Userdata that does not opt in | `"not a sharable userdata"` and the catalogue | | Type mismatch on an existing key | `"already holds"` and both type labels | ## Examples ### A cross-worker counter The following example publishes an atomic counter under a fixed key. Any worker calling [`workers.shared`](#workers-shared) with the same key sees the same atomic. ```lua local workers = require("std:workers") local collections = require("std:collections") local hits = workers.shared("global_hits", collections.counter()) hits:increment() print(hits:get()) ``` ### A single bound socket fanned out to all workers The following example binds one TCP listener and shares it across all workers. Each worker accepts against the same file descriptor. ```lua local net = require("std:net") local workers = require("std:workers") local hyper = require("vnd:hyper") local listener = workers.shared("http", net.listener("0.0.0.0:3000")) hyper.serve(listener, function(req) return { status = 200, body = "hello" } end) ``` ### A JSON document shared across workers The following example publishes a parsed JSON document from one worker and observes it from another. ```lua local workers = require("std:workers") local json = require("lib:json") local serde_json = require("vnd:serde_json") -- Worker A — publish. local body = serde_json.from_str('{"name":"Alice","tags":["x","y"]}') local doc = workers.shared("session:42", body) doc:set("active", true) -- Worker B — pick it up; mutations from A are visible. local doc = workers.shared("session:42", json.object()) print(doc:get("name")) -- Alice print(doc:get("active")) -- true ``` ### Graceful shutdown The following example blocks the main task until Ctrl-C, then closes a long-running server. ```lua local workers = require("std:workers") local hyper = require("vnd:hyper") local net = require("std:net") local server = hyper.serve(net.listener("0.0.0.0:3000"), function(req) return { status = 200, body = "ok" } end) workers.shutdown_signal():wait() server:close() server:wait() ``` ## Acceptance The following scenarios must hold. They are exercised by the integration tests under [`tests/std/workers.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/std/workers.test.luau). 1. **First-call publish, later-call lookup.** `workers.shared("k", X())` followed by `workers.shared("k", X())` from any worker returns handles that observe each other's mutations. 2. **Type mismatch raises.** Registering a `map` at key `k` and then calling `workers.shared("k", counter())` raises an error containing `"already holds"` and both type labels. 3. **Plain table rejected.** `workers.shared("k", { a = 1 })` raises an error. 4. **Non-sharable userdata rejected.** `workers.shared("k", json.null)` raises an error containing `"not a sharable userdata"` and the current catalogue. 5. **JSON Object and Array round-trip.** Sharing an Object parsed in one worker and looking it up with a placeholder `json.object()` in another returns a handle whose `:get(key)` exposes the original data, including any mutations written through any handle. 6. **Mismatch with JSON.** Registering at a key with `collections.map()` then registering `json.object()` at the same key raises an error containing `"already holds"`. 7. **Shutdown signal idempotent wait.** Calling `workers.shutdown_signal():wait()` multiple times after the signal has fired returns immediately each time. ## See also * [The sandboxing model](/guides/sandboxing-model#the-worker-model) — How the worker pool fits into the broader engine. * [`std:collections`](/std/reference/collections) — Sharable collection types. * [`std:net`](/std/reference/net) — TCP listeners shared across workers. * [`lib:json`](/lib/reference/json) — Sharable JSON document handles. --- --- url: /lib.md --- # `lib:*`: Blessed gap-fillers and project-native modules The `lib:*` namespace contains modules that do not fit cleanly into either `std:*` (which mirrors the Rust standard library) or `vnd:*` (which exposes single vendored crates). Two kinds of module live here: blessed *gap-fillers* that plug a hole the Rust standard library does not cover (such as `lib:base64`), and *project-native* modules written specifically for runblox-core (such as `lib:test` and `lib:json`). A module qualifies for `lib:*` when its purpose is well-defined, its surface is stable, and it is genuinely useful to most scripts. Speculative or experimental functionality belongs elsewhere until it has earned a place here. ## Reference * [`lib:base64`](/lib/reference/base64) — Base64 encoding and decoding. * [`lib:json`](/lib/reference/json) — JSON Object and Array document handles, with `null` sentinel. * [`lib:test`](/lib/reference/test) — The Luau test runner and assertion surface. * [`lib:vcr`](/lib/reference/vcr) — Record-and-replay HTTP fixtures for tests. ## See also * [The module system](/guides/module-system) — How `require("lib:name")` resolves. * [`std:*`](/std/) — Standard-library mirrors. * [`vnd:*`](/vnd/) — Vendored crates. --- --- url: /lib/reference/base64.md --- # `lib:base64` module **Experimental** The **`lib:base64`** module encodes and decodes Base64, including the URL-safe variant. | Property | Value | |---|---| | Namespace | `lib` | | Source | [`src/lua/lib/base64.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/lib/base64.rs) | | Tests | [`tests/lib/base64.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/lib/base64.test.luau) | | Stability | Experimental | | Mirror | [RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648) | ## Syntax ```lua local base64 = require("lib:base64") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("lib:base64")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `lib:*` modules fit into the engine. * [`lib:*`](/lib/) — Other modules in this namespace. --- --- url: /lib/reference/json.md --- # `lib:json` module **Stable** The **`lib:json`** module exposes structural handles for working with JSON documents in Luau. It provides two userdata types — Object and Array — and a `null` sentinel distinct from Lua `nil`. Documents are constructed with [`json.object`](#json-object) or [`json.array`](#json-array), or received populated from a parser such as [`vnd:serde_json`](/vnd/reference/serde_json). | Property | Value | |---|---| | Namespace | `lib` | | Source | [`src/lua/lib/json.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/lib/json.rs) | | Tests | [`tests/lib/json.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/lib/json.test.luau) | | Stability | Stable | | Sharable across workers | Yes (Object and Array; not `null`) | ## Syntax ```lua local json = require("lib:json") ``` The returned table exposes module functions and the `null` sentinel. ## Description `lib:json` is *explicit* about access. There is no `obj.field` sugar, no `arr[1]` indexing, and no implicit `#arr` length. Reads go through `:get(...)`, writes through `:set(...)` or `:push(...)`, presence checks through `:has(...)`, and length through `:len()`. The shape keeps document semantics legible to readers and consistent under Luau strict mode, which cannot statically check dynamic JSON shapes. Object and Array handles are *shareable* by default. Passing one through [`std:workers`](/std/reference/workers)`.shared(key, ...)` makes the document visible to other workers, with mutations through any handle observable through every other handle. ## Module functions #### json.object ```lua json.object(): Object ``` Returns an empty mutable Object handle. #### json.array ```lua json.array(): Array ``` Returns an empty mutable Array handle. #### json.null ```lua json.null: any ``` A unique userdata sentinel representing JSON `null`, distinct from Lua `nil` (which represents "key absent"). Comparable by `==`. Always the same value within and across workers — `serde_json.from_str("null") == json.null` holds. Not sharable through [`std:workers`](/std/reference/workers)`.shared`; passing it errors with the standard `"not a sharable userdata"` message. ## Object methods `Object:get(...path)` : Walks `path` through nested Objects and Arrays. Each segment is a string (object key) or integer (1-based array index). Returns a Lua scalar for terminal leaves, [`json.null`](#json-null) for explicit JSON nulls, a fresh Object or Array handle for nested containers, or `nil` if any segment is missing. With no arguments, returns the receiver. `Object:set(key, value)` : Assigns `value` at `key`. Accepts Lua scalars, plain Lua tables (recursively converted), other Object or Array handles, and [`json.null`](#json-null). Replacing a container preserves identity for handles already pointing into the old subtree at the same path — they reflect the new contents. `Object:has(key)` : `true` if `key` is present (including when its value is [`json.null`](#json-null)), `false` otherwise. Distinguishes "key absent" from "key explicitly null". `Object:keys()` : Returns an array of the Object's keys. Order is not guaranteed; sort explicitly if needed. `Object:len()` : Returns the number of entries in the Object. `Object:to_table()` : Returns a deep Lua-table copy of the Object's current contents. Mutations to the returned table do not flow back into the document. ## Array methods `Array:get(...path)` : Same path-walking semantics as [`Object:get`](#object-methods). The leading segment is a 1-based integer index. `Array:set(index, value)` : Assigns `value` at the 1-based `index`. Accepts the same value shapes as [`Object:set`](#object-methods). `Array:push(value)` : Appends `value` to the end of the array. `Array:len()` : Returns the number of elements in the array. ## Errors `lib:json` does not raise on missing-path reads. `:get(...)` returns `nil` when any segment fails to resolve. Type mismatches at write time follow Luau's normal error path — passing a function to `:set` raises synchronously. [`json.null`](#json-null) is comparable with `==` but is *not* sharable through [`std:workers`](/std/reference/workers)`.shared`. ## Examples ### Parsing, navigating, and mutating The following example parses a JSON string, reads nested fields, mutates the document, and serialises the result. ```lua local json = require("lib:json") local serde_json = require("vnd:serde_json") local doc = serde_json.from_str('{"name":"Alice","age":30,"tags":["a","b"]}') print(doc:get("name")) -- Alice print(doc:get("tags", 2)) -- b print(doc:has("missing")) -- false doc:set("city", "Springfield") doc:get("tags"):push("c") print(serde_json.to_string(doc)) -- {"age":30,"city":"Springfield",...} ``` ### Distinguishing null from absent The following example shows how `:has` and `:get` together distinguish a key whose value is explicitly `null` from a key that is missing entirely. ```lua local json = require("lib:json") local serde_json = require("vnd:serde_json") local v = serde_json.from_str('{"explicit":null}') print(v:has("explicit")) -- true print(v:get("explicit") == json.null) -- true print(v:has("missing")) -- false print(v:get("missing")) -- nil ``` ### A nested handle observing parent mutation A handle obtained from `:get` continues to reflect the live document after the parent replaces the subtree. ```lua local serde_json = require("vnd:serde_json") local doc = serde_json.from_str('{"nested":{"x":1}}') local nested = doc:get("nested") doc:set("nested", { x = 99, y = 100 }) print(nested:get("x")) -- 99 print(nested:get("y")) -- 100 ``` ### Building a document from scratch The following example constructs an Array, then an Object holding the array and other values, then writes [`json.null`](#json-null) into a third field. ```lua local json = require("lib:json") local arr = json.array() arr:push(1) arr:push("two") arr:push(true) local obj = json.object() obj:set("name", "Alice") obj:set("scores", arr) obj:set("missing", json.null) ``` ### Sharing a document across workers The following example publishes a parsed document under the key `"doc:42"` from one worker and picks it up from another. ```lua local workers = require("std:workers") local json = require("lib:json") local serde_json = require("vnd:serde_json") -- Worker A local body = serde_json.from_str('{"hits":0}') local shared = workers.shared("doc:42", body) shared:set("hits", 1) -- Worker B local doc = workers.shared("doc:42", json.object()) print(doc:get("hits")) -- 1 ``` ## Acceptance The following scenarios must hold. They are exercised by the integration tests under [`tests/lib/json.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/lib/json.test.luau). 1. **Constructors.** [`json.object`](#json-object) and [`json.array`](#json-array) return empty handles whose `:len()` is `0`. 2. **Round-trip with parser.** `serde_json.from_str('{"a":1}')` returns an Object handle for which `:get("a")` is `1`. 3. **Variadic path walk.** For `'{"user":{"tags":["x","y"]}}'`, `:get("user", "tags", 2)` returns `"y"`. Any missing segment yields `nil`. 4. **Null versus absent.** For `'{"explicit":null}'`, `:has("explicit")` is `true` and `:get("explicit") == json.null`. `:has("missing")` is `false` and `:get("missing")` is `nil`. 5. **Scalar JSON values.** `serde_json.from_str('"hi"')` returns the Lua string `"hi"`. `'42'` returns `42`. `'true'` returns `true`. `'null'` returns [`json.null`](#json-null). 6. **Nested handle observes mutation.** A handle obtained via `doc:get("nested")` continues to reflect the live document after `doc:set("nested", ...)` replaces the subtree. 7. **Array push, set, len.** `arr:push(x)` increases `:len()` by one. `arr:get(n)` returns the element at the 1-based index `n`. `arr:set(n, y)` overwrites that element. 8. **Object keys.** `obj:keys()` returns an array containing every key present, with `#obj:keys() == obj:len()`. 9. **`to_table` is a snapshot.** Mutating the table returned by `:to_table()` does not affect the underlying document. 10. **Shareable.** Object and Array handles round-trip through [`std:workers`](/std/reference/workers)`.shared`. [`json.null`](#json-null) does not. ## See also * [`std:workers`](/std/reference/workers) — Cross-worker sharing of Object and Array handles. * [`vnd:serde_json`](/vnd/reference/serde_json) — Parser and serialiser that produces and consumes `lib:json` handles. * [`vnd:jsonschema`](/vnd/reference/jsonschema) — Schema validation for JSON documents. * [The module system](/guides/module-system) — How `require("lib:json")` resolves. --- --- url: /lib/reference/test.md --- # `lib:test` module **Experimental** The **`lib:test`** module is the Luau test runner. It registers test cases, runs them, and reports pass/fail with assertion details. | Property | Value | |---|---| | Namespace | `lib` | | Source | [`src/lua/lib/test.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/lib/test.rs) | | Tests | [`tests/lib/test.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/lib/test.test.luau) | | Stability | Experimental | | Mirror | — | ## Syntax ```lua local test = require("lib:test") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("lib:test")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `lib:*` modules fit into the engine. * [`lib:*`](/lib/) — Other modules in this namespace. --- --- url: /lib/reference/vcr.md --- # `lib:vcr` module **Experimental** The **`lib:vcr`** module records and replays HTTP request and response fixtures, providing deterministic tests for code that calls external services. | Property | Value | |---|---| | Namespace | `lib` | | Source | [`src/lua/lib/vcr.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/lib/vcr.rs) | | Tests | [`tests/lib/vcr.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/lib/vcr.test.luau) | | Stability | Experimental | | Mirror | — | ## Syntax ```lua local vcr = require("lib:vcr") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("lib:vcr")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `lib:*` modules fit into the engine. * [`lib:*`](/lib/) — Other modules in this namespace. --- --- url: /vnd.md --- # `vnd:*`: Vendored crate modules The `vnd:*` namespace exposes individual Rust crates to Lua. Each module wraps exactly one upstream crate, and the module name matches the crate name verbatim. There is no renaming, no aliasing, no merging of crates into composite modules — `require("vnd:hyper")` always means *the* `hyper` crate, and never anything else. This naming rule is non-negotiable. It is what lets scripts reach for an upstream crate by its real name, find its real surface (modulo Lua adaptations), and locate its real documentation without indirection. ## Reference ### HTTP and networking * [`vnd:hyper`](/vnd/reference/hyper) — HTTP client and server backed by the `hyper` crate. ### Data formats * [`vnd:serde_json`](/vnd/reference/serde_json) — JSON parsing and serialisation. * [`vnd:serde_yaml`](/vnd/reference/serde_yaml) — YAML parsing and serialisation. * [`vnd:toml`](/vnd/reference/toml) — TOML parsing and serialisation. * [`vnd:quick_xml`](/vnd/reference/quick_xml) — XML parsing and serialisation via `quick-xml`. * [`vnd:jsonschema`](/vnd/reference/jsonschema) — JSON Schema validation. ### Databases * [`vnd:rusqlite`](/vnd/reference/rusqlite) — SQLite via `rusqlite` (bundled). * [`vnd:sqlx_mysql`](/vnd/reference/sqlx_mysql) — MySQL via `sqlx`. * [`vnd:sqlx_postgres`](/vnd/reference/sqlx_postgres) — PostgreSQL via `sqlx`. ### Test infrastructure * [`vnd:testcontainers_modules`](/vnd/reference/testcontainers_modules) — Test containers for integration tests. ## See also * [The module system](/guides/module-system) — How `require("vnd:name")` resolves and the naming rule. * [`std:*`](/std/) — Standard-library mirrors. * [`lib:*`](/lib/) — Blessed gap-fillers and project-native modules. --- --- url: /vnd/reference/hyper.md --- # `vnd:hyper` module **Experimental** The **`vnd:hyper`** module exposes the [`hyper`](https://crates.io/crates/hyper) HTTP client and server crate to Lua. | Property | Value | |---|---| | Namespace | `vnd` | | Source | [`src/lua/vnd/hyper.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/vnd/hyper.rs) | | Tests | [`tests/vnd/hyper.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/vnd/hyper.test.luau) | | Stability | Experimental | | Mirror | [`hyper`](https://docs.rs/hyper/) | ## Syntax ```lua local hyper = require("vnd:hyper") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("vnd:hyper")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `vnd:*` modules fit into the engine. * [`vnd:*`](/vnd/) — Other modules in this namespace. --- --- url: /vnd/reference/serde_json.md --- # `vnd:serde_json` module **Experimental** The **`vnd:serde_json`** module exposes the [`serde_json`](https://crates.io/crates/serde_json) crate for parsing and serialising JSON. Parsed documents are returned as [`lib:json`](/lib/reference/json) Object and Array handles. | Property | Value | |---|---| | Namespace | `vnd` | | Source | [`src/lua/vnd/serde_json.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/vnd/serde_json.rs) | | Tests | [`tests/vnd/serde_json.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/vnd/serde_json.test.luau) | | Stability | Experimental | | Mirror | [`serde_json`](https://docs.rs/serde_json/) | ## Syntax ```lua local serde_json = require("vnd:serde_json") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("vnd:serde_json")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `vnd:*` modules fit into the engine. * [`vnd:*`](/vnd/) — Other modules in this namespace. --- --- url: /vnd/reference/serde_yaml.md --- # `vnd:serde_yaml` module **Experimental** The **`vnd:serde_yaml`** module exposes the [`serde_yaml`](https://crates.io/crates/serde_yaml) crate for parsing and serialising YAML. | Property | Value | |---|---| | Namespace | `vnd` | | Source | [`src/lua/vnd/serde_yaml.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/vnd/serde_yaml.rs) | | Tests | [`tests/vnd/serde_yaml.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/vnd/serde_yaml.test.luau) | | Stability | Experimental | | Mirror | [`serde_yaml`](https://docs.rs/serde_yaml/) | ## Syntax ```lua local serde_yaml = require("vnd:serde_yaml") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("vnd:serde_yaml")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `vnd:*` modules fit into the engine. * [`vnd:*`](/vnd/) — Other modules in this namespace. --- --- url: /vnd/reference/toml.md --- # `vnd:toml` module **Experimental** The **`vnd:toml`** module exposes the [`toml`](https://crates.io/crates/toml) crate for parsing and serialising TOML. | Property | Value | |---|---| | Namespace | `vnd` | | Source | [`src/lua/vnd/toml.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/vnd/toml.rs) | | Tests | [`tests/vnd/toml.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/vnd/toml.test.luau) | | Stability | Experimental | | Mirror | [`toml`](https://docs.rs/toml/) | ## Syntax ```lua local toml = require("vnd:toml") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("vnd:toml")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `vnd:*` modules fit into the engine. * [`vnd:*`](/vnd/) — Other modules in this namespace. --- --- url: /vnd/reference/quick_xml.md --- # `vnd:quick_xml` module **Experimental** The **`vnd:quick_xml`** module exposes the [`quick-xml`](https://crates.io/crates/quick-xml) crate for parsing and serialising XML. | Property | Value | |---|---| | Namespace | `vnd` | | Source | [`src/lua/vnd/quick_xml.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/vnd/quick_xml.rs) | | Tests | [`tests/vnd/quick_xml.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/vnd/quick_xml.test.luau) | | Stability | Experimental | | Mirror | [`quick-xml`](https://docs.rs/quick-xml/) | ## Syntax ```lua local quick_xml = require("vnd:quick_xml") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("vnd:quick_xml")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `vnd:*` modules fit into the engine. * [`vnd:*`](/vnd/) — Other modules in this namespace. --- --- url: /vnd/reference/jsonschema.md --- # `vnd:jsonschema` module **Experimental** The **`vnd:jsonschema`** module exposes the [`jsonschema`](https://crates.io/crates/jsonschema) crate for validating JSON documents against a schema. | Property | Value | |---|---| | Namespace | `vnd` | | Source | [`src/lua/vnd/jsonschema.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/vnd/jsonschema.rs) | | Tests | [`tests/vnd/jsonschema.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/vnd/jsonschema.test.luau) | | Stability | Experimental | | Mirror | [`jsonschema`](https://docs.rs/jsonschema/) | ## Syntax ```lua local jsonschema = require("vnd:jsonschema") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("vnd:jsonschema")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `vnd:*` modules fit into the engine. * [`vnd:*`](/vnd/) — Other modules in this namespace. --- --- url: /vnd/reference/rusqlite.md --- # `vnd:rusqlite` module **Experimental** The **`vnd:rusqlite`** module exposes the [`rusqlite`](https://crates.io/crates/rusqlite) SQLite driver. The SQLite library itself is bundled into the binary. | Property | Value | |---|---| | Namespace | `vnd` | | Source | [`src/lua/vnd/rusqlite.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/vnd/rusqlite.rs) | | Tests | [`tests/vnd/rusqlite.test.luau`](https://github.com/flying-dice/runblox-core/blob/main/tests/vnd/rusqlite.test.luau) | | Stability | Experimental | | Mirror | [`rusqlite`](https://docs.rs/rusqlite/) | ## Syntax ```lua local rusqlite = require("vnd:rusqlite") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("vnd:rusqlite")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `vnd:*` modules fit into the engine. * [`vnd:*`](/vnd/) — Other modules in this namespace. --- --- url: /vnd/reference/sqlx_mysql.md --- # `vnd:sqlx_mysql` module **Experimental** The **`vnd:sqlx_mysql`** module exposes the MySQL driver from the [`sqlx`](https://crates.io/crates/sqlx) async SQL toolkit. | Property | Value | |---|---| | Namespace | `vnd` | | Source | [`src/lua/vnd/sqlx_mysql.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/vnd/sqlx_mysql.rs) | | Tests | Not in CI (requires Docker / containers) | | Stability | Experimental | | Mirror | [`sqlx`](https://docs.rs/sqlx/) (`mysql` feature) | ## Syntax ```lua local sqlx_mysql = require("vnd:sqlx_mysql") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("vnd:sqlx_mysql")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `vnd:*` modules fit into the engine. * [`vnd:*`](/vnd/) — Other modules in this namespace. --- --- url: /vnd/reference/sqlx_postgres.md --- # `vnd:sqlx_postgres` module **Experimental** The **`vnd:sqlx_postgres`** module exposes the PostgreSQL driver from the [`sqlx`](https://crates.io/crates/sqlx) async SQL toolkit. | Property | Value | |---|---| | Namespace | `vnd` | | Source | [`src/lua/vnd/sqlx_postgres.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/vnd/sqlx_postgres.rs) | | Tests | Not in CI (requires Docker / containers) | | Stability | Experimental | | Mirror | [`sqlx`](https://docs.rs/sqlx/) (`postgres` feature) | ## Syntax ```lua local sqlx_postgres = require("vnd:sqlx_postgres") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("vnd:sqlx_postgres")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `vnd:*` modules fit into the engine. * [`vnd:*`](/vnd/) — Other modules in this namespace. --- --- url: /vnd/reference/testcontainers_modules.md --- # `vnd:testcontainers_modules` module **Experimental** The **`vnd:testcontainers_modules`** module exposes preset container images from [`testcontainers-modules`](https://crates.io/crates/testcontainers-modules) for use in integration tests. | Property | Value | |---|---| | Namespace | `vnd` | | Source | [`src/lua/vnd/testcontainers_modules.rs`](https://github.com/flying-dice/runblox-core/blob/main/src/lua/vnd/testcontainers_modules.rs) | | Tests | Not in CI (requires Docker / containers) | | Stability | Experimental | | Mirror | [`testcontainers-modules`](https://docs.rs/testcontainers-modules/) | ## Syntax ```lua local testcontainers_modules = require("vnd:testcontainers_modules") ``` ## Description > \[!WARNING] > This reference page is a **stub**. The full Lua surface is captured in the module source listed above and exercised by the test file. A spec PR following [the spec-driven development workflow](/rfcs/0000-spec-driven-development) will land the contracted surface here. > > Until that lands, the source and tests are the contract. ## See also * [The module system](/guides/module-system) — How `require("vnd:testcontainers_modules")` resolves. * [The sandboxing model](/guides/sandboxing-model) — How `vnd:*` modules fit into the engine. * [`vnd:*`](/vnd/) — Other modules in this namespace. --- --- url: /rfcs.md --- # RFCs Design proposals for cross-cutting changes to `runblox-core`. Module-shaped changes are specced on the module reference pages directly under `docs/src//.md`; RFCs are for everything else — process, build, new namespaces, multi-module design, anything that does not fit on a single module page. The full process is defined in [RFC 0000 — Spec-driven development](./0000-spec-driven-development). ## Status Every RFC carries a `status` in its front matter. A draft banner is rendered at the top of any RFC that is not yet accepted, so readers of this site cannot mistake a proposal for the contract. | Status | Meaning | |---|---| | `draft` | Under refinement. Not the contract. May be edited freely. | | `accepted` | Merged. Awaiting implementation. | | `implemented` | Merged and the runtime is in line. | | `rejected` | Closed without acceptance. Kept for design history. | | `superseded` | Replaced by a later RFC. | ## Index ### Draft * [0000 — Spec-driven development](./0000-spec-driven-development) ### Accepted *None yet.* ### Implemented *None yet.* ### Rejected *None yet.* ### Superseded *None yet.* --- --- url: /rfcs/0000-spec-driven-development.md --- # 0000 — Spec-driven development > \[!WARNING] > This RFC is `draft`. It is under refinement and is **not** the contract. ## Summary Non-trivial changes to `runblox-core` land in two pull requests: a **spec PR** that ships the user-facing description of the change to the public docs site (deploying to a Cloudflare branch preview), and an **implementation PR** that brings the runtime in line with what the merged docs promise. The deployed preview of the spec PR is the spec review surface. ## Motivation Refinement happens in GitHub issues and chat, but historically it has not been captured as a reviewable artifact before code lands. Implementation PRs end up carrying both the *what* and the *how* in the same diff, which makes them harder to review and harder for downstream readers (humans and LLM agents in the Seeker fleet) to reconstruct intent from. The repository already publishes a VitePress documentation site at . The site exposes `llms.txt` and `llms-full.txt` feeds via `vitepress-plugin-llms`, and Cloudflare Pages deploys a preview for every branch. Together these give us everything needed to make the docs site itself the spec surface: * A **rendered** review surface — reviewers (or LLMs) read the spec on the deployed site, not in raw markdown. * A **machine-readable** review surface — `/llms.txt` and `/llms-full.txt` are consumable by Seekers without parsing diffs. * A **public** record — accepted specs are visible to anyone using the runtime, not buried in an internal directory. External tooling (Spec Kit, Kiro, ADR CLIs) was considered and rejected: they assume an engineer at a terminal driving a CLI. The Seeker fleet interacts with this repo as teammates would — through issues, PRs, comments, and labels — and any tool requiring local state or initialisation steps does not fit that operating model. GitHub plus the existing docs site is the toolkit. ## Detailed design ### Where specs live Three shapes of long-lived design artifact live in `docs/src/`, distinguished by whether the change is module-shaped, forward-looking, or retrospective: | Path | Kind of artifact | Lifecycle | |---|---|---| | `docs/src//.md` | Module-shaped: add or change a module's Lua surface | Living; edited forever as the module evolves | | `docs/src/rfcs/NNNN-name.md` | Forward-looking: cross-cutting design proposals not yet accepted | Frozen on accept/reject; status front matter | | `docs/src/adrs/NNNN-name.md` | Retrospective: records of decisions already in force, in [npryce/adr-tools](https://github.com/npryce/adr-tools) format | Frozen on acceptance; superseded by later ADRs | The distinction between RFCs and ADRs is timing. RFCs are *proposals* — they capture a design before it lands and may be rejected or revised during review. ADRs are *records* — they document a decision that is already in force, so a future reader can understand the rationale without reconstructing it from code archaeology. An accepted RFC introducing a load-bearing decision typically produces a corresponding ADR; the RFC carries the design discussion, the ADR carries the decision and its consequences. Investigation notes and ad-hoc working memos do not belong on the docs site. They live in the issue, the PR description, or are not written down at all. ### Lifecycle 1. **Refinement** — issue is opened. Discussion, scoping, and option analysis happen on the issue. The issue starts with the `draft` label per the fleet operating discipline; a CODEOWNER removes it when refinement is complete. 2. **Spec PR** — once the issue leaves `draft`, anyone (Seeker or human) opens a docs-only PR adding or editing the relevant page(s). The PR references the issue. Cloudflare deploys a branch preview. Review happens on the preview URL. CODEOWNER approval is required to merge. 3. **Implementation PR** — opened against the merged spec, citing it in the PR description. Brings the runtime in line with the contracted surface. Required pipelines must be green and CODEOWNER must approve before merge. The `wip` label gates a PR that is not yet ready for review. ### When an RFC is needed A separate RFC under `docs/src/rfcs/` is required when the work is **not** module-shaped. Concretely: * New namespace (e.g. introducing `ext:*`) * Cross-cutting change touching multiple modules in concert * Process, build, infrastructure, or operational change * New module whose surface is large or contentious enough that the design merits standalone discussion A small change to an existing module's surface, or a new module with a self-evident shape, does **not** need an RFC — the docs PR on the module page is the spec. ### Trivial escape hatch The two-PR split is overhead. Changes labelled `trivial` skip it: * Typo fixes, doc-only edits that do not introduce new surface * Dependency bumps with no surface change * Internal refactors not visible to Lua scripts The label is applied by the PR author and accepted at CODEOWNER discretion. If a reviewer disagrees, the label is removed and the change splits. ### RFC numbering and status * Filename: `NNNN-kebab-name.md`. `NNNN` is zero-padded, monotonic. `0000` is reserved for this RFC. * Front matter: `status` (one of `draft`, `accepted`, `rejected`, `superseded`, `implemented`), `created` (ISO date), `implemented-in` (PR number or commit SHA, set when the implementation lands). * A draft status banner appears at the top of every RFC until it is accepted, so readers of the deployed site cannot mistake a proposal for the contract. | Status | Meaning | |---|---| | `draft` | Under refinement. Not the contract. May be edited freely. | | `accepted` | Merged. Awaiting implementation. The contract for what the runtime should become. | | `implemented` | Merged and the runtime is in line. The contract for what the runtime is. | | `rejected` | Closed without acceptance. Kept for design history. | | `superseded` | Replaced by a later RFC, which is linked from this one. | ### Acceptance verification Module reference pages include runnable Luau acceptance examples in fenced code blocks. To prevent drift between the contracted surface and the runtime, those examples must be reachable from the test suite — either by extracting them into `tests//.test.luau`, or by linking the docs page to an existing test file that exercises the same scenarios. The exact mechanism is left to follow-up work; until it is wired up, drift between docs and runtime is possible and reviewers must catch it manually. This is an open question — see below. ### Roles * **Author** (Seeker or human): opens issue, opens spec PR, opens implementation PR. May be different parties at each stage. * **CODEOWNER** (per `CODEOWNERS`): reviews and approves spec PRs and implementation PRs. Removes `draft` and `needs-spec` labels. Adjudicates `trivial` disputes. ### Required labels | Label | Where | Meaning | |---|---|---| | `draft` | issues, PRs | Not ready for work / review. Only CODEOWNERS remove it. | | `wip` | PRs | Author still working; do not review yet. | | `needs-spec` | issues | Refinement complete; ready for a spec PR. | | `spec-approved` | issues | A spec PR referencing this issue has merged. | | `trivial` | PRs | Escape hatch; spec/dev split waived. CODEOWNER discretion. | | `blocked` | issues, PRs | Cannot proceed; blocker stated in a comment. | ### Migration from `specs/` The repository currently has an internal `specs/` directory containing `lib/json.md` and `std/workers.md`. These files are already shaped like reference docs and will be migrated to `docs/src/lib/json.md` and `docs/src/std/workers.md` in a follow-up PR; the `specs/` directory will be retired at the same time. Migration is intentionally scoped out of this RFC to keep the process change and the file moves separately reviewable. ## Drawbacks * **Two PRs per change is more friction than one.** Mitigated by the `trivial` escape hatch, but the friction is real for medium-sized work that does not qualify as trivial. * **Discipline-only enforcement.** Nothing automated stops an implementation PR from merging without a corresponding spec PR. The gate is the CODEOWNER. A future addition could be a GitHub Action that fails the implementation PR if it cannot find a merged spec PR linked from its body, but that is out of scope here. * **Acceptance-verification mechanism is not yet wired up.** Until docs acceptance examples are pulled into the test suite, spec and implementation can drift silently between merge of the spec PR and merge of the implementation PR. Reviewers carry that load manually for now. * **Public draft RFCs.** RFCs in `draft` are visible on the deployed site. The status banner is the mitigation; readers ignoring the banner will see proposals that may never land. ## Alternatives * **Internal `specs/` directory only (current state).** Works for module reference, but does not deploy, has no preview-review loop, and is invisible to readers of the runtime. Rejected because the deploy/preview/llms.txt loop is the load-bearing benefit of moving to the docs site. * **Spec Kit / Kiro / external SDD tooling.** Adds a CLI dependency and assumes a local engineer driving it. Does not fit the Seeker fleet operating model. Rejected. * **ADR-only.** Architecture Decision Records are narrower than full specs (decisions, not designs) and would not carry module surface. Rejected as insufficient on its own; ADRs may compose with this RFC layer in future but are not required. * **Single PR carrying both docs and code.** The status quo. Rejected on the original motivation: review surface is the diff, not the rendered site, and intent is mixed with implementation. ## Open questions * **How are docs acceptance examples wired into the test suite?** Options: extract fenced ` ```luau` blocks tagged with an `acceptance` info string and write them to `tests/`; or include explicit links in each module page to the test file that exercises its acceptance scenarios. The former is more rigorous; the latter is cheaper. To be answered by a follow-up RFC if non-trivial. * **Project board automation.** Which actions move cards through which columns, and is that driven by labels, PR state, or both? Out of scope for this RFC; to be answered when the board is set up. * **What replaces `specs/README.md` after migration?** Either redirect the content into `docs/src/index.md`, or retire it and let this RFC plus the module reference pages stand on their own. ## Implementation notes This RFC is itself the first artifact of the process it defines. The spec PR introducing it lands first (this file plus `template.md`, `index.md`, and the VitePress nav update). Follow-up PRs: 1. Migrate `specs/lib/json.md` → `docs/src/lib/json.md`, `specs/std/workers.md` → `docs/src/std/workers.md`. Retire `specs/`. 2. Wire up the GitHub plumbing — issue templates, PR template, label set, `CODEOWNERS` for `docs/`, project board. 3. Decide and implement the docs-acceptance-to-tests mechanism (separate RFC if non-trivial). --- --- url: /adrs.md --- # Architecture Decision Records Architecture Decision Records (ADRs) capture significant decisions that shape `runblox-core`. Each ADR is a short, dated record of a decision and the forces that motivated it. The format follows [npryce/adr-tools](https://github.com/npryce/adr-tools). ADRs are *retrospective*: they document decisions that have been made and are in force. By contrast, [RFCs](/rfcs/) are *forward-looking*: they propose changes that have not yet been accepted. An accepted RFC that introduces a load-bearing decision typically produces a corresponding ADR; see the relevant RFC for the design rationale, and the ADR for the resulting decision. ## Status Each ADR carries one of the following statuses in its front matter and the body: | Status | Meaning | |---|---| | `Proposed` | The decision is under discussion. Not yet in force. | | `Accepted` | The decision is in force. The codebase is expected to comply. | | `Deprecated` | The decision is no longer recommended. A replacement has not yet superseded it. | | `Superseded` | The decision has been replaced by a later ADR, which is linked from the body. | ## Index * [0001. Use Luau as the embedded scripting language](/adrs/0001-use-luau-as-the-embedded-language) * [0002. Strip the unsafe halves of the Lua standard library](/adrs/0002-strip-unsafe-lua-stdlib) * [0003. Three-namespace module structure (`std`, `lib`, `vnd`)](/adrs/0003-three-namespace-module-structure) * [0004. `vnd:*` module names match upstream crate names verbatim](/adrs/0004-vnd-module-names-match-upstream) * [0005. Tuple-return convention for recoverable failures](/adrs/0005-tuple-error-convention) * [0006. Hidden async with synchronous-looking Lua APIs](/adrs/0006-hidden-async-with-sync-looking-apis) ## See also * [Template](/adrs/template) — Copy when creating a new ADR. * [RFCs](/rfcs/) — Forward-looking design proposals. * [The module system](/guides/module-system) — Touchpoint for ADRs 0003, 0004, and 0005. --- --- url: /adrs/0001-use-luau-as-the-embedded-language.md --- # 0001. Use Luau as the embedded scripting language Date: 2026-05-03 ## Status Accepted ## Context `runblox-core` needs an embedded scripting language for user-supplied scripts running inside the host runtime. The language must run in a host that controls the execution environment tightly, must permit fine-grained sandboxing of the operating-system surface exposed to scripts, and must not require the host to ship a JIT or a full ECMAScript runtime. Several candidates were available: * **Lua 5.4** — small, well-documented, widely embedded. Has the standard library that `os`, `io`, `package`, and `debug` modules expose, much of which is unsafe in a multi-tenant runtime and would need to be stripped. * **Luau** — Roblox's dialect of Lua. Sandbox-aware by design (the language explicitly drops or restricts unsafe surface), gradual typing, deterministic execution. Implemented in C++ and bindable from Rust through the [`mlua`](https://docs.rs/mlua/) crate's `luau` feature. * **JavaScript via Deno or Boa** — large surface, heavyweight runtime, less amenable to embedding into a fixed-resource host. * **WebAssembly** — capable, but the source-language story is downstream and the tooling burden on script authors is high. * **Custom DSL** — too narrow; user scripts need general-purpose expressivity. The host runtime is written in Rust. The bindings story for the embedded language is a load-bearing concern; the choice must integrate cleanly with `mlua` or an equivalent. ## Decision `runblox-core` uses Luau as its embedded scripting language. The Luau interpreter is bundled into the binary through `mlua`'s `vendored` feature. ## Consequences * Scripts benefit from Luau's gradual type system and its sandbox-aware design. The language itself drops parts of the Lua standard library that are unsafe for embedding (notably `dofile`, `loadfile`, and parts of `package`), reducing the surface that must be stripped at the host level. * The host runtime can rely on Luau's deterministic execution semantics — no JIT, predictable allocation behaviour — when reasoning about embedded scripts. * Script authors must use Luau syntax and semantics rather than vanilla Lua. Most Lua libraries written for 5.1/5.4 are compatible with adaptation; a small number that depend on stripped surface or on Lua-specific features (such as integer/number distinction) require porting. * The choice of `mlua` as the binding layer gates this decision on the upstream maintainer's continued Luau support. Migration to a different binding layer would be possible but would require non-trivial work. * The `mlua` `vendored` feature bundles Luau into the runblox-core binary, so the host does not depend on a system Lua installation. This simplifies distribution at the cost of a larger binary. --- --- url: /adrs/0002-strip-unsafe-lua-stdlib.md --- # 0002. Strip the unsafe halves of the Lua standard library Date: 2026-05-03 ## Status Accepted ## Context Lua and Luau ship with a standard library that exposes operating-system surface to scripts: `io.open` reads and writes arbitrary files, `os.execute` spawns subprocesses, `package.loadlib` loads shared libraries, `debug` reaches into running coroutines and the registry. A general-purpose host runtime that simply embeds the language and exposes those modules inherits whatever the script chooses to do with them — file system access, process control, code injection — without the host having a say. `runblox-core` is intended to run user-supplied scripts in a controlled environment. Scripts must be able to perform real work — read files, open sockets, parse JSON, talk to databases — but only through capabilities the host has explicitly granted. The default must be deny. ## Decision The unsafe halves of the Lua standard library are stripped at engine construction. Scripts running inside `runblox-core` cannot reach the following surface: * `io.*` — file and stream I/O is reintroduced through `std:fs` and `std:io`. * `os.*` — process and time control is reintroduced through `std:thread`, `std:net`, and the worker model. * `package.*` — the `require` resolver is replaced; see [ADR 0005](/adrs/0005-tuple-error-convention). * `debug.*` — reflection over running coroutines, the registry, and upvalues is unavailable. * `loadfile`, `dofile` — file-driven script loading is replaced by the binary's argument-driven model. The remaining standard surface (`string`, `table`, `math`, `coroutine`, `bit32`, `utf8`, the basic `print` family) is available unchanged. Every capability scripts need is reintroduced through explicit modules under the `std:*`, `lib:*`, and `vnd:*` namespaces (see [ADR 0003](/adrs/0003-three-namespace-module-structure)). Scripts cannot reach a capability the host has not registered. ## Consequences * Scripts cannot perform an unsanctioned operation on the host. The capability surface is exactly what the host registers — no more. * Existing Lua and Luau libraries that depend on stripped surface require porting before they can run inside `runblox-core`. The cost falls on the script author, not the host. * The host bears the burden of reintroducing capabilities through curated modules. This is intentional: every capability is reviewed at registration time, errors are routed through documented surfaces, and the catalogue of what is available is finite and discoverable. * The strict default makes `runblox-core` unsuitable as a drop-in replacement for environments where unrestricted Lua is expected — for example, an interactive REPL or a generic Lua scripting host. That is by design. --- --- url: /adrs/0003-three-namespace-module-structure.md --- # 0003. Three-namespace module structure (`std`, `lib`, `vnd`) Date: 2026-05-03 ## Status Accepted ## Context `runblox-core` reintroduces capabilities to scripts through explicit modules ([ADR 0002](/adrs/0002-strip-unsafe-lua-stdlib)). The runtime needs a discipline for organising those modules so that script authors can reason about what each one is for, what rules it follows, and where to look when they need a particular capability. Several patterns were considered: * **Flat module space** — every module lives at the top level (`require("fs")`, `require("hyper")`). Simple, but conflates project-native modules with vendored crates and standard-library mirrors. Naming collisions with upstream crate names become a hazard. * **Single-prefix space** — every module under one prefix (`require("rb:fs")`, `require("rb:hyper")`). Solves naming collisions but does not communicate which modules mirror upstream conventions and which are project-native. * **Multi-namespace** — three or more namespaces, each carrying a discipline. Communicates intent at the call site; allows different modules to be governed by different rules. The runtime exposes three kinds of capability that have meaningfully different design rules: 1. Capabilities that mirror Rust's standard library — filesystem, networking, threading, collections, I/O. These should *look like* Rust's standard library to a reader who knows it. 2. Capabilities that are project-native — JSON document handles, the test runner, base64 encoding. These have their own design lineage. 3. Capabilities that wrap individual upstream crates — HTTP via `hyper`, JSON parsing via `serde_json`, SQLite via `rusqlite`. These should reach the upstream crate's surface and documentation directly. ## Decision `runblox-core` uses three namespaces for Lua modules, picked at module registration: `std:*` : Mirrors Rust's standard library (`std::*`). Module names track the standard library's module names where possible (`std:fs` for `std::fs`, `std:net` for `std::net`). A small number of `std:*` modules cover capabilities that have no `std::` equivalent but are broadly needed and have no clean home elsewhere — `std:workers` is the principal example. `lib:*` : Blessed gap-fillers and project-native modules. A module qualifies for `lib:*` when its purpose is well-defined, its surface is stable, and it is genuinely useful to most scripts. Speculative or experimental functionality belongs elsewhere until it has earned a place here. `vnd:*` : Vendored Rust crates exposed to Lua. Each `vnd:*` module wraps exactly one upstream crate. The naming rule for `vnd:*` is governed by [ADR 0004](/adrs/0004-vnd-module-names-match-upstream). Every module belongs to exactly one namespace, picked at registration time and not changeable after. ## Consequences * Script authors can reason about the rules a module follows from its namespace alone. `require("std:fs")` is expected to read like Rust's `std::fs`; `require("vnd:hyper")` is expected to expose the `hyper` crate; `require("lib:test")` is expected to be project-native. * Naming collisions between project-native modules and vendored crates are impossible — they live in different namespaces. * Adding a new module requires choosing the right namespace. The choice is sometimes contested (a new module might fit `lib:*` or `std:*` depending on how it is shaped); an RFC is the right surface for that conversation. * Promoting a module across namespaces (for example, from `lib:*` to `std:*`) is a breaking change for scripts that `require` it. This raises the cost of namespace decisions but rewards careful initial placement. * Documentation is organised by namespace, with one landing page and a `reference/` subdirectory per namespace. Readers locate modules through the same structure that script authors `require` them through. --- --- url: /adrs/0004-vnd-module-names-match-upstream.md --- # 0004. `vnd:*` module names match upstream crate names verbatim Date: 2026-05-03 ## Status Accepted ## Context The `vnd:*` namespace exposes individual Rust crates to Lua ([ADR 0003](/adrs/0003-three-namespace-module-structure)). Each `vnd:*` module wraps exactly one upstream crate. The runtime needs a naming rule for these modules. Three options were considered: * **Friendly aliases** — pick names that read well in Lua (`vnd:http` instead of `vnd:hyper`; `vnd:db` instead of `vnd:rusqlite`). Reads cleanly, but the script author cannot move from the Lua call site to the upstream crate's documentation without translation. * **Match upstream names exactly** — `vnd:hyper` is the `hyper` crate; `vnd:serde_json` is the `serde_json` crate. Reads less smoothly in places (Rust crate naming conventions sometimes differ from Lua's), but eliminates the indirection between the call site and the upstream documentation. * **Composite names** — merge multiple crates into a single Lua module (`vnd:http` covering both `hyper` and `reqwest`). Hides the underlying choice and complicates the upgrade path. The host runtime treats vendored crates as load-bearing dependencies; their documentation is the authoritative source for behaviour beyond the thin Lua adapter layer. Anything that obscures the route from the script back to that documentation is a footgun. ## Decision The module name in the `vnd:*` namespace must match the upstream crate name verbatim. There is no renaming, no aliasing, and no merging of multiple crates into a composite module. ```lua local hyper = require("vnd:hyper") -- the `hyper` crate local serde_json = require("vnd:serde_json") -- the `serde_json` crate local rusqlite = require("vnd:rusqlite") -- the `rusqlite` crate ``` Crates whose names contain characters that are awkward in Lua identifiers (hyphens, primarily) substitute underscores: `vnd:quick_xml` for `quick-xml`, `vnd:serde_yaml` for `serde_yaml`. The substitution is mechanical (`-` to `_`) and applied consistently. When two upstream crates need to coexist for the same conceptual capability — for example, `sqlx` exposed across multiple databases — they appear as separate `vnd:*` modules with names that disambiguate the upstream feature: `vnd:sqlx_mysql` and `vnd:sqlx_postgres`. ## Consequences * Script authors who recognise an upstream crate know what `vnd:` module to `require`. The reverse is also true: anyone reading `require("vnd:hyper")` knows to consult the [`hyper` crate documentation](https://docs.rs/hyper/) for the underlying behaviour. * Bumping a `vnd:*` module to a new major version of its upstream crate is straightforward — the module name does not change, and breaking changes propagate to scripts on a known schedule. * Replacing the upstream crate with a different one is a breaking change. A script that `require`s `vnd:hyper` cannot transparently switch to `vnd:reqwest`; the migration is explicit at every call site. This is a feature, not a bug — the runtime does not lie about which crate it is using. * Some `vnd:*` names read awkwardly in Lua because Rust crate naming conventions occasionally produce names like `serde_json` or `quick_xml`. The cost is borne at the call site for the benefit of the lookup-to-documentation path. * The naming rule is non-negotiable. New `vnd:*` modules must follow it; existing names cannot be retroactively prettified. --- --- url: /adrs/0005-tuple-error-convention.md --- # 0005. Tuple-return convention for recoverable failures Date: 2026-05-03 ## Status Accepted ## Context Lua and Luau permit functions to fail in several ways. The standard library mixes these freely: * Some functions return `nil` plus an error message: `io.open` returns `nil, ": No such file or directory"` on failure. * Some return `false` plus an error message: `os.rename` follows the same shape. * Some raise (`error(...)`): `assert`, `tonumber` with a strict base, `package.loadlib` on certain failures. * Some produce `nan` or `inf` rather than failing: `math.sqrt(-1)`, division by zero. Embedded host runtimes need a discipline. A script reading documentation and writing call sites should not have to remember per-function which shape applies. Two broad patterns dominate: * **Always return tuples.** Every fallible function returns `(value, err)` where exactly one is `nil`. Easy to write call sites for; verbose for chained calls; conflates programming errors with runtime failures. * **Always raise.** Failures propagate as Lua errors, caught with `pcall`. Idiomatic for some Lua libraries; pushes recovery cost onto every call site that might fail; loses the direct call-site signal. A hybrid is more honest: distinguish *recoverable* failures (the file is missing; the network is down; the parser was given malformed input) from *programming errors* (the wrong type was passed; a closed handle was reused; a non-sharable userdata was given to a sharing surface). Recoverable failures are runtime conditions; programming errors are bugs. ## Decision `runblox-core` modules use two failure shapes, chosen per function based on the kind of failure being signalled: `(value, err)` tuple : The function returns two values. On success, `value` holds the result and `err` is `nil`. On failure, `value` is `nil` and `err` is a string. This shape is used for *recoverable* failures — conditions a script is expected to inspect and react to. Raised error : The function calls `error(...)`, propagating up the stack until caught by `pcall` or `xpcall`. This shape is used for *programming errors* — conditions that indicate a bug in the script rather than a runtime condition the script should handle. The choice of shape is fixed per function and documented on the relevant module reference page. The two shapes are not interchangeable. Every error message — whether returned in the `err` slot of a tuple or raised — begins with a qualified prefix that names the module and function: `"fs.read_to_string: No such file or directory (os error 2)"`, `"workers.shared: not a sharable userdata; expected one of std:collections.map, ..."`. The prefix is consistent across every module so that scripts can match against it programmatically and readers of stack traces can locate the failing call site. ## Consequences * Script authors can read a function's signature and know how to handle its failures without consulting prose. A function that returns `(value, err)` is recoverable; a function that returns a single value may still raise but only on misuse. * The discipline scales: every new module follows the same rule, and every existing module's surface is auditable against it. * Recoverable failures and programming errors look different at the call site. A script that wraps a `pcall` around an I/O call has misunderstood the surface. * Functions that change shape — moving from raise to tuple, or vice versa — are breaking changes for scripts that handle their failures. The choice of shape at first introduction matters. * Some upstream crates raise where the runblox-core module surfaces a tuple, or vice versa. The Lua adapter layer is responsible for translating, and that translation is documented per module. --- --- url: /adrs/0006-hidden-async-with-sync-looking-apis.md --- # 0006. Hidden async with synchronous-looking Lua APIs Date: 2026-05-03 ## Status Accepted ## Context `runblox-core` runs scripts on top of a Tokio runtime through `mlua`'s Luau bindings. Many capabilities exposed to scripts — HTTP, networking, database access, filesystem I/O — are async at the Rust layer. The runtime needs a discipline for surfacing async work to Lua. Three patterns were considered: * **Explicit async with `:await()`** — async functions return a future or promise object, and scripts call `:await()` to unwrap it. Familiar from JavaScript, Rust, Python. Requires script authors to track which calls are async. * **Hidden async with synchronous-looking APIs** — async functions look like plain function calls. The coroutine yields to Tokio internally; the script never sees the yield. Adopted by every major Lua-on-async-runtime. * **Hybrid (callbacks + coroutines)** — both patterns coexist. The Neovim ecosystem went this route and ended up with split conventions and doubled cognitive load. Every major Lua-on-async-runtime converges on hidden async: | Runtime | Approach | |---|---| | `mlua` (Rust Lua bindings) | `create_async_function` bridges Rust futures to Lua coroutines; the Lua side calls them as plain functions. | | Lune (Luau on Tokio) | `net.request` returns the response directly. Fully synchronous-looking. | | OpenResty (ngx\_lua) | Cosockets — all I/O non-blocking but synchronous-looking. Gold standard at Cloudflare and Kong scale. | | Tarantool | Fibers — all I/O implicitly yields the current fiber. The most mature implementation of this pattern. | | Luau in Roblox | Built-in APIs are yield-based. The community `roblox-lua-promise` package exists, but official APIs do not use it. | Runtimes that surface explicit `await` to scripts (Deno, GDScript, MicroPython) all do so in languages that have `async`/`await` as language keywords. Lua has coroutines but no syntactic support for futures or promises. Exposing futures into a Lua script creates an alien, non-idiomatic surface and forces every call site to track which functions are async. The cautionary tale is Luvit, which adopted Node-style callbacks in Lua. The result is widely considered the worst developer experience in the Lua-on-async space. The Neovim ecosystem ended up with both patterns coexisting, producing the split-ecosystem cost the hybrid approach is supposed to avoid. ## Decision `runblox-core` exposes async work to Lua scripts as **hidden async with synchronous-looking APIs**. There are two layers: ### Layer 1 — synchronous-looking APIs (the default) Every async operation looks like a regular function call. The Lua coroutine yields to Tokio internally; the script never sees the yield, and there is no future, promise, or `:await()` to unwrap. ```lua local hyper = require("vnd:hyper") local resp = hyper.get("https://example.com") print(resp.body) ``` A reader cannot tell from the call site whether `hyper.get` is implemented as a blocking call or as a coroutine yield. This is by design. ### Layer 2 — concurrency primitives (opt-in) For scripts that need parallelism, two mechanisms are available: **Convenience batch APIs** for common patterns. These cover the majority of concurrency use cases without exposing the underlying primitives. ```lua local hyper = require("vnd:hyper") local responses = hyper.get_all({ "https://a.example.com", "https://b.example.com", "https://c.example.com", }) ``` **`task.spawn` / `task.await` primitives** for arbitrary concurrent work. This mirrors the OpenResty `ngx.thread.spawn` / `ngx.thread.wait` and Lune `task.spawn` patterns. ```lua local task = require("std:task") local hyper = require("vnd:hyper") local t1 = task.spawn(function() return hyper.get("https://a.example.com") end) local t2 = task.spawn(function() return hyper.get("https://b.example.com") end) local r1 = task.await(t1) local r2 = task.await(t2) ``` `task.await` on a spawned task handle is semantically distinct from a hypothetical `:await()` on every API call. It joins a concurrent unit of work — fork-join semantics — rather than unwrapping a promise. The async colour does not leak into the surrounding code. The `task` module is forward work. This decision establishes the shape it must take when it lands. ## Consequences * Script authors write code that reads as if it is synchronous. Sequential I/O is sequential at the call site, even when the underlying operations yield to Tokio. * The async-vs-sync distinction does not leak into the script's call sites or type signatures. There is no "async colour" the script must propagate. * Every Lua-on-async-runtime an experienced reader has used works the same way. There is no learning curve for the basic surface. * Concurrency requires an explicit opt-in. A script that does not reach for `task.spawn` or a batch API runs sequentially. This is the intended default — most scripts do not need parallelism, and the ones that do should declare it. * The Lua adapter layer in each module is responsible for bridging Rust futures into Lua coroutines through `mlua`'s `create_async_function`. New `vnd:*` and `std:*` modules wrapping async crates must follow this pattern; the module layer cannot expose raw futures to scripts. * The `task` module is committed to as a future surface. Until it lands, parallelism is available only through the worker pool ([`std:workers`](/std/reference/workers)) or through ad-hoc Lua coroutine use. Both are workable but not the recommended path. * Reasoning about a script's behaviour under concurrency is harder than it would be with explicit `:await()` markers. The cost is borne by anyone debugging a script that mixes I/O and shared state; the runtime's documentation must be explicit about which calls yield. ## References * [OpenResty cosocket design](https://api7.ai/learning-center/openresty/the-core-of-openresty-cosocket) * [Lune task scheduler](https://lune-org.github.io/docs/the-book/9-task-scheduler/) * [`mlua` async support](https://github.com/mlua-rs/mlua) * [Neovim async patterns](https://dzx.fr/blog/async-lua-in-neovim/) (cautionary tale) * Originating issue: [flying-dice/runblox-core#44](https://github.com/flying-dice/runblox-core/issues/44)