@unotest/web 0.42.0 → 0.43.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/CHANGELOG.md CHANGED
@@ -1,5 +1,479 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.43.1] - 2026-09-29
4
+
5
+ ### Patch Changes
6
+
7
+ - 10e03e9: A debug command written a moment after another is no longer lost. The runner read `commands.jsonl` only on file-change events, and two appends a millisecond apart (`take_wheel`, then `abort_runtime`) could arrive as one event that fired before the second write landed: the second command waited, unread, until the file changed again. The paused run was never stopped, and it kept holding the project. The runner now keeps reading while the file grows, and it also checks the file size every 250 ms.
8
+ - 75799c8: `abort_runtime` no longer answers ok for a stop the runner never took. It used to answer ok as soon as the command was written, so an agent was told a paused run had stopped while the run went on holding the project, and the next `run_test` could not start. It now answers ok once the runner has acted on the stop. A runner that is alive but does not act within ten seconds gets a `stop-not-accepted` error naming the state it is stuck in and what to do next.
9
+
10
+ What changes for you: `abort_runtime` may now answer `stop-not-accepted`
11
+ instead of `ok`; when it does, do what its `next` says.
12
+
13
+ - fd8a8b6: `box runs`, `box run`, `box queue`, `box screenshot` and `bundle push --run` no longer need `--env` when the box serves a single environment: that one is used. With several, the refusal lists them and asks for `--env <project>/<environment>`; an explicit `--env` works as before. The MCP tools `box_runs`, `box_run`, `box_run_download`, `box_queue` and `box_screenshot` follow the same rule: `environment` may be left out on a box with one environment, and calls that pass it work unchanged.
14
+ - cf3bed9: The page captured on a failure no longer holds what a `<noscript>` element contains. With JavaScript on, a browser keeps that content as plain text, so a value a page repeated there for readers without JavaScript — a secret field's included — reached the capture past the rule that hides secret fields. The element itself is kept, empty, so the page's structure is still visible.
15
+ - 6d83fe9: After `run_test` pauses and auto-attaches, `explore_start` now opens the agent's own browser instead of recording into the run's (which answered every step with `not_your_turn`), and after `release_wheel` the agent can still take screenshots, assert and wait on the run's page.
16
+ - 5433bab: Secret field values no longer leak into failures and replies. An
17
+ `assertValue` failure on a field marked as a credential (`type="password"`,
18
+ or `autocomplete` of `current-password`, `new-password` or `one-time-code`)
19
+ reports only the lengths of the two values, for example
20
+ `expected ‹hidden, 12 chars›, got ‹hidden, 32 chars›`. The same fields are
21
+ no longer read by the grounding capture, and show as `‹hidden, N chars›` in
22
+ a failure's page HTML and in a fill recorded by the viewer. MCP replies now
23
+ mask registered secrets as `‹secret:NAME›`, as the session journal already
24
+ did: text, structured content and tool error messages.
25
+
26
+ **What to check if you ran 0.43.0 or earlier.** The value of a password or
27
+ one-time-code field, and the value of a secret from `unotest/.secrets`, could
28
+ appear in plain text in MCP tool replies (for example `assert_text`,
29
+ `check_locator`, `assert_value` and the `get_failure_*` tools), in the text
30
+ of an `assertValue` failure, and in the `page.html` and `failure.json` saved
31
+ with a failed run. Search your MCP client's transcripts and your saved run
32
+ failures for your secrets and credentials; if one is there, rotate it.
33
+
34
+ - 30e45a2: `take_wheel` and `release_wheel` now answer `ok` only once the paused run has
35
+ actually handed the wheel over; before, they answered as soon as the request
36
+ was written, and on a busy machine an agent could still click in a run it had
37
+ just given back. When the run does not apply it, the tools answer
38
+ `wheel-not-accepted` with what to do next. From the moment `release_wheel` is
39
+ called, the agent's page-changing steps answer `not_your_turn` until
40
+ `take_wheel` succeeds again.
41
+
42
+ What changes for you: an agent that waited for `ok` from `take_wheel` or
43
+ `release_wheel` may now get `wheel-not-accepted` instead, and should do what
44
+ its `next` says — the wheel may simply have gone back to the human, which
45
+ `inspect_runtime` shows in its `wheel` field. A run with no runner left now
46
+ answers `no-runner` instead of `ok`. Both are marked as errors, like
47
+ `abort_runtime`'s `stop-not-accepted`.
48
+
49
+ - Updated dependencies [10e03e9]
50
+ - Updated dependencies [5433bab]
51
+ - @unotest/core@0.43.1
52
+ - @unotest/grounder-client@0.43.1
53
+ - @unotest/dsl@0.43.1
54
+ - @unotest/protocol@0.43.1
55
+ - @unotest/viewer@0.43.1
56
+
57
+ ## [0.43.0] - 2026-09-27
58
+
59
+ ### Minor Changes
60
+
61
+ - 6fc8140: A run on an environment no longer inherits a host from the base `.env`
62
+
63
+ Environment files layer: `unotest/.env` first, then the `unotest/.env.<name>`
64
+ overlay an active environment selects, the later file winning. That is the
65
+ right rule for a password or a user name, and the wrong one for a host. A
66
+ project whose base file points `API_BASE_CHAT` at production, and whose
67
+ `dev` overlay does not mention it, ran `--env dev` and sent its API calls to
68
+ production. Nothing in the output said so: the reply reported the `dev`
69
+ environment, and the requests went elsewhere.
70
+
71
+ From this release a variable that becomes a HOST has to come from the active
72
+ environment's overlay, or from the process environment. Three variables count
73
+ as hosts: `APP_BASE_URL`, which every relative `goto` resolves against; the
74
+ name passed as `apiCall(..., {base: "NAME"})`; and a variable used as the URL
75
+ in `goto(NAME)`. When the value still comes from the base file, the run is
76
+ refused before the browser starts, and `lint`, `run_test` and `explore_start`
77
+ refuse identically — with a message that names the variable, both files, and
78
+ the two ways forward. Every other variable layers exactly as before.
79
+
80
+ A base value that really is meant for every environment is declared once, in
81
+ `unotest.config.{js,mjs,ts}`:
82
+
83
+ ```js
84
+ export default { env: { shared: ["API_BASE_CHAT"] } };
85
+ ```
86
+
87
+ A value the process environment supplies — a shell variable, or the values a
88
+ box injects into a run — is never affected by this rule: it is a value
89
+ somebody stated for this run, not one silently inherited from a file the
90
+ overlay was supposed to replace.
91
+
92
+ Migration: a suite that runs with `--env <name>` and keeps its host in the
93
+ base file will now refuse, naming the variable. Either add the variable to
94
+ the overlay, which is what most projects meant, or list its name under
95
+ `env: { shared: [...] }`. A suite that never uses `--env` is unaffected.
96
+
97
+ - 703185f: A run held at its failure has a screenshot, and the tools answer while it stands
98
+
99
+ A run paused on a failure left nothing on disk: no failure bundle, no
100
+ screenshot, and `get_failure_screenshot` replied "failure bundle not
101
+ found". The frame only appeared once the run was over — which is the one
102
+ moment it is least useful, because the browser has closed and the page
103
+ that broke is gone. Meanwhile the run was sitting right there, still
104
+ showing it.
105
+
106
+ The bundle is now written at the pause, from that page, so
107
+ `list_failures`, `get_failure_screenshot`, `get_failure_a11y` and the
108
+ rest answer on the first call. Each record carries `runState`: `paused`
109
+ while the run is still held — the browser shows the failure, and a
110
+ resumed run that fails again elsewhere re-takes the frame at the new
111
+ failure — then `finished` or `aborted` once the run is over. The frame
112
+ of a finished run stays the one taken at the failure: resuming continues
113
+ with the next statement, so the page moves on while the verdict does
114
+ not, and re-taking it at the end would show a page the failure never
115
+ happened on.
116
+
117
+ Also fixed: aborting a run while it was paused left its process alive
118
+ until the 30-minute inspection TTL expired. The run now ends when it is
119
+ aborted.
120
+
121
+ - 7006895: Spending and deleting wait for a person
122
+
123
+ A key of a customer account — an agent's key, a signed-in machine's, and a CI
124
+ key issued before this release — can no longer rent a box, delete one or
125
+ switch the Picker on by itself. The cloud records what was asked for and answers with a
126
+ request instead; somebody approves or declines it in the cabinet, on the
127
+ Approvals tab, and only a signed-in browser can — not the key that asked, and
128
+ not the link in the email about it. Nothing is created and nothing is charged
129
+ until that happens.
130
+
131
+ For the CLI this is a wait, not a failure. `box create`, `box destroy` and
132
+ `box picker on` print where to answer and how long the request lives, then
133
+ carry on with what the cloud finally did and print exactly what they would
134
+ have printed anyway. Ten minutes with no answer is exit code 75 — nothing was
135
+ created and nothing was charged, run the command again. A decline is exit code 2. `--yes` does not skip it: the flag says the terminal need not ask, not that
136
+ the account need not.
137
+
138
+ Only the CI key of an operator account, one that unotest runs itself, acts
139
+ with nobody watching. A pipeline that rents or deletes boxes with a
140
+ customer's key now waits for somebody to press Approve, or has to rent its
141
+ box ahead of time.
142
+
143
+ - 4582091: `box create` with a short balance points to the dashboard instead of starting a top-up
144
+
145
+ The cloud's API for agents has no top-up: credit is added by a person, by
146
+ card, under "Add credit" on the dashboard's home page. `box create` used to
147
+ start a payment from the terminal when the balance did not cover the first
148
+ hour, against an endpoint the service does not have, so on the real cloud it
149
+ failed with a 404.
150
+
151
+ Now it stops before renting anything and says what is short: the first hour's
152
+ price, the balance and the amount missing, and the dashboard page to add it
153
+ on. It exits 75 — nothing created, nothing charged — and is run again once the
154
+ credit is there. A key that cannot read the balance (the one `login` issues
155
+ cannot) gets the price and the page, and no invented shortfall. `--json`
156
+ prints `{ "needs": "top-up", "firstHourCents", "balanceCents",
157
+ "missingCents", "currency", "url" }`, with `null` for a figure the key may not
158
+ read.
159
+
160
+ The `cloud_box_create` MCP tool answers the same case as
161
+ `{ state: "top-up-needed" }` with the same figures, instead of an error.
162
+
163
+ Migration: the `--topup <eur>` flag of `box create` is removed — top-up moved
164
+ to the dashboard. A script that passes it is refused with the usage and exit
165
+ 2; drop the flag, and add credit on the dashboard's home page before renting.
166
+
167
+ - dae29a2: The cloud account as commands and as MCP tools: `box list`, `box logs`, `approvals`, and nine `cloud_*` tools
168
+
169
+ An agent could read a box it had been pointed at and nothing more: which
170
+ boxes the account had, renting one and deleting one lived in a terminal, and
171
+ the list of boxes appeared only inside an error message. The account is now
172
+ reachable from both sides through one implementation, so a box reads the same
173
+ whether a person or an agent asked.
174
+
175
+ New commands:
176
+
177
+ - `box list` — every box on the account with its state, version and projects.
178
+ `--json` prints the same objects the `cloud_boxes_list` tool returns.
179
+ - `box logs [name] [--bootstrap|--updater]` — the end of a box's own log,
180
+ without logging in to it. A box sends it on its next five-minute check, so
181
+ the first call says it has been asked and when to run again, with exit 0.
182
+ - `approvals [id]` — what is waiting for a person on this account, or what
183
+ became of one request. Answering stays in the cabinet: a terminal holding
184
+ the key cannot approve its own request.
185
+
186
+ New MCP tools, outside the `core` toolset: `cloud_boxes_list`,
187
+ `cloud_box_status`, `cloud_box_create`, `cloud_box_destroy`,
188
+ `cloud_box_access`, `cloud_box_update`, `cloud_box_logs`, `cloud_balance`,
189
+ `cloud_approval_wait`. `cloud_box_access` writes the fresh pair into the
190
+ project the way `box access` does and answers with the names it wrote, never
191
+ the values. They answer states an agent can act on instead of
192
+ errors: `not-logged-in` with the command that fixes it, `approval-pending`
193
+ with the request to wait on, `insufficient-scope`, and `declined`,
194
+ `expired` or `still-waiting` from the wait.
195
+
196
+ While a rent or a delete waits for approval, the terminal now names it in the
197
+ words the cabinet and the email use — `Rent a box "acme", size s (€0.09 per
198
+ hour)` — and prints a line when it is approved, not only when it is declined.
199
+ The sentences live in `@unotest/protocol` (`approvalActionText`,
200
+ `approvalPrice`), shared by every surface that shows a request.
201
+
202
+ `unotest-web --help` and `box --help` list the new commands, say which
203
+ commands wait for approval, and give one table of exit codes. `box <command>
204
+ --help` now prints that command's own flags wherever `--help` sits on the
205
+ line, and `approvals` refuses an option it does not know with exit 2.
206
+
207
+ Compatibility: every call to the cloud now carries an `X-Unotest-Client`
208
+ header. The cloud refuses a call that would wait for approval when the header
209
+ is missing — an older CLI could not read the request it would be handed — with
210
+ `426 client-too-old` and a message to update. Update `@unotest/web` before
211
+ renting or deleting boxes with any key of a customer account, a CI key
212
+ included.
213
+
214
+ - ba32d73: A config key the package does not know is refused, with the key you probably meant
215
+
216
+ `chanel: "chrome"` used to be dropped in silence: the run went ahead on
217
+ the default channel, printed `1 passed` and exited 0, so the one setting
218
+ you changed was the one setting that did not apply. Every section of
219
+ `unotest/unotest.config.*` is now checked by name, before the browser
220
+ starts:
221
+
222
+ ```
223
+ e2e: config validation failed:
224
+ unknown key "chanel" — did you mean "channel"?
225
+ retry: unknown key "cuont" — did you mean "count"?
226
+ ```
227
+
228
+ A key nothing is close to gets the other answer, because it usually
229
+ comes from a config written for a newer release than the installed
230
+ package:
231
+
232
+ ```
233
+ unknown key "telemetryEndpoint" — not a config key; check your
234
+ @unotest/web version if it comes from a newer release
235
+ ```
236
+
237
+ The config `unotest-web init` writes no longer offers a `viewer` section:
238
+ the viewer's port has always come from `UNOTEST_VIEWER_PORT`, and the
239
+ section was a suggestion the schema never had. A config generated by an
240
+ earlier version needs that block removed.
241
+
242
+ **Migration.** A config that carries a key this version does not know
243
+ now fails instead of running: fix the spelling, remove the key, or
244
+ install the release the key belongs to. Nothing else about the config
245
+ changes — every known field keeps its name, its default and its merge
246
+ behaviour, and a partially written section is still merged with the
247
+ defaults key by key.
248
+
249
+ - fb12602: A step that takes minutes no longer disappears into the client's background
250
+
251
+ Waiting on a slow page — a chat answer that takes four minutes to come
252
+ back, a report that builds itself — used to be a call your agent never got
253
+ an answer to: MCP clients stop waiting at about two minutes and carry on,
254
+ and whatever the step replied after that was lost. The way round it was to
255
+ cut every wait to under a hundred seconds and repeat the step.
256
+
257
+ Now a step still running after 90 seconds answers `{ status: "accepted",
258
+ taskId }` and keeps going: same timeout, same recording into the session.
259
+ Collect it with the new `explore_step_result({ taskId })` — `running`,
260
+ `done` with the reply the step would have given, or `failed`. A batch of
261
+ steps goes to the background as one task, for one id. The `durationMs` of
262
+ a collected step is the whole step, from its start — there is no threshold
263
+ to add back by hand.
264
+
265
+ While a step is out there, the browser context says so instead of being
266
+ damaged: another step, `explore_stop` and `close_context` answer `busy`
267
+ and name the task to wait for, and `explore_state` lists what is pending.
268
+
269
+ - 82d5920: Signing in to a box with one environment opens its viewer straight away
270
+
271
+ A box configured with a single environment used to answer a sign-in with a
272
+ screen holding one option and a button. There was nothing to choose, so the
273
+ screen only stood between you and the viewer behind it.
274
+
275
+ Now that sign-in lands in the viewer. The **Environments** screen still
276
+ appears whenever it has something to say — more than one environment to pick
277
+ from, a box that has not been sent a test suite yet, or a licence in its
278
+ grace period, whose warning lives there.
279
+
280
+ So that skipping the screen does not hide what it carried, the viewer's
281
+ session menu now offers **box pages**: one link back to the box's own
282
+ pages — environments, read tokens, and administration for those who have
283
+ it. It appears when the box tells the viewer where they are, so a viewer
284
+ and a box on different versions simply do without it.
285
+
286
+ - 3aee366: take_wheel now points the session at the run it takes the wheel of
287
+
288
+ An agent debugging a paused run would call `take_wheel`, take a
289
+ screenshot, and get a picture of a completely different stand — whatever
290
+ page the session had been recording on before. Nothing was wrong with the
291
+ screenshot: the wheel only ever took the control token, while the browser
292
+ stayed where it was, and the reply said `ok` either way.
293
+
294
+ `take_wheel` now also attaches this session to the run's browser, so the
295
+ next screenshot, snapshot or step reads the page being debugged. Every
296
+ `explore_step` reply carries `context` — `{kind: "run", runtimeId}` or
297
+ `{kind: "own"}` — so the source of a frame is stated rather than guessed.
298
+ A session has exactly one browser: `take_wheel` and
299
+ `attach_debug_session` point it at a run, `explore_start` and
300
+ `new_context` at your own, and `explorationId` chooses recording and
301
+ where a file is filed, never which browser a step runs on.
302
+ `release_wheel` hands the token back and deliberately leaves the browser
303
+ on the run.
304
+
305
+ Behaviour change for anything parsing the reply: `take_wheel` no longer
306
+ always answers `ok`. When it cannot join the browser it answers
307
+ `recording_open` (a live recording would be dropped — nothing is changed,
308
+ not even the wheel), `no_browser` (the run has not published one yet) or
309
+ `no_page`, each naming what to do next.
310
+
311
+ - 9034092: Opening a box's viewer while a suite is being installed no longer looks like a failure
312
+
313
+ Sending a new suite to a box replaces that environment's viewer, and for the
314
+ second or two that takes, anyone holding the page open got a bare `502` — the
315
+ status that tells a browser nothing and tells a script the box is broken.
316
+
317
+ The box now says, while it is happening, that it is replacing that viewer. A
318
+ browser gets a short page that comes back by itself once the new viewer
319
+ answers; an agent reading the box with a token gets `503` with `Retry-After`
320
+ and `unavailable`, which is the box's existing word for "wait and ask again".
321
+
322
+ A viewer that stopped for any other reason answers the same way but promises
323
+ nothing, and the page does not reload itself: nobody is bringing it back, and
324
+ saying otherwise would be worse than saying nothing.
325
+
326
+ ### Patch Changes
327
+
328
+ - b41b02e: A cloud that will not show the price list is quoted as such, not as free
329
+
330
+ `box create` and `box picker on` quote the hourly price inside the
331
+ question they ask. When the cloud refused the price list, the question was
332
+ asked with no price in it at all — and silence there reads as "this is
333
+ free", with no way for the reader to learn that a price exists and they
334
+ were not shown it.
335
+
336
+ A refusal for want of a permission now says so: `price not visible to this
337
+ key: it needs billing:read`. On a current cloud the list is under `read`,
338
+ which every key from `login` carries — so this is what an OLDER cloud says
339
+ to a newer client, which is exactly when a client must not go quiet about
340
+ money. Every other reason stays silent, because a cloud older still has no
341
+ price list at all and a command must not fail for want of a courtesy.
342
+
343
+ - 3e7e3fc: API keys carry permissions
344
+
345
+ A key used to be all or nothing: any live key could do everything the API
346
+ offers. Now every key carries a list of permissions — see the boxes, manage
347
+ them, read the balance — and every route states the one it needs. A key
348
+ without it gets `insufficient-scope`, which is a 403 and not a 401: the
349
+ credential is good, so signing in again would change nothing.
350
+
351
+ Where the list comes from depends on the key. In the cabinet you tick the
352
+ permissions when issuing a key for an agent. Signing a machine in asks for
353
+ enough to see your boxes and work with them, never your money, and the
354
+ approval page in the browser lets you narrow that before you approve — the
355
+ key then has exactly what was left ticked. `login` and `whoami` print what
356
+ the key may do.
357
+
358
+ Keys that already exist keep the permissions they had, with one exception:
359
+ spending money and deleting with nobody watching. Only the CI key of an
360
+ operator account, one that unotest runs itself, may do that. A CI key a
361
+ customer holds from before this release works like an agent's key: renting,
362
+ deleting and switching the Picker on wait for a person to approve.
363
+
364
+ One visible change for a key that cannot read the balance: `whoami` says so
365
+ on the balance line instead of failing, and signing in with such a key is
366
+ still recognised as being signed in.
367
+
368
+ - 8353232: A rental the account cannot pay is refused before anybody is asked to approve it
369
+
370
+ A key that may not spend by itself used to get an approval request for a box
371
+ even when the balance could not cover the first hour: a person was asked to
372
+ approve it, and the rental then failed at their yes. The money is now checked
373
+ first, and such a create is answered `402 insufficient-balance` with nothing
374
+ recorded and nobody written to. The check at approval stays — a balance can
375
+ move while a request waits — and a request that fails there says so in words:
376
+ the price of the hour and where to add credit.
377
+
378
+ Every `insufficient-balance` carries `topupUrl`, the dashboard page where a
379
+ person adds credit. A key that may read the account's money also gets
380
+ `firstHourCents`, `balanceCents`, `missingCents` and `currency`; any other
381
+ key gets no figures.
382
+
383
+ `box create` prints the cloud's own figures and link when the refusal carries
384
+ them, instead of counting them again.
385
+
386
+ - 2006bf9: A refused config says which field is wrong, and `channel` takes the same words everywhere
387
+
388
+ Any bad value in `unotest/unotest.config.mjs` used to come back as one
389
+ line — `config validation failed` — with no field, no value and no list of
390
+ what would have been accepted, though all three were known. Now the
391
+ refusal names them:
392
+
393
+ ```
394
+ config validation failed:
395
+ channel: expected one of "chrome" / "msedge" / "chrome-beta" / "chromium" / "bundled" / null, got "bundeld"
396
+ defaultTimeoutMs: expected number, got string
397
+ ```
398
+
399
+ And `channel: "bundled"` is accepted. `UNOTEST_BROWSER_CHANNEL` has always
400
+ taken that word — the `.env` template says the variable merely overrides
401
+ `channel` — while the config refused it. Both inputs now share one
402
+ vocabulary, and `bundled` means what it has always meant: Playwright's own
403
+ Chromium, the default.
404
+
405
+ - e7cdab7: An empty id is refused instead of quietly changing what a tool does
406
+
407
+ `explore_step` and `explore_steps` treated `explorationId: ""` as "run
408
+ this without recording": the step went through, the reply said it had
409
+ succeeded, and nothing reached the session log. The agent found out at
410
+ `explore_stop`, with an empty recording and the work already done. The
411
+ same held for `explore_record`, and for `runtimeId: ""` in the debugger
412
+ tools, where an empty value silently meant "the session you attached to".
413
+
414
+ An empty id is now an input error naming the field, exactly as the
415
+ neighbouring tools have always answered. Leaving the key out is still how
416
+ you ask for the ad-hoc mode — absence is the mode, emptiness is a typo.
417
+
418
+ - a3f89ee: The MCP server reports the version it actually is
419
+
420
+ `initialize` answered `serverInfo` with a hard-coded `0.1.0` whatever
421
+ version of the package was running, while `--version` printed the real
422
+ number. An editor showed the wrong version in its server list, and nobody
423
+ checking a session could tell a server started from npm apart from one
424
+ started out of a working copy.
425
+
426
+ Both surfaces now read the package's own manifest through one helper, so
427
+ they cannot drift apart again. A manifest that cannot be read is an error
428
+ naming the fault, not a silent `0.0.0` — an invented version reads as a
429
+ real answer in a protocol reply and in a support transcript.
430
+
431
+ - f1f3fb1: Existing boxes install the five-minute update check themselves, and report a finished update in the tick that applied it
432
+
433
+ A box rented before the five-minute check existed only had the hourly timer:
434
+ pressing **Update** in the cabinet could take up to an hour and a half to
435
+ reach it. The updater now installs that pair of systemd units itself when the
436
+ box does not have them — exactly those two, only when the file is absent, and
437
+ never replacing a unit an operator edited.
438
+
439
+ A finished update is also reported in the tick that applied it, instead of in
440
+ the next one, so the cabinet card and `box status` stop showing "updating" for
441
+ a box that is already done.
442
+
443
+ **Both start working with the update after this one.** The move onto this
444
+ version is still carried out by the updater a box runs today: it arrives on
445
+ the hourly timer — up to an hour and a half — and that first "applied" still
446
+ comes with the next hourly tick. From there on the new updater is in charge,
447
+ a pressed **Update** lands within minutes, and the cabinet shows it as done as
448
+ soon as it is.
449
+
450
+ - 74a1f9d: The viewer finishes writing its run index before it exits
451
+
452
+ On boot, and whenever a run finishes or the environment switches, the viewer
453
+ rebuilds the derived index it serves history from. Those writes are
454
+ deliberately not waited for while the server runs — a history months deep
455
+ must not hold the UI back — but they were not waited for on the way down
456
+ either. Stopping the viewer returned while files were still landing under the
457
+ run directory: a directory the caller believed had been released kept growing
458
+ for a fraction of a second, and anything that removed or archived it right
459
+ after the viewer stopped could fail or capture a half-written index.
460
+
461
+ Shutdown now waits for the index writes that are still in flight, so a
462
+ stopped viewer is a viewer that has finished writing. A rebuild that takes
463
+ longer makes the stop take longer, which is the honest cost of not leaving
464
+ half a file behind.
465
+
466
+ - Updated dependencies [6fc8140]
467
+ - Updated dependencies [4582091]
468
+ - Updated dependencies [dae29a2]
469
+ - Updated dependencies [82d5920]
470
+ - Updated dependencies [74a1f9d]
471
+ - @unotest/core@0.43.0
472
+ - @unotest/protocol@0.43.0
473
+ - @unotest/viewer@0.43.0
474
+ - @unotest/dsl@0.43.0
475
+ - @unotest/grounder-client@0.43.0
476
+
3
477
  ## [0.42.0] - 2026-09-24
4
478
 
5
479
  ### Minor Changes
@@ -818,7 +1292,7 @@ picker on` fetches this project's own access and writes the three grounder
818
1292
 
819
1293
  That screen named one command:
820
1294
 
821
- npx @unotest/web bundle push --run --env ENVIRONMENT
1295
+ npx @unotest/web bundle push --run --env ENVIRONMENT
822
1296
 
823
1297
  In a project that has just been created, the command refuses with `no box
824
1298
  address` before it does anything: the project holds neither the box's address
@@ -827,8 +1301,8 @@ address` before it does anything: the project holds neither the box's address
827
1301
 
828
1302
  The screen now names both steps, in the order they have to happen:
829
1303
 
830
- npx @unotest/web box access larch
831
- npx @unotest/web bundle push --run --env ENVIRONMENT
1304
+ npx @unotest/web box access larch
1305
+ npx @unotest/web bundle push --run --env ENVIRONMENT
832
1306
 
833
1307
  The box's own name is filled in when its licence carries one. `box access`
834
1308
  with no name refuses, and its refusal offers to create a box — to somebody who
package/README.md CHANGED
@@ -152,15 +152,47 @@ npx @unotest/web box create # rents a box named after the project, size S
152
152
  boxes — and keeps the key in `~/.config/unotest/credentials.json`
153
153
  (mode 0600). `box create` creates the box with the environments the
154
154
  suite already runs locally (each `unotest/.env.<name>` and its
155
- `APP_BASE_URL`), asks for a top-up when the balance is short (it prints
156
- the Checkout link and waits for the payment), waits for the box, writes
155
+ `APP_BASE_URL`), waits for the box, writes
157
156
  `UNOTEST_BOX_URL` to `unotest/.env` and the push and read tokens to
158
157
  `unotest/.secrets`, pushes the suite and prints the viewer's address.
159
158
  The tokens live thirty days; `bundle push` and `box …` re-mint them
160
159
  through the cloud when a box says they have expired, and `box access`
161
- does it on demand. `box status` and `box destroy` do what they say;
162
- `whoami` and `logout` complete the set. In CI, export
163
- `UNOTEST_CLOUD_TOKEN` instead of signing in.
160
+ does it on demand. `box list` shows every box on the account, `box status`
161
+ one of them, and `box logs` the end of a box's own log without logging in to
162
+ it; `box destroy` does what it says, and `whoami` and `logout` complete the
163
+ set. In CI, export `UNOTEST_CLOUD_TOKEN` instead of signing in.
164
+
165
+ Credit is added in the cabinet, by card, under "Add credit" on its home page
166
+ — never from the terminal. When the balance does not cover a new box's first
167
+ hour, `box create` says how much is missing (as far as the key may read the
168
+ balance) and where to add it, and exits 75 with nothing created and nothing
169
+ charged; run it again once the credit is there.
170
+
171
+ Anything that spends money or cannot be undone — creating a box, deleting
172
+ one, switching the Picker on — **waits for a person** unless the key is a CI
173
+ key. The CLI prints where to approve it and how long the request lives, waits
174
+ there, and then carries on exactly as if the call had gone straight through.
175
+ Nobody answers in ten minutes: exit code 75, nothing created and nothing
176
+ charged, run the command again. Somebody declines: exit code 2. `--yes` does
177
+ not change any of this — the flag says the terminal need not ask, not that
178
+ the account need not.
179
+ `approvals` lists what is waiting, and `approvals <id>` says what became of
180
+ one.
181
+
182
+ An agent does the same through the MCP server's `cloud_*` tools — listing,
183
+ renting, deleting, logs, the balance — with the same key and the same rules:
184
+ a rent or a delete comes back as a request to wait on, and
185
+ `cloud_approval_wait` waits for the person's answer.
186
+
187
+ A key carries **permissions**. `login` asks for enough to see the
188
+ account's boxes and work with them, never its money, and the approval
189
+ page in the browser is where you can narrow that further; `login` and
190
+ `whoami` print what the key ended up with. A key that may not read the
191
+ balance is still a working key — `whoami` says the balance is not
192
+ visible to it instead of failing — and a call it may not make comes
193
+ back as `insufficient-scope`, which no amount of signing in again will
194
+ change. Keys for an agent are issued in the cabinet, with the
195
+ permissions ticked there.
164
196
 
165
197
  The **Picker** — grounding intent locators on the box's grounder — comes
166
198
  with the box today: it lists at €0.05–€0.13 an hour depending on size,