Robot Install & Drivers
Status: draft. The customer-facing side of ADR-0020 and ADR-0021: the core carries no driver; this document is how the right drivers get onto a robot without the customer becoming a packaging expert.
Principles
Section titled “Principles”- Install the core in one line, drivers only when a use case needs them. A robot that streams a USB webcam and a remote desktop never sees a stereo-camera SDK; a robot with a depth camera gets exactly that vendor’s package and its prerequisites.
- The tool knows the catalog; the customer states the use case.
Fjarr ships a machine-readable driver catalog;
fjarr-agent setupdetects what is attached, proposes a config, installs what it can, and prints precisely what it cannot (vendor EULAs, kernel modules, reboot). - Missing is a state, not a crash. A configured track whose driver
is absent is
unavailablewith the package to install — in the doctor, the manifest label, the introspection endpoint and the dashboard — and everything else runs. - The same answer everywhere. Robot-side
--check, the dashboard’s robot page and (later) the fleet view all render the same source status from the same data.
Distribution channels
Section titled “Distribution channels”| Channel | Contents | For |
|---|---|---|
apt repository (https://apt.fjarr.io, suites stable and testing, component main) | the packages below | robots on Ubuntu 26.04 LTS (the supported platform, docs/04, ADR-0022) |
| Container images | ghcr.io/fjarrio/fjarr-agent:<ver> (core) and per-vendor variants …:<ver>-zed, …:<ver>-realsense, plus -desktop-x11/-wayland; built from the same .debs, multi-arch, cosign-signed with an SBOM. The core image exists from slice 5b (docker/agent/Dockerfile, built and smoke-tested in CI, amd64, unpublished — docs/12); the variants, arm64 and publishing are this milestone | containerized robot stacks (below), the demo, CI |
| Embedding | libfjarr as a CMake package (find_package(fjarr)), headers = docs/09; the customer’s app links the core and installs the driver packages it wants | robot companies embedding the library in their own daemon |
| Install script | wget -qO- https://get.fjarr.io | sudo sh (wget rather than curl: a fresh Ubuntu desktop has only the former; the script falls back to whichever is present) — adds the repository, installs fjarr-agent, runs fjarr-agent setup | first contact |
The fjarr-agent package installs the introspection viewer’s static
files under /usr/share/fjarr/viewer and points introspect.viewer_dir
there in its shipped fjarr.toml (docs/24);
the container image carries the same directory.
The fjarr-agent package ships the systemd unit (Type=notify,
WatchdogSec=30, Restart=on-failure, running as the unprivileged
fjarr user per docs/10) and enables it; the daemon requires systemd on
robots (ADR-0019).
Pixi (conda-forge/RoboStack), Nix and Yocto follow customer by customer
(ADR-0031). Each renders
the same system profile its own way (a conda package, a
NixOS module, a meta-fjarr layer), and each takes on its own GStreamer
version as a supported, CI-tested row in docs/04.
Packages {#packages}
Section titled “Packages {#packages}”Packages install; setup switches on; --check verifies. Nothing
invasive happens as a side effect of apt install.
| Package | Installs | Switched on by |
|---|---|---|
libfjarr-dev | the static library, headers and the CMake package (find_package(fjarr)) | the customer, for embedding |
fjarr-setup | the setup tool (/usr/lib/fjarr/fjarr-setup, fjarr-agent setup … hands over to it), also as the command fjarr-setup for a host with no agent: a containerized robot’s desktop host | nothing; it is what setup runs |
fjarr-agent | the daemon, linked statically against libfjarr (depends on fjarr-setup); fjarr-agent.service; the fjarr user (sysusers) in video and render; /run/fjarr and /var/lib/fjarr (tmpfiles); /usr/share/fjarr/fjarr.toml.example; the viewer, the driver catalog and the system profile under /usr/share/fjarr/; the boot unit that creates the tunnel device before the robot’s software starts, inert unless fjarr.net is configured | the package enables the service, which starts only once /etc/fjarr/fjarr.toml exists (ConditionPathExists); setup writes that file |
fjarr-desktop-session | the desktop’s host side, with no agent: fjarr-desktop-session, the session helper, and its user unit (ADR-0028); the fjarr-desktop group (sysusers); the GDM watchdog’s timer and service; depends on fjarr-setup | setup desktop (or, on a container host, fjarr-setup setup desktop --container). Ghost screens’ EDIDs are generated by display add-ghost, not shipped |
fjarr-desktop-wayland | the Wayland module (/usr/lib/fjarr/desktop); the agent’s fjarr account into fjarr-desktop (sysusers); recommends fjarr-desktop-session, so apt install on a robot brings both and the agent image only the module | setup desktop |
fjarr-desktop-x11 | the X11 module (/usr/lib/fjarr/desktop); fjarr-x11-session, the kiosk session’s program that grants the agent’s account (xhost +si:localuser:fjarr, through the X API), lays out the outputs and re-lays them on every RandR change, and hands the agent the desktop’s sound (ADR-0034); its XDG autostart entry, installed inert under /usr/share/fjarr/ | setup desktop (links the autostart entry into /etc/xdg/autostart/) |
fjarr-tools | fjarr-connect (given cap_net_admin at install) | nothing |
fjarr-gst-<vendor> | camera drivers, per vendor and architecture | drivers install (with the design partner’s hardware) |
fjarr-inputd is not packaged: none of the chosen desktop backends needs it
(ADR-0006).
Three choices were corrected when the packaging was built (2026-09-28):
- No shared
libfjarruntil the ABI is stable (M6). A C++ shared library would change its SONAME, and so its package name, with every 0.x release. Embedders link the static library fromlibfjarr-dev, and so doesfjarr-agent. /etc/fjarr/fjarr.tomlis written bysetup, not shipped as a conffile. An enabled service with a placeholder config would restart in a loop. The unit waits for the file instead.fjarr-opsimis not shipped. It is a lab tool whose receive side decodes with libav, which is dev and CI only (docs/14). Probing a source isfjarr-agent --probe-source.
The system profile {#the-system-profile}
Section titled “The system profile {#the-system-profile}”A data file in the repository (packaging/profile.toml), installed as
/usr/share/fjarr/profile.toml. Per feature (core, net,
desktop-wayland, desktop-x11, headless), it lists the users, groups,
units, udev rules, kernel arguments and GDM settings that feature needs. It is
used three ways. The package scripts and setup apply it where the OS allows.
--check verifies it on every robot, however it was installed, and names each
missing piece. It is run as root (sudo fjarr-agent --check): a path an ordinary
user cannot look at is reported as permission denied, check as root, never
as missing. Units are the exception in a container: with no systemd
(/run/systemd/system absent) a unit row is satisfied by what does its job
there — the image’s entrypoint for fjarr-net.service, the container runtime
for fjarr-agent.service — and says so rather than reporting it missing
(containerized devices). Later channels (a NixOS module, a Yocto layer) render the same
data declaratively.
Every package declares its architecture and its vendor prerequisites
(Depends/Recommends) so apt does the dependency work where the
vendor publishes packages; where a vendor SDK is an installer behind a
EULA (some stereo-camera SDKs, CUDA), the package declares a virtual
prerequisite and the catalog carries the human steps.
The driver catalog
Section titled “The driver catalog”packaging/catalog.toml in the repository, installed as
/usr/share/fjarr/catalog.toml (also published at https://fjarr.io/drivers/
later, and versioned with the agent), is the single source the tools read.
Shipped 2026-09-28 with the built-in entries only: test, v4l2 and
rtsp, each builtin = true with source naming its type in fjarr.toml
and element what the doctor checks. Vendor entries and their packages are
M3, chosen by the design partner’s hardware; the desktop entries come with the
desktop packages (M3). Vendor entries arrive with the design partner’s hardware. The shape, with the vendor entries as they will look:
[v4l2]title = "V4L2 cameras (USB webcams, UVC, CSI through the kernel)"builtin = truesource = "v4l2"element = "v4l2src"matches = [{ api = "v4l2" }] # gst-device-monitor's device.api
[realsense]title = "Intel RealSense (D4xx, L5xx)"package = "fjarr-gst-realsense"element = "realsensesrc"arch = ["amd64", "arm64"]matches = [{ usb = "8086:0b*" }, { usb = "8086:0a*" }] # udev-visible idsprereqs = ["librealsense2 (from Intel's apt repo, added by the package)", "udev rules: 99-realsense-libusb.rules (installed by the package)"]post_install = "replug the camera or reboot for the udev rules to apply"docs = "https://fjarr.io/docs/drivers/realsense"
[zed]title = "Stereolabs ZED (2, 2i, X, Mini)"package = "fjarr-gst-zed"element = "zedsrc"arch = ["arm64"] # SDK availability decidesrequires_manual = "ZED SDK ≥ 4.x from stereolabs.com (EULA; CUDA on Jetson)"matches = [{ usb = "2b03:*" }]docs = "https://fjarr.io/docs/drivers/zed"
[desktop-x11]title = "Remote desktop on X11 (Xorg)"package = "fjarr-desktop-x11"matches = [{ display = "x11" }]
[desktop-wayland]title = "Remote desktop on Wayland (PipeWire + portal)"package = "fjarr-desktop-wayland"matches = [{ display = "wayland" }]The matches rules are what setup uses to detect hardware; element
is what the doctor checks; arch is why the tool can say “not available
on this machine” instead of failing an install. A usb rule is a vvvv:pppp
id with an optional trailing *; it beats an api rule for the same device
(a RealSense is a v4l2 device too, and the vendor entry is the one that brings
its depth stream). The tool reads the file’s order and nothing else: adding a
vendor is an entry plus a plugin package.
The commands
Section titled “The commands”| Command | Does |
|---|---|
fjarr-agent setup | interactive first run: enrolls (or takes a dev token), detects the display server, lists attached devices (gst-device-monitor-1.0 for v4l2/PipeWire plus udev ids matched against the catalog), proposes fjarr.toml with one track per detected camera, installs the matching packages (apt, with the repository already configured), prints the manual steps for anything behind a EULA, and ends with --check |
fjarr-agent drivers list | the catalog, with per-entry status on this machine: installed / available / not for this architecture / needs manual step; --json for agents |
fjarr-agent drivers install <name> | installs one entry and its prerequisites, prints the post-install steps |
fjarr-agent drivers detect | hardware currently attached, matched against the catalog, with the package each needs |
fjarr-agent --check | the doctor: every configured source and backend with element availability, plus the one-line fix for each missing one (install: sudo fjarr-agent drivers install realsense). Always a desktop row (ADR-0021, slice 2c): available with the backend serving it, or unavailable naming the package to install — and never a failure, because a robot with no desktop is the normal case |
fjarr-agent --probe-source … | bring up one source standalone (docs/09) |
fjarr-agent net setup | the tunnel’s one-time setup: address, fjarr-net.service recreating the device at every boot, optional ROS ordering and DDS file (below, docs/27) |
| fjarr-agent setup desktop | makes a desktop robot reachable unattended (ADR-0006): chooses or creates the auto-login account (no password, no remote login), enables auto-login, the GDM watchdog and the session helper’s user unit; for X11 kiosks the output layout; for headless robots, on request, a forced connector with an EDID on the kernel command line |
| fjarr-agent display list\|add-ghost\|remove-ghost | the robot’s connectors and monitors, and ghost screens for a headless robot, on free root connectors, beside any real monitors (ADR-0032); M3 |
| fjarr-agent setup --undo [<feature>] | reverses every change setup recorded for that feature (setup, net; later desktop: GDM, GRUB, accounts, units); bare, every feature, newest change first |
setup never guesses silently: every proposed track and every install
is shown and confirmed (--yes for provisioning scripts), and the result
is a plain fjarr.toml the customer can read and edit. It applies system
changes only where the OS allows it (Ubuntu with apt). On a system whose
configuration is generated or read-only, it prints the options to set
instead. It never replaces the ecosystem’s package manager. Every change it
makes is recorded, so --undo can reverse it.
The tunnel is the one piece of setup that must run before the robot’s
own software starts, and the one that needs root. The interface is created
once, here; afterwards the agent attaches to it unprivileged, and upgrading
the agent does not disturb anything bound to it
(docs/27). fjarr-agent --check reports
the interface, its address, whether the agent is attached, and whether the
configured range overlaps a route that already exists on the machine.
The setup tool {#the-setup-tool}
Section titled “The setup tool {#the-setup-tool}”Decided 2026-09-28. setup, setup desktop, net setup and drivers are one
small Rust program, fjarr-setup, shipped in the fjarr-agent package at
/usr/lib/fjarr/fjarr-setup. fjarr-agent hands those subcommands over to it
unchanged, so the commands above keep their names. Why a separate tool:
- It is installer UX and system configuration, not device runtime. An embedder of libfjarr never needs it, so it does not grow the C++ daemon.
- It reuses tested code. The boot-time tunnel device is created over
netlink by the same module
fjarr-connectuses for its own persistent device (docs/27). - One prompt library across the operator’s machine and the device:
cliclack.
fjarr-connectmoves from dialoguer to cliclack in the same slice, after its filter mode is confirmed to serve the device picker as well as dialoguer’s fuzzy select does. The repository then carries one prompt library (docs/14).
Rules for every command:
- Say “device”, not “robot”. The tool is for any connected machine. (The
configuration and protocol field
robot_idkeeps its name; renaming it would be a protocol change, taken separately if ever.) - Every prompt has a flag (
--yes,--server,--device-id,--ros-units,--dds none|cyclone|fastdds, …), so a fleet provisions with a script and the prompts are only the friendly face. - Every change is recorded in
/var/lib/fjarr/setup-changes.json, andfjarr-agent setup --undo <feature>reverses exactly those changes. Every change is attempted. One that cannot be reversed is reported and kept in the record for the next--undo, and the command exits non-zero, but the rest are still reversed. The record is saved after each change, so an undo stopped half-way (Ctrl-C) leaves only what is still to do. An accountsetup desktopcreated is removed only after its session has ended; when it does not end (a display manager’s automatic login logs it straight back in), the undo says so and keeps the account in the record. - A replaced file’s previous contents are kept under
/var/lib/fjarr/setup-backups/, and a rerun keeps the first run’s copy. A copy can be gone while the record still names it: an undo before 2026-10-05 that was stopped between restoring a file and saving the record left the mini-PC’s/etc/gdm3/custom.confthat way (M3 gate review, finding 18). Then:-
--undonames the file and the missing copy, and what to do. It leaves the file as it is and keeps the entry, like any change it cannot reverse, and exits non-zero. Its last line lists every change it did not undo. The entry is kept so that the record still says the file is not the original. Dropped, the entry would be forgotten, and the nextsetupwould copy its own earlier output and let--undo“restore” that without a word. -
The next
setupof that feature heals the entry before it changes the file. It keeps a new copy of the file as it is at that moment and says so, so--undoputs that version back. -
When the file holds setup’s own output, a copy would let
--undo“restore” setup’s output, so setup takes none. It says the original is lost and leaves the entry as it is. Setup’s own output is what this run would write, or what an earlier run wrote with other settings, which each file is told by:File An earlier setup’s output when /etc/gdm3/custom.confits automatic login names an account the record says setup desktopcreatedthe dconf database 00-fjarr-desktopit starts with setup’s header the dconf profile /etc/dconf/profile/userit has system-db:local(setup writes the profile only when that line is missing)/etc/default/grub.d/fjarr-ghosts.cfgit has a # fjarr-ghosts:linenet setup’s drop-ins and Cyclone DDS filethey say Written by `fjarr-agent net setup`/etc/tmpfiles.d/fjarr-container.confit starts with setup’s header /etc/fjarr/fjarr.tomlnever: it is the customer’s file, edited in place, so only this run’s output counts The way out is the person’s, never setup’s guess. Either put the original back by hand (for a package’s file, the version it shipped) and run that
setupagain, which copies it; or, when the file as it is now is its original (an automatic login set up by hand exactly as setup writes it, say), copy it to the kept copy’s name with the command both setup and--undoprint,sudo cp -p <file> <kept copy>. Either way,--undorestores it afterwards. No flag does this, so--yesnever can.
-
- The configuration file stays the customer’s. Setup adds and removes only
its own keys, prunes the tables they leave empty, and takes a table written
inline (
"fjarr.desktop" = {}) like any other. - System changes are applied only on Ubuntu with apt. Elsewhere the tool prints the options to set. It never replaces the ecosystem’s package manager.
- Each command ends with
fjarr-agent --check, whose profile rows verify what it did.
Built 2026-09-28: net setup, net up and setup --undo net
(signaling/crates/fjarr-setup), shipped in fjarr-agent with
fjarr-net.service, and proven on the spike machine; then, the same day,
setup (the first run, below), drivers list|detect|install
with the built-in catalog (below) and the bare
setup --undo, proven on the spike machine against the lab server and in the
package install test. setup desktop and display followed in M3 slice 3.3 (2026-10-01, below).
fjarr-connect still prompts with dialoguer; its move to cliclack is owed
(docs/14).
fjarr-agent setup
Section titled “fjarr-agent setup”The first run, which the install script calls. It detects the machine (OS,
architecture, hardware H.264 encode, display server, cameras via
gst-device-monitor-1.0 matched against the catalog), then asks:
┌ fjarr setup│◇ This device: Ubuntu 26.04 · amd64 · VA-API H.264 ✔ · GNOME on Wayland│◆ Where does it connect?│ ● Your own fjarr-server ○ Fjarr Cloud◇ Server: wss://fleet.acme.com/ws◇ Device id: dev-024 (from the hostname)◆ Device token (from your dashboard): ••••••••◇ Token accepted by the server ✔ stored in /etc/fjarr/fjarr.toml (0640, root:fjarr)│◆ Cameras found. Which should stream?│ ◼ Front Logitech C920 (usb-046d_C920…) mjpeg 1280×720@30│ ◻ Depth Intel RealSense D435 → needs the realsense driver◆ Install the realsense driver? (adds Intel's apt repository) Yes│◆ Also set up:│ ◼ Terminal → as which account? operator│ ◼ Tunnel → runs `net setup` next│ ◻ Remote desktop → runs `setup desktop` next│◇ /etc/fjarr/fjarr.toml written · fjarr-agent started · online ✔└ fjarr-agent --check: all rows ok · undo: fjarr-agent setup --undoUntil M5 builds enrollment (docs/17, docs/10),
setup asks for the device token the server accepts today and writes it
to the configuration. The prompt becomes a one-time enrollment token, redeemed
for the per-device key, when M5 lands. The flow around it does not change.
The “Fjarr Cloud” choice joins the server prompt with M7; today the prompt is
the server URL. Installing a vendor driver from this flow arrives with the first
vendor entry (with the design partner’s hardware): a detected camera that needs one is shown with its entry and left
unchecked.
What it does, in order, as built 2026-09-28 (fjarr-setup’s setup.rs):
- Detects: OS and architecture (os-release), hardware H.264 encode (the
line
fjarr-agent --checkprints, so the encoder the agent will probe is the one reported), the graphical session (loginctl), and the cameras (gst-device-monitor-1.0 Video/Source, matched against the catalog by udev usb id anddevice.api). - Asks the server URL, the device id (default: the hostname, or the
existing config’s), and the device token (a password prompt; an existing
token is offered to keep). Before anything is written it opens a TCP
connection to the server’s host and port: a typo or a closed port fails
here, with nothing changed, and
--offlineskips the check for a device provisioned before its uplink exists (offline is a normal state, ADR-0019 addendum; a first run that cannot be checked is not a finished setup, so it is said, not assumed). - Decides the encoder with the agent’s answer:
autowith hardware encode; otherwisesoftware, confirmed, because the agent has no silent fallback and would refuse to start (docs/23). - Proposes one track per camera that a built-in source serves, pre-selected;
the id is a slug of the camera’s name (unique among the file’s tracks), the
label its name, the source
{ type = "v4l2", device = <by-id name> }with the mode picked as MJPEG 1280×720@30 when the camera has it, else the largest MJPEG mode up to 1080p at ≥ 15 fps, else the largest raw mode (docs/06’s format; editable). - Asks the terminal’s account, with no default (docs/06):
the agent’s own account works now; another account is written as chosen,
with the warning that the terminal reports
unavailableuntil the agent runs as it.--yeswithout--terminalconfigures none. - Writes
/etc/fjarr/fjarr.toml,0640 root:fjarr: created plain when absent, edited in place when present (comments, order and the customer’s other keys kept; a track with the same id is replaced). The whole file is recorded, with its previous contents beside the record, so--undoputs back exactly what was there. - Starts the agent (
systemctl restart fjarr-agent.service; enabled by the package, enabled here if it was not) and waits for itsSTATUS=online(ADR-0019, second addendum;--timeout, 30 s) — the server’s acceptance of the token is the agent’s own word, not a second handshake in the tool. A failed or restarted unit ends the wait with the journal’s reason; a timeout says the configuration stays and the agent keeps trying. Without systemd (a container) the agent is not started and the tool says how to run it. - Ends with
--check, or hands over tonet setupwhen the tunnel was chosen (--net yes, with--ros,--ros-units,--ddspassed through).
The flags, one per prompt: --yes, --server, --device-id, --token (or
FJARR_DEVICE_TOKEN, for scripts that keep it off the command line),
--offline, --encoder auto|vaapi|software, --cameras all|none|<device,…>
(by-id names or /dev paths, as drivers detect lists them), --terminal none|<account>, --net yes|no, --timeout. What it leaves on the device,
recorded for --undo:
| Where | What |
|---|---|
/etc/fjarr/fjarr.toml | agent.robot_id, agent.server_url, agent.dev_token, media.encoder, one capabilities."fjarr.camera".tracks.<id> per chosen camera, capabilities."fjarr.terminal" when an account was chosen; 0640 root:fjarr |
/var/lib/fjarr | created 0700 fjarr when the service has not run yet |
fjarr-agent.service | started; recorded as enabled only when the package had not already enabled it, as started only when it was not running |
/var/lib/fjarr/setup-changes.json | the record, feature setup; the file’s previous contents under setup-backups/ |
The package install test (packaging/install-test.sh) runs the scripted form
on a clean Ubuntu 26.04 without systemd and a server: the first run fails
before writing anything and names --offline; --offline writes the file the
agent’s --check then passes on; --undo removes it and the check names
setup again.
fjarr-agent net setup
Section titled “fjarr-agent net setup”The tunnel’s one hard rule is that its device exists before any software
that should use it starts (docs/27#lifecycle), because DDS picks its interfaces
when a participant is created. net setup derives or takes the address, checks
the range against existing routes, enables fjarr.net in the config, and
installs fjarr-net.service: a oneshot that recreates the device at every
boot (owned by fjarr, at the chunk MTU, before fjarr-agent), because a
tun device does not survive a reboot.
ROS and DDS are an explicit question, never detected. ROS often runs in a
container, invisible from the host, so detection would answer “no” on exactly
the machines that need the ordering. “No” skips the ordering and DDS steps
(--ros no); “yes” asks which units start it and which DDS it uses:
┌ fjarr net setup│◇ Device id: dev-024 → tunnel address 100.70.118.224 (derived)◇ Range 100.64.0.0/10 is free on this machine ✔│◆ Does software on this machine use ROS 2 / DDS over the tunnel?│ ● Yes ○ No│◆ Which services start it? They must start after the tunnel.│ ◼ bringup.service│ ◼ docker.service (ROS in containers: they start with the runtime)│◆ Which DDS?│ ● Cyclone DDS (writes /etc/fjarr/cyclonedds.xml, LAN interface enp3s0)│ ○ Fast DDS (no file needed)│ ○ None│◇ fjarr-net.service installed · 1 ordering drop-in · config updated└ fjarr-agent --check: all net rows ok · undo: fjarr-agent setup --undo netThe ordering is a systemd drop-in per chosen unit — After= and Wants= of
both fjarr-net.service (the device exists) and fjarr-agent.service (the
agent has attached it, which is when it has carrier: the condition a Fast DDS
participant needs, docs/27#lifecycle) — which is the ordering rule applied to
the customer’s own services. The wait is bounded by the robot, not the WAN:
the agent’s READY comes once its capabilities are configured and its loop
runs, before any server is reached
(ADR-0019, second addendum).
When ROS runs in containers, the unit to order is the container runtime
(docker.service) or the unit that starts the compose stack, and the containers
need host networking to see fjarr0 (as in
containerized devices).
What it leaves on the device, each recorded for --undo net:
| Where | What |
|---|---|
/etc/fjarr/fjarr.toml | capabilities."fjarr.net".enabled = true, and address when --address pinned it; only those keys, comments and order kept |
fjarr0 | created now, owned by the agent’s account (the profile’s [net] owner), <address> peer 100.64.0.1, at the chunk MTU |
fjarr-net.service | enabled and started; at every boot it runs fjarr-setup net up, which reads the config, asks fjarr-agent --net-address and recreates the same device (a no-op when it is already there) |
/etc/systemd/system/<unit>.d/50-fjarr-net.conf | the ordering drop-in, one per --ros-units entry: after fjarr-net.service and fjarr-agent.service |
/etc/fjarr/cyclonedds.xml | with --dds cyclone: docs/27’s file with the tunnel and the LAN interface (the default route’s, or --lan-interface); the ROS 2 processes take it through CYCLONEDDS_URI |
/var/lib/fjarr/setup-changes.json | the record, with a replaced file’s previous contents beside it under setup-backups/ |
The flags: --yes, --ros yes|no (required without a terminal: it is never
detected), --ros-units a.service,b.service, --dds cyclone|fastdds|none,
--address, --lan-interface. A running agent is restarted at the end so it
attaches now; a stopped one is left alone. It ends with fjarr-agent --check,
whose net rows verify the unit and the device.
Before 2026-09-28 the drop-in ordered after the device alone, because
READY waited for the first hello-ack and ordering after the agent would
have held a robot’s bringup on the WAN; that was #32,
closed by the ADR-0019 addendum above.
fjarr-agent setup desktop
Section titled “fjarr-agent setup desktop”Built in M3, with the desktop packages (moved from M2.5 on 2026-09-28): it configures the session helper and backend M3 builds. The appliance pieces the M2 spikes showed a desktop device needs (ADR-0006, ADR-0028):
┌ fjarr setup desktop│◇ Session: GNOME 50 on Wayland under GDM → backend: mutter◇ Monitors: 1 (DELL U2422H on HDMI-1)│◆ Which account should the desktop run as? It logs in by itself at boot.│ ● Create "desktop" (no password, no remote login) ○ Use existing: operator│◇ Auto-login enabled for desktop in GDM◇ GDM watchdog enabled (restarts the login if the session dies)◇ Session helper enabled for desktop · group fjarr-desktop│◆ Ghost screens for when no monitor is attached? (free: DP-2, HDMI-A-2)│ ○ None ● 1 ○ 2 --ghost-screens N · change later: fjarr-agent display│▲ desktop must log in once for its group to apply. Reboot now? Yes / Later└ Undo: fjarr-agent setup --undo desktopWhat it writes, each change recorded for setup --undo desktop (specified
2026-10-01 for M3 slice 3.3):
| Where | What |
|---|---|
| the account | created when asked for: useradd --create-home --user-group, password locked (no login but the automatic one), added to fjarr-desktop. An existing account is only added to the group |
/etc/gdm3/custom.conf | [daemon] AutomaticLoginEnable=true, AutomaticLogin=<account>; the file’s other keys are kept |
/etc/dconf/db/local.d/00-fjarr-desktop and /etc/dconf/profile/user | no screen lock and no idle blanking (org/gnome/desktop/screensaver lock-enabled=false, org/gnome/desktop/session idle-delay=0), then dconf update. An auto-login account has no password, so a lock screen would strand the robot. This applies to every account on the machine, which suits an appliance and is said in the prompt |
~<account>/.config/systemd/user/graphical-session.target.wants/fjarr-desktop-session.service | the helper’s user unit, enabled for that account only. It starts with the graphical session, restarts on failure, and connects to /run/fjarr/desktop.sock |
~<account>/.config/systemd/user/gnome-initial-setup-first-login.service and …/gnome-initial-setup-upgrade-login.service | links to /dev/null: GNOME’s “Welcome to Ubuntu” wizard is masked for the account. Unmasked, it starts in the middle of the robot’s screen, over whatever the robot shows, and stays until someone at the machine clicks through it (found by the M3 gate review, 2026-10-11). Ubuntu starts it from gnome-session.target, one unit per mode: --existing-user while the account has no ~/.config/gnome-initial-setup-done, and --upgrade-user once it has one, until ~/.config/gnome-initial-setup/upgrade-<release>-done exists. Masking both keeps it off whatever the done file says, and after a release upgrade, where writing this release’s marker would not. The masks apply from the account’s next login. A file already at either path, the account’s own mask or override, is left alone and not recorded, and setup names an override, as the wizard can start through it |
~<account>/.config/gnome-initial-setup-done | yes, as GNOME’s wizard leaves it when finished. On its own (2026-10-11 to 10-17) it only traded the first-login mode for the upgrade mode, which then started at every boot of the mini-PC’s account, recreated each night (found 2026-10-17). Kept beside the masks because GNOME’s copy worker (gnome-initial-setup-copy-worker.service) runs at every login while it is missing, with nothing to copy into a robot’s account. Left alone when the file exists |
fjarr-desktop-watchdog.timer | enabled. Every 30 s it runs fjarr-setup desktop watchdog. When GDM is active and two checks in a row find no user session at all on seat0, only GDM’s own greeter, the watchdog restarts GDM, which logs the account in again, and says so in the journal (ADR-0006). A person logged in at the machine holds the seat and is never logged out by it (found on the mini-PC, 2026-10-01: the first version watched for the account’s own session, and would have ended the session of whoever was at the machine) |
/etc/fjarr/fjarr.toml | [capabilities."fjarr.desktop"] helper.user = "<account>", helper.group = "fjarr-desktop"; then the agent restarts |
Built 2026-10-01 (fjarr-setup’s desktop.rs): the package install test
runs setup desktop and --undo desktop on a clean Ubuntu, and checks each file,
the account and the keys; since 2026-10-17 also for an existing account, whose
home keeps everything setup did not add. The automatic login, the watchdog and
the reboot are proven on the mini-PC in 3.L.
The wizard did not cause the nightly’s input-to-photon 0/10 (corrected 2026-10-17 from “over the nightly’s test window”). The first-login wizard also ran on 2026-10-02 and 10-04, nights with ten real samples, and on 10-13 and 10-14, with the upgrade-mode wizard running, the clicks and the closing Shift release still reached the test window. The 10-14 frame dump is the full-screen test window, not the wizard (M3 gate review, finding 2).
The group applies at the account’s next login, so the command ends by offering
a reboot. --check adds rows for the module, the account and its group, the
auto-login, the watchdog and each ghost screen.
On an X11 kiosk it switches on fjarr-x11-session instead (its autostart
entry, linked into /etc/xdg/autostart/) and writes display = ":0" (or
--display) under [capabilities."fjarr.desktop"]. It does not touch
the display manager or the automatic login: the kiosk session is the robot
maker’s. For the desktop’s sound (ADR-0034)
it needs the kiosk account: --account, else LightDM’s autologin-user. It
adds that account to fjarr-desktop and records it as helper.user and the
group as helper.group, as on GNOME (all undone by --undo desktop); with
no account found it says the sound is off and how to turn it on. Advice, from ADR-0006’s spikes: LightDM with an automatic login is
the easy X11 kiosk; under GDM the X session loses the screen to a Wayland
greeter about 11 s after an automatic login, and needs a boot-time
loginctl activate. --check adds rows for the X11 module and the kiosk
session’s autostart entry ([desktop-x11] in the profile); a session that
did not grant the agent shows in the agent’s log (“cannot open X display”)
and to every operator as desktop: absent (docs/08). With no monitor, or with --ghost-screens N, it offers
ghost screens on the free connectors, and says a reboot is
needed.
Ghost screens {#ghost-screens}
Section titled “Ghost screens {#ghost-screens}”A headless robot often wants screens its applications lay windows out on, with nobody’s monitor attached. A ghost screen is a forced DRM connector carrying a Fjarr-generated EDID (“Fjarr Ghost N”, its own serial, the requested mode). It exists from boot, whether or not anyone is connected, and GNOME and the capture backends treat it like any monitor (ADR-0032; measured on the mini-PC with three at once). Real monitors can be plugged in beside ghosts, and the two never hide each other:
- Free root connectors only. A ghost goes on a connector that is
disconnectedwhen it is added, never on one a real monitor uses. Forcing a used connector would replace that monitor’s EDID with the ghost’s. - Never a DisplayPort MST branch. A daisy-chain’s
DP-1-1,DP-1-2… are created at runtime, so a boot-time kernel parameter cannot name them. Real monitors on a chain are fully supported; the root connector the chain hangs off counts as in use while the chain is plugged in. - A distinct identity. A ghost’s monitor id is its EDID slug, unique by
construction, so a ghost and a real monitor never share a
track_id(docs/09).
fjarr-agent display lists every connector and manages ghosts, as root, and setup desktop --ghost-screens N
uses the same code:
$ sudo fjarr-agent display list CONNECTOR STATE MONITOR GHOST HDMI-A-1 connected DELL U2422H (real) — DP-1 connected MST chain: 2 monitors — (not forceable: chain) DP-2 free — can be a ghost HDMI-A-2 ghost Fjarr Ghost 1 1920×1080 active
$ sudo fjarr-agent display add-ghost [--connector DP-2] [--mode 1920x1080@60] DP-2 → Fjarr Ghost 2 (1920×1080@60) · takes effect after a reboot$ sudo fjarr-agent display remove-ghost <connector|all>What a change writes, each recorded for setup --undo desktop:
| Where | What |
|---|---|
/etc/default/grub.d/fjarr-ghosts.cfg | video=<connector>:<mode>e drm.edid_firmware=<connector>:edid/fjarr-ghost-N.bin per ghost, then update-grub |
/usr/lib/firmware/edid/fjarr-ghost-N.bin | the generated EDID |
Built 2026-10-01 (M3 slice 3.3, fjarr-setup’s display.rs): connectors
are read through DRM ioctls, so a DisplayPort chain’s root is recognised by its
members’ PATH property (mst:<root>-<port>) even while it reads as
disconnected. The EDID is generated as EDID 1.4 with the mode’s CVT
reduced-blanking timing, and di-edid-decode reports it conforming. The GRUB
snippet’s # fjarr-ghosts: line is the one record of which ghosts exist. Proving
it on hardware (a reboot into the ghosts, a chain beside them) is 3.L’s, as was
open question #37: telling that a real monitor is on a
ghost connector (answered the same evening: over DDC, above). On the mini-PC (amdgpu, 2026-10-01) the ghost came up
from the first reboot with the EDID only in /usr/lib/firmware/edid, because
amdgpu is not in that machine’s initramfs and loads after the root filesystem
is mounted. A machine whose initramfs carries its GPU driver would need the
EDID inside it too. add-ghost should check with lsinitramfs and add an
initramfs hook when it does (owed).
add-ghost without --connector takes the next free root connector, and when none
is left it says how many ghosts this machine can have instead of failing after
a reboot. --check adds rows for each configured ghost: active when the
running kernel booted with it, reboot pending when it did not, and a failure
when a real monitor is plugged into a ghost connector. That monitor shows up as
the ghost, because the kernel forces the ghost’s EDID on that port, so both read
the monitor’s own EDID over the connector’s DDC bus instead (i2c-dev, which
fjarr-desktop-wayland loads at boot). A sink that answers there is a real
monitor (#37, measured on the mini-PC).
A ghost is never the primary monitor while a real one is connected. On a
desktop account with no saved layout, mutter picked one of the ghosts as primary
(the mini-PC, 2026-10-01, with a three-monitor chain beside it). The robot’s own
applications open on the primary monitor, and an operator’s view follows it. So
fjarr-desktop-session checks every layout change: when the primary is a ghost
(manufacturer FJR) and a real monitor is connected, it makes the leftmost real
monitor primary through mutter’s ApplyMonitorsConfig, with every position, mode
and scale kept, persistently, so the account’s monitors.xml remembers it. With
only ghosts connected, a ghost is primary, as it must be. It asks at most once
per layout, and not at all when a real monitor is primary already; it sends the
agent the current monitors whether or not it asked; and when mutter reports more
than one primary, the agent hears exactly one (a real monitor first, then the
leftmost). The M3 gate review found why (the mini-PC, 2026-10-11): after a Dell
came back under a new connector, mutter reported a ghost and the Dell both
primary, the helper asked again on every change, eight times a second, and each
new layout reallocated every framebuffer until the out-of-memory killer ended the
session.
Ghosts and real monitors share the GPU’s display pipes. The mini-PC’s GPU drives four; with two ghosts and the three-monitor chain (five), mutter lit one monitor, a ghost, and left the Dells dark. Count the real monitors a robot may have before adding ghosts.
A virtual monitor (mutter’s RecordVirtual) is the other kind: an extra
screen for one session, created when the operator asks and gone when they
leave. It needs no reboot and no connector, and promises no persistence
(docs/22).
fjarr-agent drivers
Section titled “fjarr-agent drivers”Mostly output, for people and scripts alike (--json):
$ fjarr-agent drivers list NAME STATUS PACKAGE NOTE v4l2 built in — source = { type = "v4l2", … } in fjarr.toml (docs/06) realsense available fjarr-gst-realsense needs Intel's apt repository (added for you) zed needs manual fjarr-gst-zed SDK behind a EULA: see the link jetson-csi not for amd64 fjarr-gst-argus available on arm64
$ fjarr-agent drivers detect /dev/video0 Logitech C920 → v4l2 (built in) mjpeg 1280×720@30 usb 8086:0b07 Intel RealSense D435 → realsense (available)The status per entry is built in, installed (dpkg’s view of the package),
available, not for <arch> or needs manual, in that precedence; --json
gives { "arch", "catalog", "drivers": [ { name, title, status, package, element, note, docs } ] } for list and { "arch", "devices": [ { name, path, api, usb_id, serial, modes, stable_name, driver, status } ] } for
detect, where stable_name is the by-id name a v4l2 source takes and
modes the caps the monitor reported. Neither needs root. detect without
gst-device-monitor-1.0 is an error naming gstreamer1.0-plugins-base-apps,
which the fjarr-agent package depends on for exactly this.
fjarr-agent drivers install v4l2 says the entry is built in and exits 0.
sudo fjarr-agent drivers install realsense will show the prerequisites, ask
before adding a vendor repository, install, reload udev, and finish with a
--probe-source of the device it found — with the first vendor entry
(with the design partner’s hardware);
until then it says so and exits non-zero.
Containerized robots {#containerized-robots}
Section titled “Containerized robots {#containerized-robots}”A reference compose file ships with each release and states what the agent needs from the host:
| Need | In compose | Why |
|---|---|---|
| WebRTC | network_mode: host | bridged networking hides the robot’s addresses from ICE and adds a NAT hop |
| Encoder, cameras | devices: /dev/dri, /dev/video*; group_add with the host’s render GID | group ids differ per host |
| Config and device key | volumes for /etc/fjarr and /var/lib/fjarr (0700) | the key survives image upgrades |
| Tunnel | user: "0", cap_add: NET_ADMIN, /dev/net/tun, host networking | the container’s entrypoint does what fjarr-net.service does on an apt device — fjarr-setup net up creates fjarr0 owned by fjarr — then drops to fjarr with no capabilities before the agent starts, so the agent itself never holds CAP_NET_ADMIN (docs/27, rule 1); it runs on every container start, which is every boot. ROS containers wait for carrier on fjarr0 before starting ROS (rule 2): compose’s depends_on orders only docker compose up, not the daemon restarting containers after a reboot |
| Desktop | the image …:<ver>-desktop-wayland; the host’s /run/fjarr bind-mounted, where the agent listens on desktop.sock; group_add with the host’s fjarr-desktop gid; the host side from the fjarr-desktop-session package (below) | the helper has to run inside the desktop user’s session; descriptors cross a bind-mounted socket unchanged |
The reference file is packaging/compose/docker-compose.yml, published with
each stable release at https://apt.fjarr.io/docker-compose.yml (its image tag
is latest unless FJARR_VERSION pins one). It is written for one job — the agent next to the customer’s own
containers — and the first run is the setup tool inside the image, which writes
the configuration into the volume and, having no systemd, says so: it starts no
agent, and net setup installs no fjarr-net.service (the entrypoint below
does its job) and writes no drop-ins:
docker compose run --rm fjarr-agent setup --server wss://… --device-id dev-024 --net yes --ros nodocker compose up -ddocker compose exec fjarr-agent fjarr-agent --checkTunnel only, on a machine with no GPU for the agent or no /dev/dri
(a Jetson, whose encoder is not VA-API): delete the /dev/dri device and
group_add. Docker refuses to start a container whose listed device is
missing, and a tunnel needs neither. Keep user: "0", cap_add,
/dev/net/tun and host networking. The host needs no Fjarr package, only a
Docker that knows the clone3 syscall (20.10.10 or later), because the
image’s glibc uses it.
The image’s entrypoint runs the setup tool as root (setup, net, drivers);
for the agent it creates the tunnel device when fjarr.net is enabled and then
setprivs to fjarr — no capabilities, keeping the supplementary groups
compose added (group_add render) along with fjarr’s own. Started as fjarr
(the image’s default user, as without the tunnel) it runs the agent directly.
The agent reads /etc/fjarr/fjarr.toml whenever it exists and no --config
is given — on an apt device too, so fjarr-agent --check checks what setup
wrote — and the file sets FJARR_AGENT_ALLOW_UNSUPERVISED=1 beside
restart: unless-stopped: the runtime is the supervisor ADR-0019 asks for.
A ROS container keeps its image’s entrypoint behind a wait for carrier:
entrypoint: ["/bin/sh", "-c", "until [ \"$$(cat /sys/class/net/fjarr0/carrier 2>/dev/null)\" = 1 ]; do sleep 0.5; done; exec /ros_entrypoint.sh \"$$@\"", "wait-for-fjarr0"]Carrier is up exactly while the agent is attached, which is the condition a
participant needs — so the wait holds however the container was started,
including by the daemon after a reboot in whatever order it restarts them.
make compose-gate proves the file: setup in the image, --check, a ROS
container started before the agent that waits and then finds the tunnel
address, an agent restart the device survives, and real IP over the tunnel
from an operator.
The image runs as fjarr with a fixed, published uid: 10001. The Dockerfile
creates the user before installing the package, and the package’s sysusers
entry then only adds video and render; on an apt robot the uid is dynamic.
The image’s health check is live: the running agent’s introspection
endpoint answers. That says more about a running container than a static
check of its files (corrected 2026-09-28 from a --check --health that was
never built). The agent checks the helper’s uid (ADR-0028; corrected
2026-10-05 from “the helper checks the agent’s”), and the helper’s socket
belongs to the host’s group, so user-namespace remapping (rootless Docker,
userns-remap) is either off or its mapped uid is configured. On an X11
kiosk, /tmp/.X11-unix is bind-mounted, the container shares the host’s IPC
namespace (ipc: host: ximagesrc reads through MIT-SHM and captures stale
frames without it), and the package creates a host account with the image’s
uid so the xhost grant can name it.
A desktop in a container {#a-desktop-in-a-container}
Section titled “A desktop in a container {#a-desktop-in-a-container}”GNOME (backend E) with the agent in a container (M3 slice 3.8). The desktop
stays the host’s: its account, GDM’s automatic login and the session helper
are host things, and the agent reaches the helper through a socket in a
bind-mounted /run/fjarr, exactly as on an apt robot. The host has no agent,
so it installs only the host side:
sudo apt install fjarr-desktop-sessionsudo fjarr-setup setup desktop --container [--account desktop] [--ghost-screens N]--container does what setup desktop does on the host (the account, the
automatic login, no screen lock, the helper for that account only, the
watchdog, ghost screens), writes no agent configuration (there is none on the
host), and makes /run/fjarr the container agent’s (uid 10001, a tmpfiles
drop-in, recorded for --undo). It ends by printing the two numbers the
container needs, which the host owns and the image cannot know: the desktop
account’s uid and the fjarr-desktop group’s gid. The container side:
# docker-compose.yml, the fjarr-agent serviceimage: ghcr.io/fjarrio/fjarr-agent:${FJARR_VERSION:-latest}-desktop-waylandvolumes: - /run/fjarr:/run/fjarr # the helper's socketgroup_add: ["${RENDER_GID:-109}", "${FJARR_DESKTOP_GID}"]docker compose run --rm fjarr-agent setup desktop --helper-uid <uid> --helper-gid <gid>which writes capabilities."fjarr.desktop".helper.uid and .gid (numbers,
not names: the image has neither the account nor the group). The agent
accepts a helper only from that uid (SO_PEERCRED), and gives its socket to
that gid, 0660. An X11 kiosk with the agent in a container is described in
the table above and is not built yet.
Repository, versions and releases {#releases}
Section titled “Repository, versions and releases {#releases}”-
The repository is static files on Cloudflare R2 at
apt.fjarr.io(bucketfjarr-apt), built from debhelper packages made inside Ubuntu 26.04 containers on native amd64 and arm64 runners (dpkg-shlibdepsderives the dependencies). Its shape (corrected 2026-09-28 from “components”, and from reprepro/aptly, when it was built):Path What pool/main/f/fjarr/*.debevery released package, shared by both channels; nothing is removed dists/testing/,dists/stable/one suite per channel: Release,InRelease,Release.gpg, andmain/binary-{amd64,arm64}/Packages{,.gz}fjarr-archive-keyring.ascthe public key, beside the repository it signs install.shthe install script ( get.fjarr.ioredirects here)docker-compose.ymlthe reference compose file of the stable release The indexes are generated with
apt-ftparchive(packaging/repo/) from the pool, each suite listing exactly the packages of the version it carries. So promotion tostableis regeneratingstable’s index over files already in the pool, which cannot be a rebuild. There is no repository database to keep in step with the bucket. A robot’s source (/etc/apt/sources.list.d/fjarr.sources, deb822) names the suite and the key:Types: deb,URIs: https://apt.fjarr.io,Suites: stable,Components: main,Signed-By: /etc/apt/keyrings/fjarr.asc. -
The signing key is held in the release environments’ secrets. The key’s custody and yearly rotation are documented beside the release runbook, and the install script carries its fingerprint. The current key: Ed25519, fingerprint
B376164F0985CFDB0DE7C26C839E1ECA17D3B8F1, created 2026-09-28, expiring 2028-09-27 (replaced yearly with a year of overlap), public half atpackaging/fjarr-archive-keyring.asc. The private half exists only in theapt-testingandapt-stableGitHub environments and offline; its revocation certificate is kept offline with it. -
One version for every artifact, from one tag
vX.Y.Z: packages, images,fjarr-connect, thefjarr-serverimage, crates and npm packages. The wire protocol keeps its own major (docs/08). 0.x until the extension API is stable (M6).make set-version V=X.Y.Zsets it everywhere it is declared (CMake, which the agent reports; the Cargo workspace and its lockfile; the npm packages;debian/changelog). That is committed, then tagged, and the release workflow refuses a tag that does not match every one of them. -
A release is: tag → the tagged commit’s CI run, waited for and required green → its packages (never rebuilt, docs/30) and images built from them to
testing→ a manual, protected promotion tostablethat copies the same artifacts and never rebuilds them (.github/workflows/release.yml; environmentsapt-testingandapt-stable, the second requiring approval, both deployable only fromv*tags). Images go toghcr.io/fjarrio/fjarr-agentas one multi-arch manifest per version, taggedX.Y.Zandtesting, withlatestmoved on promotion; each is signed with cosign (keyless, GitHub’s identity) and carries an SBOM.install.shand the referencedocker-compose.ymlare uploaded on promotion. The crates and npm packages join the promotion once their registries are set up; no token for either exists yet. Release notes come from the conventional commits since the last tag. -
Building locally:
make debbuilds the packages for the host’s architecture in a throwaway builder container (docker/deb-builder) intodist/deb/<arch>/.make deb-install-testinstalls them on a clean Ubuntu 26.04 and checks every promise in the table above (packaging/install-test.sh).make deb-embed-testbuildsdemo-robotout of tree against the installedlibfjarr-devalone (find_package(fjarr)), which is what a customer’s CMake project does. CI runs all three on native amd64 and arm64 runners. -
The install script (
get.fjarr.io,packaging/install.sh) only does what apt cannot do by itself. It detects Ubuntu 26.04 and the architecture, fetches the key and refuses it unless its fingerprint is the one the script carries, writes the source above, installsfjarr-agentand runssetup(reading the terminal, since its own stdin is the script). On anything else it refuses, naming the supported systems and the container route.--dry-runprints every step without doing it;--channel testingpicks the other suite.
In the dashboard and the fleet view
Section titled “In the dashboard and the fleet view”The introspection endpoint’s /sources (docs/24)
carries the same status per source (available, missing: fjarr-gst-realsense,
not-for-arch, needs-manual: ZED SDK), so the demo dashboard’s robot
page shows a “driver missing” badge with the install command instead of a
black tile, and Fjarr Cloud (M7) shows it fleet-wide — “3 robots configured
for RealSense without the driver” — and, once OTA campaigns exist (M8),
can push a driver package to a robot group like any other update.
What stays honest
Section titled “What stays honest”- Fjarr does not redistribute SDKs it is not licensed to redistribute; the catalog says so and links the vendor’s download, and the tool verifies the SDK is present before installing the plugin package.
- A driver that fails after install (kernel module, permissions) shows up
as a source error with the bus message, not as a generic media failure —
the
--probe-sourceoutput is the support ticket. - The catalog is data: adding a vendor is a catalog entry plus a plugin
package, never a change to
fjarr-agent.
Roadmap
Section titled “Roadmap”Packaging is the M2.5 milestone (docs/17):
the repository, the packages and their units, the system profile, the images
and the reference compose file, the install script, the release pipeline,
setup (with --undo), drivers and the catalog with
the built-in entries; setup desktop and the desktop packages are M3 (ADR-0031). The first
vendor packages come with the design partner’s hardware. Slice 3
already ships the runtime half: unavailable with reason, --check,
--probe-source, /sources.