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
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 momentvelox.socketis active, not from wheneverveloxdfinishes 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
veloxdto 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.