Map: exarch / shell eval
shell_eval.rs runs one tool call as a ral top-level run against the
persistent `Shell`. run_shell is a pure request
supplier: it builds a transport-level Source Run, dispatches it through
ral_core::carrier::dispatch_to_report against the agent’s seat transport —
the in-process IdentityTransport, or a wire engine’s WireTransport
(agent) — and returns the terminal Report that comes
back, or the Severed when the engine is gone before one arrives — the
transport is the canonical run vocabulary
(engine-protocol;
a run, end to end;
run-turn-is-host-api). Evaluation can
be entered only through a framed run door — the reduction primitive behind it
is crate-private (run-turn-host-loop) —
so core owns all the run machinery (compile, frame install, capture, the wall),
and run_shell owns only the run it builds. Formatting is Avatar::ral’s,
through report::tool_result, which also reports whether the run failed:
- source + a virtual
originnaming the call’s turn (turn N). Core’scompile_runrunscompile_and_typecheckseeded from the live session (shell.session_schemes(), the one name→scheme seed — session-scheme-continuity); the prelude’s schemes ride scope[0], installed when the annotated prelude was evaluated at boot. The check is strict — any type error is fatal — over the one inference pass every evaluated path shares (unconditional-mode-pass). Parse/type errors come back asReport::Static { rendered, status }, already the caret report the protocol drew, which the model reads clipped whole, then the rejection tip (report::rejection: no command ran and noletbound, resend the whole call); aReport::Faultedreads clipped, then the fault sentence (report::fault: some or all of the call may have run, check what stands before retrying;shell_eval/clip.rsholds the head+tail clip thatrejection/faultandagent::digestboth use); on success the annotated comp runs (unify-turn-evaluation); caps— the agent’sGrantStack(Agent::caps), pushed for the eval’s dynamic extent. This is the sandbox: the boundary is the pushed grant frame plus the top-level contract, not a source-levelgrant { … }the model could escape. External commands route through the same OS sandbox as ral (capabilities). The eval runs inMode::Session, so each landedDefinewrites back intoshell.envas it lands and the run’sContextis the session’s own:let,cd, and env persist across tool calls (the in-module tests pin this);io: RunIo::Capture— core mints the stdout/stderr buffers and returns them inReport::Ran’scaptured: the full, model-visible and logged text. Nothing echoes live; the rail surfaces cards instead, and the digest caps shape only the model’s history view;terminal: RequestedTerminalAccess::Denied— a tool run holds no terminal lease, so the foreground handoff is uninvocable: a bare pipeline cannottcsetpgrp, and the SIGTTIN crash of an agent stealing the controlling terminal becomes a state the types refuse to represent. Paired withstdin: RunStdin::Empty, the run reads no terminal at all — an explicit empty source, not a side effect of foreground denial;wall— the per-tool deadline, theraltool’stimeout_secs(60s default). Core arms it on the run’s foreground scope before compiling, so it bounds the whole run; the wall ends the run asEnding::Stoppedwith causeTimedOut, the one stoprenderfollows with the timeout tip, the exit-124 stderr described under the wall is a place below, while Esc stays an interrupt and a closed stdout renders nothing. Notryin the call can catch either. A grant body evaluates locally — no sandbox-IPC round trip to interrupt — so cancellation reaches any spawned child through the ordinary process-group / cancel-scope path;worker_cap—LIVE_WORKER_CAP(64), the admission bound core enforces at the spawn door: the 65th spawn is refused (the error namesawait/cancel) while 64 workers still run; settled entries lingering under retention hold no seat. Its sibling constantSETTLED_WORKER_RETENTION(256 ral calls, matching the binding lease’s scratch expiry) is not on the request at all — it is armed once throughShell::arm_worker_retention, and the sweep it parameterises is engine housekeeping;worker_title—TitlePolicy::Required: an untitledspawnis refused at its step, before any birth, so every worker the model starts is named by what it does in its birth card, its completion notice andworkers; Surface delivery and enquiry answering are notRunfields:dispatch_to_reporttakes onehost: Arc<dyn Host>— engine-protocol’s single object for a run’s whole host-facing surface. Every real caller buildsagent::desk::RunHost, pairing the per-callExarchDeskwith aSurfaceApplier(shell_eval/surface.rs, builtins); a bareSurfaceApplieralone is the mute host the harness runs under, whoseenquireanswers the honestEnquiryError::no_desk()rather than reaching a handler.
BINDING_IDLE_CALLS (256) is the binding lease’s constant, which this
module owns but does not put on the request: it is not
per-run policy, it is per-shell policy, armed once by
shell_eval::arm_session_ledgers — the one ledger-policy site, applied by
the one root recipe both carriers boot through (boot::engine_runtime for the
process half, boot::engine_root for the recipe, which refuses a Root that names no
scratch: the host owns it); a fork
keeps its terms through core’s Shell::bequeath and Shell::child_of
(agent; agent-binding-reaping).
Reusing the settled-worker-retention figure is deliberate: one ral-call
clock, read by both ledgers for their own idle policy. LARGE_BINDING_BYTES
(1 MiB) rides the same BindingLease at the same arming site — a
residency threshold, not a lifetime one, so the install chokepoint checks it
independently of idle age or baseline status.
Completion is dispatch_to_report returning Ok(Report); an Err(Severed)
is folded by every attend-loop caller into
agent::seat::EngineLost (agent). A spawned
worker — a
server, a watch — holds bounded deferred surface storage in core, never a clone
of the bus `Emitter`, so it cannot keep the tool run
from ending (run-turn-host-loop). Frame
teardown is core’s: IoLoan brackets the byte streams and registers a run takes
on loan from the shell, self-healing on a caught worker panic as well as on the
normal return (panic-recovery), while
the run’s invariant half — surface, deferred sink, desk, fork arm, cancel, the
worker cap — threads as an immutable &Mooring the stack itself restores. Exarch
needs no bracket of its own: its RunHost is a plain Arc Avatar::ral
builds and passes to the dispatch, never installed as shared state, so an
unwind drops it with the rest of that call’s stack. The
dynamic-context half of the contract lives in agent.
The wall is a place, and everything before it stands. A timed-out call
keeps both its effects and its bindings: the child is running, the bytes are in
the inbox, the wakeup is armed, the staged reply is still harvested, and the
lets that landed are still bound —
a failed run keeps its bindings,
pinned mid-script for the wall by
mid_script_wall_keeps_the_bindings_that_landed. The asymmetry is
before/after the wall, never bound/unbound. Nothing rolls back, and a model
told only “retry with a higher timeout_secs” would duplicate every one of
those effects. So a TimedOut stop writes four things to stderr, in
order, and exits 124:
- the engine’s rendering, verbatim — unconditionally, exactly as the ordinary-failure branch beside it. A stop is stamped on the innermost node it unwound through, and the run’s closing poll keeps that location (cancellation), so the diagnostic carries the span that locates the wall: the frontier between the steps that completed and the one that did not;
- the asymmetry and the remedy — it timed out after n seconds at the
point above; the steps before it completed and their definitions are still
bound, the step it names did not complete, and the steps after it did not
run, so resume from there rather than replaying the call. Then
recovery:— raisetimeout_secsfor work that is simply slow, orlet h = spawn "title" { … }and let the run return, since the host notifies at the next exchange boundary when it settles, andawait $h, orawait !{worker <id>}from the notice, then behaves as the block run there, without polling; - an audit of what already stands, when there is any — and not the wall’s
alone: every ending whose transcript does not otherwise show what landed
files it, a chosen
exitas much as a suffered raise, last, after whichever remedy the ending offered, because the record is owed by the failure and not by the deadline (a call that staged itsreplyand then died on a command’s non-zero exit, or choseexit 2, made that reply stand just as surely). This is the one exception to core’s own trail: the desk authors the sharedObservationvocabulary from the host side, into a per-call fragment joined at render — never into the engine’s trail, because a wire seat’s cancel can land while the engine sits parked inenquire, mid-unwind of a builtin whose act the desk already committed, and engine-side recording alone would then report a standing act as failed.HostServices::commit_actis the one door every acting handler funnels through — minted inAvatar::host_services, the one place a call’s whole desk capture is assembled, so the fragment’s extent is the call’s — and it builds oneObserved::Actper attempt, at the arm where the outcome is known, and fans it out itself: the rail’sDisplay::HarnessCallrow always, off the veryverb/subject/payload/refusedthe observation carries, and the fragment only whenrefusedisfalse. A refused attempt leaves no fragment entry: it answers one question — what stands — and an entry for work that never happened would blunt it. Because one datum feeds both readers, an eighth act cannot reach one and miss the other by construction, with no enum-adjacency discipline to maintain (harness-calls-are-acts). Aschedulecall’s subject is always the caller’s own label —schedulerequires one, so there is no minted default that could ever disagree with it.DeskActstill names the seven acts and yields both spellings, the rail’sverbcolumn — one word per act, soevictand not the family and tag it is reached by — and the audit’s past tense. - the workers that outlived the call, named. A
spawned worker is moored byMooring::for_workeronto the session root precisely so a foreground cancel cannot reach it, so a raise, the wall, or anexitleaves the work running. What the sentence reports is reachability. A handle bound by a step that completed before the failure is still bound, soawait $hreaches it; one the failing step would have bound never landed —pattern::destructureis all-or-nothing. AnEnding::Unreturnable— the run settled on a handle, block, or function, which the wire cannot carry — failed no step, so there the sentence says instead that a handle only returned was lost with the result. Either way the id still reaches the work: the sentence ends withawait !{worker N}, so no survivor is orphaned. Which workers outlived the call is a different fact from a committed act, so it is its own sentence and the desk grows no worker view to hold it. Which workers are this dispatch’s is not arithmetic across the seam: every birth its extent gave reaches the host as anObserved::Workeron the surface, ordered before theReport, and the call’sSurfaceApplierkeeps those ids;shell_eval/report.rs’srenderjoins them against the`workersprobe, decoded into core’sWorkerRowbyTransport::workersat the run boundary — legal there on a wire seat exactly as on the identity seat, since the registry never crosses. A birth still present in the registry, running or settled-unclaimed, is named (shell-state); one already claimed has left the registry and is nobody’s orphan. The sentence names each survivor as#id (title), the title being the one theworker_titlepolicy required of itsspawn, up to five, and counts aloud whatever it does not name — a silent truncation would read as “that was all of them”.
run_shell asks for no trail (trail: None). The host already hears every
observation on the surface, so a trail would only retain, until the run ends,
records nothing reads. “This command completed, that one was cut” is the
engine diagnostic’s job.
Surface decoding. shell_eval/surface.rs is everything a surfaced value
becomes in the record. decode_surface is the single decoder both delivery
regimes share: the live path — dispatch_to_report’s drain loop calling
host.surface(val) for every Event::Surface, which SurfaceApplier
decodes and records — and the deferred sink (deferred_sink, installed on
the transport before each dispatch), whose batch is posted to the inbox when
a worker flushes and recorded by surface::replay as the item arrives; both
go through one private decode_loudly and one surface::record, with no
guard layered over them. The
codomain is Surface, the shell’s own closed vocabulary, wrapped in
Decoded beside Landed and Unknown. The decoder tries observation, then
notice, card, and done — the shapes are disjoint, so the order carries no
meaning — and has no pin arm. It carries only
the structured value each channel names —
no Card mark tree, since that is built by whoever renders — a printer’s fold
over the recorded Display commit (frontend) — and
never by the decoder or by the seam that records it (surface::record):
- the register is not decoded here at all:
exarch-pinsreaches it through the desk, which buildsSurface::Pin { key, card }/Surface::Unpin { key }itself. A pin is a rendered card in a slot, so its card is the fact itself, not a copy of one, and no key is reserved. Pins are mirrored asPinDigests so the nudge layer can name pinned state without parsing rendered text — the read side reuses the same store rather than adding a second one (pins); - an
`observedvalue core emits at a redirect, exec, or capability-check door decodes throughObservation::from_surfaceintoSurface::Observation, the raw observation alone (io-surface); onelandingdeclines isDecoded::Landed, kept off the rail without a word; - a
`noticecore’s ready-boundary housekeeping pushes (a worker reap, an idle-binding prune, a large-binding warning) decodes toSurface::Notice(engine-protocol); - any other value tries
value_to_cardand becomes aSurface::Card, the one shape whose payload is a card; the closed mark set and thevalue_to_card/render_carddecode-and-bind path live in cards; - the
`donecompletion event a worker flushes at the end of its batch decodes toSurface::Done— its id, title, run time and real ending — which the inbox also words as the model’s wake-up notice (surface_notice, frontend), ending in the one call that gets the result back:errand #7 (building) finished after 3 mins 12 secs (exit 101): build failed. `await !{worker 7}` replays its output and returns its value; `workers` lists every worker.; - a value that is none of these is
Decoded::Unknown:decode_loudlyrecords aForensic::SystemNotenaming the shape it dropped, the extension law’s loud unknown class.
The producer is a direct exarch-surface call at each kit site, with no cross-language
sentinel constant. Same-thread children inherit the sink; workers do
not inherit the live sink. Core buffers their exarch-surface calls and flushes a
settled batch to exarch’s deferred sink (the inbox path), so no bus Emitter
clone outlives the tool run. Across the
OS-sandbox
boundary the events are buffered in the confined child and replayed through the
parent’s sink (carried on the IPC response), so they
are batched rather than live under the sandbox.
shell_eval/builtins.rs registers exarch’s resident host atoms — view-text/view-hash, the
grep-files search, the hash-addressed edit-hash/edit-replace, whose
file I/O happens in Rust, below the redirect frame
(io-surface) — and sources the small embedded
data/agent.ral helper library (the -around readers, the tasks kit) into the
shell at boot (builtins). The one host_surface()
value declaring these sets rides core’s boot_shell at construction and is
also the builtin surface a wire engine child’s Frame::Attach names, so a
remote shell is dressed with the same atoms.
A tool command that fails under an active OS sandbox carries a kernel-denial
diagnostic — the blocked syscall, the exact path to grant, the symlink caveat —
appended to the error’s hint. That harvesting lives in core
(core::sandbox::diag), driven by the command and pipeline runners over the
failing call’s wall window and rendered identically by both the ral REPL and
exarch (capabilities).