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’sExecrule 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 anEnvhit on a native value, and a base-frame row is aBasehit on the handler stack’s base layer, run byrun_base_framewith the argv slice (a-name-is-a-value-or-it-is-handled, argv-is-a-list-of-strings).^nameand a path-bearing head are the external directly, consulting neither the env nor the handler stack. Grant admission is an external-command property: only theExternalarm consultscapability::admits_headbefore 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, andShell::atomic_write, which every host write below the redirect frame shares), overredirect/stage.rs’s purePendingWriteandLocated::stage/preview: staging a write is the path’s, judging and committing it the shell’s.redirect/scope.rsis theRedirectStatelifecycle (open, route fd 1/2 through the shell’s sinks, restore, settle each write) andwith_redirects.capture.rs—with_captureand the audit tee, both over oneSinkScopethat restores the replaced sinks onDrop. Audit retains 1 MiB per stream as a prefix and a byte total, independently of ordinary capture.command_call.rsalso holdscall_external, an external’s door: stamp the start, tee its streams, name a failure, settle the oneObserved::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.rswire_stdiohands a>file to the child as its own fd, no pump, and keeps2>&1adup2when stderr’s sink issame_destinationas stdout’s. Submodules:head.rs(Head { shown, program }, resolved once: a bundledProgram::Tool, a hostProgram::Filewith its launch path and real path, or aMissing),vet.rs(existence → argv shape → grant policy, yielding aSpawnPlanthat holds the guard’sAdmitted),process.rs,child.rs,stdio.rs,foreground.rs,detach.rs, plusuutils.rsfor bundled coreutils.foreground.rsdecides once:ForegroundDecision(PgidPolicy),NewLeaderbeing exactly the foreground.- The program and the 126/127 verdict are two projections of one
PATHwalk.Head::resolvecallspath::searchonce, anchored atContext::cwd, the shell’s one cwd cell; a miss isMissing::NotFound(keeping the file a path looked for, so the refusal can offer the extension it lacks), a hit lacking+xMissing::NotExecutable, andvetreads 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 byTildePath::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 asargv[0](exec-rules). - The argv-shape step is one refused set read at two moments.
vet::reject_exec_argmaps each argument’s head (Value::heads) throughRefusedArg::of_head(core/src/ty/exec_arg.rs) and carries the shape’s ownremedy; the checker maps the argument’s type throughRefusedArg::of_tybefore the run, so this is the backstop for what a type variable hid from it (exec-argv-is-words, exec-boundary-gated-statically). detach.rsis that same machinery —Head,vet,build_launch— up to the one act that differs: the child is born byLaunch::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 withsandbox::Ownership::Surrendereddropping 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.detachrefuses 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 aral --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.exeexists to spawn (bundled-tools-as-exec-images, bundled-tools-always-reexec). foreground.rs’sForegroundDecision::for_standalonegates the terminal handoff on a held terminal lease (Shell::terminal_leaseisSome) 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 aLeasedrun and foregrounds its interactive children, while an exarchDeniedtool run cannot construct the handoff at all (terminal-foreground-ownership). There is no parking: a stopped standalone child is answered withSIGCONTinline, 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_stderryielding the sharedChildStdioPlan(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 (> / >> / >|, settledcommitted/aborted/failedat frame teardown), and the exec door (Host andBundledToolcompletion) each build anObservation(core/src/types/audit/observation.rs) and pass it toShell::observe(core/src/types/audit/door.rs), the one fan-out door: it reports to the run’sMooringand 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 plainValue::Maps throughValue::from_datum; a host (exarch) decodes their first-order wire form back withObservation::decode. The observation shapes and their card rendering live in io-surface (surface-reads-writes-execs).
- The program and the 126/127 verdict are two projections of one
pipeline/— pipeline planning and execution. A multi-stageCompKind::Pipelinesteps through aPipeNode:PipeNode::launchresolves the plan and spawns every stage into one process group, andjoin(drivethenfold), called in the same rule, is what the arm meets; no frame is pushed, so an unwinding panic tears the group down throughPipelineGroup’sDroprather than through any undo of the machine’s.group(the pgid anchor and the foreground guard) stays alive across bothdriveandfoldrather 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.rsfreezes each stage’s launch decision once asStageLaunch(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 aThreadstage run through that sameRedirectState; aDirectstage has none, andwire_stdiowires 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’sopen_stage_routesallocates every interior edge as an operating-system byte pipe from stage position alone — the zip ofParent : insagainstouts ++ [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 lastOk, which is the final stage’s whenever that stage isOk(pipes-are-positional-byte-wires). A non-final stage’s returned value is discarded, never serialised onto an edge; aThreadstage’s final value rides home on its ownStageObservation, no wire in between. ADirectstage is a process in the group; aThreadstage is an OS thread whoseShellstarts 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
Directlike any external, itsral --ral-bundled-toolchild the image chosen bycommand::build_launch— nothing in the pipeline distinguishes a bundled head from a host binary, so both classify asExternalcarrying the resolvedHead. 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 astartup_foregroundpredicate. pipeline.rsis the spine, andPipeNode::launchis the launch loop: resolve, group, collector, node, then onespawn_stageper 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.rswires one stage’s byte endpoints (stage_stdin, then the sharedwire_stdio) and dispatches it on its frozenStageLaunch(spawn_stage,launch_external_stage_direct);stage.rsholdsStageHandle, which dispatchescancel/cut/watch/armover its two kinds,External/Thread—cancelrecords the cause on its ownsent, joined bymax, and tells a thread’s scope (a process hears a cancellation only as the signal the collector sends next);cutis that same cancel withReaderGoneplus the external’s kill,Effect::KillStage’s action once the sentinel (sentinel.rs) has heard a dead write, never reused for another kill; andwatchhands the collector a live external’s wait handle for its own per-pid teardown — anExternalis acrate::process::Watchalone, the reaper’s own subscription: no dedicated waiter thread, sinceChildHandle::into_watch’s closure posts the exit straight onto the collector’s channel asEvent::Ended(ix, _), and a stop never reaches the collector at all — the reaper answers it withSIGCONTbefore any subscriber sees one (ral-does-not-suspend).StageEndcarries aThread’s already-builtStageObservationor anExternal’s raw outcome, unsettled pumps, jail, and thesentits handle recorded;StageEnd::settleis what turns either into oneStageObservation, 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 ownEvent::Ended,StageHandle::file_external_endreaps the watch — after the stage has leftstages, so no laterKillStagecan 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::preparespawns the pgid anchor —--ral-pipeline-anchor, ignoringSIGTSTP/SIGTTIN/SIGTTOUoutright 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, postingEvent::Heard(signal)raw per swallowed signal, and the anchor’s ownChildHandlehanded to the reaper as aWatchwhose posting closure maps its every exit toEvent::AnchorDied. Ownership is the anchor’s presence, not a flag:group()isGroup::Owns(leader)exactly when the anchor is, and only that pgid may be signalled or killed; a joining group answersGroup::Joins(membership)and lends no terminal on its own account,lendanswering aTerminalLoanfor an owning group only;PipelineGroup::joiningis that no-anchor, no-pgid shape a nested pipeline inside a stage thread gets instead, andPipelineGroup::membershipwhat a thread stage’sIocarries: 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::finishjoins the report reader and drops the watch, whoseDropreaps. The group’s verbs are lifecycle only —prepare/joining,group,membership,leader_pgid,lend,end_anchor(whichPipeNode::joincalls betweendriveandfold, 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:escalatewrites the cause on every live stage in one sweep, then one grace signal perAddressthe 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 slicedTEARDOWN_GRACEdrain that escalates as it files, a wait for live thread stages bounded by the cleanup grace (skipped underAborted), the kill (kill_live, the same addresses), a further drain, whileCollectState::dropwritesTerminatedon every live thread stage and kills whenever it is dropped with a stage still unobserved);thread.rs(launch_thread_stagewires aThreadstage’sIofrom itsStageRoute, and its closure — given itsSlot { ix, tx }, wrapped in aSettleOnDrop— builds its ownStageObservationand sendsEvent::Returned(ix, obs)as its last act, the guard armed until then so an unwind reports in its place. A non-final stage’sstdoutis 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 aCapturenode, so it runs as a thread stage: its stdin the pipe, its buffer taking what the command writes.ThreadStageis the collector’s handle onto the running thread and the stage’s node:cancelwrites the cause on that node — on Windows also oneCancelSynchronousIo, re-issued each pass of the collector’s loop while the stage’s chain carries a cause — whilejoin_after_settledreclaims a thread whoseReturnedhas already arrived andDropwritesTerminatedand 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 asRunningChild::waitalways does, structurally invisible to the collector);collect.rs(CollectStateowns the stage handles from the first one launched, anmpscchannel every producer feeds — the reaper (through a stage’s ownWatch), a stage thread’s own report, the sentinel, the anchor’s witness and its watch — received a slice at a time byrecv, which reads the current scope between slices and mintsEvent::Cancelleditself — carryingEvent::{Ended, Returned, Wrote, Cancelled, AnchorDied, Heard}, with noStopped/Continuedvariant at all, which the pure foldstepfolds over, returning at most oneEffect(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()). AHeardkey strikes the run’s node and recordspressed— the kernel gave the whole pgid that signal already, so teardown sends no second copy of it — but only through the collector’sloan: 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.foldopens by hearing out every queued report, settles each external undersentjoined with the cause on the current scope, and the loan drops with the collector, returning the terminal and striking nothing;holds_terminalreports the loan actually made, whichLaunchCx.holds_terminalfreezes. TheSlota stage hands its own producers is minted byPipeNode::launch, which holds the sending half as a local and drops it on return — the collector keeps none. Attribution rides the handle:StageHandle::sentis the strongest cause this collector ever sent that stage, joined bymaxand carried onto itsStageEnd, andStageEnd::settle, called byCollectState::fold— the one place&mut Shellreaches an external’s settlement (audit synthesis, exit-hint lookup, sandbox-denial augmentation, viafinish_external_settlement) — is the one place that reads it back against what actually happened: a thread’s forgiveness reads its own break alone (aReaderGonebreak is only ever this collector’s doing), an external’s is the collector’s reading ofsent: a death by its own kill withsent == Some(ReaderGone). The verdicts join bystronger/rank, an escape outranking an error and ties going to the earlier stage;drive/cancel_all/steptake no&Shellat all.driveiswhile live() { recv; step; run }, no interval and no backoff, returning when every stage is observed;PipeNode::joinisdrive(), thenfold(mooring, shell), the group dropped after).anchor.rsis the hidden--ral-pipeline-anchorchild’s entrypoint (Role::Anchor; a bundled tool’sRole::BundledToolis launched beside it bysandbox::reexec::launch), a ral-written stage running on a thread of the parent process instead. On Windows every external a stage thread spawns still resolvesPgidPolicy::Joinagainst the anchor’s registered Job Object, assigned at creation under the suspended create → assign → resume path (windows-spawn-boundary).
- A bundled (uutils) head routes
- A
grantis 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 bycapability::check_*(capabilities), and child-owned effects are kernel-backed at external dispatch: when a projection is active,command::build_launchobtains a confinedprocess::Launchviaprojection_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_launchtakes 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, andchild.rstracks the per-execJailCgroup, so cancel and settle kill the whole tree throughcgroup.kill(asetsid’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 underruntime/) carries onlyEngineSeedandseal/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.