@stim-cli/server 1.15.0 → 1.17.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 +333 -53
- package/dist/{protocol-MBMvJpvV.mjs → protocol-wBQmYsnp.mjs} +182 -15
- package/dist/protocol.mjs +1 -1
- package/dist/protocol.schema.json +447 -8
- package/dist/stim-frames/DuoModelAsset.swift +34 -0
- package/dist/stim-frames/DuoModelView.swift +1 -9
- package/dist/{stim-server-OiJI9xLU.mjs → stim-server-CP50F1Y_.mjs} +6866 -4774
- package/dist/stim-server.mjs +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,12 +16,14 @@ The design is in
|
|
|
16
16
|
stim-server [--port <n>] # serve paired clients, port 7787 by default
|
|
17
17
|
stim-server pair [--port <n>] [--control]
|
|
18
18
|
# print a single-use pairing payload
|
|
19
|
-
stim-server devices [list] # list
|
|
19
|
+
stim-server devices [list] # list pairings, approvals, requests and hosted sessions
|
|
20
20
|
stim-server devices grant <id> --control|--read|--build|--device-host
|
|
21
21
|
# let a paired device run actions, or only read;
|
|
22
22
|
# --build approves builds; --device-host approves device hosting
|
|
23
23
|
stim-server devices revoke <id> # revoke a paired device or client, or deny a request
|
|
24
24
|
stim-server log # list the actions paired devices ran
|
|
25
|
+
stim-server setup --client <node-id> --ticket <t> --expires <iso> --build|--device-host
|
|
26
|
+
# worker-side approval and setup, see below
|
|
25
27
|
stim-server service install|status|update|rollback|uninstall
|
|
26
28
|
# run stim-server as a macOS LaunchAgent, see below
|
|
27
29
|
```
|
|
@@ -59,11 +61,133 @@ Desktop's Build machines list uses to find stim-server on the other Macs of the
|
|
|
59
61
|
tailnet. A request from this Mac with a `Host` other than `127.0.0.1` or
|
|
60
62
|
`localhost` gets HTTP 426, like any other plain HTTP request.
|
|
61
63
|
|
|
64
|
+
`GET /setup/<hash>` returns a setup journal over the tailnet HTTP route. The
|
|
65
|
+
hash is exactly 64 lowercase hex characters: SHA-256 of the optional
|
|
66
|
+
`auth.setupTicket` on a build or device-host request (43 base64url characters).
|
|
67
|
+
The registry stores only `setupTicketHash` and retains it after approval;
|
|
68
|
+
missing or invalid tickets leave ordinary access requests unchanged.
|
|
69
|
+
|
|
70
|
+
The journal lives at `$STIM_HOME/server/setup/<hash>.json`, written atomically
|
|
71
|
+
only by stim-server and read through core's state reader. It contains the
|
|
72
|
+
client node, ISO `expiresAt`, requested capabilities, steps, granted request
|
|
73
|
+
ids, completion state and optional exit code; it contains no tokens. Only the
|
|
74
|
+
matching tailnet node can read an unexpired journal. Browser requests, loopback,
|
|
75
|
+
invalid paths, missing or malformed journals and other nodes get the same 404. Like the agent route, the caller address is the `X-Forwarded-For` header the tailnet route sets, so a local process that forges it is outside this model.
|
|
76
|
+
The route returns 503 until startup is ready, caches peer identity for 30 seconds
|
|
77
|
+
and returns 429 after repeated identity failures. All responses use
|
|
78
|
+
`cache-control: no-store`. Steps whose ids start with `tools` appear only after
|
|
79
|
+
a build grant. The server prunes journals more than one hour past expiry at
|
|
80
|
+
startup and hourly.
|
|
81
|
+
|
|
62
82
|
`stim-server` runs the `stim` version this package was released with, not the
|
|
63
83
|
one on your PATH. It reads the login shell's environment once at start, so
|
|
64
84
|
`PATH`, `ANDROID_HOME`, and `STIM_*` variables match your terminal even when
|
|
65
85
|
another app starts it.
|
|
66
86
|
|
|
87
|
+
## Set up a worker Mac
|
|
88
|
+
|
|
89
|
+
A person on the worker runs the command generated for their client Mac:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
stim-server setup --client <node-id> --ticket <43-base64url-characters> --expires <ISO-time> \
|
|
93
|
+
--build --device-host [--port <n>] [--label <name>] \
|
|
94
|
+
[--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If the server is not installed globally, use `npx --yes --package @stim-cli/server@<version> stim-server`
|
|
98
|
+
in place of `stim-server`, with the exact version from the client Mac.
|
|
99
|
+
At least one capability is required. The expiry must be in the future and at
|
|
100
|
+
most two hours ahead. The defaults are port 7787 and label `dev.stim.server`.
|
|
101
|
+
Argument and preflight refusals change nothing. Preflight requires macOS,
|
|
102
|
+
Node 22.12+, running Tailscale, the client node on this tailnet, and this user's
|
|
103
|
+
GUI login session.
|
|
104
|
+
|
|
105
|
+
Running setup on the worker is the worker-side approval. It pre-approves at
|
|
106
|
+
most one request per chosen capability, from this node, carrying this ticket,
|
|
107
|
+
until this expiry. A terminal asks y/N for each grant (default No); use `--yes`
|
|
108
|
+
to approve without those questions, including without a terminal. Without
|
|
109
|
+
a terminal or `--yes`, setup refuses before installing anything unless every
|
|
110
|
+
chosen capability already has a matching approval. A person on
|
|
111
|
+
the worker still approves; an SSH-driven run is not offered. Build and
|
|
112
|
+
device-host requests are separate grants. Build never includes read or control.
|
|
113
|
+
A rerun before expiry reports matching approved requests as already approved
|
|
114
|
+
and never grants a second request. After expiry it refuses with exit 2 before
|
|
115
|
+
any change. Requests themselves lapse after 15 minutes.
|
|
116
|
+
|
|
117
|
+
Setup installs its exact server release into
|
|
118
|
+
`~/Library/Application Support/Stim/services/<label>/versions/`, installs
|
|
119
|
+
Stim Host, installs or reuses the managed LaunchAgent, and creates or reuses a
|
|
120
|
+
tailnet-only HTTPS route on 7443 or the next free port. Older managed services
|
|
121
|
+
use the service update busy-wait and rollback behavior. Newer versions stay
|
|
122
|
+
installed; tools report a Stim build difference. A reused server must be at
|
|
123
|
+
least **1.16.0** (ignoring its prerelease suffix for this floor), the first version with setup ticket storage and the journal
|
|
124
|
+
route. An older app server refuses immediately with an update remedy.
|
|
125
|
+
|
|
126
|
+
When a Desktop-run server already answers on the port, setup reuses it and
|
|
127
|
+
installs no LaunchAgent. It requires an existing tailnet route and refuses
|
|
128
|
+
to create one it cannot record. Its permissions belong to that app. The server must
|
|
129
|
+
use the same Stim home. Export a scratch `STIM_HOME` in the shell before
|
|
130
|
+
running setup; `--env` cannot set it. Repeatable `--env KEY=VALUE` and
|
|
131
|
+
`--path-prepend <absolute-dir>` have the same validation and environment order
|
|
132
|
+
as service install. A new LaunchAgent inherits only `STIM_HOME` and `SHELL`;
|
|
133
|
+
pass paths and variables needed for CocoaPods, Java or Android explicitly.
|
|
134
|
+
|
|
135
|
+
For device hosting, setup checks screen recording and device control, requests
|
|
136
|
+
normal macOS prompts, and opens the matching Privacy & Security panes after
|
|
137
|
+
about ten seconds without a grant. Prompts appear on this Mac's screen. A
|
|
138
|
+
terminal accepts `s` to skip each permission. Without a terminal, each missing
|
|
139
|
+
permission waits at most five minutes and remains pending in the journal.
|
|
140
|
+
Missing screen recording prevents viewing hosted simulators; missing device
|
|
141
|
+
control prevents controlling them. For a Desktop-run server, allow its app
|
|
142
|
+
in the same panes. Setup never changes TCC or enables Funnel. Funneled or
|
|
143
|
+
unreadable routes refuse with a remedy. Tool checks print fixes for Xcode,
|
|
144
|
+
simulator runtimes, CocoaPods, JDK, Android SDK and the Stim build; they install
|
|
145
|
+
nothing. Android and CocoaPods checks are not needed for hosting alone.
|
|
146
|
+
|
|
147
|
+
Plain progress goes to stdout; errors use `stim-server: <message>` on stderr.
|
|
148
|
+
`--json` sends progress to stderr and prints one final payload with `ok`,
|
|
149
|
+
`label`, `port`, `route` (state, DNS name, HTTPS port), `server` (version and
|
|
150
|
+
Stim build), `managed`, separate `granted` request ids and client identities,
|
|
151
|
+
`permissions`, `tools` and `warnings`. Final permission values are `granted`,
|
|
152
|
+
`denied` (not authorized), `skipped` or `not-needed`; a timed-out wait remains
|
|
153
|
+
`pending` in the journal and names its unavailable feature. Exit codes come
|
|
154
|
+
from the final journal:
|
|
155
|
+
|
|
156
|
+
| Exit | Meaning |
|
|
157
|
+
| ---- | --------------------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| 0 | Every chosen capability, needed permission and tool is ready. |
|
|
159
|
+
| 1 | A refusal or failed step. Completed earlier steps remain in place. |
|
|
160
|
+
| 2 | No grant before expiry, or an already expired command. |
|
|
161
|
+
| 3 | At least one grant, but another chosen capability, permission or tool is missing; the output names affected features. |
|
|
162
|
+
|
|
163
|
+
Setup writes `$STIM_HOME/server/setup/<sha256(ticket)>.json` from its first
|
|
164
|
+
step and after every change, including completion and the exit code. The
|
|
165
|
+
node-bound `GET /setup/<hash>` route described above mirrors it until expiry,
|
|
166
|
+
hiding tools until a build grant. Each valid setup run prunes journals more
|
|
167
|
+
than one hour past expiry. A dedicated `server/setup.claims` ownership claim
|
|
168
|
+
serializes runs; the service update claim is released before waiting for
|
|
169
|
+
requests and permissions. A competing setup refuses with the exact claim
|
|
170
|
+
and removal command. Ctrl-C or SIGTERM finishes the running step as failed
|
|
171
|
+
with detail `interrupted`, completes the journal, releases the setup claim,
|
|
172
|
+
and exits 1. A typed N also exits 1; a question timeout follows ticket expiry
|
|
173
|
+
and exits 2.
|
|
174
|
+
|
|
175
|
+
The summary lists what is ready and the undo commands:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
stim-server devices revoke <build-request-id>
|
|
179
|
+
stim-server devices revoke <device-host-request-id>
|
|
180
|
+
stim-server service uninstall --label <label>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
If route verification or recording fails, setup removes the route it just
|
|
184
|
+
created; a failed removal prints the exact `tailscale serve --https=<port> off`
|
|
185
|
+
remedy. Uninstall removes only a route the managed install created. A reused app's
|
|
186
|
+
server and route remain managed by that app. If its health response omits
|
|
187
|
+
permission grants, setup keeps those checks pending or lets a terminal skip
|
|
188
|
+
them; Stim Host's grants do not stand in for another app's permissions. Pairings, Stim Host and its macOS
|
|
189
|
+
permissions remain; remove permission grants in System Settings if desired.
|
|
190
|
+
|
|
67
191
|
## Tailscale
|
|
68
192
|
|
|
69
193
|
Tailscale carries and encrypts the traffic between the phone and the Mac, on
|
|
@@ -185,7 +309,8 @@ The server records a pending build client bound to the peer's node, answers
|
|
|
185
309
|
with a `deviceToken`, no capabilities and
|
|
186
310
|
`approval: { "state": "pending", "expiresAt" }`, and closes the connection.
|
|
187
311
|
`stim-server devices` lists it as `pending build`. On this Mac,
|
|
188
|
-
`stim-server devices grant <id> --build` approves it
|
|
312
|
+
`stim-server devices grant <id> --build` approves it, or a person runs
|
|
313
|
+
[`stim-server setup`](#set-up-a-worker-mac) on this Mac with its node and ticket, and
|
|
189
314
|
`stim-server devices revoke <id>` denies it; Stim Desktop's **Allow** and
|
|
190
315
|
**Deny** run those commands. Until then, `hello` with its token
|
|
191
316
|
fails with `approval-pending`, which does not count as a failed attempt, while
|
|
@@ -206,16 +331,15 @@ tokens.
|
|
|
206
331
|
Device hosting has a separate `device-host` capability. An approved client can
|
|
207
332
|
reserve, boot, reconnect to and stop its own iOS simulator or Android emulator
|
|
208
333
|
through the protocol. It can deliver, install and launch a compatible iOS app
|
|
209
|
-
bundle, Android APK or prebuilt macOS app, stream the iOS simulator or macOS app and control it.
|
|
210
|
-
|
|
211
|
-
Android Metro and viewing remain in [#2266](https://github.com/appandflow/stim/issues/2266).
|
|
334
|
+
bundle, Android APK or prebuilt macOS app, stream the iOS simulator, Android emulator or macOS app and control it. Hosted
|
|
335
|
+
iOS and Android apps connect back to Metro on the client Mac. Automatic placement remains in [#2266](https://github.com/appandflow/stim/issues/2266).
|
|
212
336
|
|
|
213
337
|
The Stim client can name expected hosts with
|
|
214
338
|
`stim settings set hosting.machines '["<mac>"]'` and request access with
|
|
215
339
|
`stim doctor --fix`. It stores a separate private credential in
|
|
216
340
|
`$STIM_HOME/device-host-machines.json`, pins the worker's tailnet node, and
|
|
217
341
|
reports approval under `deviceHosts` in doctor JSON. Plain doctor makes no
|
|
218
|
-
approval request.
|
|
342
|
+
approval request. Use `stim ios --remote <machine>` or `stim android --remote <machine>` for explicit placement.
|
|
219
343
|
|
|
220
344
|
A client on the tailnet sends `hello` with
|
|
221
345
|
`auth: { "request": "device-host", "deviceName": "Laptop" }`. As with a build
|
|
@@ -226,7 +350,8 @@ and at most eight device-host requests can be pending. Build requests have a
|
|
|
226
350
|
separate limit. The same name validation and failed-attempt limit apply.
|
|
227
351
|
|
|
228
352
|
On the hosting Mac, inspect `stim-server devices`, then approve the matching
|
|
229
|
-
request with `stim-server devices grant <id> --device-host`,
|
|
353
|
+
request with `stim-server devices grant <id> --device-host`, run
|
|
354
|
+
[`stim-server setup`](#set-up-a-worker-mac) there with its node and ticket, or use **Allow**
|
|
230
355
|
in Stim Desktop. Approve only an expected request: it authorizes that Mac to
|
|
231
356
|
reserve session-owned simulators or emulators and run native app code. **Deny**
|
|
232
357
|
or `stim-server devices revoke <id>` removes it; revocation also closes its
|
|
@@ -239,6 +364,31 @@ from phone pairings and build clients. A hosting token grants no `read`,
|
|
|
239
364
|
read, control and build tokens cannot gain hosting through `devices grant`.
|
|
240
365
|
Loopback requests and `pair --device-host` are refused.
|
|
241
366
|
|
|
367
|
+
### Hosted here
|
|
368
|
+
|
|
369
|
+
`stim-server devices` adds a **Hosted here** block after pairings and approvals,
|
|
370
|
+
with the client name, platform, device label, installed app bundle id (or `-`),
|
|
371
|
+
state and start time. `--json` adds `hostedSessions` next to `devices`, including
|
|
372
|
+
an empty array when there are none. Rows are newest first; plain stopped
|
|
373
|
+
sessions are omitted, while parked and unresolved sessions stay visible.
|
|
374
|
+
A parked row uses its parking time. A removed client's name falls back to its id.
|
|
375
|
+
The command reads the host journal directly and needs no server connection.
|
|
376
|
+
|
|
377
|
+
`device-host.sessions` takes no params (or `{}`) and returns `{ sessions }`.
|
|
378
|
+
Each row has `id`, `client: { id, name }`, `platform`, nullable `device` and `app`,
|
|
379
|
+
`state`, `parked`, `since` (ISO time), and `workspace`.
|
|
380
|
+
`device-host.sessions.stop` takes `{ "session": "<id>" }` and returns
|
|
381
|
+
`{ "id": "<id>", "state": "stopping" | "stopped" }`. It stops any client's
|
|
382
|
+
session through the existing stop path, closing Metro, view, agent and logs
|
|
383
|
+
before owned-device teardown. Repeating a stopped session returns `stopped`;
|
|
384
|
+
an absent id gets `unknown-session`.
|
|
385
|
+
Both requests require an authenticated loopback Desktop connection with local
|
|
386
|
+
identity and control access. Local read-only connections, paired phones and
|
|
387
|
+
remote hosting clients get `forbidden`. A session whose active owner is not
|
|
388
|
+
attached to this server reads as `unknown`; stop reconciles it through the same
|
|
389
|
+
ownership checks. The approved-client `device-host.stop` remains limited to
|
|
390
|
+
that client's own sessions.
|
|
391
|
+
|
|
242
392
|
### Hosted availability offers
|
|
243
393
|
|
|
244
394
|
Before reserving, an approved hosting client can send:
|
|
@@ -347,14 +497,15 @@ After a lost reply or connection, replay the same reserve request or attach to
|
|
|
347
497
|
its attempt. That resolves the same session; a changed request with that attempt
|
|
348
498
|
is refused. Another attempt cannot replace an occupied workspace/slot. The
|
|
349
499
|
client never supplies a worker filesystem path or another client's session.
|
|
350
|
-
`device-host.stop` with `{"session":"<id>"}`
|
|
500
|
+
`device-host.stop` with `{"session":"<id>"}` deletes only its recorded,
|
|
351
501
|
ledger-owned simulator. Poll attach for completion. A new attempt may reserve
|
|
352
|
-
after the previous one is confirmed stopped. Stopped device records remain for
|
|
353
|
-
|
|
502
|
+
after the previous one is confirmed stopped. Stopped session homes and device records remain for reconciliation. At start,
|
|
503
|
+
stim-server retires leftover stopped iOS and Android devices through their exact
|
|
504
|
+
private ledger ownership.
|
|
354
505
|
|
|
355
506
|
The worker persists the journal under `$STIM_HOME/server/device-host-sessions/`
|
|
356
507
|
and chooses an isolated worker home under `$STIM_HOME/device-host/sessions/`.
|
|
357
|
-
An uncertain create, child exit, journal or
|
|
508
|
+
An uncertain create, child exit, journal or deletion outcome retains the slot
|
|
358
509
|
as `unknown`. Explicit stop can reconcile a complete record after a server
|
|
359
510
|
restart; a live or unverifiable owner is refused. Missing ownership records
|
|
360
511
|
need operator investigation, never a replacement inferred from a simulator name.
|
|
@@ -403,8 +554,9 @@ A ready Android `device` has `avdName`, `serial`, `consolePort`, `systemImage`,
|
|
|
403
554
|
its exact AVD name and running ABI before reporting ready. Creation remains
|
|
404
555
|
inside the worker's claimed process group. The worker opens no emulator viewer.
|
|
405
556
|
Stop and revocation use centralized teardown only for its private ledger's exact
|
|
406
|
-
AVD. They
|
|
407
|
-
|
|
557
|
+
AVD. They delete that AVD and retain the session home and record. At start,
|
|
558
|
+
stim-server retires leftover stopped sessions; unknown native outcomes retain
|
|
559
|
+
the reservation and require explicit reconciliation. Android Metro
|
|
408
560
|
and screen/input routing remain follow-ups in #2266. Android sessions
|
|
409
561
|
refuse the iOS Metro, view and input routes.
|
|
410
562
|
|
|
@@ -440,9 +592,11 @@ offsets. Upload the manifest first, then re-offer for its missing file content.
|
|
|
440
592
|
The manifest is limited to 8 MiB and 20,000 entries, with at most 1 GiB per file,
|
|
441
593
|
1 KiB per link, and 4 GiB of declared bundle content. An app attempt cannot
|
|
442
594
|
change its identity, mode, arguments or manifest. Complete a receiving attempt or stop the
|
|
443
|
-
session before starting another transfer.
|
|
595
|
+
session before starting another transfer. Manifest, file and partial blobs are shared
|
|
596
|
+
by attempts of one session in its content-addressed store; offers recheck complete
|
|
597
|
+
blob digests and request only missing bytes.
|
|
444
598
|
|
|
445
|
-
|
|
599
|
+
An iOS simulator, Android emulator or macOS session can take its content from a build this Mac ran instead: after
|
|
446
600
|
the manifest arrives, send `device-host.app.handoff` with
|
|
447
601
|
`{session, attempt, build: {handoff, sha256}}`, where `handoff` is the token
|
|
448
602
|
and `sha256` the archive digest from that job's `build.artifact` answer. The
|
|
@@ -452,7 +606,11 @@ tailnet node. It copies only regular files, and links where the manifest
|
|
|
452
606
|
declares links, whose directory resolves inside the staged bundle and whose
|
|
453
607
|
bytes match their manifest digest; it answers `{files, bytes}` for what it
|
|
454
608
|
took. The token is spent once the handoff starts. Re-offer and upload whatever
|
|
455
|
-
is still missing; an older server answers `unknown-method`.
|
|
609
|
+
is still missing; an older server answers `unknown-method`. For iOS, check the
|
|
610
|
+
`hosted-ios-data` hello feature before requesting handoff; for Android check
|
|
611
|
+
`hosted-android-data`. Without the feature, upload. Android takes only a regular
|
|
612
|
+
APK file matching the single `App.apk` manifest entry. Handoff failure falls back
|
|
613
|
+
to upload. Reused APK blobs skip transfer, while every new attempt still installs.
|
|
456
614
|
|
|
457
615
|
After every digest is verified, call `device-host.app.launch` with
|
|
458
616
|
`{session, attempt}`. Poll `device-host.app.attach` for `installed` or `unknown`.
|
|
@@ -488,11 +646,31 @@ signature or installation conflict refuses; hosting does not uninstall an
|
|
|
488
646
|
existing app to resolve it.
|
|
489
647
|
|
|
490
648
|
Release launch reports `true` only after observing a live package process.
|
|
491
|
-
Development launch remains `unverified
|
|
492
|
-
|
|
649
|
+
Development launch remains `unverified` until the client Metro bridge provides
|
|
650
|
+
bundle evidence. Replaying an installed app attempt does
|
|
493
651
|
not install or launch again. Owner loss, stop, revocation and uncertain outcomes
|
|
494
|
-
retain the same reconciliation rules as iOS.
|
|
495
|
-
|
|
652
|
+
retain the same reconciliation rules as iOS.
|
|
653
|
+
|
|
654
|
+
With `hosting.agentDriver: agent-device`, each installed Android session gets
|
|
655
|
+
its own daemon and child-aware claim under `device-host/sessions/<session>/agent-device.claims`.
|
|
656
|
+
Both Macs need agent-device 0.21.22 or later. The host verifies the serial policy
|
|
657
|
+
digest and `android-instance` backend and advertises `hosted-android-agent`.
|
|
658
|
+
`device-host.app.attach` and session attach carry a session grant with
|
|
659
|
+
`android:mobile:<serial>` and the same token and client-node checks as iOS.
|
|
660
|
+
The client writes a 0600 remote config under `hosted-android/<slot>/` and reports
|
|
661
|
+
it as `android.host.agent`. Use `agent-device <command> --remote-config <file>`.
|
|
662
|
+
Missing or unsupported agent-device reports `driver: none` with its reason.
|
|
663
|
+
|
|
664
|
+
The policy pins the ledger-owned serial and filters `devices` to that emulator.
|
|
665
|
+
Inspection and interaction commands match iOS. Installs, reinstall, uninstall,
|
|
666
|
+
boot/shutdown, `close --shutdown`, record, logs, uploads, other serials (including
|
|
667
|
+
nested batches and leases), and host paths are refused. Ambient fields are
|
|
668
|
+
stripped and replaced with host-owned platform, serial and lease scope.
|
|
669
|
+
Stim installs apps. Android snapshots use one-shot helpers without adb forwards;
|
|
670
|
+
after daemon exit the host stops and verifies only the two fixed agent-device
|
|
671
|
+
helper packages on that serial. Their APKs leave with emulator deletion.
|
|
672
|
+
Reinstall, stop, revocation and close await agent teardown before native work;
|
|
673
|
+
unresolved agent claims refuse replacement and deletion after restart too.
|
|
496
674
|
|
|
497
675
|
### Hosted macOS app sessions
|
|
498
676
|
|
|
@@ -541,29 +719,65 @@ service despite the isolated home. Stop removes the app home and delivered bytes
|
|
|
541
719
|
from every app attempt, keeping logs and receipts so `app.attach` can still report
|
|
542
720
|
the recorded state.
|
|
543
721
|
|
|
544
|
-
Once a
|
|
722
|
+
Once a hosted app is `installed`, `device-host.app.launch` and
|
|
545
723
|
`device-host.app.attach` return the agent grant the host's driver issued for that
|
|
546
724
|
session, or `agent: { driver: 'none' }` while no driver runs or it cannot scope
|
|
547
725
|
the app; the driver's reason, if any, is the result's `notice`. The field is
|
|
548
726
|
built for the response and never stored in the receipt or journal; it is absent
|
|
549
|
-
before installation
|
|
727
|
+
before installation and after stop. The worker
|
|
550
728
|
reports the app's pid, `DeviceHost` registers the running app with the agent
|
|
551
729
|
driver before the receipt reads `installed`, and ends that registration when the
|
|
552
730
|
session stops, is revoked or the server closes.
|
|
553
731
|
|
|
554
|
-
`device-host.logs.query` with `{session, cursor?}` returns `{records, cursor, more}`:
|
|
732
|
+
`device-host.logs.query` with `{session, cursor?}` returns `{records, cursor, more, checkpoint?}`:
|
|
555
733
|
the NDJSON records the session's macOS app wrote to its captured log (stdout and
|
|
556
|
-
stderr as `client` records, and its exit).
|
|
734
|
+
stderr as `client` records, and its exit). iOS sessions capture app-filtered native
|
|
735
|
+
logs as `device` records through bounded `simctl log show` worker queries, with
|
|
736
|
+
a persisted time checkpoint in the isolated home. Android sessions use
|
|
737
|
+
`logcat -d -v epoch -T <epoch> --pid <pid>` on the exact ledger-owned serial after
|
|
738
|
+
re-verifying its AVD name. Each collection resolves `pidof <package>` and also
|
|
739
|
+
queries the last observed PID of that app attempt, to drain a crash or restart.
|
|
740
|
+
Without any observed PID, logs cannot be attributed to the app. PID-based logcat
|
|
741
|
+
cannot distinguish historical reuse of an exited PID. Output is bounded
|
|
742
|
+
by the executor's 64 MiB limit per command and the collection's ten-second budget;
|
|
743
|
+
an oversized query retries the recent tail and emits a `device` warning naming
|
|
744
|
+
the dropped interval. Logcat entries already evicted from its finite buffer are
|
|
745
|
+
unavailable. Both native collectors use the same checkpoint and NDJSON writer.
|
|
746
|
+
Windows adapt to the backlog,
|
|
747
|
+
overlap by five seconds and de-duplicate event identities. `checkpoint` is the last
|
|
748
|
+
completed native window in epoch milliseconds. `log show` reads persisted entries;
|
|
749
|
+
info-level entries and persistence delayed beyond the overlap may be unavailable.
|
|
750
|
+
The worker holds a separate child-aware log claim. Queries coalesce onto an
|
|
751
|
+
in-flight collection or read captured files; collection starts at most once every
|
|
752
|
+
three seconds per session. App offers, chunks, handoff, launch admission, view and
|
|
753
|
+
control stay available. Installation and stop cancel and settle collection under
|
|
754
|
+
bounded worker and termination deadlines before native work. Each native collection has a
|
|
755
|
+
10-second step budget inside its 15-second worker deadline. The client drain has
|
|
756
|
+
a 30-second bound, reports progress on stderr and stops on a stalled cursor and
|
|
757
|
+
checkpoint. The host collects once more before deletion on stop, revocation or
|
|
758
|
+
server close, preferring the recent tail if the remaining backlog will not fit.
|
|
759
|
+
If the final collection drops a backlog interval and eventually succeeds, one
|
|
760
|
+
`device` warning record names the interval. Missing or damaged checkpoints are
|
|
761
|
+
ignored and rebuilt; they never prevent reading collected logs.
|
|
762
|
+
The client waits up to 180 seconds for stop. The worker paths fit within that
|
|
763
|
+
wait: the 90-second stop worker,
|
|
764
|
+
15-second final collection and up to three 10-second worker group-settle bounds
|
|
765
|
+
leave 45 seconds for polling and transport. An in-flight handoff copy and closing
|
|
766
|
+
Metro or view transports are outside those worker bounds.
|
|
767
|
+
The client pulls that final collection after stop. Stop removes the session blob
|
|
768
|
+
store and materialized apps, retaining receipts and collected logs. `cursor` maps each log file name to the
|
|
557
769
|
byte offset after the last complete line read; pass the previous result's cursor to
|
|
558
770
|
receive only newer records, and repeat while `more` is true. Without a cursor, each
|
|
559
771
|
file starts at most 4 MiB before its end. A page holds at most 1 MiB; a file that
|
|
560
772
|
rotated since the cursor is read from the end of its previous generation. The
|
|
561
|
-
session's own client only, for macOS sessions, and also after the session stopped,
|
|
562
|
-
because stop keeps logs.
|
|
773
|
+
session's own client only, for iOS, Android and macOS sessions, and also after the session stopped,
|
|
774
|
+
because stop keeps captured logs. iOS clients check hello feature `hosted-ios-data`
|
|
775
|
+
and Android clients check `hosted-android-data`;
|
|
776
|
+
without it, they show an update note and copied logs. Servers that predate macOS queries answer `forbidden` or
|
|
563
777
|
`unknown-method`.
|
|
564
778
|
|
|
565
779
|
macOS sessions refuse Metro. Viewing and control are supported while the hosted
|
|
566
|
-
app is running. CLI placement
|
|
780
|
+
app is running. Automatic CLI placement remains a follow-up in [#2403](https://github.com/appandflow/stim/issues/2403).
|
|
567
781
|
|
|
568
782
|
### Private hosted Metro
|
|
569
783
|
|
|
@@ -575,10 +789,18 @@ gateway accepts only that worker address and authenticates each connection
|
|
|
575
789
|
before forwarding to the fixed local Metro port. It never targets a client
|
|
576
790
|
supplied URL. Close the gateway when its session ends.
|
|
577
791
|
|
|
578
|
-
Call `device-host.metro.open` with `{session, gatewayPort, secret}` on the
|
|
792
|
+
Call `device-host.metro.open` with `{session, gatewayPort, secret, clientMetroPort?}` on the
|
|
579
793
|
approved hosted connection. The server connects only to that connection's
|
|
580
794
|
authenticated tailnet peer and returns `{port}` for the worker's loopback
|
|
581
|
-
endpoint.
|
|
795
|
+
endpoint. `clientMetroPort` is required for Android and validated as an integer
|
|
796
|
+
from 1 through 65535. The session records it alongside the bridge port. Android
|
|
797
|
+
development installation reverses `tcp:<clientMetroPort>` to `tcp:<port>` on
|
|
798
|
+
the exact ledger-owned serial and sets `debug_http_host` to
|
|
799
|
+
`localhost:<clientMetroPort>`. Reopening an installed development session runs
|
|
800
|
+
a fixed reverse worker under its child-aware claim, serialized with install
|
|
801
|
+
and stop, so a client reload restores the mapping after a host adb server
|
|
802
|
+
restart. The worker never restarts the shared adb server. iOS development
|
|
803
|
+
installation uses the bridge port for `RCT_jsLocation` and,
|
|
582
804
|
when offered, the Expo development-client deep link. HTTP and WebSocket bytes
|
|
583
805
|
stream over WireGuard with socket backpressure; no public tunnel, Funnel or
|
|
584
806
|
Tailscale serve configuration change is needed. The 64 KiB server message
|
|
@@ -589,7 +811,7 @@ the bridge available for the same session while its server owner lives. Call
|
|
|
589
811
|
`device-host.metro.close` to replace a gateway, then reopen on the same port;
|
|
590
812
|
an occupied port refuses rather than sending the app to another listener.
|
|
591
813
|
Closing a bridge interrupts its active streams. Stop, revocation and server
|
|
592
|
-
shutdown close its sockets before device
|
|
814
|
+
shutdown close its sockets before device retirement. After a server owner
|
|
593
815
|
disappears, the retained session requires explicit stop, as app delivery does.
|
|
594
816
|
|
|
595
817
|
Expo dev-launcher and CLI versions that send and honor the `Forwarded` header
|
|
@@ -600,16 +822,27 @@ placement still needs to check that contract before selecting hosted Metro.
|
|
|
600
822
|
Bare React Native uses the worker `RCT_jsLocation`. Bridge readiness and
|
|
601
823
|
manifest requests are not launch proof; development remains `unverified` until
|
|
602
824
|
the workspace observes the app's own bundle delivery.
|
|
603
|
-
|
|
604
|
-
|
|
825
|
+
Approved clients use `stim ios|android --remote auto` for automatic device placement.
|
|
826
|
+
They stay local while there is a free slot, no queued device run, normal memory
|
|
827
|
+
pressure, no budget refusal and load below `offload.maxLoadPerCore`. Otherwise
|
|
828
|
+
they rank compatible offers from `hosting.machines` and select a host, or wait
|
|
829
|
+
locally if none admits. A reserve refusal after an offer fails with
|
|
830
|
+
`STIM_HOSTING_REFUSED` and asks to retry; it does not re-place the built app.
|
|
831
|
+
Automatic native macOS placement remains separate work.
|
|
605
832
|
|
|
606
|
-
### Hosted
|
|
833
|
+
### Hosted viewer relay
|
|
607
834
|
|
|
608
|
-
The client's stim-server relays a hosted workspace's macOS view and input to
|
|
835
|
+
The client's stim-server relays a hosted workspace's iOS simulator, Android emulator or macOS view and input to
|
|
609
836
|
its host using the client's approved device-host credential over the pinned
|
|
610
837
|
tailnet connection. Stim Desktop and phones keep talking only to their own
|
|
611
|
-
server. The `macos-hosted`
|
|
612
|
-
|
|
838
|
+
server. The `macos-hosted`, `ios-hosted` and `android-hosted` features advertise these relays.
|
|
839
|
+
iOS resolves `ios.host` and Android resolves `android.host` by the requested slot; macOS retains its default slot.
|
|
840
|
+
The local platform and slot replace the host's private target fields in events
|
|
841
|
+
and iOS/Android control audit records. H.264 packets retain their flags and payload and
|
|
842
|
+
carry the local subscription ID, which identifies the platform and slot.
|
|
843
|
+
The relay forwards iOS device artwork and orientation metadata. Hosted targets
|
|
844
|
+
refuse replay (`at`/`rate`), `duoFrame` and `physical` with `bad-request`.
|
|
845
|
+
All of the client's relayed subscriptions and control sessions for one host share one connection,
|
|
613
846
|
whichever local client opened them. Each request still checks the credential
|
|
614
847
|
and the pinned node; a changed credential or endpoint opens a new connection.
|
|
615
848
|
Ending a subscription or session sends `device-host.unsubscribe` or
|
|
@@ -621,15 +854,14 @@ its packets until the next keyframe and, when the host advertises the
|
|
|
621
854
|
`hosted-congestion` feature, sends `device-host.frames.congested` at most every
|
|
622
855
|
250 ms, so the host lowers that app's bitrate as it does for a local subscriber
|
|
623
856
|
whose socket backs up.
|
|
624
|
-
Hosted frames and control
|
|
625
|
-
|
|
626
|
-
`control.begin` still needs the local `control` grant. Screen Recording for
|
|
857
|
+
Hosted macOS frames and control require the default slot; hosted iOS and Android support named slots.
|
|
858
|
+
`control.begin` still needs the local `control` grant. For macOS apps, Screen Recording for
|
|
627
859
|
viewing and Accessibility for control are granted on the host, not the client.
|
|
628
860
|
|
|
629
|
-
### Hosted iOS and macOS view and input
|
|
861
|
+
### Hosted iOS, Android and macOS view and input
|
|
630
862
|
|
|
631
863
|
An approved hosting client can subscribe to its ready session's exact owned
|
|
632
|
-
iOS simulator or running macOS app without access to the worker's registered
|
|
864
|
+
iOS simulator, Android emulator or running macOS app without access to the worker's registered
|
|
633
865
|
workspaces:
|
|
634
866
|
|
|
635
867
|
```json
|
|
@@ -649,7 +881,7 @@ subscription ID as `params.subscription`. `device-host.frames.congested`
|
|
|
649
881
|
behind on that H.264 subscription; it lowers the bitrate the same way a backed-up
|
|
650
882
|
socket does.
|
|
651
883
|
Hosted capture requires the compiled `stim-frames` helper and does not support
|
|
652
|
-
replay
|
|
884
|
+
replay or screenshot fallback. iOS supports device artwork. macOS frames show only the one
|
|
653
885
|
window of the hosted app. Viewing is refused before launch and after the app
|
|
654
886
|
exits. On the hosting Mac, `stim-server service install` runs the server and
|
|
655
887
|
`stim-frames` under Stim Host, whose Screen Recording and control grants they
|
|
@@ -659,13 +891,27 @@ System Audio Recording ends the stream; missing Device Control and Data Access
|
|
|
659
891
|
(Accessibility on macOS 26 and earlier) leaves viewing available.
|
|
660
892
|
See [Run as a service](#run-as-a-service) for the settings panes and Dev caveat.
|
|
661
893
|
|
|
894
|
+
Android capture uses the same `android-device <serial> <adb> <scrcpy-server>`
|
|
895
|
+
helper transport as physical Android, addressed only to the session's exact
|
|
896
|
+
`emulator-<port>` serial. It bypasses workspace target resolution and device
|
|
897
|
+
leases. The server rechecks the private home's created-device ledger, recorded
|
|
898
|
+
serial and system image, the running AVD identity and ABI. Unknown state refuses
|
|
899
|
+
capture and input. The helper uses the host SDK from `ANDROID_HOME`,
|
|
900
|
+
`ANDROID_SDK_ROOT` or `$HOME/Library/Android/sdk`, including under the LaunchAgent.
|
|
901
|
+
It pushes the built scrcpy jar to `/data/local/tmp` and removes both that jar
|
|
902
|
+
and its adb forward when capture stops. One viewer pool serves each session;
|
|
903
|
+
the lifetime claim tracks the helper as its child. Android capture requires no
|
|
904
|
+
macOS screen-recording permission.
|
|
905
|
+
|
|
662
906
|
Start control with `device-host.control.begin` and
|
|
663
907
|
`{"session":"<hosted-session-id>"}`. Its result returns a connection-bound
|
|
664
908
|
control session ID and `lease: null`: the hosted lifetime claim protects this
|
|
665
909
|
private device. Only one controller can drive the device; `takeOver: true`
|
|
666
910
|
replaces the previous controller. Use the returned control ID with
|
|
667
911
|
`device-host.input.touch|text`, using the same parameters as ordinary input.
|
|
668
|
-
iOS also supports `device-host.input.button|rotate|posture`.
|
|
912
|
+
iOS also supports `device-host.input.button|rotate|posture`. Android supports
|
|
913
|
+
`device-host.input.button` through scrcpy; rotation and posture are unavailable
|
|
914
|
+
on this path, as for physical Android devices. macOS also supports
|
|
669
915
|
`device-host.input.scroll|key`; these two methods are for macOS sessions only.
|
|
670
916
|
Touch coordinates range from 0 to 1 on the streamed display.
|
|
671
917
|
End it with `device-host.control.end` and `{"session":"<control-id>"}`.
|
|
@@ -686,8 +932,7 @@ Hosted posture commands hold a separate child-aware input claim. A surviving
|
|
|
686
932
|
command or unresolved child identity blocks replacement ownership, install and
|
|
687
933
|
stop even after the server and capture helper exit; the refusal names the claim
|
|
688
934
|
and its manual cleanup command. An unknown or lost owner requires explicit stop
|
|
689
|
-
before replacement.
|
|
690
|
-
protocol does not add CLI placement or a local viewer relay; those remain in
|
|
935
|
+
before replacement. Automatic placement remains in
|
|
691
936
|
[#2266](https://github.com/appandflow/stim/issues/2266).
|
|
692
937
|
|
|
693
938
|
## Run as a service
|
|
@@ -1016,11 +1261,11 @@ closes its connections and cancels its builds.
|
|
|
1016
1261
|
another client, gets `bad-request`.
|
|
1017
1262
|
- `build.artifact` takes the `job` of a successful build and sends the archive
|
|
1018
1263
|
as binary frames, each 32 bytes of its sha256 and then the next bytes, then
|
|
1019
|
-
answers `{ "name", "size", "sha256" }`, and deletes it here. For
|
|
1264
|
+
answers `{ "name", "size", "sha256" }`, and deletes it here. For an iOS simulator, Android emulator or macOS
|
|
1020
1265
|
job the answer also carries `handoff`, a single-use token, and the staged
|
|
1021
|
-
`.app` stays for 10 minutes so a hosted session on this Mac can take it with
|
|
1266
|
+
`.app` or APK stays for 10 minutes so a hosted session on this Mac can take it with
|
|
1022
1267
|
`device-host.app.handoff`. A client keeps at most one such bundle; its next
|
|
1023
|
-
fetched
|
|
1268
|
+
fetched native build or a revoked approval deletes it. An archive nobody fetched is deleted when its job
|
|
1024
1269
|
is cancelled; one left by a server that crashed stays under
|
|
1025
1270
|
`repos/<repo>/out/` until you delete it.
|
|
1026
1271
|
|
|
@@ -1095,13 +1340,28 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1095
1340
|
whose `since` is in `ownLeases` is a phone.
|
|
1096
1341
|
- `logs.query` returns `{ "records" }`, and `logs.subscribe` sends `logs`
|
|
1097
1342
|
events: first the last `tail` matching records, then new ones in batches.
|
|
1098
|
-
Both take the Stim Desktop log viewer's filters: `workspace`
|
|
1099
|
-
|
|
1343
|
+
Both take the Stim Desktop log viewer's filters: exactly one of `workspace`
|
|
1344
|
+
(an environment path) or `archive` (an `archived[].id` from status),
|
|
1345
|
+
`sources` (`metro`, `client`, `device`, `build`, `agent`, `maintenance`), `slot`, `level` (the
|
|
1100
1346
|
minimum), `grep` (a regular expression), `errors`, and `tail` (1 to 5000,
|
|
1101
1347
|
5000 by default). Without `sources`, `errors` keeps the CLI's default error
|
|
1102
1348
|
scope. They run `stim logs --json` and `stim logs --json --follow` in the
|
|
1103
1349
|
workspace. Subscribers with the same workspace and filters share one
|
|
1104
|
-
`--follow` child, which stops with the last of them.
|
|
1350
|
+
`--follow` child, which stops with the last of them. Archive requests read
|
|
1351
|
+
retained logs and agent-device session actions locally through core, with the same JSON filters, ordering,
|
|
1352
|
+
markers and error context. Archive reads are synchronous and queued per
|
|
1353
|
+
connection, yielding to the event loop between requests. Pending archive
|
|
1354
|
+
reads share the four-request limit with commands. Archived session actions
|
|
1355
|
+
retain available platform and device attribution; device ids and Stim slot
|
|
1356
|
+
assignments absent from retained files are unavailable after removal. They never follow files or use a hosted relay.
|
|
1357
|
+
An archive subscription sends its records in the same batches and limits,
|
|
1358
|
+
then `{ "event": "logs-ended", "subscription": "s3" }` and releases the
|
|
1359
|
+
subscription, including when no records match. These reads need `read`.
|
|
1360
|
+
`archive` requires an updated server: omit `workspace` so older servers
|
|
1361
|
+
refuse with `bad-request` instead of reading a live replacement at that path.
|
|
1362
|
+
Both or neither selector is `bad-request`; traversal ids are `bad-request`,
|
|
1363
|
+
and unknown, invalid or symlinked archive directories are `unknown-workspace`
|
|
1364
|
+
with a message naming the archive id.
|
|
1105
1365
|
- `stats.get` returns the same payload as `stim stats --json`, using the shared
|
|
1106
1366
|
core reader in a bounded, cancellable server child. The stats part of
|
|
1107
1367
|
`machine.details` uses the same reader. `settings.get` runs
|
|
@@ -1296,7 +1556,17 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1296
1556
|
`at` and `frames.seek` on it fail with `no-recording`, and it counts
|
|
1297
1557
|
toward no idle check.
|
|
1298
1558
|
|
|
1299
|
-
- **
|
|
1559
|
+
- **Archive detail.** `archive.detail` takes `{ archive: archived[].id }` and needs
|
|
1560
|
+
`read`. It returns `{ builds, recordings }`: the archive's own iOS and Android
|
|
1561
|
+
build history in the live environment's `builds` shape, and every retained
|
|
1562
|
+
recording slot as `{ platform, slot, spans }`. Spans use the same ranges as
|
|
1563
|
+
`replay.range`; devices without spans are omitted. Missing or unreadable state
|
|
1564
|
+
yields `builds: {}`, and no recordings yields `recordings: []`. Reads share the
|
|
1565
|
+
archive request queue, refuse unknown archives with `unknown-workspace`, and
|
|
1566
|
+
skip symlinks. Older servers refuse it with `unknown-method`.
|
|
1567
|
+
`status` keeps only the archive's build summary.
|
|
1568
|
+
|
|
1569
|
+
- **Replay.** `replay.range` takes exactly one of `workspace` or `archive`, plus `platform` and `slot`
|
|
1300
1570
|
(`default` when absent), like `frames.subscribe`, and returns what can be
|
|
1301
1571
|
replayed of that device slot's [recording](#recording):
|
|
1302
1572
|
- `enabled`: the workspace's `recording.enabled`, as the last status showed
|
|
@@ -1320,7 +1590,17 @@ Events are `{ "event", "subscription", ... }`.
|
|
|
1320
1590
|
It needs only `read`. A device with no recording gets empty `spans` and
|
|
1321
1591
|
`markers`, and runs no `stim` command.
|
|
1322
1592
|
|
|
1323
|
-
|
|
1593
|
+
Archive replay reads only closed segments under the archive's recordings
|
|
1594
|
+
directory. Its `enabled` and `recording` are false; spans and markers come
|
|
1595
|
+
from those segments, archived logs and agent-device session actions. It never requires the former checkout
|
|
1596
|
+
or reads a live replacement at that path, and never uses a hosted relay.
|
|
1597
|
+
`frames.subscribe` with `archive` requires `at` and `video: ["h264"]`,
|
|
1598
|
+
rejects physical targets, and supports `frames.seek` on that subscription.
|
|
1599
|
+
`frames.live` refuses with `frames-failed`: an archive cannot go live.
|
|
1600
|
+
Empty archives return empty ranges and `no-recording` for keyframes or frames.
|
|
1601
|
+
These reads need the same `read` capability as workspace replay.
|
|
1602
|
+
|
|
1603
|
+
`replay.keyframe` takes the same `workspace` or `archive`, `platform` and `slot`, and
|
|
1324
1604
|
`at` (epoch milliseconds), and returns one still frame for a scrubber
|
|
1325
1605
|
preview without touching any subscription. The recording is stored in
|
|
1326
1606
|
segments of about 5 seconds that each start at a keyframe; the server picks
|