Whitelist Config with Rye
- title
- Whitelist Config with Rye
- type
- summary
- summary
- Refaktor's case for embedding Rye in Go apps as a config language with zero hardcoded keywords and per-feature capability whitelisting
- parent
- rye-lang
- tags
- config-languages, go, embedded-scripting, capability-security
- sources
- ryelang-whitelist-config
- created
- 2026-05-11
- updated
- 2026-05-11
A walkthrough on the refaktor/rye blog that frames config language design as a choice between two failure modes: starting too simple and bolting on a language nobody designed (the YAML/HCL trajectory), or starting with a full language and trying to lock pieces away (the Lua/Python trajectory). rye-lang is pitched as the third option: start with no language features at all and register each one you want the config author to have.
The article cites Greenspun's tenth rule and gives the usual examples β Helm's two-language templating, Terraform's missing if faked through dynamic blocks, Nginx's accreted control flow. Then the alternative pitch: embed Lua, drop os, io, require. The blog calls this "blacklisting is preventing known dangers, but there are always unknown unknowns." See whitelist-capability-config for the general concept.
The Rye difference
Rye has no reserved syntax for control flow. Words like if, fn, loop, +, * are all functions registered at the library level. The bare evaluator can only:
- Load syntax into blocks of values
- Bind values to words via
set-word:andmod-word::
That's the zero-capability baseline. Anything else has to be explicitly registered from Go. Compared to Starlark β which is more mature but exposes if/for/def unconditionally and gates modules all-or-nothing β Rye lets you register _+ without _* and grant if without loop.
The worked example
A Go web server that serves markdown files, configured in Rye. The article shows six steps, each adding capability and counting the Go lines required:
| Step | Go lines | Capability added |
|---|---|---|
| 1 | ~80 | Static values, INI-style data |
| 2 | +2 | Arithmetic (_*, _+) |
| 3 | +6 | get-env builtin + any for fallbacks |
| 4 | +14 | route builtin, if, _= |
| 5 | +6 | fn, replace, capitalize, str β user-defined functions called from Go per request |
| 6 | +2 | probe and enter-console (live REPL into the running config context) |
Each capability is one RegisterBuiltinsFilter or RegisterBuiltin call. By step 5, the config can do this:
page-title: fn { slug } {
slug .replace "-" " " |capitalize
}
β¦and the Go runtime calls page-title on every request via ps.Ctx.GetFunction + evaldo.CallFunctionArgsN. The config has become an extension of the application.
Step 6 is the unusual one. enter-console drops the running process into a Rye REPL with the current config context populated. From the prompt you can lc to list bindings, probe port to inspect a value, or rebind via port:: "8080". When you Ctrl-c out, evaluation continues with the modified context. The pitch is config debugging that beats print statements without giving up production safety, since the REPL is opt-in per registration.
Execution caps
Beyond which words exist, two limits cap evaluator effort:
ps.MaxCallDepth = 50
ps.MaxOps = 10_000
Default zero means unlimited. Setting them before EvalBlock turns runaway recursion or infinite loops into a returned error rather than a hang.
What the post is not claiming
The closing note is explicit: this is capability control, not a security sandbox. Rye runs in the host process. Register an unsafe builtin and the config gains it immediately. The author isn't pitching Rye as a Lua replacement for hostile input β it's a way to keep a friendly config from accreting into HCL-with-extra-quirks.
Related
- whitelist-capability-config β the general pattern: default-deny embedded scripting
- rye-lang β language entity (homoiconic, Go-embedded, no reserved forms)
- ryelang-blog β blog tracker
- no-silver-bullet β config-language creep as accidental complexity
- sandboxing-ai-agents β capability layers as a recurring theme in 2026 writing