Type: AFK
What to build
Today the daemon only runs when started by hand (adj daemon), and client commands don't auto-spawn it — they error with "Start it with adj daemon." For daily use the daemon should come up at login and restart if it dies.
Add adj install-daemon / adj uninstall-daemon to manage a per-user launchd LaunchAgent (label ac.adj.acent). A LaunchAgent runs as the user, so — unlike install-port-forward and install-ca — this needs no sudo and stays within the never-root contract. The command can write the plist and launchctl bootstrap it directly rather than printing a command for the user to run.
ProgramArguments[0] is pinned to the absolute path of the currently-running adj, so the service runs the same binary that installed it. This matters for the CA: the keychain ACL is bound to that binary's cdhash (#44), so adj install-ca must be run from the same binary the agent launches.
Acceptance criteria
Out of scope / design notes
- Does not remove the pf port-forward. A LaunchAgent runs as the user and still can't bind
:80/:443; the daemon keeps binding :8080/:8443, so install-port-forward is still required for implicit-port URLs.
- Alternative considered — bind
:80/:443 without pf via socket activation. A root LaunchDaemon (/Library/LaunchDaemons, one-time sudo) can have launchd bind the privileged sockets and pass the fds to a non-root daemon (UserName key + launch_activate_socket()). That would let us drop pf entirely, but it trades the pf sudo for a LaunchDaemon sudo and adds fd-passing code. Deferred — file a follow-up if we want it.
Related
Type: AFK
What to build
Today the daemon only runs when started by hand (
adj daemon), and client commands don't auto-spawn it — they error with "Start it withadj daemon." For daily use the daemon should come up at login and restart if it dies.Add
adj install-daemon/adj uninstall-daemonto manage a per-user launchd LaunchAgent (labelac.adj.acent). A LaunchAgent runs as the user, so — unlikeinstall-port-forwardandinstall-ca— this needs no sudo and stays within the never-root contract. The command can write the plist andlaunchctl bootstrapit directly rather than printing a command for the user to run.ProgramArguments[0]is pinned to the absolute path of the currently-runningadj, so the service runs the same binary that installed it. This matters for the CA: the keychain ACL is bound to that binary's cdhash (#44), soadj install-camust be run from the same binary the agent launches.Acceptance criteria
adj install-daemonwrites~/Library/LaunchAgents/ac.adj.acent.plistwith: absolute path to the runningadj,daemonarg,RunAtLoad=true,KeepAlive=true, stdout+stderr →~/.adjacent/daemon.log.launchctl bootstrap gui/$(id -u) …) and kickstarts it — no sudo anywhere.install-daemonis idempotent: boots out the old label first, so it re-bootstraps cleanly even if the binary path changed.adj uninstall-daemonboots the agent out and removes the plist.~/.adjacent/is created if missing before launchd opens the log file (launchd creates the file, not its parent dir).install-camust run from the same binary the LaunchAgent launches.Out of scope / design notes
:80/:443; the daemon keeps binding:8080/:8443, soinstall-port-forwardis still required for implicit-port URLs.:80/:443without pf via socket activation. A root LaunchDaemon (/Library/LaunchDaemons, one-time sudo) can have launchd bind the privileged sockets and pass the fds to a non-root daemon (UserNamekey +launch_activate_socket()). That would let us drop pf entirely, but it trades the pf sudo for a LaunchDaemon sudo and adds fd-passing code. Deferred — file a follow-up if we want it.Related
install-cafrom the launched binary