Skip to content

LuaU Sandbox

codegen embeds the LuaU VM (Roblox’s typed Lua variant with a JIT compiler) as the rule execution environment. Each rule invocation gets a fresh execution context with no shared state between calls.

APIDescription
json.encode / json.decodeJSON serialisation
Native introspection APIget_node, find_struct_by_name, struct_has_attribute, struct_has_method, struct_has_member, struct_has_default_constructor, get_base_classes, get_definition_header — see LuaU Globals
LuaU stdlib subsettable, string, math, ipairs, pairs, tostring, tonumber, error, pcall, xpcall, type, select, assert
CapabilityStatus
File system (io.open, os.execute)Blocked — no io or os library
NetworkBlocked — opt in via permissions.http.allowlist
OS environment variablesBlocked — opt in via permissions.env.os_allowlist
require / module loadingBlocked — no module system in the sandbox
loadstring / loadfile / FFIBlocked
Global state across rule invocationsImpossible — each call gets fresh state
Inter-rule communicationImpossible — rules cannot reach each other
Process spawningBlocked
  • Typed: LuaU adds gradual typing, allowing rule authors to annotate their scripts for better IDE support.
  • Fast: The JIT compiler keeps tight loops over large entity lists performant.
  • Sandboxable: The LuaU VM is designed for embedding with a restricted environment; the engine replaces the global table entirely.
  • Auditable: LuaU is a small language. A non-expert can read and audit a rule script in minutes.

A handler that calls error() (or hits a Lua runtime error like indexing nil) skips that entity, emits E006, and the engine continues with remaining entities. An internal engine error (failed VM init, etc.) is fatal and exits 2.

A preamble that errors aborts the whole run with E007 (exit 1) — the preamble runs once per (rule, outputFile) pair after handlers, and there’s no per-entity granularity to fall back to.

A grouping script that errors emits E008 and the engine falls back to default 1:1 routing for that rule’s entities.

return function(input)
local ok, node = pcall(json.decode, input)
if not ok or not node then
error("failed to decode input: " .. tostring(node))
end
-- ...
end

Validate defensively at the top of every handler. The AST schema can evolve, and future engine versions may pass additional fields or change optional field presence.

Add an allowlist to the rule’s config:

permissions:
http:
allowlist:
- "https://schema-registry.internal.example.com/"

When the allowlist is non-empty, the engine injects http.get(url) into the sandbox. Each entry is matched against the URL’s host — exact (example.com), wildcard (*.example.com, subdomains only), or * for any host. A URL whose host is not on the allowlist raises an error and emits E010.

Never add a domain to the allowlist for a rule you didn’t write and audit yourself — an allowlisted host gives that rule read access to your AST data. Wildcards (*.example.com) widen the trust boundary to every subdomain; use exact hosts when you can.

Add an OS env allowlist or a dotenv file:

permissions:
env:
os_allowlist:
- "BUILD_CHANNEL"

Or drop a .env next to the rule, or a shared <rulesDir>/../shared/.env. Either path activates the env capability and exposes the values via the env module. See Permissions for surface details.

Key Takeaways
  • Each rule invocation gets fresh state — no shared globals, no inter-rule communication.
  • Always available: json + the native introspection API + a LuaU stdlib subset.
  • Network and OS env access are opt-in via permissions: in the rule config.
  • Handler errors skip the entity; preamble errors abort the run; grouping errors fall back to default routing.