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 run
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 run 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 — deleting the session’s
AppContainer profiles 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; hand corepolicy::narrowas its grant narrower, since core carries no base-tag lexicon of its own and a wire-seeded child narrows itself while booting; then serve the--engineand 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;--effortnames a rung of the/modelpicker’s ownEFFORT_LADDER;--chatdrops the system prompt and all tools). The saved project selection is the ground each of those is laid over, and any of them being given saves the result back, so a flag and the picker leave the same trace. Thenruncomposes the capability lattice (policy::for_invocation, → policy), assembles the system prompt (prompt::assemble), builds the trunk `Avatar` viaAvatar::root(RootConfig, RootSeat, provider)— the ral binary seats it onRootSeat::Identity; a wire seat drives a remote engine instead, and synod reuses the same construction — 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
One service may own many accounts. chatgpt is a single service, and a
login email can carry several accounts under it — a personal one and one per
workspace, each with its own issued id. Each account is separately selectable
in the /model picker, exactly like any API-keyed one, so there is no second
account dimension; a key-bearing service is simply the case where the service
owns exactly one account and lends it its name. The shape is
`Service` × `Account`.
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. An email two accounts answer to names neither, so it is refused with both account ids rather than taking whichever came first.accountslists the signed-in set, each named byidentity::labelagainst the others present.
The token store is an object keyed by the rendering of an AccountId, one
entry per account, and so is every map above it. run resolves each login into
its own account, ordered after the key-bearing ones. Because a key-bearing
account’s id is its service name, an existing state.json, model cache
entry, or --provider deepseek keeps working untouched; only a ChatGPT
selection changes key.
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 account — the built-in table and the endpoints declared inconfig.ral— reading eachAuth::Envservice’s conventional key variable 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 its own
OAuthcredential cell ().chatgptis a different service fromopenai, so a login and anOPENAI_API_KEYcoexist, and two logins never share a cell. - 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-run.
boot_shell— the identity seat’s constructor: clear stale ral interrupts, install ral’s handlers, chain exarch’s cancel over them, then dress the shell via the sharedexarch_shell— core’s `ral_core::boot::boot_shell` with exarch’s host surface (builtins::host_surface()) so the host builtins ride construction, theagent.rallibrary, ANSI colour suppressed at the source, the exit hints. Its siblingengine_boot_shellis the wire engine’s boot recipe (EngineInstaller::boot, run engine-side at Attach):exarch_shellplus an engine-localScratchandarm_session_ledgers— the one policy site arming the binding lease and settled-worker retention for both seats — with no signal ceremony (a cancel arrives as aControlframe) and no terminal probe.- Machine probing —
prompt::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 under itsApp’s own name ($EXARCH_SCRATCH; synod’s is$SYNOD_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.App— the product identity (EXARCH; synod names its own) that owns the directory conventions as methods:App::xdg_diris the one spelling of$XDG_<kind>_HOME/<app>/that the project state, model cache, and trusted config home all build on (exarch-config-dir);App::project_dirkeys per-project state by a slug of the launch cwd (project_slug), holding the persisted model selection (state.json); andApp::log_run_diris the durable per-run session-log directory$XDG_STATE_HOME/<app>/<project>/<run>/, so logs survive an abnormal exit.
System prompt
prompt::assemble builds an agent-invariant base from (heading, body) sections
walked by one uniform renderer, in order persona, Ral, Editing, Builtins,
Tasks, Script style, Host, [Workspace], [Skills], [Surfacing]. The
builtin placeholder and the late sections are resolved once per constructed
agent, so a root, identity fork, and wire child each receive their own surface.
- 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: string-replace overview-text(data/edit-replace.md, the default) or line-hash overview-hash(data/edit-hash.md,--edit hash); only the prompt text switches, every reader and editor stays 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.assemblebakes a placeholder here: the real per-agent list — filtered to the harness verbs that agent holds — is resolved byBuiltinIndex::applyonce the agent’s own grants —returns,allow_schedule,spawns(fuel > 0) — are in reach, without a liveShell; zero-fuel agents omitagentsalone.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, and work that must outlive the session behinddetach(survives-exit-is-its-own-verb).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.Surfacing(data/surface.md) belongs to the interactive base and carries the five Bertin marks and role set (surface-carries-documents, → cards). Per-agent resolution then appendsAgents(data/agents.md) ifffuel > 0, followed byReply(data/reply.md) iffreturns; returning interactive children therefore keep both obligations. A headless root is returning too, so with the normal positive spawn fuel it gets both late sections; only a zero-fuel returning agent getsAgentalone.
--system FILE... replaces only the persona slot with the user-supplied files;
the per-agent index and optional sections still resolve from the stored base.
Subsystems
- agent — the uniform node and the thin
Fleet: the attend loop (provider round-trips, tool-call batches 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 run 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 — theagents `startspawn (one record-spec tag,`amnemon/`mnemonby field, fuel-gated,`reply/`readgated onreturns), 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 Run
carrying the profile’s capabilities in Run.caps, dispatched across the
engine protocol (shell_eval::run_shell, → shell-eval) and
pushed onto ral’s capability stack by core’s run 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 (schedules)
— a cron expression or a one-shot after <dur>. A wakeup schedules the
agent, not a worker: at its time a synthetic user item is posted to the
agent’s own inbox and delivered at the exchange 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, agents.md, reply.md, agent.ral}— the persona rules, ral reference, editing schemes, task kit, reusable-script guide, grant legend, surfacing guidance, returning-agent and spawn contracts, 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.