Skip to content

Account-chooser hardening: capture is declared but unreachable, and a sign-in page reports as a chooser #773

Open

Description

@ffroliva

Four findings from taking over #764, split out to keep that PR reviewable. All
four were verified against the code; where a claim is inspection-only it says so.
Ranked by how much each one unblocks.

Context that makes item 1 matter more than it looks: the chooser state cannot be
staged locally
. Two cookie-surgery hypotheses were tried and refuted — deleting
only the Flow session cookie yields FlowSessionOutcome.GOOGLE_SESSION_ONLY and no
chooser; keeping only ACCOUNT_CHOOSER/__Host-GAPS and dropping every Google auth
cookie also yields no chooser. Both were predictable in hindsight: when the chooser
did appear live on 2026-09-09, the log showed flow_session_cookie_present=True,
expired=False, SAPISID present. Google decides this server-side. So the only route
to evidence is capturing it when it next happens on its own.


1. FlowAccountChooserError is declared capture-worthy and raised where capture cannot run (verified)

FlowAccountChooserError is in both _capture_triggers() and _screenshot_triggers()
(diagnostics.py:1850-1892) and incident_capture defaults True. But capture only
fires through _raise_with_incident, whose docstring calls it "the shared failure
boundary for client entry points" — and every call site is one (image_generation,
image_batch, image_upscale, video_generation, character_generation). The chooser
raises inside _bootstrap_and_resolve_locale (client.py:886), which is not an entry
point. Confirmed empirically: the live 2026-09-09 failure left no bundle in incidents/.

Why this is top of the list. No bundle and no screenshot for the one failure whose
DOM would settle whether [data-email] is Google's live anchor. That question is what
this whole feature rests on, and right now the only evidence for it is a single
accidental live run. A screenshot on the next occurrence answers it permanently.

It also gates items 3 and 4 below, which are currently unfalsifiable for the same reason.

2. A plain sign-in page is reported as an account chooser (verified)

The gate accepts any https accounts.google.com URL except the rejected-browser route
(client.py:806-821). Land on /v3/signin/identifier — no remembered accounts, just a
sign-in form — and the row count is 0, producing:

Account chooser displayed at ... but recorded account 'x' was not found among
selectable accounts.

That asserts a chooser listing other accounts when there is no chooser at all, and it
points at the wrong remediation: the operator needs to sign in, not pick differently.
#763 already names two distinct post-hop landings (/about and the chooser); this is a
third. Cheap to distinguish — a chooser has [data-email] rows, a sign-in page has none
— and testable offline today against the existing route-interception e2e harness.

3. --account mismatch leaves the wrong-account profile in place (verified)

cli.py:296-310 reports the mismatch (error text plus one auth.account_assert_failed
log line) and raises exit 38. Nothing quarantines, renames or records it. A later run
re-reads a profile authenticated as someone else with no persisted signal — and on a
billed product that is the wrong account paying. This was a deliberate "minimum" in
review round 4 of #764; recording it so the decision is revisitable rather than lost.

4. Single-pass assumption (inspection only, not reproduced)

_handle_account_chooser runs once per bootstrap. Google can chain hops — chooser →
consent → chooser. A second chooser after the first click is not handled and degrades
into the landing timeout. Unfalsifiable until item 1 lands.

5. The landing timeout is still an unmeasured number

timeout=30_000 at client.py:862 entered in 41c4ae20 as a bare literal with no
rationale, and it is exactly Playwright's default — which this repo already documents as
the cause of a 34s silent hang (incident #56), against a house style where
URL_SETTLE_TIMEOUT_MS ("8s made ffroliva setup take 11.2s") and
_LANG_SETTLE_TIMEOUT_MS (measured 2.26/2.49s) both cite measurements. Deliberately left
alone in #764: there is no evidence it is too short or too long, and guessing is what put
it there. The landing URL added in 56c71017 is what will produce that evidence.


Refs #763, #764. Sibling landing-state issue: #756.

Activity

  1. ffroliva commented on Sep 9, 2026

    @ffroliva
    OwnerAuthor

    Correction to item 1 — do NOT wire capture up as written

    Item 1 above recommends routing FlowAccountChooserError through incident capture to
    get a screenshot of the real chooser. That recommendation is wrong and should not be
    acted on.
    An xhigh code review caught it and I verified the reasoning.

    FlowAccountChooserError fires, by construction, only when page.url is on
    accounts.google.com. Capturing there writes a DOM dump and a full-page screenshot of a
    Google auth surface into the bundle — every signed-in identity's address, display name
    and avatar, and on a sign-in form the input and hidden-field DOM. That bundle is the
    artifact users are prompted to attach to GitHub issues.

    It also contradicts the codebase's own stated policy. diagnostics.py:1030 excludes
    AuthExpiredError from capture as "deterministic operator remediation; DOM adds nothing",
    and docs/DEBUGGING.md says "Never captured: ... ordinary AuthExpiredError".
    FlowAccountChooserError has the same remediation and the opposite treatment — it is
    currently in both _capture_triggers() and _screenshot_triggers().

    So the finding inverts: the defect is not that capture is unreachable, it is that the
    class is registered for capture at all. The unreachability is what has been
    protecting users from it. The fix is to remove it from both trigger lists, matching its
    policy twin.

    That leaves the open question — is [data-email] still Google's live anchor? — without
    the route I proposed. A redacted, structural answer would be needed instead: record which
    selector tier matched (attribute vs text fallback) and the row count, not the DOM. That
    is a separate design question and I am not folding it in here.

    Item 4 (single-pass assumption) said it was gated on item 1; it is not — it is gated on a
    live occurrence, same as item 5.

  2. ffroliva commented on Sep 10, 2026

    @ffroliva
    OwnerAuthor

    Verified on develop @ 132dc977 (the #764 merge): items 1 and 2 are already fixed, both by #764 itself. Recording it here so neither gets re-implemented.

    Item 1 — capture is unreachable → resolved as you redirected it

    Your correction was to remove FlowAccountChooserError from the trigger lists rather than route capture to it. That is what shipped:

    $ grep -c FlowAccountChooserError src/gflow_cli/diagnostics.py        # 2 (import + exclusion)
    $ sed -n "/def _capture_triggers/,/def _screenshot/p" ... | grep -c AccountChooser   # 0
    

    It is now in the ContentPolicyError, AuthExpiredError exclusion at diagnostics.py:1030, carrying the reasoning verbatim — "a bundle would carry a DOM dump and a full-page screenshot of a Google auth surface into the artifact users are prompted to attach to GitHub issues". git log -S puts that line in 132dc977.

    So the [data-email] question is still open, but the privacy defect is closed and the class now matches its policy twin.

    Item 2 — a sign-in page reported as a chooser → fixed

    client.py:830-836 no longer accepts every accounts.google.com landing. Two independent positive signals, either sufficient:

    on_chooser_path = parts.path.rstrip("/").endswith("accountchooser")
    if not on_chooser_path and await page.locator("[data-email]").count() == 0:
        return False

    Covered by tests/e2e/test_account_chooser_e2e.py::test_e2e_signin_page_is_not_reported_as_a_chooser.

    Still open

    • 3 (--account mismatch leaves the profile in place) — the deliberate round-4 minimum, unchanged.
    • 4 (single-pass assumption) — still gated on a live occurrence, as you noted.
    • 5 (the unmeasured timeout=30_000) — untouched.

    Related, and now fixed

    Item 2 is one of three sites where a known landing state was reported as the wrong thing. The other two — flow.google.com/about reported as UiSelectorDriftError (#756) and labs.google/fx/api/auth/signin?error=Callback reported as Could not find 'New project' CTA (the 2026-09-10 RED canary, #559) — are fixed together in bugfix/landing-state-not-drift, with a shared flow_landing_kind() next to flow_host_kind(). Item 2 needed no change there because #764 had already handled the accounts.google.com case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions