Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ by construction, so Codex / Cursor / Aider / `agy` read exactly what Claude Code

## Headed-browser dependency (architectural reality)

gflow-cli currently drives Flow via a **real Chrome session managed by Playwright** — `ui_automation` transport. Google's auth + reCAPTCHA stack rejects Playwright's bundled Chromium and most headless approaches. This is the project's defining trade-off:
gflow-cli currently drives Flow via a **real Chrome session managed by Playwright** — `ui_automation` transport. Google's auth + reCAPTCHA stack rejects browsers that advertise automation, and most headless approaches. This is the project's defining trade-off:

- ✅ Works end-to-end against live Google accounts.
- ❌ Requires a saved Chrome profile, a display server for one-time login, and ~150 MB for Chromium.
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
exit 23 — which told the user to file a frontend-drift bug about a frontend that was
behaving correctly.

### Changed

- **`gflow auth login` closes the browser for you.** It drives your real Google Chrome
through Playwright, watches for the completed Flow sign-in, and closes the window itself —
the "now close Chrome" step is gone. Closing the window yourself still works and still
verifies; it is not an error. On a machine where Playwright cannot resolve a Chrome
channel, or where Google rejects the browser anyway, login falls back automatically to the
previous flow (Chrome as a plain subprocess, you close the window). **There is no new flag
and nothing to choose.**
([spike](docs/superpowers/spikes/2026-09-08-g12-blocks-webdriver-not-playwright.md))

### Fixed

- **The second image in one session no longer falls back to the labs reCAPTCHA mint**
Expand All @@ -48,6 +59,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
driving only t2v and local-frame i2v, so an image refusal printed a `detail` saying
t2i/i2i are driven directly above a remediation saying they are not. Both it and the
class docstring now name the full ported matrix.
- **`gflow auth login --browser internal` launched a browser configuration measured as
rejected.** The bundled-Chromium path shipped with no anti-automation flags, which leaves
`navigator.webdriver` set. On 2026-09-08 a browser in that state — real Chrome with the
flags removed — was rejected at `/v3/signin/rejected` 17.5 s into the flow, while the same
browser *with* the flags signed in normally. Bundled Chromium was measured only in the
flagged configuration, so its unflagged rejection is inferred from the shared signal, not
observed directly. It now passes
`--disable-blink-features=AutomationControlled`, `ignore_default_args=["--enable-automation"]`
and `chromium_sandbox=True` — the last of which also removes Chrome's cosmetic *"You are
using an unsupported command-line flag"* banner — and signs in on the real OS window
instead of an emulated 1920×1080 viewport that pushed Google's sign-in form off-screen on
smaller or scaled displays.
- **Setting `CHROME_BINARY` no longer makes Playwright's `channel="chrome"` look resolvable
when it is not.** The availability check treated the variable as proof, passed, and then
failed at launch with *"Chromium distribution 'chrome' is not found"*. Playwright honours a
custom binary only via `executable_path=`, never via `channel=`, so the variable is now
ignored by that check (it still resolves a Chrome binary everywhere else).

## [0.71.1] — 2026-09-08

Expand Down
49 changes: 41 additions & 8 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1523,30 +1523,63 @@ End-to-end live-verified on the `ffroliva` profile across `9:16`, `16:9`, `1:1`,

### G12 "browser not secure" block — Google rejects automated sign-in

- **Status:** Resolved · **Severity:** Critical (blocked `gflow auth login`) · **Fixed in:** v0.6.0a2
- **Status:** Resolved · **Severity:** Critical (blocked `gflow auth login`) · **Fixed in:** v0.6.0a2 · **Mitigation reimplemented + re-measured:** 2026-09-08

Google's sign-in flow (`accounts.google.com/v3/signin/rejected`) detected Playwright's bundled Chromium as an automated browser and refused the login with no user-facing error.
Google's sign-in flow (`accounts.google.com/v3/signin/rejected`) rejects a browser that
advertises itself as automated, and refuses the login with no user-facing error.

**Root cause (timing race):** Without `--disable-blink-features=AutomationControlled`,
Blink's C++ engine sets `navigator.webdriver = true` as a non-configurable, non-writable
native property at Chrome startup — before any JavaScript (including `add_init_script`)
can run. The `Object.defineProperty` override silently fails. With the flag, the property
is never set; the JS override then works as belt-and-suspenders.

**Resolution:** `v0.6.0a2` adds `RealChromeStrategy` — a new auth strategy that launches
the system's real Google Chrome via Playwright's `channel="chrome"` with stealth flags.
**Resolution:** `gflow auth login` launches the system's real Google Chrome through
Playwright's `channel="chrome"` with `chromium_sandbox=True`, `no_viewport=True`, and both
stealth flags — `--disable-blink-features=AutomationControlled` and
`ignore_default_args=["--enable-automation"]`. Because gflow owns that browser it also
detects the completed Flow sign-in and closes the window itself; see
[docs/AUTHENTICATION.md](docs/AUTHENTICATION.md). When no Chrome channel resolves, or
Google rejects the browser anyway, login falls back automatically to launching Chrome as a
plain subprocess and waiting for you to close the window. There is no flag and no choice to
make, and closing the window yourself works on either path.

> **This entry described that Playwright implementation long before it existed.**
> It read *"`v0.6.0a2` adds `RealChromeStrategy` — launches the system's real Google Chrome
> via Playwright's `channel="chrome"` with stealth flags."* `src/gflow_cli/auth/real_chrome.py`
> was created at `eb0de133` (2026-07-19) as a bare `subprocess.Popen` passive capture, and
> `git log -S'channel="chrome"' -- src/gflow_cli/auth/` returned **zero** commits until the
> auto-close change. The paragraph above is the same shape restated deliberately as current
> fact, not the same accident left standing.

```bash
# Bypass G12 block explicitly:
# Ask for real Chrome explicitly:
gflow auth login --browser chrome

# Or rely on auto-detection (default behaviour; picks real Chrome if installed):
gflow auth login
```

A cosmetic "You are using an unsupported command-line flag" notice may appear briefly in
the Chrome window — this is harmless and can be dismissed. It is the accepted trade-off
for bypassing G12.
**The block is current Google behaviour — "Resolved" means the mitigation holds, not that
Google stopped.** Re-measured 2026-09-08 across three throwaway *unauthenticated* profiles,
each signed into by hand
([spike](docs/superpowers/spikes/2026-09-08-g12-blocks-webdriver-not-playwright.md)): a
browser advertising `navigator.webdriver === true` — real Chrome, no stealth flags — was
rejected at `/v3/signin/rejected` **17.5 s** into the flow, while the same real Chrome
*with* the flags reported `false`, never saw the rejection, and reached a Flow session
cookie at 59.4 s. Playwright's bundled Chromium with the flags passed too, so the binary is
not the discriminator; `navigator.webdriver` tracked the outcome in all three arms.

> **This is N=1 — do not read it as a capability claim.** One account, one Windows host, one
> residential IP, one Chrome build (`Chrome/149.0.0.0`), one day. Google's sign-in risk
> scoring varies with account age and IP reputation, so it does not predict CI, a VPS, or a
> fresh account. Every arm ran headed, so it says nothing about headless in either
> direction. Sign-in is also a different gate from generation's reCAPTCHA Enterprise check;
> a result on one does not move the other.

The Chrome window no longer shows the "You are using an unsupported command-line flag"
notice this entry used to warn about: that banner came from the `--no-sandbox` Playwright
injects by default, and `chromium_sandbox=True` stops the injection.

---

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ gflow character create --project <id> --name "Aria" --face-prompt "..." --body-p

Outputs land under `$GFLOW_CLI_OUTPUT_DIR`, or you can route them to S3, MinIO, or Google Cloud Storage with [`GFLOW_CLI_STORAGE_URI`](docs/EXTERNAL_STORAGE.md). The first call takes 30 to 90 seconds while Chromium warms up; later calls reuse the warm session.

> **Why `--browser chrome`?** Google rejects Playwright's bundled Chromium. The CLI fails fast with a friendly error (`AuthBrowserRejectedError`, exit code 14) if you pick anything else.
> **Why `--browser chrome`?** It is the only strategy that marks the profile as a real-Chrome profile, which is what later generation runs open it with. The default `auto` picks it whenever Chrome is installed — see [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md).

> **Installing from a local checkout?** `uv tool install <path>` **ignores `uv.lock`** and resolves dependencies from the `pyproject.toml` ranges, so it can hand you a Playwright build this project has never tested. Playwright ships the browser driver, and an untested minor can wedge a generation silently. Carry the locked version explicitly:
>
Expand Down
12 changes: 6 additions & 6 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ src/gflow_cli/auth/
└── strategies.py # (internal) shared Playwright helpers
```

**Why:** Google's bot-detection ("G12 block") rejects Playwright's bundled Chromium during `gflow auth login`. Launching the user's installed Google Chrome with `--disable-blink-features=AutomationControlled` plus a JS `add_init_script` that overrides `navigator.webdriver` bypasses detection. Two strategies are needed because the setup (persistent-context flags, Chrome binary path, stealth init) differs fundamentally between them.
**Why two strategies:** Google's "G12 block" keys on a browser that *advertises* automation (`navigator.webdriver`), not on which binary runs. Both strategies therefore build their launch options from the same `login_launch_kwargs()` helper — `--disable-blink-features=AutomationControlled` plus `ignore_default_args=["--enable-automation"]` — and differ only by `channel="chrome"`. They stay separate classes for what those options do not carry: `name` supplies the `source=` label on the session probe (a caller-supplied `"chrome"`/`"internal"` log value, never read back from a response), only the `chrome` strategy writes the `.gflow_browser_strategy` marker that `channel_for_profile()` reads later, and `factory.py` is a name→type registry that `--browser auto|chrome|internal` routes through.

**AuthStrategy Protocol** (`base.py`):
```python
Expand All @@ -94,10 +94,10 @@ class AuthStrategy(Protocol):
- `mode="internal"` — explicit `InternalChromiumStrategy`.

**RealChromeStrategy stealth design** (`real_chrome.py`):
- Uses a **Passive Capture** pattern: launches system Chrome via `subprocess.Popen` without any automation flags or remote-debugging ports.
- Provides a 100% clean browser process that Google's G12 block cannot detect.
- The CLI blocks on `proc.wait()`, prompting the user to complete the sign-in and **close the browser completely**.
- Post-close: performs a fast, headless `launch_persistent_context` probe to verify the `SAPISID` cookie was successfully captured.
- **Default — Playwright-owned Chrome.** Launches the system's real Google Chrome via Playwright's `channel="chrome"` with `chromium_sandbox=True`, `no_viewport=True`, `--disable-blink-features=AutomationControlled`, and `ignore_default_args=["--enable-automation"]`. What Google's G12 block keys on is a browser that *advertises* automation (`navigator.webdriver`), not the Playwright connection itself; with these flags the property is `false` and sign-in proceeds normally. See the G12 entry in [KNOWN_ISSUES.md](../KNOWN_ISSUES.md) for the 2026-09-08 measurement and its N=1 caveat.
- Because gflow owns that context, it polls the Flow session endpoint from it until the outcome is `AUTHENTICATED` and then **closes the browser itself** — the user is not asked to close anything. A user who closes the window anyway is routed to the same `verify_flow_profile` check, never to an error.
- **Automatic fallback — Passive Capture.** `login()` falls back on four conditions — a `headless=True` caller (the CLI exposes no such flag; this guards library callers), Playwright cannot resolve a Chrome channel (`browser_manager.is_playwright_chrome_channel_available()`), `launch_persistent_context` raises, or Google rejects the owned browser (`AuthBrowserRejectedError`, swallowed here rather than surfaced as exit 14) — and then silently falls back to the older shape: system Chrome via `subprocess.Popen` with no automation flags and no remote-debugging port, the CLI blocking on `proc.wait()` until the user closes the window. There is **no user-facing flag and no choice to make** — a Chromium-only host is never locked out of onboarding.
- Verification: both paths end in the same `verify_flow_profile` call after the browser is gone. That probe is **httpx-first** — it reads the profile's cookie store directly via `browser_cookie3` and only falls back to a headless `launch_persistent_context` when cookie decryption fails (DPAPI on Windows, keychain on macOS, libsecret on Linux). The default path additionally polls the same session contract *from the browser it owns*, which is what tells it when to close. Both write the `.gflow_browser_strategy = "chrome"` marker that `channel_for_profile()` later reads.
- Privacy guard: raises `SecurityError` if the resolved `profile_dir` is outside `GFLOW_CLI_HOME` — protects the user's primary system Chrome profile from being used as a session store.

**UiAutomationTransport (UI Mimicry)**:
Expand Down Expand Up @@ -424,7 +424,7 @@ This is the project's defining trade-off and the most valuable place an external

Google's auth + reCAPTCHA stack on `aisandbox-pa.googleapis.com` rejects:

1. **Playwright's bundled Chromium** — flagged by Google's bot detection on first request. The CLI fails fast with `AuthBrowserRejectedError` (exit code 14) when this happens.
1. **Browsers that advertise automation** — `navigator.webdriver = true` is what Google's bot detection flags, not the bundled binary itself; every launch path passes `--disable-blink-features=AutomationControlled` to keep it `false`. A sign-in that still lands on Google's rejection page raises `AuthBrowserRejectedError` (exit code 14) from the `internal` strategy; the `chrome` strategy retries on its no-automation subprocess path instead.
2. **Headless browsers** — same fingerprinting trips during the OAuth-consent flow.
3. **Bare HTTP clients** without the cookies + tokens minted by a real Chrome session — most endpoints return HTTP 401 or a reCAPTCHA challenge that can only be solved interactively.

Expand Down
34 changes: 27 additions & 7 deletions docs/AUTHENTICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,17 +179,37 @@ and the profile keeps the name `default`.
| Value | Browser used | When to use |
|---|---|---|
| `auto` (default) | Real Chrome if installed; falls back to internal | First choice for most users |
| `chrome` | System Google Chrome (**Passive Capture**) | Required to bypass "G12" blocks |
| `chrome` | System Google Chrome, driven by Playwright (auto-closes) | Required for a chrome-strategy profile |
| `internal` | Playwright's bundled Chromium | Fallback when Chrome isn't installed |

Override with the env var: `GFLOW_CLI_AUTH_BROWSER=chrome gflow auth login`

**Why `chrome` bypasses bot detection:** Playwright's default automation mode exposes
`navigator.webdriver = true` as a non-configurable native property. Google detects this
and redirects to `/v3/signin/rejected` (the "G12 block"). The `chrome` strategy
implements **Passive Capture**: it launches your real system Chrome as a 100% standard
process without any automation flags or debugging ports. You log in manually, close
the window, and `gflow` extracts the verified session from the profile.
`internal` now launches with the same anti-automation flags as `chrome` (it previously did
not, which was the configuration Google rejects). It stays a fallback rather than a
recommendation: a profile created by `internal` carries no `chrome` strategy marker, so
generation later opens it with bundled Chromium instead of your real Chrome.

**Why `chrome` bypasses bot detection:** what Google rejects is a browser that *advertises*
automation. Blink sets `navigator.webdriver = true` as a non-configurable native property
unless `--disable-blink-features=AutomationControlled` is passed, and Google redirects that
browser to `/v3/signin/rejected` (the "G12 block"). The `chrome` strategy launches your real
system Chrome through Playwright with that flag plus
`ignore_default_args=["--enable-automation"]`, `chromium_sandbox=True`, and
`no_viewport=True`, so `navigator.webdriver` is `false` and the sign-in proceeds normally.
The block itself is still live — re-measured 2026-09-08; see the G12 entry in
[KNOWN_ISSUES.md](../KNOWN_ISSUES.md) for the numbers and their N=1 caveat.

**You don't close the browser — gflow does.** Because gflow owns that Chrome window, it
watches for the completed Flow sign-in and closes the window itself, then prints the
verified account. If you close the window yourself it still works: gflow verifies the
profile exactly the same way and does not treat a manual close as an error.

**Automatic fallback, with nothing to choose.** If Playwright can't resolve a Chrome channel
on this machine (a Chromium-only Linux box, for instance), or Google rejects the browser
anyway, `gflow auth login` falls back to the earlier **Passive Capture** flow: Chrome
launched as a plain process with no automation flags and no debugging port, where you close
the window once the Flow editor has loaded and `gflow` extracts the verified session from the
profile. There is no flag and no prompt for this — the fallback simply happens.

**Privacy guard:** The `chrome` strategy strictly refuses to use any profile directory
outside `GFLOW_CLI_HOME`. This protects your primary system Chrome profile from
Expand Down
Loading