---
type: note
alexandria_core: true
tags: [server, reference]
author: alexandria
---

# Server setup (always-on macOS host)

The `server-macos` module turns an always-on Mac (a mini, a spare laptop that never sleeps) into the vault's runner: it runs the hourly sweep + digests on a schedule, keeps the checkout synced, and processes inbound email in near-real-time via an IMAP IDLE listener. This is the alternative to the laptop GitHub-Actions runner — pick one.

## What it installs
- `Agent/scripts/sweep/sweep.sh` — the hourly sweep wrapper (digests, Action Board, auto-safe cleanups).
- `Agent/scripts/daily-digest/{digest.sh, digest-now.sh}` — send / on-demand-send wrappers.
- `agent-listener.sh` — wrapper for the IMAP IDLE daemon (`alexandria_vault.engine.agent_listener`, in the installed package) that triggers the inbox processors the moment mail arrives.
- `Agent/scripts/sweep/vault-sync.sh` — a git pull-rebase/commit/push loop.
- `health-check.sh` — wrapper for the watchdog (`alexandria_vault.engine.health_check`) that reads the heartbeats and emails on a stale/errored component.
- `Agent/scripts/launchd/*.plist` — the launchd job templates.

Each run leaves a heartbeat in `~/Obsidian/secrets/*-status.json` and logs to `~/Library/Logs/alexandria/` — see the **Reference** at the end.

## Off-repo secrets
Create an env file OUTSIDE the vault checkout (so it's never committed), `chmod 600`:

```
~/Library/Application Support/Alexandria/alexandria.env     # ANTHROPIC_API_KEY, SMTP_*, IMAP_*, *_PASSPHRASE_SHA256, etc.
```

The wrappers source it before running. See `Agent/SECRETS.md` for each credential and how to get it. Recipient addresses are NOT here — they live in config / `Roster.md`.

## Install the launchd jobs
One command on the host substitutes the absolute wrapper paths into the plists, writes them to `~/Library/LaunchAgents/`, and loads them:

```sh
alexandria install-launchd --vault "$HOME/Obsidian/appliance-user-vault"
```

It's idempotent — re-run it after an `alexandria update` to pick up plist changes. Preview with `--dry-run` (writes and loads nothing); write the plists without loading them with `--no-load`. Verify: `launchctl list | grep com.alexandria`.

## Kill switch
Create `~/Obsidian/secrets/SWEEP_OFF` to pause all automation immediately (the sweep and digest-now check for it and abort). Delete it to resume.

## Send a digest early
Over SSH from anywhere: run `digest-now.sh` on the host (the `digest-now` skill wraps this). It regenerates the precursors and sends today's owner digests now, off the fresh vault.

## Reliability
Auto-login + never-sleep + auto-restart-after-power-loss so the jobs survive reboots; a private mesh VPN (e.g. Tailscale) for stable SSH access.

---

## Reference — the appliance at a glance
<!-- Keep in sync with docs/APPLIANCE.md § Reference (the deployer-facing copy). -->
Everything a running appliance is made of, in one place — for operating the box from an SSH session or an agent on the host.

### Runtime components (launchd jobs)
Installed/reloaded by `alexandria install-launchd`; scripts are under `Agent/scripts/`.

| Job (`com.alexandria.`) | Cadence | Runs | Does |
|---|---|---|---|
| `vault-sweep` | hourly (:00) | `sweep/sweep.sh` → `sweep.py` | due tasks — digests, Action Board, auto-safe writes; caps logs; writes `runner-status.json` |
| `vault-sync` | every 10 min | `sweep/vault-sync.sh` | `git pull --rebase` / commit / push; writes `sync-status.json` (incl. conflict state) |
| `agent-listener` | daemon (respawns) | `daily-digest/agent-listener.sh` → `agent_listener.py` | real-time IMAP IDLE inbox; writes `listener-status.json`. Active only with an inbox module + `INBOX_LISTENER_ENABLE=1` |
| `health-check` | every 30 min | `daily-digest/health-check.sh` → `health_check.py` | reads the heartbeats; emails the owner if a component is stale/errored |

### File & directory map
| Path | What it holds |
|---|---|
| `~/Obsidian/appliance-user-vault/` | the vault (a Git working tree). `Agent/` = software + Config; the PEAK pillars = content |
| `~/Library/Application Support/Alexandria/alexandria.env` | **off-repo secrets** (`chmod 600`) — every wrapper sources it. Never inside the vault |
| `~/Obsidian/secrets/` | host-local state, **not** in the vault: `sweep-state.json` (dedup); `runner-status.json` · `sync-status.json` · `listener-status.json` (heartbeats); `health-alert-state.json`; `SWEEP_OFF` (kill switch) |
| `~/Library/Logs/alexandria/*.log` (+ `.log.1`) | persistent per-job logs, size-capped at 5 MB (tail kept as `.1`) |
| `~/Library/LaunchAgents/com.alexandria.*.plist` | the installed launchd jobs |
| `~/Library/Application Support/Alexandria/app/` | the Alexandria CLI checkout — `alexandria update` pulls here |

### Secrets & environment (`alexandria.env`)
**Required** (server-macos): `ANTHROPIC_API_KEY`, `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS`, `IMAP_HOST`, `IMAP_USER`, `IMAP_PASS` — plus `OWNER_PASSPHRASE_SHA256` if `principal-inbox` is enabled. `alexandria secrets --vault ~/Obsidian/appliance-user-vault` prints the exact list. (No `ALEXANDRIA_REPO_DIST_PAT` on the box: `alexandria update` is render-only — updating the software is a `git pull` of the checkout with the box's own git access.) See `Agent/SECRETS.md` for how to get each.

**Optional knobs:**
| Var | Default | Effect |
|---|---|---|
| `ALEXANDRIA_ENV` | (searched) | explicit path to the secrets env file |
| `SWEEP_STATE` | `~/Obsidian/secrets/sweep-state.json` | relocate the state + heartbeat dir |
| `INBOX_LISTENER_ENABLE` | `0` | `1` = run the real-time IMAP listener |
| `IDLE_KEEPALIVE_SECS` | `1500` | listener keepalive / poll floor (seconds) |
| `HEALTH_ALERT_TO` | owner's 1st email | where health alerts are sent |
| `HEALTH_REPEAT_HOURS` | `12` | re-alert cadence for a persistent problem |
| `ALEXANDRIA_PYTHON` | `python3` | interpreter the wrappers exec |
| `DIGEST_MODEL`, `DIGEST_FROM`, … | from config | digest send overrides |

### Config keys that drive a server vault (`Agent/config.json`)
`automation.runner = server-macos` · `automation.send_hour_local` · `automation.weather` · `automation.health_alert_to` *(optional)* · `owner` *(name / short_name / timezone_default / emails)* · `agent` *(name / email / character)* · `areas` *(name / description / threads)* · `owner_digests` *(digest routing)* · `team` *(optional roster)* · `modules.enabled` · `secrets.backend = dotenv`.

### Host requirements (macOS)
Never-sleep (`sudo pmset -c sleep 0 disksleep 0`) · restart-after-power-loss (`sudo systemsetup -setrestartpowerfailure on`) · auto-login *(blocked by FileVault)* · Remote Login / SSH · Tailscale *(optional, stable remote access)* · Node *(Claude Desktop MCP)* · `imapclient` *(pip; the IMAP listener)*.

### Controls & health
- **Pause / resume:** `touch ~/Obsidian/secrets/SWEEP_OFF` — the sweep, digest, and health-check all abort while it exists; delete to resume.
- **Send a digest now:** `sh ~/Obsidian/appliance-user-vault/Agent/scripts/daily-digest/digest-now.sh` (locally or over SSH).
- **Inspect:** `alexandria doctor` (env + jobs + heartbeats) · `alexandria status` (version + drift) · `launchctl list | grep alexandria` · the logs in `~/Library/Logs/alexandria/`.
- **Manage jobs:** `alexandria install-launchd --vault ~/Obsidian/appliance-user-vault` installs/reloads all four (idempotent).
- **Update:** `git -C "$HOME/Library/Application Support/Alexandria/app" pull --ff-only && alexandria update --vault ~/Obsidian/appliance-user-vault` — pull the checkout, then `update` re-renders Core (render-only) and reloads launchd so the listener runs the new code.
- **Watchdog thresholds:** sweep stale > 90 min · sync > 25 min · listener > 40 min → one alert email (+ a "recovered" notice), re-alerting a persistent problem at most every 12 h. Logs cap at 5 MB.
