@stim-cli/server 1.16.0 → 1.17.1

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 CHANGED
@@ -16,7 +16,7 @@ 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 paired devices, approved clients and access requests
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
@@ -57,7 +57,7 @@ app (checked at most every 5 seconds), or `null` when the check fails. Stim
57
57
  Desktop uses it to find a running server and show its route. A request through `tailscale serve` or on a Tailscale address
58
58
  without an `Origin` or `Sec-Fetch-Site` header, which a web page's request
59
59
  carries, gets only `{ "server": "stim-server", "version", "protocol" }`, which Stim
60
- Desktop's Build machines list uses to find stim-server on the other Macs of the
60
+ Desktop's Remote Macs list uses to find stim-server on the other Macs of the
61
61
  tailnet. A request from this Mac with a `Host` other than `127.0.0.1` or
62
62
  `localhost` gets HTTP 426, like any other plain HTTP request.
63
63
 
@@ -91,7 +91,7 @@ A person on the worker runs the command generated for their client Mac:
91
91
  ```bash
92
92
  stim-server setup --client <node-id> --ticket <43-base64url-characters> --expires <ISO-time> \
93
93
  --build --device-host [--port <n>] [--label <name>] \
94
- [--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json]
94
+ [--env KEY=VALUE]... [--path-prepend <dir>]... [--yes] [--json] [--verbose]
95
95
  ```
96
96
 
97
97
  If the server is not installed globally, use `npx --yes --package @stim-cli/server@<version> stim-server`
@@ -144,8 +144,17 @@ unreadable routes refuse with a remedy. Tool checks print fixes for Xcode,
144
144
  simulator runtimes, CocoaPods, JDK, Android SDK and the Stim build; they install
145
145
  nothing. Android and CocoaPods checks are not needed for hosting alone.
146
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`,
147
+ In a terminal, setup prints a banner, then one line per step with a green check,
148
+ a yellow arrow while a step runs, or a red cross; `NO_COLOR`, `TERM=dumb` and a
149
+ non-terminal give plain `[ok]`, `[failed]` and `[pending]` marks, no colors and no
150
+ in-place line. The y/N questions stay, one line each. The run ends with a
151
+ headline such as "janics-mac-mini is ready to build for Janic's MacBook Pro", one
152
+ line per approved capability, a `Fix` line for each skipped permission or missing
153
+ tool, and one `To undo` line. `--verbose` prints every step with its detail, the
154
+ install, service and route notes, and the long summary with the undo commands, as
155
+ earlier releases did. Plain progress goes to stdout; errors use
156
+ `stim-server: <message>` on stderr (when stdout is a terminal, the refusal appears on stdout only).
157
+ `--json` sends the long (verbose) progress to stderr, without the banner, and prints one final payload with `ok`,
149
158
  `label`, `port`, `route` (state, DNS name, HTTPS port), `server` (version and
150
159
  Stim build), `managed`, separate `granted` request ids and client identities,
151
160
  `permissions`, `tools` and `warnings`. Final permission values are `granted`,
@@ -172,7 +181,7 @@ with detail `interrupted`, completes the journal, releases the setup claim,
172
181
  and exits 1. A typed N also exits 1; a question timeout follows ticket expiry
173
182
  and exits 2.
174
183
 
175
- The summary lists what is ready and the undo commands:
184
+ The `To undo` line (or the `--verbose` summary) lists the undo commands:
176
185
 
177
186
  ```bash
178
187
  stim-server devices revoke <build-request-id>
@@ -284,7 +293,7 @@ registration check. `hello` reports
284
293
  the capabilities and actions of the connection's device when it connects; a
285
294
  connection sees a new grant after it reconnects. Under Stim Host, an approved
286
295
  `hello` also carries the same `host` grants as `/health`, which `stim doctor`
287
- reports for hosting machines.
296
+ reports for remote Macs.
288
297
 
289
298
  Every method other than `hello` needs `read`. A device with only `build`
290
299
  gets `forbidden` for all of them.
@@ -331,16 +340,15 @@ tokens.
331
340
  Device hosting has a separate `device-host` capability. An approved client can
332
341
  reserve, boot, reconnect to and stop its own iOS simulator or Android emulator
333
342
  through the protocol. It can deliver, install and launch a compatible iOS app
334
- bundle, Android APK or prebuilt macOS app, stream the iOS simulator or macOS app and control it. The hosted iOS
335
- app connects back to Metro on the client Mac. Automatic CLI placement and
336
- Android Metro and viewing remain in [#2266](https://github.com/appandflow/stim/issues/2266).
343
+ bundle, Android APK or prebuilt macOS app, stream the iOS simulator, Android emulator or macOS app and control it. Hosted
344
+ iOS and Android apps connect back to Metro on the client Mac. Automatic placement remains in [#2266](https://github.com/appandflow/stim/issues/2266).
337
345
 
338
346
  The Stim client can name expected hosts with
339
- `stim settings set hosting.machines '["<mac>"]'` and request access with
347
+ `stim settings set remote.machines '["<mac>"]'` and request access with
340
348
  `stim doctor --fix`. It stores a separate private credential in
341
349
  `$STIM_HOME/device-host-machines.json`, pins the worker's tailnet node, and
342
350
  reports approval under `deviceHosts` in doctor JSON. Plain doctor makes no
343
- approval request. This setup does not yet place CLI sessions remotely.
351
+ approval request. Use `stim ios --remote <machine>` or `stim android --remote <machine>` for explicit placement.
344
352
 
345
353
  A client on the tailnet sends `hello` with
346
354
  `auth: { "request": "device-host", "deviceName": "Laptop" }`. As with a build
@@ -365,6 +373,31 @@ from phone pairings and build clients. A hosting token grants no `read`,
365
373
  read, control and build tokens cannot gain hosting through `devices grant`.
366
374
  Loopback requests and `pair --device-host` are refused.
367
375
 
376
+ ### Hosted here
377
+
378
+ `stim-server devices` adds a **Hosted here** block after pairings and approvals,
379
+ with the client name, platform, device label, installed app bundle id (or `-`),
380
+ state and start time. `--json` adds `hostedSessions` next to `devices`, including
381
+ an empty array when there are none. Rows are newest first; plain stopped
382
+ sessions are omitted, while parked and unresolved sessions stay visible.
383
+ A parked row uses its parking time. A removed client's name falls back to its id.
384
+ The command reads the host journal directly and needs no server connection.
385
+
386
+ `device-host.sessions` takes no params (or `{}`) and returns `{ sessions }`.
387
+ Each row has `id`, `client: { id, name }`, `platform`, nullable `device` and `app`,
388
+ `state`, `parked`, `since` (ISO time), and `workspace`.
389
+ `device-host.sessions.stop` takes `{ "session": "<id>" }` and returns
390
+ `{ "id": "<id>", "state": "stopping" | "stopped" }`. It stops any client's
391
+ session through the existing stop path, closing Metro, view, agent and logs
392
+ before owned-device teardown. Repeating a stopped session returns `stopped`;
393
+ an absent id gets `unknown-session`.
394
+ Both requests require an authenticated loopback Desktop connection with local
395
+ identity and control access. Local read-only connections, paired phones and
396
+ remote hosting clients get `forbidden`. A session whose active owner is not
397
+ attached to this server reads as `unknown`; stop reconciles it through the same
398
+ ownership checks. The approved-client `device-host.stop` remains limited to
399
+ that client's own sessions.
400
+
368
401
  ### Hosted availability offers
369
402
 
370
403
  Before reserving, an approved hosting client can send:
@@ -572,7 +605,7 @@ session before starting another transfer. Manifest, file and partial blobs are s
572
605
  by attempts of one session in its content-addressed store; offers recheck complete
573
606
  blob digests and request only missing bytes.
574
607
 
575
- An iOS simulator or macOS session can take its content from a build this Mac ran instead: after
608
+ An iOS simulator, Android emulator or macOS session can take its content from a build this Mac ran instead: after
576
609
  the manifest arrives, send `device-host.app.handoff` with
577
610
  `{session, attempt, build: {handoff, sha256}}`, where `handoff` is the token
578
611
  and `sha256` the archive digest from that job's `build.artifact` answer. The
@@ -583,7 +616,10 @@ declares links, whose directory resolves inside the staged bundle and whose
583
616
  bytes match their manifest digest; it answers `{files, bytes}` for what it
584
617
  took. The token is spent once the handoff starts. Re-offer and upload whatever
585
618
  is still missing; an older server answers `unknown-method`. For iOS, check the
586
- `hosted-ios-data` hello feature before requesting handoff; without it, upload.
619
+ `hosted-ios-data` hello feature before requesting handoff; for Android check
620
+ `hosted-android-data`. Without the feature, upload. Android takes only a regular
621
+ APK file matching the single `App.apk` manifest entry. Handoff failure falls back
622
+ to upload. Reused APK blobs skip transfer, while every new attempt still installs.
587
623
 
588
624
  After every digest is verified, call `device-host.app.launch` with
589
625
  `{session, attempt}`. Poll `device-host.app.attach` for `installed` or `unknown`.
@@ -619,11 +655,31 @@ signature or installation conflict refuses; hosting does not uninstall an
619
655
  existing app to resolve it.
620
656
 
621
657
  Release launch reports `true` only after observing a live package process.
622
- Development launch remains `unverified`; this driver launches the activity but
623
- does not wire an Android Metro bridge. Replaying an installed app attempt does
658
+ Development launch remains `unverified` until the client Metro bridge provides
659
+ bundle evidence. Replaying an installed app attempt does
624
660
  not install or launch again. Owner loss, stop, revocation and uncertain outcomes
625
- retain the same reconciliation rules as iOS. Android viewing, Metro and client
626
- placement remain under [#2266](https://github.com/appandflow/stim/issues/2266).
661
+ retain the same reconciliation rules as iOS.
662
+
663
+ With `hosting.agentDriver: agent-device`, each installed Android session gets
664
+ its own daemon and child-aware claim under `device-host/sessions/<session>/agent-device.claims`.
665
+ Both Macs need agent-device 0.21.22 or later. The host verifies the serial policy
666
+ digest and `android-instance` backend and advertises `hosted-android-agent`.
667
+ `device-host.app.attach` and session attach carry a session grant with
668
+ `android:mobile:<serial>` and the same token and client-node checks as iOS.
669
+ The client writes a 0600 remote config under `hosted-android/<slot>/` and reports
670
+ it as `android.host.agent`. Use `agent-device <command> --remote-config <file>`.
671
+ Missing or unsupported agent-device reports `driver: none` with its reason.
672
+
673
+ The policy pins the ledger-owned serial and filters `devices` to that emulator.
674
+ Inspection and interaction commands match iOS. Installs, reinstall, uninstall,
675
+ boot/shutdown, `close --shutdown`, record, logs, uploads, other serials (including
676
+ nested batches and leases), and host paths are refused. Ambient fields are
677
+ stripped and replaced with host-owned platform, serial and lease scope.
678
+ Stim installs apps. Android snapshots use one-shot helpers without adb forwards;
679
+ after daemon exit the host stops and verifies only the two fixed agent-device
680
+ helper packages on that serial. Their APKs leave with emulator deletion.
681
+ Reinstall, stop, revocation and close await agent teardown before native work;
682
+ unresolved agent claims refuse replacement and deletion after restart too.
627
683
 
628
684
  ### Hosted macOS app sessions
629
685
 
@@ -672,12 +728,12 @@ service despite the isolated home. Stop removes the app home and delivered bytes
672
728
  from every app attempt, keeping logs and receipts so `app.attach` can still report
673
729
  the recorded state.
674
730
 
675
- Once a macOS app is `installed`, `device-host.app.launch` and
731
+ Once a hosted app is `installed`, `device-host.app.launch` and
676
732
  `device-host.app.attach` return the agent grant the host's driver issued for that
677
733
  session, or `agent: { driver: 'none' }` while no driver runs or it cannot scope
678
734
  the app; the driver's reason, if any, is the result's `notice`. The field is
679
735
  built for the response and never stored in the receipt or journal; it is absent
680
- before installation, after stop and for iOS and Android sessions. The worker
736
+ before installation and after stop. The worker
681
737
  reports the app's pid, `DeviceHost` registers the running app with the agent
682
738
  driver before the receipt reads `installed`, and ends that registration when the
683
739
  session stops, is revoked or the server closes.
@@ -686,9 +742,19 @@ session stops, is revoked or the server closes.
686
742
  the NDJSON records the session's macOS app wrote to its captured log (stdout and
687
743
  stderr as `client` records, and its exit). iOS sessions capture app-filtered native
688
744
  logs as `device` records through bounded `simctl log show` worker queries, with
689
- a persisted time checkpoint in the isolated home. Windows adapt to the backlog,
745
+ a persisted time checkpoint in the isolated home. Android sessions use
746
+ `logcat -d -v epoch -T <epoch> --pid <pid>` on the exact ledger-owned serial after
747
+ re-verifying its AVD name. Each collection resolves `pidof <package>` and also
748
+ queries the last observed PID of that app attempt, to drain a crash or restart.
749
+ Without any observed PID, logs cannot be attributed to the app. PID-based logcat
750
+ cannot distinguish historical reuse of an exited PID. Output is bounded
751
+ by the executor's 64 MiB limit per command and the collection's ten-second budget;
752
+ an oversized query retries the recent tail and emits a `device` warning naming
753
+ the dropped interval. Logcat entries already evicted from its finite buffer are
754
+ unavailable. Both native collectors use the same checkpoint and NDJSON writer.
755
+ Windows adapt to the backlog,
690
756
  overlap by five seconds and de-duplicate event identities. `checkpoint` is the last
691
- completed iOS window in epoch milliseconds. `log show` reads persisted entries;
757
+ completed native window in epoch milliseconds. `log show` reads persisted entries;
692
758
  info-level entries and persistence delayed beyond the overlap may be unavailable.
693
759
  The worker holds a separate child-aware log claim. Queries coalesce onto an
694
760
  in-flight collection or read captured files; collection starts at most once every
@@ -713,8 +779,9 @@ byte offset after the last complete line read; pass the previous result's cursor
713
779
  receive only newer records, and repeat while `more` is true. Without a cursor, each
714
780
  file starts at most 4 MiB before its end. A page holds at most 1 MiB; a file that
715
781
  rotated since the cursor is read from the end of its previous generation. The
716
- session's own client only, for iOS and macOS sessions, and also after the session stopped,
717
- because stop keeps captured logs. iOS clients check hello feature `hosted-ios-data`;
782
+ session's own client only, for iOS, Android and macOS sessions, and also after the session stopped,
783
+ because stop keeps captured logs. iOS clients check hello feature `hosted-ios-data`
784
+ and Android clients check `hosted-android-data`;
718
785
  without it, they show an update note and copied logs. Servers that predate macOS queries answer `forbidden` or
719
786
  `unknown-method`.
720
787
 
@@ -731,10 +798,18 @@ gateway accepts only that worker address and authenticates each connection
731
798
  before forwarding to the fixed local Metro port. It never targets a client
732
799
  supplied URL. Close the gateway when its session ends.
733
800
 
734
- Call `device-host.metro.open` with `{session, gatewayPort, secret}` on the
801
+ Call `device-host.metro.open` with `{session, gatewayPort, secret, clientMetroPort?}` on the
735
802
  approved hosted connection. The server connects only to that connection's
736
803
  authenticated tailnet peer and returns `{port}` for the worker's loopback
737
- endpoint. Development installation uses that port for `RCT_jsLocation` and,
804
+ endpoint. `clientMetroPort` is required for Android and validated as an integer
805
+ from 1 through 65535. The session records it alongside the bridge port. Android
806
+ development installation reverses `tcp:<clientMetroPort>` to `tcp:<port>` on
807
+ the exact ledger-owned serial and sets `debug_http_host` to
808
+ `localhost:<clientMetroPort>`. Reopening an installed development session runs
809
+ a fixed reverse worker under its child-aware claim, serialized with install
810
+ and stop, so a client reload restores the mapping after a host adb server
811
+ restart. The worker never restarts the shared adb server. iOS development
812
+ installation uses the bridge port for `RCT_jsLocation` and,
738
813
  when offered, the Expo development-client deep link. HTTP and WebSocket bytes
739
814
  stream over WireGuard with socket backpressure; no public tunnel, Funnel or
740
815
  Tailscale serve configuration change is needed. The 64 KiB server message
@@ -756,18 +831,23 @@ placement still needs to check that contract before selecting hosted Metro.
756
831
  Bare React Native uses the worker `RCT_jsLocation`. Bridge readiness and
757
832
  manifest requests are not launch proof; development remains `unverified` until
758
833
  the workspace observes the app's own bundle delivery.
759
- This is a protocol API for approved clients; automatic iOS CLI placement,
760
- Android Metro/viewing remains in [#2266](https://github.com/appandflow/stim/issues/2266).
834
+ Approved clients use `stim ios|android --remote auto` for automatic device placement.
835
+ They stay local while there is a free slot, no queued device run, normal memory
836
+ pressure, no budget refusal and load below `server.maxLoadPerCore`. Otherwise
837
+ they rank compatible offers from `remote.machines` and select a host, or wait
838
+ locally if none admits. A reserve refusal after an offer fails with
839
+ `STIM_HOSTING_REFUSED` and asks to retry; it does not re-place the built app.
840
+ Automatic native macOS placement remains separate work.
761
841
 
762
842
  ### Hosted viewer relay
763
843
 
764
- The client's stim-server relays a hosted workspace's iOS simulator or macOS view and input to
844
+ The client's stim-server relays a hosted workspace's iOS simulator, Android emulator or macOS view and input to
765
845
  its host using the client's approved device-host credential over the pinned
766
846
  tailnet connection. Stim Desktop and phones keep talking only to their own
767
- server. The `macos-hosted` and `ios-hosted` features advertise these relays.
768
- iOS resolves `ios.host` by the requested slot; macOS retains its default slot.
847
+ server. The `macos-hosted`, `ios-hosted` and `android-hosted` features advertise these relays.
848
+ iOS resolves `ios.host` and Android resolves `android.host` by the requested slot; macOS retains its default slot.
769
849
  The local platform and slot replace the host's private target fields in events
770
- and iOS control audit records. H.264 packets retain their flags and payload and
850
+ and iOS/Android control audit records. H.264 packets retain their flags and payload and
771
851
  carry the local subscription ID, which identifies the platform and slot.
772
852
  The relay forwards iOS device artwork and orientation metadata. Hosted targets
773
853
  refuse replay (`at`/`rate`), `duoFrame` and `physical` with `bad-request`.
@@ -783,14 +863,14 @@ its packets until the next keyframe and, when the host advertises the
783
863
  `hosted-congestion` feature, sends `device-host.frames.congested` at most every
784
864
  250 ms, so the host lowers that app's bitrate as it does for a local subscriber
785
865
  whose socket backs up.
786
- Hosted macOS frames and control require the default slot; hosted iOS supports named slots.
866
+ Hosted macOS frames and control require the default slot; hosted iOS and Android support named slots.
787
867
  `control.begin` still needs the local `control` grant. For macOS apps, Screen Recording for
788
868
  viewing and Accessibility for control are granted on the host, not the client.
789
869
 
790
- ### Hosted iOS and macOS view and input
870
+ ### Hosted iOS, Android and macOS view and input
791
871
 
792
872
  An approved hosting client can subscribe to its ready session's exact owned
793
- iOS simulator or running macOS app without access to the worker's registered
873
+ iOS simulator, Android emulator or running macOS app without access to the worker's registered
794
874
  workspaces:
795
875
 
796
876
  ```json
@@ -810,7 +890,7 @@ subscription ID as `params.subscription`. `device-host.frames.congested`
810
890
  behind on that H.264 subscription; it lowers the bitrate the same way a backed-up
811
891
  socket does.
812
892
  Hosted capture requires the compiled `stim-frames` helper and does not support
813
- replay, device artwork or screenshot fallback. macOS frames show only the one
893
+ replay or screenshot fallback. iOS supports device artwork. macOS frames show only the one
814
894
  window of the hosted app. Viewing is refused before launch and after the app
815
895
  exits. On the hosting Mac, `stim-server service install` runs the server and
816
896
  `stim-frames` under Stim Host, whose Screen Recording and control grants they
@@ -820,13 +900,27 @@ System Audio Recording ends the stream; missing Device Control and Data Access
820
900
  (Accessibility on macOS 26 and earlier) leaves viewing available.
821
901
  See [Run as a service](#run-as-a-service) for the settings panes and Dev caveat.
822
902
 
903
+ Android capture uses the same `android-device <serial> <adb> <scrcpy-server>`
904
+ helper transport as physical Android, addressed only to the session's exact
905
+ `emulator-<port>` serial. It bypasses workspace target resolution and device
906
+ leases. The server rechecks the private home's created-device ledger, recorded
907
+ serial and system image, the running AVD identity and ABI. Unknown state refuses
908
+ capture and input. The helper uses the host SDK from `ANDROID_HOME`,
909
+ `ANDROID_SDK_ROOT` or `$HOME/Library/Android/sdk`, including under the LaunchAgent.
910
+ It pushes the built scrcpy jar to `/data/local/tmp` and removes both that jar
911
+ and its adb forward when capture stops. One viewer pool serves each session;
912
+ the lifetime claim tracks the helper as its child. Android capture requires no
913
+ macOS screen-recording permission.
914
+
823
915
  Start control with `device-host.control.begin` and
824
916
  `{"session":"<hosted-session-id>"}`. Its result returns a connection-bound
825
917
  control session ID and `lease: null`: the hosted lifetime claim protects this
826
918
  private device. Only one controller can drive the device; `takeOver: true`
827
919
  replaces the previous controller. Use the returned control ID with
828
920
  `device-host.input.touch|text`, using the same parameters as ordinary input.
829
- iOS also supports `device-host.input.button|rotate|posture`. macOS also supports
921
+ iOS also supports `device-host.input.button|rotate|posture`. Android supports
922
+ `device-host.input.button` through scrcpy; rotation and posture are unavailable
923
+ on this path, as for physical Android devices. macOS also supports
830
924
  `device-host.input.scroll|key`; these two methods are for macOS sessions only.
831
925
  Touch coordinates range from 0 to 1 on the streamed display.
832
926
  End it with `device-host.control.end` and `{"session":"<control-id>"}`.
@@ -847,14 +941,13 @@ Hosted posture commands hold a separate child-aware input claim. A surviving
847
941
  command or unresolved child identity blocks replacement ownership, install and
848
942
  stop even after the server and capture helper exit; the refusal names the claim
849
943
  and its manual cleanup command. An unknown or lost owner requires explicit stop
850
- before replacement. This worker
851
- protocol does not add CLI placement or a local viewer relay; those remain in
944
+ before replacement. Automatic placement remains in
852
945
  [#2266](https://github.com/appandflow/stim/issues/2266).
853
946
 
854
947
  ## Run as a service
855
948
 
856
949
  `stim-server service install` runs stim-server as a per-user LaunchAgent on
857
- macOS, so a build machine or a phone-serving Mac keeps it running without a
950
+ macOS, so a remote Mac or a phone-serving Mac keeps it running without a
858
951
  terminal:
859
952
 
860
953
  ```bash
@@ -995,18 +1088,18 @@ and uninstall of one label run one at a time.
995
1088
  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
996
1089
  second service beside the first, for example with another `STIM_HOME`.
997
1090
 
998
- To set up a build machine: install Stim and stim-server on the Mac, run
1091
+ To set up a remote Mac: install Stim and stim-server on the Mac, run
999
1092
  `stim-server service install --serve` (with the pins its toolchain needs), set
1000
- `offload.workerRoot` there, and on each client run
1001
- `stim settings set offload.machines '["<mac>"]'` and `stim doctor --fix`.
1002
- Approve the request on the build machine with
1093
+ `server.workerRoot` there, and on each client run
1094
+ `stim settings set remote.machines '["<mac>"]'` and `stim doctor --fix`.
1095
+ Approve the request on the remote Mac with
1003
1096
  `stim-server devices grant <id> --build`.
1004
1097
 
1005
1098
  ## Remote update
1006
1099
 
1007
1100
  A Mac this one approved for builds (`--build`) or device hosting
1008
1101
  (`--device-host`) can update this Mac's `stim-server service` over its tailnet
1009
- connection, so the build machine keeps up with the client without ssh. A
1102
+ connection, so the remote Mac keeps up with the client without ssh. A
1010
1103
  server that offers it lists `server-update` in its `hello` features. A paired
1011
1104
  phone, a `read` or `control` device and a connection from this Mac cannot. The
1012
1105
  methods need that approval, not `read`:
@@ -1042,8 +1135,8 @@ of an existing hosted session still answers during the drain.
1042
1135
 
1043
1136
  Stim Desktop starts these updates through its own local stim-server with two
1044
1137
  methods that only the local Desktop control connection may call:
1045
- `machines.update.start` takes `{ "machine" }`, an approved `offload.machines`
1046
- or `hosting.machines` entry. It connects to that machine's pinned node with
1138
+ `machines.update.start` takes `{ "machine" }`, an approved `remote.machines`
1139
+ entry. It connects to that machine's pinned node with
1047
1140
  the stored credential and asks it for this server's npm release. When this
1048
1141
  server runs from a Stim checkout, it instead packs the checkout's workspace
1049
1142
  packages as `npm pack` would and sends them. `machines.update.status` takes
@@ -1076,7 +1169,7 @@ closes its connections and cancels its builds.
1076
1169
  average per CPU), `builds` (this Mac's own Stim runs in prebuild, pods or
1077
1170
  compile that it did not offload, plus the offloaded builds it runs), `maxBuilds` (its
1078
1171
  `concurrency.maxBuilds`, 0 when unlimited), `maxLoadPerCore` (its
1079
- `offload.maxLoadPerCore`, default 2), and `declined`: why it would refuse a
1172
+ `server.maxLoadPerCore`, default 2), and `declined`: why it would refuse a
1080
1173
  build now, or null. It declines while it runs its limit of offloaded builds,
1081
1174
  while the worker root's volume has less than 10 GiB free, while `builds`
1082
1175
  reaches a non-zero `maxBuilds`, or while `loadPerCore` is at or above
@@ -1113,7 +1206,7 @@ closes its connections and cancels its builds.
1113
1206
  `assemble<variant>` for `abi` with ccache under the area's Stim home. A
1114
1207
  Gradle daemon leaves the build's process group, so no claim or slot tracks
1115
1208
  it. An Android build keeps the client's daemon warm, with Gradle's idle
1116
- timeout set to `offload.gradleDaemonIdleMinutes` (default 30; 0 stops it
1209
+ timeout set to `server.gradleDaemonIdleMinutes` (default 30; 0 stops it
1117
1210
  when the build ends), and stops it when the build gets SIGTERM. Gradle
1118
1211
  fixes a daemon's idle timeout when the daemon starts, so a new value
1119
1212
  applies to the next daemon. Once no build of that client runs, stim-server
@@ -1177,15 +1270,15 @@ closes its connections and cancels its builds.
1177
1270
  another client, gets `bad-request`.
1178
1271
  - `build.artifact` takes the `job` of a successful build and sends the archive
1179
1272
  as binary frames, each 32 bytes of its sha256 and then the next bytes, then
1180
- answers `{ "name", "size", "sha256" }`, and deletes it here. For an iOS simulator or macOS
1273
+ answers `{ "name", "size", "sha256" }`, and deletes it here. For an iOS simulator, Android emulator or macOS
1181
1274
  job the answer also carries `handoff`, a single-use token, and the staged
1182
- `.app` stays for 10 minutes so a hosted session on this Mac can take it with
1275
+ `.app` or APK stays for 10 minutes so a hosted session on this Mac can take it with
1183
1276
  `device-host.app.handoff`. A client keeps at most one such bundle; its next
1184
- fetched iOS or macOS build or a revoked approval deletes it. An archive nobody fetched is deleted when its job
1277
+ fetched native build or a revoked approval deletes it. An archive nobody fetched is deleted when its job
1185
1278
  is cancelled; one left by a server that crashed stays under
1186
1279
  `repos/<repo>/out/` until you delete it.
1187
1280
 
1188
- The worker root is `offload.workerRoot` in this Mac's Stim settings, or
1281
+ The worker root is `server.workerRoot` in this Mac's Stim settings, or
1189
1282
  `$STIM_HOME/build-worker`. Each client gets `<root>/<device id>/`, with its
1190
1283
  blobs, caches (`CP_HOME_DIR`, `CP_CACHE_DIR`, the pnpm store and
1191
1284
  `GRADLE_USER_HOME` and SwiftPM) and one area
@@ -1472,6 +1565,16 @@ Events are `{ "event", "subscription", ... }`.
1472
1565
  `at` and `frames.seek` on it fail with `no-recording`, and it counts
1473
1566
  toward no idle check.
1474
1567
 
1568
+ - **Archive detail.** `archive.detail` takes `{ archive: archived[].id }` and needs
1569
+ `read`. It returns `{ builds, recordings }`: the archive's own iOS and Android
1570
+ build history in the live environment's `builds` shape, and every retained
1571
+ recording slot as `{ platform, slot, spans }`. Spans use the same ranges as
1572
+ `replay.range`; devices without spans are omitted. Missing or unreadable state
1573
+ yields `builds: {}`, and no recordings yields `recordings: []`. Reads share the
1574
+ archive request queue, refuse unknown archives with `unknown-workspace`, and
1575
+ skip symlinks. Older servers refuse it with `unknown-method`.
1576
+ `status` keeps only the archive's build summary.
1577
+
1475
1578
  - **Replay.** `replay.range` takes exactly one of `workspace` or `archive`, plus `platform` and `slot`
1476
1579
  (`default` when absent), like `frames.subscribe`, and returns what can be
1477
1580
  replayed of that device slot's [recording](#recording):
@@ -1618,10 +1721,10 @@ Events are `{ "event", "subscription", ... }`.
1618
1721
  one result for 60 seconds, shared by every connection: a request while the
1619
1722
  commands run waits for them, and `measuredAt` says when they started. The
1620
1723
  commands fail after 150 seconds. Servers that predate it answer
1621
- `unknown-method`. It also carries `buildMachines`, the `buildMachines` list
1724
+ `unknown-method`. It also carries `buildMachines`, the `remoteMachines` list
1622
1725
  of `stim doctor --json --platform ios` (never with `--fix`), so a phone can
1623
- show whether each build machine takes builds. The server runs doctor only
1624
- when `offload.machines` names a machine, else the list is empty; it runs it
1726
+ show whether each remote Mac takes builds. The server runs doctor only
1727
+ when `remote.machines` names a machine, else the list is empty; it runs it
1625
1728
  in the registered workspace that ran doctor for iOS most recently, because
1626
1729
  doctor judges a machine against an app. Like a doctor run from a terminal,
1627
1730
  that records the run for the workspace. `buildMachines` is null with
@@ -1633,7 +1736,7 @@ Events are `{ "event", "subscription", ... }`.
1633
1736
  at a time, and sets `buildMachinesPending` while that background run has not
1634
1737
  settled a result yet, or the settled one is 60 seconds old or older. A client
1635
1738
  that wants the refreshed result asks `machine.details` again. It also
1636
- carries `buildClients`, the builds this Mac ran as a build machine, one entry
1739
+ carries `buildClients`, the builds this Mac ran for other Macs, one entry
1637
1740
  per client Mac read from the `build` records of the audit log, with `id`,
1638
1741
  `name`, `builds`, `failed`, `buildMs`, `lastAt` and the same three counts
1639
1742
  for `today`, most recent client first, with `today` on this Mac's local