mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-05 17:27:42 +02:00
[eric] browser: detect soft signed-out sites so the sign-in pause fires instead of a composer miss
This commit is contained in:
@@ -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]}]")
|
||||
|
||||
Reference in New Issue
Block a user