Map: core / transport
The wire layer that carries a shell across a process boundary. Its one consumer
is the sealed child engine a wire-seat agent spawns: a Bequest — the parent’s env, context,
sources, terms, fixings and the spawn’s grant — is serialised to JSON, framed, and sealed into bytes, and reconstituted in a fresh
engine process (shell-state). A pipeline stage never
rides it — stages are threads sharing the parent’s memory
(stages-are-threads) — and neither does
a grant body, which evaluates locally while its external
children are confined per command
(sandbox-external-children). The
front-end⇄engine protocol is a separate wire,
engine-protocol, though it shares this layer’s
value vocabulary and codec.
Every wire↔runtime hop is an exhaustive, field-complete map: no hop passes through a constructor that defaults a field the wire carries, and no kind round-trips through a string with a catch-all decode arm. A sealed child is indistinguishable from an in-process fork exactly when no hop drops a field or collapses a variant. The discipline is mechanical — an exhaustive match makes a new variant fail the build, a field-complete struct literal a new field:
- value walks (
types/value/first_order.rs) matchValue/FOValueexhaustively, once forward and once back; - hydration installs a complete
HandlerFramethroughHandlerStack::push_framerather than re-deriving fields like itsFrameKind, so a hydrated alias stays removable byunalias; a per-name entry is unary by construction and hydration does not re-check its arity, which the sender’s install already validated (handlers-and-aliases-are-lambdas); - kinds ride as serde enums (
SerialClosure,SpawnGrant), and floats by their IEEE-754 bits, total and exact where JSON’s number turns NaN and ±∞ intonull.
Values — core/src/first_order.rs, core/src/seed/table.rs
FOValue<X> (first_order.rs, which knows no Value) is the one value
vocabulary every seam speaks: first-order by construction, over an extension
slot X uninhabited by default (NoExt). Externally tagged on the wire, with
typed accessors (field, as_str, as_int, …) for a host reading one.
SerialValue = FOValue<SerialClosure>(seed/table.rs) fills the slot with what this wire adds:SerialClosure::Thunk(SerialThunk)— the closure’scompand aSerialEnvSnapshotnaming its scope’s row — andSerialClosure::Native, a builtin’s name and applied arguments, re-linked against the receiver’s own table.SerialBindingmirrors a scope entry, value and scheme, so a sealed child keeps each binding’s type (session-scheme-continuity).first_order/datum.rs—Datum, the one strict first-order codec every typed protocol payload goes through (encode; adecodenaming what arrived ill-shaped), withtag/untag/field/exact_keysand therecord!macro, which derives a strict record: exact keys, each once, an unknown one answered with the key it most likely meant, and whose encoding is canonical (keys sorted).variant!does the same for a payload enum,label!for an enum of bare tags (kebab-case labels offstrum’sIntoStaticStrandVariantArray). Withtyped, each also implementsty::Typed, so a record declared once yields both its encoding and its ral type; the records ral reads back live infact(ErrorRecord,Observation,Io,Receipt, …), below the checker, which reads their types off the declarations (ErrorRecord::ty());poll’s polymorphic shape isIo::ty().extend(..)over them. The doors that mint them sit above (types::error,types::audit).first_order/byte_encoding.rs— byte strings serialize to base64 in JSON and native bytes in binary codecs. Binary decoding copies or takes the whole byte buffer. Each decoder requires its encoding.
One walk each way joins Value and FOValue
(types/value/first_order.rs). FOValue::walk folds a Value, handing each
leaf that is not data to the caller as a borrowed Leaf; unwalk and
for_each_ext are its converses. from_runtime, try_from and scrubbed
differ only in that leaf function, and so do into_runtime and From<FOValue>.
A leaf that is not data has three treatments, one per seam. Value::opacity
classifies it — a block, a function, a handle:
TryFrom<&Value> for FOValuerefuses it, naming the first such leaf and whether it was nested (NotData);FOValue::scrubbedmakes the conversion total by writing every such leaf as its`opaque [type: …]placeholder — the flat wire’s treatment, taken byMooring::surface;- the fork’s scrub (
core/src/types/shell/scrub.rs, shell-state) replaces onlyHandles, with the same placeholder, and keeps closures live, since this wire interns them. It reaches every handle the fork holds — in a binding’s data, in every scope a closure captured, in a native’s applied arguments, in a handler frame’s arms — and empties the fork’s hooks.
Scopes — InternCtx and WireDecoder
Environments cross as rows of a table, one per distinct allocation, by
Env::ptr_eq identity — so a captured environment with shared structure
cannot unfold into an O(2^N) tree. A closure keeps only the bindings it
mentions, so rows are per closure and small: every empty capture is one row,
a Rec group’s siblings share one, and the linear root scan makes interning
O(roots²). A value two closures mention is written in both rows
(a-closure-keeps-only-what-it-mentions).
Σ never crosses: a row is ρ alone, and the receiving shell’s own natives and
prelude answer for a decoded closure exactly as crate::types::lookup reads
them for any other, through the receiver’s Signature.
InternCtx::intern_envonly reserves a row and queues the environment;finish, the table’s sole accessor, encodes the queue as a worklist, so encoder stack depth is bounded by data nesting within one environment, never by the length of a chain of closures (depth-proof-env-seam).finishencodes every binding: every path onto this wire is a scrubbed fork, so a handle reachingSerialValue::from_runtimeis a fault in ral, and its error says so.WireDecoder::for_runtimerebuilds the rows in dependency order by Kahn’s algorithm overcollect_scope_deps’ edges, so a stream chain decodes in linear time, refusing an out-of-range reference or a cycle. It carries the receiver’s builtin manifest, for a decodedNativeto re-link its name against — nothing else, since neither Σ tier rides the wire.SerialEnvSnapshot::into_runtime, given aWireDecoder, is the sole wire→runtime conversion of an environment.
The seed — core/src/seed.rs, core/src/guard/grant.rs
EngineSeed is a Bequest in wire form, and a codec and nothing else:
scope_table, env, context, the parent’s sources, terms, fixings and
the spawn’s grant. No frame crosses: a
sealed child’s machine starts over the empty
stack, so what rides is store, never continuation. The environment rides once,
as the row of one `Env`, ρ alone; the context is
Context<Vec<WireHandlerFrame>>, the live Context with its handler stack
swapped for interned frames (Context::try_map_handlers), so nothing mirrors
a field and every other field crosses as itself — env_overrides, cwd,
grants, args, modules. The fixings ride as the session’s table of
(UnitId, var, Fixed) triples, never inside a Site, which names only its
unit: a shipped closure’s sites name the parent’s units, so their entries
govern them in the child as in the parent. The SourceDb rides whole, so a
parent closure’s FileId resolves in the child to the file it named, and
terms is the same Terms type the constructor destructures, so a term added
to it rides or the build fails. A bequest cannot express a hook, and none
rides.
WireHandlerFrame is a handler stack frame,
each alias arm with its scheme
(session-scheme-continuity).
EngineSeed::encode(&Bequest)builds one from a bequestShell::bequeathproduced — the package both seats take, so an identity fork and a sealed child snapshot the same fragment andexarch-agents `startmeans one thing regardless of seat (agents’s one-snapshot law).encodetrusts that scrub rather than re-checking it.decode(self, &Runtime)returns theBequest, decoding everything through oneWireDecoderbuiltfor_runtime: constructible only from theRuntimewhose builtin table re-links the natives by name, so no call site picks a manifest of its own. The decoded bequest goes toShell::child_of, which pushes the grant exactly as for a parked one;bequeathhas already snapshotted the fixings and construction takes ownership of them, and a sealed child runs no recipe.seal(&Bequest) -> Result<Sealed, String>encodes and frames oneEngineSeedintoSealed, bytes the host relays and never reads (the opacity law, the-seed-rides-the-protocol-sealed);Sealed::unsealispub(crate), so only an engine can read one.Sealedis aDatumoverBytes, which is how a seal rides the enquiry asFOValue::Bytes, and isOrigin::Child’s payload.Engine::bootunseals it against the runtime it has just derived.Bytesserialises as base64 under a human-readable serializer (first_order/byte_encoding.rs), so a seal also crosses JSON.SpawnGrant—Inherit(⊤),Base(name), orRestrict(record)— crosses unfrozen: a`cwdplace in a grant names the cwd of the shell it governs, so the freeze happens on the child’s side.SpawnGrant::narrow_ontoresolves it against that shell’s cwd and home and pushes one session layer — the stepShell::child_oftakes for an adopted identity fork (IdentityTransport::adopt_parked) and a sealed child alike.Basereaches the host’sGrantNarrower, since core has no base-tag lexicon;Restrictgoes throughguard::decode_capability_map(a-spawn-is-one-layer).
Framing — core/src/frame.rs
encode + put / read_frame are length-prefixed JSON frames, carrying both
the seal’s one EngineSeed frame and the engine protocol’s WireChannel
frames (core/src/protocol/channel.rs).
fuseis the one frame-size check both doors apply —MAX_FRAME_LEN, 256 MiB — before the reader allocates a body and before the writer sends one, so an oversized frame fails locally with a sentence instead of killing the peer mid-stream. The writer’s check isencode’s alone: it hands back anEncoded(a fused length and its body), whichputwrites, failing only on I/O.- Neither side caps depth: both encode and decode under
serde_stacker, which grows the stack onto the heap, so a legal nest of any depth crosses and only the fuse bounds it. - A body that fails to decode is dumped to an owner-only file, named in the error.