Skip to content

Commit 89744d9

Browse files
author
SentienceDEV
committed
mirror Python recaptcha heuristics like selector hits
1 parent dfb680f commit 89744d9

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

src/agent-runtime.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,9 +863,43 @@ export class AgentRuntime {
863863
const iframeHits = evidence?.iframe_src_hits ?? [];
864864
const urlHits = evidence?.url_hits ?? [];
865865
const textHits = evidence?.text_hits ?? [];
866+
const selectorHits = evidence?.selector_hits ?? [];
866867
if (iframeHits.length === 0 && urlHits.length === 0 && textHits.length === 0) {
867868
return false;
868869
}
870+
// Heuristic: many sites include passive reCAPTCHA badges (v3) that should not block.
871+
// Only block when there is evidence of an interactive challenge.
872+
const hitsAll = [...iframeHits, ...urlHits, ...textHits, ...selectorHits];
873+
const hitsLower = hitsAll.map(hit => String(hit || '').toLowerCase()).filter(Boolean);
874+
const joinedHits = hitsLower.join(' ');
875+
const strongText = [
876+
"i'm not a robot",
877+
'verify you are human',
878+
'human verification',
879+
'complete the security check',
880+
'please verify',
881+
].some(needle => joinedHits.includes(needle));
882+
const strongIframe = hitsLower.some(hit =>
883+
['api2/bframe', 'hcaptcha', 'turnstile'].some(needle => hit.includes(needle))
884+
);
885+
const strongSelector = hitsLower.some(hit =>
886+
[
887+
'g-recaptcha-response',
888+
'h-captcha-response',
889+
'cf-turnstile-response',
890+
'recaptcha-checkbox',
891+
'hcaptcha-checkbox',
892+
].some(needle => hit.includes(needle))
893+
);
894+
const onlyGeneric =
895+
!strongText &&
896+
!strongIframe &&
897+
!strongSelector &&
898+
hitsLower.length > 0 &&
899+
hitsLower.every(hit => hit.includes('captcha') || hit.includes('recaptcha'));
900+
if (onlyGeneric) {
901+
return false;
902+
}
869903
const confidence = captcha.confidence ?? 0;
870904
const minConfidence = options.minConfidence ?? DEFAULT_CAPTCHA_OPTIONS.minConfidence;
871905
return confidence >= minConfidence;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { AgentRuntime } from '../src/agent-runtime';
2+
import { TraceSink } from '../src/tracing/sink';
3+
import { Tracer } from '../src/tracing/tracer';
4+
import { CaptchaDiagnostics, Snapshot } from '../src/types';
5+
import { MockPage } from './mocks/browser-mock';
6+
7+
class MockSink extends TraceSink {
8+
public events: any[] = [];
9+
emit(event: Record<string, any>): void {
10+
this.events.push(event);
11+
}
12+
async close(): Promise<void> {
13+
// no-op
14+
}
15+
getSinkType(): string {
16+
return 'MockSink';
17+
}
18+
}
19+
20+
function makeRuntime(): AgentRuntime {
21+
const sink = new MockSink();
22+
const tracer = new Tracer('test-run', sink);
23+
const page = new MockPage('https://example.com') as any;
24+
const browserLike = {
25+
snapshot: async () => ({
26+
status: 'success',
27+
url: 'https://example.com',
28+
elements: [],
29+
timestamp: 't1',
30+
}),
31+
};
32+
const runtime = new AgentRuntime(browserLike as any, page as any, tracer);
33+
runtime.setCaptchaOptions({ minConfidence: 0.1, policy: 'abort' });
34+
return runtime;
35+
}
36+
37+
function makeSnapshot(captcha: CaptchaDiagnostics): Snapshot {
38+
return {
39+
status: 'success',
40+
url: 'https://example.com',
41+
elements: [],
42+
diagnostics: { captcha },
43+
timestamp: 't1',
44+
};
45+
}
46+
47+
describe('AgentRuntime captcha detection', () => {
48+
it('ignores passive recaptcha badges', () => {
49+
const runtime = makeRuntime();
50+
const captcha: CaptchaDiagnostics = {
51+
detected: true,
52+
confidence: 0.95,
53+
provider_hint: 'recaptcha',
54+
evidence: {
55+
iframe_src_hits: ['https://www.google.com/recaptcha/api2/anchor?ar=1'],
56+
selector_hits: [],
57+
text_hits: [],
58+
url_hits: [],
59+
},
60+
};
61+
62+
const detected = (runtime as any).isCaptchaDetected(makeSnapshot(captcha));
63+
expect(detected).toBe(false);
64+
});
65+
66+
it('detects interactive captcha challenges', () => {
67+
const runtime = makeRuntime();
68+
const captcha: CaptchaDiagnostics = {
69+
detected: true,
70+
confidence: 0.95,
71+
provider_hint: 'recaptcha',
72+
evidence: {
73+
iframe_src_hits: [],
74+
selector_hits: [],
75+
text_hits: ["I'm not a robot"],
76+
url_hits: [],
77+
},
78+
};
79+
80+
const detected = (runtime as any).isCaptchaDetected(makeSnapshot(captcha));
81+
expect(detected).toBe(true);
82+
});
83+
});

0 commit comments

Comments
 (0)