Skip to content

Commit d6b9ec1

Browse files
committed
allow CAPTCHA gate abandonment
1 parent b9df6ec commit d6b9ec1

3 files changed

Lines changed: 123 additions & 5 deletions

File tree

src/chrome/src/agent/agent.js

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2697,10 +2697,18 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
26972697
_captchaGateBlockResult(tabId, toolName, toolArgs = {}) {
26982698
const gate = this._captchaGateStates.get(tabId);
26992699
const gatedCompletion = toolName === 'done' || toolName === 'done_json';
2700+
const abandonmentNavigation = Agent.NAV_TOOLS.has(toolName);
27002701
const gatedAction = this._isBrowserMutationTool(toolName)
27012702
|| gatedCompletion
27022703
|| isNetworkMutation(toolName, toolArgs);
2703-
if (!gate || !gatedAction) return null;
2704+
if (
2705+
!gate
2706+
|| !gatedAction
2707+
|| abandonmentNavigation
2708+
|| (gatedCompletion && gate.status === 'manual_required')
2709+
) {
2710+
return null;
2711+
}
27042712
if (toolName === 'solve_captcha' && gate.status === 'solve_required') return null;
27052713
if (gate.status === 'manual_required') {
27062714
return {
@@ -2735,6 +2743,25 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
27352743
};
27362744
}
27372745

2746+
_clearCaptchaGateAfterNavigation(tabId, toolName, beforeUrl, afterUrl, toolResult) {
2747+
if (!Agent.NAV_TOOLS.has(toolName)) return null;
2748+
const gate = this._captchaGateStates.get(tabId);
2749+
if (!gate) return null;
2750+
const beforeDocument = this._normalizeUrlPath(beforeUrl);
2751+
const afterDocument = this._normalizeUrlPath(afterUrl);
2752+
if (!beforeDocument || !afterDocument || beforeDocument === afterDocument) return null;
2753+
const clearedGate = {
2754+
...gate.publicGate,
2755+
status: 'cleared',
2756+
clearedByNavigation: true,
2757+
};
2758+
this._captchaGateStates.delete(tabId);
2759+
if (toolResult && typeof toolResult === 'object') {
2760+
toolResult.captchaGate = clearedGate;
2761+
}
2762+
return clearedGate;
2763+
}
2764+
27382765
_visibleChallengeDialogFromFrames(frameEntries, navigationFrames) {
27392766
const candidates = [];
27402767
const frameContexts = [];
@@ -2835,7 +2862,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
28352862
const gatedAction = this._isBrowserMutationTool(toolName)
28362863
|| gatedCompletion
28372864
|| isNetworkMutation(toolName, toolArgs);
2838-
if (!gatedAction || toolName === 'solve_captcha') return null;
2865+
if (!gatedAction || toolName === 'solve_captcha' || Agent.NAV_TOOLS.has(toolName)) return null;
28392866
const activeGate = this._captchaGateStates.get(tabId);
28402867
if (activeGate && !this._shouldRetryCaptchaManualGate(activeGate)) return null;
28412868
const challenge = await this._detectChallengeDialogBeforeMutation(tabId);
@@ -3870,6 +3897,14 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
38703897

38713898
const beforePath = this._normalizeUrlPath(beforeUrl);
38723899
const afterPath = this._normalizeUrlPath(afterUrl);
3900+
const clearedCaptchaGate = this._clearCaptchaGateAfterNavigation(
3901+
tabId,
3902+
fnName,
3903+
beforeUrl,
3904+
afterUrl,
3905+
toolResult,
3906+
);
3907+
if (clearedCaptchaGate) onUpdate('captcha_gate', clearedCaptchaGate);
38733908
// Explicit navigation tools intentionally go somewhere. For implicit
38743909
// navigation, retain the less noisy path-level warning policy: query /
38753910
// hash-only SPA changes reset state but do not force a re-plan notice.

src/firefox/src/agent/agent.js

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2367,10 +2367,18 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
23672367
_captchaGateBlockResult(tabId, toolName, toolArgs = {}) {
23682368
const gate = this._captchaGateStates.get(tabId);
23692369
const gatedCompletion = toolName === 'done' || toolName === 'done_json';
2370+
const abandonmentNavigation = Agent.NAV_TOOLS.has(toolName);
23702371
const gatedAction = this._isBrowserMutationTool(toolName)
23712372
|| gatedCompletion
23722373
|| isNetworkMutation(toolName, toolArgs);
2373-
if (!gate || !gatedAction) return null;
2374+
if (
2375+
!gate
2376+
|| !gatedAction
2377+
|| abandonmentNavigation
2378+
|| (gatedCompletion && gate.status === 'manual_required')
2379+
) {
2380+
return null;
2381+
}
23742382
if (toolName === 'solve_captcha' && gate.status === 'solve_required') return null;
23752383
if (gate.status === 'manual_required') {
23762384
return {
@@ -2405,6 +2413,25 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
24052413
};
24062414
}
24072415

2416+
_clearCaptchaGateAfterNavigation(tabId, toolName, beforeUrl, afterUrl, toolResult) {
2417+
if (!Agent.NAV_TOOLS.has(toolName)) return null;
2418+
const gate = this._captchaGateStates.get(tabId);
2419+
if (!gate) return null;
2420+
const beforeDocument = this._normalizeUrlPath(beforeUrl);
2421+
const afterDocument = this._normalizeUrlPath(afterUrl);
2422+
if (!beforeDocument || !afterDocument || beforeDocument === afterDocument) return null;
2423+
const clearedGate = {
2424+
...gate.publicGate,
2425+
status: 'cleared',
2426+
clearedByNavigation: true,
2427+
};
2428+
this._captchaGateStates.delete(tabId);
2429+
if (toolResult && typeof toolResult === 'object') {
2430+
toolResult.captchaGate = clearedGate;
2431+
}
2432+
return clearedGate;
2433+
}
2434+
24082435
_visibleChallengeDialogFromFrames(frameEntries, navigationFrames) {
24092436
const candidates = [];
24102437
const frameContexts = [];
@@ -2501,7 +2528,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
25012528
const gatedAction = this._isBrowserMutationTool(toolName)
25022529
|| gatedCompletion
25032530
|| isNetworkMutation(toolName, toolArgs);
2504-
if (!gatedAction || toolName === 'solve_captcha') return null;
2531+
if (!gatedAction || toolName === 'solve_captcha' || Agent.NAV_TOOLS.has(toolName)) return null;
25052532
const activeGate = this._captchaGateStates.get(tabId);
25062533
if (activeGate && !this._shouldRetryCaptchaManualGate(activeGate)) return null;
25072534
const challenge = await this._detectChallengeDialogBeforeMutation(tabId);
@@ -3466,6 +3493,14 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
34663493

34673494
const beforePath = this._normalizeUrlPath(beforeUrl);
34683495
const afterPath = this._normalizeUrlPath(afterUrl);
3496+
const clearedCaptchaGate = this._clearCaptchaGateAfterNavigation(
3497+
tabId,
3498+
fnName,
3499+
beforeUrl,
3500+
afterUrl,
3501+
toolResult,
3502+
);
3503+
if (clearedCaptchaGate) onUpdate('captcha_gate', clearedCaptchaGate);
34693504
// Explicit navigation tools intentionally go somewhere. For implicit
34703505
// navigation, retain the less noisy path-level warning policy: query /
34713506
// hash-only SPA changes reset state but do not force a re-plan notice.

test/run.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4829,12 +4829,50 @@ test('CAPTCHA challenge gate blocks dismiss/resubmit mutations but allows the on
48294829
`${label}: ${toolName} escaped post-solve verification`,
48304830
);
48314831
}
4832-
agent._captchaGateStates.set(88, { ...active, status: 'manual_required' });
4832+
agent._captchaGateStates.set(88, {
4833+
...active,
4834+
status: 'manual_required',
4835+
publicGate: { ...active.publicGate, status: 'manual_required' },
4836+
});
48334837
assert.equal(
48344838
agent._captchaGateBlockResult(88, 'solve_captcha')?.manualCompletionRequired,
48354839
true,
48364840
`${label}: failed/unsupported challenge allowed another paid solve`,
48374841
);
4842+
assert.equal(agent._captchaGateBlockResult(88, 'done'), null, `${label}: manual gate blocked partial completion`);
4843+
for (const toolName of ['navigate', 'new_tab', 'go_back', 'go_forward']) {
4844+
assert.equal(agent._captchaGateBlockResult(88, toolName), null, `${label}: manual gate blocked abandonment via ${toolName}`);
4845+
}
4846+
assert.equal(
4847+
agent._captchaGateBlockResult(88, 'click_ax')?.manualCompletionRequired,
4848+
true,
4849+
`${label}: abandonment exemption allowed an in-document mutation`,
4850+
);
4851+
const sameDocumentResult = {};
4852+
assert.equal(
4853+
agent._clearCaptchaGateAfterNavigation(
4854+
88,
4855+
'navigate',
4856+
'https://example.test/signup?step=1',
4857+
'https://example.test/signup?step=2',
4858+
sameDocumentResult,
4859+
),
4860+
null,
4861+
`${label}: query-only navigation bypassed the challenged document gate`,
4862+
);
4863+
assert.equal(agent._captchaGateStates.has(88), true, `${label}: same-document navigation deleted the gate`);
4864+
const abandonmentResult = {};
4865+
const abandoned = agent._clearCaptchaGateAfterNavigation(
4866+
88,
4867+
'navigate',
4868+
'https://example.test/signup',
4869+
'https://other.test/home',
4870+
abandonmentResult,
4871+
);
4872+
assert.equal(abandoned?.status, 'cleared', `${label}: leaving the challenged document did not clear the gate`);
4873+
assert.equal(abandoned?.clearedByNavigation, true, `${label}: navigation clearance reason missing`);
4874+
assert.equal(abandonmentResult.captchaGate?.clearedByNavigation, true, `${label}: navigation result omitted gate clearance`);
4875+
assert.equal(agent._captchaGateStates.has(88), false, `${label}: abandoned document gate remained persisted`);
48384876
}
48394877
});
48404878

@@ -52700,6 +52738,16 @@ test('enabled CAPTCHA gate performs a read-only dialog preflight before the firs
5270052738
assert.equal(disabledGate?.solverDisabled, true, `${build}: disabled preflight did not explain manual routing`);
5270152739
assert.equal(disabledAgent._captchaGateStates.get(2)?.challengeFrameId, 0, `${build}: disabled preflight discarded the challenge frame identity`);
5270252740
assert.equal(disabledAgent._captchaGateBlockResult(2, 'click_ax')?.manualCompletionRequired, true, `${build}: disabled preflight allowed the challenge mutation`);
52741+
52742+
const abandonmentAgent = new AgentClass({});
52743+
abandonmentAgent.captchaSolverEnabled = false;
52744+
const abandonmentGate = await abandonmentAgent._captchaMutationPreflight(
52745+
3,
52746+
'navigate',
52747+
{ url: 'https://other.test/home' },
52748+
);
52749+
assert.equal(abandonmentGate, null, `${build}: navigation away armed a CAPTCHA gate`);
52750+
assert.equal(abandonmentAgent._captchaGateStates.has(3), false, `${build}: navigation preflight persisted a challenge gate`);
5270352751
});
5270452752
}
5270552753
});

0 commit comments

Comments
 (0)