Map: core / runtime values & shell state
core/src/types/ defines what the evaluator manipulates
at runtime. types.rs is a re-export façade so the rest of the tree spells
everything crate::types::*.
Values
value.rs—Value(the runtime value category); beside ithandler.rs(the user handler stack:HandlerFrame,HandlerStack,FrameHandle),builtin.rs(BuiltinEntry/BuiltinTable, kept separate from the handler stack), andhandle.rs(the concurrency substrate behindValue::Handle:HandleInner,CompletedHandle,SurfaceBuffer). Handlers are deep and self-masking, with noresume(effects-handlers, handlers-deep-self-masking). A handler or alias arm is always a lambda:HandlerEntrycarries itsHandlerArity(Unaryfor a per-name arm or alias,CatchAllforwithin [handler: …]) fixed by the surface form, andvalidate_handler_arityrejects any non-lambda or wrong-arity value at every install boundary — the calling convention is never inferred from the runtime shape (handlers-and-aliases-are-lambdas).exec_arg.rs—RefusedArg, the shapesexecve(2)has no argument for, withof_valuefor the spawn,of_tyfor the checker, and oneremedyeach. Rendering an argv inside the shell is total (Value::render_argv); this is the one gate, declared once so the static refusal and the pre-spawn one cannot disagree (exec-argv-is-words).list.rs/map.rs—ListandMap, opaque newtypes over persistentimbl::Vector/imbl::OrdMap.flow.rs— the whole control-flow surface, four declarations:Settled<T> = Result<T, Break>,Break(Errorcatchable,Escapepropagating),Escape(Exit, andStoppedon Unix), andPolicyError(completion-escape-refactor). NoOption/null appears; optionality is open variants (optionality-via-variants).error.rs—Error,Status, and theBodyResultsplit.audit.rs— theAuditcollector, overobservation.rs’sObservation/Observed— the one vocabulary shared by the trail, the surface rail,--audit’s JSON, and the wire.env.rs— lexicalEnvandEnvVarsprocess-env overrides; a scope entry isBinding { value, scheme: Option<Arc<Scheme>> }, so the checker’s verdict rides next to the value (session-scheme-continuity). The scheme is shared, not copied:imblstores entries inline in a fixed-width node array, so an entry’s own width is what abindinto a shared environment allocates — oneArcbehind the scheme that no machine step reads takes aBindingfrom 360 bytes to 72. The wire form andbinding_schemeskeep their own types and convert at their boundaries.coerce.rs— thesig/sig_hintruntime-error constructors and theas_mapfamily ofValue→Mapcoercions, sitting below both the builtin and capability layers so each reaches them throughcrate::types::*.
Capabilities
capability.rs holds the capability types the grant
decision layer interprets: Capabilities, ExecPolicy, FsPolicy,
EditorPolicy, ShellPolicy, GrantStack, SandboxProjection, and the Meet
/ Join lattice operations (tested in capability/lattice_tests.rs). Meet
attenuates live frames; Join widens a base overlay, but Boolean false
permissions remain sticky vetoes.
Shell
shell/ partitions the interpreter state into fields by lifetime — the
field name is the invariant — joined by Shell
(turn-local-state,
the-evaluator-steps-closures):
env (the session environment, extended by every Define that lands) and
context (dynamic context — cd, aliases, hooks) — two flat fields, since a
step’s focus carries its own environment and there is no ambient scope for
a bundle to snapshot — plus Io, SessionState, and LocalState below. The
run door checkpoints and rolls back the (env, context) pair around every run
(Shell::enter), so a panicking run reports as a failed run instead of
corrupting the store.
-
Io— the run’s byte streams, and the only part of the frame theShellcarries (as the fieldio): stdin / stdout / stderr, the terminal snapshot, the launch role (io-process). These genuinely change within a run — a redirect frame swaps the sinks — so they are taken on loan and repaid:run::IoLoanswaps a freshIoin at install and restores the previous one onDrop, carrying the twoCopyregisters the run’s frame also owns for its life (session.root_fileandlocal.audit.call_site) with it, so an unwinding evaluation cannot leave a stale register behind. -
SessionState— what survives every run’s teardown: the durable cancelrootthat detached workers parent under (minted deaf to the ambient causes;Shell::face_signalsre-mints it facing, for the host that owns the process’s signals, andShell::join_sessionshares it, for a secondShellthe host runs beside that session, cancel-is-a-watermark), theanchora top-level run nests its foreground frame under — re-minted wherever the root is and never afterwards, so the scope tree is the LIFO extent it claims to be — thesourcesregistry rendered against after a run returns (append-only for the session, so a nested run’s spans can never alias an outer run’sFileId) and theroot_filenaming the current run’s root source, theexit_hintstable, the host-installedbuiltinswith the session’slibrary_docs, the session’sterminal_lease, and theguest_jail(Someonly inside a VM guest — the spawn jail’s shared uid counter, io-process). -
LocalState— host-local scratch carrying its own flow rules (audit trail, REPL scratch, theworkersregistry, thebindingsledger, thedetachbudget); the residue once run and session state are named. The worker registry (shell/workers.rs) is a per-Shelldirectory of everyspawn/watchedHandleInner, one per agent;Shell::spawn_threadshares it byArcinto a spawned worker’s own shell (so a nestedspawnregisters alongside its parent), but a sub-agent fork or pipeline stage starts with a fresh, empty one. Beside the entries it keeps theReapNoticeledger the reap policies write — one compact record per entry removed by policy, atomic with the removal under the registry’s one lock — pushed by the engine as`noticesurface events at each settled run’s ready boundary (emit_ready_boundary_notices;take_worker_reap_noticesis crate-private, that push its one caller). Which entries belong to which dispatch is not the registry’s question to answer — aspawnobserves its birth into the dispatch’s own trail (shell-eval), and a reader joins that trail against the registry (or the`workersprobe, whose rows carry the sameid) to ask what became of it. A settled entry carries asettled_epochstamp too: retention is armed once at boot (Shell::arm_worker_retention, beside the binding lease), the registry’s own clock ticks once per source dispatch (tick_epoch), and the ready-boundary sweep (sweep_retention) stamps an entry at the first sweep that observes it settled and expires it (aRetentionnotice) once its unclaimed result has sat a full retention of ral calls — a host that never arms (the REPL) retains settled entries indefinitely. Worker teardown is structural: dropping the shell cancels every registered worker’s scope throughLocalState’sDrop, so a session’s workers die with its store — a host’s/clear, which replaces the outgoing shell wholesale, needs no cancel call site, and explicit destruction outranks every lease, the durable class included. Theworkers_ownedflag is what keeps that edge honest: aspawn_threadchild shares its parent’s registry byArc, so its own drop must not reap the parent’s roster.WorkerEntryalso implements the smallResidentsignature (types/resident.rs, residency) — designator, population, capability kind, lease row, state label, cancel — so the REPL’sjobslisting and its exit-time survivor warning read a worker’s facets through it instead of hand-formatting per population.The binding-lease ledger (
shell/bindings.rs, agent-binding-reaping) sits beside the worker registry but needs no lock: it has exactly one writer, the thread that owns&mut Shellfor every run, install, and prune (verified in the module’s own doc comment). Inert (BindingLedger:: default()) untilShell::arm_binding_leaseseals every name then visible in the scope chain as permanently-exempt baseline and starts the committed-run clock. Every persistent top-level scope write funnels through one fused chokepoint,Shell::install_scope_binding(scope.rs, besidebind_value/set_var): it classifies the write byEnv::at_session_scope()and stamps the ledger only when true, so “write a scope entry” and “stamp the ledger” can never be pulled apart at a call site — the evaluator’s four writers (assign_pattern’sName/...restarms,eval_letrec’s two installs) all route here. Host verbs (bind_value,set_var) stay on the rawEnvprimitive, since every host call to them precedes arming. Idleness is use-observation, not re-installation:Shell::dispatch’s source arm ticks the committed-run clock, and a lease is renewed by reference — the compiled program’sir::referenced_namesat each successful compile (ir), plus the resolved name at anEnv-arm command dispatch. The same chokepoint runs a second, orthogonal check:BindingLeasealso carrieslarge_binding_bytes, and an install whose value’sValue::shallow_size(a structural estimate —String/Bytesbyte lengths,List/Map/Variantrecursing into elements,Lambda/Block/Handlea small fixed constant, never descended) meets it queues aLargeBindingNoticeregardless of baseline status or idle age — residency and lifetime are independent axes.Shell::leased_binding_countrounds out the accessor surface — a probe figure, like the`largest-binding-bytesprobe’slargest_binding_shallow_size; large-binding notices ride the same ready-boundary`noticepush as the reap ledger’s (take_large_binding_noticesis crate-private).The
detachbudget (shell/detached.rs) is the one member meant to outlive the session:NoneuntilShell::arm_detach, which a host calls in the same act that installsdetach’s base frame (builtins), so the verb and the budget it spends cannot drift apart.DetachPolicy::admitcounts births, not occupancy — a survivor’s death is unobservable from here, so a release would be a number nobody can compute — and the policy isArc-shared into a spawned worker’s shell like the registry, so adetachinside aspawn { }spends the owning session’s budget. It is equally deliberately absent fromLocalState’sDrop: the surviving processes are the one thing a teardown must leave alone.
io / session / local are pub(crate): the fields that encode run
safety are not a public API. Hosts drive a session through the narrow accessors
gathered in host.rs, which a host crate reaches while only mobile stays the
public embedding seam. Shell::binding_count sits there too — the lexical
scope’s probe figure for a host’s /resources fold
(probe-convention): a count, never the
values, and enumeration renews nothing.
The mooring
What a run fixes is not on the Shell at all. The eight run-invariant
members are a Mooring: the surface sink, the deferred sink (a
detached worker’s completion delivery — None outside an agent host), the
desk answering the run’s enquiries
(engine-protocol), the fork door
naming how a forked session reaches the desk that adopts it (None outside a
host that installs one; like desk, never given to a deferred worker), the
foreground cancel scope, the deferred-worker WorkerLease (deferred_lease — the idle
bound and absolute backstop travel as one value; None never reaps), the
worker_cap admission bound (Some(cap) refuses a spawn of any class while
cap workers still run; None admits freely), and the run’s
terminal_access. It is an owned local on the run
door’s own Rust stack frame, and reaches every callee as an explicit
&Mooring, placed immediately before the &Shell / &mut Shell in every
signature.
The fork door is a two-armed sum, Fork (mooring.rs), read through
Mooring::fork:
Fork::Park(Nursery)— in-process. The reentrancy law bars a same-process desk handler from holding&mut Shell, so it cannot fork a session itself: the builtin body forks throughShell::fork_into_nursery, the fork waits in the run-localNursery, and the handler redeems it byNurseryIdwithNursery::adopt.Fork::Listen— across a wire. The builtin binds a guest port and names it in its enquiry; the desk dials back and the child engine is spawned onto that connection (transport). There is no pen, andfork_into_nurserysays so in its own sentence rather than reusing the absence one.
Both arms fork the same shell, Shell::fork_scrubbed, which is the whole of
agents’s one-snapshot law: agents `start means one
thing regardless of seat.
Immutability is what makes the frame free. A value that never moves needs no
putting back: an outer run’s mooring is restored by the stack unwinding, and
a NurseryGuard beside it empties a Park arm’s pen on that same unwinding.
In effect terms the split separates a Reader (&Mooring, with Shell::run_nested as its
local — a nested run’s frame is a child of the mooring it is handed) from
State (Io under a loan). Borrow when you can, loan when you must.
&Mooring and &mut Shell are disjoint borrows, so a builtin body can surface
an event while holding the shell mutably. Mooring is not Clone:
lend_terminal is the one lawful derivation, so the raise-never-mint rule on
TerminalAccess lives in one door rather than in every bulk copy. Outside
core only cancel, Mooring::surface, Mooring::lend_terminal /
in_terminal_loan, Mooring::fork, and Mooring::adrift are reachable —
adrift() being the mooring a host builds to call a builtin body outside any
run (no surface, no rail, no desk, no fork door, and a scope under a root
nothing else holds, so cancelling it is how such a caller drives the body’s poll points).
A worker rebuilds rather than sharing (Mooring::for_worker): it keeps the
deferred rail, lease, and cap, takes its own buffering surface, and gets
neither desk nor fork door — both barred to something that outlives its run’s
Report — under a worker scope of the durable root, so a SIGTERM reaches it
and a Ctrl-C does not.
Surface
The surface sink (Mooring::surface, Option<SurfaceSink> where
SurfaceSink = Arc<dyn EventSink>) is the value-typed dual of the byte
Io sinks. EventSink is a synchronous trait taking a
borrowed first-order FOValue
(engine-protocol); the Mooring::surface
method takes a borrowed Value, encodes it once at that door, and forwards onto
the installed sink — inert when none is present (a bare REPL). Run-scoped, not
a persistent capability — a run door installs it, so a clone of it has no
liveness role and can never decide a run is over. A detached worker does not receive the live sink: its events
buffer into a SurfaceBuffer and are delivered exactly once — replayed through
the awaiting run’s surface on the first await / race, or handed to the
session-lived deferred sink at the worker’s own completion, whichever renders
first (a shared joined latch decides).
Terminal handoff
The authority to hand the controlling terminal to a child is an unforgeable
TerminalLease, not an inferred predicate
(terminal-lease). It splits across two
lifetimes:
- The lease itself lives on
SessionState::terminal_lease, minted once at startup from thetcgetpgrp == getpgrpwitness —Somewhen ral owns the foreground,Noneotherwise. It is lent, never moved or cloned. - A run’s authority to borrow it is the
TerminalAccesson itsMooring:Denied(the safe default — an exarch tool run, the boot frame),Leased(an interactive run), orExplicitLoan(a within-run elevation).Shell::terminal_lease(mooring)yields&TerminalLeaseonly when access permits and the session owns a lease, so aDeniedrun cannot construct a foreground handoff.
Because the mooring is invariant, a raise is not a mutation but a derivation:
Mooring::lend_terminal returns a new mooring with Leased raised to
ExplicitLoan for _ed-tui, and the borrow ends when that value goes out of
scope. It leaves a Denied run untouched, closing the
Denied → ExplicitLoan door so a loan can only raise an authorised run,
never mint authority.
Method modules
Methods on Shell live by concern, one submodule each:
init.rs— construction, the startup env-var seeding pass intocontext.env_overrides(env-is-dynamic-only), and the machine facts (OS_NAME/OS_ARCH/OS_FAMILY) seeded into$env;host.rs— the host-embedding accessor surface;context.rs— theContextdynamic-context verbs;scope.rs—within/grantguards realising scoping;checks.rs— forwarders to the `capability::check_*(&Context, …)` decisions, splitting the disjoint context/audit borrow for the audit-bearing checks;cwd.rs(Cwd;seed_cwdlets an in-process front end whose working directory is not the process cwd state it directly),inherit.rs(the flow matrix, below),modules.rs,detached.rs(thedetachbudget),control.rs,hooks.rs(the session-lived hook table of named run-entry points — prompt render, startup, plugin hooks — resolved by the run door’s hook-program arm),repl.rs(ReplScratch, owned by the REPL layer).
The flow matrix
inherit.rs centralises what state crosses a parent→child shell boundary —
one file rather than a decision scattered across call sites, so no inheritable
datum (the host builtin table among them) can be silently severed by a call site
copying only the fields it happened to remember. There are two regimes.
A same-thread β-step — forcing a block or applying a lambda — does not fork:
force/beta in evaluator::machine step the body’s Closure in the
caller’s Shell, no snapshot or restore around the call (evaluator).
The io, session, and local state are simply the one Shell’s, and the
caller’s &Mooring is passed along, so the body observes the caller’s audit
trail, byte sinks, builtin table, cancel scope, and terminal lease without any
of them being copied — there is no second store to drift from the first.
Block and lambda entry are uniform: an unbracketed store write in either
body (cd, alias, a hook registration) persists to the caller, no snapshot
standing between the body and the store
(the-evaluator-steps-closures);
within [dir:]/within [handlers:] are the scoped forms for a caller that
wants the old bracketing back.
The owned-Shell modes are genuine runtime forks — a different store — and so
copy state explicitly. Each starts from a freshly-defaulted SessionState and so
holds no terminal authority: no lease on the session, and the mooring the
fork is handed carries TerminalAccess::Denied — the safe
default for a store that is not the session’s:
spawn_thread— a spawned worker (spawn,par, the detached-worker helper) on a fresh OS thread that owns its own IO; nothing flows back. Its mooring is rebuilt byMooring::for_workeron the calling thread (so the door can hand the caller the worker’s scope) and moved into the thread, which is why the worker runs under a child of the durable root rather than the foreground scope, and a run timeout or Esc does not reach it.inherit_from/return_to— the per-substate manifests a cross-process pipeline stage (child_of, child-eval) leans on. Their asymmetry is the flow matrix: the dispatch call site (local.audit.call_site) and thewithin-attenuable bits do not flow back, butcontext.cwddoes.child_from— a REPL aside (the hook shell, one call site in the REPL plugin runtime): an independent sibling that clones the parent’scontext, source cursor, and builtin table without touching its IO / audit / REPL scratch; no flow-back.join_sessionis its aside specialisation, sharing the parent’s cancel root, so plugin code there is interruptible while it runs and older interrupts stay out of its reach.fork_session— the host session fork (the sub-agent case), the session-scoped specialisation ofchild_from.fork_scrubbedis the door every sub-agent fork actually passes through:fork_sessionplus a scope stripped ofValue::Handlebindings, which have no wire form, so an in-process adoption and a wire hatch’s seed snapshot the same fragment (transport). See agent.
Every genuine fork copies session.builtins (the dispatch table) and shares
session.guest_jail, so dispatch reaches the child and a guest’s workers,
stages, and forks share one jail counter; the same-thread β-step shares both
by identity.