@trusty-squire/mcp 1.1.16-rc.4 → 1.1.16-rc.6

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.
Files changed (110) hide show
  1. package/README.md +9 -3
  2. package/dist/bot/act/act.d.ts +16 -1
  3. package/dist/bot/act/act.d.ts.map +1 -1
  4. package/dist/bot/act/act.js +308 -25
  5. package/dist/bot/act/act.js.map +1 -1
  6. package/dist/bot/act/identity.d.ts +64 -0
  7. package/dist/bot/act/identity.d.ts.map +1 -0
  8. package/dist/bot/act/identity.js +198 -0
  9. package/dist/bot/act/identity.js.map +1 -0
  10. package/dist/bot/act/targets.d.ts +4 -0
  11. package/dist/bot/act/targets.d.ts.map +1 -1
  12. package/dist/bot/act/targets.js +6 -0
  13. package/dist/bot/act/targets.js.map +1 -1
  14. package/dist/bot/broker/admission-context.d.ts +10 -0
  15. package/dist/bot/broker/admission-context.d.ts.map +1 -1
  16. package/dist/bot/broker/admission-context.js +4 -0
  17. package/dist/bot/broker/admission-context.js.map +1 -1
  18. package/dist/bot/broker/authority.d.ts +13 -10
  19. package/dist/bot/broker/authority.d.ts.map +1 -1
  20. package/dist/bot/broker/authority.js +8 -18
  21. package/dist/bot/broker/authority.js.map +1 -1
  22. package/dist/bot/broker/custody.d.ts +3 -1
  23. package/dist/bot/broker/custody.d.ts.map +1 -1
  24. package/dist/bot/broker/custody.js.map +1 -1
  25. package/dist/bot/broker/daemon.d.ts +9 -19
  26. package/dist/bot/broker/daemon.d.ts.map +1 -1
  27. package/dist/bot/broker/daemon.js +16 -47
  28. package/dist/bot/broker/daemon.js.map +1 -1
  29. package/dist/bot/broker/discovery.d.ts +25 -24
  30. package/dist/bot/broker/discovery.d.ts.map +1 -1
  31. package/dist/bot/broker/discovery.js +31 -61
  32. package/dist/bot/broker/discovery.js.map +1 -1
  33. package/dist/bot/broker/forwarder.d.ts +7 -0
  34. package/dist/bot/broker/forwarder.d.ts.map +1 -1
  35. package/dist/bot/broker/forwarder.js +41 -5
  36. package/dist/bot/broker/forwarder.js.map +1 -1
  37. package/dist/bot/broker/operator.d.ts +18 -11
  38. package/dist/bot/broker/operator.d.ts.map +1 -1
  39. package/dist/bot/broker/operator.js +69 -35
  40. package/dist/bot/broker/operator.js.map +1 -1
  41. package/dist/bot/broker/protocol.d.ts +29 -3
  42. package/dist/bot/broker/protocol.d.ts.map +1 -1
  43. package/dist/bot/broker/runtime.d.ts +14 -4
  44. package/dist/bot/broker/runtime.d.ts.map +1 -1
  45. package/dist/bot/broker/runtime.js +32 -17
  46. package/dist/bot/broker/runtime.js.map +1 -1
  47. package/dist/bot/broker/transport.d.ts +1 -2
  48. package/dist/bot/broker/transport.d.ts.map +1 -1
  49. package/dist/bot/broker/transport.js +7 -9
  50. package/dist/bot/broker/transport.js.map +1 -1
  51. package/dist/bot/browser-process-owner.d.ts.map +1 -1
  52. package/dist/bot/browser-process-owner.js +9 -6
  53. package/dist/bot/browser-process-owner.js.map +1 -1
  54. package/dist/bot/browser-process-runtime.d.ts +0 -1
  55. package/dist/bot/browser-process-runtime.d.ts.map +1 -1
  56. package/dist/bot/browser-process-runtime.js +7 -5
  57. package/dist/bot/browser-process-runtime.js.map +1 -1
  58. package/dist/bot/browser.d.ts +17 -1
  59. package/dist/bot/browser.d.ts.map +1 -1
  60. package/dist/bot/browser.js +73 -15
  61. package/dist/bot/browser.js.map +1 -1
  62. package/dist/bot/capture/verification.d.ts +23 -0
  63. package/dist/bot/capture/verification.d.ts.map +1 -1
  64. package/dist/bot/capture/verification.js +54 -17
  65. package/dist/bot/capture/verification.js.map +1 -1
  66. package/dist/bot/compact-observation-v2.d.ts.map +1 -1
  67. package/dist/bot/compact-observation-v2.js +10 -7
  68. package/dist/bot/compact-observation-v2.js.map +1 -1
  69. package/dist/bot/drive-act.d.ts +32 -23
  70. package/dist/bot/drive-act.d.ts.map +1 -1
  71. package/dist/bot/drive-act.js +163 -518
  72. package/dist/bot/drive-act.js.map +1 -1
  73. package/dist/bot/drive-snapshot.d.ts +8 -0
  74. package/dist/bot/drive-snapshot.d.ts.map +1 -1
  75. package/dist/bot/drive-snapshot.js +127 -15
  76. package/dist/bot/drive-snapshot.js.map +1 -1
  77. package/dist/bot/google-login.d.ts +14 -54
  78. package/dist/bot/google-login.d.ts.map +1 -1
  79. package/dist/bot/google-login.js +46 -382
  80. package/dist/bot/google-login.js.map +1 -1
  81. package/dist/bot/operate-drive.d.ts +6 -4
  82. package/dist/bot/operate-drive.d.ts.map +1 -1
  83. package/dist/bot/operate-drive.js +223 -146
  84. package/dist/bot/operate-drive.js.map +1 -1
  85. package/dist/bot/remote-login-display.d.ts +5 -1
  86. package/dist/bot/remote-login-display.d.ts.map +1 -1
  87. package/dist/bot/remote-login-display.js +29 -1
  88. package/dist/bot/remote-login-display.js.map +1 -1
  89. package/dist/bot/session/lifecycle.d.ts.map +1 -1
  90. package/dist/bot/session/lifecycle.js +2 -1
  91. package/dist/bot/session/lifecycle.js.map +1 -1
  92. package/dist/bot/session/model.d.ts +15 -6
  93. package/dist/bot/session/model.d.ts.map +1 -1
  94. package/dist/bot/session/model.js.map +1 -1
  95. package/dist/browser-busy.d.ts +3 -0
  96. package/dist/browser-busy.d.ts.map +1 -1
  97. package/dist/browser-busy.js +5 -15
  98. package/dist/browser-busy.js.map +1 -1
  99. package/dist/install/cli.d.ts.map +1 -1
  100. package/dist/install/cli.js +11 -5
  101. package/dist/install/cli.js.map +1 -1
  102. package/dist/pairing-ttl.d.ts +4 -0
  103. package/dist/pairing-ttl.d.ts.map +1 -0
  104. package/dist/pairing-ttl.js +14 -0
  105. package/dist/pairing-ttl.js.map +1 -0
  106. package/dist/tools/provision-drive.d.ts +2 -2
  107. package/dist/tools/provision-drive.d.ts.map +1 -1
  108. package/dist/tools/provision-drive.js +4 -0
  109. package/dist/tools/provision-drive.js.map +1 -1
  110. package/package.json +1 -1
package/README.md CHANGED
@@ -85,9 +85,9 @@ operator does not add a broad secret scanner or claim to defeat that page.
85
85
  npx @trusty-squire/mcp connect
86
86
  ```
87
87
 
88
- `connect` signs you in with Google or GitHub, detects your coding agent, and merges the `squire` MCP server into its existing configuration. On an enrolled machine, sign-in opens a tab in Trusty Squire's shared browser. When that browser is on the machine's own screen and you are running `connect` there, you finish sign-in in the window in front of you — no URL and no virtual display. Otherwise — the shared browser started without a screen and sits on a private Linux display, or you are running `connect` over SSH or a TTY with no screen of your own — the ceremony prints a single-use noVNC URL; open that URL on another device. The URL shows and controls the whole display that browser runs on until the ceremony ends: other sessions' tabs, and on a machine with its own screen, everything else on that screen. macOS and Windows draw their windows natively, so the shared browser's tab is on the screen in front of you there and nothing is exposed over noVNC. If the display cannot be shown, connect stops with an explanation instead of waiting indefinitely. On a first connect where no broker can serve, connect launches its own sign-in browser, using a visible desktop when one still answers or a login-scoped Xvfb + noVNC stack on headless Linux (including SSH/TTY sessions with an inherited virtual display, and a display that has stopped answering since startup).
88
+ `connect` signs you in with Google or GitHub, detects your coding agent, and merges the `squire` MCP server into its existing configuration. Sign-in opens a tab in Trusty Squire's shared browser — the broker's browser, which is the only browser this product launches, on a first connect as much as on an enrolled machine. When that browser is on the machine's own screen and you are running `connect` there, you finish sign-in in the window in front of you — no URL and no virtual display. Otherwise — the shared browser started without a screen and sits on a private Linux display, or you are running `connect` over SSH or a TTY with no screen of your own — the ceremony prints a single-use noVNC URL; open that URL on another device. The URL shows and controls the whole display that browser runs on until the ceremony ends: other sessions' tabs, and on a machine with its own screen, everything else on that screen. macOS and Windows draw their windows natively, so the shared browser's tab is on the screen in front of you there and nothing is exposed over noVNC. If the display cannot be shown, connect stops with an explanation instead of waiting indefinitely. Nothing falls back to a browser of its own.
89
89
 
90
- Ceremony completion, timeout, failure, or interruption tears down its quick tunnel and local exposure helpers. The shared browser and its display remain with the broker; a self-launched ceremony also closes its own browser and display. Operators may set both `TS_LOGIN_PUBLIC_HOSTNAME` and `TS_LOGIN_LOCAL_PORT` to reuse an externally managed named tunnel, which Trusty Squire never creates or stops. If that fixed local port is busy, login reports it and uses a one-off quick tunnel instead. Restart the agent and ask for the finished website outcome. Trusty Squire is free to start.
90
+ Ceremony completion, timeout, failure, or interruption tears down its quick tunnel and local exposure helpers. Those helpers also hold a bound of their own — the sign-in link's lifetime plus a short grace, counted from the moment they are stood up — so a `connect` that is abandoned, wedged, or never waits still tears the display down and exits non-zero instead of leaving a tunnel published. The shared browser and its display remain with the broker. Operators may set both `TS_LOGIN_PUBLIC_HOSTNAME` and `TS_LOGIN_LOCAL_PORT` to reuse an externally managed named tunnel, which Trusty Squire never creates or stops. If that fixed local port is busy, login reports it and uses a one-off quick tunnel instead. Restart the agent and ask for the finished website outcome. Trusty Squire is free to start.
91
91
 
92
92
  `connect` is also the only way to sign in again: `--force-relogin` switches the bound account and `--force-relogin=google` or `--force-relogin=github` refreshes one provider session. It re-checks provider sessions before reporting success; when the shared browser keeps the profile busy, that check uses committed cookies, which can outlive a server-side session. Use `--force-relogin` to refresh a stale session. The detailed [provider-probe and ceremony contract](docs/browser-broker.md) describes completion and failure handling.
93
93
 
@@ -127,7 +127,7 @@ Each report carries seven fields:
127
127
  - `browser_location` — where the ceremony browser actually opened, reported by the code that placed it: `{kind:"host_screen",display?}` (the screen you are at), `{kind:"virtual",url}` (a display nobody is sitting at — `url` is the noVNC address that reaches it, reported on a non-terminal line the moment the browser lands there, while the tunnel is up), `{kind:"unreachable",reason}` (nothing here could be shown the page — also the answer when a ceremony ran but no path ever reported a placement, with `reason` naming the failure), `{kind:"none"}` (no browser was opened), or `{kind:"unknown",reason}` (a browser opened that Squire did not place — `--skip-browser` hands the link to the machine's default browser). Squire decides this; callers do not detect screens.
128
128
  - `reason` — only what the five fields above cannot say: `provider_session_missing`, `requested_provider_missing`, `account_mismatch`, `profile_unverifiable`, `install_expired`, `cached_cookie_evidence`, `run_failed`. `null` otherwise.
129
129
 
130
- Connect never waits longer than the sign-in link lives — the wait is the link's lifetime counted from the moment connect received it, so a skewed machine clock cannot shorten or overrun it. A `sign_in_url` it hands back is still open; a wait that reached that deadline, or a link the server reported lapsed, is `no-browser` with `reason: "install_expired"` and no URL at all.
130
+ Connect never waits longer than the sign-in link lives — the wait is the link's lifetime counted from the moment connect received it, so a skewed machine clock cannot shorten or overrun it. A `sign_in_url` it hands back is still open; a wait that reached that deadline, or a link the server reported lapsed, is `no-browser` with `reason: "install_expired"` and no URL at all. A ceremony display that outlives its own bound answers the same way, on a terminal line written before connect exits — so a run that wedged before it ever reached that wait still ends the machine channel.
131
131
 
132
132
  Supported targets: `claude-code`, `cursor`, `codex`, `opencode`, `goose`, `cline`, `continue`, and `hermes`.
133
133
 
@@ -190,6 +190,12 @@ grant; host scoping, auditing, and revocation still apply. When a grant reaches
190
190
  an explicit hourly limit, the proxy returns `429 rate_limited` with
191
191
  `scope: "grant"`, `Retry-After`, and window/reset metadata.
192
192
 
193
+ The proxy passes the provider's response through as it arrives and forwards the
194
+ upstream response headers (cookies, hop-by-hop headers, and the upstream
195
+ content-length removed), so a streaming completion — `stream: true`,
196
+ `text/event-stream` — reaches the app event by event rather than in one block
197
+ once generation has finished. The body is not buffered and carries no size cap.
198
+
193
199
  The result contains a host-scoped egress `base_url` and a `token`, not the Clerk secret key. The token is returned once through the MCP result and remains valid until revoked. That means the scoped grant token can enter agent context; it is not the provider key. Move it directly into backend-only deployment secret storage, never browser code, logs, or source control. If you need zero grant-token exposure to the model, use `use_credential` for agent-initiated requests instead. Trusty Squire removes the grant authorization at the boundary and injects the vaulted provider credential into the upstream request.
194
200
 
195
201
  ## Security and threat model
@@ -5,6 +5,19 @@ import { type ObserveDetail } from "../observe/observe.js";
5
5
  import { type CompactV2TargetAuthorization } from "./targets.js";
6
6
  import type { Observation, ProvisionAction } from "../provision-session.js";
7
7
  import type { Session } from "../session/model.js";
8
+ export type ActExecutorOptions = {
9
+ drive?: boolean;
10
+ };
11
+ export type DriveActResult = {
12
+ kind: "ok";
13
+ combobox: boolean;
14
+ needsUser?: NonNullable<Observation["needs_user"]>;
15
+ } | {
16
+ kind: "stale";
17
+ reason: string;
18
+ } | {
19
+ kind: "unsupported";
20
+ };
8
21
  export declare function runSerializedGoogleIdentityOperation<T>(session: Session, operation: (browser: BrowserController) => Promise<T>, options?: {
9
22
  deadline?: OAuthActionDeadline;
10
23
  }): Promise<{
@@ -17,9 +30,11 @@ export interface InternalActResult {
17
30
  outcome: {
18
31
  selectedOption?: string;
19
32
  };
33
+ combobox?: boolean;
20
34
  }
21
- export declare function actInternally(sessionId: string, action: ProvisionAction, detail?: ObserveDetail, compactV2Authorization?: CompactV2TargetAuthorization, operationPage?: Page): Promise<InternalActResult>;
35
+ export declare function actInternally(sessionId: string, action: ProvisionAction, detail?: ObserveDetail, compactV2Authorization?: CompactV2TargetAuthorization, operationPage?: Page, options?: ActExecutorOptions): Promise<InternalActResult>;
22
36
  export declare function act(sessionId: string, action: ProvisionAction, detail?: ObserveDetail, outputFormat?: "compact" | "full", compactMapEmitted?: boolean): Promise<Observation>;
37
+ export declare function dispatchDriveAct(sessionId: string, action: ProvisionAction): Promise<DriveActResult>;
23
38
  export declare function compactV2SelectionFailureReason(error: unknown): string;
24
39
  export declare function settleAfterDriveAction(page?: Page, combobox?: boolean): Promise<void>;
25
40
  export declare function settleAfterStateChange(browser: BrowserController, page?: Page, options?: {
@@ -1 +1 @@
1
- {"version":3,"file":"act.d.ts","sourceRoot":"","sources":["../../../src/bot/act/act.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAGL,KAAK,iBAAiB,EAEvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAWL,KAAK,mBAAmB,EAIzB,MAAM,mBAAmB,CAAC;AAY3B,OAAO,EAOL,KAAK,aAAa,EAMnB,MAAM,uBAAuB,CAAC;AAU/B,OAAO,EAiBL,KAAK,4BAA4B,EAClC,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AA4EnD,wBAAsB,oCAAoC,CAAC,CAAC,EAC1D,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,EACrD,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAAO,GAC/C,OAAO,CAAC;IAAE,OAAO,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,CAOpD;AA+GD,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,WAAW,CAAC;IACzB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,OAAO,EAAE;QACP,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAqED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,EACvB,MAAM,GAAE,aAAyB,EACjC,sBAAsB,CAAC,EAAE,4BAA4B,EACrD,aAAa,CAAC,EAAE,IAAI,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAgD5B;AAED,wBAAsB,GAAG,CACvB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,EACvB,MAAM,GAAE,aAAyB,EACjC,YAAY,GAAE,SAAS,GAAG,MAAe,EACzC,iBAAiB,UAAO,GACvB,OAAO,CAAC,WAAW,CAAC,CAwGtB;AAsjBD,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtE;AA+FD,wBAAsB,sBAAsB,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCzF;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,iBAAiB,EAC1B,IAAI,CAAC,EAAE,IAAI,EACX,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAChD,OAAO,CAAC,IAAI,CAAC,CAUf"}
1
+ {"version":3,"file":"act.d.ts","sourceRoot":"","sources":["../../../src/bot/act/act.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAS,IAAI,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAIL,KAAK,iBAAiB,EAEvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAWL,KAAK,mBAAmB,EAIzB,MAAM,mBAAmB,CAAC;AAY3B,OAAO,EAOL,KAAK,aAAa,EAMnB,MAAM,uBAAuB,CAAC;AAU/B,OAAO,EAiBL,KAAK,4BAA4B,EAClC,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAuBnD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAIF,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,WAAW,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAAA;CAAE,GACrF;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC;AA4E5B,wBAAsB,oCAAoC,CAAC,CAAC,EAC1D,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,EACrD,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAAO,GAC/C,OAAO,CAAC;IAAE,OAAO,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,CAOpD;AAqHD,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,WAAW,CAAC;IACzB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,OAAO,EAAE;QACP,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAqED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,EACvB,MAAM,GAAE,aAAyB,EACjC,sBAAsB,CAAC,EAAE,4BAA4B,EACrD,aAAa,CAAC,EAAE,IAAI,EACpB,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CA4D5B;AAED,wBAAsB,GAAG,CACvB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,EACvB,MAAM,GAAE,aAAyB,EACjC,YAAY,GAAE,SAAS,GAAG,MAAe,EACzC,iBAAiB,UAAO,GACvB,OAAO,CAAC,WAAW,CAAC,CAwGtB;AAyxBD,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,cAAc,CAAC,CAyEzB;AAED,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtE;AA+FD,wBAAsB,sBAAsB,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCzF;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,iBAAiB,EAC1B,IAAI,CAAC,EAAE,IAAI,EACX,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAChD,OAAO,CAAC,IAAI,CAAC,CAUf"}
@@ -1,4 +1,4 @@
1
- import { BrowserClickDispatchError, clickDispatchStatusForError, } from "../browser.js";
1
+ import { BrowserClickDispatchError, DRIVE_DISPATCH_PACING, clickDispatchStatusForError, } from "../browser.js";
2
2
  import { loginWithOAuth, oauthActionDeadline, oauthActionRemainingMs, oauthAutomatedActionTimeoutMs, oauthHumanHandoffTimeoutMs, oauthLoginLeaseCooldownMs, resetOAuthActionDeadline, settleAfterOAuth, withOAuthActionLease, withinOAuthActionDeadline, OAuthAwaitingHumanError, OAuthFailedError, OAuthOnboardingRequiredError, } from "../oauth-login.js";
3
3
  import { compactV2AuditUrl, safeStageV2 } from "../compact-observation-v2.js";
4
4
  import { ProvenPreDispatchMutationError } from "../mutation-dispatch-evidence.js";
@@ -11,6 +11,10 @@ import { substituteCardTokens } from "../card-secret-tokens.js";
11
11
  import { clickScreenshot, ScreenshotClickError } from "../screenshot-click.js";
12
12
  import { composeOperatorSignals, currentOperatorRequestSignal, operatorMutationDispatchPhase, } from "../request-cancellation.js";
13
13
  import { AmbiguousProvisionTargetError, compactV2AuthorizationForTarget, frameTargetFor, isRequiredShippingAddressLine1, parseLocatorTarget, preparedOAuthLoginTarget, resolveAuthorizedCompactV2Target, resolveTarget, staleTargetError, TargetStaleError, throwCompactV2StaleRef, CompactV2ActionFailureError, CompactV2StaleRefError, CompactV2UnresolvedLabelError, ProvisionTargetMissingError, ProvisionTargetNotAllowedError, } from "./targets.js";
14
+ import { resolveIdentityScope, resolveLiveControlIdentity, } from "./identity.js";
15
+ import { evaluateBound } from "../drive-evaluate.js";
16
+ import { clickCrossOriginFrameTarget, commitDriveListOption, selectDriveOption, overlayOptionLabels, waitForOpenedOverlay, waitForOverlayOptionsToChange, } from "../drive-act.js";
17
+ const DRIVE_DISPATCH = { drive: true };
14
18
  async function withOAuthActionBoundary(session, provider, run, outputFormat) {
15
19
  if (provider === "google") {
16
20
  const gate = await googleSessionGateForSession(session.id);
@@ -73,10 +77,15 @@ export async function runSerializedGoogleIdentityOperation(session, operation, o
73
77
  : await withinOAuthActionDeadline(operation(browser), options.deadline);
74
78
  return { browser, result };
75
79
  }
76
- async function runSerializedOAuthBoundary(session, authorizedElement, authorizedElements, provider, deadline, compactAuthorization, bindPreparedTargetAtDispatch = false) {
77
- const authorizedRef = provisionElementRefs(authorizedElements).get(authorizedElement);
78
- if (authorizedRef === undefined) {
79
- throw new Error("OAuth action target was not present in the authorized action map");
80
+ async function runSerializedOAuthBoundary(session, authorizedElement, authorizedElements, provider, deadline, compactAuthorization, bindPreparedTargetAtDispatch = false, driveResolved = false) {
81
+ // A drive ref is authorized by its own in-page identity record (the caller
82
+ // already re-proved the node is still the observed control), not by the
83
+ // tools' action map. Only the tools path needs the map-membership proof.
84
+ if (!driveResolved) {
85
+ const authorizedRef = provisionElementRefs(authorizedElements).get(authorizedElement);
86
+ if (authorizedRef === undefined) {
87
+ throw new Error("OAuth action target was not present in the authorized action map");
88
+ }
80
89
  }
81
90
  const expectedGoogleAccountEmail = session.userEmail ?? undefined;
82
91
  const completed = await runSerializedGoogleIdentityOperation(session, async (browser) => {
@@ -206,13 +215,18 @@ function oauthOnboardingRequiredObservation(session, error) {
206
215
  };
207
216
  return compactV2PublicObservation(session, { stage: "form", guidance, oauth, url });
208
217
  }
209
- export async function actInternally(sessionId, action, detail = "compact", compactV2Authorization, operationPage) {
218
+ export async function actInternally(sessionId, action, detail = "compact", compactV2Authorization, operationPage, options) {
210
219
  const session = sessionForCall(sessionId);
211
220
  const capturedOperationPage = operationPage ?? (session === undefined ? undefined : operationPageForSession(session));
212
221
  const oauthProvider = action.kind === "oauth_login" || action.kind === "oauth_click" ? action.provider : undefined;
213
222
  try {
214
223
  const execute = async (deadline) => {
215
- const run = async () => await executeAct(sessionId, action, detail, true, compactV2Authorization, deadline, capturedOperationPage, false);
224
+ const run = async () => {
225
+ const act = async () => await executeAct(sessionId, action, detail, true, compactV2Authorization, deadline, capturedOperationPage, false, undefined, "full", true, options);
226
+ return session === undefined || options?.drive !== true
227
+ ? await act()
228
+ : await session.browser.withDispatchPacing(DRIVE_DISPATCH_PACING, act);
229
+ };
216
230
  return (action.kind === "click" ||
217
231
  action.kind === "js_click" ||
218
232
  action.kind === "oauth_login" ||
@@ -240,7 +254,7 @@ export async function actInternally(sessionId, action, detail = "compact", compa
240
254
  return { observation: oauthOnboardingRequiredObservation(session, error), outcome: {} };
241
255
  }
242
256
  if (session?.compactV2Active === true && !(error instanceof ProvisionTargetMissingError)) {
243
- throw new CompactV2ActionFailureError(compactV2ActionFailureReason(error, action.kind));
257
+ throw new CompactV2ActionFailureError(compactV2ActionFailureReason(error, action.kind), clickDispatchStatusForError(error));
244
258
  }
245
259
  throw error;
246
260
  }
@@ -341,10 +355,90 @@ function actDriverTarget(el) {
341
355
  return { kind: "frame", frame, selector: el.selector };
342
356
  return { kind: "selector", selector: el.selector };
343
357
  }
358
+ function actsThroughOverlay(el, picker) {
359
+ if (picker)
360
+ return true;
361
+ const role = (el.role ?? "").toLowerCase();
362
+ const type = (el.type ?? "").toLowerCase();
363
+ return (role === "combobox" ||
364
+ role === "searchbox" ||
365
+ type === "search" ||
366
+ type === "date" ||
367
+ type === "datetime-local" ||
368
+ type === "month");
369
+ }
370
+ function submitsOnEnter(el, ariaLabel) {
371
+ const role = (el.role ?? "").toLowerCase();
372
+ const type = (el.type ?? "").toLowerCase();
373
+ return (role === "searchbox" ||
374
+ type === "search" ||
375
+ el.name === "q" ||
376
+ /search/i.test(`${ariaLabel} ${el.placeholder ?? ""}`));
377
+ }
378
+ function scopeForElement(page, el) {
379
+ if (el.framePath === undefined || el.framePath === null || el.framePath.length === 0)
380
+ return page;
381
+ let frame = page.mainFrame();
382
+ for (const part of el.framePath.split("/")) {
383
+ const child = frame.childFrames()[Number(part)];
384
+ if (child === undefined)
385
+ return page;
386
+ frame = child;
387
+ }
388
+ return frame;
389
+ }
390
+ // The occlusion guard the drive used to carry in its own dispatch: a cookie
391
+ // banner or sticky footer over the target swallows the click while the page
392
+ // still reports a dispatch, so the caller records a step that never landed.
393
+ // Absent a box (still loading) it says nothing and the actionability waits rule.
394
+ async function clickTargetOccluded(scope, selector) {
395
+ return await evaluateBound(scope, (sel) => {
396
+ const element = document.querySelector(sel);
397
+ if (element === null)
398
+ return false;
399
+ // The snapshot keeps offscreen fillables so the model can name them, and
400
+ // the dispatch scrolls before clicking — so measure where the click will
401
+ // actually land, not where the element sits right now. "instant" is
402
+ // load-bearing: a page with scroll-behavior:smooth would animate and the
403
+ // rect below would still be the pre-scroll one.
404
+ const before = element.getBoundingClientRect();
405
+ const inView = before.width > 0 &&
406
+ before.height > 0 &&
407
+ before.bottom > 0 &&
408
+ before.top < innerHeight &&
409
+ before.right > 0 &&
410
+ before.left < innerWidth;
411
+ if (!inView) {
412
+ element.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
413
+ }
414
+ const rect = element.getBoundingClientRect();
415
+ if (rect.width <= 0 || rect.height <= 0)
416
+ return false;
417
+ const hit = document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2);
418
+ return hit !== null && hit !== element && !element.contains(hit) && !hit.contains(element);
419
+ }, selector).catch(() => false);
420
+ }
344
421
  // Re-resolve against FRESH elements — never trust a stale index. Shared by the
345
422
  // type_secret / select / click-group ref paths; `internalLabel`/`noMatchPrefix`
346
423
  // keep each caller's error wording, `withVisibleCandidates` its candidate list.
347
424
  async function resolveFreshActTarget(session, browser, compactV2ActionPage, compactV2Authorization, resolutionTarget, internalAccess, internalLabel, noMatchPrefix, actionTarget, withVisibleCandidates) {
425
+ const driveIdentity = session.drive?.identities?.get(resolutionTarget);
426
+ const livePage = compactV2ActionPage ?? browser.page;
427
+ if (driveIdentity !== undefined && livePage !== null) {
428
+ const live = await resolveLiveControlIdentity(livePage, resolutionTarget, driveIdentity);
429
+ if (live !== null) {
430
+ return { el: live, fresh: session.lastElements, driveIdentity };
431
+ }
432
+ if (session.compactV2Active) {
433
+ if (!internalAccess)
434
+ throwCompactV2StaleRef();
435
+ throw new CompactV2StaleRefError("stale_ref");
436
+ }
437
+ const stale = staleTargetError(session, actionTarget, session.lastElements);
438
+ if (stale !== null)
439
+ throw stale;
440
+ throw new CompactV2StaleRefError("stale_ref");
441
+ }
348
442
  const fresh = (await browser.extractBrowserUseObservation(compactV2ActionPage)).elements;
349
443
  retainSessionElements(session, fresh);
350
444
  // resolveTarget recomputes identities (incl. volatile positional-group
@@ -354,7 +448,7 @@ async function resolveFreshActTarget(session, browser, compactV2ActionPage, comp
354
448
  ? resolveTarget(fresh, resolutionTarget)
355
449
  : resolveAuthorizedCompactV2Target(session, fresh, compactV2Authorization);
356
450
  if (el !== null)
357
- return { el, fresh };
451
+ return { el, fresh, driveIdentity: undefined };
358
452
  if (session.compactV2Active) {
359
453
  if (!internalAccess)
360
454
  throwCompactV2StaleRef();
@@ -373,15 +467,18 @@ async function resolveFreshActTarget(session, browser, compactV2ActionPage, comp
373
467
  : "";
374
468
  throw new Error(`${prefix}no element matched target "${actionTarget}".${visible}`);
375
469
  }
376
- async function executeAct(sessionId, action, detail, internalAccess, internalAuthorization, oauthDeadline, operationPage, preparedOAuthDispatch = false, onScreenshotDispatched, outputFormat = "full", compactMapEmitted = true) {
470
+ async function executeAct(sessionId, action, detail, internalAccess, internalAuthorization, oauthDeadline, operationPage, preparedOAuthDispatch = false, onScreenshotDispatched, outputFormat = "full", compactMapEmitted = true, options) {
377
471
  const session = sessionForCall(sessionId);
378
472
  if (session === undefined)
379
473
  throw new Error(`unknown provision session ${sessionId}`);
380
474
  let browser = session.browser;
381
475
  const compactV2ActionPage = operationPage ?? operationPageForSession(session);
382
476
  const driveSettle = session.drive !== null && action.kind !== "oauth_login" && action.kind !== "oauth_click";
477
+ const skipToolSettle = options?.drive === true;
383
478
  let settleMs = 0;
384
479
  const settle = async (combobox = false) => {
480
+ if (skipToolSettle)
481
+ return;
385
482
  const started = Date.now();
386
483
  await settleAfterStateChange(browser, compactV2ActionPage, {
387
484
  drive: driveSettle,
@@ -390,6 +487,7 @@ async function executeAct(sessionId, action, detail, internalAccess, internalAut
390
487
  settleMs += Date.now() - started;
391
488
  };
392
489
  const actStarted = Date.now();
490
+ let actedCombobox = false;
393
491
  let actionPageAfter = compactV2ActionPage;
394
492
  let completedAction = action;
395
493
  let resolutionTarget;
@@ -529,7 +627,10 @@ async function executeAct(sessionId, action, detail, internalAccess, internalAut
529
627
  }
530
628
  case "select": {
531
629
  const el = (await resolveFreshActTarget(session, browser, compactV2ActionPage, compactV2Authorization, resolutionTarget, internalAccess, "select", "select", action.target, true)).el;
532
- const committedText = await browser.select(actDriverTarget(el), action.text, compactV2ActionPage);
630
+ const driveSelectPage = compactV2ActionPage ?? browser.page;
631
+ const committedText = options?.drive === true && driveSelectPage !== null && driveSelectPage !== undefined
632
+ ? await selectDriveOptionOrRefuse(scopeForElement(driveSelectPage, el), el.selector, action.text ?? "")
633
+ : await browser.select(actDriverTarget(el), action.text, compactV2ActionPage);
533
634
  session.committedSelectValues.set(compactV2CommittedSelectKey(session, el.selector), compactV2CommittedSelectValue(session, committedText));
534
635
  completedAction = { ...action, text: committedText };
535
636
  await settle();
@@ -629,18 +730,79 @@ async function executeAct(sessionId, action, detail, internalAccess, internalAut
629
730
  break;
630
731
  }
631
732
  // Re-resolve against FRESH elements every act — never trust a stale index.
632
- const { el, fresh } = await resolveFreshActTarget(session, browser, compactV2ActionPage, compactV2Authorization, resolutionTarget, internalAccess, action.kind, "", action.target, true);
733
+ const { el, fresh, driveIdentity } = await resolveFreshActTarget(session, browser, compactV2ActionPage, compactV2Authorization, resolutionTarget, internalAccess, action.kind, "", action.target, true);
734
+ actedCombobox = actsThroughOverlay(el, driveIdentity?.picker === true);
735
+ const ariaLabelAttribute = driveIdentity === undefined ? (el.ariaLabel ?? "") : (driveIdentity.ariaLabel ?? "");
633
736
  // Preserve frame identity (origin + path) for the frame-scoped fill.
634
737
  if (action.kind === "click" || action.kind === "js_click") {
738
+ const clickPage = compactV2ActionPage ?? browser.page;
739
+ // Only the drive's extra dispatch arms consume a frame scope; the
740
+ // tools reach a frame through actDriverTarget's frame target, so
741
+ // walking the positional frame path for them would be dead work.
742
+ const clickScope = options?.drive !== true || clickPage === null || clickPage === undefined
743
+ ? undefined
744
+ : scopeForElement(clickPage, el);
745
+ if (action.kind === "click" &&
746
+ clickScope !== undefined &&
747
+ (await clickTargetOccluded(clickScope, el.selector))) {
748
+ throw new BrowserClickDispatchError("not_dispatched", "click target is occluded by an overlay");
749
+ }
750
+ // One adoption arm/grace around whichever dispatch runs: an option row
751
+ // or a cross-origin coordinate click can open a target=_blank tab
752
+ // just as an ordinary click can, and paying the grace per candidate
753
+ // would charge every click for the branches that declined.
754
+ const commitsListOption = action.kind === "click" &&
755
+ clickPage !== null &&
756
+ clickPage !== undefined &&
757
+ clickScope !== undefined;
758
+ const crossOriginScope = action.kind === "click" && clickScope !== undefined && "parentFrame" in clickScope
759
+ ? clickScope
760
+ : undefined;
635
761
  actionPageAfter =
636
762
  (await adoptTabOpenedByClick(session, browser, async () => {
763
+ if (commitsListOption &&
764
+ clickPage !== null &&
765
+ clickPage !== undefined &&
766
+ clickScope !== undefined &&
767
+ (await commitDriveListOption(clickPage, clickScope, el.selector))) {
768
+ return;
769
+ }
770
+ if (crossOriginScope !== undefined &&
771
+ clickPage !== null &&
772
+ clickPage !== undefined &&
773
+ (await clickCrossOriginFrameTarget(clickPage, crossOriginScope, el.selector))) {
774
+ return;
775
+ }
637
776
  await actClick({ ...actDriverTarget(el), method: action.kind });
638
777
  })) ?? actionPageAfter;
639
778
  }
640
779
  else if (action.kind === "type") {
641
780
  clearCommittedSelectValue(session, el.selector);
642
781
  const actTarget = actDriverTarget(el);
643
- await actType(actTarget, typedText, false);
782
+ if (options?.drive === true && actedCombobox && compactV2ActionPage !== undefined) {
783
+ // The click may remount the field into an overlay that takes focus,
784
+ // so the text goes to whatever is focused — after an explicit
785
+ // select-all, because insertText alone APPENDS to a committed value.
786
+ // The suggestion baseline is read AFTER the overlay opens; reading
787
+ // it before would make the refresh wait return on the stale rows.
788
+ await actClick({ ...actTarget, method: "click" });
789
+ await waitForOpenedOverlay(compactV2ActionPage).catch(() => undefined);
790
+ const overlayBefore = await overlayOptionLabels(compactV2ActionPage);
791
+ await compactV2ActionPage.keyboard.press("ControlOrMeta+a");
792
+ await compactV2ActionPage.keyboard.insertText(typedText ?? "");
793
+ await waitForOverlayOptionsToChange(compactV2ActionPage, overlayBefore);
794
+ if (submitsOnEnter(el, ariaLabelAttribute)) {
795
+ await compactV2ActionPage.keyboard.press("Enter").catch(() => undefined);
796
+ }
797
+ }
798
+ else {
799
+ await actType(actTarget, typedText, false);
800
+ if (options?.drive === true && submitsOnEnter(el, ariaLabelAttribute)) {
801
+ await (compactV2ActionPage ?? browser.page)?.keyboard
802
+ .press("Enter")
803
+ .catch(() => undefined);
804
+ }
805
+ }
644
806
  // #635 fix (not a gate on typing): Shopify only enables delivery-rate
645
807
  // selection after the required address line is committed by
646
808
  // blur/change, not merely after the raw keystrokes land.
@@ -690,24 +852,48 @@ async function executeAct(sessionId, action, detail, internalAccess, internalAut
690
852
  }
691
853
  // Atomic OAuth deliberately accepts only the observed stable ref. A raw
692
854
  // locator would lose the same stale-reference guarantees as every other
693
- // action before the provider transition begins.
694
- const fresh = (await browser.extractBrowserUseObservation(compactV2ActionPage)).elements;
695
- retainSessionElements(session, fresh);
696
- const el = compactV2Authorization === undefined
697
- ? resolveTarget(fresh, resolutionTarget)
698
- : resolveAuthorizedCompactV2Target(session, fresh, compactV2Authorization);
699
- if (el === null) {
700
- if (session.compactV2Active) {
701
- if (!internalAccess)
702
- throwCompactV2StaleRef();
855
+ // action before the provider transition begins. Resolve it by the
856
+ // identity it was OBSERVED under: a drive ref carries the drive's own
857
+ // record, a tools ref the tools' authorization/fresh inventory. A
858
+ // drive session never populates the tools' compact-v2 index, so mixing
859
+ // the two namespaces is what lost the handoff target.
860
+ const driveResolved = internalAccess && session.drive?.identities?.get(resolutionTarget) !== undefined;
861
+ let fresh;
862
+ let el;
863
+ if (driveResolved) {
864
+ const livePage = compactV2ActionPage ?? browser.page;
865
+ const identity = session.drive.identities.get(resolutionTarget);
866
+ fresh = session.lastElements;
867
+ el =
868
+ livePage === null
869
+ ? null
870
+ : await resolveLiveControlIdentity(livePage, resolutionTarget, identity);
871
+ if (el === null) {
872
+ if (session.compactV2Active)
873
+ throw new CompactV2StaleRefError("stale_ref");
703
874
  throw new Error("oauth_login: internal live target changed");
704
875
  }
705
- throw new Error(`oauth_login: no element matched target "${action.target}". Re-observe and use the OAuth control ref.`);
876
+ }
877
+ else {
878
+ fresh = (await browser.extractBrowserUseObservation(compactV2ActionPage)).elements;
879
+ retainSessionElements(session, fresh);
880
+ el =
881
+ compactV2Authorization === undefined
882
+ ? resolveTarget(fresh, resolutionTarget)
883
+ : resolveAuthorizedCompactV2Target(session, fresh, compactV2Authorization);
884
+ if (el === null) {
885
+ if (session.compactV2Active) {
886
+ if (!internalAccess)
887
+ throwCompactV2StaleRef();
888
+ throw new Error("oauth_login: internal live target changed");
889
+ }
890
+ throw new Error(`oauth_login: no element matched target "${action.target}". Re-observe and use the OAuth control ref.`);
891
+ }
706
892
  }
707
893
  if (oauthDeadline === undefined) {
708
894
  throw new Error("OAuth action deadline was not established");
709
895
  }
710
- browser = await runSerializedOAuthBoundary(session, el, fresh, action.provider, oauthDeadline, compactV2Authorization, preparedOAuthDispatch);
896
+ browser = await runSerializedOAuthBoundary(session, el, driveResolved ? [el] : fresh, action.provider, oauthDeadline, compactV2Authorization, preparedOAuthDispatch, driveResolved);
711
897
  const completedPage = browser.completedOAuthPage() ?? undefined;
712
898
  rememberOAuthCompletionSourcePage(session, completedPage);
713
899
  actionPageAfter = completedPage ?? actionPageAfter;
@@ -755,6 +941,7 @@ async function executeAct(sessionId, action, detail, internalAccess, internalAut
755
941
  if (session.drive !== null) {
756
942
  const observeMs = Date.now() - observeStarted;
757
943
  session.drive.lastActProfile = {
944
+ ...session.drive.lastActProfile,
758
945
  act_ms: Math.max(0, Date.now() - actStarted - settleMs - observeMs),
759
946
  settle_ms: settleMs,
760
947
  observe_ms: observeMs,
@@ -782,8 +969,104 @@ async function executeAct(sessionId, action, detail, internalAccess, internalAut
782
969
  outcome: {
783
970
  ...(completedAction.kind === "select" ? { selectedOption: completedAction.text } : {}),
784
971
  },
972
+ combobox: actedCombobox,
785
973
  };
786
974
  }
975
+ async function selectDriveOptionOrRefuse(scope, selector, text) {
976
+ const { outcome, committed } = await selectDriveOption(scope, selector, text);
977
+ if (outcome === "ok")
978
+ return committed;
979
+ if (outcome === "missing")
980
+ throw new CompactV2StaleRefError("stale_ref");
981
+ throw new CompactV2ActionFailureError(outcome);
982
+ }
983
+ async function guardDriveOauthTarget(sessionId, target) {
984
+ const session = sessionForCall(sessionId);
985
+ const identity = session?.drive?.identities?.get(target);
986
+ const page = session?.browser.page ?? null;
987
+ if (session === undefined || identity === undefined || page === null) {
988
+ // No drive identity record: this target came from a fallback observation,
989
+ // so it is the tools' to resolve, not the drive's to dispatch.
990
+ return { kind: "unsupported" };
991
+ }
992
+ const scope = await resolveIdentityScope(page, target, identity);
993
+ if (scope === null)
994
+ return { kind: "stale", reason: "stale_ref" };
995
+ if (await clickTargetOccluded(scope, identity.selector)) {
996
+ return { kind: "stale", reason: "occluded" };
997
+ }
998
+ // Clean and still the control the drive observed: DISPATCH it. An OAuth
999
+ // handoff is an ordinary action; refusing it here and falling through to the
1000
+ // tools path is what let a drive ref lose its target to the tools' index.
1001
+ return { kind: "ok", combobox: false };
1002
+ }
1003
+ export async function dispatchDriveAct(sessionId, action) {
1004
+ // "unsupported" means the shared executor has no drive verb for this action
1005
+ // and the caller should fall back to the tools path. A DISPATCH failure is a
1006
+ // different thing — the act did not land on the control, which is stale.
1007
+ if (action.kind !== "click" &&
1008
+ action.kind !== "type" &&
1009
+ action.kind !== "select" &&
1010
+ action.kind !== "scroll" &&
1011
+ action.kind !== "oauth_login") {
1012
+ return { kind: "unsupported" };
1013
+ }
1014
+ // An OAuth handoff is a drive action like any other. The drive guard proves
1015
+ // the control is still the one it observed and refuses an occluded target
1016
+ // BEFORE the OAuth deadline is spent, then the shared executor dispatches it
1017
+ // by that identity — never by the tools' index, which a drive session never
1018
+ // populated. A target with no drive identity record is the tools' to resolve.
1019
+ if (action.kind === "oauth_login") {
1020
+ const guard = await guardDriveOauthTarget(sessionId, action.target);
1021
+ if (guard.kind !== "ok")
1022
+ return guard;
1023
+ }
1024
+ else if (action.kind !== "scroll" &&
1025
+ sessionForCall(sessionId)?.drive?.identities?.get(action.target) === undefined) {
1026
+ return { kind: "unsupported" };
1027
+ }
1028
+ try {
1029
+ const result = await actInternally(sessionId, action, "none", undefined, undefined, DRIVE_DISPATCH);
1030
+ // An OAuth act can end on a wall (no live provider session, a 2FA hand-off)
1031
+ // WITHOUT dispatching: the executor returns that as an observation. The
1032
+ // drive must surface it — dropping it here would re-observe the unchanged
1033
+ // page and look like the action did nothing.
1034
+ return {
1035
+ kind: "ok",
1036
+ combobox: result.combobox === true,
1037
+ ...(result.observation.needs_user === undefined
1038
+ ? {}
1039
+ : { needsUser: result.observation.needs_user }),
1040
+ };
1041
+ }
1042
+ catch (error) {
1043
+ if (error instanceof CompactV2StaleRefError)
1044
+ return { kind: "stale", reason: "stale_ref" };
1045
+ if (error instanceof TargetStaleError)
1046
+ return { kind: "stale", reason: "stale" };
1047
+ const message = error instanceof Error ? error.message : String(error);
1048
+ // A tracked click that reached the element and only then failed did land —
1049
+ // telling the drive it never executed invites a second submit. The evidence
1050
+ // is a CLICK's own dispatch listener, so it says nothing about a type whose
1051
+ // focusing click landed before the keystrokes were written.
1052
+ const dispatchStatus = error instanceof CompactV2ActionFailureError
1053
+ ? error.dispatchStatus
1054
+ : clickDispatchStatusForError(error);
1055
+ if (action.kind === "click" && dispatchStatus === "dispatched") {
1056
+ return { kind: "ok", combobox: false };
1057
+ }
1058
+ // A select that could not take the value was refused by the CONTROL; the
1059
+ // drive must not blame the ref and retire it. A ref that went stale before
1060
+ // the select was ever attempted stays stale.
1061
+ if (action.kind === "select" && !/stale_ref|reobserve_required/i.test(message)) {
1062
+ return { kind: "stale", reason: message === "not_select" ? "not_select" : "option_missing" };
1063
+ }
1064
+ return {
1065
+ kind: "stale",
1066
+ reason: /occluded|intercepts pointer/i.test(message) ? "occluded" : "stale_ref",
1067
+ };
1068
+ }
1069
+ }
787
1070
  export function compactV2SelectionFailureReason(error) {
788
1071
  return compactV2ActionFailureReason(error, "select");
789
1072
  }