Sai runtime
Ship the Sai runtime in your desktop app and supervise it, with its command line, events and exit codes.
The Sai runtime (sai-runtime) lets Sai work on your customer's own computer. Your desktop app ships it and starts it in the background. It enrolls the computer into the customer's Tenant once, and from then on Sai, which runs in Simular's cloud, uses the computer through it: the screen, keyboard and mouse, a browser and the shell.
Who does what
Your app talks to the runtime only through the process: its command line, stdin, stdout and exit code. Your app doesn't call the Sai API for the runtime, and never holds your Organisation Key.
What the runtime can reach
- Its credential stays on the computer. The runtime creates its machine credential when it enrolls and keeps it in its state folder. Your backend and your app only ever handle the enrollment code, which is spent once used and expires after 15 minutes.
- A computer can't act for its Tenant. The runtime's sign-in can't start work, answer approvals or read conversations. It reaches only what a run needs: the model, image generation and the Tenant's files. Code running on one computer can't drive another computer of the same Tenant.
- Work comes from your Organisation. Sai sends the runtime only work started for its Tenant with a Tenant token: by your backend, or by an owner or admin in the console's Playground.
Builds
- macOS on Apple silicon (arm64). Windows is not supported yet.
- Google Chrome. Browser tasks need Google Chrome installed.
- Pinned versions.
sai-runtime --versionprints the build's version, such as0.1.0. The runtime never updates itself: your app ships one version. Sai keeps a minimum version, and a runtime below it exits12(see Exit codes), so ship runtime updates in your app's own updates.
Each build is a zip named for its version, with its SHA-256 beside it:
https://storage.googleapis.com/simular-pro-agent-updates/runner-daemon-enrolled/sai-runtime-<version>-darwin-arm64.zip
https://storage.googleapis.com/simular-pro-agent-updates/runner-daemon-enrolled/sai-runtime-<version>-darwin-arm64.zip.sha256A published version never changes, so pin the version and its SHA-256 in your build. To find the newest version, read latest-runtime-darwin-arm64.yml in the same folder: it names the version, the zip's path, its sha256 and size.
base=https://storage.googleapis.com/simular-pro-agent-updates/runner-daemon-enrolled
curl -fsSLO "$base/sai-runtime-0.1.0-darwin-arm64.zip"
curl -fsSLO "$base/sai-runtime-0.1.0-darwin-arm64.zip.sha256"
shasum -a 256 -c sai-runtime-0.1.0-darwin-arm64.zip.sha256The zip holds one folder, sai-runtime/:
Unpack it with ditto, which keeps file modes:
ditto -x -k sai-runtime-0.1.0-darwin-arm64.zip .
sai-runtime/sai-runtime --versionShip it inside your app
These rules hold whatever your app is built with.
- Keep the folder whole, inside your app bundle. Ship
sai-runtime/with everything in it, unchanged. The executable loadsnative/from beside itself when it starts, and stops if anything there is missing. Don't move, rename, prune, compress or deduplicate anything in it. - Keep it out of archives. Install it as ordinary files and folders on disk. If your app packs its resources into an archive, leave the runtime out: started from inside one, it can't find
native/and never starts. - Don't re-sign it. Ship it with Simular's signature as it is. Many packaging tools sign, strip or compress every executable inside an app by default: exclude this folder from all three.
- Keep file modes. The executable must stay executable.
- Start it from your app, from your app's own process or a child of it, never as a launch agent or daemon. macOS holds the app that started a process responsible for that process's permissions; see macOS permissions.
- Update by replacing your whole app. Ship each update as a new app with the runtime version you want inside. Quit your app, which stops the runtime, replace the whole app, and launch it again. Never change files inside an installed app: that breaks your app's signature, and macOS stops matching your app to its permissions.
macOS permissions
macOS holds your app responsible for the runtime it starts, so the user grants the runtime's permissions to your app, under your app's name. The runtime never asks; it reports what's missing in a permissions_missing event. Your app asks.
Or open the permission's pane in System Settings for the user:
x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility
x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture- Ask before Sai works. Ask when the user turns Sai on, or as soon as
permissions_missinglists something. - Without a permission, the actions that need it fail, and the runtime keeps running. Without Accessibility, the first keyboard or mouse action also makes macOS show its own dialog, naming your app.
- Test your app launched the way users launch it: from the Finder, the Dock or
open. If you start your app's executable from a terminal, macOS holds the terminal responsible, so the permissions are the terminal's, not your app's.
Command line
sai-runtime run [--enroll] [--name <label>]
sai-runtime --versionStart it and keep it running
- Start
sai-runtime runwhen your app starts, or when the user turns Sai on, and keep it running for as long as Sai should be able to use the computer. It never outlives your app: see Stopping. If Sai should work while your window is closed, keep your app running in the background. - Give it three pipes: stdin, which you keep open, stdout for events and stderr for its log. Read stdout and stderr all the time: a pipe nobody reads fills up and can stall the runtime.
- Start with plain
run. Add--enrollonly after it exits10. - One runtime runs per OS user; a second one exits
14. If your app can run twice, only the copy that started the runtime supervises it. - Until the first
ready, it is signing in and connecting. Without a network it keeps retrying, backing off up to 5 minutes between tries, and says nothing. - Its state folder is
~/Library/Application Support/Sai Runtime/. It holds the machine credential (readable by the user only), a lock, crash logs and files Sai uses during runs. Don't read, change or move anything in it. Deleting it makes the computer forget its machine: the nextrunexits10, and enrolling again creates a new machine.
A supervisor, in outline:
start:
spawn <runtime folder>/sai-runtime run
stdin: a pipe you keep open stdout: a pipe stderr: a pipe
each stdout line → parse as JSON → handle the event; skip unknown types
each stderr line → your log file
on exit → act on the exit code
enroll (after exit 10):
code ← your backend
spawn <runtime folder>/sai-runtime run --enroll --name <label>
with SAI_ENROLLMENT_TOKEN=<code> in this child's environment only
stop:
write {"type":"stop"} and a newline to stdin, or close stdin
wait for the exit; kill the process if it hasn't exited after 10 sEvents
The runtime writes events to stdout, one JSON object per line:
{"type":"enrolled","machineId":"tm_5a1f0c9e2b7d4e6a8c3f1b2d"}
{"type":"permissions_missing","missing":["accessibility","screen_recording"]}
{"type":"ready","machineId":"tm_5a1f0c9e2b7d4e6a8c3f1b2d"}
{"type":"busy"}
{"type":"idle"}busyandidlecome once per run, not once per action.- Drops shorter than 30 seconds say nothing. A connection that dies without closing is noticed within about 40 seconds, so
disconnectedcan come about 70 seconds after the network goes. - Ignore event types you don't know, since a later runtime may add some, and lines that aren't JSON.
Exit codes
At start, exits 10 to 14 come within a second or two. Exits 11, 12 and 13 can also come later, when the runtime next signs in, within the hour.
Enrolling
runexits10.- Your app asks your backend for an enrollment code, as the user signed in to your app. Your backend creates the code for that user's Tenant, never for a Tenant the request names.
- Your app starts
sai-runtime run --enroll --name "<label>"withSAI_ENROLLMENT_TOKEN=<code>in its environment. - The runtime enrolls and prints
enrolledwith the newmachineId. Your app sends it to your backend, which checks it withGET /v1/tenants/{tenantId}/machines/{machineId}and records it against the user. The runtime then connects as on any start.
- The code goes in the environment variable, never on the command line. Other processes, and security tools that record command lines, can see a command line. The runtime removes the variable from its own environment once it has read it, so nothing it starts inherits the code.
- Only for that one start. Set the variable on that child process alone, never in your app's own environment. Don't log the code, store it or reuse it.
- A refused code exits
10again. Get a new one. If several in a row are refused, back off instead of asking for more. - Name the machine.
--nameis how the machine appears in the Tenant's machine list. Without it, the name is the computer's host name, which often includes the person's name. - Enroll at first launch, not in your installer. The code is for the Tenant of the user signed in to your app, and an installer package runs as root, so the credential would belong to root.
Stopping
- Write
{"type":"stop"}and a newline to the runtime's stdin, or close its stdin. It ends the action in progress, which Sai sees as failed, tells Sai it is offline, closes its connection and exits0, within about 5 seconds. - If it hasn't exited after 10 seconds, kill it. A killed runtime can't tell Sai it is offline, so Sai shows it offline within 3 minutes.
- Stop it before your app quits, and wait up to those 10 seconds for it to exit.
- If your app crashes or is killed, the runtime's stdin closes and it stops by itself. Keeping stdin open is what keeps it running.
- Stopping doesn't end a shell command that a run started: that command keeps running.
- It also stops on SIGTERM and SIGINT.
Lifecycle
First launch on a computer
- Your app starts
run. It exits10. - Your app asks your backend for a code. Your backend finds or creates the user's Tenant and creates a code for it.
- Your app starts
run --enrollwith the code. The runtime printsenrolled, and your app sends themachineIdto your backend, which checks and records it. - The runtime prints
permissions_missing, and your app asks the user for them. - The runtime prints
ready. Your backend can send it work.
Every later launch
Your app starts run, and the runtime prints ready. No code is needed.
The user signs out of your app
To pause Sai on the computer, stop the runtime. The machine stays enrolled and shows offline. To take the computer off the user's Tenant, your backend also revokes the machine.
A different user signs in on the same OS account
There is one machine per OS account, and it belongs to the previous user's Tenant.
- Before the runtime starts again, your backend revokes the previous user's machine.
- Your app stops the runtime if it runs, and starts
run. It exits11and deletes its credential. - Your app enrolls it for the new user, with a code for the new user's Tenant.
A customer's access ends, then comes back
Your backend disables the Tenant. Sai refuses its work at once. A running runtime exits 13 when it next signs in, within the hour; stop it from your app to end it now. Every later start exits 13, and your app tries again now and then. Once your backend enables the Tenant again, the next try connects, with no enrollment.
Sai retires your runtime version
The runtime exits 12. Your app tells the user it needs updating, and doesn't start the runtime again until it is updated.