mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] browser: let a hidden window take the bot challenge before the card loads a borrowed site
This commit is contained in:
@@ -16,6 +16,7 @@ BROWSER_CMD_TIMEOUTS = {
|
||||
"wait": 12.0, # smart-wait already caps itself well under this
|
||||
"perform_action": 35.0, # session-borrow shims pack navigate + wait + scrape into ONE command, so it needs more than navigate alone
|
||||
"find_composer": 30.0, # packs trigger + scroll-ladder + retop + open-first into ONE command; on the 15s default the last two tiers were unreachable and heavy pages died mid-ladder (measured: linkedin timed out 2 of 3 runs). The in-page routine self-caps well under this.
|
||||
"import_session": 40.0, # applies the borrowed cookies AND lets a hidden window sit through the site's bot challenge so the card inherits the clearance; warmBorrowedSession.js self-caps at 15+2.5+5s, and this must outlast that or the warm is killed mid-challenge and we throw away the whole point of it (same trap as find_composer).
|
||||
}
|
||||
BROWSER_CMD_REBROADCAST_S = 3.0
|
||||
# A CPU-starved renderer can briefly drop its WS (a missed heartbeat) and the frontend auto-reconnects a beat later; bridge that gap instead of hard-failing a live run into it. Short enough that a genuinely-closed window still fails quickly (and no LLM turns are ever burned waiting); long enough to ride out a reconnect even on a loaded machine.
|
||||
|
||||
@@ -236,6 +236,23 @@ async def test_wall_handoff_asks_a_human_once_the_door_borrow_did_not_take(monke
|
||||
browser_agent.p_signin_borrowed.discard("acme.example")
|
||||
|
||||
|
||||
def test_import_timeout_outlasts_the_hidden_window_warm():
|
||||
"""INVARIANT, and the second time this exact trap has bitten (see find_composer): the warm sits
|
||||
through the site's bot challenge inside the import command, so the command's timeout has to
|
||||
outlast the warm's own budget. Set them past each other and the window is killed mid-challenge,
|
||||
which throws away the entire reason the warm exists while still looking like a clean import."""
|
||||
from backend.apps.agents.core.ws_manager import BROWSER_CMD_TIMEOUTS, BROWSER_CMD_TIMEOUT_DEFAULT
|
||||
|
||||
with open(os.path.join(P_REPO_ROOT, "electron", "warmBorrowedSession.js"), encoding="utf-8") as fh:
|
||||
src = fh.read()
|
||||
budget_ms = sum(int(m) for m in re.findall(
|
||||
r"^const (?:LOAD_TIMEOUT_MS|SETTLE_MS|DESTROY_GRACE_MS) = (\d+);", src, re.M))
|
||||
assert budget_ms > 0, "warm budget constants not found; did warmBorrowedSession change shape?"
|
||||
timeout_s = BROWSER_CMD_TIMEOUTS.get("import_session", BROWSER_CMD_TIMEOUT_DEFAULT)
|
||||
assert timeout_s * 1000 > budget_ms, (
|
||||
f"import_session timeout {timeout_s}s must outlast the warm budget {budget_ms}ms")
|
||||
|
||||
|
||||
def test_agent_checks_the_opt_in_before_reading_anything():
|
||||
"""INVARIANT: the borrow helper must consult the setting FIRST. Pinned by source because the
|
||||
ordering is the whole consent story, and an innocent-looking reorder would start reading the
|
||||
|
||||
+10
-1
@@ -16,6 +16,7 @@ const BROWSER_PARTITION = 'persist:openswarm-browser';
|
||||
// token because we never borrow for it: its own sign-in is the thing the token exists to satisfy.
|
||||
const p_borrowedSessionDomains = new Set();
|
||||
const p_uaSwapLogged = new Set();
|
||||
const { warmBorrowedSession } = require('./warmBorrowedSession');
|
||||
|
||||
function bareChromeUserAgent(ua) {
|
||||
return String(ua || '').replace(/\s*(?:openswarm|Electron)\/\S+/gi, '').replace(/\s{2,}/g, ' ').trim();
|
||||
@@ -2897,7 +2898,15 @@ async function writePartitionCookies(domain, cookies) {
|
||||
// Borrowing the session and presenting as the browser that earned it are one decision, not two:
|
||||
// apply the cookies without the matching UA and the site refuses them.
|
||||
if (set > 0) p_borrowedSessionDomains.add(d);
|
||||
return { ok: set > 0, set, total: list.length };
|
||||
// Let a plain hidden window take the site's challenge before the card does. It shares this
|
||||
// partition, so whatever clearance it earns is already waiting when the card loads.
|
||||
let warmed = false;
|
||||
if (set > 0) {
|
||||
const ua = bareChromeUserAgent(session.fromPartition(BROWSER_PARTITION).getUserAgent());
|
||||
warmed = await warmBorrowedSession(BROWSER_PARTITION, `https://${d}/`, ua);
|
||||
console.log(`[borrowed-warm] ${d} warmed=${warmed}`);
|
||||
}
|
||||
return { ok: set > 0, set, total: list.length, warmed };
|
||||
}
|
||||
ipcMain.handle('set-partition-cookies', (_e, domain, cookies) => writePartitionCookies(domain, cookies));
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Warm a borrowed sign-in in a hidden window before the visible card loads the site.
|
||||
//
|
||||
// Measured 2026-07-27: transplanting the user's own Chrome session into the browser partition works
|
||||
// (claude.ai accepted it and showed the real account name and plan), but sites behind a serious
|
||||
// anti-bot edge refuse it in a browser CARD: chatgpt.com, medium.com and instagram.com all had the
|
||||
// cookies applied and the user agent matched and still reported signed-out. The decisive clue is
|
||||
// that onboarding's harvest beats Cloudflare on chatgpt.com with those SAME cookies, and the only
|
||||
// thing it does differently is load them in a plain hidden BrowserWindow instead of a <webview>
|
||||
// guest. A guest carries a preload and the automation tells that come with being embedded; a hidden
|
||||
// window is just a browser.
|
||||
//
|
||||
// So we let the context that passes do the handshake. The hidden window shares the card's partition,
|
||||
// which means any clearance it earns lands in the same cookie jar the card is about to use. The
|
||||
// card then arrives already cleared instead of being challenged on its first request.
|
||||
//
|
||||
// Main-process only (an offscreen BrowserWindow is not a renderer webview). Always destroys its
|
||||
// window in a finally, so a failure can never leak one, and never throws: a warm that does not work
|
||||
// just leaves the card exactly as it would have been.
|
||||
const { BrowserWindow } = require('electron');
|
||||
|
||||
const LOAD_TIMEOUT_MS = 15000;
|
||||
// Anti-bot edges run a JS challenge after the document lands; the clearance cookie is only written
|
||||
// once that finishes, so returning at load time would throw away the entire point of doing this.
|
||||
const SETTLE_MS = 2500;
|
||||
const DESTROY_GRACE_MS = 5000;
|
||||
|
||||
async function warmBorrowedSession(partition, url, userAgent) {
|
||||
let win = null;
|
||||
try {
|
||||
win = new BrowserWindow({
|
||||
show: false,
|
||||
width: 1280,
|
||||
height: 900,
|
||||
webPreferences: {
|
||||
partition,
|
||||
sandbox: true,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const killer = setTimeout(() => {
|
||||
try { if (win && !win.isDestroyed()) win.destroy(); } catch { /* already gone */ }
|
||||
}, LOAD_TIMEOUT_MS + SETTLE_MS + DESTROY_GRACE_MS);
|
||||
|
||||
try {
|
||||
// Passed to loadURL, not just setUserAgent: the popup-UA spoofer in main.js rewrites any
|
||||
// contents of type 'window' during construction, and the per-load option is what wins.
|
||||
if (userAgent) {
|
||||
try { win.webContents.setUserAgent(userAgent); } catch { /* the load option still carries it */ }
|
||||
}
|
||||
const opts = userAgent ? { userAgent } : undefined;
|
||||
// loadURL rejects when any sub-resource aborts even though the main frame is fine, so a
|
||||
// rejection here is noise; what matters is that the challenge got time to run.
|
||||
const load = win.loadURL(url, opts).catch(() => {});
|
||||
await Promise.race([load, new Promise((r) => setTimeout(r, LOAD_TIMEOUT_MS))]);
|
||||
await new Promise((r) => setTimeout(r, SETTLE_MS));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
clearTimeout(killer);
|
||||
try { if (win && !win.isDestroyed()) win.destroy(); } catch { /* already gone */ }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { warmBorrowedSession, LOAD_TIMEOUT_MS, SETTLE_MS, DESTROY_GRACE_MS };
|
||||
Reference in New Issue
Block a user