youros

How it worksUsing it

Machines

A service is a name bound to a handler, in the first release a process on a local socket, given the verified caller, the verb, its arguments and a stream. The node checks the call (access.md), then hands it over.

Services as processes

A service is a process, so services written in any language port, and a crash takes down one service, not the node. The terminal keeps a real pty per shell, owned by the node, not by the session, so a dropped pipe leaves the shell running to reattach to. The files service confines a caller's path to its root. MCP is a service too, off until turned on (mcp.md).

Where it breaks: a crashed service fails its calls, and state worth keeping is the service's job. A local socket must be reachable only by the node. A service that calls another is a caller with a key of its own (access/service-to-service).

The desktop

The desktop is a program of its own, youros-desktop, in Rust beside the node, which starts it when it finds it there. It links the machine's GStreamer and GLib, so it can't be in the node's static build, and an update's trial never waits on it. It drives mutter's RemoteDesktop and ScreenCast over D-Bus (zbus), and runs GStreamer inside the process (gstreamer-rs): PipeWire in, x264 (or VP8 where x264 isn't installed) out of an appsink. Errors and frames come to the service as they happen, rather than being guessed from a child process's silence. Input goes back through the same mutter session. One desktop per user on a node.

The machine The pane gets Root
logged in, unlocked own; mirror when built no
logged in, locked own no
nobody logged in own, under the user's lingering systemd no
no GNOME nothing yet; a wlroots compositor run the same way would fit —

own is gnome-shell --headless --wayland --no-x11 started as the user, on a D-Bus of its own, with XDG_SESSION_ID naming no session. Measured on GNOME 46, Ubuntu 24.04 (2026-10-19), while the machine's own session was locked:

  • it came up in about a second, using 140 MB;
  • it was unlocked;
  • it streamed a RecordVirtual monitor;
  • it took remote pointer input.

youros-desktop did the same there, the physical session's LockedHint unchanged throughout; e2e/desktop proves the logged-out case.

The private bus lets it sit beside the user's own session, where a second logind session for the same account could not: both would hold org.gnome.Shell on one bus. The session id matters too. With none, gnome-shell takes the user's graphical session from logind (or the greeter's, when nobody is logged in), locks when that session is told to, and writes its own lock state onto it. With an id logind doesn't know, it takes nothing and carries on. Nobody logged in needs lingering (loginctl enable-linger), which keeps the user's systemd, PipeWire and /run/user up; polkit lets any user turn that on for themselves.

mirror (not built) is RecordMonitor on the user's own session. GNOME ends every cast when the screen locks and refuses new ones until it is unlocked, and we leave that alone. Keeping the cast going through the lock would put a remote unlock in front of a screen somebody locked and walked away from.

Frames go as session datagrams (session/datagram-window), each split into pieces under the relay's 64 KiB frame. The socket between node and service carries a datagram as a frame whose length has its top bit set. The browser says got for each frame that arrives whole. A viewer three frames behind its last got holds back the encoder, whose input drops frames until it catches up; what is encoded is then always a chain the viewer can decode. A viewer silent for two seconds stops holding the others back. A frame missing a piece is dropped, and the browser asks for a keyframe. It decodes with WebCodecs onto a canvas.

Captures. The capture is the pane's size: the caps right after pipewiresrc are what mutter builds the monitor from, and a resize is a new capture, because a PipeWire stream keeps its first format for life. A new capture is made before the old one stops, so the desktop always has a monitor; with none for a moment, mutter takes windows out of full screen. Each capture has a PipeWire connection of its own. Sharing one, as pipewiresrc does by default, hands a new capture the dead connection after PipeWire restarts, and it waits on it for ever. A second, idle connection ends when PipeWire does, which is how its death is heard at once; a pipewiresrc that has nothing to say posts no error. An idle screen sends nothing, so pipewiresrc sends its last frame again every second, and three silent seconds mean a dead stream. A capture is stopped on a thread of its own: a pipeline whose PipeWire died can hang going to NULL. mutter makes its virtual pointer on the first motion, and a window binds it only after, so the service moves the pointer once as each capture starts; otherwise a viewer's first click reaches nothing. Input moves the seat to the pane it came from, and each viewer hears who holds it. No call into GStreamer or mutter is made with the service's lock held.

Proofs run in e2e/desktop: a GNOME machine under systemd that nobody logs in to, in Docker, installs the node with the line the page shows, and Chromium reads a test app's frame counter and colours off the pane's canvas. Measured there (2026-10-19, llvmpipe, no GPU):

  • first frame 0.85 s after the pane opens;
  • 28 frames a second, directly;
  • a click on the canvas changes it after 110 ms (median), 165 ms (worst of ten);
  • a key after 45 to 80 ms;
  • a resize settles in 1.9 s;
  • frames are back 1 s after PipeWire is killed, and 8 to 9 s after the shell is (a new shell starts);
  • 12 frames a second over the relay, where the e2e relay writes every message to disk.

Where it breaks:

  • own is the user's account, not a sandbox. It shares the same files, keyring and PipeWire. An app that locks its profile, like a browser, runs in one desktop or the other.
  • Windows open at the machine aren't in own. Seeing those is mirror.
  • Locking is off in own alone. It gets a dconf database of its own (youros_desktop, seeded from the user's settings), with locking off: the node's grant is the gate, and an unlock dialog with no logind session behind it may never unlock.
  • A PipeWire restart takes the monitor with it. mutter drops it before anything can be made in its place, so full-screen and maximised windows come back out of those states.
  • A killed shell takes its windows with it. A new shell starts empty.
  • The direct pipe is still one ordered, reliable channel. A lost packet holds the frames behind it until SCTP resends it; the second, unordered channel the design wants (desktop/latest-frame) isn't built.
  • Pacing is by round trip. Three frames per round trip caps the frame rate on a slow relay.
  • It renders in software (llvmpipe), and GTK apps fall back from EGL.
  • The node depends on mutter's private D-Bus API, and doesn't check mutter's version yet.

Updates

A release is a numbered manifest at /dl/manifest on the host the node was installed from, with each build's SHA-256, and its signature beside it at /dl/manifest.sig. It names its component, node for now; a service or an extension would be a component of its own with its own rules (trust.md), and a node takes only node. Our signature is the floor: the account approving each release in its log (browser/approved-releases) is a second check on top, once built. Today a node takes every release of its own once idle; the tiers (trust/tiers) will make a change to the trusted base also wait for the account (trust/base-approved), which this updater doesn't do yet. The number is the count of commits on the line releases come from, which only grows because that line is never rewritten; a build knows its own number, and a build with none (made by hand, in tests) never updates itself. The release key is hybrid like an account's (D-2); its secret stays on the machine that makes releases, outside the repo, and the node carries the public half compiled in. The installer can only check a hash, since a shell can't verify ML-DSA; from the first run on, the node checks signatures itself.

The node checks on start, every fifteen minutes (a static file, free to serve), and at once when its relay hints at a later release; the hint only says when to look, so a relay that withholds it delays a node by a quarter of an hour at most (relay.md, Release hints). A newer, correctly signed release is downloaded, checked against its hash and kept beside the running file as youros-node.next. The node goes on serving until no session has carried a message for three minutes and no file is open in a room, or a day has passed; then it tells each session it is restarting, stops its services and starts the new build as a trial, while the old one waits. The trial has a minute to attach to the relay, have its services answer and load the account's log, and says so in a file. If it does, the old build is kept as youros-node.prev, the new one renamed into place, and the node replaces itself with it in the same process, so systemd and the installer's background start both carry on. If the trial exits or stays silent, it is stopped, its release is recorded as bad and never fetched again, and the old build starts afresh. A build that passed its trial but then fails to come up three times in ten minutes swaps the kept one back itself. Going back to the build kept on this machine is not a downgrade in the sense of node/no-downgrade, which is about what the server offers: it returns to what this machine was already running. Browsers open a new session as soon as the node is back, and the page shows each machine's release beside it.

Today the services are the node's own binary, so they update with it, and a restart ends every shell: the two future promises above are what lifts that, a service released and restarted on its own, and services that the new node adopts through their sockets instead of starting afresh. An extension (sandbox.md) is never in our release: it has a publisher, and its updates are signed by them (D-32).

Where it breaks: one release key, so losing it means a new key shipped by hand to every machine, and a stolen one signs code every machine runs; splitting it (several keys, a threshold) is open (trust/urgent-fix asks for a second key). Idle is what the node sees: no session message for three minutes and no file open in a room (files/open-is-live); a shell left open with nothing running counts as idle, and ends with the restart.