Map: repl / startup
The ral binary startup is a four-part front door: startup.rs decides what
kind of process this is and holds the binary’s engine installers, cli.rs
distils argv into a Mode, main.rs dispatches the answer, and batch.rs
runs non-interactive programs through an engine it boots, speaking only the
protocol. Interactive sessions then hand to the REPL, above a
pre-clap dispatch that lets the binary re-enter itself in confined or helper
roles.
main itself holds no substance — it refuses to run setuid, adopts
the process dispositions, asks startup::identify() what this process is, and
runs the session named. identify answers with a two-armed Invocation:
Shell(Mode), or Exit(ExitCode) for a re-exec child that has already done
its work — or for an argv that is not text: ral’s arguments become strings,
so one that is not UTF-8 is refused with status 2, never mangled.
Pre-main dispatch
One pure classifier names the hidden role a process was started for, and
one dispatch serves it. ral_core::classify(argv)
(core/src/role.rs) reads the first argument alone over Role::ALL and
returns an Invocation: Engine, PipelineAnchor, Warrant,
BundledTool, or Shell. Each flag is spelled once,
by Role::flag, and disjointness lives there and nowhere else — a flag a
script was given, or a tool’s own --engine, names no role. ral_core::invocation::serve(&role, installers)
is the one place a role is served, each role’s body staying with the code it
serves. ral’s identify hands it the role it classified; exarch’s main
calls ral_core::invocation::serve_process(installers), which reads argv
itself, and every test #[ctor] its twin serve_process_before_main, which
reads it where a constructor can (on musl std has no argv before main, so
/proc/self/cmdline, though the role it serves still runs without one: see
exarch). They differ only in what they do with its Option<u8>; what
it decides is when each role is pinned. Before argv
is parsed, main refuses to run setuid — the shell inherits the caller’s
environment and must not run elevated, and neither must any re-exec child, so
the refusal precedes the whole chain — and restores the Unix signal
dispositions. Then, per role:
- Confined child (
Warrant) — served byserve_warrantalone, before any pin, so the child pins and opens nothing before it is confined.ral --warrantis the whole argv of a confined re-exec: the child takes its warrant off fd 99, enters the OS sandbox, and only thenexecves the host program or runs the bundled tool in-process. Windows refuses it. - Pipeline anchor —
pipeline::anchor::serve(--ral-pipeline-anchor, the one multicall re-exec a pipeline uses: a stage itself runs on a thread of the parent process, but a multi-stage pipeline needs one process to hold its pgid open for its whole life). It spawns nothing, so it is not pinned. - Every other role runs
sandbox::bootfirst, which pins the binary (and, on Linux, bwrap) and, on Windows, sweeps what a crashed prior session left registered; the bundled tool onlysandbox::pins, as a re-exec child that reaches no ledger.- Engine (Unix) — then hands the process to
ral_core::engine::run_engine(installers), pinned exactly as the shell is: its grant-confined launches need the envelope as much as the shell’s.ralpassesstartup::engine::INSTALLERS, so a wire-engine child boots through the very recipes the in-process front-ends boot through. The table holds twoEngineInstallers:repl(boot_replover the REPL surface, configured by nothing) andbatch(boot_batchover the batch surface, decodingBatchConfig {args}), each registering the boot door. Both share one grant policy, a refusal (no_seeded_children), because the shell spawns no child engines and has no base-tag lexicon to resolve a grant against. - Bundled tool (
serve_bundled_tool,--ral-bundled-tool <tool> …) — then runs the bundled coreutils/ripgrep image in-process, under whatever sandbox the process already inherited. A bundled tool runs as an external child whenever process semantics are required (bundled-tools-as-exec-images), and each external child launches under the effective policy (sandbox-external-children). - Shell —
None, andidentifyanswersShell(Mode).
- Engine (Unix) — then hands the process to
The roles that are not the shell exit here, never reaching clap.
Modes
ral/src/cli.rs owns the clap surface, and Mode::from_argv is the one door
through it: terminator injection, clap, then Cli::into_mode. The mode tests
enter by the same door, so the parse they exercise cannot drift from the one
startup::identify performs. Cli::into_mode distils the parsed flags into a
Mode: Interactive(InteractiveOpts), Script, or Command — login is not a
mode of its own but a flag on InteractiveOpts (login, set by -l or a
--prefixed argv[0]), so a login shell with -c or a script positional
still resolves to Command/Script and runs it, as cron, su -, and
$SHELL -l -c … require; the flag matters only once resolution has landed on
Interactive, where it gates login-profile sourcing at REPL boot
(loop). RunOpts (--recursion-limit, --capabilities)
rides every mode; BatchOpts adds --audit / --pretty / halt: Option<Halt>
(--check / --dump-ast / --dump-ir, one clap group); InteractiveOpts adds -l, --norc, -i, -s, and
--surface.
- Argv terminator.
inject_arg_terminatorsplices a--before the first positional, and immediately after a-c, so flag-shaped script arguments and a flag-shaped inline-code token (ral -c '--version') survive clap parsing verbatim. A long flag that takes a separate value token (--surface readline,--recursion-limit 4096) must carry that value past itself rather than have the terminator fenced between them; the set of such flags is read from clap’s own model (value_taking_longs), so the injector can never disagree with theClidefinition. - Frontend selection.
--surface <minimal|readline>(a clapValueEnum, defaultreadline) picks the interactive frontend, overriding the rcsurface:key;Nonedefers to rc, then the default. The capability axis stays theRAL_INTERACTIVE_MODEenv var — the escape hatch for setups that do not own argv — and still wins over the surface choice. Surface internals live in frontend. - Interactive/script fork.
InteractiveOpts::reads_stdin_as_scriptencodes the precedence:-sforces stdin-as-script and beats-i,-iforces the REPL, otherwise stdin being a tty decides; a script positional, if present, is run instead of either. The REPL path hands off to `repl::run_interactive`.
Batch execution
ral/src/batch.rs owns the whole non-interactive pipeline, from the source
text inwards: run_file reads a script path, run_stdin reads a piped script,
and both meet -c at run_source, which is therefore where line endings are
normalised — one door, one rule. Batch holds no Shell: it boots an
IdentityTransport from the batch installer and speaks only the protocol,
through dispatch_to_report under the mute host (), so it shares the REPL’s
one ending law and one typecheck
(engine-protocol).
- Static work.
--dump-astand--dump-irparse and elaborate, printing the tree or the elaborated phrases;--checkiscompile_and_typecheckagainstPRELUDE’s schemes plus the batch surface’s builtin table, rendered and exited throughStaticDiagnostics::render. None boots anything. - Boot, stage one.
IdentityTransport::bootwith thebatchinstaller (the table is notcfg(unix)-gated, onlyrun_engineis), whose recipe decodesRoot.config—{args}— strictly, boots the batch surface, sets exit hints and args, and registers the boot door.platform::local_attachbuilds the attach at this process’s cwd and home, the probed terminal inEngineSpec.terminal, and batch adds its config. A refusal exits 2 with its sentence on stderr. - Boot, stage two. The first dispatch is
Program::HookonSession "boot", the_ral-bootbuiltin itself (ral/src/boot_door.rs), applied to the REPL’s own record,{login: false, no-rc: true, recursion-limit, capabilities}. The door is one-shot — it unregisters its own hook — sources no startup file here, sets the recursion limit, and applies--capabilitiesunder its own mooring. Each profile path reaches it absolute, anchored at the invocation cwd byBoot::profiles, so no profile’scdcan redirect a later one.boot_door::settlereads itsEnding: a load failure isRaisedstatus 2, anexit NisExited(N), and either stops batch there, exactly as it stops the REPL. - The run. The script is a
Program::Sourcedispatch underGrantStack::root()and the mute host, inheriting IO and stdin; the engine typechecks it once against the live session. The terminal isLeasedonly when the probed terminal carriesstartup_foreground(terminal-lease). A watched worker’s lines reach batch’s deferred sink, which prints them to stdout. - The verdict. batch prints the
Report’srenderedas the REPL does and exits with the ending’s status throughplatform::exit_byte. - Audit.
--auditasks the script’s dispatch for its trail (Run.trail) and builds the envelope from theReport: the trail, and an`erroutcome from the failing ending’srecord— the onetryhands its handler.
Embedding and the baked prelude
A run-evaluating host needs three things before rc files or capability
frames: the prelude as a baked `Comp`, its top-level scheme
list, and its own builtin surface as a ral_core::HostSurface. Every mode
reaches for them through ral_core::boot — the Shell-embedding seam — via the
crate-root PRELUDE: ral_core::boot::BakedPrelude static built by the
ral_core::baked_prelude!() macro, and
ral_core::boot::boot_shell(terminal, &PRELUDE, surface), which constructs
the shell, installs the surface next to CORE_BUILTINS, seeds default env
vars, and registers builtins against the prelude comp. BakedPrelude lazily
postcard-decodes the IR blob on first access. Probing the
underlying machine is a separate concern, owned by ral_core::host.
build.rs is the git-hash block — stamping RAL_VERSION_SUFFIX (+<hash>
in a git checkout, empty in a release tarball) into the version string — plus
one call to ral_core::boot::bake_prelude_to_out_dir, which
parses, elaborates, and bake_preludes prelude.ral (annotating each top-level
bind with its inferred scheme in one checked pass), then serialises the
annotated Comp into OUT_DIR. This is the consumer half of core’s schema-less prelude discipline
(core): a crate’s build script cannot depend on the crate it builds,
so core cannot bake its own prelude, and each embedding host bakes it from core’s
source. Evaluating the annotated prelude installs each binding’s scheme into
scope[0], so the per-run seed and the prelude’s own schemes agree by construction
(session-scheme-continuity).
Platform glue
ral/src/platform.rs centralises the host queries and shared exits the
binary needs: probe_terminal (under RAL_INTERACTIVE_MODE),
local_attach (an in-process engine’s Attach, at this process’s own cwd and
home), load_exit_hints (user override in the data dir, else the embedded
data/exit-hints.txt), and exit_byte (the one clamp-and-narrow every mode’s
final code funnels through).
Default-env seeding is core’s: boot_shell seeds the host’s env and cwd
(boot::seed_env, private) between building the shell and seating the
prelude.
Builtins are shell-scoped: each mode declares its surface as one
HostSurface value and hands it to boot_shell, so the checker surface and
the runtime surface cannot drift. Batch’s surface is core plus
WATCH_BUILTIN, SURFACE_BUILTIN and the boot door, and the same value seeds --check’s builtin table
(HostSurface::manifest, the checker’s Σ with no live shell); the REPL’s
adds the `_ed-*` doors and the plugin load doors, all
static. Both ral front-ends print a watched worker’s lines, so watch is the
ral hosts’ to install
(watch-repl-builtin).