Docs/Sai runtime
Organisations

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

WhoHoldsDoes
Your backendThe Organisation KeyCreates the Tenant and its enrollment codes, checks and revokes machines, sends work with a Tenant token.
Your appNothing long-livedStarts and supervises the runtime, hands it an enrollment code once, asks for macOS permissions.
The runtimeIts own machine credentialEnrolls once, keeps the computer connected to Sai, does the work.

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 --version prints the build's version, such as 0.1.0. The runtime never updates itself: your app ships one version. Sai keeps a minimum version, and a runtime below it exits 12 (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:

Download URLsText
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.sha256

A 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.

Download and check a buildShell
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.sha256

The zip holds one folder, sai-runtime/:

PathWhat it is
sai-runtime/sai-runtimeThe executable.
sai-runtime/native/Modules the executable loads when it starts.
sai-runtime/licenses.txtThe licenses of the open-source software inside.

Unpack it with ditto, which keeps file modes:

Unpack a buildShell
ditto -x -k sai-runtime-0.1.0-darwin-arm64.zip .
sai-runtime/sai-runtime --version

Ship it inside your app

These rules hold whatever your app is built with.

  1. Keep the folder whole, inside your app bundle. Ship sai-runtime/ with everything in it, unchanged. The executable loads native/ from beside itself when it starts, and stops if anything there is missing. Don't move, rename, prune, compress or deduplicate anything in it.
  2. 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.
  3. 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.
  4. Keep file modes. The executable must stay executable.
  5. 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.
  6. 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.

PermissionNeeded toShow the system prompt with
AccessibilityUse the keyboard and mouseAXIsProcessTrustedWithOptions with kAXTrustedCheckOptionPrompt
Screen RecordingSee the screenCGRequestScreenCaptureAccess()

Or open the permission's pane in System Settings for the user:

System Settings panesText
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_missing lists 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

UsageText
sai-runtime run [--enroll] [--name <label>]
sai-runtime --version
ArgumentNotes
runConnect the computer to Sai and stay connected until stopped.
--enrollEnroll first, with the enrollment code in the SAI_ENROLLMENT_TOKEN environment variable. Ignored once the computer is enrolled. See Enrolling.
--name <label>The machine's name when it enrolls, up to 100 characters. Default: the computer's host name.
--versionPrint the version and exit.

Start it and keep it running

  • Start sai-runtime run when 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 --enroll only after it exits 10.
  • 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 next run exits 10, and enrolling again creates a new machine.

A supervisor, in outline:

SupervisorText
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 s

Events

The runtime writes events to stdout, one JSON object per line:

stdoutJSON
{"type":"enrolled","machineId":"tm_5a1f0c9e2b7d4e6a8c3f1b2d"}
{"type":"permissions_missing","missing":["accessibility","screen_recording"]}
{"type":"ready","machineId":"tm_5a1f0c9e2b7d4e6a8c3f1b2d"}
{"type":"busy"}
{"type":"idle"}
typeWhenYour app
enrolledOnce, right after the computer enrolled. Carries the new machineId.Send machineId to your backend. See Enrolling.
readyConnected to Sai: on every start, and again after disconnected. Carries the same machineId each time.
disconnectedNo connection to Sai for 30 seconds after a ready: the network went, or the computer slept.Don't restart it: it reconnects by itself.
busyA run started using the mouse and keyboard.
idleThat run ended: about a minute after its last action, or when the runtime stops.
permissions_missingAt start, and whenever the set changes, including to none ("missing": []). missing lists accessibility, screen_recording or both.Ask the user for what's missing. See macOS permissions.
  • busy and idle come 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 disconnected can 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

CodeMeansYour app
0Stopped: you asked, or it got a signal.Nothing. If you didn't ask (the user is logging out, say), don't start it again.
10Not enrolled: no machine yet, or the enrollment code was missing, used or expired.Get a code from your backend and start run --enroll. See Enrolling.
11The machine was revoked. The runtime deleted its credential.Don't restart it. Enroll again only if your backend wants this computer back, as a new machine.
12The runtime is older than Sai accepts.Don't restart it. Your app needs an update with a newer runtime.
13The Tenant or your Organisation is disabled. The credential is kept.Try again later, every 10 minutes or so, or when your backend says the Tenant is active. Never enroll again for this.
14Another runtime already runs for this OS user.Don't start another. Leave the running one to the copy of your app that started it.
15Its connection to Sai stalled, and only a new process gets a fresh one.Start it again at once, without backoff.
Any otherA crash (1), a command line it didn't understand (2), or a signal you didn't send.Start it again after a backoff: 1 second, doubling up to a minute, with some jitter. A 2 is a bug in your app.

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

  1. run exits 10.
  2. 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.
  3. Your app starts sai-runtime run --enroll --name "<label>" with SAI_ENROLLMENT_TOKEN=<code> in its environment.
  4. The runtime enrolls and prints enrolled with the new machineId. Your app sends it to your backend, which checks it with GET /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 10 again. Get a new one. If several in a row are refused, back off instead of asking for more.
  • Name the machine. --name is 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 exits 0, 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

  1. Your app starts run. It exits 10.
  2. Your app asks your backend for a code. Your backend finds or creates the user's Tenant and creates a code for it.
  3. Your app starts run --enroll with the code. The runtime prints enrolled, and your app sends the machineId to your backend, which checks and records it.
  4. The runtime prints permissions_missing, and your app asks the user for them.
  5. 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.

  1. Before the runtime starts again, your backend revokes the previous user's machine.
  2. Your app stops the runtime if it runs, and starts run. It exits 11 and deletes its credential.
  3. 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.