@stim-cli/server 1.20.2 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +785 -276
- package/dist/{protocol-CjUChTWJ.mjs → protocol-ZY9j2h32.mjs} +1038 -79
- package/dist/protocol.mjs +1 -1
- package/dist/protocol.schema.json +2760 -122
- package/dist/stim-frames/ProcessStream.swift +2 -1
- package/dist/stim-frames/SimulatorHID.swift +33 -7
- package/dist/stim-frames/SimulatorPosture.swift +0 -6
- package/dist/{stim-server-FWcuNpW7.mjs → stim-server-BKcEacbU.mjs} +4894 -2942
- package/dist/stim-server.mjs +1 -1
- package/dist/stim-service-runner.d.mts +13 -0
- package/dist/stim-service-runner.mjs +96 -0
- package/package.json +8 -3
- package/dist/stats-read.d.mts +0 -1
- package/dist/stats-read.mjs +0 -46
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# @stim-cli/server
|
|
2
2
|
|
|
3
3
|
`stim-server` serves Stim state to paired clients, such as the Stim phone app,
|
|
4
|
-
and lets the clients the
|
|
5
|
-
in a workspace. It runs on the
|
|
6
|
-
|
|
4
|
+
and lets the clients the machine grants control run `stim reload` and `stim stop`
|
|
5
|
+
in a workspace. It runs on the same machine as Stim. Private Desktop project
|
|
6
|
+
checks also record completed doctor checks in project state. It writes its own pairing state and action
|
|
7
7
|
log under `$STIM_HOME/server/`, and the device recordings described under
|
|
8
8
|
[Recording](#recording).
|
|
9
9
|
|
|
@@ -16,23 +16,22 @@ The design is in
|
|
|
16
16
|
stim-server [--port <n>] [--loopback-only]
|
|
17
17
|
# serve paired clients, port 7787 by default
|
|
18
18
|
stim-server pair [--port <n>] [--control]
|
|
19
|
-
# print a single-use pairing payload
|
|
20
|
-
stim-server devices [list] # list pairings,
|
|
21
|
-
stim-server devices grant <id> --control|--read
|
|
22
|
-
# let a paired device run actions, or only read
|
|
23
|
-
|
|
24
|
-
stim-server devices revoke <id> # revoke a paired device or client, or deny a request
|
|
19
|
+
# print a single-use pairing payload with this server's key fingerprint
|
|
20
|
+
stim-server devices [list] # list pairings, approved clients and hosted sessions
|
|
21
|
+
stim-server devices grant <id> --control|--read
|
|
22
|
+
# let a paired device run actions, or only read
|
|
23
|
+
stim-server devices revoke <id> # revoke a paired device or approved client and close its connections
|
|
25
24
|
stim-server log # list the actions paired devices ran
|
|
26
|
-
stim-server setup --
|
|
27
|
-
# worker-side approval
|
|
25
|
+
STIM_SETUP_TICKET=<t> stim-server setup --expires <iso> --build|--device-host
|
|
26
|
+
# worker-side approval of the machine holding this ticket, see below
|
|
28
27
|
stim-server service install|status|update|rollback|uninstall
|
|
29
28
|
# run stim-server as a macOS LaunchAgent, see below
|
|
30
29
|
```
|
|
31
30
|
|
|
32
31
|
`--loopback-only` makes the server listen on `127.0.0.1` only and refuse any request
|
|
33
32
|
that names a remote peer (`X-Forwarded-For`, as `tailscale serve --https` adds), so
|
|
34
|
-
phones and other
|
|
35
|
-
Without it, the server also listens on this
|
|
33
|
+
phones and other machines cannot connect; `/health` then reports `loopbackOnly: true`.
|
|
34
|
+
Without it, the server also listens on this machine's Tailscale addresses while Tailscale
|
|
36
35
|
runs (see [Tailscale](#tailscale)). Stim Desktop starts its own server with the flag
|
|
37
36
|
unless it serves phones.
|
|
38
37
|
|
|
@@ -42,17 +41,16 @@ serving command: see [Run as a service](#run-as-a-service). When serving, a bare
|
|
|
42
41
|
|
|
43
42
|
`pair --json` prints `{ "qr": <payload>, "expiresAt": "<ISO time>" }`, and
|
|
44
43
|
`devices --json` prints `{ "devices": [...] }` with each device's `id`, `name`,
|
|
45
|
-
`
|
|
46
|
-
followed by the [build clients](#build-access)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
pending record without `requestedCapability` is a build request.
|
|
44
|
+
`from` (where it paired from, as a label only), `keyFingerprint`, `pairedAt`,
|
|
45
|
+
`lastSeenAt` and `capabilities`, followed by the [build clients](#build-access)
|
|
46
|
+
and [device-host clients](#device-host-approval). The plain listing shows
|
|
47
|
+
`from` and the first 12 characters of the key fingerprint.
|
|
50
48
|
`log --json` prints `{ "actions": [...] }`, the records described under
|
|
51
49
|
[Actions](#actions).
|
|
52
50
|
|
|
53
|
-
`GET http://127.0.0.1:7787/health` answers requests from this
|
|
51
|
+
`GET http://127.0.0.1:7787/health` answers requests from this machine with the
|
|
54
52
|
server's name, versions, `stimBuild` (the digest of the bundled Stim's build,
|
|
55
|
-
which offload compares between
|
|
53
|
+
which offload compares between machines), protocol, `stimHome`, `startup`, `busy`
|
|
56
54
|
(the offloaded `builds` and `hostedSessions` running now) and the current
|
|
57
55
|
Tailscale state. It answers HTTP 200 once `startup.state` is `ready`; while the
|
|
58
56
|
state is `pending` or `degraded` (see the LaunchAgent notes below) it answers
|
|
@@ -65,75 +63,320 @@ setup command would use. When the server runs under Stim Host, it also carries
|
|
|
65
63
|
app (checked at most every 5 seconds), or `null` when the check fails. Stim
|
|
66
64
|
Desktop uses it to find a running server and show its route. A request through `tailscale serve` or on a Tailscale address
|
|
67
65
|
without an `Origin` or `Sec-Fetch-Site` header, which a web page's request
|
|
68
|
-
carries, gets only `{ "
|
|
69
|
-
Desktop's Remote
|
|
70
|
-
tailnet. A request from this
|
|
66
|
+
carries, gets only `{ "version" }`, which Stim
|
|
67
|
+
Desktop's Remote Machines list uses to find stim-server on the other machines of the
|
|
68
|
+
tailnet. A request from this machine with a `Host` other than `127.0.0.1` or
|
|
71
69
|
`localhost` gets HTTP 426, like any other plain HTTP request.
|
|
72
70
|
|
|
73
|
-
`GET /setup/<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
The journal lives at `$STIM_HOME/server/setup/<
|
|
80
|
-
only by stim-server and read through core's state reader. It contains
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
The route returns 503 until startup is ready, caches peer identity for 30 seconds
|
|
86
|
-
and returns 429 after repeated identity failures. All responses use
|
|
71
|
+
`GET /setup/<journal>` returns a setup journal. The name is exactly 64
|
|
72
|
+
lowercase hex characters, `SHA-256("stim setup journal v1" || ticket)` of the
|
|
73
|
+
32-byte setup ticket, so only the ticket's holder can derive it. The journal
|
|
74
|
+
holds no secret, so the route serves it to whoever names it; it never
|
|
75
|
+
authenticates the caller.
|
|
76
|
+
|
|
77
|
+
The journal lives at `$STIM_HOME/server/setup/<journal>.json`, written atomically
|
|
78
|
+
only by stim-server and read through core's state reader. It contains ISO
|
|
79
|
+
`expiresAt`, requested capabilities, steps, granted record ids, completion
|
|
80
|
+
state and optional exit code; it contains no ticket, PSK or token. Browser
|
|
81
|
+
requests, invalid paths, missing, malformed or expired journals get the same 404. The route returns 503 until startup is ready and 429 after repeated
|
|
82
|
+
failures from one peer address. All responses use
|
|
87
83
|
`cache-control: no-store`. Steps whose ids start with `tools` appear only after
|
|
88
84
|
a build grant. The server prunes journals more than one hour past expiry at
|
|
89
85
|
startup and hourly.
|
|
90
86
|
|
|
91
87
|
`stim-server` runs the `stim` version this package was released with, not the
|
|
92
|
-
one on your PATH. It
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
one on your PATH. It never runs a login shell. At start it fills the gaps in
|
|
89
|
+
the environment it was given, the way `stim doctor` reports under **Toolchain**:
|
|
90
|
+
it appends the default Node of nvm, fnm, Volta, mise or asdf, Homebrew,
|
|
91
|
+
`~/.local/bin`, pnpm's and bun's global directories and the Android SDK tools to
|
|
92
|
+
`PATH`, sets `ANDROID_HOME` to `~/Library/Android/sdk` when that exists, and sets
|
|
93
|
+
`JAVA_HOME` to a Homebrew `openjdk` (17, then 21, then the newest), else Android
|
|
94
|
+
Studio's bundled runtime, when no JDK is installed under
|
|
95
|
+
`/Library/Java/JavaVirtualMachines`. It never replaces a variable it was given,
|
|
96
|
+
and its own Node's directory is always the last `PATH` entry. CocoaPods uses the
|
|
97
|
+
project's `.ruby-version` Ruby, else rvm's or rbenv's default Ruby, and reads the
|
|
98
|
+
login shell only when neither exists. Anything else a shell profile sets, such as
|
|
99
|
+
`STIM_*` variables, reaches the server only through its environment or `--env`.
|
|
100
|
+
|
|
101
|
+
## Private Desktop bootstrap
|
|
102
|
+
|
|
103
|
+
When Desktop launches its bundled server, it passes a fresh 32-byte key through
|
|
104
|
+
the child's stdin pipe. The key stays in memory; it is absent from arguments,
|
|
105
|
+
environment values, logs and pairing records. `STIM_DESKTOP_BOOTSTRAP_FD=0`
|
|
106
|
+
marks that private startup input and is removed before the server starts other
|
|
107
|
+
processes. Invalid or missing marked input refuses startup.
|
|
108
|
+
Desktop keeps the pipe open until its child exits. The server shuts down when
|
|
109
|
+
that pipe closes, including when Desktop crashes or is forcibly killed.
|
|
110
|
+
|
|
111
|
+
A connection on the local Unix socket can send `desktopBootstrap: true` in
|
|
112
|
+
`hello`. The server returns a fresh per-connection nonce and an HMAC-SHA256
|
|
113
|
+
proof over `server:<nonce>`. Desktop verifies the child before sending the
|
|
114
|
+
separate `client:<nonce>` proof through `desktop.authenticate`. Each challenge
|
|
115
|
+
answers one proof, so a wrong, replayed or late proof cannot win; disconnect
|
|
116
|
+
ends the authority, and it adds no persisted capability. Network connections,
|
|
117
|
+
which run Noise, never get a challenge.
|
|
118
|
+
|
|
119
|
+
On a server Desktop started, only a socket that passed this proof can call the
|
|
120
|
+
Desktop-only methods (`route.setup`, `machines.update.*`, `device-host.sessions`
|
|
121
|
+
and `device-host.sessions.stop`, plus `desktop.reports.*`, `desktop.settings.get`,
|
|
122
|
+
`desktop.logs.subscribe`, `desktop.doctor.run` and `desktop.stop.subscribe`); another client of this user on the socket, such
|
|
123
|
+
as the CLI, keeps read and control without them. A server Desktop adopted or that
|
|
124
|
+
runs on its own, such as a `stim-server service`, has no private key: it offers no
|
|
125
|
+
challenge and keeps serving those methods to this user's socket. Starting a fresh
|
|
126
|
+
bundled child establishes a new private key. Desktop also refuses a socket whose
|
|
127
|
+
server reports another TCP port than the server it adopted, before it sends any
|
|
128
|
+
proof.
|
|
129
|
+
|
|
130
|
+
`desktop.settings.write` requires the private proof even on an adopted or
|
|
131
|
+
standalone server. Only a server started by Desktop advertises the local-only
|
|
132
|
+
`desktop-settings-write` feature. The method accepts an absolute existing `cwd`,
|
|
133
|
+
setting `key`, optional `scope`, and either `"action": "set"` with a text `value`,
|
|
134
|
+
or `"action": "unset"` without `value`. It runs the shared settings API in process
|
|
135
|
+
and returns its masked setting payload or a typed refusal (`code`, `message`,
|
|
136
|
+
`remedy`). Timeout and output failures use the RPC error channel. Writes resolve
|
|
137
|
+
the selected folder through the shared settings API and share its workspace
|
|
138
|
+
admission with control actions. They stay admitted until their cleanup
|
|
139
|
+
drains, including after timeout or disconnect. Pending writes cancel; recording
|
|
140
|
+
deletion after commit finishes before shutdown. The audit records the key, scope
|
|
141
|
+
and result without setting values. This method grants no phone or network access.
|
|
142
|
+
|
|
143
|
+
The local-only `desktop-doctor-run` feature provides `desktop.doctor.run` with
|
|
144
|
+
an absolute `cwd` and optional `ios` or `android` `platform`. It performs a fresh
|
|
145
|
+
doctor check without a CLI process or repairs, returns the doctor JSON report,
|
|
146
|
+
and records the completed native check in project state. A chosen supported
|
|
147
|
+
project need not already be registered. Recording happens after report execution
|
|
148
|
+
and owned cleanup succeed. Recording waits asynchronously for the config lock,
|
|
149
|
+
honors cancellation and reports unavailable state. The existing private Desktop bootstrap gate applies;
|
|
150
|
+
phone and network clients cannot call it or see the feature. Request caps, output
|
|
151
|
+
limits and the report execution/cleanup budgets apply. Disconnect or shutdown
|
|
152
|
+
cancels the request and drains its owned work. Cached doctor report reads do not
|
|
153
|
+
record checks.
|
|
154
|
+
|
|
155
|
+
The local-only `desktop-run` feature provides `desktop.run.subscribe` with an
|
|
156
|
+
absolute `cwd` and `options` matching the typed `stim.run` selectors. For example,
|
|
157
|
+
`{ "cwd": "/absolute/app", "options": { "platform": "ios", "slot": "fold", "remote": "mini" } }`
|
|
158
|
+
builds, installs and launches through the in-process API. Only a proved private
|
|
159
|
+
Desktop session can call it; adopted servers and local control alone cannot.
|
|
160
|
+
`desktop-run-progress` carries ordered `stream`/`message` chunks, followed by
|
|
161
|
+
`desktop-run-ended` with `exitCode`, a human `message`, and `stdout` containing
|
|
162
|
+
the serialized `{ platform, facts }` API result or typed refusal. The default
|
|
163
|
+
execution budget is one hour; progress plus the terminal JSON are limited to
|
|
164
|
+
1 MiB. Cancellation, unsubscribe, disconnect and server shutdown cancel and drain
|
|
165
|
+
owned work before releasing workspace admission. Unresolved cleanup retains the
|
|
166
|
+
reservation until every recorded claim is recovered. If cleanup cannot name its
|
|
167
|
+
claims, admission stays blocked until the server restarts; recover the owned work
|
|
168
|
+
before restarting. Reconnect never replays a run. Desktop's platform buttons
|
|
169
|
+
still use the CLI until their consumer migration.
|
|
170
|
+
|
|
171
|
+
The private `desktop-build` feature provides `desktop.build.subscribe` with an
|
|
172
|
+
absolute `cwd` and artifact-only `options`: `ios` accepts `configuration`, `scheme`
|
|
173
|
+
and `arch`; `android` accepts `variant` and `abi`; both accept `buildCache` and
|
|
174
|
+
`remoteBuild`. `macos` accepts `remoteBuild`. It invokes `createStim.build`
|
|
175
|
+
without acquiring a device or starting a runtime. Device, slot, hosted-launch,
|
|
176
|
+
Metro and EAS selectors are refused. It uses the same proved Desktop authority,
|
|
177
|
+
one-hour default budget, 1 MiB output bound and cleanup admission as platform
|
|
178
|
+
run. Ordered `desktop-build-progress` events end with `desktop-build-ended`,
|
|
179
|
+
whose `stdout` contains `{ platform, facts }` or the typed refusal. Reconnecting
|
|
180
|
+
never replays a build. This transport is a migration prerequisite; existing
|
|
181
|
+
Desktop buttons do not use it yet.
|
|
182
|
+
|
|
183
|
+
The private `desktop-gc` feature provides `desktop.gc.subscribe` with an absolute
|
|
184
|
+
existing `cwd` and optional `cache`, `olderThan` (whole days), `worktrees`, `delete`
|
|
185
|
+
and `idleMs` (positive whole milliseconds). It invokes `createStim.collectGarbage`
|
|
186
|
+
with the same scoped cleanup and ownership rules as the CLI. Only the proved
|
|
187
|
+
Desktop connection can call it; phone, network and adopted-server connections
|
|
188
|
+
cannot. Ordered `desktop-gc-progress` events end with `desktop-gc-ended`, whose
|
|
189
|
+
`stdout` contains the GC JSON report or typed refusal and whose `exitCode` preserves
|
|
190
|
+
partial cleanup failures. It preserves the API's 60-second execution and 35-second
|
|
191
|
+
cleanup budgets. The server's request deadline can cancel execution earlier; its
|
|
192
|
+
progress plus terminal JSON bound is 1 MiB. Unsubscribe, disconnect, shutdown and timeout
|
|
193
|
+
cancel and drain owned work before releasing admission; unresolved cleanup retains
|
|
194
|
+
the reservation and its recovery claims. Settled requests invalidate cached Desktop
|
|
195
|
+
reports. Reconnect never repeats cleanup. Desktop GC buttons still use the CLI
|
|
196
|
+
until their consumer migration.
|
|
197
|
+
|
|
198
|
+
The private `desktop-worktree-remove` feature provides
|
|
199
|
+
`desktop.worktree.remove.subscribe` with an absolute existing `cwd`, an optional
|
|
200
|
+
absolute `target`, and optional boolean `force`. It calls `createStim.removeWorktree`
|
|
201
|
+
with the CLI's safety checks and ownership rules. A separate target can be absent
|
|
202
|
+
on disk when retrying pending branch cleanup. Only the proved connection of the
|
|
203
|
+
Desktop that started this server can call it; paired phones and adopted local
|
|
204
|
+
connections cannot.
|
|
205
|
+
|
|
206
|
+
Ordered `desktop-worktree-remove-progress` events end with
|
|
207
|
+
`desktop-worktree-remove-ended`. Its `stdout` contains `{ removed, exitCode }` or
|
|
208
|
+
a typed refusal. Admission covers the resolved checkout or project and every
|
|
209
|
+
registered descendant reclaimed by cleanup, including when the request names a
|
|
210
|
+
subfolder. Deletion and cleanup success remain separate: source cleanup
|
|
211
|
+
can succeed without deleting a checkout, and branch cleanup can fail after a
|
|
212
|
+
linked checkout was deleted. `force` has the CLI's destructive meaning. The API's
|
|
213
|
+
60-second execution and 35-second cleanup budgets and the server's deadline and
|
|
214
|
+
output limit apply. Cancellation drains owned work before releasing admission;
|
|
215
|
+
unresolved cleanup retains its recovery claims. Settled requests invalidate
|
|
216
|
+
cached Desktop reports. Reconnect never repeats removal. Desktop callers migrate
|
|
217
|
+
separately.
|
|
218
|
+
|
|
219
|
+
The private `desktop-worktree-warm` feature provides
|
|
220
|
+
`desktop.worktree.warm.subscribe` with only an absolute existing `cwd`. It calls
|
|
221
|
+
`createStim.warmWorktree` for plain copying; refresh and dependency installation
|
|
222
|
+
remain CLI-only. Only the proved connection of the Desktop that started this
|
|
223
|
+
server can call it. Phones, network clients and adopted local connections cannot.
|
|
224
|
+
|
|
225
|
+
Ordered `desktop-worktree-warm-progress` events end with
|
|
226
|
+
`desktop-worktree-warm-ended`, whose `stdout` contains `{ exitCode }` or a typed
|
|
227
|
+
refusal. The default request budget is 90 minutes, with an explicitly configured
|
|
228
|
+
server action deadline taking precedence. The API's 35-second cleanup budget and
|
|
229
|
+
the server's 1 MiB progress-plus-result bound apply. Admission covers the resolved
|
|
230
|
+
linked checkout, its registered descendants and the inferred app before effects.
|
|
231
|
+
Cancellation drains owned work before releasing admission; unverifiable cleanup
|
|
232
|
+
retains admission until its exact recovery claims are cleared. Settled requests
|
|
233
|
+
invalidate cached Desktop reports. Reconnect never repeats warming. Desktop
|
|
234
|
+
callers migrate separately.
|
|
235
|
+
|
|
236
|
+
The private `desktop-plan` feature provides `desktop.plan.get` with an absolute
|
|
237
|
+
existing `cwd`, `platform` (`ios`, `android` or `macos`) and an optional iOS/Android
|
|
238
|
+
`slot`. An optional `request` identifies that connection's check. It returns the shared `createStim.plan` report or a typed `{ code, message, remedy }` refusal without a CLI process,
|
|
239
|
+
building or launching. Only the proved Desktop connection can select folders;
|
|
240
|
+
phones and ordinary local connections cannot use it. The existing `build.plan`
|
|
241
|
+
method remains limited to registered iOS/Android workspaces. Both paths share
|
|
242
|
+
per-directory queues, connection request limits, output bounds and cancellation.
|
|
243
|
+
The planner has a 60-second execution and 35-second cleanup budget inside the
|
|
244
|
+
server's 150-second request deadline, which starts after queued work drains.
|
|
245
|
+
These bounded plans support EAS and Stim cache providers; legacy custom Expo
|
|
246
|
+
cache plugins are checked only by CLI plans.
|
|
247
|
+
Desktop preview consumers use this private read. `desktop.plan.cancel { request }` cancels only a check on the same proved connection and replies after its queued work and cleanup drain. A retained cleanup failure is included as `refusal` in the cancellation result; other connections cannot cancel it. Shared subscriptions stay connected.
|
|
248
|
+
|
|
249
|
+
The local-only `desktop-stop` feature provides `desktop.stop.subscribe` with an
|
|
250
|
+
absolute `cwd` and optional `slot` (`web` closes only the owned Chrome). It stops
|
|
251
|
+
that exact canonical workspace through the shared API, without searching for a
|
|
252
|
+
monorepo child or starting the CLI. The response gives a subscription; ordered
|
|
253
|
+
`desktop-stop-progress` events carry `stream` and `message` chunks, followed by
|
|
254
|
+
`desktop-stop-ended` with `exitCode`, CLI-compatible JSON `stdout` and a human
|
|
255
|
+
summary or failure `message`. Cumulative progress and final JSON have separate
|
|
256
|
+
output bounds, and the action duration limit applies. A terminal
|
|
257
|
+
timeout or output failure does not release workspace admission until actual
|
|
258
|
+
cleanup finishes. Public control actions share this admission. Unsubscribe or
|
|
259
|
+
disconnect cancels waiting work; teardown already underway finishes. Server
|
|
260
|
+
shutdown cancels and drains it, and the action audit records the result. This
|
|
261
|
+
method does not change phone or network permissions. Desktop stop consumers
|
|
262
|
+
still use the CLI until they adopt this feature.
|
|
263
|
+
|
|
264
|
+
The local-only `desktop-settings` feature provides `desktop.settings.get` with
|
|
265
|
+
`{ "cwd": "/absolute/folder" }`. It reads the same masked settings, layers,
|
|
266
|
+
environment overrides and source files as `stim settings --json`, including for
|
|
267
|
+
a selected folder that is not registered with Stim. Each call reads current
|
|
268
|
+
settings, without a CLI process or a shared report cache. The existing Desktop
|
|
269
|
+
bootstrap boundary, request cap, byte quota, cancellation and 60-second read limit
|
|
270
|
+
apply. Network clients retain `settings.get`'s registered-workspace restriction.
|
|
271
|
+
|
|
272
|
+
The local-only `desktop-reports` feature provides full doctor and GC dry-run
|
|
273
|
+
reports without starting a CLI process. `desktop.reports.get` takes
|
|
274
|
+
`{ "report": "gc" }` or `{ "report": "doctor", "cwd": "/absolute/project" }`.
|
|
275
|
+
Doctor accepts a selected folder before it is registered with Stim. The result is
|
|
276
|
+
`{ "pending": false, "value": { ... }, "measuredAt": "..." }`; a failed read
|
|
277
|
+
returns `stim-failed`. Doctor never repairs the project or records a CLI run, and
|
|
278
|
+
GC never deletes anything.
|
|
279
|
+
|
|
280
|
+
The local-only `desktop-report-platform` feature adds an optional `"platform": "ios"`
|
|
281
|
+
or `"platform": "android"` to doctor targets. Omitting it checks all selected platforms.
|
|
282
|
+
Reports for each cwd and selector have separate cache entries and subscriptions.
|
|
283
|
+
Clients requiring a filtered report check this feature before requesting one.
|
|
284
|
+
|
|
285
|
+
`desktop.reports.subscribe` takes the same target and returns a subscription id.
|
|
286
|
+
It pushes `desktop-report` events with that `subscription`, `target`, and a
|
|
287
|
+
`snapshot` containing `pending`, `value`, optional `error`, and `measuredAt` after
|
|
288
|
+
the first completed read. During refresh it retains the last settled value or
|
|
289
|
+
error. All connections share one in-flight read and a 60-second result per target.
|
|
290
|
+
Active subscriptions refresh after 60 seconds; no refresh timer runs without
|
|
291
|
+
subscribers. `unsubscribe` and disconnect remove the subscription. Cancelling a
|
|
292
|
+
get request does not cancel another reader's shared work.
|
|
293
|
+
|
|
294
|
+
After changing settings, running an action, or explicitly refreshing a view,
|
|
295
|
+
Desktop calls `desktop.reports.invalidate` without parameters. This invalidates
|
|
296
|
+
all report targets and refreshes active subscriptions. Results started before
|
|
297
|
+
invalidation cannot replace a newer result or satisfy a waiting get. The existing
|
|
298
|
+
request and subscription caps, report byte quota, API execution and cleanup
|
|
299
|
+
budgets, and 150-second server limit apply. At most four shared report probes run
|
|
300
|
+
at once across connections and targets. A new target above that limit returns a
|
|
301
|
+
busy error without starting or queuing work; invalidate and retry after a probe
|
|
302
|
+
finishes. Unsubscribing does not release a probe's slot before it drains.
|
|
303
|
+
Server shutdown stops refreshes and
|
|
304
|
+
cancels and drains the shared probes. These methods and events are excluded from
|
|
305
|
+
the phone protocol and unavailable over network connections.
|
|
306
|
+
|
|
307
|
+
`desktop.logs.subscribe` reads a registered workspace's complete retained logs
|
|
308
|
+
for the private local Desktop connection. It accepts `workspace` and optional
|
|
309
|
+
`sources`, `level`, `grep` and `errors`; Desktop applies scoped build time, platform
|
|
310
|
+
and legacy slot bounds. `desktop-full-logs` advertises it only on the local socket,
|
|
311
|
+
with the same private bootstrap admission as Desktop reports. Network and phone
|
|
312
|
+
clients keep the existing 5,000-record tail limit.
|
|
313
|
+
|
|
314
|
+
Full backfill selects timeline records in batches of at most 500 and waits for
|
|
315
|
+
the send queue before selecting more. Agent actions still use the existing
|
|
316
|
+
attribution reader and retain matching records during backfill. A disconnect,
|
|
317
|
+
unsubscribe or backfill deadline cancels the read and releases its open files.
|
|
318
|
+
|
|
319
|
+
## Set up a worker machine
|
|
320
|
+
|
|
321
|
+
A person on the worker runs the command generated for their client machine.
|
|
322
|
+
The client machine makes the setup ticket, 32 random bytes: Stim Desktop's Add
|
|
323
|
+
Machine flow shows the command, and on a machine without Desktop,
|
|
324
|
+
`stim doctor --fix` prints it.
|
|
99
325
|
|
|
100
326
|
```bash
|
|
101
|
-
|
|
327
|
+
STIM_SETUP_TICKET=<43-base64url-characters> stim-server setup --expires <ISO-time> \
|
|
102
328
|
--build --device-host [--port <n>] [--label <name>] \
|
|
103
329
|
[--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json] [--verbose]
|
|
104
330
|
```
|
|
105
331
|
|
|
332
|
+
The ticket travels in the `STIM_SETUP_TICKET` environment variable, because
|
|
333
|
+
other users on the worker can read a process's command-line arguments but not
|
|
334
|
+
its environment. Setup refuses `--ticket` and `--client`: the ticket alone
|
|
335
|
+
names the requesting machine. On Windows the generated command runs in a PowerShell script block that
|
|
336
|
+
sets `$env:STIM_SETUP_TICKET` and removes it when setup ends.
|
|
337
|
+
|
|
106
338
|
If the server is not installed globally, use `npx --yes --package @stim-cli/server@<version> stim-server`
|
|
107
|
-
in place of `stim-server`, with the exact version from the client
|
|
339
|
+
in place of `stim-server`, with the exact version from the client machine.
|
|
108
340
|
At least one capability is required. The expiry must be in the future and at
|
|
109
341
|
most two hours ahead. The defaults are port 7787 and label `dev.stim.server`.
|
|
110
342
|
Argument and preflight refusals change nothing. Preflight requires macOS,
|
|
111
|
-
Node 22.12+, running Tailscale,
|
|
112
|
-
GUI login session.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
343
|
+
Linux or Windows, Node 22.12+, running Tailscale, and
|
|
344
|
+
this user's GUI login session on macOS or systemd user manager on Linux. On
|
|
345
|
+
Linux and Windows, setup takes `--build` only and refuses `--device-host`, because hosting
|
|
346
|
+
needs a Mac; see [On Linux](#on-linux) and [On Windows](#on-windows). Off macOS, stim-server also refuses a
|
|
347
|
+
device-host connection at hello.
|
|
348
|
+
|
|
349
|
+
Running setup on the worker is the worker-side approval. It approves the
|
|
350
|
+
client key that holds this ticket, once per chosen capability, until this
|
|
351
|
+
expiry. A terminal asks Y/n for each grant (default Yes: Enter, `y` and `yes` approve;
|
|
117
352
|
`n`, `no` and any other answer decline); use `--yes` to approve without those
|
|
118
353
|
questions, including without a terminal. Without
|
|
119
354
|
a terminal or `--yes`, setup refuses before installing anything unless every
|
|
120
355
|
chosen capability already has a matching approval. A person on
|
|
121
|
-
the worker still approves; an SSH-driven run is not offered.
|
|
122
|
-
|
|
356
|
+
the worker still approves; an SSH-driven run is not offered. Only after the
|
|
357
|
+
person approves does setup store the ticket's derived PSK and lookup in
|
|
358
|
+
`$STIM_HOME/server/secrets.json`. Until then the requesting machine's handshake
|
|
359
|
+
gets `unknown-secret`, and its `stim doctor` reports the machine as pending and
|
|
360
|
+
tries again on every run. The requester's next first-contact handshake with the
|
|
361
|
+
ticket records its public key once per approved capability, which spends the
|
|
362
|
+
ticket. From then on the ticket admits only that key, until it expires, so a
|
|
363
|
+
requester that lost the reply can retry; setup deletes an unspent secret when it
|
|
364
|
+
ends. Build and
|
|
365
|
+
device-host approvals are separate records. A build grant carries no `read` or
|
|
123
366
|
`control` capability, but it is full access to this account in practice (see
|
|
124
367
|
[Build access](#build-access)).
|
|
125
|
-
A rerun before expiry reports
|
|
126
|
-
|
|
127
|
-
any change. Requests themselves lapse after 15 minutes.
|
|
368
|
+
A rerun before expiry reports capabilities the ticket already recorded as
|
|
369
|
+
already approved. After expiry it refuses with exit 2 before any change.
|
|
128
370
|
|
|
129
|
-
Setup installs its exact server release into
|
|
130
|
-
`~/Library/Application Support/Stim/services/<label>/versions
|
|
131
|
-
Stim Host, installs or reuses the managed
|
|
371
|
+
Setup installs its exact server release into the service's `versions/`
|
|
372
|
+
directory (`~/Library/Application Support/Stim/services/<label>/versions/` on
|
|
373
|
+
macOS), installs Stim Host on macOS, installs or reuses the managed
|
|
374
|
+
LaunchAgent, systemd user service or scheduled task, and creates or reuses a
|
|
132
375
|
tailnet-only HTTPS route on 7443 or the next free port. Older managed services
|
|
133
376
|
use the service update busy-wait and rollback behavior. Newer versions stay
|
|
134
|
-
installed; tools report a Stim build difference. A reused server must
|
|
135
|
-
|
|
136
|
-
|
|
377
|
+
installed; tools report a Stim build difference. A reused server must
|
|
378
|
+
speak protocol 2 (stim-server **1.21.0** or later), which setup tickets
|
|
379
|
+
authenticate with. An older server refuses immediately with an update remedy.
|
|
137
380
|
|
|
138
381
|
When a Desktop-run server already answers on the port, setup reuses it and
|
|
139
382
|
installs no LaunchAgent. It requires an existing tailnet route and refuses
|
|
@@ -168,7 +411,7 @@ earlier releases did. Plain progress goes to stdout; errors use
|
|
|
168
411
|
`stim-server: <message>` on stderr (when stdout is a terminal, the refusal appears on stdout only).
|
|
169
412
|
`--json` sends the long (verbose) progress to stderr, without the banner, and prints one final payload with `ok`,
|
|
170
413
|
`label`, `port`, `route` (state, DNS name, HTTPS port), `server` (version and
|
|
171
|
-
Stim build), `managed`, separate `granted`
|
|
414
|
+
Stim build), `managed`, separate `granted` record ids and client names,
|
|
172
415
|
`permissions`, `tools` and `warnings`. Final permission values are `granted`,
|
|
173
416
|
`denied` (not authorized), `skipped` or `not-needed`; a timed-out wait remains
|
|
174
417
|
`pending` in the journal and names its unavailable feature. Exit codes come
|
|
@@ -181,13 +424,13 @@ from the final journal:
|
|
|
181
424
|
| 2 | No grant before expiry, or an already expired command. |
|
|
182
425
|
| 3 | At least one grant, but another chosen capability, permission or tool is missing; the output names affected features. |
|
|
183
426
|
|
|
184
|
-
Setup writes `$STIM_HOME/server/setup/<
|
|
427
|
+
Setup writes `$STIM_HOME/server/setup/<journal>.json` from its first
|
|
185
428
|
step and after every change, including completion and the exit code. The
|
|
186
|
-
|
|
429
|
+
`GET /setup/<journal>` route described above mirrors it until expiry,
|
|
187
430
|
hiding tools until a build grant. Each valid setup run prunes journals more
|
|
188
431
|
than one hour past expiry. A dedicated `server/setup.claims` ownership claim
|
|
189
432
|
serializes runs; the service update claim is released before waiting for
|
|
190
|
-
|
|
433
|
+
the requesting machine and permissions. A competing setup refuses with the exact claim
|
|
191
434
|
and removal command. Ctrl-C or SIGTERM finishes the running step as failed
|
|
192
435
|
with detail `interrupted`, completes the journal, releases the setup claim,
|
|
193
436
|
and exits 1. A typed N also exits 1; a question timeout follows ticket expiry
|
|
@@ -196,8 +439,8 @@ and exits 2.
|
|
|
196
439
|
The `To undo` line (or the `--verbose` summary) lists the undo commands:
|
|
197
440
|
|
|
198
441
|
```bash
|
|
199
|
-
stim-server devices revoke <build-
|
|
200
|
-
stim-server devices revoke <device-host-
|
|
442
|
+
stim-server devices revoke <build-client-id>
|
|
443
|
+
stim-server devices revoke <device-host-client-id>
|
|
201
444
|
stim-server service uninstall --label <label>
|
|
202
445
|
```
|
|
203
446
|
|
|
@@ -211,12 +454,13 @@ permissions remain; remove permission grants in System Settings if desired.
|
|
|
211
454
|
|
|
212
455
|
## Tailscale
|
|
213
456
|
|
|
214
|
-
Tailscale carries
|
|
215
|
-
|
|
216
|
-
|
|
457
|
+
Tailscale carries the traffic between the phone and the Mac, on the same
|
|
458
|
+
Wi-Fi or across networks. Stim authenticates and encrypts every network
|
|
459
|
+
connection itself with a Noise handshake (see [Pairing](#pairing)), so
|
|
460
|
+
Tailscale's identity of a peer only labels it. You need Tailscale on the Mac and on the phone, in the
|
|
217
461
|
same tailnet or with the Mac shared to the phone's user.
|
|
218
462
|
|
|
219
|
-
`stim-server` listens only on `127.0.0.1` and on the
|
|
463
|
+
`stim-server` listens only on `127.0.0.1` and on the machine's Tailscale
|
|
220
464
|
addresses, never on every interface. It re-reads the Tailscale state in the
|
|
221
465
|
background, so a server that started before Tailscale was up, or while it did
|
|
222
466
|
not answer, starts listening on the Tailscale addresses once it runs, and stops
|
|
@@ -247,10 +491,10 @@ Funnel port makes it public, and `pair` refuses.
|
|
|
247
491
|
When Tailscale is not running, `stim-server` listens on loopback only and
|
|
248
492
|
says so on stderr. Start Tailscale, then restart `stim-server`.
|
|
249
493
|
|
|
250
|
-
Desktop uses `route.setup` with no parameters on its
|
|
251
|
-
|
|
252
|
-
no-op. The server refuses
|
|
253
|
-
|
|
494
|
+
Desktop uses `route.setup` with no parameters on its connection to the local
|
|
495
|
+
Unix socket (see [Pairing](#pairing)). The result is a verified
|
|
496
|
+
`ServeRoute`; an existing route is a no-op. The server refuses it on any network
|
|
497
|
+
connection, unknown configuration and any route exposing this server
|
|
254
498
|
through Funnel. Only the fixed `tailscale serve --bg --https=<free port>
|
|
255
499
|
http://127.0.0.1:<server port>` invocation runs, followed by route verification.
|
|
256
500
|
No LaunchAgent or Funnel configuration is created. Failure output remains
|
|
@@ -261,35 +505,72 @@ available for an explicit retry, including Tailscale's HTTPS approval link.
|
|
|
261
505
|
`stim-server pair` prints the JSON that the pairing QR code encodes:
|
|
262
506
|
|
|
263
507
|
```json
|
|
264
|
-
{
|
|
508
|
+
{
|
|
509
|
+
"v": 2,
|
|
510
|
+
"name": "Janic's MacBook Pro",
|
|
511
|
+
"endpoint": "wss://janics-mbp.tail1234.ts.net:7443",
|
|
512
|
+
"pairingToken": "...",
|
|
513
|
+
"fingerprint": "..."
|
|
514
|
+
}
|
|
265
515
|
```
|
|
266
516
|
|
|
267
|
-
The endpoint names the route's port, and omits it for 443.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
517
|
+
The endpoint names the route's port, and omits it for 443. `fingerprint` is
|
|
518
|
+
base64url, without padding, of the SHA-256 of the server's public key. When a
|
|
519
|
+
route to the server is on a port with Funnel on, `pair` refuses and exits 1
|
|
520
|
+
without creating a token.
|
|
521
|
+
|
|
522
|
+
Every network connection, loopback TCP included, runs a Noise handshake before
|
|
523
|
+
any request, as the
|
|
524
|
+
[design](../../docs/specs/2026-10-13-pluggable-transports-design.md#authentication)
|
|
525
|
+
specifies. The server has one long-term X25519 key in
|
|
526
|
+
`$STIM_HOME/server/identity.key`, and each client has its own. There are no
|
|
527
|
+
device tokens.
|
|
528
|
+
|
|
529
|
+
- **First contact (`XXpsk2`).** The pairing token is 32 random bytes, pairs
|
|
530
|
+
one client key and expires after 5 minutes. The client proves it holds the token in the
|
|
531
|
+
handshake, then pins the server's key; the phone also checks the key against
|
|
532
|
+
the QR code's `fingerprint`. The server records the client's public key in
|
|
533
|
+
`devices.json` with the capabilities stored with the token, and spends the
|
|
534
|
+
token: until it expires it admits only that key again, so a client that lost
|
|
535
|
+
the reply can retry. The server keeps only the token's derived PSK and lookup in
|
|
536
|
+
`$STIM_HOME/server/secrets.json`.
|
|
537
|
+
- **Returning client (`XX`).** The server accepts the client's key only if a
|
|
538
|
+
record has it, and the client accepts only the server key it pinned.
|
|
539
|
+
|
|
540
|
+
A refusal during the handshake closes the WebSocket with code 4401 and the
|
|
541
|
+
reason `unknown-secret` (no valid token or ticket matches), `unauthorized` (no
|
|
542
|
+
record has the key, or it was revoked) or `handshake-failed`. A client that
|
|
543
|
+
gets another server key than it pinned closes with `server-key-mismatch`. A
|
|
544
|
+
protocol 1 client gets `protocol-unsupported` in cleartext with an "update,
|
|
545
|
+
then pair again" message. Pairings, approvals and credentials from protocol 1
|
|
546
|
+
are not carried over: phones pair again, and remote machines are approved
|
|
547
|
+
again with a new setup ticket.
|
|
548
|
+
|
|
549
|
+
Stim Desktop and other clients of the server's own user on the same machine do
|
|
550
|
+
not pair. They connect to the Unix socket `$STIM_HOME/server/local.sock`, mode
|
|
551
|
+
0600 in a 0700 directory, which serves only this user and needs no handshake.
|
|
552
|
+
That connection gets read and control. The Desktop-only methods (`route.setup`,
|
|
553
|
+
`machines.update.*`, `device-host.sessions` and `device-host.sessions.stop`)
|
|
554
|
+
work only there; on a server Desktop started, only after the private Desktop
|
|
555
|
+
bootstrap proof (see [Private Desktop bootstrap](#private-desktop-bootstrap)).
|
|
556
|
+
Windows has no socket.
|
|
557
|
+
|
|
558
|
+
The network address, `tailscale whois` and the `X-Forwarded-For` header that
|
|
559
|
+
`tailscale serve` adds never authenticate. They only label a record (`from` in
|
|
560
|
+
`stim-server devices`) and key the rate limits below.
|
|
561
|
+
|
|
562
|
+
A connection must finish the handshake and send `hello` within 5 seconds of
|
|
563
|
+
each step. Five failed attempts from the same peer address within a minute
|
|
564
|
+
block new connections from it for up to a minute. An `unknown-secret` refusal
|
|
565
|
+
is not a failed attempt: a requesting machine retries with its setup ticket
|
|
566
|
+
until setup on this machine stores it.
|
|
286
567
|
The server answers 403 to a WebSocket upgrade that carries `Sec-Fetch-Site`, or
|
|
287
568
|
an `Origin` other than the requested host (`http:` on loopback, `https:`
|
|
288
569
|
through the route), and counts no failed attempt, so a web page can neither
|
|
289
|
-
connect nor block this
|
|
570
|
+
connect nor block this machine's clients.
|
|
290
571
|
|
|
291
572
|
Paired devices live in `$STIM_HOME/server/devices.json`. Revoking a device
|
|
292
|
-
closes its open connections. The server checks registrations on file changes
|
|
573
|
+
deletes its record and closes its open connections. The server checks registrations on file changes
|
|
293
574
|
and once a second, so a missed filesystem notification cannot leave a revoked
|
|
294
575
|
connection authorized.
|
|
295
576
|
|
|
@@ -300,7 +581,7 @@ A paired device has the `read` capability, which serves state, or also
|
|
|
300
581
|
With the `workspace-diff` feature, `read` also serves changed and untracked
|
|
301
582
|
text file contents in registered workspaces, including any non-ignored
|
|
302
583
|
untracked text file such as an unignored `.env`. Pairing grants `read` only,
|
|
303
|
-
unless the pairing code came from `stim-server pair --control`. On the
|
|
584
|
+
unless the pairing code came from `stim-server pair --control`. On the machine,
|
|
304
585
|
`stim-server devices grant <id> --control` adds control to a paired device and
|
|
305
586
|
`--read` takes it away. Nothing a client sends changes its own capabilities.
|
|
306
587
|
The server checks the device's capabilities in `devices.json` on every action
|
|
@@ -310,17 +591,45 @@ registration check. `hello` reports
|
|
|
310
591
|
the capabilities and actions of the connection's device when it connects; a
|
|
311
592
|
connection sees a new grant after it reconnects. Under Stim Host, an approved
|
|
312
593
|
`hello` also carries the same `host` grants as `/health`, which `stim doctor`
|
|
313
|
-
reports for remote
|
|
594
|
+
reports for remote machines.
|
|
314
595
|
|
|
315
596
|
Every method other than `hello` needs `read`, except the methods with their own
|
|
316
597
|
approval: `build.*` needs `build`, the `device-host.*` methods that run a hosted
|
|
317
598
|
session need `device-host`, and `server.update.*` needs the build or device-host
|
|
318
|
-
approval of the requesting
|
|
599
|
+
approval of the requesting machine (see [Remote update](#remote-update)). A device
|
|
319
600
|
with only `build` gets `forbidden` for every other method.
|
|
320
601
|
|
|
602
|
+
## Pause incoming work
|
|
603
|
+
|
|
604
|
+
On the worker machine, pause new builds and hosted sessions independently:
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
stim settings set server.buildsPaused true
|
|
608
|
+
stim settings set server.devicesPaused true
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
The running server reads these machine settings without a restart. A pause
|
|
612
|
+
rejects new work even when a client received an offer before the setting changed.
|
|
613
|
+
Running builds and existing hosted sessions continue; clients can still attach,
|
|
614
|
+
fetch completed artifacts, and stop their sessions. Hosted sessions include iOS,
|
|
615
|
+
Android and macOS. Client approvals are preserved, and local CLI builds and devices
|
|
616
|
+
remain available. These worker settings apply to every requesting machine, including
|
|
617
|
+
explicitly selected workers; Desktop's pool switches only affect that requester's
|
|
618
|
+
automatic placement.
|
|
619
|
+
|
|
620
|
+
Resume either capability when the machine is available again:
|
|
621
|
+
|
|
622
|
+
```bash
|
|
623
|
+
stim settings set server.buildsPaused false
|
|
624
|
+
stim settings set server.devicesPaused false
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
Both settings default to `false`. Pausing does not make the machine idle: let
|
|
628
|
+
existing work finish or stop the relevant sessions before timing benchmarks.
|
|
629
|
+
|
|
321
630
|
## Build access
|
|
322
631
|
|
|
323
|
-
`build` lets another
|
|
632
|
+
`build` lets another machine on the tailnet run its project's code on this machine to
|
|
324
633
|
build for it: config plugins, CocoaPods hooks, Xcode script phases and Gradle
|
|
325
634
|
plugins run as the user `stim-server` runs as. The offload worker runs with that
|
|
326
635
|
user's real home directory and no sandbox, so a build grant is equivalent to
|
|
@@ -331,40 +640,27 @@ real home directory still reaches) to grant itself either, read other build
|
|
|
331
640
|
clients' checkouts and blobs, and persist through LaunchAgents or shell startup
|
|
332
641
|
files. `devices grant` never turns a paired device into a build client, and the
|
|
333
642
|
protocol does not list `read` or `control` among a build client's capabilities,
|
|
334
|
-
but do not rely on that as a boundary. Grant it only to
|
|
643
|
+
but do not rely on that as a boundary. Grant it only to machines you trust with
|
|
335
644
|
that. Revoking the grant stops future builds and kills the running job's
|
|
336
|
-
process group. It does not undo anything project code already did.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
`
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
**Deny** run those commands. Until then, `hello` with its token
|
|
356
|
-
fails with `approval-pending`, which does not count as a failed attempt, while
|
|
357
|
-
the request itself does, so a peer cannot send more requests than failed
|
|
358
|
-
attempts. A request lapses after 15 minutes. Each node has at most one pending
|
|
359
|
-
request, the newest, and the server holds at most 8; more fail with
|
|
360
|
-
`limit-exceeded`. There is no pairing code for `build`: `stim-server pair`
|
|
361
|
-
never grants it.
|
|
362
|
-
|
|
363
|
-
`devices grant` never gives `build` to a paired device, or `read` or
|
|
364
|
-
`control` to a build client. Build clients live in
|
|
365
|
-
`$STIM_HOME/server/build-clients.json`, apart from `devices.json`, so a
|
|
366
|
-
`stim-server` release without `build` never reads them and refuses their
|
|
367
|
-
tokens.
|
|
645
|
+
process group. It does not undo anything project code already did. Approve a
|
|
646
|
+
machine only when you expect it.
|
|
647
|
+
|
|
648
|
+
A machine gets `build` only through
|
|
649
|
+
[`stim-server setup`](#set-up-a-worker-machine) on this machine. The requesting
|
|
650
|
+
machine makes a setup ticket and connects with it in a first-contact handshake,
|
|
651
|
+
naming itself in the handshake (at most 64 characters). Until a person runs
|
|
652
|
+
setup with that ticket and approves, the handshake fails with `unknown-secret`
|
|
653
|
+
and the requester retries. Once approved, the next handshake records the
|
|
654
|
+
requester's public key as a build client, and the requester pins this server's
|
|
655
|
+
key in its `$STIM_HOME/build-machines.json`. Later connections send `hello`
|
|
656
|
+
with `access: "build"` and authenticate by that key alone.
|
|
657
|
+
`stim-server devices revoke <id>` deletes the record and closes its
|
|
658
|
+
connections. There is no pairing code for `build`: `stim-server pair` never
|
|
659
|
+
grants it.
|
|
660
|
+
|
|
661
|
+
`devices grant` takes only `--control` or `--read`: it never gives `build` to a
|
|
662
|
+
paired device, or `read` or `control` to a build client. Build clients live in
|
|
663
|
+
`$STIM_HOME/server/build-clients.json`, apart from `devices.json`.
|
|
368
664
|
|
|
369
665
|
## Device-host approval
|
|
370
666
|
|
|
@@ -372,37 +668,30 @@ Device hosting has a separate `device-host` capability. An approved client can
|
|
|
372
668
|
reserve, boot, reconnect to and stop its own iOS simulator or Android emulator
|
|
373
669
|
through the protocol. It can deliver, install and launch a compatible iOS app
|
|
374
670
|
bundle, Android APK or prebuilt macOS app, stream the iOS simulator, Android emulator or macOS app and control it. Hosted
|
|
375
|
-
iOS and Android apps connect back to Metro on the client
|
|
671
|
+
iOS and Android apps connect back to Metro on the client machine. Automatic placement remains in [#2266](https://github.com/appandflow/stim/issues/2266).
|
|
376
672
|
|
|
377
673
|
The Stim client can name expected hosts with
|
|
378
674
|
`stim settings set remote.machines '["<mac>"]'` and request access with
|
|
379
|
-
`stim doctor --fix
|
|
380
|
-
|
|
381
|
-
|
|
675
|
+
`stim doctor --fix`, which makes a setup ticket and prints the setup command.
|
|
676
|
+
Once a person runs it on the worker, doctor stores a separate private credential
|
|
677
|
+
in `$STIM_HOME/device-host-machines.json` that pins the worker's stim-server key,
|
|
678
|
+
and reports approval under `deviceHosts` in doctor JSON. Plain doctor makes no
|
|
382
679
|
approval request. Use `stim ios --remote <machine>` or `stim android --remote <machine>` for explicit placement.
|
|
383
680
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
On the hosting Mac, inspect `stim-server devices`, then approve the matching
|
|
393
|
-
request with `stim-server devices grant <id> --device-host`, run
|
|
394
|
-
[`stim-server setup`](#set-up-a-worker-mac) there with its node and ticket, or use **Allow**
|
|
395
|
-
in Stim Desktop. Approve only an expected request: it authorizes that Mac to
|
|
396
|
-
reserve session-owned simulators or emulators and run native app code. **Deny**
|
|
397
|
-
or `stim-server devices revoke <id>` removes it; revocation also closes its
|
|
398
|
-
open authenticated connections. The local-process trust boundary described in
|
|
399
|
-
[Build access](#build-access) applies here too.
|
|
681
|
+
As with build, the client spends its setup ticket in a first-contact
|
|
682
|
+
handshake and gets `unknown-secret` until a person on the hosting Mac runs
|
|
683
|
+
[`stim-server setup`](#set-up-a-worker-machine) with `--device-host` and that
|
|
684
|
+
ticket. One ticket can approve build and device hosting together, as one record
|
|
685
|
+
for each. Later connections send `hello` with `access: "device-host"`. Approve
|
|
686
|
+
only an expected machine: it can reserve session-owned simulators or emulators
|
|
687
|
+
and run native app code. `stim-server devices revoke <id>` removes it and also
|
|
688
|
+
closes its open authenticated connections.
|
|
400
689
|
|
|
401
690
|
Hosting clients live in `$STIM_HOME/server/device-host-clients.json`, separate
|
|
402
|
-
from phone pairings and build clients. A hosting
|
|
403
|
-
`control` or `build` access, including access to unrelated workspaces.
|
|
404
|
-
|
|
405
|
-
|
|
691
|
+
from phone pairings and build clients. A hosting record grants no `read`,
|
|
692
|
+
`control` or `build` access, including access to unrelated workspaces. Read,
|
|
693
|
+
control and build records cannot gain hosting through `devices grant`, and
|
|
694
|
+
`pair` never grants it.
|
|
406
695
|
|
|
407
696
|
### Hosted here
|
|
408
697
|
|
|
@@ -422,9 +711,9 @@ Each row has `id`, `client: { id, name }`, `platform`, nullable `device` and `ap
|
|
|
422
711
|
session through the existing stop path, closing Metro, view, agent and logs
|
|
423
712
|
before owned-device teardown. Repeating a stopped session returns `stopped`;
|
|
424
713
|
an absent id gets `unknown-session`.
|
|
425
|
-
Both requests
|
|
426
|
-
|
|
427
|
-
|
|
714
|
+
Both requests work only on the local Unix socket that Stim Desktop uses (see
|
|
715
|
+
[Pairing](#pairing)). Every network connection, paired phones and remote
|
|
716
|
+
hosting clients included, gets `forbidden`. A session whose active owner is not
|
|
428
717
|
attached to this server reads as `unknown`; stop reconciles it through the same
|
|
429
718
|
ownership checks. The approved-client `device-host.stop` remains limited to
|
|
430
719
|
that client's own sessions.
|
|
@@ -465,7 +754,12 @@ This protocol does not select a host for `stim ios` or `stim android`.
|
|
|
465
754
|
|
|
466
755
|
On the hosting Mac, `hosting.agentDriver` names the tool that lets a client's
|
|
467
756
|
coding agent drive the macOS app hosted for that client. `none` (the default)
|
|
468
|
-
starts nothing.
|
|
757
|
+
starts nothing. Each tool is a driver package implementing
|
|
758
|
+
[`@stim-cli/agent-driver`](../agent-driver/README.md) that stim-server depends
|
|
759
|
+
on; today that is `agent-device`, from `@stim-cli/agent-driver-agent-device`. A
|
|
760
|
+
named driver that fails to load leaves the app with `{ "driver": "none" }` and
|
|
761
|
+
a generic notice; the reason, which can name paths on the hosting Mac, goes to
|
|
762
|
+
stim-server's stderr. `stim-server` reference-counts the driver's daemon on running
|
|
469
763
|
hosted macOS apps: it starts with the first, stops after the last, on
|
|
470
764
|
revocation and on server shutdown, and restarts if it exits while apps run. The
|
|
471
765
|
daemon is held under an ownership claim at `server/agent-device.claims` whose
|
|
@@ -481,10 +775,13 @@ ANY /device-host/agent/<session>/<driver path>
|
|
|
481
775
|
|
|
482
776
|
on the tailnet `serve` route is forwarded to the loopback daemon, with the
|
|
483
777
|
daemon's own token, only when all of these hold: the request carries that
|
|
484
|
-
session's token as a bearer token,
|
|
485
|
-
session's
|
|
486
|
-
|
|
487
|
-
|
|
778
|
+
session's token as a bearer token, the session's device-host client still
|
|
779
|
+
holds `device-host`, the session's app is running, and the request comes from the
|
|
780
|
+
tailnet node that client's latest authenticated connection came from. The token is
|
|
781
|
+
the credential; the node check only stops a machine that took over the host's
|
|
782
|
+
MagicDNS name from replaying it, and a request whose node no transport identifies
|
|
783
|
+
is refused. Browser requests and
|
|
784
|
+
other sessions' tokens are refused with 403, an unknown or stopped session with 404.
|
|
488
785
|
|
|
489
786
|
The `agent-device` adapter runs the binary that `STIM_AGENT_DEVICE_BIN` names
|
|
490
787
|
in `stim-server`'s environment, or else the first of
|
|
@@ -636,13 +933,13 @@ session before starting another transfer. Manifest, file and partial blobs are s
|
|
|
636
933
|
by attempts of one session in its content-addressed store; offers recheck complete
|
|
637
934
|
blob digests and request only missing bytes.
|
|
638
935
|
|
|
639
|
-
An iOS simulator, Android emulator or macOS session can take its content from a build this
|
|
936
|
+
An iOS simulator, Android emulator or macOS session can take its content from a build this machine ran instead: after
|
|
640
937
|
the manifest arrives, send `device-host.app.handoff` with
|
|
641
938
|
`{session, attempt, build: {handoff, sha256}}`, where `handoff` is the token
|
|
642
939
|
and `sha256` the archive digest from that job's `build.artifact` answer. The
|
|
643
940
|
server hands the build over only when the build client that ran it still has
|
|
644
941
|
`build`, the caller has `device-host`, and both approvals belong to the same
|
|
645
|
-
|
|
942
|
+
client key. It copies only regular files, and links where the manifest
|
|
646
943
|
declares links, whose directory resolves inside the staged bundle and whose
|
|
647
944
|
bytes match their manifest digest; it answers `{files, bytes}` for what it
|
|
648
945
|
took. The token is spent once the handoff starts. Re-offer and upload whatever
|
|
@@ -693,10 +990,10 @@ retain the same reconciliation rules as iOS.
|
|
|
693
990
|
|
|
694
991
|
With `hosting.agentDriver: agent-device`, each installed Android session gets
|
|
695
992
|
its own daemon and child-aware claim under `device-host/sessions/<session>/agent-device.claims`.
|
|
696
|
-
Both
|
|
993
|
+
Both machines need agent-device 0.21.22 or later. The host verifies the serial policy
|
|
697
994
|
digest and `android-instance` backend and advertises `hosted-android-agent`.
|
|
698
995
|
`device-host.app.attach` and session attach carry a session grant with
|
|
699
|
-
`android:mobile:<serial>` and the same token
|
|
996
|
+
`android:mobile:<serial>` and the same token checks as iOS.
|
|
700
997
|
The client writes a 0600 remote config under `hosted-android/<slot>/` and reports
|
|
701
998
|
it as `android.host.agent`. Use `agent-device <command> --remote-config <file>`.
|
|
702
999
|
Missing or unsupported agent-device reports `driver: none` with its reason.
|
|
@@ -873,8 +1170,9 @@ Automatic native macOS placement remains separate work.
|
|
|
873
1170
|
### Hosted viewer relay
|
|
874
1171
|
|
|
875
1172
|
The client's stim-server relays a hosted workspace's iOS simulator, Android emulator or macOS view and input to
|
|
876
|
-
its host using the client's approved device-host credential over
|
|
877
|
-
|
|
1173
|
+
its host using the client's approved device-host credential, over a connection
|
|
1174
|
+
that goes ahead only with the host that proves the server key pinned at
|
|
1175
|
+
approval. Stim Desktop and phones keep talking only to their own
|
|
878
1176
|
server. The `macos-hosted`, `ios-hosted` and `android-hosted` features advertise these relays.
|
|
879
1177
|
iOS resolves `ios.host` and Android resolves `android.host` by the requested slot; macOS retains its default slot.
|
|
880
1178
|
The local platform and slot replace the host's private target fields in events
|
|
@@ -884,7 +1182,7 @@ The relay forwards iOS device artwork and orientation metadata. Hosted targets
|
|
|
884
1182
|
refuse replay (`at`/`rate`), `duoFrame` and `physical` with `bad-request`.
|
|
885
1183
|
All of the client's relayed subscriptions and control sessions for one host share one connection,
|
|
886
1184
|
whichever local client opened them. Each request still checks the credential
|
|
887
|
-
and the pinned
|
|
1185
|
+
and the pinned key; a changed credential or endpoint opens a new connection.
|
|
888
1186
|
Ending a subscription or session sends `device-host.unsubscribe` or
|
|
889
1187
|
`device-host.control.end`, and the connection closes when the last one ends.
|
|
890
1188
|
The host's per-connection limits, such as its 32 subscriptions and input
|
|
@@ -978,18 +1276,26 @@ before replacement. Automatic placement remains in
|
|
|
978
1276
|
## Run as a service
|
|
979
1277
|
|
|
980
1278
|
`stim-server service install` runs stim-server as a per-user LaunchAgent on
|
|
981
|
-
macOS,
|
|
982
|
-
|
|
1279
|
+
macOS, a systemd user service on Linux ([On Linux](#on-linux)), or a scheduled
|
|
1280
|
+
task on Windows ([On Windows](#on-windows)), so a remote
|
|
1281
|
+
machine or a phone-serving Mac keeps it running without a terminal. Other
|
|
1282
|
+
systems refuse; start stim-server from your own service manager there:
|
|
983
1283
|
|
|
984
1284
|
```bash
|
|
985
1285
|
stim-server service install [--port <n>] [--label <name>] [--serve]
|
|
986
1286
|
[--env KEY=VALUE]... [--path-prepend <dir>]...
|
|
987
1287
|
stim-server service status [--label <name>] [--json]
|
|
988
|
-
stim-server service update [--label <name>] --release <version>|--from <dir>
|
|
1288
|
+
stim-server service update [--label <name>] [--no-wait] --release <version>|--from <dir>
|
|
989
1289
|
stim-server service rollback [--label <name>]
|
|
990
1290
|
stim-server service uninstall [--label <name>]
|
|
991
1291
|
```
|
|
992
1292
|
|
|
1293
|
+
Updates wait for offloaded builds and hosted sessions to finish by default.
|
|
1294
|
+
Use `--no-wait` to restart with work still active: shutdown interrupts those
|
|
1295
|
+
builds and sessions. Service shutdown, health checks and rollback still apply.
|
|
1296
|
+
`stim-server setup --no-wait` uses the same policy when it updates a managed
|
|
1297
|
+
service; if setup requires a separate service update, its remedy includes the flag.
|
|
1298
|
+
|
|
993
1299
|
`install` writes `~/Library/LaunchAgents/<label>.plist` (label `dev.stim.server`
|
|
994
1300
|
by default) and starts it with `launchctl bootstrap gui/<uid>`. The job starts
|
|
995
1301
|
Stim Host, which stays alive as the parent of the absolute `node` and
|
|
@@ -1066,10 +1372,9 @@ foreground `tailscale serve` session counts as present. To move a service that
|
|
|
1066
1372
|
created a route to another `--port`, run `uninstall` first.
|
|
1067
1373
|
|
|
1068
1374
|
`--env KEY=VALUE` and `--path-prepend <dir>` pin variables and PATH entries for
|
|
1069
|
-
the server.
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
the server and to the builds it starts. The plist stores each `--env` value in
|
|
1375
|
+
the server. A LaunchAgent gets no shell profile, so these are how a service sees
|
|
1376
|
+
a variable or tool that toolchain discovery does not find. They apply after
|
|
1377
|
+
discovery, to the server and to the builds it starts. The plist stores each `--env` value in
|
|
1073
1378
|
`EnvironmentVariables`, so it stays out of the server's process arguments, and
|
|
1074
1379
|
passes only the name as `--env KEY`; the server reads that variable from the
|
|
1075
1380
|
environment launchd gives it. The values sit in plain text in the plist. A
|
|
@@ -1099,14 +1404,16 @@ the job to it. `--release <version>` takes an exact version from the public npm
|
|
|
1099
1404
|
registry (`https://registry.npmjs.org/`, never a registry `.npmrc` names), and
|
|
1100
1405
|
npm checks each package against the registry's integrity. `--from <dir>`
|
|
1101
1406
|
installs the `.tgz` packages in that directory, such as `pnpm pack` output of
|
|
1102
|
-
the `@stim-cli/core`, `@stim-cli/cache`, `@stim-cli/metro`, `stim
|
|
1103
|
-
`@stim-cli/
|
|
1407
|
+
the `@stim-cli/core`, `@stim-cli/cache`, `@stim-cli/metro`, `stim`,
|
|
1408
|
+
`@stim-cli/transport`, `@stim-cli/transport-tailscale` and `@stim-cli/server`
|
|
1409
|
+
packages of a checkout, and records each file's sha256.
|
|
1104
1410
|
Either way, `npm audit signatures` must verify the registry signature of every
|
|
1105
1411
|
package that came from the registry, and the provenance attestation of each one
|
|
1106
1412
|
that has it, or the update stops before it touches the job. Install scripts
|
|
1107
1413
|
never run. Each install goes to
|
|
1108
1414
|
`~/Library/Application Support/Stim/services/<label>/versions/`, in a directory
|
|
1109
|
-
named by its version, Stim build and server code,
|
|
1415
|
+
named by its version, Stim build and server code, plus the package files for
|
|
1416
|
+
`--from`, and must start with the job's
|
|
1110
1417
|
`node` before the switch. `update` then waits up to 30 minutes for `/health` to
|
|
1111
1418
|
report no offloaded build and no hosted session (a server older than `busy` in
|
|
1112
1419
|
`/health` restarts without waiting), rewrites only the server script in the
|
|
@@ -1125,24 +1432,116 @@ and uninstall of one label run one at a time.
|
|
|
1125
1432
|
installed and, when `install` created it, the serve route. Logs, pairings and the host app stay; other labels may use the app. Use `--label` and `--port` to run a
|
|
1126
1433
|
second service beside the first, for example with another `STIM_HOME`.
|
|
1127
1434
|
|
|
1128
|
-
To set up a remote
|
|
1435
|
+
To set up a remote machine: install Stim and stim-server on it, run
|
|
1129
1436
|
`stim-server service install --serve` (with the pins its toolchain needs), set
|
|
1130
1437
|
`server.workerRoot` there, and on each client run
|
|
1131
|
-
`stim settings set remote.machines '["<
|
|
1132
|
-
|
|
1133
|
-
|
|
1438
|
+
`stim settings set remote.machines '["<machine>"]'` and `stim doctor --fix`.
|
|
1439
|
+
A person on the remote machine approves it by running the `stim-server setup`
|
|
1440
|
+
command doctor printed (see [Set up a worker machine](#set-up-a-worker-machine)).
|
|
1441
|
+
|
|
1442
|
+
### On Linux
|
|
1443
|
+
|
|
1444
|
+
On Linux, `service` manages a systemd user unit with the same commands, flags
|
|
1445
|
+
and checks. A Linux machine takes offloaded Android builds only, so it never
|
|
1446
|
+
installs Stim Host and has no permission grants.
|
|
1447
|
+
|
|
1448
|
+
- `install` writes `~/.config/systemd/user/<label>.service`, runs
|
|
1449
|
+
`systemctl --user daemon-reload`, `enable` and `start`, and logs to
|
|
1450
|
+
`~/.local/state/stim/<label>.log`. The unit runs the absolute `node` and `stim-server.mjs` of the
|
|
1451
|
+
install that ran the command, with `Restart=always` after 30 seconds,
|
|
1452
|
+
`STIM_HOME` and `SHELL` from the installing shell, each `--env` value in an
|
|
1453
|
+
`Environment=` line, and `STIM_SERVICE_LABEL=<label>`, which tells the
|
|
1454
|
+
server its label. The `[X-Stim]` section, which systemd ignores, marks the
|
|
1455
|
+
unit as one `install` wrote and records the serve route and the previous
|
|
1456
|
+
server script. The unit sits in plain text, values included.
|
|
1457
|
+
- `update` and `rollback` keep versions under
|
|
1458
|
+
`~/.local/share/stim/services/<label>/versions/`, whatever the XDG
|
|
1459
|
+
variables say, so every caller finds the same versions; they rewrite only
|
|
1460
|
+
`ExecStart=` and the previous script, run `daemon-reload` and restart the
|
|
1461
|
+
unit, and switch back when the new server does not answer.
|
|
1462
|
+
- `uninstall` stops and disables the unit, removes it and runs
|
|
1463
|
+
`daemon-reload`.
|
|
1464
|
+
- The unit stops when its user manager does, which by default is when you log
|
|
1465
|
+
out. Turn on linger once so it starts at boot and keeps running:
|
|
1466
|
+
`sudo loginctl enable-linger $USER`. `install` prints that command when
|
|
1467
|
+
linger is off, and `status` reports it; Stim never runs sudo.
|
|
1468
|
+
- `systemctl --user` needs your user manager. An SSH or desktop login has one;
|
|
1469
|
+
without a session, Stim falls back to `XDG_RUNTIME_DIR=/run/user/<uid>`,
|
|
1470
|
+
which works once linger is on.
|
|
1471
|
+
- `--serve` runs `tailscale serve` as you, which Tailscale on Linux allows only
|
|
1472
|
+
for its operator: run `sudo tailscale set --operator=$USER` first.
|
|
1473
|
+
- A systemd unit has no login shell, and the service finds the Android SDK only
|
|
1474
|
+
in `~/Android/Sdk`. Pin the JDK and an SDK elsewhere:
|
|
1475
|
+
|
|
1476
|
+
```bash
|
|
1477
|
+
stim-server service install --serve \
|
|
1478
|
+
--env JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
|
|
1479
|
+
--env ANDROID_HOME=$HOME/Android/Sdk
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
- A remote update the server starts for an approved client runs under
|
|
1483
|
+
`systemd-run --user --scope`, so restarting the unit does not stop it.
|
|
1484
|
+
- `stim-server setup` works on Linux for `--build`. It refuses
|
|
1485
|
+
`--device-host`, needs no GUI session, and skips Stim Host, the permission
|
|
1486
|
+
steps and the Xcode, iOS runtime and CocoaPods checks.
|
|
1487
|
+
|
|
1488
|
+
### On Windows
|
|
1489
|
+
|
|
1490
|
+
On Windows, `service` manages a per-user scheduled task with the same commands,
|
|
1491
|
+
flags and checks, and never needs elevation. A Windows machine takes offloaded
|
|
1492
|
+
Android builds only, so it never installs Stim Host and has no permission
|
|
1493
|
+
grants.
|
|
1494
|
+
|
|
1495
|
+
- `install` writes the service file
|
|
1496
|
+
`%LOCALAPPDATA%\Stim\services\<label>.json` (the server's `node`, script,
|
|
1497
|
+
arguments and environment, `STIM_SERVICE_LABEL=<label>`, the serve route and
|
|
1498
|
+
the previous server script), copies the supervisor
|
|
1499
|
+
`stim-service-runner.mjs` into `%LOCALAPPDATA%\Stim\services\<label>\`, and
|
|
1500
|
+
registers the task `\Stim\<label>` with `schtasks /Create`, then starts it.
|
|
1501
|
+
It logs to `%LOCALAPPDATA%\Stim\Logs\<label>.log`.
|
|
1502
|
+
- The task starts at your logon and runs whether or not you are logged on
|
|
1503
|
+
(Task Scheduler's S4U logon type, which an ordinary user can register), so it
|
|
1504
|
+
keeps running after you sign out; after a reboot it starts at your next
|
|
1505
|
+
logon. Task Scheduler restarts a task only when it fails to start, not when
|
|
1506
|
+
its process exits, so the task runs the supervisor, which restarts the
|
|
1507
|
+
server 30 seconds after it exits and reads the service file again each time.
|
|
1508
|
+
The supervisor records its pid, the server's, the number of runs and the last
|
|
1509
|
+
exit code in `supervisor.json`, which `status` reports, and its own errors,
|
|
1510
|
+
such as an unreadable service file, in `supervisor.log` beside it. The task
|
|
1511
|
+
runs at normal priority, so builds are not slowed by Task Scheduler's
|
|
1512
|
+
below-normal default.
|
|
1513
|
+
- Stopping the service ends the supervisor's process tree with
|
|
1514
|
+
`taskkill /T /F`; a remote update the server starts runs detached from that
|
|
1515
|
+
tree, so restarting the service does not stop it.
|
|
1516
|
+
- `update` and `rollback` keep versions under
|
|
1517
|
+
`%LOCALAPPDATA%\Stim\services\<label>\versions\` and rewrite only the
|
|
1518
|
+
service file's arguments and previous script. `uninstall` stops the task,
|
|
1519
|
+
deletes it and removes the service file.
|
|
1520
|
+
- The task's environment is your user environment, without a login shell.
|
|
1521
|
+
Builds start tools such as Expo's config loader with the first `node.exe` on
|
|
1522
|
+
`PATH`, and a version manager's `node.exe` shim, such as pnpm's, breaks their
|
|
1523
|
+
Node IPC, so builds fail with `fingerprint-mismatch`. Stim keeps `PATH` as
|
|
1524
|
+
you set it; `install` names a first `node.exe` that is not the Node running
|
|
1525
|
+
the server, and `--path-prepend` puts the real Node first:
|
|
1526
|
+
|
|
1527
|
+
```powershell
|
|
1528
|
+
stim-server service install --serve --path-prepend "C:\Program Files\nodejs" `
|
|
1529
|
+
--env JAVA_HOME="C:\Program Files\Eclipse Adoptium\jdk-17" `
|
|
1530
|
+
--env ANDROID_HOME="$env:LOCALAPPDATA\Android\Sdk"
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
- `stim-server setup` works on Windows for `--build`, as on Linux.
|
|
1134
1534
|
|
|
1135
1535
|
## Remote update
|
|
1136
1536
|
|
|
1137
|
-
A
|
|
1138
|
-
(`--device-host`) can update this
|
|
1139
|
-
connection, so the remote
|
|
1537
|
+
A machine this one approved for builds (`--build`) or device hosting
|
|
1538
|
+
(`--device-host`) can update this machine's `stim-server service` over its tailnet
|
|
1539
|
+
connection, so the remote machine keeps up with the client without ssh. A
|
|
1140
1540
|
server that offers it lists `server-update` in its `hello` features. A paired
|
|
1141
|
-
phone, a `read` or `control` device and
|
|
1142
|
-
cannot. The methods need that approval, not `read`:
|
|
1541
|
+
phone, a `read` or `control` device and the local socket connection cannot. The methods need that approval, not `read`:
|
|
1143
1542
|
|
|
1144
1543
|
- `server.update.status` returns `server` (`version` and `stimBuild`),
|
|
1145
|
-
`service` (the label of the
|
|
1544
|
+
`service` (the label of the service this server runs under, or `null` when
|
|
1146
1545
|
it does not run as a `stim-server service` and so cannot update itself),
|
|
1147
1546
|
`acceptsClientBuilds`, `running` (the update in progress, with its `state`,
|
|
1148
1547
|
`missing` package bytes and the last lines of its `log`) and `last` (how the
|
|
@@ -1154,8 +1553,8 @@ cannot. The methods need that approval, not `read`:
|
|
|
1154
1553
|
`{ "packages": [{ "name", "size", "sha256" }] }`, at most 8 `.tgz` packages
|
|
1155
1554
|
of the client's own build. A `release` older than the version this server
|
|
1156
1555
|
runs is refused with `bad-request`; run `stim-server service update --release`
|
|
1157
|
-
on this
|
|
1158
|
-
`true` on this
|
|
1556
|
+
on this machine to downgrade it. Packages need `server.acceptClientBuilds` set to
|
|
1557
|
+
`true` on this machine (`stim settings set server.acceptClientBuilds true`); a
|
|
1159
1558
|
release needs no setting. One update runs at a time.
|
|
1160
1559
|
- `server.update.chunk` takes `{ id, name, offset, data }`, at most 32 KiB of
|
|
1161
1560
|
base64 at the package's next offset. A package whose bytes do not match its
|
|
@@ -1175,10 +1574,10 @@ arrives once every package is in gets `action-busy`, and a retried reservation
|
|
|
1175
1574
|
of an existing hosted session still answers during the drain.
|
|
1176
1575
|
|
|
1177
1576
|
Stim Desktop starts these updates through its own local stim-server with two
|
|
1178
|
-
methods that only the local
|
|
1577
|
+
methods that only the local Unix socket connection may call:
|
|
1179
1578
|
`machines.update.start` takes `{ "machine" }`, an approved `remote.machines`
|
|
1180
|
-
entry. It connects to that machine
|
|
1181
|
-
the
|
|
1579
|
+
entry. It connects to that machine with the stored credential, goes ahead only
|
|
1580
|
+
with the stim-server that proves the key pinned at approval, and asks it for this server's npm release. When this
|
|
1182
1581
|
server runs from a Stim checkout, it instead packs the checkout's workspace
|
|
1183
1582
|
packages as `npm pack` would and sends them. `machines.update.status` takes
|
|
1184
1583
|
the same `{ "machine" }` and returns `remote` (the machine's
|
|
@@ -1202,18 +1601,23 @@ connected since stim-server started keeps its directory when revoked.
|
|
|
1202
1601
|
same Ruby selection and UTF-8 locale defaults as `pod install`; omitting
|
|
1203
1602
|
`rubyVersion` uses the worker's default Ruby. It
|
|
1204
1603
|
returns `toolchain` (`stimBuild`, a digest of the bundled Stim's built code;
|
|
1604
|
+
`os`, the OS stim-server runs on as Node names it (`darwin`, `linux`, `win32`);
|
|
1205
1605
|
`arch`; `xcode`; `simulatorSdk`; `macosSdk` (from
|
|
1206
1606
|
`xcrun --sdk macosx --show-sdk-version`); `cocoapods`; `runtimes`, the simulator
|
|
1207
1607
|
runtimes with an iPhone simulator to build for; `jdk`, the major version of
|
|
1208
1608
|
the JDK in `JAVA_HOME` or the macOS default; and `androidSdk`, the `ndk`,
|
|
1209
|
-
`buildTools` and `platforms` directories of the SDK in `ANDROID_HOME` or
|
|
1210
|
-
`~/
|
|
1211
|
-
(`checkout`, `dependencies
|
|
1212
|
-
|
|
1213
|
-
|
|
1609
|
+
`buildTools` and `platforms` directories of the SDK in `ANDROID_HOME`, else `~/Library/Android/sdk` on macOS or
|
|
1610
|
+
`~/Android/Sdk` on Linux, null without one), `capacity` and `warm`
|
|
1611
|
+
(`checkout`, `dependencies`, and the legacy `build` field, now always false)
|
|
1612
|
+
for that repository. For React Native and Expo builds, clients supporting `build-dependency-cache` can send
|
|
1613
|
+
`dependencies: { project, packageName }` alongside `lockfile`; the dependency
|
|
1614
|
+
hint requires the selected project's installed modules and a matching install
|
|
1615
|
+
marker, including the package-manager arguments. Without that query it is
|
|
1616
|
+
false. Clients ignore older workers' unverified warmth and do not rank
|
|
1617
|
+
checkout or build-directory presence as cache state. `capacity` holds
|
|
1214
1618
|
`running` and `max` offloaded builds, `diskFreeBytes` of the worker root's
|
|
1215
1619
|
volume and `minDiskFreeBytes`, `cpus`, `loadPerCore` (the 5-minute load
|
|
1216
|
-
average per CPU), `builds` (this
|
|
1620
|
+
average per CPU), `builds` (this machine's own Stim runs in prebuild, pods or
|
|
1217
1621
|
compile that it did not offload, plus the offloaded builds it runs), `maxBuilds` (its
|
|
1218
1622
|
`concurrency.maxBuilds`, 0 when unlimited), `maxLoadPerCore` (its
|
|
1219
1623
|
`server.maxLoadPerCore`, default 2), `memoryUsedBytes` (Activity Monitor's
|
|
@@ -1227,12 +1631,57 @@ connected since stim-server started keeps its directory when revoked.
|
|
|
1227
1631
|
simulator-runtime facts. Unused report fields are null. This report has its own
|
|
1228
1632
|
cache, ignores Ruby selection, and is reused for native Xcode build admission.
|
|
1229
1633
|
Clients use the complete report on older native-build-capable hosts.
|
|
1634
|
+
With `build-artifact-cache`, a native Xcode offer can also send
|
|
1635
|
+
`artifact: { fingerprint, cacheKey }`. Its `artifact` result is null on a miss,
|
|
1636
|
+
or `{ fingerprint, cacheKey, name, size, sha256 }` for a completed archive in
|
|
1637
|
+
this client's repository area. The key includes source, toolchain, SDK,
|
|
1638
|
+
architecture and target options. These are exact lookups, not a full inventory.
|
|
1639
|
+
With `react-native-peer-cache`, a React Native/Expo development offer can send
|
|
1640
|
+
`reactNativeArtifact: { project, fingerprint, isExpo, platform, toolchain, ios | android }`.
|
|
1641
|
+
The query includes the caller's compatible toolchain and normalized native
|
|
1642
|
+
build choices; the worker constructs its own receipt around compilation.
|
|
1643
|
+
The returned identity is opaque, not an alias for the caller's local cache key.
|
|
1644
|
+
The completed receipt binds the worker-verified post-mutation fingerprint
|
|
1645
|
+
and native options to the toolchain checked before and after compilation.
|
|
1646
|
+
Lookup uses that immutable receipt, so a later checkout or toolchain change
|
|
1647
|
+
does not discard a matching archive. Older workers receive no new query
|
|
1648
|
+
parameters and continue through their normal build path.
|
|
1649
|
+
With `swiftpm-peer-cache`, `swiftpmArtifact` carries `project`, `sourceDigest`,
|
|
1650
|
+
`stimBuild`, complete `inputs` (`ignored`, `outputs`, `localFiles`,
|
|
1651
|
+
`environment`) and `build` (`product`, `infoPlist`, `resources`, `assetCatalog`).
|
|
1652
|
+
Resource and catalog paths are repository-relative; the plist is
|
|
1653
|
+
project-relative. The worker verifies actual transferred files and full
|
|
1654
|
+
artifact identity around compilation, then retains an opaque receipt.
|
|
1655
|
+
Private input values stay on that worker and are rechecked on every lookup.
|
|
1656
|
+
SDKROOT, then SWIFTPM_SDKROOT_macosx, selects the SDK ahead of the default;
|
|
1657
|
+
its real path and build version are revalidated.
|
|
1658
|
+
A matching archive survives checkout and job-output pruning when those
|
|
1659
|
+
private inputs remain available and unchanged and its direct Apple toolchain
|
|
1660
|
+
selection can be reproduced. Command paths and versions are checked from the
|
|
1661
|
+
package directory, including SWIFT_EXEC and SWIFT_EXEC_MANIFEST compiler overrides.
|
|
1662
|
+
Overrides and custom Swift, xcrun or xcodebuild launchers require the
|
|
1663
|
+
checkout and become a miss after it is removed.
|
|
1664
|
+
- `build.cached-artifact` takes `repo` and `artifact: { fingerprint, cacheKey }`.
|
|
1665
|
+
It requires the existing build approval. It verifies and streams the cached
|
|
1666
|
+
archive with the same digest-prefixed binary frames as `build.artifact`, and
|
|
1667
|
+
returns its descriptor or null if missing or corrupt. It starts no build and
|
|
1668
|
+
takes no build slot, even when new builds are declined. Completed native Xcode
|
|
1669
|
+
worker archives survive job-output cleanup in their client's repository cache;
|
|
1670
|
+
removing that client's worker area removes the cache too. Other clients and
|
|
1671
|
+
repositories cannot look up those entries.
|
|
1672
|
+
React Native archives also require the same `reactNativeArtifact` query on
|
|
1673
|
+
download, checked against the completed receipt again. An identity alone
|
|
1674
|
+
cannot fetch them. Archives use the existing complete-file cache and digest verification;
|
|
1675
|
+
a hit uploads no source and takes no compiler slot. A missing or corrupt
|
|
1676
|
+
archive falls back through the caller's existing placement policy.
|
|
1677
|
+
SwiftPM archives require the same `swiftpmArtifact` query on fetch and another
|
|
1678
|
+
private-input check; their opaque keys alone cannot fetch an archive.
|
|
1230
1679
|
- `build.sync` takes `repo`, `files` and `done`. `files` is one page of the
|
|
1231
1680
|
manifest, each `{ "path", "kind": "file"|"exec"|"link", "size", "sha256" }`,
|
|
1232
1681
|
a link's blob being its target. Pages accumulate until `done`; the next
|
|
1233
1682
|
`build.sync` starts a new manifest. A path is relative, with no `.`, `..`,
|
|
1234
1683
|
empty or `.git` component (in any case). The result's `missing` lists the digests of the
|
|
1235
|
-
page this
|
|
1684
|
+
page this machine lacks. The client then sends each as binary frames: 32 bytes of
|
|
1236
1685
|
the sha256, then the next bytes of that blob, one blob after another. A frame
|
|
1237
1686
|
for a blob that was not asked for, one that overruns its size, or bytes that
|
|
1238
1687
|
do not match the digest close the connection with 4400. Blobs are kept per
|
|
@@ -1249,10 +1698,12 @@ connected since stim-server started keeps its directory when revoked.
|
|
|
1249
1698
|
`fingerprint`, `packageName`, `isExpo`, `optimizations`, `android` (for
|
|
1250
1699
|
`android`: `variant`, `abi`, `gradleBuildCache`, `pch` and `compilerCache`,
|
|
1251
1700
|
`ccache` or `none`) and `stimBuild`, after the whole manifest of `repo` was
|
|
1252
|
-
synced on this connection.
|
|
1701
|
+
synced on this connection. A peer-cache-capable React Native caller can set
|
|
1702
|
+
`cacheArtifact: true` to retain a completed development archive when its cache
|
|
1703
|
+
write and remote policies allow it. It returns `{ "job" }`. It fails with
|
|
1253
1704
|
`build-busy` while the offer would be declined, naming the reason, or while
|
|
1254
1705
|
another build of the same client and repository runs, or while every one
|
|
1255
|
-
of this
|
|
1706
|
+
of this machine's `concurrency.maxBuilds` slots is held, and with
|
|
1256
1707
|
`build-refused` when `stimBuild` differs. The build runs `offload-worker.mjs` of the bundled Stim with
|
|
1257
1708
|
`STIM_HOME` set to the repository's area. It makes the area's checkout
|
|
1258
1709
|
hold exactly the manifest (a file git lists as untracked and not ignored,
|
|
@@ -1265,12 +1716,15 @@ connected since stim-server started keeps its directory when revoked.
|
|
|
1265
1716
|
Gradle daemon leaves the build's process group, so no claim or slot tracks
|
|
1266
1717
|
it. An Android build keeps the client's daemon warm, with Gradle's idle
|
|
1267
1718
|
timeout set to `server.gradleDaemonIdleMinutes` (default 30; 0 stops it
|
|
1268
|
-
when the build ends), and stops it when the build gets SIGTERM
|
|
1719
|
+
when the build ends), and stops it when the build gets SIGTERM (on Windows,
|
|
1720
|
+
where a cancel ends the build's process tree with `taskkill /T /F`, the
|
|
1721
|
+
daemon sweep below stops it). Gradle
|
|
1269
1722
|
fixes a daemon's idle timeout when the daemon starts, so a new value
|
|
1270
1723
|
applies to the next daemon. Once no build of that client runs, stim-server
|
|
1271
1724
|
sends SIGTERM to each daemon whose command line runs from the client's
|
|
1272
|
-
Gradle home
|
|
1273
|
-
|
|
1725
|
+
Gradle home (on Windows, every process running from it, the Kotlin daemon
|
|
1726
|
+
included, since Windows cannot delete a jar a running process opened) when a build of the client was cancelled, when the client
|
|
1727
|
+
loses `build`, and, for every client, while this machine has less than 2 GB of
|
|
1274
1728
|
available memory (total memory minus Activity Monitor's Memory Used). It
|
|
1275
1729
|
checks every minute and on each change under its server directory. A daemon
|
|
1276
1730
|
whose Gradle home is deleted stops itself within seconds, because Gradle
|
|
@@ -1303,7 +1757,11 @@ connected since stim-server started keeps its directory when revoked.
|
|
|
1303
1757
|
It returns that app as a tar archive, its manifest fingerprint, timings and
|
|
1304
1758
|
`compilationCache: {}`. The client verifies the digest, identity, executable
|
|
1305
1759
|
and signature before promotion and launches locally. Every offload failure
|
|
1306
|
-
falls back locally
|
|
1760
|
+
falls back locally. A caller with remote artifact caching enabled can set
|
|
1761
|
+
`cacheArtifact: true` and `macos.artifactInputs` for declared repeatable,
|
|
1762
|
+
relocatable apps. Completed archive retention requires matching worker
|
|
1763
|
+
input identities before and after the build; ignored bundle files need
|
|
1764
|
+
explicit private declarations. Matching Xcode and macOS
|
|
1307
1765
|
SDK are required, and the worker needs network access for the first SwiftPM
|
|
1308
1766
|
dependency fetch; the dependency cache is shared only within that client.
|
|
1309
1767
|
- `build.progress` events `{ "event": "build.progress", "job", ... }` carry a
|
|
@@ -1330,23 +1788,30 @@ connected since stim-server started keeps its directory when revoked.
|
|
|
1330
1788
|
as binary frames, each 32 bytes of its sha256 and then the next bytes, then
|
|
1331
1789
|
answers `{ "name", "size", "sha256" }`, and deletes it here. For an iOS simulator, Android emulator or macOS
|
|
1332
1790
|
job the answer also carries `handoff`, a single-use token, and the staged
|
|
1333
|
-
`.app` or APK stays for 10 minutes so a hosted session on this
|
|
1791
|
+
`.app` or APK stays for 10 minutes so a hosted session on this machine can take it with
|
|
1334
1792
|
`device-host.app.handoff`. A client keeps at most one such bundle; its next
|
|
1335
1793
|
fetched native build or a revoked approval deletes it. An archive nobody fetched is deleted when its job
|
|
1336
1794
|
is cancelled; one left by a server that crashed stays under
|
|
1337
1795
|
`repos/<repo>/out/` until you delete it.
|
|
1338
1796
|
|
|
1339
|
-
The worker root is `server.workerRoot` in this
|
|
1797
|
+
The worker root is `server.workerRoot` in this machine's Stim settings, or
|
|
1340
1798
|
`$STIM_HOME/build-worker`. Each client gets `<root>/<device id>/`, with its
|
|
1341
1799
|
blobs, caches (`CP_HOME_DIR`, `CP_CACHE_DIR`, the pnpm store and
|
|
1342
1800
|
`GRADLE_USER_HOME` and SwiftPM) and one area
|
|
1343
1801
|
per repository under `repos/<repo>/`: the checkout, its own Stim home with
|
|
1344
1802
|
DerivedData, the compilation cache and ccache, and the output. A build never reads or
|
|
1345
|
-
writes this
|
|
1803
|
+
writes this machine's own Stim home. An ownership claim at
|
|
1346
1804
|
`repos/<repo>.claims`, whose child is the build's process group, guards each
|
|
1347
1805
|
area; it is released only once that group is gone, and a claim whose server
|
|
1348
|
-
and build are both gone is recovered by the next build.
|
|
1349
|
-
|
|
1806
|
+
and build are both gone is recovered by the next build. Windows has no
|
|
1807
|
+
process groups, so there the claim follows the build's worker process, and a
|
|
1808
|
+
cancel ends its whole tree with `taskkill /T /F` before the claim is
|
|
1809
|
+
released. The NDK's ninja on Windows cannot open paths longer than 260
|
|
1810
|
+
characters, so a Windows worker keeps each area's checkout and each client's
|
|
1811
|
+
Gradle home in private short directories, `<SystemDrive>\sw\<6 hex>`, with
|
|
1812
|
+
an owner key beside each; the area's `src` is a junction to its checkout.
|
|
1813
|
+
Removing a client deletes them too. With a non-zero
|
|
1814
|
+
`concurrency.maxBuilds`, the build also holds one of this machine's build slots
|
|
1350
1815
|
(`$STIM_HOME/build-slots/`), the same claims local Stim builds take, with the
|
|
1351
1816
|
same child, so a local build waits for it and `builds` counts it. Each finished build
|
|
1352
1817
|
appends a `build` record to the [action log](#actions) with the repository as
|
|
@@ -1355,40 +1820,44 @@ client's directory to reclaim it.
|
|
|
1355
1820
|
|
|
1356
1821
|
## Protocol
|
|
1357
1822
|
|
|
1358
|
-
JSON messages over a WebSocket.
|
|
1823
|
+
JSON messages over a WebSocket. On a network connection they follow the Noise
|
|
1824
|
+
handshake (see [Pairing](#pairing)) and each WebSocket message is one encrypted
|
|
1825
|
+
transport message; on the local Unix socket they are plain JSON. Requests are `{ "id", "method", "params" }`,
|
|
1359
1826
|
answered by `{ "id", "result" }` or `{ "id", "error": { "code", "message" } }`.
|
|
1360
1827
|
Events are `{ "event", "subscription", ... }`.
|
|
1361
1828
|
|
|
1362
|
-
- `hello` must come first. Params: `
|
|
1363
|
-
`
|
|
1364
|
-
|
|
1365
|
-
|
|
1829
|
+
- `hello` must come first. Params: `client` (`name`, `version`), and
|
|
1830
|
+
optionally `access`, `build` or `device-host`, which picks which of the key's
|
|
1831
|
+
own records the connection acts as (see [Build access](#build-access));
|
|
1832
|
+
without it, the pairing in `devices.json`. It never widens what a record
|
|
1833
|
+
allows. The result carries `protocol` (2), the server name and versions, the
|
|
1366
1834
|
device's `capabilities` (see [Scopes](#scopes)), the server's `features`
|
|
1367
1835
|
(`physical-ios` and `physical-android` when it serves `physical: true` for
|
|
1368
1836
|
that platform: see `frames.subscribe` below for an iPhone and
|
|
1369
1837
|
[Physical Android devices](#physical-android-devices), and `notifications`
|
|
1370
1838
|
for `notifications.list` and the `notification` event),
|
|
1371
1839
|
the `actions` it may run
|
|
1372
|
-
(none without `control`)
|
|
1373
|
-
|
|
1840
|
+
(none without `control`) and the record the connection authenticated as.
|
|
1841
|
+
`server.home` is the home folder
|
|
1374
1842
|
of the user the server runs as, so clients can show paths under it as
|
|
1375
|
-
`~/...`.
|
|
1843
|
+
`~/...`.
|
|
1376
1844
|
- `status.subscribe` returns a subscription id. Each `status` event carries a
|
|
1377
1845
|
full payload as `stim status --watch --json` prints it, including each
|
|
1378
1846
|
environment's `physicalDevices`, the phones it leases, and its `agents`, the
|
|
1379
1847
|
Claude Code and Codex sessions working in it. Those carry the session id,
|
|
1380
1848
|
working directory, the short session name the tool keeps, and times, never
|
|
1381
1849
|
prompts or conversation. `openUrl` there is a `claude://` or `codex://` link
|
|
1382
|
-
that only the
|
|
1850
|
+
that only the machine can open; the server has no action that opens it. `webUrl`
|
|
1383
1851
|
is a Claude Code session's `https://claude.ai/code/` link while Remote Control
|
|
1384
1852
|
is connected, which a phone opens in the Claude app or a browser.
|
|
1385
1853
|
`endedAgents` lists the sessions that stopped running there in the last 3
|
|
1386
1854
|
days, with the same fields as last found, no `pid`, and `endedAt`. The server
|
|
1387
1855
|
drops `CLAUDE_CODE_SESSION_ID` and `CODEX_THREAD_ID` from the environment of
|
|
1388
1856
|
the `stim` commands it runs, so a phone's reload is not recorded as the work
|
|
1389
|
-
of the agent session that started the server.
|
|
1390
|
-
|
|
1391
|
-
subscriber
|
|
1857
|
+
of the agent session that started the server. The server runs the status
|
|
1858
|
+
watch itself, in a worker thread, from the moment it is ready, so a
|
|
1859
|
+
subscriber gets the latest payload at once; all subscribers share it.
|
|
1860
|
+
While any status subscription is open, the server keeps a CPU
|
|
1392
1861
|
and memory history from each payload's `machine.owners`, and every `status`
|
|
1393
1862
|
event carries it beside the payload as `usage`, once it holds a reading:
|
|
1394
1863
|
`{ "intervalMs", "endAt", "environments", "devices" }`. It covers the last 10
|
|
@@ -1405,6 +1874,35 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1405
1874
|
`grantedAt` times as `ownLeases`, so a client can tell a person controlling a
|
|
1406
1875
|
device from an agent driving it: an `activity.driver` of `stim device lock`
|
|
1407
1876
|
whose `since` is in `ownLeases` is a phone.
|
|
1877
|
+
- A server that offers the following status views lists `status-views` in its
|
|
1878
|
+
`hello` features.
|
|
1879
|
+
- `workspaces.subscribe` returns a subscription id and sends `workspaces`
|
|
1880
|
+
events, each with a `seq` that starts at 0 and grows by one. The first
|
|
1881
|
+
carries `snapshot`: `{ "workspaces", "worktrees", "archived" }`. Each
|
|
1882
|
+
workspace is its environment as `status` reports it, without what only a
|
|
1883
|
+
shown workspace needs: a build keeps its outcome, timing and first located
|
|
1884
|
+
diagnostic, each platform's history only its newest build and the failures
|
|
1885
|
+
right before it, `logs` only `errorsSinceMarker`, a pull request no
|
|
1886
|
+
`reviewDecision` or `checkedAt`, and a macOS app no launch `arguments`.
|
|
1887
|
+
`worktrees` are the unprovisioned worktrees' `path`, `branch`, `repository`,
|
|
1888
|
+
`git`, `gitChip` and pull request without `reviewDecision` or `checkedAt`; `archived` is
|
|
1889
|
+
`{ "count", "bytes", "newest" }` with the 10 newest archives. Later events
|
|
1890
|
+
carry only what changed: `workspaces` and `worktrees` as
|
|
1891
|
+
`{ "upsert", "remove" }` (whole entries by `path`, and the paths that are
|
|
1892
|
+
gone), and `archived` when it changed. A client that misses a `seq`
|
|
1893
|
+
subscribes again for a new snapshot.
|
|
1894
|
+
- `workspace.subscribe` with `{ "workspace" }` sends `workspace` events with
|
|
1895
|
+
that workspace's full `environment`, on subscribing and on every change to
|
|
1896
|
+
it, or `null` while no workspace has that path.
|
|
1897
|
+
- `machine.subscribe` sends `machine` events with `capacity`, `machine`,
|
|
1898
|
+
`deviceLeases`, `simctlAvailable` and `maintenance` without `recent` and
|
|
1899
|
+
`sizes`, plus `usage` and `ownLeases` as on `status` events.
|
|
1900
|
+
- `builds.history` with `{ "workspace" }` returns that workspace's full
|
|
1901
|
+
`{ "builds" }`. `archived.list` with optional `before` and `limit` (at most 50) returns `{ "archived", "usage", "next" }`: archives newest removal first,
|
|
1902
|
+
and the `before` of the next page, or null. `worktrees.list` returns the full
|
|
1903
|
+
`{ "worktrees" }` and `maintenance.get` the full `{ "maintenance" }`. These
|
|
1904
|
+
answer from the server's latest status, waiting up to 30 seconds for the
|
|
1905
|
+
first one.
|
|
1408
1906
|
- `logs.query` returns `{ "records" }`, and `logs.subscribe` sends `logs`
|
|
1409
1907
|
events: first the last `tail` matching records, then new ones in batches.
|
|
1410
1908
|
Both take the Stim Desktop log viewer's filters: exactly one of `workspace`
|
|
@@ -1431,8 +1929,8 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1431
1929
|
with a message naming the archive id.
|
|
1432
1930
|
- `stats.get` returns the same payload as `stim stats --json`, using the shared
|
|
1433
1931
|
core reader in a bounded, cancellable server child. The stats part of
|
|
1434
|
-
`machine.details` uses the same reader. `settings.get`
|
|
1435
|
-
|
|
1932
|
+
`machine.details` uses the same reader. `settings.get` uses the in-process
|
|
1933
|
+
settings API, which masks sensitive values. Without `workspace`,
|
|
1436
1934
|
these reads use the home directory as their project context.
|
|
1437
1935
|
- `frames.subscribe` takes `workspace`, `platform` (`ios`, `android`, `web` or `macos`),
|
|
1438
1936
|
`slot` (`default` when absent), `fps` (1 to 30, 5 by default) and `maxEdge`
|
|
@@ -1532,7 +2030,7 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1532
2030
|
and on an iPhone Duo bit 1 while folded or bit 2 while unfolded,
|
|
1533
2031
|
u16 header length, u32 sequence number of the messages sent on this
|
|
1534
2032
|
subscription, f64 capture time in milliseconds since the epoch on the
|
|
1535
|
-
|
|
2033
|
+
machine's clock, u16 width, u16 height, u8 subscription id length and the
|
|
1536
2034
|
ASCII id), then one Annex-B access unit. Every keyframe carries its SPS
|
|
1537
2035
|
and PPS, and the stream has no B-frames, so each access unit is shown as
|
|
1538
2036
|
it arrives. A change of size, such as a rotation or a Duo fold, restarts
|
|
@@ -1640,7 +2138,7 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1640
2138
|
it.
|
|
1641
2139
|
- `recording`: true while the server records the device now.
|
|
1642
2140
|
- `spans`: the recorded time ranges `{ start, end }`, in epoch milliseconds on
|
|
1643
|
-
the
|
|
2141
|
+
the machine's clock, oldest first. Segments less than 1.5 seconds apart form
|
|
1644
2142
|
one span; the gaps between spans are time nothing was recorded, such as
|
|
1645
2143
|
after `stim stop`.
|
|
1646
2144
|
- `markers`: `{ at, kind, command?, label }` from the start of the first span
|
|
@@ -1714,13 +2212,13 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1714
2212
|
deleted.
|
|
1715
2213
|
- `build.plan` takes `workspace`, `platform` (`ios` or `android`) and `slot`
|
|
1716
2214
|
(`default` when absent), and returns the payload of
|
|
1717
|
-
`stim <platform> --plan --json`
|
|
2215
|
+
`stim <platform> --plan --json` through the shared in-process API: the fingerprint, the
|
|
1718
2216
|
cache result the next build would get (`local`, `remote` or `false`), the
|
|
1719
2217
|
prebuild decision, `expectedMs` with its `basis`, and on a predicted cold
|
|
1720
2218
|
build its `missReason`. It builds, boots and
|
|
1721
2219
|
installs nothing and writes no Stim state; a remote cache check can
|
|
1722
|
-
download the artifact into a temporary directory
|
|
1723
|
-
|
|
2220
|
+
download the artifact into a temporary directory. The planner has a 60-second
|
|
2221
|
+
execution and 35-second cleanup budget inside the 150-second request limit. A plan predicting that the build would refuse is a result
|
|
1724
2222
|
whose `refusal` holds the code, message and remedy. A plan that cannot be
|
|
1725
2223
|
computed is a `stim-failed` error. One plan runs per workspace at a time,
|
|
1726
2224
|
across all connections; later requests wait their turn, and the 150
|
|
@@ -1756,7 +2254,7 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1756
2254
|
without running `stim`: `volumes`, one per volume that holds a Stim
|
|
1757
2255
|
workspace, Stim home, or the simulators, with `mount`, `holds`, `freeBytes`
|
|
1758
2256
|
(free space without purgeable space, which Stim's disk budget measures) and
|
|
1759
|
-
`totalBytes`; `memory` with `totalBytes`, `usedBytes` (the
|
|
2257
|
+
`totalBytes`; `memory` with `totalBytes`, `usedBytes` (the machine's memory in
|
|
1760
2258
|
use as Activity Monitor's "Memory Used" counts it: app memory, wired and
|
|
1761
2259
|
compressed; null off macOS) and the macOS `pressure` level (`normal`,
|
|
1762
2260
|
`warning`, `critical`, or null); `load` with the 1, 5 and 15
|
|
@@ -1769,23 +2267,27 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1769
2267
|
critical) and `diskFreeBytes` of the startup volume; a field is null when it
|
|
1770
2268
|
cannot be read. `sinceMs` returns only the samples taken after it.
|
|
1771
2269
|
- `machine.details` returns `{ "gc", "stats", "measuredAt" }`: the payloads of
|
|
1772
|
-
|
|
2270
|
+
GC dry-run and stats report APIs, both read in process from the home directory, so
|
|
1773
2271
|
a phone can show what Stim Desktop's Machine page shows about disk: every
|
|
1774
2272
|
simulator, AVD, iOS runtime and system image, shared caches, recordings,
|
|
1775
2273
|
workspace build outputs and logs, and what `stim gc --delete` would free. The
|
|
1776
|
-
|
|
1777
|
-
anything for this request. A part is null when its
|
|
2274
|
+
GC read is always the dry run with no other scope; the server never deletes
|
|
2275
|
+
anything for this request. A part is null when its read failed, and
|
|
1778
2276
|
`gcError` or `statsError` says why. It needs only `read`. The server keeps
|
|
1779
2277
|
one result for 60 seconds, shared by every connection: a request while the
|
|
1780
|
-
|
|
1781
|
-
|
|
2278
|
+
reads run waits for them, and `measuredAt` says when they started. GC and
|
|
2279
|
+
doctor have the API's 60 second execution budget; the shared reads also retain
|
|
2280
|
+
the server's 150 second limit. API cleanup has a separate 35 second budget;
|
|
2281
|
+
unfinished cleanup returns `STIM_CLAIM_UNAVAILABLE`, and an unverifiable
|
|
2282
|
+
child is left untouched. Servers that predate it answer
|
|
1782
2283
|
`unknown-method`. It also carries `buildMachines`, the `remoteMachines` list
|
|
1783
|
-
of `stim doctor --json
|
|
1784
|
-
show whether each remote
|
|
2284
|
+
of `stim doctor --json` (never with `--fix`), so a phone can
|
|
2285
|
+
show whether each remote machine takes builds. The server runs doctor only
|
|
1785
2286
|
when `remote.machines` names a machine, else the list is empty; it runs it
|
|
1786
2287
|
in the registered workspace that ran doctor for iOS most recently, because
|
|
1787
|
-
doctor judges a machine against an app.
|
|
1788
|
-
|
|
2288
|
+
doctor judges a machine against an app. This read does not repair or record a
|
|
2289
|
+
CLI doctor run. Changing the selected workspace clears its cached result and
|
|
2290
|
+
prevents an older read from overwriting the new workspace's result. `buildMachines` is null with
|
|
1789
2291
|
`buildMachinesError` when doctor failed, the config could not be read, or no
|
|
1790
2292
|
registered workspace exists. The reply never waits for doctor, whose offer
|
|
1791
2293
|
to an unreachable machine can take about 13 seconds: it carries the last
|
|
@@ -1794,10 +2296,10 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1794
2296
|
at a time, and sets `buildMachinesPending` while that background run has not
|
|
1795
2297
|
settled a result yet, or the settled one is 60 seconds old or older. A client
|
|
1796
2298
|
that wants the refreshed result asks `machine.details` again. It also
|
|
1797
|
-
carries `buildClients`, the builds this
|
|
1798
|
-
per client
|
|
2299
|
+
carries `buildClients`, the builds this machine ran for other machines, one entry
|
|
2300
|
+
per client machine read from the `build` records of the audit log, with `id`,
|
|
1799
2301
|
`name`, `builds`, `failed`, `buildMs`, `lastAt` and the same three counts
|
|
1800
|
-
for `today`, most recent client first, with `today` on this
|
|
2302
|
+
for `today`, most recent client first, with `today` on this machine's local
|
|
1801
2303
|
calendar day. Servers that predate it leave it out.
|
|
1802
2304
|
- `unsubscribe` ends a subscription.
|
|
1803
2305
|
- `push.register` takes `token`, an Expo push token, `events`, one or more
|
|
@@ -1830,8 +2332,8 @@ a time. `logs.query`, `stats.get` and `settings.get` fail after 60 seconds,
|
|
|
1830
2332
|
`build.plan` after 150; these four fail at 32 MiB of output. Closing the
|
|
1831
2333
|
connection stops running requests. When the command refuses with Stim's error
|
|
1832
2334
|
contract on stdout, the `stim-failed` message is its code, message and remedy;
|
|
1833
|
-
otherwise
|
|
1834
|
-
|
|
2335
|
+
otherwise the message describes the failed read. In-process reads cancel their
|
|
2336
|
+
owned children and drain cleanup before releasing their request slot. A log subscriber whose socket has more than
|
|
1835
2337
|
4 MiB unsent gets no more batches until it catches up; past 20,000
|
|
1836
2338
|
waiting records the server ends that subscription with `slow-client`.
|
|
1837
2339
|
|
|
@@ -1842,8 +2344,8 @@ so clients can replay what happened while nobody watched. It records a device
|
|
|
1842
2344
|
while `stim status` shows an automation tool driving it (`activity.state` is
|
|
1843
2345
|
`driven`), or while a client has a `frames.subscribe` subscription to it. It
|
|
1844
2346
|
records nothing else, holds no device awake and changes no device setting. To
|
|
1845
|
-
see drivers, the
|
|
1846
|
-
|
|
2347
|
+
see drivers, it reads the status watch the server runs for as long as it
|
|
2348
|
+
runs, shared with status subscribers.
|
|
1847
2349
|
|
|
1848
2350
|
Recording goes through the device's `stim-frames` helper, shared with live
|
|
1849
2351
|
subscribers. The helper runs a second H.264 encoder for it, at 720 pixels on
|
|
@@ -1878,7 +2380,7 @@ the `.part` segments of a server that stopped. When a status payload shows
|
|
|
1878
2380
|
workspace within seconds and deletes its recordings. `stim stop` ends recording
|
|
1879
2381
|
and keeps the footage; `stim worktree remove` and `stim gc` delete it. A gc that
|
|
1880
2382
|
deletes the segment being written loses the rest of that segment; the next one
|
|
1881
|
-
is written as usual. Recordings stay on the
|
|
2383
|
+
is written as usual. Recordings stay on the machine and are served only to paired
|
|
1882
2384
|
clients.
|
|
1883
2385
|
|
|
1884
2386
|
## Push notifications
|
|
@@ -1896,7 +2398,7 @@ shows them in its attention strip. The server pushes, per device and only for
|
|
|
1896
2398
|
the events the device chose:
|
|
1897
2399
|
|
|
1898
2400
|
- `started`: a workspace began warming (`phase` `warming`), or an agent first
|
|
1899
|
-
drove one of its devices. It is grouped per
|
|
2401
|
+
drove one of its devices. It is grouped per machine, and opens the workspace, or the device viewer
|
|
1900
2402
|
once an agent drives it. An agent driving the workspace updates the warming
|
|
1901
2403
|
notification in place.
|
|
1902
2404
|
- `stuck`: an agent drove the workspace, its devices are still up, and nothing
|
|
@@ -1950,9 +2452,9 @@ registers does not push. During the phone's quiet hours nothing pushes: a
|
|
|
1950
2452
|
problem that still holds when they end pushes then, and what started or
|
|
1951
2453
|
finished during them does not.
|
|
1952
2454
|
|
|
1953
|
-
While at least one device is paired, the server
|
|
1954
|
-
|
|
1955
|
-
|
|
2455
|
+
While at least one device is paired, the server reads its status watch, even
|
|
2456
|
+
with no client connected, and reads the free space of Stim's volumes and the
|
|
2457
|
+
memory pressure every minute.
|
|
1956
2458
|
While a device wants `finished`, it looks up the pull requests of worktree
|
|
1957
2459
|
branches that have an upstream every 5 minutes, with one `gh api graphql` call
|
|
1958
2460
|
per repository, the lookup `stim gc` uses. Without `gh`, or when it is signed
|
|
@@ -1964,9 +2466,9 @@ count. More than three at once become one summary push that opens the
|
|
|
1964
2466
|
phone's home screen.
|
|
1965
2467
|
|
|
1966
2468
|
Pushes go to the Expo push service, `https://exp.host/--/api/v2/push/send`,
|
|
1967
|
-
which forwards them to Apple. No APNs key or other secret lives on the
|
|
1968
|
-
workspace push carries the workspace title, a one-line cause and the
|
|
1969
|
-
as the subtitle; a machine or summary push has the
|
|
2469
|
+
which forwards them to Apple. No APNs key or other secret lives on the machine. A
|
|
2470
|
+
workspace push carries the workspace title, a one-line cause and the machine's name
|
|
2471
|
+
as the subtitle; a machine or summary push has the machine's name as its title. In
|
|
1970
2472
|
`data` it carries the `ref`, the screen to open (`home`, `machine`,
|
|
1971
2473
|
`workspace`, `device`, `build` or `url`), and the workspace's absolute path,
|
|
1972
2474
|
with the platform and slot for a device, the platform for build details and
|
|
@@ -2001,9 +2503,9 @@ and needs-attention vectors. Notification delivery remains local to each.
|
|
|
2001
2503
|
|
|
2002
2504
|
## Notification history
|
|
2003
2505
|
|
|
2004
|
-
The server logs the notifications the rules produce for the
|
|
2506
|
+
The server logs the notifications the rules produce for the machine, whether or not
|
|
2005
2507
|
a phone was pushed, so a phone can list what it missed. While any device is
|
|
2006
|
-
paired, it runs the rules once more for the
|
|
2508
|
+
paired, it runs the rules once more for the machine itself, with every category, no
|
|
2007
2509
|
quiet hours and the lowest `stuckMinutes` a registered device asked for (15
|
|
2008
2510
|
without one). Each notification it produces is an entry; a `control` conflict is
|
|
2009
2511
|
an entry too, listed only to the phone that lost the device. Pull request
|
|
@@ -2040,13 +2542,14 @@ A device with `control` can send `action` with params `{ "action", "workspace" }
|
|
|
2040
2542
|
| Action | Params | Runs in the workspace |
|
|
2041
2543
|
| -------- | -------------------------------------------------------------------- | ------------------------------- |
|
|
2042
2544
|
| `reload` | `platform` (`ios` or `android`), optional; needed when both are live | `stim reload [platform] --json` |
|
|
2043
|
-
| `stop` | none |
|
|
2545
|
+
| `stop` | none | in-process workspace stop API |
|
|
2044
2546
|
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2547
|
+
`reload` passes its fixed argument list to the bundled `stim`, without a shell.
|
|
2548
|
+
`stop` calls the typed API in the server process and acts on the exact registered
|
|
2549
|
+
workspace, including a partial workspace that no longer has an app manifest.
|
|
2550
|
+
`workspace` must be a project path Stim has registered, the `path` a status
|
|
2551
|
+
payload lists. The result's `output` retains the CLI JSON shape; stop returns
|
|
2552
|
+
`{ root, ok, ...outcomes }`. The server refuses the request and runs nothing with:
|
|
2050
2553
|
|
|
2051
2554
|
- `forbidden` when the device has only `read`;
|
|
2052
2555
|
- `unknown-action` for any other action;
|
|
@@ -2057,10 +2560,13 @@ nothing with:
|
|
|
2057
2560
|
- `action-busy` while another action runs in that workspace. The server runs
|
|
2058
2561
|
one action per workspace at a time, across all connections.
|
|
2059
2562
|
|
|
2060
|
-
|
|
2061
|
-
and
|
|
2062
|
-
and
|
|
2063
|
-
|
|
2563
|
+
Failures use `action-failed` with the command's error or the stop cleanup summary
|
|
2564
|
+
and bounded diagnostic detail. Registration storage failures retain the original
|
|
2565
|
+
record and report the storage error. An action fails with `action-failed` after
|
|
2566
|
+
120 seconds: reload's CLI child gets SIGTERM, while stop is cancelled. A stop
|
|
2567
|
+
waiting for native work ends its wait; one already tearing down finishes cleanup.
|
|
2568
|
+
The workspace remains busy until that work drains, and server shutdown waits
|
|
2569
|
+
for it. An action keeps running when its client disconnects.
|
|
2064
2570
|
|
|
2065
2571
|
Every `action` request from a paired device, refused or run, appends one line
|
|
2066
2572
|
to `$STIM_HOME/server/actions.ndjson`: `at`, `device` (`id` and `name`),
|
|
@@ -2226,8 +2732,11 @@ appends a line to the action log, with `action` set to `control.begin`,
|
|
|
2226
2732
|
ended or whom it took the device from. Inputs are not logged.
|
|
2227
2733
|
|
|
2228
2734
|
The error codes `unauthorized`, `pairing-expired`, and `protocol-unsupported`
|
|
2229
|
-
refuse the client until it pairs again or updates; `
|
|
2230
|
-
|
|
2735
|
+
refuse the client until it pairs again or updates; `unknown-secret` means the
|
|
2736
|
+
server does not hold the pairing token or setup ticket, or not yet, so a client
|
|
2737
|
+
with a setup ticket retries until setup runs; clients retry the others.
|
|
2738
|
+
Refusals during the handshake arrive as the WebSocket close reason with code
|
|
2739
|
+
4401, not as a JSON error.
|
|
2231
2740
|
|
|
2232
2741
|
The package exports the message types, and the build writes their JSON Schema
|
|
2233
2742
|
to `dist/protocol.schema.json`, exported as `@stim-cli/server/protocol.schema.json`.
|