Files
fips/packaging/macos/com.fips.ble-agent.plist
ArjenandJohnathan Corgan d208a9db81 feat(ble): lend the radio to the macOS daemon from a login-session agent
macOS never grants Bluetooth to a root launchd daemon: CoreBluetooth
answers it Unauthorized whatever the privacy settings say, so the
packaged daemon could not use the CoreBluetooth backend at all.

When CoreBluetooth refuses the daemon, it listens on ble-agent.sock
beside the control socket (/var/run/fips, group fips), and a per-user
LaunchAgent runs `fips-ble-agent --ble-agent` in the login session,
lending its radio to the daemon over a small framed protocol
(io_macos/agent*.rs). Each agent connection that says a valid hello is
installed as the radio, replacing the previous one, so the transport is
unchanged; a lost agent is a radio switched off. A connection has five
seconds to say hello. The agent re-resolves the socket path on every
reconnect, so one started before the daemon created its runtime
directory does not settle on a fallback path.

The agent connects only once its radio is up, and its hello carries the
PSM CoreBluetooth published, read afresh for every connection. Before the
radio is up `listen` reports only the requested fallback, so a hello sent
then told the daemon PSM 133 while the listener was on another, and kept
telling it so across reconnects and Bluetooth toggles.

fips-ble-agent is a hard link to fips, so Activity Monitor tells the two
processes apart. The agent logs to ~/Library/Logs/fips/ble-agent.log,
rolled like the daemon's log, rather than to a shared path in /tmp
another account could plant a file or symlink in. Install, upgrade and
uninstall stop and start the agent in every GUI session, so none keeps
running a replaced binary. The agent ends on the stop signal the process holds from
the start, so a bootout does not wait out launchd's timeout.

The daemon/agent pair is tested end to end over a real Unix socket and
MockBleIo on every host: listener PSM, advertising, adverts, dialled and
inbound channels both ways, refusal, a lost agent, a connection that
never says hello, and an agent whose radio comes up after it starts.
2026-10-13 20:56:07 +00:00

34 lines
1.3 KiB
Plaintext

<?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>com.fips.ble-agent</string>
<!-- macOS never lets a root launchd daemon use Bluetooth: CoreBluetooth
answers it Unauthorized whatever the privacy settings say. This agent
runs the same binary in the user's login session, where Bluetooth
permission can be granted, and lends the radio to the daemon over
/var/run/fips/ble-agent.sock. It needs the user in the `fips` group.
fips-ble-agent is a hard link to fips, so Activity Monitor shows the
agent under its own name. -->
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/fips-ble-agent</string>
<string>--ble-agent</string>
</array>
<key>LimitLoadToSessionType</key>
<string>Aqua</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>10</integer>
<!-- The agent logs to ~/Library/Logs/fips/ble-agent.log itself, rolled
like the daemon's log: launchd cannot expand ~ here, and a shared
path such as /tmp would let one account plant a file or symlink the
next account's agent appends to. stdout and stderr default to
/dev/null. -->
</dict>
</plist>