Map: synod
synod is a second product over the same engine — an office-work delegate
where exarch is a coding one. The user grants one folder,
describes a task in plain English, and the agent works in it — the folder
itself, in place, under a safety net: checkpoint before the job, a
plain-language change report after it, conflict-checked undo per file or whole
job. It is not a fork of exarch and not a mode of it: it depends on exarch as
a library and supplies only what differs
(synod-is-a-second-product).
It is one crate in two halves: the library modules are the engine anyone
could drive, and the desktop shell rooted at synod/src/main.rs is the only
thing that drives it.
The design record is dev/docs/VM/SYNOD.md; the landed state is
dev/docs/VM/SYNOD-v1.md. Every conversation boots a real hardware machine
from shipped boot media — there is no software-only fallback — and the engine
runs inside the guest, one engine process per session, driven over the
design’s §3 wire
(engine-protocol). One guest,
two lifecycle backends: Virtualization.framework on macOS arm64, Hyper-V
through the Host Compute System API on Windows x86_64
(windows-hyper-v-backend). The
two are the same machine assembled from the parts each platform has, and the
guest cannot tell which one booted it, because every difference is either
invisible from inside (the disk bus, the socket family) or carried on the
kernel command line. On Windows the machine is created by a LocalSystem
service the installer registers, asked for over a named pipe by an
unprivileged window, so nobody has to be given a hypervisor’s privilege to run
synod (windows-machine-broker).
synod/ — the library
-
lib.rs— the crate doc names the five differences from exarch. -
build.rs— the Tauri build, plus the one thing the bundle cannot check for itself:boot_contractreadsvm-image/out/boot/boot-manifest.txtat the resource map’s own path and puts it toral_daemon::boot::check_media, so media whose boot contract is not this host’s fails the build rather than the guest’s own reading of its command line (boot-contract-is-versioned).ral-daemonis a build dependency for exactly this; absent media is not a failure here, since the bundle is where Tauri names a missing resource. -
boot.rs— the media this build ships, found and readied once.boot_media()looks in three places, each simply a place a file might be rather than a#[cfg]branch: a macOS bundle’sContents/Resources/boot/, a Windows installation’sboot/beside the executable, then the development pipeline’svm-image/out/.BootPlan::realiseinflates a shipped zstd rootfs into the XDG cache against itssha256sidecar — a signed bundle is read-only, so the cache is the only writable home the image has — and yields avm_manager::BootArtifact. Whatsession::beginhandsvm_manager::detectis avm_manager::BootMediaclosure overrealise, not the artifact itself. -
grant.rs— the folder becomes aral_core::types::Capabilities(grant), and avm_manager::MachineSpecnaming the same folder. One grant, read twice: once as authority, once as a workspace. -
prompt.rs+data/*.md— the office persona and the office toolbox, assembled through exarch’s own section renderer (exarch::prompt::render) and grant rendering (exarch::prompt::grant_summary), over ahost_sectionof synod’s own that tells the agent guest truths only. Synod’s base ends in Talking to the user (data/surface.md); the shared exarch per-agent resolver then appends Agent to returning helpers, after that office guidance, while the conversing synod trunk receives no return section. The same resolver appends spawn guidance while the trunk or helper still has fuel, so synod’s prompt composition stays on the shared construction rules. -
session.rs—Conversation, one folder held open from first message to last.beginopens the grant, boots the machine, and seats exarch’s agent on the wire the machine hands back (exarch::agent::RootSeat::WireoverMachine::take_wires, attached at the guest’s/work).control_seatcarries no platform condition at all:take_wireshands back each platform’s own owned handles andral_core::protocol::WireTransport::adopttakes either, so the protocol is one function (engine-protocol). Before any of that,beginstat-measures the folder (workspace::manifest::measure) and composes the large-folder warning — including a free-space sentence offworkspace::history::free_bytes— before a single byte is read, then opens the store and spawns the before-checkpoint on its own thread as aBaseline(Pending/Ready/Failed/Crashed/Settling);beginnever joins that thread itself, so the boot runs alongside the walk rather than after it and the conversation opens the moment the boot is done (store-lives-as-long-as-the-conversation).exchangesettles the baseline first — joining the capture thread on its first call, every call after finding it already settled — before it drives anything, since the guest must never write into a folder whose baseline is still being read, then drives one message throughexarch::headless::converse_settled(exchange-ends-at-fleet-quiescence), bracketed by the safety net — a checkpoint before, a checkpoint after, even after a failed run; the after-checkpoint waits for fleet quiescence, not merely the trunk’s own silence, so a helper still writing to the folder never races the report.endcloses the wire — the guest halts itself — and only then joins a baseline stillPending(a conversation closed before its first message) and wipes the store: closing the window is accepting the folder as it stands, so undo ends with the conversation. The trunk’s fuel isSPAWN_FUEL(3), the same depth budget exarch’s own trunks carry — promotedpubfor exactly this reuse — andRootConfigcarries aDialimplementation overMachine::connect_guest(below), so synod’s office assistant may delegate to helpers that run concurrently in the same guest, against the same folder, under the same safety net. The model picker lives here too:menu/refresh_menulist what the computer’s credentials can reach (cached-instant and fetched-complete), and aChoicenames anAccountId, model, and effort — an id and never a display name, because a name that happened to match another account’s would start a conversation on someone else’s login. What flows the other way for display — the opening’s and a finished sign-in’s account name — is spelledlabelon the wire, so an id and a display string cannot be mistaken for one another in either direction.sign_indrives exarch’s browser login flow (exarch::provider::oauth::login_flow) and admits the fresh account to the live store and catalog, so a ChatGPT plan signed in from the window is usable without a restart — the credential store is behind aMutexfor exactly that reason, taken only for an account list or an admission, never across a fetch or a boot.prepareitself only delegates: where synod’s accounts come from isaccounts.rs. -
accounts.rs— synod’s own credential story, and the one place it stops borrowing exarch’s. A key reaches exarch through the environment because exarch is started from a shell; synod is double-clicked, inherits the desktop’s environment, and faces someone with no.zshrcto export from. So there are two sources in one order: the computer’s credential manager (exarch::provider::keychain, entries named(synod, account-id)) first, because it is the one a person can see and change from inside synod, and the environment underneath it — the same sweep and scrub as exarch, still run first because it is the step that must happen while the process is single-threaded. That order is now one call:CredentialStore::admit_fromover aSecretVault, whichKeychainimplements, in place of the two-step synod used to perform by hand. Which services exist is a third thing and no secret, and synod re-derives none of it: the table isexarch::provider::identity::built_in_services, and further endpoints are declared in$XDG_CONFIG_HOME/synod/providers.ral— synod’s file, in synod’s directory, holding addresses and protocols but never keys — read throughexarch::provider::accounts.What stays synod’s is what is about synod’s window. A row carries the account’s
identity::label, where the credential in force came from, the hint naming this computer’s vault, whether the row can be withdrawn, and at most the key’s last four characters; thesourceis read off the store’s own record of which door the key came through, so drawing the list costs the vault nothing. A keyless local server is reported asno-keyoutright rather than left to be inferred from a missing hint. The old three-waykindis gone with the provenance it encoded — “is this a login” issource == SignedIn— and since one service may own several ChatGPT accounts, the static sign-in card gives way to rows drawn fromstore.available(). See synod-keeps-its-own-accounts.
synod/src/workspace/ — the safety net
The module the product’s guarantee lives in, all host-side, exercised under
ordinary cargo test:
manifest.rs— a folder’s state at one moment: path, kind, size, blake3 hash,mtime_nsbeside it — the stat facts a capture checks before it reopens a file. A path that vanishes between listing and reading is a deletion to record, never an error to raise (WalkError::Vanishedinternally;hash_fileitself answersOk(None)for a file gone by the time it is opened); empty folders and symlink targets recorded, links never followed. A cheapmeasure— stat only, no bytes read — feeds the large-folder warning (~2 GiB) before the real walk starts.history.rs— the per-folder store: content-addressedobjects/(identical bytes kept once, ever) pluscheckpoints/<id>.json, withBefore,After, andUndomoments.captureis a stat walk once a baseline exists: an entry whose size andmtime_nsmatch the folder’s latest checkpoint, and whose mtime is strictly older than that checkpoint’s owntaken_at_ms(git’s racy guard, stamped at walk start, not walk end), reuses the recorded hash without reopening the file. Every byte a job or an undo replaces is in the store before it is touched. Every open store holds a shared advisory lock on alockfile beside it (flockon unix,LockFileExon Windows, onelock_impper platform);wipedrops that lock and removes the store’s directory whole at a cleanConversation::end, andsweep_stale— called once frommain.rsbefore any conversation can open its own store — probes every<slug>/historywith a non-blocking exclusive lock and removes only the ones nothing holds, a crashed session’s leavings; a live shared hold always refuses the probe, so a running conversation is never swept.free_bytes(statvfson unix,GetDiskFreeSpaceExWon Windows) feeds the large-folder warning’s free-space sentence.changes.rs— the delta between two manifests: created, modified, deleted, renamed (a deleted and a created file with identical bytes, paired).restore.rs— the conflict-checked driver: a path edited after the job is a conflict resolved only by the caller (KeepCurrentor explicitPutBack); nothing silently overwritten, nothing destroyed.report.rs— the GUI’s seam: the job report,undo_file(either name of a rename undoes both sides),undo_all, the headless run’s plain-text rendering.
synod/src/shell/ — the window
The desktop shell (Tauri v2, hand-written static frontend, no bundler, pure
cargo) is the one process: it holds the Conversation in-process — no child
binary, no stdin framing. One window, three states: choose a folder and a
model (with a Thinking control beside the Assistant picker) and describe the
job; watch the assistant work, its narration streamed in; then read what
changed and put anything back. commands.rs holds the folder picker, the
conversation verbs (start, send, restart, end), the model listing (instant
from the cache, one background refresh), and opening before/after versions
with the user’s own applications; keys.rs is the accounts screen’s five
commands — list, save or forget a key, declare or withdraw an endpoint — each
returning the fresh list and ending in the same models-refreshed event a
sign-in ends in, so the picker converges the same way whichever door a
credential arrived through; sink.rs is the bridge that streams the
conversation’s narration into the window, and knows the root agent’s id so it
can route by depth rather than by blindly assuming there is only one agent —
the two comments that once justified id-blindness by fuel: 0 are retired
along with the fuel figure. A helper’s Token/Thinking/State events are
dropped (a helper’s prose is not the conversation); its Observation/Done/Notice/
Resources fold to ProcessCard exactly as the root’s already do, and its
Card folds in beside them — deliberately, where the root’s own Card stays
first-class, since a helper’s card is process, not conversation; Usage
still counts, the bill being the exchange’s whoever spent it. Born/Died
become SynodEvent::Helpers { live: u32 }, a counter the sink accumulates and
emits on change — one fixed-position magnitude mark in the dial region, no
stream, no animation, nothing entering the transcript. SubagentDone — which
arrives on the root’s emitter, since announce runs in the parent’s own
drain — becomes SynodEvent::HelperDone { name, ok, elapsed_secs }, rendered
as one process line inside the dial’s rung; WaitingOnAgents maps to a
status-bar label. Plain register throughout: the window says helpers,
never agent/session/model, and there is deliberately no tab strip — the
assistant delegates onward, and the window reports the folder, not the org
chart. signin.rs runs the opening screen’s
“Sign in with ChatGPT” button — one sign-in at a time, cancellable, its
progress and outcome events (sign-in-step, sign-in-done) rendered beneath
the button, and the account it wins arriving as the same models-refreshed
the picker already renders through; with no account set up the sign-in is the
screen’s primary button and the folder picker waits for it; review.rs
translates the workspace vocabulary into cards and runs the
gentle-then-explicit conflict flow; synod/src/main.rs runs exarch’s
dispatch_pre_main re-exec trampoline first, like every
multicall binary here.
The frontend is one file, synod/ui/index.html — markup, style and script
together — beside the three libraries it vendors and the nothing it fetches:
marked.min.js (GFM), purify.min.js, and katex/ (KaTeX 0.18.1, its
stylesheet and its twenty woff2 faces; the only web fonts the app ships).
Assistant prose is markdown with TeX, and renderAssistantMarkdown is the
single path model text takes to the DOM. Its order is the load-bearing part:
each formula is lifted out before marked sees it — markdown would read
x_1 * x_2 as emphasis, and breaks: true would cut a multi-line $$ with
a <br> — leaving a private-use sentinel that the prose carries through
marked and DOMPurify unharmed; the typeset formula is put back into the
scrubbed tree afterwards, because DOMPurify’s CSS filter would strip the
inline metrics KaTeX’s layout is made of. That is safe only because KaTeX
runs with trust off, where it can emit neither a link nor a raw node: the
scrub still covers everything that came from the model as markup. An
unterminated formula is not one, which is what keeps a half-streamed $$
from flashing red while it arrives; a $ inside code, beside a space, or
against a digit ($5-$10) is a dollar sign, not a delimiter. Code is judged
twice, because it must be: the scan before marked knows only fences and
backticks — whether four spaces open a code block or continue a list item is
a question only a block parser can answer — so a sentinel that marked put
inside a <code> is handed back as the text it was written as. No message
wears a name above it — who spoke is said by the bubble’s side and colour.
vm-manager/ — the machine
One trait each side of a boot: Hypervisor::boot(&MachineSpec) -> Result<Box<dyn Machine>, Error>, with MachineSpec::resolve the one
platform-independent judgment of a spec, called by every backend so a bad
spec is refused in the same words everywhere. BootArtifact::resolve is its
twin for the media, and makes every file absolute: the paths are opened by
another process — vmcompute runs in C:\Windows\System32 — so a relative
path that resolved for the caller names nothing by the time the machine is
built. Machine::take_wires is the one signature that varies — Wires holds
an OwnedFd per wire on Unix and an OwnedSocket per wire on Windows —
because each platform owns its own accepted sockets, and both are adopted by
the engine protocol unchanged.
The crate boots only real machines. detect(Option<BootMedia>) answers
Vz, Brokered, or Hyperv, or refuses with a sentence for a
non-programmer: not a platform with a hypervisor at all, no boot media, a
macOS build unsigned for virtualization, or a Windows account the compute
service will not serve. On Windows the order is the machine broker first —
which needs no boot media from this process at all, since the service has its
own installed beside it — and only then the in-process Hyperv, which is a
checkout rather than an installation. There is deliberately no software
fallback: a synod that cannot put hardware between the agent and the rest of
the computer refuses to start rather than degrade to a weaker mode.
examples/boot-smoke.rs is the human-driven boot, one body over both
backends.
-
vz.rs—Vz: Virtualization.framework, macOS arm64, bound throughobjc2-virtualization. It builds and validates the full configuration — direct kernel boot, RO rootfs + RW sparse session disks, a virtiofs share of the granted folder withread_onlyas the mount’s law, a vsock device and no network device, console to the host log — drives the!Sendmachine from a dedicated thread against a private serial dispatch queue, and declares boot only when the guest’s daemon has dialled both the control port and the net port. One socket device multiplexes them: a second network device is exactly the fix that must never be made, and a test assertssocketDevices().count() == 1to say so. Those accepted connections are the host ends of the §3 control plane and the §6 net wire:Machine::take_wireshands both out exactly once (a second ask panics as a caller’s bug), and a second guest dial is refused. Booting requires thecom.apple.security.virtualizationentitlement —vz::entitled()is a process check, not a platform check.Those two listeners are the only ones: every other wire a machine carries the host opens.
Machine::connect_guest(port)dials a listener bound inside the guest and hands back its host end — aCommand::Connecton the machine thread under this backend, defaulted to a refusal sentence on any backend that cannot dial inwards. There is no third listener, no accept pump, no published preamble, and no host-side token table: a connection the host opened needs nothing to correlate it, because the side that opened it already knows what it opened it for.synod/src/machine_dial.rsis that seam’s synod end —MachineDial, anexarch::agent::Dial(agent) over a machine it holds in trust fromConversation::begintoConversation::end, which takes it back withinto_machineonce the agent that shared it is gone. TheMutexit wraps the machine in is forSync, not for the dial:Machineis declaredSendand no more, and oneArc<dyn Dial>crosses the desk’s threads.
vm-manager/src/hcs/ — the Windows machine
Hyperv: Hyper-V through the Host Compute System API — computecore.dll, the
surface the Virtual Machine Platform feature provides and the one WSL 2 and
Linux containers are built on. This is the module the broker below runs in its
own process; detect reaches it directly only in a checkout. available()
answers in one of three remedies rather than one failure: the feature is not
installed, this account is outside the computer’s local Hyper-V
Administrators group, or the compute service is not answering at all. An HCS
system has no thread affinity — it is a handle, not a queue-bound object — so a
Guest holds its machine directly and the only threads in the backend serve
blocking I/O.
mod.rs—Hyperv,Guest,available(), the refusal texts, and the table of correspondences withvz.rs.boot’s order is load-bearing at three points: the console pipe exists before the machine that names it, the control-plane listener is bound before the machine starts, andHcsGrantVmAccessruns on the four boot files before a worker process opens them as its own virtual account.Guest::stopcloses the wire first, so the guest powers itself off from inside, then revokes every access entry it granted, so none naming a dead per-machine identity is left on anyone’s folder;Dropshares that path. Three constants carry the teardown’s own patience —REMOVE_GRACE/REMOVE_PULSE, over whichremovewaits out the worker process that holds the session disk pastStopped, andORPHAN_AGE, above whichHyperv::new’ssweep_orphansreclaims what earlier runs left;session_disk_epochis what makes that sweep incapable of naming anything but a session disk (session-disk-outlives-its-machine). Both dial timeouts end inconsole_says, which quotes the guest’s own last lines and names its log, andGuest::dialledis what decides whether that log survives the teardown (guest-console-outlives-stdout).api.rs— the entry points, resolved withLoadLibraryW/GetProcAddressrather than statically imported, so a Windows without the feature gets a sentence instead of a process that will not start.Api::settleholds the whole operation protocol — mint an operation, hand it to the call, read the real outcome and the service’s own JSON error text out ofHcsWaitForOperationResult— in one place, andHCS_E_ACCESS_DENIEDis the one code recognised rather than merely reported.HcsGrantVmAccess/HcsRevokeVmAccessare looked for incomputecore.dllandcomputestorage.dll, because they are exported by the former — Microsoft’s own documentation and Go binding name the latter, which on 10.0.26100 exports neither.spec.rs— the machine as one JSON document, since HCS takes no builder objects:Chipset.LinuxKernelDirect,ComputeTopology,Devices.Scsi(the rootfs at LUN 0, the session disk at LUN 1),Devices.Plan9(the granted folder, withLINUX_METADATAalways andREAD_ONLYwhen the grant is),Devices.HvSocket,Devices.ComPorts,ShouldTerminateOnLastHandleClosed, and no network adapter at all — absent, not disabled. Being data rather than a sequence of setter calls, the document is built and read back under ordinarycargo testwith no machine and no privilege.kernel_command_linewritesral.portandral.plan9from named fields, never positionally: the guest would mount its own control plane if the two were ever swapped.hvsock.rs— the control plane.service_guidis the entire bridge between the two addressing schemes: a Linux vsock portpis the service GUIDpppppppp-facb-11e6-bd58-64006a7986d3, which is why a guest that knows nothing of Windows can still be dialled.socket_sddlnames SYSTEM, built-in Administrators, and this user’s own SID — never a wildcard, since this socket is one of two doors into a machine with no network adapter of its own (the guest’s actual network rides the secondHvSocketport,NET_PORT, into a host process — egress) — andfresh_machine_iddraws onProcessPrngbecause the machine’s identifier is half the socket’s address.vhd.rs— aVirtualDiskattachment must be a VHD, so the raw ext4 images are wrapped as fixed VHDs: the sectors verbatim followed by one 512-byte footer, which makes wrapping an append rather than a conversion — no block map, nothing transcoded, the filesystem identically placed.ensure_rootfs_vhddoes it once into%LOCALAPPDATA%\Synod\Machine\behind a marker recording which image was wrapped, and passes a shipped.vhdthrough untouched;create_session_vhdmakes the session disk, which the guest formats on every boot and the machine’s teardown deletes. That one is a dynamic VHD declaring 8 GiB — ~18 KB of metadata when empty — because Hyper-V refuses a virtual disk whose file is sparse (0xC03A001A), so growth has to be the format’s business rather than the filesystem’s.console.rs— the guest’sttyS0on a named pipe the compute service dials as a client, because without it a boot that failed and a boot that is merely slow are the same timeout. The pump tees:stdout, a per-machinesynod-console-<id>.login the same cache the disks live in, andTail, a ring of the last lines the boot failure quotes.RETAINED_LINES,LINE_LIMIT,LOG_LIMITandLOG_LIFETIMEare the four bounds that keep the diagnostic from becoming litter, anddiscardis what a boot that dialled calls on its own log (guest-console-outlives-stdout).Console::wakeconnects to its own pipe to release a pump parked on a machine that never started.
vm-manager/src/broker/ — the privileged half, on Windows
The service that owns the machine so the window does not have to, and the
client that asks it. One instruction crosses (Request::Boot — a folder and a
read-only flag), and everything else about the machine is the service’s own
(windows-machine-broker).
mod.rs— the protocol and the argument:PIPE(\\.\pipe\synod-machine-broker),VERSIONchecked before anything else, theRequest/Replypair, and length-prefixed JSON with a 64 KiB frame cap written out here rather than borrowed fromral-core(a machine layer that needed the shell to talk to its own service would have the dependency backwards).Request::Adoptedis the third step ofBoot→Booted→Adopted, and its doc is where the ordering trap is stated.client.rs—Brokered, aHypervisorthat asks rather than acts, sodetectcan prefer it withoutsynod::sessionor the seat knowing which backend it got;Brokered::available()is the probedetectasks.adopt_socketturns the service’sWSAPROTOCOL_INFOWbytes back into anOwnedSocket.BrokeredGuestholds the pipe as the lease:shutdownandDropboth close it, and closing it is what stops the machine.service.rs— the only privileged code synod ships, and the file to review:PIPE_SDDL(D:P, SYSTEM + built-in Administrators + interactively logged-on users, neverAUand never a wildcard),serve/serve_client(one machine per connection, held in the serving thread’s local),readable_by_client(ImpersonateNamedPipeClient, the folder opened as the caller, reverted by aDropguard on every path out including an unwind),client_process(GetNamedPipeClientProcessId— the kernel’s answer, not the client’s),describe_socket(WSADuplicateSocketWfor that one process id),media(the boot artifact beside this executable), andcache(%ProgramData%\Synod\Machine, machine-wide because the wrapped rootfs is identical for every user andLocalSystem’s%LOCALAPPDATA%is SYSTEM’s profile).vm-manager/src/bin/synod-machine-broker/— the program:main.rs, the entry point every platform gets, since a Cargo binary target belongs to the package and not to a platform; andservice.rs, the two ways it starts on Windows — the service control dispatcher (SERVICE_NAME=SynodMachineBroker, reportRUNNINGbefore serving, stop by process exit since the threads own the machines), and--console, the same behaviour with a terminal attached, which is how a maintainer sees the guest’s own console say why a kernel did not come up.synod/wix/broker-service.wxs— the installer side: a WiX fragment (referenced fromsynod/tauri.windows.conf.json) declaring the service intoINSTALLDIR, so it shares the oneboot\directory with the application;LocalSystem, automatic, started at install, removed on uninstall.just broker-install/broker-uninstalldo the same from a checkout withsc.exe.
ral-daemon/ — the guest’s PID 1
Runs inside every booted guest. Every decision — the kernel-cmdline
boot::Boot, the mount plan and its inside-before-outside ordering invariant
(mounts.rs), the guest-wide sysctls the §5 jail depends on (sysctl.rs),
the engine’s command line and fd plumbing (engine.rs: the vsock connection
arrives as fd 3), the classification of a wait result (reap.rs) — is a pure
function unit-tested on any machine; the syscalls are a thin edge only a
guest can exercise. The overlay root is deliberately the initramfs’s job; the
daemon verifies and names what it was handed. No ral semantics, no authority
policy. When the engine exits — the wire’s EOF is its cue — the daemon powers
the machine off from inside: the clean inside-out halt.
Boot::workspace is where the guest learns which hypervisor’s folder it has:
an Export, either Virtiofs { tag } or Plan9 { name, port }, decided by
whether ral.plan9 names a port on the command line. The 9p arm is the one
mount whose options cannot be written before the mount is attempted, because
trans=fd names the descriptor of a fresh vsock connection to the host’s
server — the daemon dials, sizes the socket’s buffers, and mounts
trans=fd,rfdno=N,wfdno=N,msize=…,version=9p2000.L,aname=<share> over it.
ral.plan9 and ral.port are two sockets for two jobs and are read by name,
never by order.
The ral. key set and its value grammar are one versioned agreement, and
boot.rs is where the version lives, beside the command line’s only writer and
only reader: boot::CONTRACT, MANIFEST_KEY, and check_media, the judgment a
host build runs over the media it is about to package
(boot-contract-is-versioned).
ral-daemon/examples/boot-contract.rs prints that constant and nothing else, so
the media’s manifest records a number it compiled rather than one it read.
vm-image/ and ral-initramfs/ — the boot media
The design record’s §7 built, and ARCH-parametric over synod’s two guests:
ARCH=arm64 for Virtualization.framework, ARCH=amd64 for Hyper-V. A build
refuses a container that is not its own architecture rather than let qemu-user
emulation quietly produce something else. build.sh assembles the rootfs — a
pinned Ubuntu 26.04 LTS (resolute) office userland (LibreOffice headless,
the Python document stack, pandoc, OCR, wide fonts, full locales, no
toolchain) via mmdebstrap → ext4 → zstd in a native container, checksummed and
version-manifested. build-boot.sh builds the boot pair, stamping the git hash
and boot_contract= into boot-manifest.txt — the one line a host build reads
back — and the kernel is where the two guests part: arm64
takes Ubuntu’s generic kernel apart to the raw Image VZLinuxBootLoader wants,
amd64 keeps that same vmlinuz verbatim, because it already is the bzImage
LinuxKernelDirect loads. So do the module sets — virtio on arm64; on amd64
hv_vmbus and the drivers on it (hv_storvsc, hv_utils, hv_balloon),
vsock + hv_sock, and 9p as three modules: 9p, 9pnet, and 9pnet_fd,
the trap worth naming, since upstream split trans_fd out and a trans=fd
mount with 9pnet loaded and 9pnet_fd missing fails with a bare ENODEV.
There is deliberately no hv_netvsc: the guest has no network device to drive.
vm-image/README.md records the corrections of §7’s prose to real package
names and the open questions (squashfs, distribution, determinism).
ral-initramfs/ is the initramfs itself, and every decision in it is a typed
plan — assemble the overlay root, make the session disk, install daemon and
engine, switch_root — unit-tested on any machine. It hardcodes no disk:
plan.rs’s resolve_disks probes candidate pairs in order, virtio
(/dev/vda + /dev/vdb) first, then Hyper-V’s SCSI (/dev/sda + /dev/sdb),
and refuses by naming every candidate it looked for.
What is not here
Two things about the Windows machine, neither of them settled by the code compiling:
- A completed guest boot is not witnessed yet. What is: a machine created
and started through the broker, booting a kernel and an initramfs that formats
the session disk and reaches the daemon — known because the daemon refused a
command line carrying a
ral.key its own build predated, and said so on its own console. What is not: a guest that finishes booting and dials, since the media rebuilt against this host’s boot contract has not been booted yet. The path is otherwise compiled, clippy- and rustdoc-clean, and unit-tested wherever a test can reach without a machine — the document’s shape, the VHD footer’s checksum and geometry, the port→service-GUID mapping, the socket’s own descriptor, the pipe’s, the console ring’s line bookkeeping, the release of a disk another process holds.vm-manager/examples/boot-smoke.rsandsynod/examples/boot-run.rsare the vehicles for the rest. - Whether the host’s 9p server can read the granted folder unaided is
untested.
HcsGrantVmAccessis called on the four boot files, which a worker process really does open as its own virtual account, and deliberately not on the user’s folder. The broker’s impersonation check answers a different question — may the caller read it — so if a guest’s mount is refused, a session-scoped grant is still the knob.
Also the image pipeline’s open questions above. The rest of the design record
runs — end to end on macOS, and everything above the machine on both: the §3
wire carries real runs (synod/examples/boot-run.rs witnesses
boot → shared folder → engine → settled report), the §5 spawn jail stands
inside the guest (a fresh uid and a cgroup between the engine and what it
runs), and §6 gives the guest a network of its own — a tun whose only peer
is guest-net, a user-mode TCP/IP stack in a host process — rather than the
single fetch-url verb an earlier draft made the whole egress surface
(egress,
the-guest-gets-a-network-not-a-verb).
vm-workspaces-cross-by-copy
records where synod’s work-in-place workspace deliberately departs from
exarch’s cross-by-copy position.
Where to look
dev/docs/VM/SYNOD.md— the design record;dev/docs/VM/SYNOD-v1.md— what landed;dev/docs/VM/EXARCH-VM-v2.md— the shared VM implementation plan;vm-image/README.md— the boot-media pipeline and its open questions.- exarch — the sibling binary and the library synod embeds.
- exarch-architecture — the loop both products run.
- windows-hyper-v-backend — why the Windows machine is assembled the way it is.
- windows-machine-broker — why a
LocalSystemservice creates it, what the Hyper-V Administrators group would have cost every user, and what keeps the service’s surface narrow. - boot-contract-is-versioned — why the kernel command line carries a version, and why the build rather than the boot is where a host/guest mismatch is caught.
- guest-console-outlives-stdout — why the guest’s console is teed to a log and quoted in the failure, and why the broker protocol did not have to change to carry it.
- session-disk-outlives-its-machine — why teardown waits out the worker process, and why a starting backend sweeps the cache.
- transport — the framed seam the control plane rides, whose stream type is std’s owner for a connected socket and not a claim about the address family.
- agent — the one-exchange wire spawn synod’s
fleet rides: the guest binds a port for the duration of one spawn and names
it in its enquiry, the desk dials it while answering, and the
Dialtrait synod implements above is the seam it dials through. - agents — the one-snapshot law:
Shell::fork_scrubbedis the one fork both the identity arm’s nursery park and the wire arm’sEngineSeedtake, soagents `startmeans one thing regardless of seat. - engine-protocol — why the guest listens and the host dials, and why that direction is what deleted the correlation machinery rather than shrinking it.
- exchange-ends-at-fleet-quiescence — why synod’s after-checkpoint waits for the whole fleet, not just the trunk.
- io-process — the guest spawn jail
(
jail.rs) whose unfilteredsocket(AF_VSOCK)a hatch’s eight token bytes are the second line against, the guest kernel’s refusal of a guest-local dial being the first, until a seccomp address-family filter lands.