@qoniai/qoni 0.7.0 → 0.9.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,128 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 - 2026-10-07
4
+
5
+ DoAnything interactions are now typed by what the user has to do, never by
6
+ business: `site_login`, `take_control`, `ask_user`, `confirmation`, plus the
7
+ system `wait`.
8
+
9
+ ### Changed
10
+
11
+ - `Interaction` is a union distributed over `type`: `switch (interaction.type)`
12
+ narrows `payload` in each branch, including `handle.interaction` inside
13
+ `onInteraction`. `Interaction<"site_login">` still picks one member.
14
+ `TypedInteraction` names the union. Because it is now a type alias, not an
15
+ interface, `interface X extends Interaction` no longer compiles (TS2312):
16
+ use `type X = Interaction & { ... }`. Code that builds an `Interaction` from a
17
+ separate `type` and `payload` must narrow them together or cast the object.
18
+ - The wire's `clarification` is presented as `ask_user`, with payload
19
+ `{ question, answerType, options? }`. `answerType` is `text`, `number`,
20
+ `boolean`, `single_choice` or `multiple_choice`, and options are
21
+ `{ value, label }`. A clarification with choices becomes `single_choice`; one
22
+ without choices is `text`. A native `ask_user` keeps the `answer_type` it
23
+ sends, and every string option value is kept, including `""`.
24
+ Migration: compare `type` with `"ask_user"` instead of `"clarification"`
25
+ (TypeScript no longer compiles the old comparison); read
26
+ `payload.answerType` / `payload.options` instead of `payload.inputType`
27
+ (removed) or the answer action's `inputSchema` enum.
28
+ `InteractionTypes.Clarification` is deprecated and now equals
29
+ `InteractionTypes.AskUser`: it still matches interactions the SDK hands out,
30
+ but not a raw wire `type` or a `"clarification"` string you stored.
31
+ `ClarificationPayload` is a deprecated alias of `AskUserPayload`, which
32
+ requires `answerType`.
33
+ - `InteractionHandle.answer(value)` accepts the structured value its
34
+ `answerType` names (string, number, boolean, one option value, or an array of
35
+ option values) and throws `QoniValidationError` before sending a value that
36
+ does not fit.
37
+ - `InteractionHandle.confirmSignedIn()` saves the login opened by `openLogin()`
38
+ on the same interaction (`POST /profiles/{profileId}/confirm`, scope
39
+ `webagent.site_login:confirm`), then tells the agent to recheck. Sign-in is
40
+ `openLogin()` → `confirmSignedIn()`:
41
+ - It saves once and rechecks once per login session, however many handles
42
+ or concurrent calls ask; the deprecated `OpenLoginResult.confirm()` shares
43
+ the same save.
44
+ - A user who is not signed in yet, or 409 `no_pending_login`, still gets a
45
+ recheck: the agent raises a new `site_login` when needed.
46
+ - When the backend keeps the session but could not save it
47
+ (`probeResult: "release_failed"`), it throws a retryable `QoniError` coded
48
+ `site_login.save_failed` and does not tell the agent to go on; calling it
49
+ again saves again. Any other save failure is thrown as well.
50
+ - When `openLogin()` calls overlap, the latest call that succeeds owns the
51
+ session, whichever response arrives last.
52
+ - Reopening that returns the same backend session shares its save; when a
53
+ newer backend session replaces it, `confirm()` on the old result throws
54
+ `QoniValidationError` instead of saving. If the login is reopened while a
55
+ `confirmSignedIn()` is still saving, that call throws a retryable
56
+ `QoniError` coded `site_login.session_replaced` without telling the agent
57
+ to go on; call it again to save the newer login.
58
+ - Handles from the same `RunHandle` or `MonitorHandle` share the login
59
+ session, so a handle from a later callback saves it too. A RunHandle from
60
+ another `attach()` or process does not.
61
+ - `connectControl()` and `refreshControl()` resolve to a `ControlSession`
62
+ (`{ liveUrl, expiresAt?, status?, raw }`, the signed URL of the browser the
63
+ user now controls) instead of `void`. `await handle.connectControl()` is
64
+ unaffected; code that returns the call where `Promise<void>` is expected,
65
+ such as `const cb: () => Promise<void> = () => handle.connectControl()`, no
66
+ longer compiles (TS2322) — wrap it: `async () => { await handle.connectControl(); }`.
67
+ - `run.events()`, `wait({ onInteraction })` and `monitor.events()` keep an
68
+ interaction's type and content when the backend settles it with an empty
69
+ shell (a resolve labelled `clarification` with an empty question, or a
70
+ `site_login` with one empty site): only `status`, `resolvedAt` and `actions`
71
+ change. A settled update with content of its own, such as an expired
72
+ take-over with its reason, replaces the card as before. Run and monitor
73
+ handles keep about 256 interactions, forgetting settled cards first, and
74
+ logins once they are saved or their interaction ended; a pending card or a
75
+ login still in progress is never dropped.
76
+
77
+ ### Added
78
+
79
+ - `QoniScopes.USER_PROFILE_READ` (`user.profile:read`), `USER_PROFILE_WRITE`
80
+ (`user.profile:write`), `USER_PAYMENT_USE` (`user.payment:use`),
81
+ `AGENT_MAIL_READ` (`agent.mail:read`) and `AGENT_MAIL_SEND`
82
+ (`agent.mail:send`). They belong to no product bundle: request them
83
+ explicitly, and allow them in the AccessKey policy. The constants only name
84
+ the scopes; requesting them does not by itself enable Profile, payment or
85
+ mailbox access in a run.
86
+ - Exported types `AskUserPayload`, `AskUserOption`, `AskUserAnswer`,
87
+ `AskUserAnswerType`, `InteractionBase`, `TypedInteraction` and
88
+ `ControlSession`.
89
+
90
+ ### Fixed
91
+
92
+ - A scope written without its service prefix names every matching scope:
93
+ `profile:read` suggests both `gumem.profile:read` and `user.profile:read`.
94
+
95
+ ### Deprecated
96
+
97
+ - `user` / `userId` on interactive delegation input: the authorization page
98
+ identifies the signed-in user, and the delegation service ignores them.
99
+ - `DelegateTokenResponse.grantedScopes`: the delegation service does not return
100
+ it. Read `qoni.genauth.introspectDelegationToken({ token }).data.scope`.
101
+ - `OpenLoginResult.confirm()`: `confirmSignedIn()` saves the login itself.
102
+
103
+ ## 0.8.0 - 2026-10-04
104
+
105
+ ### Added
106
+
107
+ - `qoni.genauth.delegateAgent(input)` returns the grant directly (`grant.token`),
108
+ or an interactive authorization request, through the existing signed endpoint.
109
+ - `agentKey` is an alias for `agent`. Existing root and `qoni.qoni` entries keep
110
+ their `QoniResponse` envelopes. Conflicting Agent labels are rejected;
111
+ the existing top-level `userId` precedence is unchanged.
112
+
113
+ ### Fixed
114
+
115
+ - Expiry strings such as `"15m"` are converted to integer seconds; valid
116
+ durations are 60–86400 seconds, matching the GenAuth DTO.
117
+ - Unsupported `deniedScopes` is rejected rather than forwarding an unenforced
118
+ deny policy. Legacy `mailbox.*`, `web.search`, and `web.extract` scopes are rejected without mapping to product scopes.
119
+
120
+ ### Changed
121
+
122
+ - Expiry validation now happens locally before signing. Invalid formats and
123
+ values outside 60–86400 seconds throw `QoniValidationError`; numeric seconds
124
+ strings are sent as integers.
125
+
3
126
  ## 0.7.0 - 2026-10-02
4
127
 
5
128
  ### Changed
@@ -127,7 +250,7 @@ compatible fixes.
127
250
  - Optional abort signals on product run-detail and artifact-list wire reads;
128
251
  existing calls without a signal remain compatible.
129
252
 
130
- ## Unreleased
253
+ ## Earlier changes
131
254
 
132
255
  ### Breaking
133
256
 
@@ -136,8 +259,8 @@ compatible fixes.
136
259
 
137
260
  ### Changed
138
261
 
139
- - The default Qoni host is now `https://dashboard.qoni.ai` (was
140
- `https://anima.eazo.ai/dashboard`). Callers that never passed an explicit `host`
262
+ - The default Qoni host is now `https://dashboard.qoni.ai`.
263
+ Callers that never passed an explicit `host`
141
264
  reach a different control plane after upgrading. Note that the new default has
142
265
  no `/dashboard` path segment — pass `host` explicitly for private or local
143
266
  deployments.
package/README.md CHANGED
@@ -151,7 +151,7 @@ const run = await qoni.doAnything.run({
151
151
 
152
152
  const result = await run.wait({
153
153
  onScreenshot: (img, i) => fs.writeFileSync(`step-${i}.jpg`, img.bytes),
154
- onInteraction: (i) => { // login / clarify / confirm / take-control
154
+ onInteraction: (i) => { // site_login / take_control / ask_user / confirmation
155
155
  if (i.can("confirm_signed_in")) return i.confirmSignedIn();
156
156
  if (i.can("confirm")) return i.confirm();
157
157
  },
@@ -251,7 +251,7 @@ Silent product calls receive `token: delegation.data.token`. If that token is an
251
251
 
252
252
  - **Silent** (`mode: "silent"`, the default) — acts for one end user, so a user
253
253
  is **required**: pass `user: { id: <genauthUserId> }` (preferred) or the
254
- deprecated top-level `userId`. Omitting it throws `QoniValidationError`
254
+ top-level `userId`. Omitting it throws `QoniValidationError`
255
255
  locally before any request. Authorize with the `products` sugar and/or
256
256
  explicit `scopes`.
257
257
  - **Interactive** (`mode: "interactive"`) — pass `redirectUri`, `state`,
@@ -286,18 +286,27 @@ const grant = await qoni.delegateToken({
286
286
  scopes: ["gumem.memory:read", "webagent.do_anything:manage"],
287
287
  });
288
288
 
289
+ // The callback will not carry grantId: save it with your business state, keyed by grantState.
290
+ await savePendingGrant(grant.data.grantState, {
291
+ grantId: grant.data.grantId,
292
+ businessState: "business-csrf-state",
293
+ });
289
294
  redirectUserTo(grant.data.authorizationUrl);
290
295
  ```
291
296
 
292
- The user visits `authorizationUrl` in Qoni Console/BFF. If they are not logged in, Console/BFF sends them through GenAuth login, confirms authorization, completes the grant internally, then redirects back to your `redirectUri` with a one-time `code`, your `state`, and `grant_state`/`grantId`. The browser never receives a delegation token.
297
+ The user visits `authorizationUrl` in Qoni Console/BFF. If they are not logged in, Console/BFF sends them through GenAuth login, confirms authorization, completes the grant internally, then redirects back to your `redirectUri` with a one-time `code`, your `state`, and `grant_state`. The callback does **not** carry `grantId`, so look up the record you saved by `grant_state`. The browser never receives a delegation token.
293
298
 
294
299
  Your callback handler runs on the server and exchanges the callback fields with AK/SK:
295
300
 
296
301
  ```ts
302
+ const pending = await loadPendingGrant(String(req.query.grant_state));
303
+ // Your business state comes back unchanged; reject forged or mixed-up callbacks.
304
+ if (!pending || req.query.state !== pending.businessState) throw new Error("Invalid delegation callback");
305
+
297
306
  const completed = await qoni.completeDelegateToken({
298
- grantId: String(req.query.grantId ?? req.query.grant_id),
307
+ grantId: pending.grantId,
299
308
  code: String(req.query.code),
300
- state: String(req.query.state),
309
+ state: String(req.query.grant_state), // grant_state, not your business state
301
310
  });
302
311
 
303
312
  await qoni.gumem.recall({
@@ -306,7 +315,35 @@ await qoni.gumem.recall({
306
315
  });
307
316
  ```
308
317
 
309
- `completeDelegateToken` is the recommended server callback helper. `delegateAgent` and `completeDelegateAgent` remain as deprecated compatibility names, but the current callback contract is `{ grantId, code, state }`; old `{ code, state }` completion is not supported.
318
+ `completeDelegateToken` is the recommended server callback helper. Root `qoni.delegateAgent` and `qoni.completeDelegateAgent` remain as deprecated compatibility names, but the current callback contract is `{ grantId, code, state }`; old `{ code, state }` completion is not supported.
319
+
320
+ ## Original delegation style (0.8.0+)
321
+
322
+ ```ts
323
+ const grant = await qoni.genauth.delegateAgent({
324
+ userId: process.env.QONI_USER_ID!, // The current user's GenAuth ID
325
+ agentKey: "email-assistant", // Agent audit label; alias for agent
326
+ scopes: [
327
+ "webagent.do_anything:read", // Read status, events, and artifacts
328
+ "webagent.do_anything:manage", // Start/cancel tasks and respond to interactions
329
+ ],
330
+ expiresIn: "15m", // Converted to 900 seconds before signing
331
+ });
332
+ console.log(grant.grantId, grant.auditId);
333
+ ```
334
+
335
+ This entry returns the grant directly; root `delegateToken` still returns
336
+ `QoniResponse`, with the token in `response.data.token`. Interactive mode returns
337
+ the authorization request directly; use `completeDelegateToken()` in the server
338
+ callback after consent. `agentKey` aliases `agent`; both labels must agree when
339
+ supplied together. Top-level `userId` retains precedence over `user.id`.
340
+ Expiry accepts integer seconds or whole-number
341
+ `s/m/h/d` durations, between 60 and 86400 seconds. These additions are available
342
+ in the published `@qoniai/qoni@0.8.0` release.
343
+
344
+ Legacy `mailbox.*`, `web.search`, and `web.extract` scopes, and `deniedScopes`, are unsupported and rejected.
345
+ They are never expanded into broader product permissions. Product scopes do not
346
+ prohibit email sending or deletion; a prompt is not an enforced deny policy.
310
347
 
311
348
  ## Choosing Scopes
312
349
 
@@ -320,7 +357,9 @@ await qoni.delegateToken({
320
357
  });
321
358
  ```
322
359
 
323
- Scope strings are validated locally before any request: a scope missing its service prefix (e.g. `do_anything:manage` instead of `webagent.do_anything:manage`) throws `QoniValidationError` immediately with the correct form in the message.
360
+ Scope strings are validated locally before any request: a scope missing its service prefix (e.g. `do_anything:manage` instead of `webagent.do_anything:manage`) throws `QoniValidationError` immediately with the correct form in the message — every candidate when the short form is ambiguous (`profile:read` → `gumem.profile:read` or `user.profile:read`).
361
+
362
+ The site-login, user Profile, payment and Agent mailbox scopes (`QoniScopes.SITE_LOGIN_*`, `USER_PROFILE_READ`, `USER_PROFILE_WRITE`, `USER_PAYMENT_USE`, `AGENT_MAIL_READ`, `AGENT_MAIL_SEND`) belong to no product: request them explicitly in `scopes`, and allow them in the AccessKey policy. The user Profile, payment and Agent mailbox constants only name the scopes; requesting them does not by itself enable Profile, payment or mailbox access in a run.
324
363
 
325
364
  Use explicit scope strings when the requested permission boundary should be visible in code.
326
365
 
@@ -332,6 +371,10 @@ Use explicit scope strings when the requested permission boundary should be visi
332
371
  | Search public web | `webagent.web_search:read`, `webagent.web_search:manage` | Agent can search public pages and read search results. |
333
372
  | Run a bounded web task | `webagent.do_anything:read`, `webagent.do_anything:manage` | Agent can run a visible web task, read progress, stop it, and intervene when needed. |
334
373
  | Create a monitor | `webagent.track:read`, `webagent.track:manage` | Agent can configure monitors, read events, run checks, and stop monitors. |
374
+ | Sign in to sites during a run | `webagent.site_login:request`, `webagent.site_login:confirm` | The user signs in through a live browser; the login is saved for later tasks. |
375
+ | Use the user's Profile | `user.profile:read`, `user.profile:write` | Agent can read the user's saved details (name, phone, shipping address, card brand and last 4) and save new ones the user provides. |
376
+ | Pay with the user's saved card | `user.payment:use` | After the user approves a payment, the saved card is filled in for them. |
377
+ | Use the Agent's own mailbox | `agent.mail:read`, `agent.mail:send` | Agent can read mail sent to its own address (such as verification codes) and send mail from it. |
335
378
 
336
379
  Silent mode is usually appropriate for low-risk actions such as reading current-user memory, writing a confirmed summary, or searching public web pages.
337
380
 
@@ -606,81 +649,113 @@ the backend contract.
606
649
 
607
650
  ### Interactions (HITL)
608
651
 
609
- When a run needs the user — sign in to a site, answer a clarification, approve a
610
- plan, take over the browser, or wait — it emits an `interaction` event whose
611
- `event.data` is a typed `Interaction` (discriminated by `type`: `site_login` /
612
- `clarification` / `confirmation` / `take_control` / `wait`). Wrap it in a handle
613
- and call the action methods; each posts to the endpoint the backend declared in
614
- the interaction's `actions[]`, so you never hard-code a route:
652
+ When a run needs the user, it pauses and emits an `interaction` event whose
653
+ `event.data` is a typed `Interaction`. Interactions are typed by what the user
654
+ has to do, never by business, so one handler covers every task:
615
655
 
616
- ```ts
617
- for await (const event of run.events()) {
618
- if (event.type === "interaction") {
619
- const i = run.interactionHandle(event.data); // event.data is the Interaction
620
- if (i.type === "clarification" && i.can("answer")) await i.answer("the blue button");
621
- else if (i.can("confirm")) await i.confirm();
622
- else if (i.can("confirm_signed_in")) await i.confirmSignedIn();
623
- else if (i.can("release_control")) await i.releaseControl();
624
- }
625
- }
626
- ```
627
-
628
- Methods: `answer(text)`, `skip()`, `confirm()`, `reject()`, `openLogin({ siteId?, profileId? })`,
629
- `confirmSignedIn()`, `connectControl()`, `refreshControl()`, `releaseControl()`,
630
- `retry()`, `switchProfile()`. `i.can(kind)` reports whether the interaction
631
- currently offers that action; calling a method the backend did NOT declare
632
- throws (so a "recovery" button only exists when it actually works).
656
+ | `type` | The user… | Methods |
657
+ | --- | --- | --- |
658
+ | `site_login` | signs in to a site in the live browser | `openLogin()`, `confirmSignedIn()` |
659
+ | `take_control` | operates the browser by hand (captcha, 2FA, …) | `connectControl()`, `refreshControl()`, `releaseControl()` |
660
+ | `ask_user` | answers a question shaped by `answerType` / `options` | `answer()`, `skip()` |
661
+ | `confirmation` | allows or rejects an action | `confirm()`, `reject()` |
662
+ | `wait` | nothing — the system is waiting (rate limit, external event) | `retry()` |
633
663
 
634
- `openLogin()` starts an interactive login session for a `site_login`
635
- interaction of a run. Pass `siteId` (one of
636
- `i.interaction.payload.sites[].siteId`), or omit it when exactly one site is
637
- listed. Show the returned `liveUrl` to the user; after they sign in, persist the
638
- login with `login.confirm()`, then tell the agent with `confirmSignedIn()`:
664
+ `Interaction` is a union distributed over `type`, so `switch (request.type)`
665
+ narrows `request.payload` in each branch. Each method posts to the endpoint the
666
+ backend declared in the interaction's `actions[]`, so you never hard-code a route:
639
667
 
640
668
  ```ts
641
- import { QoniError } from "@qoniai/qoni";
669
+ import type { InteractionHandle } from "@qoniai/qoni";
642
670
 
643
- const login = await i.openLogin(); // { profileId, siteId, sessionId, liveUrl }
644
- showToUser(login.liveUrl); // the user signs in there
645
- await waitForUserToFinish();
646
- let result;
647
- try {
648
- result = await login.confirm(); // ends the session; saves the login if found
649
- } catch (error) {
650
- // 409: no pending login for this profile (session already ended, or profile gone).
651
- if (!(error instanceof QoniError && error.code === "no_pending_login")) throw error;
652
- }
653
- if (!result || result.active || result.probeResult === "no_session") {
654
- await i.confirmSignedIn(); // the agent rechecks; it asks again if still signed out
655
- } else {
656
- // The user was not signed in, and confirm() closed that session's browser.
657
- const retry = await i.openLogin({ siteId: login.siteId, profileId: login.profileId });
658
- showToUser(retry.liveUrl); // a new session for the same profile
671
+ const handled = new Set<string>();
672
+
673
+ async function handleInteraction(handle: InteractionHandle) {
674
+ const request = handle.interaction;
675
+ // Creation, status updates and replays all call back: act once per pending id.
676
+ if (request.status !== "pending" || handled.has(request.id)) return;
677
+ handled.add(request.id);
678
+
679
+ switch (request.type) {
680
+ case "site_login": {
681
+ const login = await handle.openLogin(); // { liveUrl, ... } of a live login browser
682
+ await showToUser(login.liveUrl); // resolves when the user clicks "I'm signed in"
683
+ await handle.confirmSignedIn(); // saves the login, then the agent rechecks and continues
684
+ break;
685
+ }
686
+ case "take_control": {
687
+ // connectControl() resolves to the signed control URL, when the backend offers it
688
+ const control = handle.can("connect_control") ? await handle.connectControl() : undefined;
689
+ await showToUser(control?.liveUrl ?? request.payload.liveUrl);
690
+ await handle.releaseControl(); // hand the browser back
691
+ break;
692
+ }
693
+ case "ask_user": // payload: { question, answerType, options? }
694
+ await handle.answer(await askUser(request.payload));
695
+ break;
696
+ case "confirmation": // payload: { summary }
697
+ if (await approve(request.payload.summary)) await handle.confirm();
698
+ else await handle.reject();
699
+ break;
700
+ }
659
701
  }
660
- ```
661
702
 
662
- `probeResult: "no_session"` means the backend no longer holds the login session —
663
- it may already have saved the login on its own once it detected the sign-in, or
664
- it restarted. Some browser providers instead reject `confirm()` with a
665
- `QoniError` coded `no_pending_login` (HTTP 409), which the backend also uses when
666
- the profile no longer exists for this user; the example handles it the same way,
667
- because the agent rechecks and asks again if the site is still signed out. Delegate
703
+ const result = await run.wait({ onInteraction: handleInteraction });
704
+ ```
705
+
706
+ Interactions arrive one at a time: the run waits until the current one is
707
+ handled. The same interaction calls back again on every status change and on
708
+ reconnect replays, always with the same `id` and `type` — key your UI cards by
709
+ `request.id`. An update with content of its own replaces the card (a take-over
710
+ that became `active` carries its new `liveUrl`); when the backend settles an
711
+ interaction with an empty shell, the card keeps its original `payload`. A
712
+ `take_control` that arrives already `active` was started by the user, for
713
+ example from Console; show it if your UI offers control there. Methods: `answer(value)`, `skip()`, `confirm()`,
714
+ `reject()`, `openLogin({ siteId?, profileId? })`, `confirmSignedIn()`,
715
+ `connectControl()`, `refreshControl()`, `releaseControl()`, `retry()`,
716
+ `switchProfile()`. `connectControl()` and `refreshControl()` resolve to
717
+ `{ liveUrl, expiresAt?, status?, raw }`: the signed URL of the browser the user
718
+ now controls. `handle.can(kind)` reports whether the interaction currently
719
+ offers that action; calling a method the backend did NOT declare throws (so a
720
+ "recovery" button only exists when it actually works).
721
+
722
+ **`ask_user` answers are structured.** `answer()` takes the value matching
723
+ `payload.answerType` — `text` → string, `number` → number, `boolean` → boolean,
724
+ `single_choice` → one `options[].value`, `multiple_choice` → an array of option
725
+ values — and throws `QoniValidationError` before sending anything that does not
726
+ fit. The wire still calls this type `clarification`; the SDK presents it as
727
+ `ask_user` (choices arrive as `{ value, label }` options).
728
+
729
+ **Sign-in takes two calls.** `openLogin()` opens a live login browser for the
730
+ `site_login` interaction (pass `siteId` from `payload.sites[].siteId`, or omit it
731
+ when exactly one site is listed). Once the user is signed in, `confirmSignedIn()`
732
+ saves the login so later tasks reuse it, then tells the agent to recheck the site
733
+ and continue; if the user turns out not to be signed in, the agent raises a new
734
+ `site_login`. It saves once and rechecks once per login session, however many
735
+ times it is called. If the backend could not save the login yet, it throws a
736
+ retryable `QoniError` coded `site_login.save_failed` without telling the agent to
737
+ go on; call it again. The same holds for `site_login.session_replaced`, thrown
738
+ when the login was reopened while it was being saved. Handles from the same `RunHandle` (or `MonitorHandle`)
739
+ share the login session, so `confirmSignedIn()` also works on the handle of a
740
+ later callback; a RunHandle from a new `attach()` does not. Delegate
668
741
  `QoniScopes.SITE_LOGIN_REQUEST` and `QoniScopes.SITE_LOGIN_CONFIRM`, and allow
669
- both in the AccessKey policy.
670
-
671
- The discriminant strings used above are also exported as constants so you can
672
- avoid bare literals: `InteractionTypes` (`site_login` / `clarification` /
673
- `confirmation` / `take_control` / `wait`), `InteractionStatuses`, and
674
- `ActionKinds` (the `i.can(...)` action names). Authorization scope strings have
675
- the same treatment — `QoniScopes` (every individual scope), `QoniProductScopes`
676
- (the per-product `read`+`manage` pairs the `products` sugar expands to), and
677
- `QoniScopeBundles` (curated bundles). Use them in place of hand-typed strings:
742
+ both in the AccessKey policy. `openLogin()`'s result still offers `confirm()` for
743
+ callers that save the login themselves; it is deprecated.
744
+
745
+ The discriminant strings are also exported as constants so you can avoid bare
746
+ literals: `InteractionTypes` (`SiteLogin` / `TakeControl` / `AskUser` /
747
+ `Confirmation` / `Wait`; the deprecated `Clarification` now equals `AskUser`),
748
+ `InteractionStatuses`, and `ActionKinds` (the `handle.can(...)` action names).
749
+ Authorization scope strings have the same treatment — `QoniScopes` (every
750
+ individual scope), `QoniProductScopes` (the per-product `read`+`manage` pairs the
751
+ `products` sugar expands to), and `QoniScopeBundles` (curated bundles). Use them in
752
+ place of hand-typed strings:
678
753
 
679
754
  ```ts
680
755
  import { InteractionTypes, ActionKinds, QoniScopes } from "@qoniai/qoni";
681
756
 
682
- if (i.type === InteractionTypes.Clarification && i.can(ActionKinds.answer)) {
683
- await i.answer("the blue button");
757
+ if (handle.type === InteractionTypes.AskUser && handle.can(ActionKinds.answer)) {
758
+ await handle.answer("the blue button");
684
759
  }
685
760
  ```
686
761
 
@@ -690,7 +765,7 @@ The 1:1 low-level methods live under `qoni.<product>.api.*` (e.g. `qoni.doAnythi
690
765
 
691
766
  `event.raw.event` is the **raw wire event type** — an internal transport name (`run.action.started`, `run.section_completed`, …) whose naming is historical and inconsistent, exposed as a plain string. This is an implementation detail, not the product API: developers match on the curated `event.type` above. The full wire-type catalog is intentionally NOT exported — reach for `event.raw.event` (a string) only when you need an exact wire match the curated layer folds into `progress` (e.g. telling a specific browser action apart).
692
767
 
693
- Browser Use scopes are reserved until their product runtime SDK methods are exported. Site Login scopes (`QoniScopes.SITE_LOGIN_REQUEST`, `QoniScopes.SITE_LOGIN_CONFIRM`) back `InteractionHandle.openLogin()` and `confirm()` on run interactions.
768
+ Browser Use scopes are reserved until their product runtime SDK methods are exported. Site Login scopes (`QoniScopes.SITE_LOGIN_REQUEST`, `QoniScopes.SITE_LOGIN_CONFIRM`) back `InteractionHandle.openLogin()` and `confirmSignedIn()` on run interactions.
694
769
 
695
770
  ### Response Shape
696
771