Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 61 additions & 13 deletions src/chrome/src/agent/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -3007,6 +3007,56 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
if (!pageUrl) {
try { pageUrl = await this._currentUrl(tabId); } catch {}
}
let detection = null;
let detectionFailed = false;
let failedDiagnostics = null;
let detectionAttempted = false;
const inspectCaptchaFrames = async () => {
if (detectionAttempted) return;
detectionAttempted = true;
try {
detection = await detectCaptcha(tabId);
} catch (error) {
detectionFailed = true;
failedDiagnostics = error?.captchaDiagnostics || null;
}
};
let languageNeutralFrameTrigger = false;
const hasDialogSurface = toolResult.pageGate?.surface === 'dialog'
|| /^\s*(?:dialog|alertdialog)(?=\s|$)/im.test(toolResult.pageContent);
if (!challenge && hasDialogSurface) {
await inspectCaptchaFrames();
const selectedActiveFrame = detection?.selected?.activeChallengeFrame === true
&& detection?.selected?.activeChallengeFrameVisible === true
&& detection?.selected?.visible === true
&& detection?.selected?.frameVisible !== false;
const diagnosticActiveFrame = (detection?.diagnostics?.frames || []).find(frame =>
frame?.activeChallengeFrame === true && frame?.visible === true
);
Comment thread
esokullu marked this conversation as resolved.
if (selectedActiveFrame || diagnosticActiveFrame) {
const vendor = diagnosticActiveFrame?.vendor
|| (String(detection?.selected?.type || '').startsWith('recaptcha')
? 'recaptcha'
: detection?.selected?.type || 'captcha');
const vendorLabel = vendor === 'recaptcha'
? 'reCAPTCHA'
: vendor === 'hcaptcha'
? 'hCaptcha'
: vendor === 'arkose'
? 'Arkose'
: 'CAPTCHA';
challenge = detectChallengeDialog(`dialog ${JSON.stringify(`Visible ${vendorLabel} CAPTCHA challenge`)}`);
languageNeutralFrameTrigger = !!challenge;
if (selectedActiveFrame) {
observedChallengeFrameId = Number.isInteger(detection.selected.frameId)
? detection.selected.frameId
: observedChallengeFrameId;
observedChallengeFrameUrl = String(
detection.selected.frameUrl || observedChallengeFrameUrl
);
}
}
}
const requestedPage = toolArgs?.page;
const requestedMaxDepth = toolArgs?.maxDepth;
const parsedMaxDepth = Number(requestedMaxDepth);
Expand Down Expand Up @@ -3157,17 +3207,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
return { gate: existing.publicGate, loopCheck };
}

let detection = null;
let detectionFailed = false;
let failedDiagnostics = null;
if (this.captchaSolverEnabled) {
try {
detection = await detectCaptcha(tabId);
} catch (error) {
detectionFailed = true;
failedDiagnostics = error?.captchaDiagnostics || null;
}
}
await inspectCaptchaFrames();
const diagnostics = detection?.diagnostics || failedDiagnostics || {
vendors: [],
candidateTypes: [],
Expand All @@ -3181,8 +3221,15 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
&& frame?.visible === true
))
.map(frame => frame.vendor))];
const selectedCorrelated = detection?.selected?.dialogAssociated === true
&& detection?.selected?.frameVisible !== false;
const selectedCorrelated = detection?.selected?.frameVisible !== false
&& (
detection?.selected?.dialogAssociated === true
|| (
detection?.selected?.activeChallengeFrame === true
&& detection?.selected?.activeChallengeFrameVisible === true
&& detection?.selected?.visible === true
)
);
const supported = this.captchaSolverEnabled
&& !detectionFailed
&& !detection?.error
Expand All @@ -3199,6 +3246,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
...(!this.captchaSolverEnabled ? { solverDisabled: true } : {}),
...(detection?.error ? { selectionFailed: true } : {}),
...(detection?.selected && !selectedCorrelated ? { candidateNotCorrelated: true } : {}),
...(languageNeutralFrameTrigger ? { languageNeutralFrameTrigger: true } : {}),
};
this._captchaGateStates.set(tabId, {
key,
Expand Down
83 changes: 75 additions & 8 deletions src/chrome/src/agent/captcha-frame-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,18 +263,28 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
const candidate = reconcileAncestorLoader(rawCandidate);
const frameVisible = frameIsVisible(candidate?.frameId)
&& candidate?.frameVisibleWithinAnchor !== false;
const visible = candidate?.visible === true && frameVisible;
return {
...candidate,
frameVisible,
websiteURL: nearestHttpUrl(candidate),
dialogAssociated: candidate?.dialogAssociated === true
|| frameIsDialogAssociated(candidate?.frameId),
visible: candidate?.visible === true && frameVisible,
visible,
normalCheckbox: candidate?.normalCheckbox === true && candidate?.visible === true && frameVisible,
activeChallengeFrameVisible: candidate?.activeChallengeFrame === true && visible,
};
});
}

function isVisibleActiveChallengeFrame(candidate) {
if (candidate?.activeChallengeFrameVisible === true) return true;
if (candidate?.activeChallengeFrameVisible === false) return false;
return candidate?.activeChallengeFrame === true
&& candidate?.visible === true
&& candidate?.frameVisible !== false;
}

function candidateSummary(candidate) {
return {
frameId: Number.isInteger(candidate?.frameId) ? candidate.frameId : null,
Expand All @@ -291,6 +301,8 @@ function candidateSummary(candidate) {
visible: candidate?.visible === true,
normalCheckbox: candidate?.normalCheckbox === true,
challengeFrame: candidate?.challengeFrame === true,
activeChallengeFrame: candidate?.activeChallengeFrame === true,
activeChallengeFrameVisible: isVisibleActiveChallengeFrame(candidate),
dialogAssociated: candidate?.dialogAssociated === true,
frameVisible: candidate?.frameVisible !== false,
isInvisible: candidate?.isInvisible === true,
Expand Down Expand Up @@ -326,9 +338,7 @@ function candidateScore(candidate) {
// Priority is tiered so no combination of secondary signals can make a
// generic visible/background integration outrank an active challenge
// frame or visible checkbox.
const activeChallengeFrame = candidate?.challengeFrame
&& candidate?.visible === true
&& candidate?.frameVisible !== false;
const activeChallengeFrame = isVisibleActiveChallengeFrame(candidate);
const primary = (candidate?.normalCheckbox && candidate?.visible) || activeChallengeFrame;
const tier = primary ? 3 : (candidate?.visible ? 2 : 1);
let score = tier * 1000;
Expand Down Expand Up @@ -406,6 +416,10 @@ export function selectCaptchaCandidate(candidates, constraints = {}) {
visible: previous.visible === true || candidate.visible === true,
normalCheckbox: previous.normalCheckbox === true || candidate.normalCheckbox === true,
challengeFrame: previous.challengeFrame === true || candidate.challengeFrame === true,
activeChallengeFrame: previous.activeChallengeFrame === true
|| candidate.activeChallengeFrame === true,
Comment thread
esokullu marked this conversation as resolved.
activeChallengeFrameVisible: isVisibleActiveChallengeFrame(previous)
|| isVisibleActiveChallengeFrame(candidate),
dialogAssociated: previous.dialogAssociated === true || candidate.dialogAssociated === true,
responseField: previous.responseField === true || candidate.responseField === true,
responseTokenPresent: previous.responseTokenPresent === true
Expand Down Expand Up @@ -580,7 +594,7 @@ function selectedReason(candidate, constraints) {
if (constraints.frameUrl) return 'exact frameUrl match';
if (constraints.websiteKey) return 'exact websiteKey match';
if (candidate.normalCheckbox && candidate.visible) return 'visible checkbox challenge';
if (candidate.visible && candidate.challengeFrame && candidate.frameVisible !== false) return 'visible challenge frame';
if (isVisibleActiveChallengeFrame(candidate)) return 'visible challenge frame';
if (candidate.visible) return 'visible CAPTCHA widget';
if (candidate.challengeFrame && candidate.frameVisible !== false) return 'challenge frame candidate';
return 'only detected CAPTCHA candidate';
Expand Down Expand Up @@ -627,6 +641,29 @@ export function detectCaptchaCandidatesInPage(scope = null) {
return null;
}
};
// Page-serialized counterpart of captchaActiveChallengeFrameVendor in
// captcha-gate.js. Only challenge routes, never checkbox routes, may arm it.
const activeChallengeFrameVendor = (urlStr) => {
try {
const parsed = new URL(String(urlStr || ''), frameUrl || 'https://dummy.host');
const host = parsed.hostname.toLowerCase();
const path = parsed.pathname.toLowerCase();
if (
(/(^|\.)google\.com$/.test(host) || /(^|\.)recaptcha\.net$/.test(host))
&& /\/recaptcha\/(?:api2|enterprise)\/bframe(?:\/|$)/.test(path)
) return 'recaptcha';
if (/(^|\.)hcaptcha\.com$/.test(host)) {
const frame = new URLSearchParams(String(parsed.hash || '').replace(/^#/, '')).get('frame');
if (frame === 'challenge') return 'hcaptcha';
}
if (
/(^|\.)(?:arkoselabs|funcaptcha)\.com$/.test(host)
&& /\/fc\/gc(?:\/|$)/.test(path)
) return 'arkose';
} catch (_) {}
return '';
};
const documentChallengeVendor = activeChallengeFrameVendor(frameUrl);
const visibleElement = (element) => {
if (!element) return false;
try {
Expand Down Expand Up @@ -714,7 +751,9 @@ export function detectCaptchaCandidatesInPage(scope = null) {
candidates.push({
...serializableCandidate,
frameUrl,
challengeFrame,
challengeFrame: candidate.challengeFrame === true || challengeFrame,
activeChallengeFrame: candidate.activeChallengeFrame === true
|| !!documentChallengeVendor,
responseField,
responseTokenPresent: candidate.responseTokenPresent === true
|| alsoResponseTokenPresent === true,
Expand Down Expand Up @@ -873,16 +912,21 @@ export function detectCaptchaCandidatesInPage(scope = null) {
const recaptchaFrames = iframeUrls.filter(({ url }) =>
/recaptcha\/(api2|enterprise)\/anchor/i.test(url)
);
const recaptchaChallengeFrames = iframeUrls.filter(({ url }) =>
activeChallengeFrameVendor(url) === 'recaptcha'
);

for (const { element, url } of iframeUrls) {
if (/hcaptcha\.com/i.test(url)) {
const websiteKey = urlParam(url, 'sitekey');
if (websiteKey) {
const activeChallengeFrame = activeChallengeFrameVendor(url) === 'hcaptcha';
add({
type: 'hcaptcha',
websiteKey,
visible: visibleElement(element),
normalCheckbox: visibleElement(element),
normalCheckbox: visibleElement(element) && !activeChallengeFrame,
activeChallengeFrame,
...responseFieldIdentity(
element,
'h-captcha-response',
Expand Down Expand Up @@ -919,10 +963,31 @@ export function detectCaptchaCandidatesInPage(scope = null) {
}
continue;
}
if (!/recaptcha\/(api2|enterprise)\/anchor/i.test(url)) continue;
const recaptchaChallengeFrame = activeChallengeFrameVendor(url) === 'recaptcha';
if (!recaptchaChallengeFrame && !/recaptcha\/(api2|enterprise)\/anchor/i.test(url)) continue;
const websiteKey = urlParam(url, 'k');
if (!websiteKey) continue;
const isEnterprise = /recaptcha\/enterprise/i.test(url);
if (recaptchaChallengeFrame) {
const challengeIndex = recaptchaChallengeFrames.findIndex(frame => frame.element === element);
add({
type: isEnterprise ? 'recaptcha_v2_enterprise' : 'recaptcha_v2',
websiteKey,
isInvisible: false,
isEnterprise,
visible: visibleElement(element),
normalCheckbox: false,
activeChallengeFrame: true,
...responseFieldIdentity(
element,
'g-recaptcha-response',
challengeIndex,
recaptchaChallengeFrames.length,
),
detectedVia: 'url',
}, element);
continue;
}
const isInvisible = urlParam(url, 'size') === 'invisible';
const matchingV3Script = scriptUrls.find(scriptUrl => urlParam(scriptUrl, 'render') === websiteKey) || null;
const isV3 = !!matchingV3Script;
Expand Down Expand Up @@ -994,13 +1059,15 @@ export function detectCaptchaCandidatesInPage(scope = null) {
try { url = String(element.src || ''); } catch (_) {}
try { loadedUrl = String(element.contentWindow?.location?.href || ''); } catch (_) {}
try { name = String(element.name || element.getAttribute?.('name') || ''); } catch (_) {}
const activeChallengeFrame = !!activeChallengeFrameVendor(loadedUrl || url);
return {
index,
url,
loadedUrl,
name,
visible: visibleElement(element),
dialogAssociated: elementInChallengeDialog(element),
activeChallengeFrame,
};
});
let frameName = '';
Expand Down
Loading
Loading