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, what it does, and the typed parts it reads and makes |
| 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 |
| Mime checks | beside the config or the agent, referenced by a mime row's check |
Whether bytes claiming one of your mime types are that type, before they are stored |
| 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.
A region hook, a stage hook and an output validator are all named by path in the manifest, so a file
sitting beside the agent is invisible until something names it. If you are building an editor rather
than writing the manifest by hand,
GET /api/scripts?agent=<name>&include=candidates
lists the .rhai files under an agent's directory that nothing declares yet. Each comes with the
manifest-relative path to write into validator = "..." or [stages.<name>.hooks].
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, five million for a mime check scanning a file. The host
functions each extension point offers are the only way out of it. 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.
What every script can call
Available at every extension point, including the ones that get no host access at all. None of these reach outside the process. They only transform values the script already holds:
| Group | Functions |
|---|---|
| Strings | contains, starts_with, ends_with, trim, join, split |
| Content | count_tokens, is_json, is_markdown, is_mermaid, is_empty, content_format |
| JSON | parse_json, to_json |
On top of that, tool and provider scripts get encode_uri, encode_base64, decode_base64 and
html_to_text, plus the host functions that do reach outside. Those are what
[tool_script_permissions] governs. The full list
per point is on that point's own page: tools,
providers, regions, hooks,
validators, mime checks.
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.
Policy rules reload as well. The daemon stats policy.toml and every rules/*.rhai beside it, so a
rule you add, edit, or delete gates the next run. Nothing is restarted for that either.
A mime check named by the operator's rows is recompiled whenever mime_types.toml or the
config changes. That reaches the runs already under way as well as the next one. One named
by a blueprint is compiled at spawn, like the agent's other scripts.
Neither is scanned or executed until something actually references it, so dropping a file into a directory does not by itself run it.