agent-joule/crates/joule_coded/launchd/install-joule-coded.sh
David Charlot 3fd9605a13 joule_coded: add fleet CLI subcommands and launchd packaging
Turn the worker binary into a fleet CLI over the same control protocol that
drives a GUI editor — any node, daemon or window, is reachable the same way.

- Subcommands: `serve` (the daemon, as before), `fleet` (every node on this
  machine with a one-line state summary), `status` (one node's full snapshot),
  `submit` (a coding task to a node's on-device agent), and `ctl` (the raw
  control method escape hatch). All are thin clients over
  joule_core::control::{run_client, run_client_tcp, list_peers} — no new wire.
- launchd install script generates a user agent in the GUI (Aqua) domain via
  `launchctl bootstrap gui/$(id -u)`. The GUI domain (not nohup-over-SSH) is
  required so the worker can initiate outbound local-network connections for
  the federation transport — a macOS process-responsibility constraint we
  root-caused (see lockstep-quic/MACOS_LAUNCH_CONTEXT.md). Generated plist
  passes plutil -lint.

Live-verified: serve a daemon, then ctl/status/spawn_session/submit/fleet.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 10:17:12 -04:00

123 lines
3.7 KiB
Shell
Executable file

#!/usr/bin/env bash
#
# Install joule-coded as a launchd *user agent* in the GUI (Aqua) domain.
#
# Why the GUI domain and not `nohup … &`: on macOS a process detached from a
# login session is denied permission to *initiate* outbound local-network
# connections (it can still bind/accept). The headless worker's federation
# transport (Phase 3) needs to dial peers, so it must run session-attached or as
# a launchd agent in the GUI domain. `launchctl bootstrap gui/$(id -u)` gives it
# that context; `nohup`-over-SSH does not (we root-caused this — see
# open-standards lockstep-quic/MACOS_LAUNCH_CONTEXT.md).
#
# Usage:
# JOULE_CODE_ROOT=/path/to/repo \
# JOULECLAW_L3_MODEL=/path/to/model.gguf \ # optional; enables the L3 tier
# ./install-joule-coded.sh
#
# Override any of: JOULE_CODED_BIN, JOULE_CODE_ROOT, JOULE_CODE_SOCK,
# JOULECLAW_L3_MODEL, JOULE_CODE_GRANT_TIERS.
set -euo pipefail
LABEL="dev.openie.joule-coded"
HERE="$(cd "$(dirname "$0")" && pwd)"
# Resolve the binary: an explicit override, else the repo's release/debug build.
REPO_ROOT="$(cd "$HERE/../../.." && pwd)"
BIN="${JOULE_CODED_BIN:-}"
if [ -z "$BIN" ]; then
for candidate in "$REPO_ROOT/target/release/joule-coded" "$REPO_ROOT/target/debug/joule-coded"; do
[ -x "$candidate" ] && BIN="$candidate" && break
done
fi
if [ -z "$BIN" ] || [ ! -x "$BIN" ]; then
echo "error: joule-coded binary not found; build it (cargo build -p joule_coded) or set JOULE_CODED_BIN" >&2
exit 1
fi
ROOT="${JOULE_CODE_ROOT:-$PWD}"
SOCK="${JOULE_CODE_SOCK:-$HOME/.joule/joule-coded.sock}"
TIERS="${JOULE_CODE_GRANT_TIERS:-l0,l1,l2,l3}"
MODEL="${JOULECLAW_L3_MODEL:-}"
DEST="$HOME/Library/LaunchAgents/$LABEL.plist"
mkdir -p "$HOME/logs" "$(dirname "$SOCK")" "$HOME/Library/LaunchAgents"
# The L3 model is optional: included only when a path is given.
MODEL_ARGS=""
if [ -n "$MODEL" ]; then
MODEL_ARGS=" <string>--model</string>
<string>$MODEL</string>
"
fi
cat > "$DEST" <<PLIST
<?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">
<plist version="1.0">
<dict>
<key>Label</key>
<string>$LABEL</string>
<key>ProgramArguments</key>
<array>
<string>$BIN</string>
<string>serve</string>
<string>--root</string>
<string>$ROOT</string>
<string>--sock</string>
<string>$SOCK</string>
<string>--tiers</string>
<string>$TIERS</string>
$MODEL_ARGS </array>
<key>WorkingDirectory</key>
<string>$ROOT</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>HOME</key>
<string>$HOME</string>
<key>RUST_LOG</key>
<string>info</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>Crashed</key>
<true/>
</dict>
<key>ThrottleInterval</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>$HOME/logs/joule-coded.out.log</string>
<key>StandardErrorPath</key>
<string>$HOME/logs/joule-coded.err.log</string>
</dict>
</plist>
PLIST
# (Re)load into the GUI domain.
DOMAIN="gui/$(id -u)"
launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
launchctl bootstrap "$DOMAIN" "$DEST"
launchctl kickstart -k "$DOMAIN/$LABEL"
echo "installed $LABEL"
echo " binary : $BIN"
echo " root : $ROOT"
echo " sock : $SOCK"
echo " model : ${MODEL:-<none — L3 disabled>}"
echo "drive it: joule-coded fleet | joule-coded status --sock $SOCK"
echo "uninstall: launchctl bootout $DOMAIN/$LABEL && rm $DEST"