Map: core / IO, process & stream
The byte plumbing under the evaluator’s pipelines and external commands: where a stage’s bytes come from and go, the signals and process groups that govern a foreground child, the daemon that fires every scheduled action, and the labels the lazy Stream protocol shares with the type system. Authority over the controlling terminal is carried as a value, not re-derived from process state — the foreground handoff is gated on a held TerminalLease.
IO — core/src/io/
io.rs holds Io, the per-Shell bundle (stdin / stdout / stderr /
interactive / terminal / launch_role / capture_outer), and
LaunchRole — the process-group role distinguishing the top-level
orchestrator (TopLevel) from a pipeline-local child (PipelineStage). It
decides pgid placement (a top-level standalone external may lead its own
group so a watchdog cancel can kill(-pgid, …) the whole subtree; a stage
joins the pipeline’s pgid) and says whether a child’s reader is the caller or
the next stage — never who may foreground. Io::inherit_from / return_to move the read-once stdin
between parent and child shells.
source.rs—Source, a stage’s byte input:Pipe(upstream stage),File(a<fileredirect parked here),Terminal(fall through to fd 0), andEmpty(no input — immediate EOF, child stdin to/dev/null, no fall-through to fd 0).Emptyis what an exarch tool run installs so a tool command can never steal the TUI’s terminal; it is kept distinct fromTerminalprecisely so denial of byte input and denial of foreground stay separate effects.sink.rs—Sink, byte output and child stdio routing (ChildStdioPlan): terminal, stderr, redirect file, in-memoryByteBuffercapture, tee, frontend printer, line-framing adapter.child_stdout/child_stderrcentralise the (stdio, pump) decision so no caller computes inherit-vs-pipe by hand. AByteBufferisArc<CapturedBytes>: the bytes under a mutex, and beside them theoverflowedflagwrite_cappedraises atSINK_BUFFER_CAP. The flag exists because the write path cannot report the cap — a pump returns()from its own thread — sobuffer_overflowedis read once the writers have joined, by whoever means to make the bytes a value (capture).terminal.rs—TerminalState: cached startup isatty / ANSI / NO_COLOR / mode bits.startup_foregroundrecords whether ral’s group owned the controlling terminal’s foreground at entry; it is no longer a per-handoff oracle but the lease’s mint condition (terminal-foreground-ownership). On Windows it also ownsconsole_mode_snapshot/restore_console_mode, the termios-snapshot analogue a panic hook restores a raw-mode console through.
Redirect reads and writes — < file, > file and friends — open through the
File source/sink here, and the runtime emits a byte-level I/O door at each:
the read fires eagerly when stdin is redirected, the write at frame settle with
its committed / aborted / failed outcome. The event shapes and their card
rendering belong to io-surface.
Process — core/src/process/
-
outcome.rs—Signal,WaitOutcome, and the user-facingSpawnFailure/CommandFailurethe evaluator surfaces. A death ral itself caused is its own variant (Cancelled, carrying theCancelCauseand the signal we sent), so a torn-down child reports the cause — an expired time limit, acancel, an interrupt, a shutdown — while a signal from outside ral still reports its number. Same status either way, and it carries the one forgiveness: a non-final stage the collector itself killed (StageKill, sent once that stage’s reader is reaped) keeps no failure; every other status is kept, because the kill precedes the wait and cannot rewrite a recorded status (a-stage-ral-stopped-has-no-failure). -
lease.rs—TerminalLease, the unforgeable authority to hand the controlling terminal to a child viatcsetpgrp. No public constructor, neitherClonenorCopy: a host cannot forge or duplicate it. Minted at most once at session construction iff ral owned the foreground at startup (Noneon a backgrounded or tty-less launch, and always on platforms with notcsetpgrp), then lent per run as&TerminalLeaseto the one chokepoint that foregrounds —ForegroundGuard::try_acquire, which is uninvocable without the borrow. The type lives at shell-state; the rationale at terminal-lease. -
reaper.rs— one lazily started, process-global daemon (ral-reaper) owning a min-ordered heap of(when, action)entries, firing each at itsInstant. Deadlines are data, not a thread per worker:arm_lifetime/arm_callbackpush an entry and return a#[must_use]Deadlineguard — dropped, the entry disarms;keep-consumed, it fires regardless (the fire-and-forget mode a detached worker’s lease needs, since the worker outlives thespawnthat armed it). The fired action isCancel(scope) | Run(closure):Cancelcancels aCancelScopewithCancelCause::Deadline(the foreground wall);Runinvokes an opaque host closure once, the shape a scheduled wakeup rides — exarch arms aRunthat posts a prompt and wakes its idle loop, a detached agent worker arms aRunthat cancels its own token at its ceiling, and a detachedspawnworker’s idle-observation lease chain (builtins) is akeep-edRunthat re-arms itself until it reaps or the worker settles. The reaper stays ignorant of prompts, cron, and sessions; recurrence is not a reaper concept — a recurring producer re-arms from inside its ownRun, fired outside the heap lock so it cannot deadlock (scheduled-wakeups, concurrency-primitives). -
cancel.rs— the cause-bearingCancelScopetree (DurableRoot/ForegroundScope,CancelCause) for structured-concurrency cancellation, polled cooperatively in hot loops (hot-path-cancellation). A scope’s cancellation is a join: one privatefoldover its chain’s flags and the ambient causes the nodes were minted folding (Hears), so a signal handler or TUI thread contributes a cause without holding — or aliasing — a scope (cancel-is-a-join). The two ambient causes have different shapes:REQUESTED_ROOTis an absoluteAtomicU8, while the interrupt is a per-cause watermark of instants (CLOCK,STAMPED) a frame reads against its birth (cancel-is-a-watermark). -
signal.rs— signals are causes: the platform handlers translate each delivered signal into aCancelCauseon the ambient causes — SIGINT → foregroundInterrupt, SIGTERM/SIGHUP → rootTerminate— so one cancel-aware wait loop serves user interrupts, timeouts, and termination alike (signals-are-causes).checkis scope-only;clearis the boundary acknowledgment; theESCALATIONcounter backs only the third-delivery_exitladder (escalation_pendingis the probe). A raw-mode frontend’s Esc drives the same non-escalating foreground cancel (esc-non-escalating-interrupt). AlsoPgid/PgidPolicy/ChildHandleand the platformspawn_with_pgidfamily for process-group placement. UnixForegroundGuardtakes the&TerminalLease, performs thetcsetpgrphandoff, snapshots and restores tty foreground / termios, and blocks SIGTTOU for the parent-only restore window; unixinterrupt_foreground_childre-sends raw-mode Esc/Ctrl-C to a foreground external group,relay_handlerfans SIGINT to active external pgids, andquit_handleris the Ctrl-\root abort. Platform handlers live insignal/unix.rsandsignal/windows.rs. Unix child and process-group waits pass through typed blocking / polling funnels for pids and pgids: rustix owns pid and total status types, while the funnel shape owns optionality and makesEINTRinvisible without conflatingNOHANGwithECHILD(total-wait-status). The Windows side carries the console-control escalation ladder (CTRL_BREAK_EVENTfan-out, thenTerminateJobObject, then exit),relay_interrupt—relay_handler’s non-escalating twin, whose fan-out skips a detached worker’s group — andbreak_pipeline_group, the SIGTERM-grade cooperative break a job teardown sends before escalating tokill_pipeline_group. -
launch.rs— the owned launch value and its platform interpreters, and the two births:spawn, which hands back a child this process owns, and thecfg(unix)spawn_detached, a double-fork whose grandchild is reparented to init — its pid comes back but nothing else does, so there is no handle, no wait, and (below) noJailCgroup. Unix lowers tostd::process::Commandand keeps thepre_execpgid/fd discipline; Windows owns the rawCreateProcessWboundary, including command-line/env rendering, explicit helper-handle allow lists, theSECURITY_CAPABILITIESattribute a confined spawn attaches (its projection’s AppContainer SID and the per-path fs and network capability SIDs — capabilities), the launch mutex, suspended create → Job Object assignment → resume, and the widenedChildHandleraw process wrapper (windows-spawn-boundary). The whole stop-work flow — theInterrupt < Explicit < Deadline < Terminate < RootAbortorder — is narrated in cancellation. -
jail.rs— the guest spawn jail’s decision layer:GuestJail::planmints each exec a fresh unprivileged uid/gid and a fresh transient cgroup underJailLimits(memory / pids / CPU), with no syscall in the plan. EachGuestJailis one per booted engine — a hatched wire-seat child (agents) installs its own, beside the trunk’s — so the sequence number behind both the uid and the cgroup name is minted guest-globally, not per-engine:linux::next_guest_seqlocks a counter file (/run/ral/jail.seq,O_CREAT, read-increment-write under an exclusiveflock, released when the file drops) so every booted engine’s jail mints off the one number line and two engines’ first spawned commands cannot collide on a uid or a cgroup name. The cgroup path carries the engine’s own pid as a grouping label, not a uniqueness guarantee (the sequence file already gives that):ral-exec/engine-<pid>/ exec-<seq>, so a hatched child’s teardown stays scoped to its own engine’s tree and pid recycling can at worst hand a later engine a dead engine’s stale directory name, which its own teardown tolerates. Off a real Linux guest, whereGuestJailis never actually installed,planfalls back to a plain in-process counter so the module’s own tests stay portable —linux.rsremains the only place a decision reaches the kernel.jail/linux.rsis the thin platform edge that realises aJailPlan— the cgroup tree and limit writes, the pre-exec supplementary-group clear /setresgid/setresuid/NO_NEW_PRIVS, kill-whole viacgroup.kill, andEBUSY-polled removal. The tree is built by descent, and the reason is a cgroup2 rule: a controller’s files appear in a child only once the parent’scgroup.subtree_controlenables it, somemory.maxat the leaf needs an unbroken chain of enabling parents up to the cgroup2 mount itself.make_delegatedtherefore climbs to the first ancestor that already exists — the mount root at the latest, which is exempt from the no-internal-process rule and so may delegate while the daemon and engine still live in it — and enables on the way back down, one level ahead of each mkdir. A parent that will not delegate is refused outright rather than tolerated: a jail whose limits cannot be written must not run the command uncapped instead.JailCgroupis plain dataRunningChildcarries uniformly (Noneoff a real guest); a detached birth is handed none at all, since a caller that cannot name the process cannot know when its cgroup is empty — the survivor keeps the transient cgroup’s limits and leaves one inert directory until the guest reboots. The jail itself is session state (session.guest_jail), inherited exactly like the builtin table so workers, pipeline stages, and forks share one counter; only a guest engine (RAL_GUEST) installs it, and there it replaces the per-command OS projection (runtime,docs/SPEC.md§12.11).The recorded gap. The jail is uid + cgroup +
NO_NEW_PRIVS— there is no seccomp filter, so nothing stops a jailed process from callingsocket(AF_VSOCK). What that would buy it is a race with the host for the ephemeral guest port a parent engine binds while spawning a child (transport, synod), and so a child engine seeded with the parent’s scope — impersonation, not escalation, since grant narrowing is a plain meet and a run’s capabilities are enforced inside the guest engine regardless of who computes them. Two things stand in the way, and only the first is confinement. A jailed process cannot dial a guest-local vsock port at all — measured 2026-08-24: the guest kernel refusesVMADDR_CID_LOCALwithECONNRESET, and/dev/vsock, the only way to read one’s own CID, isEACCESunder the jail. That refusal is the standing defence. Behind it stand the eight token bytes the host must write before the listener will hatch anything: minted from the OS entropy source, held by the thread that owns the listener, dead when that one spawn ends. The token is the second line, against a jailed process that guesses a CID rather than reading one. A seccomp address-family filter that closes this at the syscall is unbuilt debt, not a design gap: the jail plan above has room for it (one more syscall-time check besideNO_NEW_PRIVS).
Spawning an external command is capability-gated; that gate lives in capabilities, and the command/pipeline dispatch that drives this plumbing in runtime.
Stream — core/src/stream.rs
Shared label vocabulary for the lazy Stream protocol: runtime variant labels
more / done and the head / tail payload fields, with the type-row
spellings (`more / `done) kept beside them so runtime and
typechecker recognition cannot drift. docs/SPEC.md §14.5
covers Stream semantics.