granclaw 0.0.1-beta.36 → 0.0.1-beta.38
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.
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* captcha-detect.ts
|
|
4
|
+
*
|
|
5
|
+
* A stringified IIFE we ship to `agent-browser eval` to decide whether the
|
|
6
|
+
* currently-open page is blocked behind some kind of bot/captcha wall. The
|
|
7
|
+
* runner polls this snippet after every navigation command so CapMonster and
|
|
8
|
+
* the stealth extension have a chance to clear the challenge before the
|
|
9
|
+
* agent starts reading the page.
|
|
10
|
+
*
|
|
11
|
+
* It covers:
|
|
12
|
+
* - Cloudflare JS interstitials ("Just a moment...", "Checking your browser…",
|
|
13
|
+
* "Attention Required"), including the common localised titles.
|
|
14
|
+
* - Cloudflare managed-challenge DOM markers (#challenge-stage,
|
|
15
|
+
* #challenge-running, .cf-browser-verification, .cf-im-under-attack).
|
|
16
|
+
* - The classic widget iframes: reCaptcha, hCaptcha, Cloudflare Turnstile,
|
|
17
|
+
* DataDome, Arkose/FunCaptcha.
|
|
18
|
+
* - Inline widgets: .g-recaptcha, .h-captcha, [class*="captcha"].
|
|
19
|
+
* - PerimeterX / HUMAN bot check: #px-captcha.
|
|
20
|
+
*
|
|
21
|
+
* IMPORTANT: this runs as a string inside Chrome, NOT in Node. No ES features
|
|
22
|
+
* newer than the detector's tested Chrome baseline, no TypeScript. Keep it
|
|
23
|
+
* ES5-flavoured so it stays safe if the agent-browser build ever targets an
|
|
24
|
+
* older chromium.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.CAPTCHA_DETECT_JS = void 0;
|
|
28
|
+
exports.CAPTCHA_DETECT_JS = `(function() {
|
|
29
|
+
var title = (document.title || '').toLowerCase();
|
|
30
|
+
var titlePatterns = [
|
|
31
|
+
'just a moment', // Cloudflare interstitial (EN)
|
|
32
|
+
'un momento', // CF Spanish
|
|
33
|
+
'un instant', // CF French
|
|
34
|
+
'einen moment', // CF German
|
|
35
|
+
'checking your browser', // CF older / generic anti-bot
|
|
36
|
+
'attention required', // CF WAF block page
|
|
37
|
+
'please wait', // generic "please wait while we check"
|
|
38
|
+
];
|
|
39
|
+
for (var i = 0; i < titlePatterns.length; i++) {
|
|
40
|
+
if (title.indexOf(titlePatterns[i]) !== -1) return 'captcha';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
var selectors = [
|
|
44
|
+
// Widget iframes
|
|
45
|
+
'iframe[src*="captcha-delivery"]', // DataDome
|
|
46
|
+
'iframe[src*="geo.captcha"]', // DataDome geo
|
|
47
|
+
'iframe[src*="recaptcha"]', // reCaptcha v2/v3
|
|
48
|
+
'iframe[src*="hcaptcha"]', // hCaptcha
|
|
49
|
+
'iframe[src*="challenges.cloudflare"]', // Cloudflare Turnstile
|
|
50
|
+
'iframe[src*="arkoselabs"]', // Arkose FunCaptcha
|
|
51
|
+
'iframe[src*="funcaptcha"]',
|
|
52
|
+
// Inline widgets
|
|
53
|
+
'.g-recaptcha',
|
|
54
|
+
'.h-captcha',
|
|
55
|
+
'[class*="captcha"]',
|
|
56
|
+
// Cloudflare interstitial / managed challenge
|
|
57
|
+
'#challenge-stage',
|
|
58
|
+
'#challenge-running',
|
|
59
|
+
'#cf-challenge-running',
|
|
60
|
+
'.cf-browser-verification',
|
|
61
|
+
'.cf-im-under-attack',
|
|
62
|
+
// PerimeterX / HUMAN
|
|
63
|
+
'#px-captcha',
|
|
64
|
+
];
|
|
65
|
+
for (var j = 0; j < selectors.length; j++) {
|
|
66
|
+
if (document.querySelector(selectors[j])) return 'captcha';
|
|
67
|
+
}
|
|
68
|
+
return 'clear';
|
|
69
|
+
})()`;
|
|
@@ -51,6 +51,7 @@ const schedules_db_js_1 = require("../schedules-db.js");
|
|
|
51
51
|
const cron_parser_1 = require("cron-parser");
|
|
52
52
|
const session_manager_js_1 = require("../browser/session-manager.js");
|
|
53
53
|
const stealth_js_1 = require("../browser/stealth.js");
|
|
54
|
+
const captcha_detect_js_1 = require("./captcha-detect.js");
|
|
54
55
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
55
56
|
/**
|
|
56
57
|
* Resolve the templates directory.
|
|
@@ -683,27 +684,19 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
683
684
|
// Privileged commands (record, close, tab close for session 0,
|
|
684
685
|
// session management) are rejected — the runtime owns those.
|
|
685
686
|
/**
|
|
686
|
-
* After a navigation command, check
|
|
687
|
-
*
|
|
687
|
+
* After a navigation command, check whether the page is blocked by any
|
|
688
|
+
* bot/captcha wall (widget captcha OR Cloudflare-style JS interstitial)
|
|
689
|
+
* and poll until CapMonster + the stealth extension have cleared it.
|
|
690
|
+
*
|
|
691
|
+
* Budget is 45s — Cloudflare's "Just a moment..." managed challenge often
|
|
692
|
+
* takes 15–20s on a cold datacenter IP, and CapMonster-solved widgets can
|
|
693
|
+
* take another pass, so shorter deadlines were causing real unblocks to
|
|
694
|
+
* be reported as "unsolved" (regression verified on mariados 2026-04-15).
|
|
695
|
+
*
|
|
688
696
|
* Returns 'clear' (no captcha / solved), 'unsolved' (still blocked after timeout).
|
|
689
697
|
*/
|
|
690
698
|
async function waitForCaptchaResolution(bin, sessionId) {
|
|
691
|
-
const
|
|
692
|
-
var patterns = [
|
|
693
|
-
'iframe[src*="captcha-delivery"]',
|
|
694
|
-
'iframe[src*="geo.captcha"]',
|
|
695
|
-
'iframe[src*="recaptcha"]',
|
|
696
|
-
'iframe[src*="hcaptcha"]',
|
|
697
|
-
'iframe[src*="challenges.cloudflare"]',
|
|
698
|
-
'.g-recaptcha',
|
|
699
|
-
'.h-captcha',
|
|
700
|
-
'[class*="captcha"]',
|
|
701
|
-
];
|
|
702
|
-
return patterns.some(function(s) {
|
|
703
|
-
return !!document.querySelector(s);
|
|
704
|
-
}) ? 'captcha' : 'clear';
|
|
705
|
-
})()`;
|
|
706
|
-
const evalArgv = (id) => ['--session', id, 'eval', CAPTCHA_JS];
|
|
699
|
+
const evalArgv = (id) => ['--session', id, 'eval', captcha_detect_js_1.CAPTCHA_DETECT_JS];
|
|
707
700
|
const check = async () => {
|
|
708
701
|
try {
|
|
709
702
|
const { stdout } = await execFileAsync(bin, evalArgv(sessionId), { timeout: 8_000 });
|
|
@@ -717,8 +710,8 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
717
710
|
await new Promise(r => setTimeout(r, 1500));
|
|
718
711
|
if (!await check())
|
|
719
712
|
return 'clear';
|
|
720
|
-
//
|
|
721
|
-
const deadline = Date.now() +
|
|
713
|
+
// Challenge detected — poll every 2s until the page clears or 45s elapses.
|
|
714
|
+
const deadline = Date.now() + 45_000;
|
|
722
715
|
while (Date.now() < deadline) {
|
|
723
716
|
await new Promise(r => setTimeout(r, 2_000));
|
|
724
717
|
if (!await check())
|
|
@@ -756,7 +749,7 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
756
749
|
'Do not call `record start`, `record stop`, `close`, or `session` — the runtime manages those.',
|
|
757
750
|
'You do not need to screenshot for audit — the whole session is recorded as video automatically.',
|
|
758
751
|
'Saved logins persist automatically when the user has set up a profile via the dashboard Browser view.',
|
|
759
|
-
'CAPTCHA handling:
|
|
752
|
+
'CAPTCHA & Cloudflare handling: the browser ships an automatic CapMonster extension plus a stealth extension. After `open` or `reload` the runtime automatically polls for up to ~45 seconds while any captcha widget OR Cloudflare JS interstitial ("Just a moment...", "Checking your browser…", "Attention Required") clears — do NOT bail out early, do NOT call request_human_browser_takeover just because the first snapshot shows the interstitial. If, after your tool call returns, the response still contains the "CAPTCHA detected and not auto-solved" warning, THEN call request_human_browser_takeover.',
|
|
760
753
|
'Examples: {"command":"open","args":["https://example.com"]}, {"command":"click","args":["--ref","e12"]}, {"command":"fill","args":["--ref","e5","Alice"]}',
|
|
761
754
|
],
|
|
762
755
|
parameters: {
|
|
@@ -831,7 +824,7 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
831
824
|
return {
|
|
832
825
|
content: [{
|
|
833
826
|
type: 'text',
|
|
834
|
-
text: out + '\n\n⚠️ CAPTCHA detected and not auto-solved after
|
|
827
|
+
text: out + '\n\n⚠️ CAPTCHA detected and not auto-solved after 45s. ' +
|
|
835
828
|
'Use request_human_browser_takeover to let the user solve it, or try a different URL.',
|
|
836
829
|
}],
|
|
837
830
|
};
|
|
@@ -6,17 +6,19 @@
|
|
|
6
6
|
*
|
|
7
7
|
* stealthArgv() returns an argv fragment for the `agent-browser` boot command:
|
|
8
8
|
*
|
|
9
|
-
* --
|
|
10
|
-
* Loads the GranClaw stealth MV3 extension. The extension
|
|
11
|
-
* runs at document_start in world=MAIN — before any page
|
|
12
|
-
* navigator.webdriver, UA, plugins, WebGL, canvas, audio
|
|
13
|
-
*
|
|
9
|
+
* --extension <dir>
|
|
10
|
+
* Loads the GranClaw stealth MV3 extension (repeatable). The extension
|
|
11
|
+
* content script runs at document_start in world=MAIN — before any page
|
|
12
|
+
* JS — patching navigator.webdriver, UA, plugins, WebGL, canvas, audio.
|
|
13
|
+
* MUST use --extension (not --args --load-extension=…): agent-browser's
|
|
14
|
+
* --args flag is NOT repeatable and silently drops all but the LAST value,
|
|
15
|
+
* which is how the stealth extension was leaking in production.
|
|
14
16
|
*
|
|
15
|
-
* --args --
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* --args "--disable-blink-features=AutomationControlled,--headless=new,…"
|
|
18
|
+
* ALL chromium flags must live inside a SINGLE --args value, comma-joined.
|
|
19
|
+
* agent-browser collapses repeated --args to the last one and splits each
|
|
20
|
+
* value on commas before forwarding to Chrome. --headless=new is required
|
|
21
|
+
* here because agent-browser does not auto-add it when --extension is set.
|
|
20
22
|
*
|
|
21
23
|
* --executable-path <path>
|
|
22
24
|
* Uses real installed Chrome/Chromium instead of Playwright's bundled
|
|
@@ -198,26 +200,40 @@ function detectChromeUA() {
|
|
|
198
200
|
function stealthArgv(options = {}) {
|
|
199
201
|
if (process.env.GRANCLAW_STEALTH_DISABLED === '1')
|
|
200
202
|
return [];
|
|
201
|
-
const argv = [
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
const argv = [];
|
|
204
|
+
// All Chromium command-line switches MUST live inside a single --args value.
|
|
205
|
+
// agent-browser's --args flag is NOT repeatable: only the LAST value survives,
|
|
206
|
+
// and that value is comma-split before being forwarded to Chrome. Emitting
|
|
207
|
+
// several --args flags was how every stealth switch except the last silently
|
|
208
|
+
// vanished in production, leaving navigator.webdriver detectable.
|
|
209
|
+
const chromeFlags = [
|
|
210
|
+
'--disable-blink-features=AutomationControlled',
|
|
211
|
+
// V8 heap memory API (performance.memory) — disabled by default in headless.
|
|
212
|
+
'--enable-precise-memory-info',
|
|
206
213
|
];
|
|
207
|
-
//
|
|
208
|
-
//
|
|
214
|
+
// agent-browser does not auto-add --headless=new once --extension is set,
|
|
215
|
+
// so headless contexts must request it explicitly. Headed previews must NOT
|
|
216
|
+
// include it (the orchestrator also passes --headed and Chrome refuses both).
|
|
217
|
+
if (options.headless !== false) {
|
|
218
|
+
chromeFlags.push('--headless=new');
|
|
219
|
+
}
|
|
220
|
+
argv.push('--args', chromeFlags.join(','));
|
|
221
|
+
// Override the UA using agent-browser's dedicated --user-agent flag.
|
|
222
|
+
// navigator.userAgent in Chrome 120+ has a non-configurable prototype
|
|
209
223
|
// property that JS Object.defineProperty cannot patch, so the only reliable
|
|
210
|
-
// fix is to set it at the browser level. We use the top-level flag rather
|
|
211
|
-
// --args because --args values are comma-
|
|
224
|
+
// fix is to set it at the browser level. We use the top-level flag rather
|
|
225
|
+
// than --args because --args values are comma-split and a UA string contains
|
|
212
226
|
// commas/spaces that would be mis-parsed.
|
|
213
227
|
const ua = detectChromeUA();
|
|
214
228
|
if (ua) {
|
|
215
229
|
argv.push('--user-agent', ua);
|
|
216
230
|
}
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
231
|
+
// Extensions go through agent-browser's dedicated (repeatable) --extension
|
|
232
|
+
// flag — NOT via --args --load-extension=… which is silently dropped by the
|
|
233
|
+
// single-value --args collapse described above.
|
|
234
|
+
if (exports.STEALTH_EXTENSION_DIR) {
|
|
235
|
+
argv.push('--extension', exports.STEALTH_EXTENSION_DIR);
|
|
236
|
+
}
|
|
221
237
|
const capmonsterKey = options.capmonsterKey
|
|
222
238
|
|| process.env.CAPMONSTER_KEY
|
|
223
239
|
|| process.env.CAPMONSTER_API_KEY;
|
|
@@ -225,12 +241,7 @@ function stealthArgv(options = {}) {
|
|
|
225
241
|
if (capmonsterKey && capmonsterEnabled) {
|
|
226
242
|
const capmonsterDir = capmonsterExtensionDir(capmonsterKey);
|
|
227
243
|
if (capmonsterDir)
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
if (extDirs.length > 0) {
|
|
231
|
-
const extList = extDirs.join(',');
|
|
232
|
-
argv.push('--args', `--load-extension=${extList}`);
|
|
233
|
-
argv.push('--args', `--disable-extensions-except=${extList}`);
|
|
244
|
+
argv.push('--extension', capmonsterDir);
|
|
234
245
|
}
|
|
235
246
|
const chrome = detectChromePath();
|
|
236
247
|
if (chrome) {
|
|
@@ -1397,7 +1397,7 @@ function createServer() {
|
|
|
1397
1397
|
'--session', req.params.id,
|
|
1398
1398
|
'--headed',
|
|
1399
1399
|
'--profile', profileDir,
|
|
1400
|
-
...(0, stealth_js_1.stealthArgv)(),
|
|
1400
|
+
...(0, stealth_js_1.stealthArgv)({ headless: false }),
|
|
1401
1401
|
'open', url,
|
|
1402
1402
|
];
|
|
1403
1403
|
execFileSync('agent-browser', argv, {
|