herdr phase 0 follow-up: persistent daemon via systemd/launchd

Phase 0 started Herdr via nohup — died on reboot / logout. Replaces
that with proper service management:

  - deploy/fleet/herdr-persistence/herdr.service — systemd user unit
    (Linux). Restart=on-failure with an 8-in-24h burst cap so a
    broken binary doesn't hot-loop. Requires linger enabled so the
    user's systemd manager runs without a login session; install.sh
    does that via loginctl.
  - deploy/fleet/herdr-persistence/dev.herdr.plist — launchd
    LaunchAgent (macOS). ProgramArguments + PATH templated so the
    install script substitutes actual paths at deploy time.
  - deploy/fleet/herdr-persistence/install.sh — idempotent installer
    that autodetects OS, drops the unit/plist in the right place,
    enables + starts, prints status.

Rolled to tank + architect + morpheus (systemd) + smith + macbook
(launchd). All 5 nodes confirmed `status: running` post-install.

Phase 0 gap closed: Herdr now survives node reboots and logouts,
which is the prerequisite for the fleet_herdr dispatch path to be
reliable across mission_orchestrator restarts.
This commit is contained in:
Omar Sobh
2026-07-20 11:27:35 -07:00
parent a5588b0289
commit 0d4cb2c2bc
3 changed files with 138 additions and 0 deletions
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
Launchd user agent for Herdr's headless server. Install as:
cp dev.herdr.plist ~/Library/LaunchAgents/
launchctl load -w ~/Library/LaunchAgents/dev.herdr.plist
Reads $HOME/.local/bin/herdr — fleet_herdr and the node daemon's
PtyTarget::Command resolver both expect that path.
-->
<plist version="1.0">
<dict>
<key>Label</key>
<string>dev.herdr.server</string>
<key>ProgramArguments</key>
<array>
<string>__HERDR_BIN__</string>
<string>server</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>Crashed</key>
<true/>
</dict>
<!-- 5 second throttle so a bad binary doesn't hot-loop. -->
<key>ThrottleInterval</key>
<integer>5</integer>
<!-- Herdr writes its own logs at ~/.config/herdr/herdr-server.log;
launchd captures anything on stdout/stderr as a safety net. -->
<key>StandardOutPath</key>
<string>/tmp/herdr-launchd.out.log</string>
<key>StandardErrorPath</key>
<string>/tmp/herdr-launchd.err.log</string>
<!-- Herdr's install script writes to $HOME/.local/bin; make sure
the daemon's PATH includes it so the binary is resolvable. -->
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>__HOME__/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,24 @@
[Unit]
Description=Herdr headless server (terminal multiplexer for coding agents)
Documentation=https://herdr.dev/docs/
After=network-online.target
Wants=network-online.target
[Service]
# Reads $HOME/.local/bin/herdr; the resolver in fleet_herdr expects this path.
ExecStart=%h/.local/bin/herdr server
# Herdr's socket lives under ~/.config/herdr/herdr.sock — the daemon
# creates its own state dir, no need to pre-provision anything.
Restart=on-failure
RestartSec=5
# 24 hours between restart bursts so a crashing binary doesn't hot-loop.
StartLimitIntervalSec=86400
StartLimitBurst=8
# Herdr owns its own logs (~/.config/herdr/herdr-server.log); systemd
# journal captures anything that escapes.
StandardOutput=journal
StandardError=journal
[Install]
# WantedBy=default.target for user units — no wants for multi-user.target.
WantedBy=default.target
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Idempotent install of a persistent Herdr daemon for the current user.
# Autodetects Linux (systemd --user) vs macOS (launchd LaunchAgent).
#
# Usage (locally):
# ./install.sh
# Usage (remote):
# scp -r deploy/fleet/herdr-persistence <host>:/tmp/
# ssh <host> 'bash -lc "/tmp/herdr-persistence/install.sh"'
#
# Preconditions: `herdr` is installed at $HOME/.local/bin/herdr
# (via `curl -fsSL https://herdr.dev/install.sh | sh`).
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
herdr="$HOME/.local/bin/herdr"
if [ ! -x "$herdr" ]; then
echo "herdr binary not found at $herdr — run the herdr install script first." >&2
exit 1
fi
os="$(uname -s)"
case "$os" in
Linux)
unit_dir="$HOME/.config/systemd/user"
unit="$unit_dir/herdr.service"
mkdir -p "$unit_dir"
cp "$here/herdr.service" "$unit"
# Enable lingering so the user's systemd manager runs without a
# login session — otherwise the daemon dies at logout.
if command -v loginctl >/dev/null 2>&1; then
sudo loginctl enable-linger "$USER" 2>/dev/null || true
fi
systemctl --user daemon-reload
systemctl --user enable --now herdr.service
systemctl --user status --no-pager herdr.service | head -8
;;
Darwin)
plist_dir="$HOME/Library/LaunchAgents"
plist="$plist_dir/dev.herdr.server.plist"
mkdir -p "$plist_dir"
sed \
-e "s|__HERDR_BIN__|$herdr|g" \
-e "s|__HOME__|$HOME|g" \
"$here/dev.herdr.plist" > "$plist"
# Unload first so a re-install replaces cleanly.
launchctl unload "$plist" 2>/dev/null || true
launchctl load -w "$plist"
sleep 1
launchctl list | grep dev.herdr.server || echo "(agent not in list yet)"
;;
*)
echo "unsupported OS: $os" >&2
exit 2
;;
esac
echo
echo "herdr daemon persistence installed. Verify with: herdr status server"