Skip to content
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **An unported image form is refused by name, on a fresh or a reused client (#891).**
The browser transport no longer pre-mints a reCAPTCHA token for images: Flow's page
mints its own, and the client's token was never read. After a successful image the
pre-mint ran on the parked `about:blank` page and turned exit 36 into a misleading
`RecaptchaError`. The refusal now comes from the composer and names the form, e.g.
"the IMAGEN_3_5 model is not ported yet". Reachable from `gflow run --config` with a
per-prompt `imagen4` after a successful prompt: that run used to crash with exit 1 and
lose its results table; now the prompt fails by name and the run completes. This
corrects the 0.78.0 note that project-less image runs "keep the served host": the
client creates the project first, so they are served by the migrated composer. The HTTP
image transports still mint.
- **A clip whose download connection dropped is no longer recorded as never generated
(#896, #898).** When Flow reports a video done but the signed-media connection drops
on every retry, the catalog now marks the asset `MEDIA_GENERATION_STATUS_SUCCESSFUL`
Expand Down
1 change: 1 addition & 0 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ close Chrome yourself. On the measured account, `/about` was gone on the next ru
- **`--reference-entity` was refused on `r2v` but not on `t2v`** ([#716](https://github.com/ffroliva/gflow-cli/issues/716), fixed in v0.71.0): the "not ported, exit 36" refusal above sat inside the r2v branch of the routing gate, so a `t2v` request carrying a character entity returned from that gate without its entities ever being inspected — and nothing downstream attaches one on this host. The generation was **submitted and billed** with the entity silently dropped, returning a plausible clip of the wrong person. The check is now mode-independent and ahead of every early return. If you ran `gflow video t2v @Name …` or `--reference-entity` on a moved account before this fix, the identity in those clips was never bound.
- **The migrated composer image path is now driven** ([#692](https://github.com/ffroliva/gflow-cli/issues/692)): the first probe established a hit-testable Image mode; the 2026-09-08 follow-up captured real T2I and local-file I2I submissions on `ogiZ0b`, including page-owned reCAPTCHA, upload ids, response records and signed JPEG downloads. See [the submit-wire spike](docs/superpowers/spikes/2026-09-08-migrated-image-submit-wire.md).
- **Image commands on a moved account** ([#673](https://github.com/ffroliva/gflow-cli/issues/673), fixed in v0.69.0): through v0.68.0 `image t2i` / `i2i` (and `upscale`, `extend`) died with exit 1 `RecaptchaError` within seconds, before any submit, instead of the exit 36 above, because the labs client minted the reCAPTCHA token on the `flow.google.com` project grid before any migration guard ran. The guard now runs at the mint. If you still see a `RecaptchaError` on a moved account right after `auth login`, that is the labs logged-out landing page from the [#644](https://github.com/ffroliva/gflow-cli/issues/644) cookie harvest, not this.
- **A second image request on one client hit the same `RecaptchaError`** ([#891](https://github.com/ffroliva/gflow-cli/issues/891), fixed after v0.79.1): after a successful image on an account served flow.google.com, the page is parked on `about:blank`, and the client's pre-mint ran there, so an unported form (e.g. `--model imagen4`) failed with "the Flow editor page may have failed to load" instead of exit 36. The browser transport never used that token, so it no longer mints; the composer refuses the form by name. Reachable from `gflow run --config` with a per-prompt `"model": "imagen4"` after a successful prompt, where it also crashed the whole run (exit 1, results table lost); it now reports that prompt as exit 36 and the run completes.

Google is moving Flow off Labs onto its own origin. On a migrated page load,
`https://labs.google/fx/tools/flow/project/<id>` redirects to
Expand Down
2 changes: 1 addition & 1 deletion docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1751,7 +1751,7 @@ gflow run --config FILE [--output-dir DIR] [--profile NAME] [--continue-on-error

The config is a JSON file with a top-level `prompts` array; each entry
produces 1–4 images through one `FlowApiClient` session (one Playwright
browser, one Flow project, sequential reCAPTCHA mints).
browser, one Flow project; prompts run one after another).

### Config schema

Expand Down
18 changes: 11 additions & 7 deletions docs/superpowers/memory/migrated-host-driver-wire-lessons.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ cost a real run to learn; each is now a unit test in `tests/api/transports/`.
so `discover_site_key` raised `RecaptchaError` — a `RuntimeError` unmapped in
`EXIT_CODE_MAP` — as exit 1 "unexpected" instead of exit 36. The guard now runs at the
mint too (`client.py`, `at="mint_recaptcha_token"`); `git grep raise_if_migrated` is
the current list of sites. Reviewing anything that adds a pre-transport step: ask
the current list of sites. Since #891 that mint guard covers only callers that really send
the token (HTTP image transports, upscale, extend — #914); UI images no longer mint. Reviewing anything that adds a pre-transport step: ask
"which page is the pool holding at that moment on a moved account?"

Related: [[flow-recon-must-run-on-denon82-ffroliva-migrated]],
Expand Down Expand Up @@ -194,12 +195,15 @@ rounds); e2e `tests/e2e/test_migrated_host_e2e.py`. Read this before re-mining t
`flow.google.com` grid, which carries no `enterprise.js`, so the mint failed before
any guard could classify it (#673). The client now skips minting when the transport
reports `uses_page_owned_image_recaptcha()` and lets the project page mint + submit.
- **Derive that capability from a latch, not from `page.url`.** The image path parks the
page on `about:blank` when it finishes, which routes as `labs` — so a URL-derived
capability answers `False` on the *second* image in one client session and falls back
to the very mint it exists to avoid. Invisible to every single-image test; reachable
from `gflow image batch`, which runs every prompt through one `FlowApiClient`. Same
shape as the r2v listener above: both halves correct, the join stateful and wrong.
- **The UI transport never needed the client token at all (#891).** Nothing in
`ui_automation.py`, `drivers/` or `migrated_composer.py` reads `recaptcha_token`, and
`git log -S` shows it never did: Flow's page mints its own on click, on either host. The
capability is therefore unconditional. The earlier URL- and then latch-derived versions
existed only to decide a mint that was dead weight, and the dead mint was not harmless:
after a successful run the page is parked on `about:blank`, where it raised
`RecaptchaError` instead of exit 36. For UI images, exit 36 now comes from the
composer, which names the unported form. Review lesson: before tuning *when* a step
runs, grep whether anything consumes its output.
- **Enumerate the axis before mapping to it.** The image aspect radiogroup carried four
radios — `crop_16_9`, `crop_landscape`, `crop_square`, `crop_9_16` — and no
`crop_portrait`. A driver that maps 3:4 to a guessed ligature does not fail as "not
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Stop pre-minting image reCAPTCHA for the UI transport (#891) — Implementation Plan

> **For agentic workers:** Run `/gflow:status --feature 891-warm-client-premint` to find the next
> unchecked task. Implement one task at a time. Run `/gflow:check` before every commit.

**Goal:** An unported image form never ends as a misleading `RecaptchaError`. On an account
served flow.google.com it is refused as `FlowHostMigratedError` (exit 36), naming the form, on a
fresh or warm client.

**Architecture:** `FlowApiClient._drive_images_generation` stops pre-minting whenever the transport
declares `uses_page_owned_image_recaptcha`. Only `UiAutomationTransport` does, and neither it nor
its drivers ever read `request.recaptcha_token`: Flow's page mints its own on click. The URL and
latch logic inside that capability existed only to feed the mint decision, so it collapses to a
constant, and the client no longer imports `migrated_images_prefer`. Routing
(`_generate_images_locked`) and the experimental HTTP transports are unchanged.

**Predict verdict:** CAUTION 7/10 (posted on #891, 2026-09-29), narrowed by SCENARIO.md: the latched-client
reroute (predict design step 2) is dropped because no shipped surface reaches it.

**Risk register:**

| Severity | Risk | Mitigation |
|---|---|---|
| Medium | An account served labs relies on the client token after all | Grep proof: no reader in `ui_automation.py`, `drivers/`, `migrated_composer.py`. The labs arm cannot be observed here (308 on every profile), so record it, don't claim it |
| **High** | **Today's exit 36 on the shipped path COMES FROM the mint's host guard.** Baseline 2026-09-29 (`develop` 342f39df, `ffroliva`, `t2i --model imagen4`, $0): both arms exit 36 with `ui_driver.migrated_host_bail at=mint_recaptcha_token` — `--project` at `flow.google.com/?hl=en`, no-project at `flow.google.com/project/<id>`. Removing the mint removes that raise, so the transport must take over | By code the transport should raise instead: `migrated_route(flow.google.com URL, auto)` returns `migrated`, then `run_images` refuses the unported form with exit 36 and names the model. **Unrun.** Task 3 re-runs these exact two commands after the change, and the PR is not mergeable until both exit 36 |
| Medium | A fresh client whose page is still the labs bootstrap takes the labs detour before exit 36 | Same as today: the old mint did not bail there either (it minted, then the transport ran). Covered by e2e #3 |
| Low | Merge conflict with #882 (`_mint_recaptcha_token`) and #824 (`run_images`) | Neither function body changes |

---

## File structure

### Modified files
```
src/gflow_cli/api/client.py
_drive_images_generation: skip the mint when the transport owns its page; drop the
migrated_images_prefer import; update the #673 comment in _mint_recaptcha_token.
src/gflow_cli/api/transports/ui_automation.py
uses_page_owned_image_recaptcha: return True; delete the URL/latch logic that only fed the mint.
src/gflow_cli/api/transports/migrated_composer.py
migrated_images_prefer docstring: drop "the mint decision calls it" and the mint rationale.
tests/api/test_image_capability_and_cookie_bar.py
replace test_drive_mints_when_request_is_not_migrated_servable; fix the project-less test.
tests/api/transports/test_migrated_images.py
drop the URL-branch tests of the capability (:274-356); keep the routing tests.
tests/e2e/test_migrated_host_e2e.py
e2e #3 (fresh client, unported model) and #4 (warm client).
scripts/dev/spike_mint_on_about_blank.py, scripts/dev/spike_warm_client_unported_form.py
committed as the #891 evidence.
docs/superpowers/spikes/2026-09-20-warm-client-premint-on-about-blank.md
verdict doc.
KNOWN_ISSUES.md, CHANGELOG.md
```

---

## Task 1 — Red tests
**What:** Pin the new contract before touching `src/`.
**Steps:**
- [x] Replace `test_drive_mints_when_request_is_not_migrated_servable` with `test_drive_never_mints_for_a_page_owning_transport`: unported request (`reference_entities`), and the mint must not be awaited.
- [x] Rewrite `test_migrated_images_prefer_keeps_project_less_runs_on_labs` so it asserts the predicate as routing uses it (`page_url` given), not the unreachable `project_id=None` client call.
**Tests created (red):**
- [x] `test_drive_never_mints_for_a_page_owning_transport` fails on `develop`.

## Task 2 — Client + capability
**Steps:**
- [x] `_drive_images_generation`: `if callable(getattr(self.transport, "uses_page_owned_image_recaptcha", None))` → no mint.
- [x] `UiAutomationTransport.uses_page_owned_image_recaptcha` → `return True`, with a docstring naming the fact (the token is never read). Delete the URL/latch body.
- [x] Delete the capability's URL-branch tests in `test_migrated_images.py`; keep `test_client_keeps_legacy_mint_for_other_image_transports`.
- [x] Fix the `migrated_images_prefer` docstring and the `_mint_recaptcha_token` #673 comment.
**Tests:** Task 1 green; `tests/api`, `tests/features` green.

## Task 3 — E2E (Iron Law)
- [x] #3: fresh client, `--model imagen4` on a served-flow.google.com profile → exit 36, $0. Re-run BOTH baseline arms (with and without `--project`) and compare them against the 2026-09-29 baseline: exit code, error type, and which step raised.
- [x] #4: warm client, one t2i then `imagen4` → exit 36 (ports the spike). One image of the daily cap.
- [x] MCP twin of #3 via `gflow_generate_image`, or record the reason with evidence.
- [x] Run on `ffroliva`, and paste the result into the PR.

## Task 4 — Evidence + docs
- [x] Commit the two spike scripts and a spike verdict doc.
- [x] Add the #891 line under KNOWN_ISSUES #673.
- [x] CHANGELOG `### Fixed`.
- [x] File the deferred issues listed in SCENARIO.md.

## Task 5 — Gates
- [x] `/gflow:check` green, then PR → `/gflow:pr-council-review` (council pending).

---

## Definition of done
- [x] All tasks checked; e2e #3 and #4 pasted in the PR; deferred issues filed; Closes #891.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Scenario: stop pre-minting image reCAPTCHA for the UI transport (#891)

Inputs: the predict verdict posted on #891 (2026-09-29, CAUTION 7/10), plus two corrections
found while scoping this document. Both corrections are verified against `develop` 342f39df.

## Corrections to the predict

1. **No shipped surface can reach the #891 failure today.** It needs a successful image run
followed by an unported image form **on one `FlowApiClient`**:
- A single `gflow image t2i` / `i2i` run opens one client for one request.
- MCP and the worker open a client per task (`worker/daemon.py:183`).
- Multi-prompt `t2i` (`run_image_batch`) builds every item with the same model/aspect and
no refs (`image_batch.py:502`), so it cannot mix a ported and an unported form.
- `gflow image batch` (manifest) goes to `generate_images_batch`, which is refused outright on
flow.google.com (`test_image_batch_is_refused_on_the_migrated_host_before_any_submit`).
- `gflow movie` and the services layer do not generate images.

Only direct library use reaches it, which is what the 2026-09-20 spike did. The predict's
"`gflow image batch` with `batch:0`" trigger is wrong: `_to_request` (`image_batch.py:882`)
drops `ref`/`reference_entity`, so they only order the DAG. That is a separate latent bug.
2. **So design step 2 of the predict (reroute a latched client from `about:blank`) guards a path
nothing takes.** It is dropped under YAGNI. Design step 1 alone removes the whole failure mode,
because the UI transport never needs the client-minted token.

## Coverage map

| Dim | Active? | Why |
|---|---|---|
| D2 WAF/reCAPTCHA | yes | The change removes a `grecaptcha.execute` call |
| D5 Page pool | yes | Pool pages 1..n are never navigated; the mint used to run on them |
| D7 Exit codes | yes | An unported form must end as exit 36, never `RecaptchaError` |
| D9 Transport | yes | The experimental HTTP transports still need the token |
| D12 Observability | yes | `recaptcha_mint_failed_off_migrated_host` stops firing on the image path |
| D13 MCP | yes | `gflow_generate_image` (direct and queued) runs the same client code |
| D1, D3, D4, D6, D8, D10, D11 | no | No auth, selector, manifest, data, path, headless or input change |

## Scenario table

| # | Dim | Scenario | Severity | Expected behaviour | Test category |
|---|---|---|---|---|---|
| 1 | D9 | Experimental HTTP transport (`bearer`, `evaluate_fetch`) generates an image | **Critical** | Still mints; the token is in the request body | Unit (existing `test_client_keeps_legacy_mint_for_other_image_transports`) |
| 2 | D2 | UI transport with an **unported** form (`--model imagen4`, `-i`, UUID `--ref`, entity) | **High** | `_mint_recaptcha_token` is never awaited; the transport decides | Unit (replaces `test_drive_mints_when_request_is_not_migrated_servable`) |
| 3 | D7 | Fresh client, account served flow.google.com, unported form (the shipped CLI/MCP path) | **High** | `FlowHostMigratedError`, exit 36, form named, $0 | E2E live (`e2e_image`, $0: refused pre-submit) |
| 4 | D2 | Warm client: a successful t2i, then an unported form on the same client (the #891 repro) | **High** | Not `RecaptchaError`; ends `FlowHostMigratedError` exit 36 | E2E live (`e2e_image`, one image of the daily cap) |
| 5 | D2 | UI transport, ported request | High | Unchanged: no mint (was already skipped) | Unit (existing `test_drive_skips_mint_for_servable_request`) |
| 6 | D13 | `gflow_generate_image` with an unported form, direct and queued | High | Same exit-36 Problem Details as the CLI; no docstring or payload change | E2E live: MCP twin of #3, or record why it is the same client call |
| 7 | D2 | Account served labs, unported form, on the labs driver | Medium | Page mints its own token on click; the client token was never read | Not observable here (labs answers 308 on every profile, 2026-09-14 survey). Record, do not claim |
| 8 | D5 | `GFLOW_CLI_CONCURRENCY>1` on the UI transport | Low | No mint, so the never-navigated pool pages are no longer involved | Unit (covered by #2) |
| 9 | D12 | The image path no longer emits `recaptcha_mint_failed_off_migrated_host` | Low | The event name is unchanged (asserted by `test_client_migrated_mint.py:153` for the mint itself) | Existing test |
| 10 | — | `migrated_images_prefer` project-less claim (#891 point 2) | Medium | The test asserts a shape production can produce; the CHANGELOG is corrected | Unit |

## Must-cover before merge
- #1, #2, #5: offline.
- #3, #4: e2e. `tests/e2e/test_migrated_host_e2e.py` is the home; #4 ports `scripts/dev/spike_warm_client_unported_form.py`.
- #6: run the MCP twin, or record that the change sits below both adapters and the adapter is untouched. Iron Law: prefer the run.
- #10.

## Deferred (file as issues)
- **Batch rows drop `ref`/`reference_entity`** (`image_batch.py:_to_request`, `:502`). References only order the DAG.
- **`upscale_image` / `extend_video` mint on the pool page** (`client.py:2547`, `:2268`) and genuinely need the token. On a warm client parked at `about:blank` they should hit the same `RecaptchaError`. Inferred, not run: needs a spike.
- **`RecaptchaError` is a `RuntimeError`, not a `GFlowError`** (`recaptcha.py:25`), so it aborts a batch and ignores `--continue-on-error`.
- **The misleading "Still reads as labs" branch on `about:blank`.** Wait for #882, which rewrites `_mint_recaptcha_token`.

## Suggested BDD scenarios

```gherkin
@e2e @e2e_image
Feature: an unported image form on flow.google.com is refused by name
Scenario: warm client after a successful image
Given a client that has generated one image on flow.google.com
When the same client is asked for an image with an unported model
Then it fails with FlowHostMigratedError naming the model
And no reCAPTCHA token was minted by the client
```

## Known-issues cross-reference
- **KNOWN_ISSUES #673** (`RecaptchaError` on a moved account): resolved for the warm-client shape by this change. Add the #891 line, worded as "on an account served flow.google.com".
- **#692** (handoff race during the mint): the image path no longer mints, so its race no longer applies to images. The experimental HTTP transports and video/upscale keep the guard.
Loading
Loading