Files
vdm/packaging/systemd/README.md
T
samiandClaude Sonnet 5 eb72aa522c daemon: build velox-nmhost, systemd socket activation + units, velox(1) man page
Closes build order items 7 (the systemd half) and 9 (D11 in deferrals.md).

velox-nmhost (nmhost/src/main.cpp, 185 lines): a poll()-driven byte pump between
Firefox's native-messaging framing on stdio (4-byte native-byte-order length prefix)
and veloxd's own NDJSON framing on the Unix socket. Reframes each direction, no JSON
parsing, no retry/backoff (the extension relaunches a fresh host on its own
reconnect), exits the moment either side closes. Deliberately dependency-free — no
veloxd_* library, no nlohmann_json — since it runs unconfined outside Firefox's
sandbox regardless of packaging format.

Two real bugs found and fixed while getting the integration test to actually pass
rather than hang, both exactly the class of bug a "trivial pump" invites:
1. Never set the pumped fds non-blocking, so the "drain what's available" read loop
   blocked on its own second read() instead of returning to poll().
2. stdin and stdout are two different descriptors (0 and 1), not one — an early draft
   polled POLLOUT on fd 0, which is opened read-only, so EOF and writability were
   never both observable through the same pollfd entry.

packaging/nativehost/com.velox.host.json + its own README.md supersede
AGENT-DAEMON.md's stale "four locations" line: spike S1 / ADR 0003 found only three
manifest locations are real (~/.mozilla/native-messaging-hosts/ for BOTH deb/tarball
and snap Firefox, /usr/lib/mozilla/... for deb/tarball only, the flatpak sandbox path)
— the fourth, ~/snap/firefox/common/.mozilla/..., is not read by snap Firefox at all.
The README spells out the per-user-manifest / postinst enumeration implication for
PKG/QA (postinst runs once as root; the two ~/-relative locations are per-user) and
flags that docs/07-packaging.md's own install-layout line only shows the one
root-owned path.

Socket activation: rpc/systemd_activation.cpp is a from-scratch sd_listen_fds() (env
vars only — LISTEN_PID/LISTEN_FDS, fd 3 — no libsystemd link) that UdsServer::start()
checks first, skipping its own create/bind/chmod/listen when systemd already bound
the socket. packaging/systemd/velox.socket + velox.service are the unit pair,
verified both by systemd-analyze verify and by an actual fork/dup2/execve simulation
of the activation handshake — a real session.hello round-tripped over the handed-off
fd with no bind() ever called inside the daemon for that run. velox.service
deliberately skips ProtectSystem=/ProtectHome=/ReadWritePaths=: saveTo.allowedRoots is
user-configurable to anywhere on the filesystem, and a sandbox here would turn a
legitimately-configured save location into an opaque EROFS/EACCES instead of the
daemon's own clear -32011.

cli/man/velox.1 documents the CLI as it actually exists today (add/ls/pause/resume/rm,
--json) — the queue/settings subcommands AGENT-DAEMON.md's build order originally
sketched aren't implemented in cli/src/main.cpp yet, so the page doesn't claim they
are. Checked warning-free with groff -mandoc -ww -z.

Full ctest: 57/57 (excluding the pre-existing, unrelated conformance failure noted in
earlier commits).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
2026-09-12 22:46:52 +04:00

3.8 KiB

packaging/systemd — the user unit pair for veloxd

Provided by lane DAEMON (daemon/docs/AGENT-DAEMON.md build step 7: "systemd user units — velox.service + velox.socket for socket activation"), for PKG/QA to install per docs/07-packaging.md's layout:

/usr/lib/systemd/user/velox.service
/usr/lib/systemd/user/velox.socket

packaging/ outside nativehost/ is PKG/QA's per CLAUDE.md's lane table; these two files are here because they are inputs to that packaging step, not a claim on the rest of the directory — same relationship packaging/nativehost/ already has.

Why both files, and what RuntimeDirectory= is doing

velox.socket binds $XDG_RUNTIME_DIR/velox/velox.sock before veloxd ever runs and hands the daemon the already-listening fd at startup (daemon/src/rpc/systemd_activation.cpp implements the receiving half — LISTEN_PID/LISTEN_FDS, fd 3 — without a libsystemd link). Two things this buys over the daemon binding its own socket on every start:

  • No window where a client gets ECONNREFUSED. The socket exists and queues connections from the moment velox.socket is active, not from whenever veloxd finishes starting up — this is the actual point of socket activation, not just "start on demand."
  • Cold-boot ordering is free. Nothing has to wait for veloxd to be ready before the GUI, the CLI, or a native-messaging host can attempt a connection; the kernel queues it.

RuntimeDirectory=velox on the socket unit creates %t/velox (mode 0700) before the ListenStream= bind — without it, binding fails outright the first time (nothing has created the parent directory yet). veloxd itself creates that same directory (ensure_private_dir in runtime_dir.cpp) for the case where it's started directly, outside systemd (./veloxd in a terminal, still supported and how most of this daemon's own testing runs) — the two paths converge on the same directory with the same mode either way.

UdsServer::start() (daemon/src/rpc/uds_server.cpp) checks for the activated fd first and, if present, skips create/bind/chmod/listen entirely — the socket file's lifecycle then belongs to the unit (including RemoveOnStop=yes on stop), not to the daemon. Falls back to binding its own socket exactly as before when not socket-activated (a manual run, or a distro that ships the daemon without the unit files).

What was deliberately left out

velox.service does not set ProtectSystem=, ProtectHome=, or ReadWritePaths=. saveTo.allowedRoots is user-configurable to anywhere on the filesystem — an external drive, a second mount, anywhere fs/safepath.hpp's own canonicalize-and-check accepts — not a fixed set of directories a unit file could enumerate ahead of time. A filesystem-level sandbox here would turn a legitimately-configured save location into an opaque EROFS/EACCES the daemon can't explain, in place of its own clear -32011 — worse than no sandbox, specifically for a download manager. NoNewPrivileges=yes is kept: it has no such trade-off.

Verifying socket activation without a real install

systemd-analyze verify --user velox.service velox.socket checks unit-file syntax (it will complain that /usr/bin/veloxd and the velox(1) man page don't exist on a dev box that hasn't installed the package — expected, not a unit bug). To exercise the actual activation handshake without installing anything: bind a Unix socket, dup2 it onto fd 3, fork, set LISTEN_PID=<child pid> and LISTEN_FDS=1 in the child's environment, clear FD_CLOEXEC on fd 3, and execve veloxd — a real session.hello round-trips over that fd with no bind()/listen() call ever happening inside the daemon for that run. This is exactly what velox.socket's Requires=/ExecStart sequence does in production; systemd supplies the fd, veloxd doesn't know the difference.