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
This commit is contained in:
2026-09-12 22:46:52 +04:00
co-authored by Claude Sonnet 5
parent 4f6c0cc9d2
commit eb72aa522c
21 changed files with 932 additions and 2 deletions
+65
View File
@@ -0,0 +1,65 @@
# 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.
+38
View File
@@ -0,0 +1,38 @@
[Unit]
Description=Velox download manager daemon
Documentation=man:velox(1)
# Socket activation (velox.socket) means this unit does not need to be enabled or started
# directly for the RPC transport to come up on demand — the first connection attempt after
# boot starts veloxd with the listening socket already bound (see velox.socket's own
# comment). Requires=/After= still matter for a manual `systemctl --user start velox`.
Requires=velox.socket
After=velox.socket
# Never more than one real instance for this user regardless of how it was started — the
# abstract-socket single-instance lock (main.cpp, keyed off the resolved runtime dir) is
# the actual enforcement; this just keeps systemd itself from racing two starts.
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=simple
ExecStart=/usr/bin/veloxd
# main.cpp's SIGTERM handler stops the event loop and falls through to a clean shutdown
# (flushes buffers, closes the store, releases the single-instance lock) — the default
# KillSignal=SIGTERM and TimeoutStopSec are already the right shape for that; no
# ExecStop/KillMode override needed.
Restart=on-failure
RestartSec=2
# Hardening deliberately stops here, not at ProtectSystem=/ProtectHome=/ReadWritePaths=:
# saveTo.allowedRoots is user-configurable to anywhere (an external drive, a second
# mount — fs/safepath.hpp is the daemon's own validation boundary, not a fixed set of
# directories a unit file could enumerate up front). A filesystem-level sandbox here would
# silently turn a legitimately-configured save location into an opaque EROFS/EACCES the
# daemon can't explain, instead of its own clear -32011 — worse than no sandbox, for a
# download manager specifically. NoNewPrivileges is free of that trade-off.
NoNewPrivileges=yes
[Install]
WantedBy=default.target
Also=velox.socket
+28
View File
@@ -0,0 +1,28 @@
[Unit]
Description=Velox download manager — RPC socket
[Socket]
# %t is $XDG_RUNTIME_DIR for a user unit — the exact path veloxd itself resolves
# (daemon/src/rpc/runtime_dir.cpp's resolve_runtime_dir), and the exact path velox(1)
# resolves too (default_socket_path() in cli/src/client.cpp). All three must agree; this
# is the one line that has to.
ListenStream=%t/velox/velox.sock
# RuntimeDirectory creates %t/velox (mode 0700, this user's own) before binding, so the
# ListenStream= path above always has somewhere to land — veloxd itself does the same
# thing (ensure_private_dir) when it creates the directory unassisted (the non-activated
# path, e.g. a manual `veloxd` run outside systemd).
RuntimeDirectory=velox
RuntimeDirectoryMode=0700
# Same-UID-only, matching the socket's own authorization once a connection is accepted
# (SO_PEERCRED, checked again in uds_server.cpp regardless of this mode bit — belt and
# braces, not a substitute for it).
SocketMode=0600
# The socket file belongs to systemd's socket-activation state, not to whatever's on disk
# from a previous boot; remove it on stop so a stale entry never shadows the next start.
RemoveOnStop=yes
[Install]
WantedBy=sockets.target