connect any Mac you own and it becomes a programmable cloud runtime, driven by agents, SDKs & CLIs from anywhere. real macOS, not emulated: xcode, simulators, codesigning, homebrew and native apps. build & ship iOS/macOS apps autonomously, automate real Safari, manage a fleet of Macs in parallel, and run agent-driven workloads & CI/CD on actual Apple hardware.
fastapi control plane, outbound websocket daemon, per-sandbox process isolation
python
asyncio
fastapi
websockets
pydantic
sqlite
postgres
typer
macos
seatbelt
launchd
xcode
next.js
typescript
cloudflared
the problemThere is no cloud for the Mac you already own
Anything that needs real macOS is stuck. Apple's licensing means nobody rents dense macOS capacity the way Modal or Fly rent Linux, and most developers already have a Mac doing nothing for twenty hours a day.
SSH is the obvious answer and it fails for boring reasons. It gives you a shell, not a runtime, and the machine is behind NAT anyway.
what a Mac gives you that a container cannota shell versus a runtime
the ideaThe Mac dials home, and the work comes back down the same pipe
One decision shapes everything: the Mac opens a single outbound WebSocket to a control plane and never listens for anything. Self hosted GitHub runners, Cloudflare Tunnel and Temporal workers all make the same move, for the same reason.
The price is that the socket is stateful, so the control plane cannot scale by adding processes behind a load balancer. Every frame carries a request_id and one connection multiplexes exec, filesystem RPC, HTTP proxying and raw TCP.
control plane
FastAPI, holds agent sockets, dispatches exec, fans out logs
daemon
asyncio process on the Mac, one outbound WebSocket, runs the work
sdk + cli
synchronous Python, httpx to start a job, websockets to stream it
relay
gives a host a stable subdomain without any inbound port
one outbound socket vs an inbound portenrolling a Mac into the fleeta Mac joins the fleet
how it worksFrom mac.run() to a process on somebody's desk
The control plane stores almost nothing: machines, keys and job rows in SQLite, while volumes, sandboxes, images and every byte of build output stay on the Mac. It is a router with a job table attached.
The Hub buffers every frame per request and replays it to late subscribers, so a client that connects a hundred milliseconds in misses nothing. Fan out is an in-memory map of request_id to asyncio queues, with exactly two methods, which is the seam where Redis goes the day there is a second process.
sandbox
~/.herds/sandboxes/<id>/{workspace,tmp,home}, HOME and TMPDIR redirected into it
seatbelt
sandbox-exec fences writes to the tree plus mounted volumes, network is a toggle
why caches move
two Xcode builds sharing DerivedData corrupt each other and it reads as a compiler bug
why not xcode-select
--switch is global and would repoint a concurrent job
one command, end to enda log line, the other directionfour layers of isolation, because macOS has no cgroups
the hard partsFailures I only found by running a real fleet
Every interesting bug was about something going away at the wrong moment, and I found all of them by running two Macs against one account for days rather than by reading the code.
A hang is a worse failure than an error, because an error has a message in it. That one sentence is the fix in all three cases below.
retry policy
forever for a dropped link, never for a rejected token, because that fails identically a million times
64KB reads
readline() dies on a 1.3MB base64 screenshot and takes the log pump with it
admission
8 concurrent, 32 queued behind it, exit 75 past that, plus a cpu high water mark
reapers
idle sessions at 30 min, sandbox trees at 24h, a machine with no metrics for 60s is offline
three ways a fleet strands itselfthe socket drops mid-jobthe startup race between herds host and launchd
the surfaceWhat a caller actually gets
The SDK is synchronous on purpose. User code is ordinary blocking Python, so all the asyncio lives inside the control plane and the daemon where it belongs.
Driving real HID events needed a trick: macOS no longer ships PyObjC with the system interpreter, so the input driver is a ctypes script against CoreGraphics, kept Python 3.9 clean because that is what every Mac has. AppleScript can type but cannot move a mouse to a coordinate.
run, stream, map
blocking Result, chunk generator, or a callable fanned across the fleet
scoped read < run < admin, revocable, checked on every route
SKILL.md
shipped in the package, every call in it checked against the live SDK by a test
107 public sdk methods, by classa sandbox, made and unmadean iOS build, and the artifact back
the stackWhat runs where, and what it cost
FastAPI and websockets rather than gRPC. Bidirectional gRPC is the better long term wire and the documented graduation path, but a browser dashboard and a pip install with no build step both want plain HTTP.
The daemon installs as a launchd LaunchAgent rather than a root daemon, which is the whole point: it runs as the user, with their toolchains, signing identities and keychain. inherit_home=True is a named opt out of the sandbox for tools like git and gh, and I would rather that be a flag than a surprise.
what sits on whatthe storage split, and whytwo Macs, one subdomain
where it standsShipped, and what is still wrong
It is on PyPI at 0.9.12 and it works end to end: a remote agent holding only a URL and a token can build an iOS app, boot a simulator, expose a port and stream logs back. Many of the 369 tests are regressions written after a real two-Mac failure, with the observed symptom in the docstring.
Isolation here is workspace confinement, not an adversarial jail. Real multi-tenancy means Tart VMs, and Apple's EULA caps that at two macOS VMs per host, which makes VM slots a scarce resource the scheduler would have to model.