From 1dd4d5ef7821f11016410a22e082a1be4666d2b6 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 17 Jul 2026 16:43:54 -0700 Subject: [PATCH] [eric] browser: login-wall guard so the scripted send never fills an auth page --- .../agents/browser/browser_send_script.py | 36 ++++++++++++++++++- backend/tests/test_browser_send_script.py | 29 +++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/backend/apps/agents/browser/browser_send_script.py b/backend/apps/agents/browser/browser_send_script.py index 2513cc7d..bf619fa0 100644 --- a/backend/apps/agents/browser/browser_send_script.py +++ b/backend/apps/agents/browser/browser_send_script.py @@ -37,6 +37,31 @@ P_COMPOSER_NAME_RE = re.compile( re.I, ) +# Login/auth walls: a logged-out card lands here, and the structural reveal-finder would +# otherwise fill a login field and arm the page's own submit as a "send" (measured live on +# instagram/threads). A real composer never lives on one of these, so decline outright. +P_LOGIN_WALL_URL_RE = re.compile( + r"accounts\.google\.com|/i/flow/login|/accounts/login|/uas/login|/users/sign_in|" + r"/sessions/new|/checkpoint|force_authentication|" + r"/(?:log[_-]?in|sign[_-]?in|signin|logon)(?:[/?#]|$)", + re.I, +) +P_LOGIN_WALL_STATE_RE = re.compile( + r'<\s*textbox\s+"[^"]*(?:password|passwd)|(?:log|sign)\s?in to |' + r"continue with (?:google|apple|facebook)", + re.I, +) + + +def looks_like_login_wall(current_url: str, state_text: str) -> bool: + """A login/auth page (by URL) or an auth form in the perception (a password field, a + 'Log in to X' heading, an OAuth 'Continue with ...'). The scripted send declines here: + a real composer never shares a page with these, and filling here types a login field.""" + if current_url and P_LOGIN_WALL_URL_RE.search(current_url): + return True + return bool(state_text and P_LOGIN_WALL_STATE_RE.search(state_text)) + + ToolRunner = Callable[[str, dict, str, str], Awaitable[dict]] @@ -187,6 +212,9 @@ async def run_send_script( if P_READONLY_RE.search(task) or P_READONLY_RE.search(payload_source or ""): logger.info("[browser-sendscript] decline: read-only directive in task") return None + if looks_like_login_wall(current_url, state_text): + logger.info(f"[browser-sendscript] decline: login/auth wall ({(current_url or '')[:60]!r})") + return None payload = quoted_payload(payload_source or task) if not payload: logger.info("[browser-sendscript] decline: no unambiguous quoted payload") @@ -255,7 +283,13 @@ async def run_send_script( break logger.info("[browser-sendscript] reveal navigated (open-first); re-perceiving the destination") await asyncio.sleep(1.5) - await fresh_list() + dest = await fresh_list() + # open-first can land on a login redirect (a logged-out feed's first item); + # stop before the NEXT fill so we never type into the auth form we just opened. + if looks_like_login_wall("", dest): + logger.info("[browser-sendscript] decline: reveal landed on a login/auth wall") + fc = {} + break if isinstance(fc, dict) and fc.get("found") and fc.get("filled"): p_struct_selector = str(fc.get("selector") or "") logger.info(f"[browser-sendscript] structural composer role={fc.get('role')!r} " diff --git a/backend/tests/test_browser_send_script.py b/backend/tests/test_browser_send_script.py index 3c758ab1..9f1eb42a 100644 --- a/backend/tests/test_browser_send_script.py +++ b/backend/tests/test_browser_send_script.py @@ -224,3 +224,32 @@ async def test_readonly_probe_never_fires(): payload_source=TASK, current_url=THREAD_URL) assert r is None assert not calls["clicks"] + + +def test_looks_like_login_wall_hits_and_false_positives(): + # login/auth URLs (the live instagram/threads mis-fire was one of these) + assert ss.looks_like_login_wall("https://www.instagram.com/accounts/login/?force_authentication", "") + assert ss.looks_like_login_wall("https://accounts.google.com/v3/signin/identifier", "") + assert ss.looks_like_login_wall("https://www.reddit.com/login/", "") + assert ss.looks_like_login_wall("https://x.com/i/flow/login", "") + # auth-form perception signals with an innocuous url + assert ss.looks_like_login_wall("https://site.com/x", '[3]') + assert ss.looks_like_login_wall("https://site.com/x", "Log in to X to continue") + # false positives: a real composer page, a blog path, gmail inbox, a /author/ path + assert not ss.looks_like_login_wall("https://x.com/home", X_COMPOSER) + assert not ss.looks_like_login_wall("https://example.com/blog/login-tips", "") + assert not ss.looks_like_login_wall("https://mail.google.com/mail/u/0/#inbox?compose=new", "") + assert not ss.looks_like_login_wall("https://site.com/author/jane", "") + + +@pytest.mark.asyncio +async def test_login_wall_url_declines_before_any_fill(): + """A login URL declines even when the perception carries a composer and the task quotes a + payload: a real send surface never shares a page with a login wall, and filling here types + into the auth form (the live instagram/threads mis-fire under the reveal finder).""" + ex, calls = make_exec([X_COMPOSER]) + r = await ss.run_send_script('post this exactly: "hello from the test x9"', "b1", "", X_COMPOSER, ex, + send_submit_index_in_state, payload_in_textbox, + current_url="https://x.com/i/flow/login") + assert r is None + assert not calls["clicks"]