Elyra
Elyra The coding agent e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Start here
Concepts
Release notes
What's new
Elyra
Installing on macOS

Installing on macOS

From a Mac with a coding-agent CLI on it to a runtime showing online in Félagi, and then to a machine that is still online next month. Written for the case it is usually deployed in: a Mac mini that nobody sits at, serving agents around the clock.

This page is the whole macOS path, so it can be followed start to finish. The platform-neutral detail lives in Installing and Running as a service; this repeats what a Mac needs and adds the five things that are only true here.


The short version

xcode-select --install                                    # the linker, once per machine
npm install -g @elyracode/coding-agent                    # gives you `elyra`
npm install -g @anthropic-ai/claude-code                  # or this, gives you `claude`
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
git clone <daemon-repository-url> felagi-daemon && cd felagi-daemon
cargo build --release
sudo install -m 755 target/release/felagi /usr/local/bin/felagi

felagi providers                                          # must list your CLI
felagi setup --server https://felagi.example.com --token fdt_… --name mac-mini
felagi start                                              # leave it, watch Admin → Runtimes

Then read Keeping it up, because everything above survives until the next reboot and no longer.


No, you do not need to notarize anything

The question comes up because macOS refuses unsigned software people download, so it is worth answering plainly rather than leaving as a doubt.

You build the binary on the machine that runs it. The linker signs it ad-hoc as it links, which is what Apple Silicon requires in order to execute at all, and nothing ever sets the com.apple.quarantine attribute because nothing downloaded it. Gatekeeper therefore never assesses the binary. There is no dialog, no spctl exception, no Developer ID, and no notarization.

$ codesign -dv /usr/local/bin/felagi
Format=Mach-O thin (arm64)
CodeDirectory … flags=0x20002(adhoc,linker-signed)
Signature=adhoc

That is the expected output. Signature=adhoc is not a warning.

Notarization becomes relevant only if this project starts publishing prebuilt binaries for people to download, and even then it is narrower than it sounds: a browser sets the quarantine attribute and curl -O does not, so a curl install line works unsigned while the same file fetched from a release page is refused. If that day comes it needs Developer ID signing, notarizing and stapling, and a paid Apple Developer account. Until then, there is nothing to do.


What the machine needs

Notes
Xcode Command Line Tools xcode-select --install. Rust links through cc, so without them cargo build fails on the link step with linker 'cc' not found — after compiling everything, which makes it look like a code problem
Rust 1.85 or newer rustup puts cargo in ~/.cargo/bin. rustc --version to check. Needed to build, never to run
An agent CLI on PATH claude or elyra, both installed with npm — see below. Where the binary lands is whatever npm config get prefix says plus /bin, which differs per machine. Do not guess it: felagi providers prints the real path and you will need it verbatim later
That CLI signed in as this user Its credentials live in this user's $HOME. Not logged in on every run is this and nothing else
Git, with working credentials An SSH agent, a credential helper or a deploy key. The daemon holds none of its own
A daemon token Admin → Runtimes → Connect a machine in Félagi. Shown once

Nothing else. In particular no Full Disk Access and no other permission grant — see what macOS does not ask you for.

Installing the agent CLIs

Both are npm packages, and the package name is not the command name:

npm install -g @elyracode/coding-agent      # the command is `elyra`
npm install -g @anthropic-ai/claude-code    # the command is `claude`

One is enough; the daemon registers a runtime for each it finds. Install them as the user the daemon will run as, not with sudo: a CLI installed as root signs in as root, and the daemon then runs as you and finds no credentials. If npm -g needs sudo on this machine, set a user prefix instead:

npm config set prefix ~/.npm-global
# then put ~/.npm-global/bin on your PATH

Then sign each one in, as that user. That is a browser flow, so do it while you are actually sitting at the machine or connected over Screen Sharing, before you make the daemon a service.

The test for whether this machine is ready is not a checklist. Sign in as the user the daemon will run as and do these two things:

claude -p "hello"
git clone git@github.com:acme/api.git

If a person cannot, the daemon cannot, and no configuration will fix it.


Build and install

git clone <daemon-repository-url> felagi-daemon && cd felagi-daemon
cargo build --release

A release build takes a minute or two the first time. Then:

sudo install -m 755 target/release/felagi /usr/local/bin/felagi
felagi --version

install rather than cp, so the mode is set in the same step and a later build replaces the running binary atomically.

/usr/local/bin is deliberate. It is not on launchd's default PATH, which is why the service definition later names the binary by its absolute path. Putting it in ~/.cargo/bin instead works from your terminal and then fails as a service for a reason that takes an afternoon to find.


Check the machine before configuring it

felagi providers
  elyra        /Users/kh/.npm-global/bin/elyra
  claude_code  /Users/kh/.npm-global/bin/claude

A CLI that is installed and not listed is not on the PATH of this shell. Write down the directory it printed — that is npm's prefix on this machine, and it is not the same on the next one. You will need them verbatim in the service definition, because a service starts with a far shorter PATH than your terminal has, and a daemon that found two providers by hand finds none as a service.


Configure

felagi setup --server https://felagi.example.com --token fdt_… --name mac-mini

That writes ~/.felagi/config.json with mode 0600, because it holds the token. Name the machine after itself: mac-mini, studio-in-the-rack. That name is what you read later when deciding where a run happened.

Re-running setup keeps the machine's identity and replaces the rest, which is how a token is rotated without the server thinking a new machine appeared.

Run it in the foreground first

felagi start
INFO registered workspace=acme runtimes=2 providers=claude_code, elyra sandbox=confined

That line is the whole test, and Admin → Runtimes should show the machine online within a few seconds. If it does not, felagi status prints the configuration, the providers it found and whether the server answered.

Hand an agent an issue and watch a real run finish before going further. A service that has never been seen to work by hand is a service that cannot be debugged.


Keeping it up

Stop the foreground process and install it as a LaunchAgent, so it runs as you and finds your $HOME. launchd does not expand ~, so every path here is absolute.

<!-- ~/Library/LaunchAgents/no.felagi.daemon.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>no.felagi.daemon</string>

  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/felagi</string>
    <string>start</string>
  </array>

  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>

  <!-- The daemon hands its tasks back on SIGTERM and needs up to 30 s to do it. -->
  <key>ExitTimeOut</key>
  <integer>40</integer>

  <key>EnvironmentVariables</key>
  <dict>
    <key>FELAGI_LOG</key>
    <string>info</string>
    <!-- The directories `felagi providers` printed, then the system ones. -->
    <key>PATH</key>
    <string>/Users/kh/.npm-global/bin:/usr/local/bin:/usr/bin:/bin</string>
  </dict>

  <key>StandardOutPath</key>
  <string>/Users/kh/Library/Logs/felagi.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/kh/Library/Logs/felagi.log</string>
</dict>
</plist>

Replace /Users/kh throughout with this machine's home.

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/no.felagi.daemon.plist
launchctl print gui/$(id -u)/no.felagi.daemon | head -20
tail -f ~/Library/Logs/felagi.log

To stop it, or before loading an edited plist:

launchctl bootout gui/$(id -u)/no.felagi.daemon

Three things about launchd that cost people time:

The plist must be owned by you and not group-writable, or bootstrap refuses with an input/output error that says nothing about permissions. chmod 644 it.

An edited plist is not reloaded. bootout then bootstrap again. There is no reload.

bootstrap, not load. launchctl load is the legacy interface and still half-works, which is worse than not working.

The log will grow for ever

macOS rotates nothing you write yourself. One line per registration and per task start is not much, but a year is a year:

# /etc/newsyslog.d/felagi.conf
# logfilename                         [owner:group]  mode count size when  flags
/Users/kh/Library/Logs/felagi.log     kh:staff       644  7     5120 *     J

Seven files of five megabytes, compressed. sudo newsyslog -nvv shows what it would do without doing it.


A Mac that is meant to stay up

This is the part that has nothing to do with the daemon and decides whether it is running when you need it. All of it applies to a Mac mini in a cupboard.

It has to be logged in, and FileVault fights that

A LaunchAgent runs only while its user is logged in. Not "the Mac is on" — logged in. A locked screen is fine; the login window is not.

So you turn on automatic login: System Settings → Users & Groups → Automatically log in as.

And FileVault makes that option unavailable. With FileVault on, the disk has to be unlocked by a password at the login window after every boot, before any user session exists — so after a power cut, a software update or any reboot, the machine sits at that screen and the daemon is not running. It will show offline in Félagi and stay offline until somebody types the password.

There is no way around it, and it is worth being clear about why: running as a system daemon in /Library/LaunchDaemons/ does not help, because the disk is still encrypted, and neither does anything the daemon could do differently. The choice is yours to make:

FileVault off The machine boots to a session unattended and the daemon comes back on its own. Defensible for a machine in a locked room that holds a checkout and a CLI's token; a stolen disk is readable
FileVault on Every reboot needs a person. Correct for a machine that could walk, and a real operational cost

Decide on purpose. The failure mode of not deciding is a Mac that is offline every time the power flickers and nobody knows why.

It must not sleep

A run stops when the machine sleeps: the CLI is stopped, the heartbeat stops, the server marks the runtime offline within 45 seconds, and the task is requeued when its lease expires. Nothing is lost and nothing hangs, but a machine that sleeps is not one to rely on.

sudo pmset -a sleep 0 disksleep 0 displaysleep 0 powernap 0
sudo pmset -a autorestart 1        # come back by itself after a power failure
sudo pmset -g custom               # check it

autorestart exists on desktops and not on laptops, so it is the one line that will be refused on a MacBook. powernap 0 because a Power Nap wake is not a session that runs agents, and it makes the runtime flicker.

It does not need a display

A headless mini is fine. Screen Sharing or ssh is enough to administer it, and the LaunchAgent does not care whether anything is plugged into HDMI. Log in once with a display or over Screen Sharing, confirm automatic login works by rebooting, and then take the display away.

Software updates reboot it

An automatic macOS update is a reboot, which is the FileVault question again and a gap in coverage either way. On a machine serving agents, install updates deliberately rather than automatically: System Settings → General → Software Update → Automatic updates, and turn off installing macOS updates automatically.


What macOS does not ask you for

Worth stating, because it is the classic macOS trap and you will not hit it: no Full Disk Access, and no permission prompts.

A process launched by launchd cannot show a permission dialog. When one is needed, the operation is simply denied, silently, and the only sign is a failure with no reason. That happens to background tools that touch ~/Documents, ~/Desktop, ~/Downloads or iCloud Drive.

The daemon keeps everything — its configuration, its per-task checkouts, each run's own temporary directory — under ~/.felagi/, which is outside every protected location. Nothing it does needs a grant. Keep it that way: point a repository at a path under ~/Documents and you have invented the problem.

The sandbox works here without installing anything

confined is the default policy and macOS enforces it with sandbox-exec, which ships with every install. Writes outside the task directory are refused; the network is untouched, because an agent CLI exists to call a model.

Linux needs bubblewrap installed for the same guarantee. macOS needs nothing.

One thing to know for the long term: Apple has deprecated sandbox-exec. It works, and on current macOS it runs without even printing a deprecation notice, so nothing pollutes a run's output — but it is the one part of this page that could need revisiting on some future release. The policy in force is reported to the server and shown on the runtimes page, so if it ever falls back to limits, that is visible in Félagi without logging in to the machine.


When it does not work

Symptom Cause
linker 'cc' not found after a long build Xcode Command Line Tools. xcode-select --install
felagi providers lists nothing The CLI is not on this shell's PATH
Works by hand, finds no providers as a service The PATH in the plist. Put the directories felagi providers printed in it
Not logged in on every run The CLI is signed in as a different user than the service runs as
A clone fails as a service, works by hand Same cause: HOME, and the credentials in it
bootstrap fails with an I/O error Plist ownership or mode. chmod 644, owned by you
Runtime offline after every reboot Automatic login is off, or FileVault is on. See above
Runtime flickers offline The machine is sleeping, or Power Nap is on
Tasks stuck as running for five minutes after a restart The service manager killed the daemon before it could hand them back. ExitTimeOut 40
Sandbox reported as limits, not confined Not possible from a missing tool on macOS — sandbox-exec is always there. Check sandbox in ~/.felagi/config.json

Updating

There is no self-update. When the server answers 426 Upgrade Required the daemon says so and stops.

cd felagi-daemon && git pull --ff-only && cargo build --release

launchctl bootout gui/$(id -u)/no.felagi.daemon          # hands its tasks back, up to 30 s
sudo install -m 755 target/release/felagi /usr/local/bin/felagi
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/no.felagi.daemon.plist

In that order, and not with kickstart -k. bootout takes the normal termination path, so the daemon stops claiming, kills each running CLI and reports its tasks back as retryable before exiting — which is what ExitTimeOut in the plist is for. launchctl kickstart -k is one command instead of three and the manual page promises only to "kill the running instance", which is not a promise that the tasks get handed back. Stopping first also means the binary is not replaced underneath a process that is using it.

A daemon stopped properly requeues its work at once. Killed, the tasks it held sit as running in Félagi until the sweeper notices, five minutes later.