feat: local macOS launchd installer with scheduled pipelines#405
feat: local macOS launchd installer with scheduled pipelines#405norrietaylor merged 3 commits intomainfrom
Conversation
Installs a supervised Distillery HTTP server as a LaunchAgent plus four scheduled webhook agents (poll every 30 min, classify every 2 h, rescore daily, maintenance weekly) and a weekly image-update agent. All ingestion goes through the running server's /api/* surface — DuckDB is single-writer and the server owns the lock, so sibling CLI processes can't write. Secrets (JINA_API_KEY, DISTILLERY_WEBHOOK_SECRET) live in the macOS login Keychain; the webhook secret is generated on first install. The installer is idempotent and auto-detects OrbStack vs Docker Desktop. Uninstaller preserves the DB and Keychain entries by default. Adds a 'Local macOS (launchd)' page under Getting Started with setup, verification, customization, and troubleshooting. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds a launchd-based local macOS deployment for Distillery: installer/uninstaller scripts, Keychain-managed secrets, six supervised LaunchAgents, webhook-invoking maintenance/poll/classify/rescore scripts, Docker-run MCP server templates and configs, docs, and operational/troubleshooting guidance (HTTP at 127.0.0.1:8000/mcp). Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Installer as install.sh
participant macOS as "macOS (security, launchctl)"
participant Docker as "Docker/OrbStack"
participant Keychain as "Keychain"
participant FS as "FileSystem (~/.distillery)"
participant launchctl as "launchctl"
User->>Installer: ./install.sh [--jina-key / options]
Installer->>macOS: verify tools (security, launchctl, curl, python3)
Installer->>Docker: locate docker binary & docker info
Installer->>Keychain: read/store JINA_API_KEY
Installer->>Keychain: generate/store DISTILLERY_WEBHOOK_SECRET (if missing)
Installer->>FS: create ~/.distillery, write distillery.yaml, scripts, plists
Installer->>launchctl: bootout prior agents
Installer->>launchctl: bootstrap new LaunchAgents
Installer->>Docker: kickstart/run server container (via launchd)
Installer->>User: print paths, logs, example MCP config
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
scripts/local-macos/install.sh (2)
220-239: Add trailing newline at end of file.The script is missing a trailing newline after line 239.
♻️ Add trailing newline
Then run /setup inside Claude Code to configure the reporting routines. EOF +🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/local-macos/install.sh` around lines 220 - 239, The file ends the here-doc (EOF) output without a final newline which can cause POSIX tools and editors to warn; add a single trailing newline at the end of scripts/local-macos/install.sh so the last line (the EOF block that prints Data dir/LaunchAgents/Logs/Uninstall and the JSON snippet) is terminated by a newline. Locate the here-doc that uses cat <<EOF and the variables DATA_DIR, LAUNCHAGENTS, and SCRIPT_DIR and ensure the file ends with one final '\n' after the closing EOF.
30-40: Consider validating--jina-keyhas an argument.If a user runs
./install.sh --jina-keywithout providing a value,$2will be unset, causing an unclear error due toset -u. Adding a check improves UX.♻️ Suggested improvement
case "$1" in - --jina-key) JINA_KEY_CLI="$2"; shift 2 ;; + --jina-key) + [ $# -lt 2 ] && { echo "--jina-key requires a value" >&2; exit 2; } + JINA_KEY_CLI="$2"; shift 2 ;; --no-kickstart) DO_KICKSTART=0; shift ;;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/local-macos/install.sh` around lines 30 - 40, The --jina-key option handling in the install.sh argument parsing can read an empty $2 (JINA_KEY_CLI) if the user supplies no value, which breaks with set -u; update the case for --jina-key in the while/case block to validate that a following argument exists and is not another flag before assigning to JINA_KEY_CLI, and if missing print a helpful error and exit non-zero (use the same error channel as other branches), e.g., check "$#" and that "$2" doesn't start with "-" before shift 2.scripts/local-macos/uninstall.sh (1)
105-122: Add trailing newline at end of file.The script is missing a trailing newline after line 122. POSIX conventions expect text files to end with a newline.
♻️ Add trailing newline
if [ "$PURGE_SECRETS" -eq 0 ]; then log "Keychain entries preserved (pass --purge-secrets to delete)" fi +🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/local-macos/uninstall.sh` around lines 105 - 122, The file ends without a trailing newline (after the final log/prompt block referencing PURGE_SECRETS, PURGE_DATA, bold and log), violating POSIX text-file conventions; open the script (uninstall.sh) and add a single newline character at the end of the file so the last line ends with a newline, then save the file without altering any other content.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@scripts/local-macos/install.sh`:
- Around line 220-239: The file ends the here-doc (EOF) output without a final
newline which can cause POSIX tools and editors to warn; add a single trailing
newline at the end of scripts/local-macos/install.sh so the last line (the EOF
block that prints Data dir/LaunchAgents/Logs/Uninstall and the JSON snippet) is
terminated by a newline. Locate the here-doc that uses cat <<EOF and the
variables DATA_DIR, LAUNCHAGENTS, and SCRIPT_DIR and ensure the file ends with
one final '\n' after the closing EOF.
- Around line 30-40: The --jina-key option handling in the install.sh argument
parsing can read an empty $2 (JINA_KEY_CLI) if the user supplies no value, which
breaks with set -u; update the case for --jina-key in the while/case block to
validate that a following argument exists and is not another flag before
assigning to JINA_KEY_CLI, and if missing print a helpful error and exit
non-zero (use the same error channel as other branches), e.g., check "$#" and
that "$2" doesn't start with "-" before shift 2.
In `@scripts/local-macos/uninstall.sh`:
- Around line 105-122: The file ends without a trailing newline (after the final
log/prompt block referencing PURGE_SECRETS, PURGE_DATA, bold and log), violating
POSIX text-file conventions; open the script (uninstall.sh) and add a single
newline character at the end of the file so the last line ends with a newline,
then save the file without altering any other content.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0bebb0cf-f02d-4d4a-a409-b2dd2a9520df
📒 Files selected for processing (19)
docs/getting-started/local-macos.mdmkdocs.ymlscripts/local-macos/README.mdscripts/local-macos/install.shscripts/local-macos/templates/_webhook_common.shscripts/local-macos/templates/classify.shscripts/local-macos/templates/distillery.yamlscripts/local-macos/templates/launchd/local.distillery-classify.plistscripts/local-macos/templates/launchd/local.distillery-maintenance.plistscripts/local-macos/templates/launchd/local.distillery-poll.plistscripts/local-macos/templates/launchd/local.distillery-rescore.plistscripts/local-macos/templates/launchd/local.distillery-update.plistscripts/local-macos/templates/launchd/local.distillery.plistscripts/local-macos/templates/maintenance.shscripts/local-macos/templates/poll.shscripts/local-macos/templates/rescore.shscripts/local-macos/templates/run.shscripts/local-macos/templates/update.shscripts/local-macos/uninstall.sh
Previously, `./install.sh --jina-key` (with no following argument) would silently accept an empty key under set -u or, worse, consume the next flag. Reject both cases with a clear error and exit 2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Addressed CodeRabbit's nitpicks in 1e0ed62: Fixed
Skipped (false positive)
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@scripts/local-macos/install.sh`:
- Around line 63-67: The script currently runs even under sudo/root which causes
per-user LaunchAgents and keychain items to be installed into root's
environment; add a guard (before proceeding with installation, e.g., just after
detecting macOS or at script start) that detects running as root or under sudo
by checking EUID==0 and/or presence of SUDO_USER/SUDO_UID, and if detected print
a clear error (refusing to run as root/sudo) and exit non‑zero; update any
user-facing messages (the existing log "macOS detected" area and the installer
entry point) to abort early when running as root so installs always target the
invoking desktop user.
- Around line 96-100: The docker check using DOCKER and `docker info` only
ensures a daemon is reachable but not that it’s local; add a validation that the
active Docker context/host is local (127.0.0.1 or unix socket) before
proceeding. Specifically, after the existing `if ! "$DOCKER" info` block,
inspect DOCKER_HOST (and if absent use `docker context inspect` / `docker info
--format` to get the endpoint) and fail with a clear message if the host is
remote (eg. starts with ssh:// or tcp:// and is not 127.0.0.1), since later code
assumes a local daemon at http://127.0.0.1:8000. Use the DOCKER variable,
DOCKER_HOST env, and `docker context inspect` to detect non-local contexts and
exit with an error prompting the user to switch to a local context.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1c672c19-7390-4ac4-b2ae-e6c0d5916064
📒 Files selected for processing (1)
scripts/local-macos/install.sh
Two preflight guards in the local-macOS installer: - Refuse to run as root or under sudo. LaunchAgents and login-Keychain items are per-user; running under sudo would install them for root instead of the desktop user, producing a silently broken install. - Reject remote docker contexts (ssh://, tcp://). `docker info` succeeds against a remote daemon, but the install binds the server to 127.0.0.1:8000 on the daemon host — a remote daemon leaves the LaunchAgent workers unable to reach the server. Only unix:// contexts are accepted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Addressed the two CodeRabbit findings in 37ba75c:
Unrelated: the failing "Scan (distillery)" check is CVE-2026-3298 (CPython 3.13.13-r2, no upstream fix yet) — a new entry in Grype's 2026-04-23 DB. Main was green yesterday; this PR touches only shell scripts and docs, no Dockerfile or deps. Suppression decision is yours; happy to add one to |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
scripts/local-macos/install.sh (1)
109-117:⚠️ Potential issue | 🟠 MajorHandle
DOCKER_HOSTenv override in the remote-daemon guard.Line 109–117 validates the active context host, but Docker can be redirected by
DOCKER_HOST. WithDOCKER_HOST=ssh://...ortcp://..., Line 101 may pass against a remote daemon and still allow an install that later assumes local loopback behavior.Suggested patch
# Reject remote docker contexts (ssh://, tcp://) — the install binds the # server to 127.0.0.1:8000 on the docker host, so a remote daemon would # produce a "successful" install that the LaunchAgent workers can't reach. +DOCKER_HOST_ENV="${DOCKER_HOST:-}" +if [ -n "$DOCKER_HOST_ENV" ] && [[ "$DOCKER_HOST_ENV" != unix://* ]]; then + echo "DOCKER_HOST is set to '$DOCKER_HOST_ENV'" >&2 + echo " this installer needs a local daemon (unix://) — unset DOCKER_HOST or switch local context" >&2 + exit 1 +fi DOCKER_CONTEXT=$("$DOCKER" context show 2>/dev/null || true) DOCKER_HOST_URI=$("$DOCKER" context inspect "$DOCKER_CONTEXT" \ --format '{{.Endpoints.docker.Host}}' 2>/dev/null || true) if [ -n "$DOCKER_HOST_URI" ] && [[ "$DOCKER_HOST_URI" != unix://* ]]; then#!/bin/bash set -euo pipefail f="scripts/local-macos/install.sh" echo "== Remote-daemon guard block ==" sed -n '101,120p' "$f" echo echo "== Search for explicit DOCKER_HOST env handling ==" rg -n '\$\{DOCKER_HOST:-\}|DOCKER_HOST=' "$f" || true echo echo "Expected:" echo "1) Guard should include an explicit \${DOCKER_HOST:-} check." echo "2) If absent, remote env overrides can bypass context-only validation."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/local-macos/install.sh` around lines 109 - 117, The remote-daemon guard uses DOCKER_CONTEXT and DOCKER_HOST_URI but ignores the DOCKER_HOST env override, allowing DOCKER_HOST=ssh://... or tcp://... to bypass the local-daemon check; update the guard in scripts/local-macos/install.sh to explicitly check ${DOCKER_HOST:-} (or DOCKER_HOST) and treat any non-empty DOCKER_HOST that is not a unix:// socket as remote, so combine the existing DOCKER_HOST_URI check with an explicit DOCKER_HOST condition (using DOCKER_HOST variable name) and fail with the same explanatory message if DOCKER_HOST points to a non-unix:// endpoint.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@scripts/local-macos/install.sh`:
- Around line 109-117: The remote-daemon guard uses DOCKER_CONTEXT and
DOCKER_HOST_URI but ignores the DOCKER_HOST env override, allowing
DOCKER_HOST=ssh://... or tcp://... to bypass the local-daemon check; update the
guard in scripts/local-macos/install.sh to explicitly check ${DOCKER_HOST:-} (or
DOCKER_HOST) and treat any non-empty DOCKER_HOST that is not a unix:// socket as
remote, so combine the existing DOCKER_HOST_URI check with an explicit
DOCKER_HOST condition (using DOCKER_HOST variable name) and fail with the same
explanatory message if DOCKER_HOST points to a non-unix:// endpoint.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: eb44b2f0-51c2-4b41-84db-6f77085d3bce
📒 Files selected for processing (1)
scripts/local-macos/install.sh
Summary
scripts/local-macos/install.sh) that sets up a self-hosted Distillery HTTP server as a macOS LaunchAgent, plus four scheduled webhook agents (poll / classify / rescore / maintenance) and a weekly image-update agent./api/*surface — DuckDB is single-writer and the server holds the lock, so sibling CLI processes (docker exec ... distillery poll) can't write. The four worker scripts curl the webhooks with a bearer token stored in the login Keychain.docs/getting-started/local-macos.mdpage walks through prerequisites, install, verification, customization, and troubleshooting. Added to the MkDocs nav under Getting Started.What gets installed
local.distilleryghcr.io/norrietaylor/distillery:latest)local.distillery-updatedocker pull; kickstart if digest changedlocal.distillery-pollPOST /api/polllocal.distillery-classifyPOST /api/hooks/classify-batchlocal.distillery-rescorePOST /api/rescorelocal.distillery-maintenancePOST /api/maintenanceSecrets live in the macOS login Keychain (
JINA_API_KEY,DISTILLERY_WEBHOOK_SECRET). The webhook secret is generated on first install and reused on re-runs. Installer is idempotent; uninstaller preserves DB and Keychain entries by default (--purge-data,--purge-secretsflags opt-in to destruction).Design notes
command -v docker, then~/.orbstack/bin/docker,/usr/local/bin/docker,/opt/homebrew/bin/docker. Works with both OrbStack and Docker Desktop.linux/amd64. Runs under Rosetta on Apple Silicon; no separate arm64 manifest is published on GHCR._webhook_common.shso auth / reachability / error-handling is fixed in one place. Treats 202/409/429 as success (queued / in-flight / cooldown) — those are benign control-flow signals from the server, not failures.__HOME__and__DOCKER_DIR__placeholders, substituted at install time withsed.StartCalendarIntervalRunOnMissed=trueso they fire on next wake if the Mac was asleep at the scheduled minute.Test plan
bash -n install.sh && bash -n uninstall.shcleanzsh -non all template shell scripts cleanplutil -linton all plist templates cleanplutil -linton templates after__HOME__/__DOCKER_DIR__substitution cleanmkdocs build --strictsucceeds with the new page and nav entrydev.minimal.*label prefix; this PR templatizes tolocal.*for generic reuse)~/.distillery/— confirm the interactive prompt flow, Keychain writes, and that all six agents come up🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation