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 ctx they 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.