@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.
- package/README.md +9 -3
- package/dist/bot/act/act.d.ts +16 -1
- package/dist/bot/act/act.d.ts.map +1 -1
- package/dist/bot/act/act.js +308 -25
- package/dist/bot/act/act.js.map +1 -1
- package/dist/bot/act/identity.d.ts +64 -0
- package/dist/bot/act/identity.d.ts.map +1 -0
- package/dist/bot/act/identity.js +198 -0
- package/dist/bot/act/identity.js.map +1 -0
- package/dist/bot/act/targets.d.ts +4 -0
- package/dist/bot/act/targets.d.ts.map +1 -1
- package/dist/bot/act/targets.js +6 -0
- package/dist/bot/act/targets.js.map +1 -1
- package/dist/bot/broker/admission-context.d.ts +10 -0
- package/dist/bot/broker/admission-context.d.ts.map +1 -1
- package/dist/bot/broker/admission-context.js +4 -0
- package/dist/bot/broker/admission-context.js.map +1 -1
- package/dist/bot/broker/authority.d.ts +13 -10
- package/dist/bot/broker/authority.d.ts.map +1 -1
- package/dist/bot/broker/authority.js +8 -18
- package/dist/bot/broker/authority.js.map +1 -1
- package/dist/bot/broker/custody.d.ts +3 -1
- package/dist/bot/broker/custody.d.ts.map +1 -1
- package/dist/bot/broker/custody.js.map +1 -1
- package/dist/bot/broker/daemon.d.ts +9 -19
- package/dist/bot/broker/daemon.d.ts.map +1 -1
- package/dist/bot/broker/daemon.js +16 -47
- package/dist/bot/broker/daemon.js.map +1 -1
- package/dist/bot/broker/discovery.d.ts +25 -24
- package/dist/bot/broker/discovery.d.ts.map +1 -1
- package/dist/bot/broker/discovery.js +31 -61
- package/dist/bot/broker/discovery.js.map +1 -1
- package/dist/bot/broker/forwarder.d.ts +7 -0
- package/dist/bot/broker/forwarder.d.ts.map +1 -1
- package/dist/bot/broker/forwarder.js +41 -5
- package/dist/bot/broker/forwarder.js.map +1 -1
- package/dist/bot/broker/operator.d.ts +18 -11
- package/dist/bot/broker/operator.d.ts.map +1 -1
- package/dist/bot/broker/operator.js +69 -35
- package/dist/bot/broker/operator.js.map +1 -1
- package/dist/bot/broker/protocol.d.ts +29 -3
- package/dist/bot/broker/protocol.d.ts.map +1 -1
- package/dist/bot/broker/runtime.d.ts +14 -4
- package/dist/bot/broker/runtime.d.ts.map +1 -1
- package/dist/bot/broker/runtime.js +32 -17
- package/dist/bot/broker/runtime.js.map +1 -1
- package/dist/bot/broker/transport.d.ts +1 -2
- package/dist/bot/broker/transport.d.ts.map +1 -1
- package/dist/bot/broker/transport.js +7 -9
- package/dist/bot/broker/transport.js.map +1 -1
- package/dist/bot/browser-process-owner.d.ts.map +1 -1
- package/dist/bot/browser-process-owner.js +9 -6
- package/dist/bot/browser-process-owner.js.map +1 -1
- package/dist/bot/browser-process-runtime.d.ts +0 -1
- package/dist/bot/browser-process-runtime.d.ts.map +1 -1
- package/dist/bot/browser-process-runtime.js +7 -5
- package/dist/bot/browser-process-runtime.js.map +1 -1
- package/dist/bot/browser.d.ts +17 -1
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +73 -15
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/capture/verification.d.ts +23 -0
- package/dist/bot/capture/verification.d.ts.map +1 -1
- package/dist/bot/capture/verification.js +54 -17
- package/dist/bot/capture/verification.js.map +1 -1
- package/dist/bot/compact-observation-v2.d.ts.map +1 -1
- package/dist/bot/compact-observation-v2.js +10 -7
- package/dist/bot/compact-observation-v2.js.map +1 -1
- package/dist/bot/drive-act.d.ts +32 -23
- package/dist/bot/drive-act.d.ts.map +1 -1
- package/dist/bot/drive-act.js +163 -518
- package/dist/bot/drive-act.js.map +1 -1
- package/dist/bot/drive-snapshot.d.ts +8 -0
- package/dist/bot/drive-snapshot.d.ts.map +1 -1
- package/dist/bot/drive-snapshot.js +127 -15
- package/dist/bot/drive-snapshot.js.map +1 -1
- package/dist/bot/google-login.d.ts +14 -54
- package/dist/bot/google-login.d.ts.map +1 -1
- package/dist/bot/google-login.js +46 -382
- package/dist/bot/google-login.js.map +1 -1
- package/dist/bot/operate-drive.d.ts +6 -4
- package/dist/bot/operate-drive.d.ts.map +1 -1
- package/dist/bot/operate-drive.js +223 -146
- package/dist/bot/operate-drive.js.map +1 -1
- package/dist/bot/remote-login-display.d.ts +5 -1
- package/dist/bot/remote-login-display.d.ts.map +1 -1
- package/dist/bot/remote-login-display.js +29 -1
- package/dist/bot/remote-login-display.js.map +1 -1
- package/dist/bot/session/lifecycle.d.ts.map +1 -1
- package/dist/bot/session/lifecycle.js +2 -1
- package/dist/bot/session/lifecycle.js.map +1 -1
- package/dist/bot/session/model.d.ts +15 -6
- package/dist/bot/session/model.d.ts.map +1 -1
- package/dist/bot/session/model.js.map +1 -1
- package/dist/browser-busy.d.ts +3 -0
- package/dist/browser-busy.d.ts.map +1 -1
- package/dist/browser-busy.js +5 -15
- package/dist/browser-busy.js.map +1 -1
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +11 -5
- package/dist/install/cli.js.map +1 -1
- package/dist/pairing-ttl.d.ts +4 -0
- package/dist/pairing-ttl.d.ts.map +1 -0
- package/dist/pairing-ttl.js +14 -0
- package/dist/pairing-ttl.js.map +1 -0
- package/dist/tools/provision-drive.d.ts +2 -2
- package/dist/tools/provision-drive.d.ts.map +1 -1
- package/dist/tools/provision-drive.js +4 -0
- package/dist/tools/provision-drive.js.map +1 -1
- 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.
|
|
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.
|
|
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
|
package/dist/bot/act/act.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/bot/act/act.js
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 () =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
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
|
}
|