@stim-cli/server 1.15.0 → 1.16.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 CHANGED
@@ -22,6 +22,8 @@ stim-server devices grant <id> --control|--read|--build|--device-host
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 and
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
@@ -207,7 +332,7 @@ 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
334
  bundle, Android APK or prebuilt macOS app, stream the iOS simulator or macOS app and control it. The hosted iOS
210
- app connects back to Metro on the client Mac. Automatic CLI placement, client view/control relays and
335
+ app connects back to Metro on the client Mac. Automatic CLI placement and
211
336
  Android Metro and viewing remain in [#2266](https://github.com/appandflow/stim/issues/2266).
212
337
 
213
338
  The Stim client can name expected hosts with
@@ -226,7 +351,8 @@ and at most eight device-host requests can be pending. Build requests have a
226
351
  separate limit. The same name validation and failed-attempt limit apply.
227
352
 
228
353
  On the hosting Mac, inspect `stim-server devices`, then approve the matching
229
- request with `stim-server devices grant <id> --device-host`, or use **Allow**
354
+ request with `stim-server devices grant <id> --device-host`, run
355
+ [`stim-server setup`](#set-up-a-worker-mac) there with its node and ticket, or use **Allow**
230
356
  in Stim Desktop. Approve only an expected request: it authorizes that Mac to
231
357
  reserve session-owned simulators or emulators and run native app code. **Deny**
232
358
  or `stim-server devices revoke <id>` removes it; revocation also closes its
@@ -347,14 +473,15 @@ After a lost reply or connection, replay the same reserve request or attach to
347
473
  its attempt. That resolves the same session; a changed request with that attempt
348
474
  is refused. Another attempt cannot replace an occupied workspace/slot. The
349
475
  client never supplies a worker filesystem path or another client's session.
350
- `device-host.stop` with `{"session":"<id>"}` shuts down only its recorded,
476
+ `device-host.stop` with `{"session":"<id>"}` deletes only its recorded,
351
477
  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
- ownership and reconciliation; stop does not delete the simulator.
478
+ after the previous one is confirmed stopped. Stopped session homes and device records remain for reconciliation. At start,
479
+ stim-server retires leftover stopped iOS and Android devices through their exact
480
+ private ledger ownership.
354
481
 
355
482
  The worker persists the journal under `$STIM_HOME/server/device-host-sessions/`
356
483
  and chooses an isolated worker home under `$STIM_HOME/device-host/sessions/`.
357
- An uncertain create, child exit, journal or shutdown outcome retains the slot
484
+ An uncertain create, child exit, journal or deletion outcome retains the slot
358
485
  as `unknown`. Explicit stop can reconcile a complete record after a server
359
486
  restart; a live or unverifiable owner is refused. Missing ownership records
360
487
  need operator investigation, never a replacement inferred from a simulator name.
@@ -403,8 +530,9 @@ A ready Android `device` has `avdName`, `serial`, `consolePort`, `systemImage`,
403
530
  its exact AVD name and running ABI before reporting ready. Creation remains
404
531
  inside the worker's claimed process group. The worker opens no emulator viewer.
405
532
  Stop and revocation use centralized teardown only for its private ledger's exact
406
- AVD. They shut down and retain its AVD data and record; unknown native outcomes
407
- retain the reservation and require explicit reconciliation. Android Metro
533
+ AVD. They delete that AVD and retain the session home and record. At start,
534
+ stim-server retires leftover stopped sessions; unknown native outcomes retain
535
+ the reservation and require explicit reconciliation. Android Metro
408
536
  and screen/input routing remain follow-ups in #2266. Android sessions
409
537
  refuse the iOS Metro, view and input routes.
410
538
 
@@ -440,9 +568,11 @@ offsets. Upload the manifest first, then re-offer for its missing file content.
440
568
  The manifest is limited to 8 MiB and 20,000 entries, with at most 1 GiB per file,
441
569
  1 KiB per link, and 4 GiB of declared bundle content. An app attempt cannot
442
570
  change its identity, mode, arguments or manifest. Complete a receiving attempt or stop the
443
- session before starting another transfer.
571
+ session before starting another transfer. Manifest, file and partial blobs are shared
572
+ by attempts of one session in its content-addressed store; offers recheck complete
573
+ blob digests and request only missing bytes.
444
574
 
445
- A macOS session can take its content from a build this Mac ran instead: after
575
+ An iOS simulator or macOS session can take its content from a build this Mac ran instead: after
446
576
  the manifest arrives, send `device-host.app.handoff` with
447
577
  `{session, attempt, build: {handoff, sha256}}`, where `handoff` is the token
448
578
  and `sha256` the archive digest from that job's `build.artifact` answer. The
@@ -452,7 +582,8 @@ tailnet node. It copies only regular files, and links where the manifest
452
582
  declares links, whose directory resolves inside the staged bundle and whose
453
583
  bytes match their manifest digest; it answers `{files, bytes}` for what it
454
584
  took. The token is spent once the handoff starts. Re-offer and upload whatever
455
- is still missing; an older server answers `unknown-method`.
585
+ 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.
456
587
 
457
588
  After every digest is verified, call `device-host.app.launch` with
458
589
  `{session, attempt}`. Poll `device-host.app.attach` for `installed` or `unknown`.
@@ -551,19 +682,44 @@ reports the app's pid, `DeviceHost` registers the running app with the agent
551
682
  driver before the receipt reads `installed`, and ends that registration when the
552
683
  session stops, is revoked or the server closes.
553
684
 
554
- `device-host.logs.query` with `{session, cursor?}` returns `{records, cursor, more}`:
685
+ `device-host.logs.query` with `{session, cursor?}` returns `{records, cursor, more, checkpoint?}`:
555
686
  the NDJSON records the session's macOS app wrote to its captured log (stdout and
556
- stderr as `client` records, and its exit). `cursor` maps each log file name to the
687
+ stderr as `client` records, and its exit). iOS sessions capture app-filtered native
688
+ 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,
690
+ 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;
692
+ info-level entries and persistence delayed beyond the overlap may be unavailable.
693
+ The worker holds a separate child-aware log claim. Queries coalesce onto an
694
+ in-flight collection or read captured files; collection starts at most once every
695
+ three seconds per session. App offers, chunks, handoff, launch admission, view and
696
+ control stay available. Installation and stop cancel and settle collection under
697
+ bounded worker and termination deadlines before native work. Each native collection has a
698
+ 10-second step budget inside its 15-second worker deadline. The client drain has
699
+ a 30-second bound, reports progress on stderr and stops on a stalled cursor and
700
+ checkpoint. The host collects once more before deletion on stop, revocation or
701
+ server close, preferring the recent tail if the remaining backlog will not fit.
702
+ If the final collection drops a backlog interval and eventually succeeds, one
703
+ `device` warning record names the interval. Missing or damaged checkpoints are
704
+ ignored and rebuilt; they never prevent reading collected logs.
705
+ The client waits up to 180 seconds for stop. The worker paths fit within that
706
+ wait: the 90-second stop worker,
707
+ 15-second final collection and up to three 10-second worker group-settle bounds
708
+ leave 45 seconds for polling and transport. An in-flight handoff copy and closing
709
+ Metro or view transports are outside those worker bounds.
710
+ The client pulls that final collection after stop. Stop removes the session blob
711
+ store and materialized apps, retaining receipts and collected logs. `cursor` maps each log file name to the
557
712
  byte offset after the last complete line read; pass the previous result's cursor to
558
713
  receive only newer records, and repeat while `more` is true. Without a cursor, each
559
714
  file starts at most 4 MiB before its end. A page holds at most 1 MiB; a file that
560
715
  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. Servers that predate it answer `forbidden` or
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`;
718
+ without it, they show an update note and copied logs. Servers that predate macOS queries answer `forbidden` or
563
719
  `unknown-method`.
564
720
 
565
721
  macOS sessions refuse Metro. Viewing and control are supported while the hosted
566
- app is running. CLI placement and client view/control relays remain follow-ups in [#2403](https://github.com/appandflow/stim/issues/2403).
722
+ app is running. Automatic CLI placement remains a follow-up in [#2403](https://github.com/appandflow/stim/issues/2403).
567
723
 
568
724
  ### Private hosted Metro
569
725
 
@@ -589,7 +745,7 @@ the bridge available for the same session while its server owner lives. Call
589
745
  `device-host.metro.close` to replace a gateway, then reopen on the same port;
590
746
  an occupied port refuses rather than sending the app to another listener.
591
747
  Closing a bridge interrupts its active streams. Stop, revocation and server
592
- shutdown close its sockets before device shutdown. After a server owner
748
+ shutdown close its sockets before device retirement. After a server owner
593
749
  disappears, the retained session requires explicit stop, as app delivery does.
594
750
 
595
751
  Expo dev-launcher and CLI versions that send and honor the `Forwarded` header
@@ -601,15 +757,21 @@ Bare React Native uses the worker `RCT_jsLocation`. Bridge readiness and
601
757
  manifest requests are not launch proof; development remains `unverified` until
602
758
  the workspace observes the app's own bundle delivery.
603
759
  This is a protocol API for approved clients; automatic iOS CLI placement,
604
- client iOS view/control relays and Android Metro/viewing remain in [#2266](https://github.com/appandflow/stim/issues/2266).
760
+ Android Metro/viewing remains in [#2266](https://github.com/appandflow/stim/issues/2266).
605
761
 
606
- ### Hosted macOS relay
762
+ ### Hosted viewer relay
607
763
 
608
- The client's stim-server relays a hosted workspace's macOS view and input to
764
+ The client's stim-server relays a hosted workspace's iOS simulator or macOS view and input to
609
765
  its host using the client's approved device-host credential over the pinned
610
766
  tailnet connection. Stim Desktop and phones keep talking only to their own
611
- server. The `macos-hosted` feature advertises this relay. All of the client's
612
- relayed subscriptions and control sessions for one host share one connection,
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.
769
+ 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
771
+ carry the local subscription ID, which identifies the platform and slot.
772
+ The relay forwards iOS device artwork and orientation metadata. Hosted targets
773
+ refuse replay (`at`/`rate`), `duoFrame` and `physical` with `bad-request`.
774
+ All of the client's relayed subscriptions and control sessions for one host share one connection,
613
775
  whichever local client opened them. Each request still checks the credential
614
776
  and the pinned node; a changed credential or endpoint opens a new connection.
615
777
  Ending a subscription or session sends `device-host.unsubscribe` or
@@ -621,9 +783,8 @@ its packets until the next keyframe and, when the host advertises the
621
783
  `hosted-congestion` feature, sends `device-host.frames.congested` at most every
622
784
  250 ms, so the host lowers that app's bitrate as it does for a local subscriber
623
785
  whose socket backs up.
624
- Hosted frames and control reject `physical`, a non-default slot, and replay
625
- (`at`/`rate`).
626
- `control.begin` still needs the local `control` grant. Screen Recording for
786
+ Hosted macOS frames and control require the default slot; hosted iOS supports named slots.
787
+ `control.begin` still needs the local `control` grant. For macOS apps, Screen Recording for
627
788
  viewing and Accessibility for control are granted on the host, not the client.
628
789
 
629
790
  ### Hosted iOS and macOS view and input
@@ -1016,11 +1177,11 @@ closes its connections and cancels its builds.
1016
1177
  another client, gets `bad-request`.
1017
1178
  - `build.artifact` takes the `job` of a successful build and sends the archive
1018
1179
  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 a macOS
1180
+ answers `{ "name", "size", "sha256" }`, and deletes it here. For an iOS simulator or macOS
1020
1181
  job the answer also carries `handoff`, a single-use token, and the staged
1021
1182
  `.app` stays for 10 minutes so a hosted session on this Mac can take it with
1022
1183
  `device-host.app.handoff`. A client keeps at most one such bundle; its next
1023
- fetched macOS build or a revoked approval deletes it. An archive nobody fetched is deleted when its job
1184
+ fetched iOS or macOS build or a revoked approval deletes it. An archive nobody fetched is deleted when its job
1024
1185
  is cancelled; one left by a server that crashed stays under
1025
1186
  `repos/<repo>/out/` until you delete it.
1026
1187
 
@@ -1095,13 +1256,28 @@ Events are `{ "event", "subscription", ... }`.
1095
1256
  whose `since` is in `ownLeases` is a phone.
1096
1257
  - `logs.query` returns `{ "records" }`, and `logs.subscribe` sends `logs`
1097
1258
  events: first the last `tail` matching records, then new ones in batches.
1098
- Both take the Stim Desktop log viewer's filters: `workspace` (required),
1099
- `sources` (`metro`, `client`, `device`, `build`, `agent`), `slot`, `level` (the
1259
+ Both take the Stim Desktop log viewer's filters: exactly one of `workspace`
1260
+ (an environment path) or `archive` (an `archived[].id` from status),
1261
+ `sources` (`metro`, `client`, `device`, `build`, `agent`, `maintenance`), `slot`, `level` (the
1100
1262
  minimum), `grep` (a regular expression), `errors`, and `tail` (1 to 5000,
1101
1263
  5000 by default). Without `sources`, `errors` keeps the CLI's default error
1102
1264
  scope. They run `stim logs --json` and `stim logs --json --follow` in the
1103
1265
  workspace. Subscribers with the same workspace and filters share one
1104
- `--follow` child, which stops with the last of them.
1266
+ `--follow` child, which stops with the last of them. Archive requests read
1267
+ retained logs and agent-device session actions locally through core, with the same JSON filters, ordering,
1268
+ markers and error context. Archive reads are synchronous and queued per
1269
+ connection, yielding to the event loop between requests. Pending archive
1270
+ reads share the four-request limit with commands. Archived session actions
1271
+ retain available platform and device attribution; device ids and Stim slot
1272
+ assignments absent from retained files are unavailable after removal. They never follow files or use a hosted relay.
1273
+ An archive subscription sends its records in the same batches and limits,
1274
+ then `{ "event": "logs-ended", "subscription": "s3" }` and releases the
1275
+ subscription, including when no records match. These reads need `read`.
1276
+ `archive` requires an updated server: omit `workspace` so older servers
1277
+ refuse with `bad-request` instead of reading a live replacement at that path.
1278
+ Both or neither selector is `bad-request`; traversal ids are `bad-request`,
1279
+ and unknown, invalid or symlinked archive directories are `unknown-workspace`
1280
+ with a message naming the archive id.
1105
1281
  - `stats.get` returns the same payload as `stim stats --json`, using the shared
1106
1282
  core reader in a bounded, cancellable server child. The stats part of
1107
1283
  `machine.details` uses the same reader. `settings.get` runs
@@ -1296,7 +1472,7 @@ Events are `{ "event", "subscription", ... }`.
1296
1472
  `at` and `frames.seek` on it fail with `no-recording`, and it counts
1297
1473
  toward no idle check.
1298
1474
 
1299
- - **Replay.** `replay.range` takes `workspace`, `platform` and `slot`
1475
+ - **Replay.** `replay.range` takes exactly one of `workspace` or `archive`, plus `platform` and `slot`
1300
1476
  (`default` when absent), like `frames.subscribe`, and returns what can be
1301
1477
  replayed of that device slot's [recording](#recording):
1302
1478
  - `enabled`: the workspace's `recording.enabled`, as the last status showed
@@ -1320,7 +1496,17 @@ Events are `{ "event", "subscription", ... }`.
1320
1496
  It needs only `read`. A device with no recording gets empty `spans` and
1321
1497
  `markers`, and runs no `stim` command.
1322
1498
 
1323
- `replay.keyframe` takes the same `workspace`, `platform` and `slot`, and
1499
+ Archive replay reads only closed segments under the archive's recordings
1500
+ directory. Its `enabled` and `recording` are false; spans and markers come
1501
+ from those segments, archived logs and agent-device session actions. It never requires the former checkout
1502
+ or reads a live replacement at that path, and never uses a hosted relay.
1503
+ `frames.subscribe` with `archive` requires `at` and `video: ["h264"]`,
1504
+ rejects physical targets, and supports `frames.seek` on that subscription.
1505
+ `frames.live` refuses with `frames-failed`: an archive cannot go live.
1506
+ Empty archives return empty ranges and `no-recording` for keyframes or frames.
1507
+ These reads need the same `read` capability as workspace replay.
1508
+
1509
+ `replay.keyframe` takes the same `workspace` or `archive`, `platform` and `slot`, and
1324
1510
  `at` (epoch milliseconds), and returns one still frame for a scrubber
1325
1511
  preview without touching any subscription. The recording is stored in
1326
1512
  segments of about 5 seconds that each start at a keyframe; the server picks
@@ -187,6 +187,13 @@ function protocolJsonSchema() {
187
187
  "deviceKey"
188
188
  ],
189
189
  additionalProperties: false,
190
+ oneOf: [{
191
+ required: ["backend"],
192
+ properties: { deviceKey: { pattern: "^ios:mobile:[a-fA-F0-9-]{36}$" } }
193
+ }, {
194
+ not: { required: ["backend"] },
195
+ properties: { deviceKey: { pattern: "^[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)+@[1-9][0-9]{0,9}$" } }
196
+ }],
190
197
  properties: {
191
198
  tenant: {
192
199
  type: "string",
@@ -200,10 +207,8 @@ function protocolJsonSchema() {
200
207
  type: "string",
201
208
  pattern: "^[A-Za-z0-9._-]{1,128}$"
202
209
  },
203
- deviceKey: {
204
- type: "string",
205
- pattern: "^[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)+@[1-9][0-9]{0,9}$"
206
- }
210
+ backend: { const: "ios-instance" },
211
+ deviceKey: { type: "string" }
207
212
  }
208
213
  }
209
214
  }
@@ -720,6 +725,12 @@ function protocolJsonSchema() {
720
725
  deviceName: {
721
726
  type: "string",
722
727
  minLength: 1
728
+ },
729
+ setupTicket: {
730
+ type: "string",
731
+ minLength: 43,
732
+ maxLength: 43,
733
+ pattern: "^[A-Za-z0-9_-]{43}$"
723
734
  }
724
735
  }
725
736
  }
@@ -819,13 +830,19 @@ function protocolJsonSchema() {
819
830
  },
820
831
  LogFilter: {
821
832
  type: "object",
822
- required: ["workspace"],
833
+ oneOf: [{ required: ["workspace"] }, { required: ["archive"] }],
823
834
  additionalProperties: false,
824
835
  properties: {
825
836
  workspace: {
826
837
  type: "string",
827
838
  description: "An environment path from a status payload."
828
839
  },
840
+ archive: {
841
+ type: "string",
842
+ minLength: 1,
843
+ pattern: "^(?!.*\\.\\.)[^/\\\\\\u0000]+$",
844
+ description: "An archived[].id from a status payload; omit workspace."
845
+ },
829
846
  sources: {
830
847
  type: "array",
831
848
  minItems: 1,
@@ -958,13 +975,27 @@ function protocolJsonSchema() {
958
975
  },
959
976
  FrameTarget: {
960
977
  type: "object",
961
- required: ["workspace", "platform"],
978
+ required: ["platform"],
979
+ oneOf: [{
980
+ required: ["workspace"],
981
+ not: { required: ["archive"] }
982
+ }, {
983
+ required: ["archive", "at"],
984
+ not: { required: ["workspace"] },
985
+ properties: { physical: { const: false } }
986
+ }],
962
987
  additionalProperties: false,
963
988
  properties: {
964
989
  workspace: {
965
990
  type: "string",
966
991
  description: "An environment path from a status payload."
967
992
  },
993
+ archive: {
994
+ type: "string",
995
+ minLength: 1,
996
+ pattern: "^(?!.*\\.\\.)[^/\\\\\\u0000]+$",
997
+ description: "An archived[].id from a status payload; omit workspace."
998
+ },
968
999
  platform: { enum: [...PLATFORMS] },
969
1000
  slot: {
970
1001
  type: "string",
@@ -1678,7 +1709,8 @@ function protocolJsonSchema() {
1678
1709
  video: {
1679
1710
  type: "array",
1680
1711
  items: { type: "string" }
1681
- }
1712
+ },
1713
+ deviceFrame: { type: "boolean" }
1682
1714
  })),
1683
1715
  request("device-host.frames.keyframe", {
1684
1716
  type: "object",
@@ -1816,10 +1848,16 @@ function protocolJsonSchema() {
1816
1848
  }),
1817
1849
  request("replay.range", {
1818
1850
  type: "object",
1819
- required: ["workspace", "platform"],
1851
+ required: ["platform"],
1852
+ oneOf: [{ required: ["workspace"] }, { required: ["archive"] }],
1820
1853
  additionalProperties: false,
1821
1854
  properties: {
1822
1855
  workspace: { type: "string" },
1856
+ archive: {
1857
+ type: "string",
1858
+ minLength: 1,
1859
+ pattern: "^(?!.*\\.\\.)[^/\\\\\\u0000]+$"
1860
+ },
1823
1861
  platform: { enum: [...RELOAD_PLATFORMS] },
1824
1862
  slot: {
1825
1863
  type: "string",
@@ -1829,14 +1867,16 @@ function protocolJsonSchema() {
1829
1867
  }),
1830
1868
  request("replay.keyframe", {
1831
1869
  type: "object",
1832
- required: [
1833
- "workspace",
1834
- "platform",
1835
- "at"
1836
- ],
1870
+ required: ["platform", "at"],
1871
+ oneOf: [{ required: ["workspace"] }, { required: ["archive"] }],
1837
1872
  additionalProperties: false,
1838
1873
  properties: {
1839
1874
  workspace: { type: "string" },
1875
+ archive: {
1876
+ type: "string",
1877
+ minLength: 1,
1878
+ pattern: "^(?!.*\\.\\.)[^/\\\\\\u0000]+$"
1879
+ },
1840
1880
  platform: { enum: [...RELOAD_PLATFORMS] },
1841
1881
  slot: {
1842
1882
  type: "string",
@@ -2500,6 +2540,8 @@ function protocolJsonSchema() {
2500
2540
  additionalProperties: false,
2501
2541
  properties: {
2502
2542
  event: { const: "device-frame" },
2543
+ platform: { enum: [...PLATFORMS] },
2544
+ slot: { type: "string" },
2503
2545
  subscription: { type: "string" },
2504
2546
  artwork: { oneOf: [{ type: "null" }, { $ref: "#/$defs/DeviceFrameArtwork" }] }
2505
2547
  }
@@ -2574,6 +2616,15 @@ function protocolJsonSchema() {
2574
2616
  }
2575
2617
  }
2576
2618
  },
2619
+ {
2620
+ type: "object",
2621
+ required: ["event", "subscription"],
2622
+ additionalProperties: false,
2623
+ properties: {
2624
+ event: { const: "logs-ended" },
2625
+ subscription: { type: "string" }
2626
+ }
2627
+ },
2577
2628
  {
2578
2629
  type: "object",
2579
2630
  required: [
@@ -2653,6 +2704,8 @@ function protocolJsonSchema() {
2653
2704
  additionalProperties: false,
2654
2705
  properties: {
2655
2706
  event: { const: "frame-delayed" },
2707
+ platform: { enum: [...PLATFORMS] },
2708
+ slot: { type: "string" },
2656
2709
  subscription: { type: "string" },
2657
2710
  delayed: { type: "boolean" },
2658
2711
  reason: { type: "string" }
@@ -2683,6 +2736,8 @@ function protocolJsonSchema() {
2683
2736
  additionalProperties: false,
2684
2737
  properties: {
2685
2738
  event: { const: "control-ended" },
2739
+ platform: { enum: [...PLATFORMS] },
2740
+ slot: { type: "string" },
2686
2741
  session: { type: "string" },
2687
2742
  reason: { enum: [...CONTROL_END_REASONS] },
2688
2743
  message: { type: "string" }
@@ -2698,6 +2753,8 @@ function protocolJsonSchema() {
2698
2753
  additionalProperties: false,
2699
2754
  properties: {
2700
2755
  event: { const: "error" },
2756
+ platform: { enum: [...PLATFORMS] },
2757
+ slot: { type: "string" },
2701
2758
  subscription: { type: "string" },
2702
2759
  error: { $ref: "#/$defs/ProtocolError" }
2703
2760
  }
package/dist/protocol.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { t as protocolJsonSchema } from "./protocol-MBMvJpvV.mjs";
1
+ import { t as protocolJsonSchema } from "./protocol-BHSl7bQ_.mjs";
2
2
  export * from "@stim-cli/core/protocol";
3
3
  export { protocolJsonSchema };