granclaw 0.0.1-beta.26 → 0.0.1-beta.27
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.
|
@@ -708,6 +708,11 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
708
708
|
}
|
|
709
709
|
if (!browserState.handle.recordingStarted) {
|
|
710
710
|
await (0, session_manager_js_1.startRecording)(browserState.handle);
|
|
711
|
+
// Re-inject stealth into whatever daemon was just booted by
|
|
712
|
+
// startBrowserRecording. Page.addScriptToEvaluateOnNewDocument
|
|
713
|
+
// registered here will run before every subsequent navigation
|
|
714
|
+
// on this page target, so stealth patches survive open commands.
|
|
715
|
+
void (0, stealth_js_1.injectStealthViaCdp)(agent.id, workspaceDir);
|
|
711
716
|
}
|
|
712
717
|
// Build argv: --session <id> [--profile <path>] [--extension ...] [--executable-path ...] <command> <args...>
|
|
713
718
|
// agent-browser only applies launch flags on the command that boots
|
|
@@ -727,6 +732,11 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
727
732
|
maxBuffer: 10 * 1024 * 1024,
|
|
728
733
|
});
|
|
729
734
|
(0, session_manager_js_1.appendCommand)(browserState.handle, `${command} ${args.join(' ')}`.trim());
|
|
735
|
+
// Re-inject stealth after 'open' — agent-browser may create a new
|
|
736
|
+
// page target for the navigation, and new targets don't inherit
|
|
737
|
+
// Page.addScriptToEvaluateOnNewDocument from previous targets.
|
|
738
|
+
if (command === 'open')
|
|
739
|
+
void (0, stealth_js_1.injectStealthViaCdp)(agent.id, workspaceDir);
|
|
730
740
|
const out = stdout.trim() || stderr.trim() || 'ok';
|
|
731
741
|
return { content: [{ type: 'text', text: out }] };
|
|
732
742
|
}
|
|
@@ -265,4 +265,20 @@
|
|
|
265
265
|
configurable: true,
|
|
266
266
|
});
|
|
267
267
|
});
|
|
268
|
+
|
|
269
|
+
// ── 13. screen dimensions ───────────────────────────────────────────────
|
|
270
|
+
// Headless Chrome defaults to 800×600 which is trivially detected.
|
|
271
|
+
// Spoof a common 1920×1080 desktop resolution.
|
|
272
|
+
safe(() => {
|
|
273
|
+
const W = 1920, H = 1080;
|
|
274
|
+
const props = {
|
|
275
|
+
width: { get: () => W, configurable: true },
|
|
276
|
+
height: { get: () => H, configurable: true },
|
|
277
|
+
availWidth: { get: () => W, configurable: true },
|
|
278
|
+
availHeight: { get: () => H - 40, configurable: true }, // taskbar ~40px
|
|
279
|
+
colorDepth: { get: () => 24, configurable: true },
|
|
280
|
+
pixelDepth: { get: () => 24, configurable: true },
|
|
281
|
+
};
|
|
282
|
+
Object.defineProperties(Screen.prototype, props);
|
|
283
|
+
});
|
|
268
284
|
})();
|
|
@@ -108,13 +108,22 @@ function detectChromePath() {
|
|
|
108
108
|
// ── argv builder ──────────────────────────────────────────────────────────────
|
|
109
109
|
/**
|
|
110
110
|
* Return the argv fragment for the `agent-browser` command that boots the
|
|
111
|
-
* daemon.
|
|
112
|
-
*
|
|
111
|
+
* daemon.
|
|
112
|
+
*
|
|
113
|
+
* --executable-path Use real Chrome when available (less flagged than
|
|
114
|
+
* Playwright's bundled build).
|
|
115
|
+
* --args Chrome-level launch flags:
|
|
116
|
+
* --disable-blink-features=AutomationControlled
|
|
117
|
+
* Removes navigator.webdriver = true, which is the
|
|
118
|
+
* #1 bot-detection signal set by Playwright/CDP.
|
|
119
|
+
*
|
|
120
|
+
* The User-Agent is handled separately in prewarmStealthDaemon via a
|
|
121
|
+
* two-phase boot (discover real UA → restart with --user-agent <patched>).
|
|
113
122
|
*/
|
|
114
123
|
function stealthArgv() {
|
|
115
124
|
if (process.env.GRANCLAW_STEALTH_DISABLED === '1')
|
|
116
125
|
return [];
|
|
117
|
-
const argv = [];
|
|
126
|
+
const argv = ['--args', '--disable-blink-features=AutomationControlled'];
|
|
118
127
|
const chrome = detectChromePath();
|
|
119
128
|
if (chrome) {
|
|
120
129
|
argv.push('--executable-path', chrome);
|
|
@@ -182,6 +191,9 @@ async function fetchPageTargets(browserCdpUrl) {
|
|
|
182
191
|
* Page.addScriptToEvaluateOnNewDocument.
|
|
183
192
|
* 2. Run it immediately on the current document via Runtime.evaluate so
|
|
184
193
|
* patches are live without a reload.
|
|
194
|
+
*
|
|
195
|
+
* User-Agent patching is handled at the Chrome level via --user-agent in
|
|
196
|
+
* prewarmStealthDaemon, so it applies to every page target automatically.
|
|
185
197
|
*/
|
|
186
198
|
function injectIntoPage(wsUrl, script) {
|
|
187
199
|
return new Promise((resolve) => {
|
|
@@ -202,7 +214,6 @@ function injectIntoPage(wsUrl, script) {
|
|
|
202
214
|
method: 'Runtime.evaluate',
|
|
203
215
|
params: { expression: script, returnByValue: false },
|
|
204
216
|
}));
|
|
205
|
-
// Allow a short window for Chrome to ack, then close.
|
|
206
217
|
setTimeout(() => { clearTimeout(timer); cleanup(ws); }, 400);
|
|
207
218
|
});
|
|
208
219
|
ws.on('error', () => { clearTimeout(timer); resolve(); });
|
|
@@ -238,37 +249,98 @@ async function injectStealthViaCdp(sessionId, workspaceDir) {
|
|
|
238
249
|
}
|
|
239
250
|
}
|
|
240
251
|
// ── Daemon pre-warm ───────────────────────────────────────────────────────────
|
|
252
|
+
/**
|
|
253
|
+
* Fetch the raw User-Agent string from the browser's CDP /json/version endpoint.
|
|
254
|
+
* This reads the actual Chrome UA before any JS patches touch navigator.userAgent.
|
|
255
|
+
*/
|
|
256
|
+
async function fetchRawBrowserUA(cdpUrl) {
|
|
257
|
+
return new Promise((resolve) => {
|
|
258
|
+
const match = /^wss?:\/\/([^/]+)\//.exec(cdpUrl);
|
|
259
|
+
if (!match) {
|
|
260
|
+
resolve('');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const host = match[1];
|
|
264
|
+
const req = http_1.default.get(`http://${host}/json/version`, { timeout: 3000 }, (res) => {
|
|
265
|
+
let body = '';
|
|
266
|
+
res.on('data', (c) => { body += c.toString(); });
|
|
267
|
+
res.on('end', () => {
|
|
268
|
+
try {
|
|
269
|
+
const ua = JSON.parse(body)['User-Agent'] ?? '';
|
|
270
|
+
resolve(ua);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
resolve('');
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
req.on('error', () => resolve(''));
|
|
278
|
+
req.on('timeout', () => { req.destroy(); resolve(''); });
|
|
279
|
+
});
|
|
280
|
+
}
|
|
241
281
|
/**
|
|
242
282
|
* Pre-warm the agent's browser daemon and register stealth before any
|
|
243
283
|
* agent navigation. Call this once at agent process startup for agents
|
|
244
284
|
* that have the browser tool enabled.
|
|
245
285
|
*
|
|
246
|
-
*
|
|
247
|
-
* 1
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
286
|
+
* Two-phase boot:
|
|
287
|
+
* Phase 1 — Boot the daemon without a UA override to discover the real
|
|
288
|
+
* browser UA from /json/version (reading navigator.userAgent
|
|
289
|
+
* would give the JS-patched value, not the raw one).
|
|
290
|
+
* Phase 2 — Kill the daemon and restart it with:
|
|
291
|
+
* --user-agent <Chrome/X.Y.Z> strips "HeadlessChrome"
|
|
292
|
+
* --args --disable-blink-features=AutomationControlled
|
|
293
|
+
* removes navigator.webdriver = true for every page target
|
|
294
|
+
* Then inject Page.addScriptToEvaluateOnNewDocument so the
|
|
295
|
+
* JS stealth patches (WebGL, plugins, permissions…) apply to
|
|
296
|
+
* all subsequent navigations on the initial page target.
|
|
252
297
|
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* the live view relay re-attaches (which also injects stealth).
|
|
298
|
+
* Chrome-level flags apply globally to every page target the daemon creates,
|
|
299
|
+
* so they survive agent-browser open commands that spawn new targets.
|
|
256
300
|
*/
|
|
257
301
|
async function prewarmStealthDaemon(sessionId, workspaceDir) {
|
|
258
302
|
if (process.env.GRANCLAW_STEALTH_DISABLED === '1')
|
|
259
303
|
return;
|
|
304
|
+
const bin = process.env.AGENT_BROWSER_BIN ?? 'agent-browser';
|
|
305
|
+
// ── Phase 1: boot daemon to discover the real browser UA ─────────────────
|
|
306
|
+
try {
|
|
307
|
+
await execFileAsync(bin, ['--session', sessionId, 'open', 'about:blank'], { cwd: workspaceDir, timeout: 15000 });
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return; // No Chrome / wrong env — skip silently.
|
|
311
|
+
}
|
|
312
|
+
let patchedUA = '';
|
|
313
|
+
const cdpUrl = await discoverCdpUrl(sessionId, workspaceDir);
|
|
314
|
+
if (cdpUrl) {
|
|
315
|
+
const rawUA = await fetchRawBrowserUA(cdpUrl);
|
|
316
|
+
if (rawUA.includes('HeadlessChrome/')) {
|
|
317
|
+
patchedUA = rawUA.replace('HeadlessChrome/', 'Chrome/');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Kill the daemon so we can restart it with corrected launch flags.
|
|
321
|
+
try {
|
|
322
|
+
await execFileAsync(bin, ['--session', sessionId, 'close', '--all'], { cwd: workspaceDir, timeout: 5000 });
|
|
323
|
+
}
|
|
324
|
+
catch { /* ignore — daemon may already be gone */ }
|
|
325
|
+
// ── Phase 2: restart with stealth Chrome flags ────────────────────────────
|
|
326
|
+
// stealthArgv() includes --args --disable-blink-features=AutomationControlled
|
|
327
|
+
// (fixes navigator.webdriver) and --executable-path when real Chrome is found.
|
|
328
|
+
// Persist the patched UA as a process-level env var so that every subsequent
|
|
329
|
+
// agent-browser call in this process (including startBrowserRecording's
|
|
330
|
+
// `record start`) inherits the correct UA even if the daemon restarts.
|
|
331
|
+
if (patchedUA)
|
|
332
|
+
process.env.AGENT_BROWSER_USER_AGENT = patchedUA;
|
|
333
|
+
const launchArgs = ['--session', sessionId, ...stealthArgv()];
|
|
334
|
+
if (patchedUA)
|
|
335
|
+
launchArgs.push('--user-agent', patchedUA);
|
|
336
|
+
launchArgs.push('open', 'about:blank');
|
|
260
337
|
try {
|
|
261
|
-
|
|
262
|
-
// Boot the daemon (or no-op if already running) and land on about:blank.
|
|
263
|
-
// Use execFileAsync with a short timeout — we don't care about the result,
|
|
264
|
-
// only that the daemon is now up.
|
|
265
|
-
await execFileAsync(bin, ['--session', sessionId, ...stealthArgv(), 'open', 'about:blank'], { cwd: workspaceDir, timeout: 15000 });
|
|
338
|
+
await execFileAsync(bin, launchArgs, { cwd: workspaceDir, timeout: 15000 });
|
|
266
339
|
}
|
|
267
340
|
catch {
|
|
268
|
-
// Daemon failed to start (no Chrome, wrong env, etc.) — skip silently.
|
|
269
341
|
return;
|
|
270
342
|
}
|
|
271
|
-
// Daemon is up. Register stealth for
|
|
343
|
+
// Daemon is up with correct flags. Register stealth JS for future navigations.
|
|
272
344
|
await injectStealthViaCdp(sessionId, workspaceDir);
|
|
273
345
|
}
|
|
274
346
|
// ── Test helpers ──────────────────────────────────────────────────────────────
|
|
@@ -51,6 +51,12 @@ function startAllAgents() {
|
|
|
51
51
|
const child = spawnAgent(agent, wsPort);
|
|
52
52
|
registry.set(agent.id, { config: agent, wsPort, bbPort: null, pid: child.pid });
|
|
53
53
|
console.log(`[orchestrator] agent "${agent.id}" started on ws port ${wsPort} (pid ${child.pid})`);
|
|
54
|
+
// Pre-warm the browser daemon for browser-capable agents so stealth is
|
|
55
|
+
// registered before the first navigation. Fire-and-forget.
|
|
56
|
+
if (agent.allowedTools?.includes('browser')) {
|
|
57
|
+
const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir);
|
|
58
|
+
void (0, stealth_js_1.prewarmStealthDaemon)(agent.id, workspaceDir);
|
|
59
|
+
}
|
|
54
60
|
});
|
|
55
61
|
}
|
|
56
62
|
function getManagedAgents() {
|
|
@@ -75,6 +81,10 @@ function restartAgent(agentId) {
|
|
|
75
81
|
const child = spawnAgent(agent, managed.wsPort);
|
|
76
82
|
registry.set(agentId, { config: agent, wsPort: managed.wsPort, bbPort: null, pid: child.pid });
|
|
77
83
|
console.log(`[orchestrator] agent "${agentId}" restarted (pid ${child.pid})`);
|
|
84
|
+
if (agent.allowedTools?.includes('browser')) {
|
|
85
|
+
const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir);
|
|
86
|
+
void (0, stealth_js_1.prewarmStealthDaemon)(agent.id, workspaceDir);
|
|
87
|
+
}
|
|
78
88
|
}
|
|
79
89
|
/**
|
|
80
90
|
* Start a new agent. Assigns the next available port.
|