# Whitelist Capability Config

A pattern for embedded config or scripting where the host application declares, capability by capability, what the config author is allowed to invoke. The contrast is with the blacklist approach common to embedded Lua and Python: load the full language, then try to remove `os`, `io`, `require`, and hope nothing else escalates.

The [[ryelang-whitelist-config]] post puts it sharply: "blacklisting is preventing known dangers, but there are always unknown unknowns, and you can't prevent what you don't know you should." The unknown-unknowns argument is the same one that motivates default-deny network policies and capability-based OS designs.

## The two failure modes the pattern is reacting to

**Bottom-up creep.** Configs that started as INI-style key-value end up reinventing a language. YAML grew template engines (Helm, Ansible), Nginx added `if`, Terraform invented HCL with workarounds like `dynamic` blocks for missing `if`. The hidden language is informal, often inconsistent, and acquires a parser-plus-templater-plus-evaluator pipeline nobody designed end-to-end. Greenspun's tenth rule, applied to config.

**Top-down embedding.** "Just embed Lua." The config can now do everything, including `os.execute` and `io.open`. The mitigation is to remove dangerous bindings before evaluating untrusted code. This requires knowing the full surface of what's dangerous, which you don't.

The whitelist alternative is to pick a language that doesn't include those forms in the first place, and to ship the host with a per-feature registration API.

## Two ways to implement it

**Hard-coded restricted dialect.** Starlark is the canonical example: Python-shaped, no recursion, no `while`, no I/O. The restriction set is fixed at the language level. Modules are gated by import lists. You can refuse to register a module, but you can't refuse just one builtin inside it.

**No-reserved-form host language.** [[rye-lang]] is the example the post is making. The base evaluator can parse syntax and bind names, and that's it. `if`, `fn`, `+`, `*` are all functions in the standard library that you choose to register. You can grant `_+` without `_*`, or `if` without `loop`. The granularity is per-word.

The tradeoff is maturity vs. expressiveness: Starlark is production-tested at Bazel scale; Rye's approach is a research-grade exploration with a smaller blast radius per granted capability.

## What you still need on top

Capability whitelisting controls *which words exist*. It does not control *how much work the evaluator does*. Both languages also expose execution caps — call-stack depth, total operation count, wall-clock time — to turn runaway code into errors instead of hangs.

It also doesn't bound the host-side effects of any builtin you register. The closing warning of [[ryelang-whitelist-config]] is explicit: "this is capability control, not a security sandbox." Rye runs in the host process; if you register `read-file`, the config can read any file the host can read. For untrusted input you need a real boundary on top — process isolation, seccomp, or a separate VM. See [[sandboxing-ai-agents]] for the recurring four-layer model.

## Where it shows up

- Config-as-code where the author is friendly but you want to keep the expressive surface small (the Rye article's framing)
- Plugin systems where third-party authors get a fixed set of host primitives and nothing else
- AI-agent action languages where each new capability is a deliberate decision (loose analogue to [[mcp-vs-skills]] capability scopes)
- Game scripting where designers get a vocabulary curated by engineers

## Related

- [[ryelang-whitelist-config]] — the article that lays out the pattern in six steps
- [[rye-lang]] — the example implementation
- [[no-silver-bullet]] — config-language accidental complexity as a recurring shape
- [[sandboxing-ai-agents]] — the layer above capability control, when input is hostile
