[eric] browser: detect soft signed-out sites so the sign-in pause fires instead of a composer miss

This commit is contained in:
ciregenz
2026-07-26 22:33:57 -07:00
parent acab50115b
commit da886afcdb
4 changed files with 49 additions and 7 deletions
+3 -1
View File
@@ -1658,8 +1658,10 @@ async def run_browser_agent(
# in this card (the persistent partition keeps the session, so future runs won't ask
# again), then continue. At most one pause per domain per run; the model's own
# RequestHumanIntervention stays as the fallback for walls this detector misses.
# Soft signed-out (composer withheld behind a "Sign in") only counts once the agent has
# actually tried and is still stuck, so a first-turn glance can't raise a false prompt.
p_wall_dom = browser_login_handoff.login_wall_domain(
last_seen_url, "\n".join(attached_state_seen))
last_seen_url, "\n".join(attached_state_seen), allow_soft=(turn >= 2))
if p_wall_dom and p_wall_dom not in p_login_prompted:
p_login_prompted.add(p_wall_dom)
p_login_problem, p_login_instruction = browser_login_handoff.prompt_copy(p_wall_dom)
@@ -73,12 +73,21 @@ def record_login(url_or_host: str) -> None:
@typechecked
def login_wall_domain(current_url: str, state_text: str) -> Optional[str]:
def login_wall_domain(current_url: str, state_text: str, allow_soft: bool = False) -> Optional[str]:
"""The registrable domain of a login wall the agent is stuck on, or None. One definition of
'login wall', shared with the send-script's decline gate."""
if not browser_send_parse.looks_like_login_wall(current_url or "", state_text or ""):
return None
return registrable_domain(current_url) or None
'login wall', shared with the send-script's decline gate.
`allow_soft` additionally accepts a SOFT signed-out page: browsable, no auth form, composer
simply withheld behind a "Sign in" control (bsky/stackoverflow/tiktok). Those never match the
hard wall, so the run used to fail as "couldn't find the compose box" instead of offering the
one thing that fixes it. Off by default because this pause interrupts the user: the caller
turns it on only once the agent is demonstrably stuck, so a stray "Sign up" link on a page
we're actually signed into can't raise a spurious prompt."""
if browser_send_parse.looks_like_login_wall(current_url or "", state_text or ""):
return registrable_domain(current_url) or None
if allow_soft and browser_send_parse.looks_signed_out(state_text or ""):
return registrable_domain(current_url) or None
return None
@typechecked
@@ -60,6 +60,31 @@ def looks_like_login_wall(current_url: str, state_text: str) -> bool:
return bool(state_text and P_LOGIN_WALL_STATE_RE.search(state_text))
# SOFT signed-out: the site serves a browsable page with no auth form and no login URL, it just
# withholds the composer and offers a "Sign in" control (bsky, stackoverflow, tiktok, threads all
# behave this way). The hard-wall gate above sees nothing, so the run used to report "I couldn't
# find the compose box" when the truth was "you are not signed in", which is a different problem
# with a different fix. Only ever consulted AFTER a composer miss, so it cannot affect a success.
P_SIGNIN_AFFORDANCE_RE = re.compile(
r'<\s*(?:link|button)\s+"[^"]*(?:sign[_ -]?in|log[_ -]?in|sign[_ -]?up|create account|join now)',
re.I)
# Anything only a signed-IN page shows. Its presence vetoes the verdict, so a stray "Log in" on an
# authenticated page (a second product's promo) can't make us tell the user to sign in again.
P_SIGNED_IN_RE = re.compile(
r'(?:sign|log)[_ -]?out|your profile|account menu|my account|notifications|'
r'<\s*(?:link|button)\s+"[^"]*(?:profile|avatar|inbox)',
re.I)
def looks_signed_out(state_text: str) -> bool:
"""True when the page offers a way to sign IN and shows nothing only a signed-in user sees."""
if not state_text:
return False
if P_SIGNED_IN_RE.search(state_text):
return False
return bool(P_SIGNIN_AFFORDANCE_RE.search(state_text))
def is_readonly(text: str) -> bool:
"""A read-only directive ('verify whether', 'do not send') that must decline the scripted
send even with a quoted payload in hand. Keeps the regex private to this file."""
@@ -244,7 +244,13 @@ async def run_send_script(
else:
logger.info(f"[browser-sendscript] structural finder: no usable composer ({str(fc)[:120]})")
if not composer:
logger.info("[browser-sendscript] decline: no composer, opener, or structural editable")
# Name WHY. A site that withholds the composer because nobody is signed in is a
# different problem from one whose composer we failed to find, and only the first is
# fixable by the user (sign in once). Consulted only here, on the already-failed path.
if browser_send_parse.looks_signed_out(state_text):
logger.info("[browser-sendscript] decline: signed OUT (composer withheld, sign-in offered)")
else:
logger.info("[browser-sendscript] decline: no composer, opener, or structural editable")
return None
# No Send-button precondition: composer sites (LinkedIn) lazy-render Send only AFTER text commits, so it's resolved post-fill; never appearing = clean pre-click abort.
logger.info(f"[browser-sendscript] fill target {composer[1]!r} [{composer[0]}]")