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 by serve_warrant alone, before any pin, so the child pins and opens nothing before it is confined. ral --warrant is the whole argv of a confined re-exec: the child takes its warrant off fd 99, enters the OS sandbox, and only then execves 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::boot first, which pins the binary (and, on Linux, bwrap) and, on Windows, sweeps what a crashed prior session left registered; the bundled tool only sandbox::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. ral passes startup::engine::INSTALLERS, so a wire-engine child boots through the very recipes the in-process front-ends boot through. The table holds two EngineInstallers: repl (boot_repl over the REPL surface, configured by nothing) and batch (boot_batch over the batch surface, decoding BatchConfig {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, and identify answers Shell(Mode).

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_terminator splices 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 the Cli definition.
  • Frontend selection. --surface <minimal|readline> (a clap ValueEnum, default readline) picks the interactive frontend, overriding the rc surface: key; None defers to rc, then the default. The capability axis stays the RAL_INTERACTIVE_MODE env 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_script encodes the precedence: -s forces stdin-as-script and beats -i, -i forces 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-ast and --dump-ir parse and elaborate, printing the tree or the elaborated phrases; --check is compile_and_typecheck against PRELUDE’s schemes plus the batch surface’s builtin table, rendered and exited through StaticDiagnostics::render. None boots anything.
  • Boot, stage one. IdentityTransport::boot with the batch installer (the table is not cfg(unix)-gated, only run_engine is), whose recipe decodes Root.config — {args} — strictly, boots the batch surface, sets exit hints and args, and registers the boot door. platform::local_attach builds the attach at this process’s cwd and home, the probed terminal in EngineSpec.terminal, and batch adds its config. A refusal exits 2 with its sentence on stderr.
  • Boot, stage two. The first dispatch is Program::Hook on Session "boot", the _ral-boot builtin 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 --capabilities under its own mooring. Each profile path reaches it absolute, anchored at the invocation cwd by Boot::profiles, so no profile’s cd can redirect a later one. boot_door::settle reads its Ending: a load failure is Raised status 2, an exit N is Exited(N), and either stops batch there, exactly as it stops the REPL.
  • The run. The script is a Program::Source dispatch under GrantStack::root() and the mute host, inheriting IO and stdin; the engine typechecks it once against the live session. The terminal is Leased only when the probed terminal carries startup_foreground (terminal-lease). A watched worker’s lines reach batch’s deferred sink, which prints them to stdout.
  • The verdict. batch prints the Report’s rendered as the REPL does and exits with the ending’s status through platform::exit_byte.
  • Audit. --audit asks the script’s dispatch for its trail (Run.trail) and builds the envelope from the Report: the trail, and an `err outcome from the failing ending’s record — the one try hands 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).