macOS runtime and launchd service (SF-075)
SuiFleet's daemon runs on macOS as a system LaunchDaemon. It supervises components with the same lifecycle, readiness, restart, rollback and IPC semantics as on Linux (supervisor, COMPONENT_IPC.md). This is a native SuiFleet platform, not AWS IoT Greengrass compatibility: the Greengrass nucleus officially supports Linux and Windows only (see references).
Supported platform
| Validated | Not validated | |
|---|---|---|
| OS | macOS 15 (Sequoia) 15.7.x, Darwin 24, xnu-11417.140.69 (GitHub-hosted macos-15 runner) |
macOS 14 and earlier, macOS 26 |
| Architecture | arm64 (aarch64-apple-darwin) |
Intel x86_64-apple-darwin (builds, untested) |
| Toolchain | Rust 1.94.0 (repository baseline), default features | pkcs11 feature |
A Linux result does not establish macOS support, and this macOS result does not establish any physical Mac, Intel or other release. Physical target acceptance stays in SF-031.
For a first device, start with Install the device client, which covers the address, network manifest and configuration on every platform.
Install and service lifecycle
cargo build --locked --release --manifest-path suifleet-edge/Cargo.toml
sudo suifleet-edge/packaging/macos/install.sh suifleet-edge/target/release/suifleet
# write /etc/suifleet/config.toml and deployment.json (root-owned, 0644), then:
sudo launchctl bootstrap system /Library/LaunchDaemons/org.suifleet.daemon.plist
install.sh (root) creates the
hidden _suifleet account (first free ID in 200–400) when missing, installs
/usr/local/bin/suifleet and the
plist, creates
/var/lib/suifleet (0700) and /var/log/suifleet, and provisions the device
key on the machine: /etc/suifleet/device.key is generated once, kept on
reinstall and made 0600 _suifleet with no ACL (chmod -N on the key,
/etc/suifleet and /var/lib/suifleet; macOS evaluates ACL entries before
the mode, and directories pass inheritable entries on). Only the public address is printed and
written to device.address. Packages must not contain keys, enrollment or state.
The installer writes no configuration and starts nothing.
| Operation | Command | Behaviour |
|---|---|---|
| Start | launchctl bootstrap system <plist> |
Loads and starts the job (RunAtLoad). |
| Stop | launchctl bootout system/org.suifleet.daemon |
SIGTERM: the daemon runs component shutdown commands (dependents first) and stops every descendant; launchd sends SIGKILL after ExitTimeOut (60 s). |
| Graceful stop, kept loaded | launchctl kill SIGTERM system/org.suifleet.daemon |
A clean exit (status 0) is not restarted (KeepAlive.SuccessfulExit = false). |
| Restart | launchctl kickstart -k system/org.suifleet.daemon |
|
| Status | launchctl print system/org.suifleet.daemon |
pid, runs, last exit code. |
| Logs | /var/log/suifleet/daemon.log |
Component output stays in each component's private component.log. |
A crash or failure exit is restarted after at least ThrottleInterval (5 s).
launchd has no readiness or watchdog protocol like systemd's Type=notify and
WatchdogSec: a hung (not exited) daemon is not restarted automatically.
Process containment
macOS has no cgroups and no child subreaper. launchd starts every job in a
resource coalition of its own; each child joins its parent's coalition at
fork and stays in it through setsid, a cleared environment and reparenting to
launchd. Spawning into another coalition requires a privileged coalition or an
Apple entitlement. The daemon uses that coalition like the systemd unit's cgroup:
- At start (after reaping leftovers) it records its coalition in
supervised/instance.jsononly when it is the coalition's only member and not root: the launchd job's own coalition. A daemon started from a shell shares its parent's coalition and records none, and logs a warning. - Each run keeps its own process group, killed when it ends or stops. Every second, and after each run ends, the daemon kills any member of its coalition outside its own process tree, an orphan whichever way it detached. Without a recorded coalition, only orphans of the daemon user that still carry the state root's environment marker are found.
- After a crash (daemon SIGKILL), launchd restarts the job; launchd itself kills
only the daemon's process group, so components survive the crash. The
successor freezes and kills the recorded runs, their groups, marker carriers,
their descendants and every member of the previous instance's recorded
coalition (same boot, from
kern.bootsessionuuid), then accepts the record only if that coalition now holds no process but itself. Without that proof (no sealed coalition recorded, or a survivor) it refuses to start components until a reboot or an operator clears the record, as on Linux. - If the daemon cannot start at all after a crash (for example a configuration
error), the earlier instance's components keep running until a daemon starts
successfully or the host reboots. Inspect with
ps -U _suifleet.
Coalition IDs increase monotonically within a boot and are never 0. Interrupted installs are not replayed: their outcome stays indeterminate and unreported, as on Linux (PLATFORM_INSTALLERS.md).
Boundary. The coalition contains descendants of the daemon. It is not a
sandbox: components run as _suifleet, like the daemon, and can read its state
and talk to other local services. It also does not cover processes another
service starts on request. On the tested runner, as the daemon user,
launchctl submit and launchctl bootstrap user/UID or gui/UID were refused
and at jobs never ran (atrun is disabled by default), but cron ran a job
outside the coalition. install.sh therefore adds _suifleet to
/usr/lib/cron/cron.deny and at.deny (and refuses an allow file listing it),
and CI checks that a component's crontab and at requests are refused and
start nothing. Do not grant the daemon user other job-scheduling services.
IPC authentication
The native IPC listener (COMPONENT_IPC.md) authenticates
each connection by the kernel's Unix-socket peer credentials (getpeereid,
LOCAL_PEERCRED on macOS, SO_PEERCRED on Linux); the configuration and ACL
rules are identical. Use a daemon-owned, non-group/world-writable socket
directory the operator creates, for example /var/run/suifleet (/var/run is
cleared at boot; create it before every start, for example from the installer
of the IPC clients). Component principals need distinct non-root UIDs other than
_suifleet, provisioned by the platform.
Secrets and permissions
| Path | Owner, mode | Enforced by |
|---|---|---|
/etc/suifleet/device.key |
_suifleet, 0600 |
installer; the daemon refuses to start on macOS with a key that is not a regular file owned by its user, any group/other access, or any extended ACL entry (an allow entry can grant another user a 0600 file) |
/etc/suifleet/config.toml, deployment.json |
root, 0644 | operator; the daemon cannot change its own policy |
/var/lib/suifleet |
_suifleet, 0700 |
installer |
component component.log, configuration.json |
_suifleet, 0600 |
daemon (umask 077 and explicit modes) |
Other local users cannot read the key, state, journal or component logs
(checked against nobody in CI). The service signer that would keep the key
away from component processes on Linux (KEY_ISOLATION.md) is
not available on macOS, so components can read the device key there.
Component selection
The catalog's platform OS vocabulary is linux, macos (Rust's
std::env::consts::OS) and *; architectures are x86_64, aarch64, arm
and * (Apple silicon is aarch64). The contract's private validator, the
daemon and the dApp accept the same values, and the shared vectors in
component-cases.json cover macOS
selection. The daemon selects the first manifest whose OS and architecture
match exactly or by *; a version with no macOS manifest fails on a Mac with
an unsupported-platform receipt and no download.
Compatibility: macos needs the upgraded package; older package versions keep
rejecting it. Daemons released before this change reject a whole version that
lists a macos manifest, so update Linux fleets before publishing mixed
versions (or publish macOS builds as separate versions). AWS recipes name macOS
darwin; the Greengrass recipe importer still omits those manifests.
Platform-owned operations
macOS updates, reboots, firmware and boot health stay with the platform (for
example softwareupdate or an MDM). SuiFleet runs only explicitly configured
commands, never reboots the Mac and has no OS-installation state machine.
Unsupported platform APIs on macOS
Refused at configuration load (the daemon exits instead of weakening them):
| Feature | Reason |
|---|---|
[device.signer] kind = "service" (KEY_ISOLATION.md) |
Trusts the daemon by systemd unit, cgroup and pidfd |
apply.mode = "privileged-helper" |
A systemd path-activated root unit |
apply.max_memory_bytes |
macOS does not enforce RLIMIT_AS |
[daemon_update] (DAEMON_UPDATE.md) |
Handoff and rollback not validated under launchd |
Not provided, without a substitute: the systemd sandbox of suifleet.service
(ProtectSystem, NoNewPrivileges, system-call filter, MemoryMax,
TasksMax), the readiness/watchdog protocol, per-component execution users
(also absent on Linux), Yocto packaging and the Raspberry Pi image. PKCS#11
signing builds but is untested on macOS.
Pinned upstream references
Read on 2026-10-16 for semantics only; no AWS or Apple code is copied. Implementation uses public interfaces (libproc, sysctl, launchd property lists).
- AWS IoT Greengrass V2 component recipe reference:
Platform.os"Common platforms include …linux,windows,darwin(macOS)". - Greengrass nucleus: installs on Linux and Windows; macOS is not an officially supported platform.
- Configure the Core software:
system service via systemd (Linux) or a Windows service;
runWithDefaultcomponent users. SuiFleet's macOS service has no Greengrass counterpart. launchd.plist(5)(Darwin manual dated 30 July 2019):KeepAlive/SuccessfulExit,ExitTimeOut(SIGTERM then SIGKILL),UserName/GroupName,Umask,ThrottleInterval(default 10 s), resource limits, and "When a job dies, launchd kills any remaining processes with the same process group ID as the job" (unlessAbandonProcessGroup).- XNU
xnu-11417.140.69(the runner's kernel):bsd/kern/kern_exec.cposix_spawnrefuses (EPERM) a coalition other than the caller's unless the caller is in a privileged coalition or holds the coalition-spawn entitlement;bsd/kern/kern_fork.cfork1(non-spawn children always inherit the parent's coalitions);osfmk/kern/coalition.c(coalition_next_id, monotonically increasing; no coalition 0);bsd/sys/proc_info_private.h(PROC_PIDCOALITIONINFO= 20,struct proc_pidcoalitioninfo);bsd/kern/proc_info.c(no same-user check for that flavor).
Evidence
The macOS runtime (launchd) CI job runs on every pull request:
cargo test --lockedon macOS (platform table, coalition inheritance and proof vectors, supervisor lifecycle/containment, refused features, key checks);- test_supervisor_launchd.py (root,
disposable runner only): the shipped installer and plist as a real job against
the local chain/Walrus simulator: sealed coalition as
_suifleet; grouped, setsid, env-cleared and reparented descendants contained; a component'sposix_spawnattr_setcoalition_npescape refused; component crash; daemon SIGKILL with launchd restart and successor reap/accept; SIGKILL during an install (indeterminate, never replayed); key/state/log permissions against another user; a group-readable key stopping the daemon;launchctl bootoutwith graceful shutdown and an empty coalition. Evidence JSON is uploaded as themacos-logsartifact; - test_ipc.py through
sudo: kernel-authenticated IPC across distinct UIDs, caller/topic denial and idle-flood limits.
First full pass: CI run 39011599674
(macOS 15.7.9 24G830 arm64, rustc 1.94.0): 166 unit tests, 10 launchd checks
(the component's coalition spawn failed with EPERM; its crontab and at
were refused) and 4 IPC checks passed.
These are hosted-runner software results. No physical Mac, AWS account, paid service or production credential is involved.
Source: Edit this page