Rhai scripting
Eventually you want something Leviath does not ship: a tool for your internal API, a model provider nobody has added yet, a rule about which calls are allowed. You should not have to fork the project and rebuild it for that.
So Leviath embeds Rhai, a small scripting language that looks a lot like Rust. Drop a .rhai
file in the right directory and it becomes part of the runtime, with no recompile and usually no
restart.
Each script stays small, because Leviath keeps the hard parts. Transport, budgets, sandboxing, and retries stay with the runtime, and the script gets only the one decision that is specific to you.
You do not need to know Rhai to read the pages below. Each one has a complete, working script you can copy and change. rhai.rs has the language reference if you want it.
The extension points
| What | Where it goes | What the script decides |
|---|---|---|
| Model providers | ~/.leviath/providers/<name>.rhai |
How to map a request onto some HTTP API, and the response back |
| Context regions | beside the agent, referenced by script = |
How one region renders, accepts writes, and sheds content under pressure |
| Stage hooks | beside the agent, referenced by [stages.<name>.hooks] |
What happens at seven points in an agent's lifecycle, from entering a stage through to the end |
| Global tools | ~/.leviath/tools/*.rhai, or an agent's own tools/ |
A new tool, its schema, and what it does |
| Output validators | beside the agent, referenced by the stage's [output] block |
Whether a submitted final output is accepted, and what the model is told when it is not |
| Policy rules | rules/*.rhai in your OS config dir, see configuration |
Whether a given tool call is allowed to fire |
Each page walks its point end to end with a complete, copy-pasteable example.
The sandbox they all share
Every script runs in a hardened engine: no eval, no import, no ambient filesystem or
network access, bounded operations and expression depth, a capped call depth, and print/debug
muted. The operation budget scales with the job: 500k operations for tool scripts and providers,
100k for hooks, regions, and validators. The host functions each extension point offers are the only way out of it, and they differ
by point:
- Provider scripts get HTTP, JSON, SSE parsing, and encoding helpers, because mapping an API is their whole job.
- Tool scripts get HTTP, shell, file, and environment access, each independently gated by
[tool_script_permissions]. - Region hooks and policy rules get nothing. They are pure data transforms over the
ctxthey are handed.
Names are matched exactly, and the match is enforced. A script missing the function its surface
needs, or defining it with the wrong arity, fails at spawn with a compile error rather than
silently never firing. lev validate and lev tools catch it before a run does.
Warning
A script tool can read environment variables, but a name that looks like a credential is refused
unless you list it in [security] allow_env_vars. Without that,
a two-line tool reading ANTHROPIC_API_KEY and POSTing it elsewhere was a working exfiltration
path with no prompt anywhere in it.
Hot reload
Provider scripts are recompiled when the file's mtime changes, so an edit takes effect on the next run with no daemon restart. Region scripts are read and compile-checked once, at spawn, so an edit applies to the next run rather than an in-flight one.
Neither is scanned or executed until something actually references it, so dropping a file into a directory does not by itself run it.