The sandboxing model
What neoc strips from Lua, why, and what replaces it.
Why sandbox
Luau ships modules exposing unrestricted OS access — io.open, os.execute, package.loadlib, debug library. Embedding without stripping inherits whatever scripts choose to do.
neoc takes the opposite default. Unsafe standard library stripped at engine construction. Every capability reintroduced through explicit modules under std:*, lib:*, vnd:*. Scripts cannot reach unregistered capabilities.
What is stripped
| Surface | Reason |
|---|---|
io.* | Replaced by std:fs and std:io |
os.* | Replaced by std:thread, std:net, worker model |
package.* | require replaced; see module system |
debug.* | Reflection unsafe in multi-tenant runtime |
loadfile, dofile | Replaced by argument-driven model |
Available unchanged: string, table, math, coroutine, bit32, utf8, buffer, vector, print family.
What replaces it
std:* : Rust standard library mirrors. Filesystem, networking, threading, collections, I/O. See std:*.
lib:* : Gap-fillers and project-native modules. JSON handles, base64, test runner, VCR fixtures. See lib:*.
vnd:* : Vendored Rust crates. Module name matches upstream crate name. See vnd:*.
Note: Scripts cannot invent access to stripped capabilities. No FFI, no shared library loading, no subprocess execution. Only the host extends the capability surface by registering modules at construction.
The worker model
Scripts execute in independent Luau VMs called workers. Each worker: self-contained interpreter with private globals, locals, upvalues, userdata.
Cross-worker state follows Sendable protocol via shared table in thread.spawn. Sendable types: Map, Counter from std:collections, TCP listener from std:net. Non-Sendable types (json handles, json.null) rejected at spawn time. Plain Lua values pass via args field.
- Process coordination:
std:workers - Worker spawning:
std:thread