▗▄▆███▆▄▖
▗▟▀ ╋ ▀▙▖
◢███◣
◢█◣ █◠◠◠█ ◢█◣
▐█▌█████╳█████▐█▌
▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔A coding agent that works in a shell, inside limits it cannot touch.
Its one tool is a persistent ral session. Every program it writes runs under a grant you chose, and every agent it starts runs under a smaller one.
curl -fsSL https://lambdabetaeta.github.io/ral/scripts/exarch-install.sh | sh
Authority only narrows
An exarch was a Byzantine viceroy: full power, within a
bounded province. Here the province is a grant, and
provinces nest.
--base reasonable
every program it writes, every turn
grant: `read-only
a sub-agent, started from ral
grant: `restrict [net: `offline, fs: [read: [`cwd notes/a]]]
its own sub-agent; the checks fold every ring above it
Each ring is checked by folding it together with every ring outside it, so a sub-agent that asks for more than its parent holds simply gets less. The model never sees a switch that widens the boundary, because there is none.
One tool, and the tool is a language
Most agents choose from a menu: read, grep, edit, bash. exarch
writes a ral program. Reading, searching, editing,
testing and starting sub-agents are all functions in the same
language, so one call can do what a menu needs five round trips
for.
The session persists. Bindings, the working directory and the environment carry from one call to the next, and each call is type-checked against everything already bound. Results come back as data: lists and records the next call can filter, not text to be split again.
let suite = spawn "tests" { cargo test -q 2>&1 }
let impl = grep-files #'fn render'# src/widget
let log = lines !{await $suite}
[impl: !{take 4 $impl}, log: !{take 20 $log}]
The test suite runs in the background while the search does; the call answers with one record.
A program is checked whole before any of it runs. A type error on the last line means the first line never happened.
let hits = grep-files #'fn render'# .
cargo test -q
echo $[!{length $hits} + #' matches'#]
[T0010] Error: couldn't match type Integer with type String
╭─[ turn.ral:3:6 ]
│
3 │ echo $[!{length $hits} + #' matches'#]
│ ────────────────┬────────────────
│ ╰────────────────── Integer doesn't match String
│
│ Help: the two sides of a `+` / `-` / `*` / `/` must have the same numeric type
───╯
Limits the model cannot edit
The grant is not a line in the agent's program, where the agent
could delete it. exarch pushes it onto the shell before each call
runs. The agent may write its own grant to narrow what
a block can do; it has no way to widen one. Nested grants meet:
grant [exec: [allow: [`name git, `name ls]]] {
grant [exec: [allow: [`name git], deny: [`name ls]]] {
git status # allowed
ls # denied
}
}
[R0001] Error: command 'ls' denied by active grant: '/usr/bin/ls' is excluded by `` `name ls ``; an allowance cannot override an exclusion ╭─[ turn.ral:4:9 ] │ 4 │ ls # denied │ ─┬ │ ╰── here │ │ Help: the exclusion belongs to the policy that imposes it; change that policy ───╯
A denial is an error with a reason, not a bare
EPERM, so the agent can change course. Two enforcers
share the work, each covering what the other cannot see:
| what | checked by |
|---|---|
| which commands run | ral, before anything is spawned |
| files the shell reads and writes itself | ral, on the object a path resolves to |
| files and network of every external program | the operating system, per command: Seatbelt on macOS, bubblewrap with Landlock and seccomp on Linux, AppContainer on Windows |
| API keys | removed from the environment at startup; no program the agent runs inherits them |
| exarch's own credential files and restriction files | denied in every profile that restricts the filesystem |
Profiles
A profile is a ral program that returns a grant. Pick one with
--base, add to it once with --extend
(an allowance widens it; an exclusion still wins), or narrow it with any number of --restrict files.
--restrict.
This is defence in depth for a development tool, not a hardened jail; the profiles page lists what each one admits and where the gaps are. For a wall, run the shell under synod, which puts it in a virtual machine whose only route out is an allow-listed proxy.
Sub-agents are started from the shell
There is no separate delegation tool. The agent starts
sub-agents with exarch-agents, from inside the same
ral program, so it can compute their prompts, names and grants,
and start a dozen in one loop.
A child begins with a snapshot of its parent's bindings, so the prompt names data instead of pasting it. It runs under one more grant folded onto its parent's. It answers with ral data, which the parent binds and computes with. Children can start children of their own, three generations deep, and every one is the same agent.
let dirs = glob #'notes/*'#
for !{range 0 !{length $dirs}} { |i|
let only-mine = [net: `offline, fs: [read: [`cwd $dirs[$i]]]]
exarch-agents `start [
prompt: "Summarise the notes under $dirs[$i]. Reply [topic: Str, claims: [Str]].",
name: "notes-$i",
type: `amnemon,
grant: `restrict $only-mine,
search: false,
model: `inherit,
]
}
Each child may read one directory and nothing else, and has no network.
Sub-agents run in the background. When one replies, its parent is woken with a one-line notice, and reads the value only when it wants it, so a large answer never floods the parent's context uninvited:
let parts = map { |i| let r = exarch-agents `read "notes-$i"; $r[reply] } !{range 0 !{length $dirs}}
Each child can run on its own provider and model. It stays addressable after it replies: send it a message, and it resumes with its context intact.
Around the shell
- A transcript drawn as a graphic
- A two-column rail marks each block: its kind by shape, the agent by hue, size by lightness. Nothing streams or spins; waiting is a bar that grows from a fixed point. Bursts of calls fold into one group you can open.
- Any provider
- Set a key and it is found at startup, then scrubbed from the
environment. Sign in to ChatGPT, with several accounts if you
like; add any compatible endpoint in
config.ral. anthropicopenaichatgptopenroutergeminideepseekxaiqwenopencode-zenopencode-go - Headless
exarch --prompt "…"runs one task and prints the reply;--output-format jsonadds turns, tokens and cost.- One record
- Every session is written to a single log, one event at a
time; the screen and the model's context are both replays of it.
--resumepicks a session up again. - Your instructions
AGENTS.mdfiles from the repository root down to the working directory, and skills loaded on demand. They add to the prompt, never to the grant.
Get exarch
One binary, with the ral shell built in. On macOS or Linux:
curl -fsSL https://lambdabetaeta.github.io/ral/scripts/exarch-install.sh | sh
On Windows, in PowerShell:
irm https://lambdabetaeta.github.io/ral/scripts/exarch-install.ps1 | iex
Then set a provider key, go to the project, and run it:
ANTHROPIC_API_KEY=… exarch