The seed rides the protocol, sealed

A payload the host relays but never reads crosses as bytes; its content is a contract between the engines at its two ends, and the spawner is what makes those ends one binary. A sub-agent’s bequest therefore rides the protocol twice, as FOValue::Bytes in the parent’s enquiry and as Origin::Child(Sealed) in the child’s Attach, and the guest daemon, not a parent engine, spawns every engine. Amends the sentence of engine-protocol that said the seed “never touches the engine protocol”; realised in core/src/seed.rs (seal, Sealed), core/src/engine.rs (Engine::boot), exarch/src/agent/desk/agents.rs (make_engine), exarch/src/agent/factory.rs (Factory), ral-daemon/src/init.rs (make_engines) and synod/src/machine_factory.rs.

Context

Inside the guest only an engine could start an engine, so a parent engine did the host’s job: it bound a guest port, minted a token, kept a listener thread, re-exec’d --engine with the seed on an inherited fd, wrote an ack byte, and kept a table of children it swept but never killed, so a dead parent’s children were orphans the daemon buried. Each of those existed because the guest had no process that made engines on request, though the daemon already spawns the root onto a connection on fd 3 and engine::spawn is a function of any connected descriptor. The one-fork construction (Shell::child_of, Runtime/Image/Terms, Bequest) made the seed a pure codec; what remained was who spawns.

Decision

The opacity law. The first-order law governs positions the host reads. The seal is read by no host: in the enquiry it is FOValue::Bytes, a first-order datum the desk relays untouched, and in Attach it is one closed envelope field gated by PROTOCOL_VERSION like every other. The two parties that do interpret the bytes are two instances of one binary, both spawned by the factory from one path (ral.engine on a read-only rootfs); their agreement is structural, not versioned. Sealed::unseal is pub(crate) to core, so the host cannot read it.

Every engine is born from an Attach. An Attach is an EngineSpec (version, installer tag, terminal, home: what names the engine) and an Origin: Origin::Root(Root { cwd, env, config }) runs the root recipe over those three; Origin::Child(Sealed) unseals against the engine’s own Runtime and constructs with Shell::child_of, and names no cwd, env or config, since the seal carries them. The split makes the old defect unrepresentable: a sealed attach no longer has cwd/env fields for a desk to fill with its parent’s stale values, and a wire seat keeps only the EngineSpec. Engine::boot(installers, &attach) is the one door; PROTOCOL_VERSION is 21 for this shape.

Builtins live on the Runtime alone. Engine::boot derives one Arc<Runtime> from installer.runtime(&spec); the root shell, a sealed child and every identity-adopted child are built over it, so every seat decodes and resolves against the same Σ. Shell has no way to install builtins or to give its runtime back (install_builtins, install_captured_builtins, install_set and Shell::runtime are gone); tests dress the Runtime before boot.

The host makes engines; the daemon is the factory. The wire arm of the fork builtin does no I/O: bequeath, seal, name the bytes in the enquiry as `sealed <bytes>. The desk dials its Factory (synod’s is MachineFactory over Machine::connect_guest), attaches the child as the parent was attached plus the seal, waits for Attached, and only then answers. The daemon listens on FACTORY_PORT for the VM’s life and spawns one engine per accepted connection, exactly as it spawns the root. The root keeps dialling out (its dial is the boot’s readiness signal); children are dialled in, because the host already holds the spawn the connection is for.

Admission is the peer’s CID. The daemon accepts only VMADDR_CID_HOST (is_host) and drops anything else. A guest process cannot present the host’s CID: the source CID is written by the guest kernel’s transport. Losing the token loses nothing.

The scratch is the host’s, on every seat. Every root Attach names $EXARCH_SCRATCH in its Root.env; engine_root refuses one that does not (a child’s rides its bequest’s env overrides); no engine mints a scratch, so no lock crosses a process boundary and nothing must live as long as an engine.

The scratch carries the tool homes (CARGO_HOME, GOPATH, …), shared by every agent in one tree; on the guest the scratch is /tmp itself. A narrowed child (a read-only grant may still write the scratch) can therefore plant $CARGO_HOME/config.toml or $CARGO_HOME/bin/* for a wider sibling or parent to execute. Accepted: agents in one tree trust each other. Grant narrowing bounds what a child may touch in the user’s world, not what it may plant for its own tree. This held on the host seat before and now holds on the guest too.

Who ends what. A child ends when the host drops its connection (EOF on fd 3); halt signals every engine’s process group.

Why no stamp

A build-id stamp on the seal would guard against a mismatch the spawner makes impossible, and the one agreement that can drift, host against guest media, is already gated by PROTOCOL_VERSION at Attach, checked before any seed is decoded. The boot contract moves to 3 (boot-contract-is-versioned) for the factory port.

What this deletes

The hatch: seed/hatch.rs (listen_for_hatch, hatch_over, seed_from_env, HATCHED and its sweep), the token, HATCH_ACK, the seed fd (RAL_ENGINE_SEED_FD), the table of unreaped children, the guest port builtin, the old hatch-era Birth, Dial/MachineDial, and the orphan question: no engine has children it must outlive.