Map: core / IO & process
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, and the daemon that fires every scheduled action. 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.
Windows process creation immediately owns both returned handles as
OwnedHandle; failed Job Object assignment or thread resumption releases both.
The child keeps the process handle, and the spawning thread releases the thread handle.
IO — core/src/io/
io.rs holds Io, the per-Shell bundle (stdin / stdout / stderr /
interactive / terminal / stage), where stdout is wherever the running
computation writes — a capture buffer, inside a capture — and
stage: Option<Membership> — None at the top level, the orchestrator;
Some for a stage’s own children, carrying the group an external spawned
anywhere inside that stage must join — at the stage’s own root or nested
arbitrarily deep, thread or process alike — and the stage scope whose causes
the group’s owner delivers, so the member’s own teardown opens only with a
cause that scope does not hold.
It decides pgid placement (a top-level standalone external may lead its own
group so a watchdog cancel can kill(-pgid, …) the whole subtree; anything
inside a stage joins that stage’s pgid) and says whether a child’s reader is
the caller or the next stage — never who may foreground. pipeline/launch.rs’s
stage_stdin resolves a stage thread’s stdin against its route — the upstream
edge, or (ByteIn::Parent) a duplicate of the parent’s own shell.io.stdin
— never taking or moving it, since Source::reader only ever hands out a
duplicate (pipeline execution).
source.rs—Source, a stage’s byte input:Terminal(fall through to fd 0),Empty(no input — immediate EOF, child stdin to/dev/null, no fall-through to fd 0), andReader(SourceReader)— a pipe (an upstream stage, a here-string) or a file (a<fileredirect), carrying the scope a blocked read samples between slices (poll.rs’spoll_slice); a cause in force reads as EOF, never as an interrupted-read error a retry loop would spin on.Source::readernever takes a source: it hands every consumer — a builtin, an external’s stdin, a nested stage — a duplicate, the scope left behind when a child inherits it, so a stage’s stdin outlives every command that reads it.Emptyis also what a tty-owning pipeline’s stage 0 resolves to when it has no upstream reader — a thread in the shell process cannot read a terminal whose foreground belongs to the externals’ group — and 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, a watched worker’s line surface (Watch, each whole line handed to a line function the builtin builds aroundwatch_event, soioknows no deferred sink), a stage thread’s pipe edge (Sink::Pipe { writer, edge, stage },stagenaming the writing stage’s node for its reader-gone stop; the scope a write polls is handed in,write_under).child_stdout/child_stderrcentralise the (stdio, pump) decision so no caller computes inherit-vs-pipe by hand;same_destinationsays whether two sinks deliver to one destination, which decides whether a child’s stderr may share stdout’s descriptor. AByteBufferisArc<CapturedBytes>: the prefix and its total byte count under one mutex, with a retention cap.overflowedcompares that count against the cap (take,take_counted,peek,total,overflowedare its readers).new_bufferkeeps 16 MiB plus a marker; the audit tee retains its caller’s smaller prefix without a marker. The write path cannot report the cap — a pump returns()from its own thread — sooverflowedis read once the writers have joined, by whoever means to make the bytes a value (capture).poll.rs—poll_slice, the one sliced fd wait (Unix): wait for readiness for at mostPOLL_SLICE,EINTRretried against the slice’s own deadline. A blocked pipe write (Sink::write_under) or read (read_interruptible) is a loop of (read the scope; one slice), so another thread ends it by striking a scope, never by waking it. Windows cannot poll an anonymous pipe: the thread waiting on the blocked one aborts its call (interrupt,CancelSynchronousIo), the collector a thread stage’s andPumps::join_undera pump’s, and the aborted read or write (aborted) re-reads its scope and, under a shield, retries; writes go a 4 KiB chunk at a time, so the sentinel’s one read lands one (cancellation).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 not 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, and every write at frame settle
with its target before and after — a stream committed whatever its body did,
an atomic > committed, aborted or failed. 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.WaitOutcome::classifyyields aChildEnd:Failed(CommandFailure), orCancelled(CancelCause)for a death by one of the cause’ssignals_of(grace, key, kill), read off the one readerWaitOutcome::death; no death mints a cause, so a torn-down child reports the cause — an expired time limit, acancel, an interrupt, a shutdown — exactly as a poll point does, while a signal from outside ral still reports its number (cancellation).classifytakescause: Option< CancelCause>, the strongest cause in force when the child ended, joined bymaxwhere two parties each ended it — a cancellation in force outranks the collector’s reader-gone kill (raised only once the sentinel hears a dead write) by that order rather than by a special case. It is the sole input to forgiveness for an external: onlySome(ReaderGone), and only a death that kill actually caused, 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, the-cut-is-at-the-write). -
spawn_lock.rs(target_vendor = "apple") — the process-wideRwLockclosing Apple’s fork/CLOEXECrace: neitherpipe2norSOCK_CLOEXECis atomic there, so aforkracing a pipe’s create-then-fcntlwindow can hand an exec’d child a fd it should never have inherited.cloexec_pipe/cloexec_socketpairtake the shared side;spawn(cmd)is the only fork door, holding the exclusive side aroundCommand::spawnand nothing else — a.output()/.status()caller must spawn through it and wait outside the lock, or a child’s whole lifetime would block every other thread’s fd creation.clippy.toml’sdisallowed-methodsenforces both doors on this target. Every other target is the identity: no lock, no cost. -
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 —TerminalLoan::try_acquire, which is uninvocable without the borrow. The type lives at shell-state; the rationale at terminal-lease. -
deadline.rs— one lazily started, process-global daemon (ral-deadline) 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 replied agent’s follow-up lease needs, since the lease outlives the call that armed it). The fired action isCancel(scope) | Run(closure):Cancelcancels aScopeNodewithCancelCause::TimedOut(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, and an agent worker arms aRunthat cancels its own token at its ceiling. The scheduler stays ignorant of prompts, cron, and sessions; recurrence is not a scheduler concept — a recurring producer re-arms from inside its ownRun, fired outside the heap lock so it cannot deadlock (scheduled-wakeups, concurrency-primitives). -
reaper.rs— the process-wide child watch, the one door onto a wait:ChildHandle::into_watchis how a spawned child reaches it,Reaper:: watchhow a bare pid does. ASIGCHLDhandler on Unix (reaper/unix.rs) whose whole body is a write to a self-pipe, and a thread that per wake asks each watched pid twowaitidquestions — a stop, consumed and answered withSIGCONTon the spot so it never reaches a subscriber and is never re-reported on every wake, and an exit, observed withWNOWAITand left as a zombie untilWatch::reap, since the pid would otherwise be free for reuse while its owner may still signal it. The exit code is masked to a byte, aswaitpidwould report it: Darwin’swaitidreturns all ofexit()’s argument, so macOS bash 3.2’s syntax-error 258 would otherwise surface as 258 instead of 2. Not reaping is the point: holding the zombie is what closes pid reuse structurally, rather than by care. Windows watches the same shape (reaper/windows.rs) viaRegisterWaitForSingleObject, where a stop cannot arise. Every subscriber names its own event type — the pipeline collector’sEvent::Ended, the standalone command’s bareWaitOutcomechannel,spawn_detached’s own — overWaitOutcome, the reaper’s one wire type. -
cancel.rs— the cause-bearing scope tree (Scoperead,ScopeNodewritten,DurableRoot,CancelCause) for structured-concurrency cancellation, polled cooperatively in hot loops (hot-path-cancellation). A scope’s cancellation is a join: one privateresolveover its chain’s flags, naming the outermost node that attains it (ScopeId) and masking at a shielded node (shielded) within the grace from the chain’s first strike;POLL_SLICEis every sliced wait’s period. A signal handler raises an ambient cause instead —REQUESTED_ROOT, an absoluteAtomicU8, or an interrupt on theINTERRUPTScount — which no scope folds:forward_ambienthands each to a host thread, which forwards it to its engine asControl(cancellation). -
signal.rs— signals are causes: the platform handlers translate each delivered signal into aCancelCauseon the ambient causes — SIGINT → foregroundInterrupted, SIGTERM/SIGHUP → rootTerminated— so one cancel-aware wait loop serves user interrupts, timeouts, and termination alike (signals-are-causes). The poll point isShell::poll, scope-only;clearis the boundary acknowledgment; theESCALATIONcounter backs only the third-delivery_exitladder (escalation_pendingis the probe), whose_exitreadsCancelCause::code. A raw-mode frontend’s Esc drives the same non-escalating foreground cancel (esc-non-escalating-interrupt).signals_ofis one function over each platform’sgrace_signal,gesture_signalandKILL;interrupt_handleris the interactive SIGINT disposition — a barerequest_interrupt(), with no delivery of its own, since a pipeline’s processes hear a cancellation through the collector — andquit_handleris the Ctrl-\root abort. Unixinstall_interrupt_handlerinstalls SIGINT withoutSA_RESTARTso the REPL’s blocked input can return on interruption; it snapshots inherited ignored dispositions before changing them.grace_signal(cause)is the one cause→signal table both teardowns read (RunningChild::terminateand the pipeline collector’scancel_all):Interrupt→ SIGINT,Explicit/Deadline/Terminate→ SIGTERM (on Windows all four → Ctrl-Break, named by itsSTATUS_CONTROL_C_EXIT),ReaderGone/RootAbort→None, straight to the kill (one-delivery-path). Platform handlers live insignal/unix.rsandsignal/windows.rs; the Windows side carries the console-control ladder in the Unix shape (tworequest_interrupt()s, thenExitProcess), which signals no process. -
child.rs—ChildHandle, the spawned child, andinto_watch, the one door from it to the reaper. Every Unix child wait goes through the reaper’swaitid, the one funnel (total-wait-status, superseded on the pid side). -
group.rs—Pgid/PgidPolicyand the platformspawn_with_pgidfamily (group/unix.rs) for process-group placement, and the two facts of who signals a group:Membership(a joined pipeline group and its stage scope) andGroup(Owns(Pgid), signalled and killed whole, orJoins(Membership)). A pgid is signalled directly bykill(-pgid, …), never waited on. On Windows (group/windows.rs) a group is a Job Object andbreak_pipeline_groupis the grace an owned group’s teardown sends —RunningChild::terminateforGroup::Owns, the collector’sAddress::signalfor a group — before escalating tokill_pipeline_group; a pid gets the kill alone. -
foreground.rs— unixTerminalLoantakes the&TerminalLeaseand the run’s node (ScopeNode), performs thetcsetpgrphandoff, snapshots and restores tty foreground / termios, blocks SIGTTOU for the parent-only restore window, hears the tenant’s key back (hear,reclaim;gesturereadsGESTURES) and strikes it on the frame on drop — uninhabited on Windows;interrupt_foreground_childre-sends raw-mode Esc/Ctrl-C to a foreground external group. -
limits.rs— what a child spawned under an active grant may consume: no core dump, and a process budget (macOS’s fork brake,limits/fork_brake.rs, a Windows Job Object cap). Resource-limit installation is checked; the macOS cap respects the inherited hard limit, andlimits::failure_hintoffers a conditional process-limit explanation for status 128 when no kernel denial was attributed.Launch::limit_resourcesarms the pre-exec half andChildHandle::limit_processesthe post-spawn one; a brake that cannot be armed is anErr, never a warning.runtime::command::brakecaps the job the child already sits in; when it cannot, the child is killed, any group it leads released, and the launch refused as sandbox-unavailable (two-enforcers). -
tree.rs—sample_descendants, onepssample of the process table, whichwith_denialsreads only for a sandboxed, uncancelled failure. -
launch.rs— the owned launch value and its platform interpreters, and the two births:spawn, which hands back a child this process owns and the pgid of a group it leads —NewLeader,NewSession, or behind an envelope the payload’s own — never one it merely joined or inherited, 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. There is oneGuestJailper guest process, installed once by the first engine to boot (jail::install_guest, gated onRAL_GUEST, whose value is the cgroup root, ral-daemon’smounts::EXEC_CGROUP, which the daemon makes and delegates into at boot; the jail makesengine-<pid>/exec-<seq>below it, ordetached/exec-<seq>for adetachsurvivor, and the daemon removes a dead engine’s tree) and read byjail::guest(); a sealed wire-seat child engine (agents) is its own process and installs its own, 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 child engine’s teardown stays scoped to its own engine’s tree (the daemon kills a dead engine’s commands throughengine-<pid>/cgroup.kill, since they leave its process group) and pid recycling can at worst hand a later engine a dead engine’s stale directory name, which its own teardown tolerates.plantakes the launch’ssandbox::Ownership: aSurrendered(detached) exec goes toral-exec/detached/exec-<seq>instead, out of every engine’s tree, so an engine’s end spares its survivors as it does on the host, and only the daemon’s halt reaches them. 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 process-global (aOnceLockinjail.rs), 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). The one guest listener is the daemon’s engine factory (synod), and a connection it accepts is an unjailed engine that receives whatever itsAttachconfers, so admission is everything. Two independent lines hold, both kernel-enforced. 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. And were a dial to arrive, the daemon accepts only a peer whose CID isVMADDR_CID_HOST, which the guest kernel’s transport writes and no guest process can present. The old token is gone, and loses nothing. A seccomp address-family filter that closes the syscall is unbuilt debt, not a design gap, and its reason is host-side only: the host’s own listeners (the VZ device is synod’s; the Hyper-V socket’s SDDL names the one user) are what a jailedsocket(AF_VSOCK)could still reach. The jail plan above has room for it (one more syscall-time check besideNO_NEW_PRIVS).
Spawning an external command is capability-checked; that in-process guard lives in capabilities, and the command/pipeline dispatch that drives this plumbing in runtime.