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