# Whitelist Config with Rye

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:` and `mod-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:

```clojure
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:

```go
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
