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-main trampoline. Before any setup, dispatch_pre_main short-circuits a re-exec child, returning Option<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_else the OS-sandbox stage (`serve_sandbox_early_init`). main and every test #[ctor] run this identical function — they differ only in how they act on Some (exit vs return the u8). A test binary reaches main only 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 / accounts manage signed-in ChatGPT accounts (see below), --model and the session flags are ignored on this path.
  • Session. Absent a subcommand, run resolves the initial provider+model (--provider pins the identity; --effort sets reasoning effort; --chat drops 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.

  • login adds or refreshes one account (opening a browser, or --device-auth for a headless host).
  • /login performs 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.
  • logout removes one account by email or id, or --all.
  • accounts lists 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_scrub sweeps every known provider — the famous ProviderKinds and the custom providers from config.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 OAuth credential cell (), a distinct identity from an OPENAI_API_KEY provider, 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 (stripping LD_PRELOAD/LD_AUDIT/LD_LIBRARY_PATH under 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 the agent.ral library, suppress ANSI colour at the source, and seed the exit hints.
  • Machine probinghost::snapshot formats the live machine into the prompt’s Host section over core’s ral_core::host probes (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-project project_dir.
  • xdg_app_dir — the one spelling of the $XDG_<kind>_HOME/exarch/ convention that project_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-command handlers: entries are unary { |args| … }, the catch-all handler: binary { |name args| … }.
  • Editing documents the file-editing scheme the --edit flag 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 with help/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 behind defer/await.
  • Host is 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 the AGENTS.md instruction files, outermost first so the deepest file’s recency wins: the operator’s <config>/AGENTS.md, then every repo AGENTS.md from the git root down to cwd (the walk stops at the first .git entry; outside a repo, only cwd/AGENTS.md). Present whenever any is found; project guidance that cannot widen the grant (agents-md-injection).
  • Skills lists each discovered readable skill as one name: description line, loaded on demand with the skill builtin — progressive disclosure again.
  • The closing section is chosen by mode, last where recency carries: headless gets Agent (data/agent.md), the reply return-channel contract; interactive gets Surfacing (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), the parent predicate, 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.
  • toolsral is the one tool; tools.rs is a thin seam over it, with no registry. Every other harness verb — the agent spawn (one record-spec verb, `amnemon/`mnemon by 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.ral helpers (why).
  • frontend — the agent/UI boundary (event bus, session log) and the two frontends, the inline TUI and headless.
  • cards — the render document surface carries: a card of 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.