Revamp login guards into general per-firm page guards
Replace the single hardcoded signed-out check with a guards list on each firm: any guard can trip mid-run via a URL substring (checked on every step) or a selector probe (checked when a step fails). Each guard defines recovery steps or a human-wait fallback, a per-run trigger cap, and a resume policy — 'continue' retries the tripped step, 'restart' rewinds the run to step 1. Runner bumped to 0.20.0; the server still ships the legacy signedOut/authSteps shape for stale runners, and a new runner synthesizes a guard from a legacy payload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
82b98b9d2d
commit
b1dccd7dec
+38
-19
@@ -15,7 +15,8 @@ class StepError(Exception):
|
||||
"""A step refused to run, or ran and could not be verified.
|
||||
|
||||
`code` mirrors the CLI's exit codes: 3 = refused before acting,
|
||||
4 = acted but verification failed, 5 = the session is signed out.
|
||||
4 = acted but verification failed, 5 = a page guard tripped (login
|
||||
redirect, blocking modal, and the like).
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, code: int = 3):
|
||||
@@ -23,33 +24,44 @@ class StepError(Exception):
|
||||
self.code = code
|
||||
|
||||
|
||||
class SignedOutError(StepError):
|
||||
"""The tab is on the login page, so the session has expired.
|
||||
class GuardTripped(StepError):
|
||||
"""A page guard's condition is on the page — the run cannot proceed past it.
|
||||
|
||||
Distinct from an ordinary failure because nothing is wrong with the
|
||||
automation — it just cannot proceed until a human signs in.
|
||||
Distinct from an ordinary failure because nothing is wrong with the step:
|
||||
the page has been taken over by something (a login redirect, an expired-
|
||||
session modal) and every selector now resolves against the wrong document.
|
||||
A click or a keystroke aimed at the old page would land on whatever the
|
||||
guard page happens to render in the same place. The runner decides how to
|
||||
recover; the CLI just stops with exit code 5.
|
||||
"""
|
||||
|
||||
def __init__(self, url: str):
|
||||
def __init__(self, guard: dict, url: str):
|
||||
name = guard.get("label") or guard.get("id") or "page"
|
||||
super().__init__(
|
||||
f"signed out — the tab is on {url}. Log in to the site, then run this again.",
|
||||
f"{name} guard tripped — the tab is on {url}. "
|
||||
"Deal with the page, then run this again.",
|
||||
code=5,
|
||||
)
|
||||
self.guard = guard
|
||||
self.url = url
|
||||
|
||||
|
||||
def check_signed_in(found: dict, signed_out: str) -> None:
|
||||
"""Abort if the page redirected to a login screen.
|
||||
def check_guards(found: dict, guards: list | None) -> None:
|
||||
"""Abort if the page has tripped any URL guard.
|
||||
|
||||
Worth doing before every action, not just at the start: a session can expire
|
||||
mid-run, and once it does, every selector resolves against a login form. A
|
||||
click or a keystroke aimed at the old page would land on whatever that form
|
||||
happens to render in the same place.
|
||||
Worth doing before every action, not just at the start: a session can
|
||||
expire (or a guard page appear) mid-run. Only URL guards are checked here —
|
||||
the URL rides along with every locate answer, so they are free. Selector
|
||||
guards cost a locate round trip each and are probed by the runner only once
|
||||
a step has already failed.
|
||||
"""
|
||||
if not signed_out:
|
||||
if not guards:
|
||||
return
|
||||
url = found.get("url") or ""
|
||||
if signed_out in url:
|
||||
raise SignedOutError(url)
|
||||
for guard in guards:
|
||||
pattern = guard.get("urlContains") or ""
|
||||
if pattern and pattern in url:
|
||||
raise GuardTripped(guard, url)
|
||||
|
||||
|
||||
# Display scaling factors that actually exist. Anything else means the two sides
|
||||
@@ -117,6 +129,7 @@ def perform(
|
||||
url: str = "",
|
||||
open_url: str = "",
|
||||
signed_out: str = "",
|
||||
guards: list | None = None,
|
||||
frame: str = "",
|
||||
text: str | None = None,
|
||||
clear: bool = False,
|
||||
@@ -142,6 +155,12 @@ def perform(
|
||||
rng = rng or random.Random()
|
||||
say = report or (lambda _m: None)
|
||||
|
||||
# `guards` is the runner's list of page guards; `signed_out` is the CLI's
|
||||
# single --signed-out pattern. Same check either way, so fold them together.
|
||||
guards = list(guards or [])
|
||||
if signed_out:
|
||||
guards.append({"id": "signed-out", "label": "signed out", "urlContains": signed_out})
|
||||
|
||||
# `frame` names the <iframe> the element lives in; the extension resolves the
|
||||
# selector inside it and offsets the coordinates back. Absent for the common
|
||||
# top-document case, and then locate behaves exactly as before.
|
||||
@@ -150,7 +169,7 @@ def perform(
|
||||
found = dash.locate(selector, index, url, timeout, open_url, options=frame_opts)
|
||||
|
||||
# Before any check that assumes we are on the real page.
|
||||
check_signed_in(found, signed_out)
|
||||
check_guards(found, guards)
|
||||
|
||||
# Give a transient overlay a chance to finish leaving before giving up on it.
|
||||
if found.get("covered") and not force:
|
||||
@@ -160,7 +179,7 @@ def perform(
|
||||
while found.get("covered") and time.time() < deadline:
|
||||
time.sleep(COVERED_POLL_SECONDS)
|
||||
found = dash.locate(selector, index, url, timeout, open_url, options=frame_opts)
|
||||
check_signed_in(found, signed_out)
|
||||
check_guards(found, guards)
|
||||
if not found.get("covered"):
|
||||
say("it cleared")
|
||||
|
||||
@@ -231,7 +250,7 @@ def perform(
|
||||
# Typing into a field that never took focus, or that ignores the input, fails
|
||||
# silently and is otherwise indistinguishable from success.
|
||||
after = dash.locate(selector, index, url, timeout, open_url, options=frame_opts)
|
||||
check_signed_in(after, signed_out)
|
||||
check_guards(after, guards)
|
||||
got = after.get("value")
|
||||
if got is None:
|
||||
say("element exposes no value to verify against")
|
||||
|
||||
Reference in New Issue
Block a user