Map: core / runtime

core/src/runtime/ is the OS plumbing the CBPV machine dispatches into — command execution, pipeline orchestration, and the per-child confinement choice. It re-enters evaluation only through the pipeline::StageEval the machine passes to PipeNode::launch (a stage’s (comp, env), run from pipeline/thread.rs) — stages carry computation closures, so the mutual recursion is irreducible, though runtime names no evaluator item; the evaluator reaches it at PipeNode::launch/join and, dispatching an Exec node, at command_call::classify_command → run_base_frame / run_external, and the redirect scope (redirect::scope), which the machine’s Frame::Redirect enters (evaluator-runtime-split).

  • command_call.rs — classify_command, the single site that resolves a head (env → handlers → external) and admits it, and the runners (run_base_frame, run_external) the machine’s Exec rule then calls for the chosen arm; the evaluator’s down-seam for a bare command. There is no builtin arm: a native table entry is an Env hit on a native value, and a base-frame row is a Base hit on the handler stack’s base layer, run by run_base_frame with the argv slice (a-name-is-a-value-or-it-is-handled, argv-is-a-list-of-strings). ^name and a path-bearing head are the external directly, consulting neither the env nor the handler stack. Grant admission is an external-command property: only the External arm consults capability::admits_head before any argument evaluates, refusing the head outright; the env, base, and handler arms pass through. Handler and alias thunks are lambdas — a unary { |args| … } or a catch-all { |name args| … } — with the calling convention fixed by surface position, not inferred from a value’s runtime shape (handlers-and-aliases-are-lambdas).
  • redirect.rs — the doors that open a redirect target (open_write, open_read, install_stdin_redirect, and Shell::atomic_write, which every host write below the redirect frame shares), over redirect/stage.rs’s pure PendingWrite and Located::stage/preview: staging a write is the path’s, judging and committing it the shell’s. redirect/scope.rs is the RedirectState lifecycle (open, route fd 1/2 through the shell’s sinks, restore, settle each write) and with_redirects.
  • capture.rs — with_capture and the audit tee, both over one SinkScope that restores the replaced sinks on Drop. Audit retains 1 MiB per stream as a prefix and a byte total, independently of ordinary capture.
  • command_call.rs also holds call_external, an external’s door: stamp the start, tee its streams, name a failure, settle the one Observed::Command.
  • command.rs — the External arm: vet the resolved head, launch its admitted program, wire stdio from the sinks the call’s redirects installed, spawn, and reap. stdio.rs wire_stdio hands a > file to the child as its own fd, no pump, and keeps 2>&1 a dup2 when stderr’s sink is same_destination as stdout’s. Submodules: head.rs (Head { shown, program }, resolved once: a bundled Program::Tool, a host Program::File with its launch path and real path, or a Missing), vet.rs (existence → argv shape → grant policy, yielding a SpawnPlan that holds the guard’s Admitted), process.rs, child.rs, stdio.rs, foreground.rs, detach.rs, plus uutils.rs for bundled coreutils. foreground.rs decides once: ForegroundDecision(PgidPolicy), NewLeader being exactly the foreground.
    • The program and the 126/127 verdict are two projections of one PATH walk. Head::resolve calls path::search once, anchored at Context::cwd, the shell’s one cwd cell; a miss is Missing::NotFound (keeping the file a path looked for, so the refusal can offer the extension it lacks), a hit lacking +x Missing::NotExecutable, and vet reads that rather than probing the disk, so walk and verdict cannot disagree and a missing command never reaches the in-process guard (one-walk-one-anchor). A path head’s native target — as written, or a ~ head expanded by TildePath::expand, never re-read for a sigil — is anchored at the launch cwd with only . folded, so the guard judges the file the kernel will reach and the launcher runs that very file, by its real path, under the shown spelling as argv[0] (exec-rules).
    • The argv-shape step is one refused set read at two moments. vet::reject_exec_arg maps each argument’s head (Value::heads) through RefusedArg::of_head (core/src/ty/exec_arg.rs) and carries the shape’s own remedy; the checker maps the argument’s type through RefusedArg::of_ty before the run, so this is the backstop for what a type variable hid from it (exec-argv-is-words, exec-boundary-gated-statically).
    • detach.rs is that same machinery — Head, vet, build_launch — up to the one act that differs: the child is born by Launch::spawn_detached (io-process), so its pgid is never observed here and nothing can signal, await, or reap it. What replaces the handle is a first-order {pid, desc} receipt. The birthing frame’s projection is rendered into the launch exactly as for a child we keep, only with sandbox::Ownership::Surrendered dropping the parent-death tie, so the survivor’s authority is frozen as that frame left it and no later frame can widen it — it cannot name the process at all (capabilities). All three of its standard descriptors are /dev/null. detach refuses a head a handler intercepts (an arm, the catch-all, or a base frame): a handler runs inside this session, so nothing could be detached; its scheme is [String] → F [pid: Int, desc: String].
    • A bundled coreutils/diffutils/ripgrep head is a Program::Tool, always run as a ral --ral-bundled-tool <tool> child — its inherited stdio, env, cwd, process group, and sandbox are the execution context, so it threads the same spawn/RunningChild/audit machinery as a host external, and behaves identically on Windows where no .exe exists to spawn (bundled-tools-as-exec-images, bundled-tools-always-reexec).
    • foreground.rs’s ForegroundDecision::for_standalone gates the terminal handoff on a held terminal lease (Shell::terminal_lease is Some) plus top-level launch role and a terminal-bound sink with no shell-side pump, not on REPL interactivity — so a non-interactive script launched at a terminal holds a Leased run and foregrounds its interactive children, while an exarch Denied tool run cannot construct the handoff at all (terminal-foreground-ownership). There is no parking: a stopped standalone child is answered with SIGCONT inline, by whoever waits on it, the same rule everywhere (ral-does-not-suspend).
    • Redirects install on the handler arm (redirect-drop-on-handler-dispatch). One interpreter, runtime::redirect::scope::RedirectState, installs them for an external and a block alike (redirects-are-bindings). Child stdio routing — the (Stdio, pump) plan for a spawned child’s stdout/stderr — is one shape, Sink::child_stdout / child_stderr yielding the shared ChildStdioPlan (io-process), through which the standalone command, a direct pipeline stage, and an external a stage thread spawns internally all route; the tty-inherit predicate (stdio::inherit_tty) is likewise shared.
    • The read door (< file), the write door (> / >> / >|, settled committed/aborted/failed at frame teardown), and the exec door (Host and BundledTool completion) each build an Observation (core/src/types/audit/observation.rs) and pass it to Shell::observe (core/src/types/audit/door.rs), the one fan-out door: it reports to the run’s Mooring and to the open audit trail alike, judging neither’s interest — the host filters the rail. It judges only whether anything happened: a write onto the discard device is dropped there (audit). Core emits plain Value::Maps through Value::from_datum; a host (exarch) decodes their first-order wire form back with Observation::decode. The observation shapes and their card rendering live in io-surface (surface-reads-writes-execs).
  • pipeline/ — pipeline planning and execution. A multi-stage CompKind::Pipeline steps through a PipeNode: PipeNode::launch resolves the plan and spawns every stage into one process group, and join (drive then fold), called in the same rule, is what the arm meets; no frame is pushed, so an unwinding panic tears the group down through PipelineGroup’s Drop rather than through any undo of the machine’s. group (the pgid anchor and the foreground guard) stays alive across both drive and fold rather than dropped early (evaluator). PipeNode’s field order is its teardown order: the collector drops before the group, so its kill still reaches a live pgid before the anchor is waited on. resolve.rs freezes each stage’s launch decision once as StageLaunch (Direct { id, args } | Thread) from the head’s resolution, its redirects, and whether a !{…} audit captures bytes, so launch reads a decision rather than re-deriving a dispatch gate. A stage with redirects is a Thread stage run through that same RedirectState; a Direct stage has none, and wire_stdio wires its stage pipes without interpreting a redirect list. Nothing about a stage’s value enters that decision: a stage’s dispatch may not depend on what its neighbours return, or the choice would stop being observationally transparent. The pipeline’s value is its final stage’s, read once in the fold. route.rs’s open_stage_routes allocates every interior edge as an operating-system byte pipe from stage position alone — the zip of Parent : ins against outs ++ [Parent] — and no stage is told which one it is: every stage reports what it settled, and the fold, running in launch order, keeps the last Ok, which is the final stage’s whenever that stage is Ok (pipes-are-positional-byte-wires). A non-final stage’s returned value is discarded, never serialised onto an edge; a Thread stage’s final value rides home on its own StageObservation, no wire in between. A Direct stage is a process in the group; a Thread stage is an OS thread whose Shell starts from the parent’s session, and only an external it spawns — at any nesting depth — is a process. Ordinary application and bind do not enter this runtime.
    • A bundled (uutils) head routes Direct like any external, its ral --ral-bundled-tool child the image chosen by command::build_launch — nothing in the pipeline distinguishes a bundled head from a host binary, so both classify as External carrying the resolved Head. Value-style composition is evaluator application, never a stage-transport concern. The terminal-ownership decision (resolve_terminal_plan) likewise gates on a reachable terminal lease and a terminal-bound final sink, not on a startup_foreground predicate.
    • pipeline.rs is the spine, and PipeNode::launch is the launch loop: resolve, group, collector, node, then one spawn_stage per stage pushed onto the collector. The routes are a local declared after the node, so an error part-way through closes whatever the loop never consumed before the node tears down and a half-wired neighbour sees EOF. launch.rs wires one stage’s byte endpoints (stage_stdin, then the shared wire_stdio) and dispatches it on its frozen StageLaunch (spawn_stage, launch_external_stage_direct); stage.rs holds StageHandle, which dispatches cancel/cut/watch/arm over its two kinds, External/Thread — cancel records the cause on its own sent, joined by max, and tells a thread’s scope (a process hears a cancellation only as the signal the collector sends next); cut is that same cancel with ReaderGone plus the external’s kill, Effect::KillStage’s action once the sentinel (sentinel.rs) has heard a dead write, never reused for another kill; and watch hands the collector a live external’s wait handle for its own per-pid teardown — an External is a crate::process::Watch alone, the reaper’s own subscription: no dedicated waiter thread, since ChildHandle::into_watch’s closure posts the exit straight onto the collector’s channel as Event::Ended(ix, _), and a stop never reaches the collector at all — the reaper answers it with SIGCONT before any subscriber sees one (ral-does-not-suspend). StageEnd carries a Thread’s already-built StageObservation or an External’s raw outcome, unsettled pumps, jail, and the sent its handle recorded; StageEnd::settle is what turns either into one StageObservation, in the fold and nowhere else. The collector publishes a direct external’s observation; a thread’s already-published observations only merge into the trail. On an external’s own Event::Ended, StageHandle::file_external_end reaps the watch — after the stage has left stages, so no later KillStage can ever name a reaped pid — and releases the held-open edge, leaving the jail and the pumps, which both wait on the group kill, to the fold; group.rs (PipelineGroup::prepare spawns the pgid anchor — --ral-pipeline-anchor, ignoring SIGTSTP/SIGTTIN/SIGTTOU outright and immune to termination signals — on every platform before any stage exists. On Unix it waits for a zero readiness byte after the signal handlers are installed, before lending the terminal or admitting stages. It starts its witness in the same act: a report reader blocking to EOF on the anchor’s stdout, posting Event::Heard(signal) raw per swallowed signal, and the anchor’s own ChildHandle handed to the reaper as a Watch whose posting closure maps its every exit to Event::AnchorDied. Ownership is the anchor’s presence, not a flag: group() is Group::Owns(leader) exactly when the anchor is, and only that pgid may be signalled or killed; a joining group answers Group::Joins(membership) and lends no terminal on its own account, lend answering a TerminalLoan for an owning group only; PipelineGroup::joining is that no-anchor, no-pgid shape a nested pipeline inside a stage thread gets instead, and PipelineGroup::membership what a thread stage’s Io carries: an owner’s leader paired with the stage’s scope, or a joiner’s own. The anchor’s own rare stop is answered by the reaper like any other watched pid’s, with no thread of the anchor’s own involved. AnchorProcess::finish joins the report reader and drops the watch, whose Drop reaps. The group’s verbs are lifecycle only — prepare/joining, group, membership, leader_pgid, lend, end_anchor (which PipeNode::join calls between drive and fold, so every report is on the channel), Drop: signalling and killing belong to the collector, which is what holds the live stages. CollectState::cancel_all(cause) spells cancel: escalate writes the cause on every live stage in one sweep, then one grace signal per Address the collector enumerates — the pipeline’s group or pids, unless the tty already delivered it (pressed) or, for a joining collector, unless the cause is one its membership does not owe, and every confined stage’s envelope group regardless — a sliced TEARDOWN_GRACE drain that escalates as it files, a wait for live thread stages bounded by the cleanup grace (skipped under Aborted), the kill (kill_live, the same addresses), a further drain, while CollectState::drop writes Terminated on every live thread stage and kills whenever it is dropped with a stage still unobserved); thread.rs (launch_thread_stage wires a Thread stage’s Io from its StageRoute, and its closure — given its Slot { ix, tx }, wrapped in a SettleOnDrop — builds its own StageObservation and sends Event::Returned(ix, obs) as its last act, the guard armed until then so an unwind reports in its place. A non-final stage’s stdout is its own edge; a final stage’s is the parent’s, so a discarded statement in the final stage writes where an enclosing capture’s buffer (shell.io.stdout) is (capture). A captured final stage is a Capture node, so it runs as a thread stage: its stdin the pipe, its buffer taking what the command writes. ThreadStage is the collector’s handle onto the running thread and the stage’s node: cancel writes the cause on that node — on Windows also one CancelSynchronousIo, re-issued each pass of the collector’s loop while the stage’s chain carries a cause — while join_after_settled reclaims a thread whose Returned has already arrived and Drop writes Terminated and joins one never observed a slice at a time; a stage thread’s own interior wait, for whatever child it spawned, answers a process stop inline exactly as RunningChild::wait always does, structurally invisible to the collector); collect.rs (CollectState owns the stage handles from the first one launched, an mpsc channel every producer feeds — the reaper (through a stage’s own Watch), a stage thread’s own report, the sentinel, the anchor’s witness and its watch — received a slice at a time by recv, which reads the current scope between slices and mints Event::Cancelled itself — carrying Event::{Ended, Returned, Wrote, Cancelled, AnchorDied, Heard}, with no Stopped/Continued variant at all, which the pure fold step folds over, returning at most one Effect (Option<Effect>) for a thin interpreter (CollectState::run) to perform: ArmEdge, KillStage, CancelAll { cause }. Completion is not among them — the pipeline is finished exactly when no stage handle is left (live()). A Heard key strikes the run’s node and records pressed — the kernel gave the whole pgid that signal already, so teardown sends no second copy of it — but only through the collector’s loan: Option<TerminalLoan>, the one ear for a key: with no loan, or for a signal no terminal sends, it is inert; the teardown follows from the current scope read at the next slice. fold opens by hearing out every queued report, settles each external under sent joined with the cause on the current scope, and the loan drops with the collector, returning the terminal and striking nothing; holds_terminal reports the loan actually made, which LaunchCx.holds_terminal freezes. The Slot a stage hands its own producers is minted by PipeNode::launch, which holds the sending half as a local and drops it on return — the collector keeps none. Attribution rides the handle: StageHandle::sent is the strongest cause this collector ever sent that stage, joined by max and carried onto its StageEnd, and StageEnd::settle, called by CollectState::fold — the one place &mut Shell reaches an external’s settlement (audit synthesis, exit-hint lookup, sandbox-denial augmentation, via finish_external_settlement) — is the one place that reads it back against what actually happened: a thread’s forgiveness reads its own break alone (a ReaderGone break is only ever this collector’s doing), an external’s is the collector’s reading of sent: a death by its own kill with sent == Some(ReaderGone). The verdicts join by stronger/rank, an escape outranking an error and ties going to the earlier stage; drive/cancel_all/step take no &Shell at all. drive is while live() { recv; step; run }, no interval and no backoff, returning when every stage is observed; PipeNode::join is drive(), then fold(mooring, shell), the group dropped after). anchor.rs is the hidden --ral-pipeline-anchor child’s entrypoint (Role::Anchor; a bundled tool’s Role::BundledTool is launched beside it by sandbox::reexec::launch), a ral-written stage running on a thread of the parent process instead. On Windows every external a stage thread spawns still resolves PgidPolicy::Join against the anchor’s registered Job Object, assigned at creation under the suspended create → assign → resume path (windows-spawn-boundary).
  • A grant is a dynamic effect scope, not a process boundary, so the grant body always evaluates locally — the machine steps its body in process, with no router in between: nested grants compose by intersection over authority, an algebra of the evaluator’s dynamic context. Confinement happens elsewhere — the RAL-owned effects are decided in process by capability::check_* (capabilities), and child-owned effects are kernel-backed at external dispatch: when a projection is active, command::build_launch obtains a confined process::Launch via projection_enforceable / sandboxed_command — per-command Seatbelt / bwrap confinement on Unix; on Windows the LowBox token of the projection’s own AppContainer profile, carrying the per-path fs capability SIDs its projection names (path-derived-capability-sids) — and net/fs fail-closed fires when a child is actually spawned, not at grant-body entry (sandbox-external-children). Inside a guest, build_launch takes neither projection branch: jail::guest() marks every spawn as already confined by the spawn jail — a fresh unprivileged uid and a per-exec cgroup, process/jail.rs (io-process) — since bwrap needs the user namespaces the guest boot disables; the in-process guards apply unchanged, and child.rs tracks the per-exec JailCgroup, so cancel and settle kill the whole tree through cgroup.kill (a setsid’d grandchild cannot leave its cgroup) while the grace phase stays pgid-addressed (docs/SPEC.md §12.11). core/src/seed.rs (crate root, beside the wire layer it rides, not under runtime/) carries only EngineSeed and seal/unseal, the engine seat’s own seed wire for a sealed child engine; a pipeline stage never crosses a wire at all and this module carries no pipeline-stage type (stages-are-threads, child-eval-unification, superseded).

The Shell state these thread is shell-state; the serde mirror and wire envelope they ride is transport.