▗▄▆███▆▄▖
  ▗▟▀    ╋    ▀▙▖
       ◢███◣
 ◢█◣   █◠◠◠█   ◢█◣
 ▐█▌█████╳█████▐█▌
 ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔

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.

exarch itself outside every grant; holds the API keys, writes the record
the agent --base reasonable

every program it writes, every turn

plan-review grant: `read-only

a sub-agent, started from ral

notes-a 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.

one call
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.

the agent writes
let hits = grep-files #'fn render'# .
cargo test -q
echo $[!{length $hits} + #' matches'#]
and reads back; cargo never started
[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:

grants intersect
grant [exec: [allow: [`name git, `name ls]]] {
    grant [exec: [allow: [`name git], deny: [`name ls]]] {
        git status   # allowed
        ls           # denied
    }
}
what the agent is told
[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:

whatchecked 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.

reasonable Everyday coding. Reads and writes the project; writes scratch and caches; runs your toolchain; network on.
edit-only Patching. Writes the project and scratch; git and interactive shells denied; nothing in the project may run.
read-only Review. The project is read-only; writes go to scratch alone.
minimal A small base to extend: project, scratch, system tools, no home directory.
confined An offline build jail: one project, scratch space, no network.
dangerous Restricts nothing. Once you restrict, exarch excludes its own binary, its credential files and your restriction files. For a VM or container, or as the base for --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.

a grant computed per child
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:

a later call
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 json adds 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. --resume picks a session up again.
Your instructions
AGENTS.md files 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
macOS arm64 exarch-macos-arm64
Linux x86_64 exarch-linux-x86_64
Linux arm64 exarch-linux-arm64
Windows x86_64 exarch-windows.exe

Then set a provider key, go to the project, and run it:

ANTHROPIC_API_KEY=… exarch

Built with funding from the Advanced Research + Invention Agency (ARIA).