Map: exarch
exarch is a small LLM coding agent — a separate workspace binary that
embeds ral-core (ral-core = { path = "../core" }). A model
(Anthropic / OpenAI / Gemini / xAI / Qwen / OpenRouter / DeepSeek / OpenCode, a
custom provider, or a signed-in ChatGPT account) is
given one ral tool, and each call is evaluated as a ral top-level turn
against a persistent in-process Shell, under capabilities the user chose. It
ships as one executable (single-binary); the same
binary re-execs itself as the OS sandbox child.
The agent is a provider loop over one tool, each turn grant-framed — the design argument lives in exarch-architecture. This page maps the binary’s front door: what runs at startup, and how the system prompt is assembled.
Entry and dispatch
run (in lib.rs, lifted out of main so integration tests link the whole
crate) is the session path; main.rs is the thin shell over it. On the way out,
main runs ral_core::sandbox::teardown_session — reverting the session’s
AppContainer grant ACEs and deleting its profile on Windows, a no-op elsewhere —
before surrendering the exit code.
- Pre-
maintrampoline. Before any setup,dispatch_pre_mainshort-circuits a re-exec child, returningOption<u8>:install_child_hooks_and_serve_helpers(set the child-shell extension that dresses a sandbox-IPC child with exarch’s host builtins, then serve the pipeline-stage / test-helper re-execs).or_elsethe OS-sandbox stage (`serve_sandbox_early_init`).mainand every test#[ctor]run this identical function — they differ only in how they act onSome(exit vs return theu8). A test binary reachesmainonly through libtest yet is the same multicall executable a child re-execs; skip the sandbox stage and the confined transport stays unpinned, so that binary’s confined-path tests cannot run. - Subcommands (
cli.rs) run an out-of-band action and exit before any session setup:login/logout/accountsmanage signed-in ChatGPT accounts (see below),--modeland the session flags are ignored on this path. - Session. Absent a subcommand,
runresolves the initial provider+model (--providerpins the identity;--effortsets reasoning effort;--chatdrops the system prompt and all tools), composes the capability lattice (policy::for_invocation, → policy), assembles the system prompt (prompt::assemble), builds the trunk `Agent` +Provider, and hands off to one frontend — the inline TUI or, under--headless, the pipe-friendly headless runner — which wraps the trunk’s shared handles in a `Fleet`.
Accounts
Several ChatGPT subscriptions can be signed in at once; each is its own
selectable provider, switched in the /model picker exactly like any API-keyed
provider — there is no second account dimension.
loginadds or refreshes one account (opening a browser, or--device-authfor a headless host)./loginperforms the same sign-in inside a running TUI session, then admits the returned shared OAuth credential to both the live store and model catalog; a refresh of the selected account is visible to its next request.logoutremoves one account by email or id, or--all.accountslists the signed-in set.
The token store holds a list of logins keyed by account id; run resolves each
into its own OAuth-backed provider, ordered after the API-key providers.
Credentials and the env scrub
provider/credential.rs resolves every provider’s secret once at startup and scrubs
the key variables from the process environment, so no child a tool call spawns
can inherit a live key.
CredentialStore::resolve_and_scrubsweeps every known provider — the famousProviderKinds and thecustomproviders fromconfig.ral— reading each one’s conventional key variable (key_env) into the in-memory store, then removing from the environment every key variable that was present, whether or not it yielded a usable key. A malformed value (a pasted newline) is still a live secret, so it is swept too.- The scrub makes resolution eager: once a variable is gone it cannot be re-read, so a key absent at startup stays absent for the run. Read and removal happen while the process is still single-threaded — before any session worker — so the env mutation cannot race.
- A signed-in ChatGPT account never touches the environment: its login is
loaded from the OAuth token store into an
OAuthcredential cell (), a distinct identity from anOPENAI_API_KEYprovider, so the two coexist. - This is the subtractive half of exarch’s env shaping; the additive half —
NO_COLOR,$EXARCH_SCRATCH, and the redirected tool homes — is seeded onto the session shell (). Per-spawn loader-variable hardening (strippingLD_PRELOAD/LD_AUDIT/LD_LIBRARY_PATHunder an active grant) is ral-core’s, not exarch’s. The why is provider-config-ral-script; the boundary a child actually inherits is ral’s grant.
Bootstrap
bootstrap.rs holds the once-per-process pieces; nothing here is per-turn.
boot_shell— the one constructor that may boot a session shell: clear stale ral interrupts, install ral’s handlers, chain exarch’s cancel over them, call core’s `ral_core::driver::boot_shell` with exarch’s host surface (builtins::host_surface()) so the host builtins ride construction, then source theagent.rallibrary, suppress ANSI colour at the source, and seed the exit hints.- Machine probing —
host::snapshotformats the live machine into the prompt’sHostsection over core’sral_core::hostprobes (os,now,cwd,user,home,git,exarch logs), best-effort: a missing value drops its line. Scratch— the disposable per-session directory exposed as$EXARCH_SCRATCH, with the legacy build-tool homes (CARGO_HOME, …) redirected into it so a write lands inside the grant rather than in a denied real cache.log_run_dir— the durable per-run session-log directory at$XDG_STATE_HOME/exarch/<project>/<run>/, keyed by a slug of the project cwd (project_slug), so logs survive an abnormal exit. The persisted model selection (state.json) lives under the same per-projectproject_dir.xdg_app_dir— the one spelling of the$XDG_<kind>_HOME/exarch/convention thatproject_dir(state), the model cache (cache), and the trusted config home all build on (exarch-config-dir).
System prompt
prompt::assemble builds the prompt from (heading, body) sections walked by one
uniform renderer, in order persona, Ral, Editing, Builtins, Tasks,
Script style, Host, [Workspace], [Skills], Agent|Surfacing.
- Persona (
data/system.md, unheaded — it sets the tone, not a topic) frames the session as one continuing shell script: definitions, working directory, and worker threads persist across turns, and the working method is act early, batch what belongs together, never re-derive. Ral(data/ral.md) is the language and tool reference; its handler docs follow the lambda-only install rule (handlers-and-aliases-are-lambdas): per-commandhandlers:entries are unary{ |args| … }, the catch-allhandler:binary{ |name args| … }.Editingdocuments the file-editing scheme the--editflag selects: line-hash (data/edit-hash.md) or string-replace (data/edit-replace.md); only the prompt text switches, both builtins stay registered (hash-addressed-editing).Builtins(builtin_index) lists every builtin and prelude function by name only — a progressive-disclosure index the agent expands at runtime withhelp/explain, so the prompt cannot drift.Tasks(data/tasks.md) is the task-management kit API.Script style(data/script-style.md) is the reuse guide: one program, not a nervous probe — define then query, parameterised blocks, records for knobs, blocks as policy, long-running work behinddefer/await.Hostis the environment snapshot (host::snapshot, ) with the live grant under it: a static legend (data/grant-legend.md) over the capability bullets, or one ambient-authority line when nothing is attenuated.Workspace(discover_agents) collects theAGENTS.mdinstruction files, outermost first so the deepest file’s recency wins: the operator’s<config>/AGENTS.md, then every repoAGENTS.mdfrom the git root down to cwd (the walk stops at the first.gitentry; outside a repo, onlycwd/AGENTS.md). Present whenever any is found; project guidance that cannot widen the grant (agents-md-injection).Skillslists each discovered readable skill as onename: descriptionline, loaded on demand with theskillbuiltin — progressive disclosure again.- The closing section is chosen by mode, last where recency carries: headless
gets
Agent(data/agent.md), thereplyreturn-channel contract; interactive getsSurfacing(data/surface.md), the five Bertin marks and the role set (surface-carries-documents, → cards).
--system FILE... replaces only the persona slot with the user-supplied files;
every other section stands.
Subsystems
- agent — the uniform node and the thin
Fleet: the turn loop (provider round-trips, tool dispatch with prompt-queue steering, auto-compaction, the nudge-retry policy, sub-agent fork), theparentpredicate, the owned hot-swappable provider, dynamic focus, and the subtree cancel cascade. - provider — LLM transport over genai: streaming, the retry driver, prompt caching, usage and dollar accounting.
- shell-eval — one tool call as a ral top-level turn under a pushed capabilities frame; the streaming digest and the surface host sink.
- policy — capability composition (base ∨ extend ⊓ restrict) and the bake-in profiles; the boundary is ral’s grant.
- tools —
ralis the one tool;tools.rsis a thin seam over it, with no registry. Every other harness verb — theagentspawn (one record-spec verb,`amnemon/`mnemonby field, fuel-gated),reply, the schedule family — is a builtin reached through it, answered by the desk. The sub-agent model is agents. - builtins — the resident host atoms and the harness
verbs: the hash-addressed edit primitives, the spawn/schedule/reply
family, and the
agent.ralhelpers (why). - frontend — the agent/UI boundary (event bus, session log) and the two frontends, the inline TUI and headless.
- cards — the render document
surfacecarries: acardof closed Bertin marks decoded once and drawn by one generic interpreter; open card set, closed mark set. - io-surface — every redirect read/write and exec image as one card: core emits an I/O event at the runtime doors, exarch binds it to a mark; the closed door set is clippy- and meta-test-enforced.
Sandbox
exarch does not invent its own sandbox. Each tool call is one transport Turn
carrying the profile’s capabilities in Turn.caps, dispatched across the
host seam (shell_eval::run_shell, → shell-eval) and
pushed onto ral’s capability stack by core’s turn door — so the safety
boundary is ral’s grant mechanism — authority attenuated by
intersection. There is no source-level grant { … } the model could escape;
the frame is installed by the host. Profiles ship as .exarch.ral files in
exarch/data/ (dangerous, reasonable, edit-only, read-only, minimal,
confined); see exarch/PROFILES.md and policy.
Scheduled wakeups
With --allow-schedule, the agent may schedule its own wakeups (schedule,
schedules, unschedule) — a cron expression or a one-shot after <dur>. A
wakeup schedules the agent, not a worker: at its time a synthetic user turn is
posted to the agent’s own inbox and delivered at the turn boundary, re-engaging
the loop with no human present. It is off by default — waking yourself
indefinitely is real authority. The inbox/reaper mechanics live on the
frontend and agent pages; see
scheduled-wakeups.
Where to look
exarch/data/{system.md, ral.md, edit-hash.md, edit-replace.md, tasks.md, script-style.md, grant-legend.md, surface.md, agent.md, agent.ral}— the persona rules, ral reference, editing schemes, task kit, reusable-script guide, grant legend, surfacing guidance, headless reply contract, and the embedded agent helper library.- Provider configuration — a famous provider auto-populates from its env key, an
unusual one from a hand-written XDG
config.ral(provider-config-ral-script). exarch/README.md,exarch/PROFILES.md— human docs.
repl is the sibling ral binary over the same engine.