Map
↑Rye

Whitelist Config with Rye

Wiki summaryconfig-languagesgoembedded-scriptingcapability-security ↳ show in map Markdown
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
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: 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:

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.