The Daemon
Status: Tier 2. Repo: fermi-daemon. A set of small processes on your Mac that turn the Worker's task queue into real work done by a real coding harness.
The core idea
The Worker cannot run Claude Code. Your Mac can. The daemon closes that gap pull-only: the Mac calls out to the Worker, claims tasks, runs the harness locally, and completes them. Without MacOSMCP, nothing on the internet can push into your Mac — the daemon holds an outbound bearer token, no inbound port.
What runs (who owns what)
| Process | File | Owns |
|---|---|---|
| Poller | poll.sh | lane-based task drain: claims from its queue, spawns a fresh harness per task, completes with the result |
| Warm worker | warm-worker.mjs | latency: keeps one harness pre-booted so the first token of a reply is seconds, not a cold boot; drains main |
| Channel bridges | wa-bridge, dc-bridge (in fermi-mcp packages/) | sockets a Worker can't hold (WhatsApp Web); relay to Worker webhooks |
| Broker executor | tools/broker-executor.mjs | the only process anywhere that sees decrypted cookies; drives Playwright for fleet browser ops — see Session broker |
| Session capture | tools/capture-session.mjs | logs into a site headfully once, ships storageState to /admin/session/capture |
| Box runner | box-runner.mjs | not run on the Mac — this is the file neutrinos boot; it lives here so it can be SHA-pinned from one repo |
With vs. without MacOSMCP
| Daemon only (Tier 2) | + MacOSMCP (Tier 3) | |
|---|---|---|
| Direction | Mac pulls tasks | Worker can also push tool calls in |
| Latency | queue poll (seconds) | live round-trip |
| Surface | what your task prompt allows the harness | 25 mac_* tools for any connected host |
| Inbound exposure | none | Cloudflare Tunnel + MACOS_MCP_TOKEN |
| Typical use | "fix issue #12 and open a PR" from Telegram | "log into the vendor portal and screenshot the invoice" from your phone |
They compose: the reference deployment runs both. Tier 2 handles queued work; Tier 3 handles interactive control. The daemon's harness itself connects to Fermi over MCP, so queued tasks still get memory, skills, secrets, and channels.
Phone calls from a chat
A lane lives for minutes; a call can last hours. So a call requested from a channel is split in two: one lane confirms the details with you, starts the call with phone_call_start, and finishes; the call then runs inside the Worker; and when it ends the Worker enqueues a result task that a fresh lane relays to your chat. The drain prompt requires the lane to state who it will call and what it will say, and to wait for your yes before dialing. Details: Phone calls.
Setup
git clone https://github.com/abel30567/fermi-daemon && cd fermi-daemon
cp .env.example .env # FERMI_URL, FERMI_BEARER_TOKEN, lane/queue names
bun install # playwright, for the broker executor
./poll.sh # or install the launchd plists for boot persistence
node warm-worker.mjs # optional but worth itWhat success looks like
- [ ] Message your Telegram bot; a reply arrives produced on your Mac
- [ ]
task_list(status: done)shows the task claimed by your daemon's identity - [ ] Kill the daemon mid-task: the lease expires and the task becomes claimable again (no lost work)
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Tasks pile up pending | daemon not running / wrong queue name | check lane config vs. enqueue queue |
| Replies from "root" or wrong git author | harness env leaks through | daemon exports GIT_AUTHOR_*/GIT_COMMITTER_*; see Committing as root |
| Fleet tasks stolen by your Mac | you're on a pre-Sep-2026 worker | update: fleet queues are excluded from the channel drain |
| Two broker executors fighting | you ran it on two Macs | exactly one owns the browser profiles; kill the rest |