Skip to content
Closed
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 @@ -47,7 +47,7 @@ by construction, so Codex / Cursor / Aider / `agy` read exactly what Claude Code
- Python CLI and MCP server for [Google Flow](https://labs.google/fx/tools/flow) — drives Veo (image-to-video, text-to-video) and Imagen (text-to-image) generations from the terminal by reverse-engineering Flow's private REST API at `aisandbox-pa.googleapis.com` — and, for accounts Google has moved to `flow.google.com`, that frontend's `batchexecute` wire (text-to-video, image-to-video from a local `--initial-frame`, reference-to-video from local `--ref` files, and text-to-image/local-file image-to-image with `@Character` mentions by name, today; `GFLOW_CLI_FLOW_HOST`).
- Python 3.11+ · `uv`-managed · `hatchling` builds · Playwright Chromium transport · `pyright` strict · `ruff` · `pytest`.
- Single-package modular monolith. Top-level modules under `src/gflow_cli/`: `api/`, `auth/`, `data/`, `mcp/`, `services/`, `tools/`, `ui/`, `worker/`, `browser_manager.py`, `cli.py`, `_cli_helpers.py`, `diagnostics.py`, `json_output.py`, `media.py`, `profile_lease.py`, `redaction.py`, `storage.py`, `winsec.py`, `cli_project.py`, `cli_character.py`, `cli_credits.py`, `cli_data.py`, `cli_image.py`, `cli_instructions.py`, `cli_models.py`, `cli_movie.py`, `cli_run.py`, `cli_scene.py`, `cli_tools.py`, `cli_video.py`, `chain.py`, `chain_manifest.py`, `cli_doctor.py`, `cli_docs.py`, `docs_catalog.py`, `cli_update.py`, `composition.py`, `config.py`, `errors.py`, `file_integrity.py`, `flow_selectors/`, `update_check.py`, `exceptions.py`, `image_batch.py`, `movie_manifest.py`, `observability.py`, `paths.py`, `profile_store.py`.
- Command surface: `gflow auth`, `gflow credits` (user/list — read-only Veo balance), `gflow image` (t2i/i2i/batch/upload/upscale), `gflow video` (t2v/i2v/r2v/chain/extend/upscale — `extend` continues an existing clip past Flow's 8s ceiling, server-seeded from the source so the join is continuous; `upscale` exports to 1080p, 720p, or 270p GIF; no `batch` subcommand; the nonfunctional stub was removed, loop `gflow video t2v`/`i2v` from the shell for multi-clip runs), `gflow character` (create/list/show/rm/voices — reusable project-scoped Flow Character entities), `gflow scene` (create/show — Add Clip / Scenes, with `create --output` for credit-free server-side extended video), `gflow instructions` (persistent Agent-Mode brief cards — add/list/enable/disable/rm/apply/toggle-mode, credits-free, `--project` required), `gflow movie` (run/template — multi-scene manifest pipeline), `gflow tools` (list/show/run — prompt-rewriting tools, also `--tool` on generation commands), `gflow data` (catalog queries), `gflow docs` (browse the bundled documentation — bare lists every topic, `gflow docs <topic>` prints one as Markdown, `gflow docs --search TERM` searches all of them; there are **no** `list`/`print`/`search` subcommands — read-only, offline, deliberately no MCP twin), `gflow doctor` (read-only pre-flight diagnostic, exit 33 = findings present), `gflow update` (self-update through the installer that put it here — uv tool / pipx / pip; `--check` only reports; source installs refused, exit 11; deliberately no MCP twin), `gflow project`, `gflow models`, `gflow run` (JSON image-batch config; a row may generate from an earlier row with `"ref": "batch:N"`, referenced in place, no upload, or from a local image file, uploaded once per run), `gflow mcp` (run/setup — stdio MCP server), and `gflow serve` (Streamable HTTP at `/mcp`; `--transport sse` is deprecated).
- Command surface: `gflow auth` (login/status/renew/list/use/logout — `renew` refreshes saved sessions headlessly, no sign-in), `gflow credits` (user/list — Veo balance; read-only unless `--refresh`, which renews expired sessions first), `gflow image` (t2i/i2i/batch/upload/upscale), `gflow video` (t2v/i2v/r2v/chain/extend/upscale — `extend` continues an existing clip past Flow's 8s ceiling, server-seeded from the source so the join is continuous; `upscale` exports to 1080p, 720p, or 270p GIF; no `batch` subcommand; the nonfunctional stub was removed, loop `gflow video t2v`/`i2v` from the shell for multi-clip runs), `gflow character` (create/list/show/rm/voices — reusable project-scoped Flow Character entities), `gflow scene` (create/show — Add Clip / Scenes, with `create --output` for credit-free server-side extended video), `gflow instructions` (persistent Agent-Mode brief cards — add/list/enable/disable/rm/apply/toggle-mode, credits-free, `--project` required), `gflow movie` (run/template — multi-scene manifest pipeline), `gflow tools` (list/show/run — prompt-rewriting tools, also `--tool` on generation commands), `gflow data` (catalog queries), `gflow docs` (browse the bundled documentation — bare lists every topic, `gflow docs <topic>` prints one as Markdown, `gflow docs --search TERM` searches all of them; there are **no** `list`/`print`/`search` subcommands — read-only, offline, deliberately no MCP twin), `gflow doctor` (read-only pre-flight diagnostic, exit 33 = findings present), `gflow update` (self-update through the installer that put it here — uv tool / pipx / pip; `--check` only reports; source installs refused, exit 11; deliberately no MCP twin), `gflow project`, `gflow models`, `gflow run` (JSON image-batch config; a row may generate from an earlier row with `"ref": "batch:N"`, referenced in place, no upload, or from a local image file, uploaded once per run), `gflow mcp` (run/setup — stdio MCP server), and `gflow serve` (Streamable HTTP at `/mcp`; `--transport sse` is deprecated).
- Works with any Google account that has Flow access. All generations bill against the user's own Google account.

## Headed-browser dependency (architectural reality)
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`gflow auth renew` refreshes saved sessions without signing in again.** It opens each
profile (`--profile`, or `--all`) in a headless browser and loads Flow, so Google and Flow
rotate the saved cookies. It then verifies the session like `gflow auth status`. No credits
are spent. A revoked session still needs `gflow auth login`, and a profile in use by another
gflow process is skipped. It reports a profile as renewed only when it saw it renewed: an
account Flow holds at its public `/about` page is `public_landing`; a labs.google session that
stayed expired, or never answered readably, is `labs_stale` / `labs_unverified`; a browser
that cannot start (the profile is open in your own Chrome) is a `launch_error` row, and the
rest of an `--all` run carries on. `--json` prints JSON on every path, including errors.
- **`gflow credits user|list --refresh` renews expired sessions again.** Since #802, an expired
session fails the credits read without opening a browser. That is the right default, because
re-asking aisandbox-pa with the same Bearer cannot change its answer. It also removed the only
automatic renewal that `credits list` used to give stale profiles. `--refresh` brings it back as
an opt-in. A profile whose fast path raises an expired-session error is renewed through
`auth renew` and asked again, while the others are untouched. A failed renewal keeps the
original diagnosis. The MCP twin is `gflow_get_credits(refresh=true)`. The credits 401 hint
now points at `--refresh` / `gflow auth renew` instead of "check your balance in Flow"; an
account whose labs session issues no token at all is not helped by renewal, and says so.

## [0.85.0] — 2026-10-14

### Added
Expand Down
19 changes: 15 additions & 4 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -742,16 +742,27 @@ Google's web session cookies aren't permanent. They expire when:

When this happens, the next API call returns 401/403 and `gflow-cli` raises `AuthExpiredError`.

**Workaround:**
**Workaround, stale session (the common case):**
```bash
gflow auth renew --profile <name> # or: gflow auth renew --all
gflow credits list --refresh # renews only the expired profiles, then reads them
```

A profile that has just sat unused has a lapsed labs.google token, not a dead Google session.
`auth renew` loads Flow and labs.google in a headless browser and waits for labs to sign in
again on its own, with no prompt. Measured 2026-10-14: 7 of 10 stale profiles recovered.

**Workaround, revoked session:**
```bash
gflow auth login --profile <name>
```

Re-running `auth login` reuses the existing profile dir (you typically just click "Continue as <you>" on the Google account chooser). No data is lost; only the cookie jar is refreshed.

**Why we don't auto-refresh:** Google's session-refresh flow can include CAPTCHA / device verification that only a human can complete. A community SDK can't reliably automate that step. See [docs/AUTHENTICATION.md § Refresh / expiry](docs/AUTHENTICATION.md#refresh--expiry).

**Roadmap:** not scheduled. The Phase 4 hardening pass (v0.4.0a2) added typed `AuthExpiredError` + exit code `3` so scripts can branch on auth expiry deterministically. A periodic "session liveness" check + a `gflow auth refresh` command are still candidates for a later phase, but not committed to a version yet.
**What renewal cannot do:** complete a step only a human can, such as CAPTCHA, device
verification, or "Confirm it's you". The 3 profiles that did not recover on 2026-10-14 were held
at Flow's public `/about` page ([entry above](#every-command-lands-on-flowgooglecomabout-exit-31-while-gflow-auth-status-says-the-session-is-fine)),
and `auth renew` reports them as not renewed. See [docs/AUTHENTICATION.md § Refresh / expiry](docs/AUTHENTICATION.md#refresh--expiry).

---

Expand Down
48 changes: 47 additions & 1 deletion docs/AUTHENTICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,35 @@ Flow session verified as you@example.com.

> Note: `cookies_present: True` only confirms the file exists. The final verdict line is the live probe result, and it has three failing shapes — all exit 1, each with a different hint. A **dead session** prints a `gflow auth login` remediation hint. A probe that **cannot reach the endpoint** (offline, 5xx) suggests checking connectivity instead of re-logging in. And a profile **missing its `.gflow_browser_strategy` marker** — the state a failed *first* login leaves behind, since that rolls the marker back — says so and points at `gflow auth login --browser chrome --profile <name>`; it is local profile state, not a network fault, and "check connectivity" would send you to the wrong place ([#796](https://github.com/ffroliva/gflow-cli/issues/796), 0.73.2). The MCP twin `gflow_auth_status` reports the same three as HTTP 401, 503 (`retryable: true`) and 409 (`retryable: false`) — see [MCP.md](MCP.md).

### `gflow auth renew`

Refreshes saved sessions **without signing in again**. Google rotates its session cookies, and
Flow re-mints its own session, only when a real browser visits the app. A profile left unused for
weeks therefore drifts towards an expired session even though nothing about the account changed.
`auth renew` opens the profile in a hidden browser and loads Flow. It then loads
`labs.google/fx/` and waits up to 20 s for the labs app to sign in again on its own: a stale labs
session reports `ACCESS_TOKEN_REFRESH_NEEDED`, and the page renews it through Google without a
prompt. This step matters because `gflow credits` reads its token from that labs session, and the
Flow visit alone never refreshes it. The browser saves whatever cookies were rotated, and the
result is verified exactly like `gflow auth status`. It spends no credits.

```bash
gflow auth renew # the default profile
gflow auth renew --profile work # one named profile
gflow auth renew --all --json # every profile with a saved session
```

It exits `0` when every renewed profile verifies and `1` otherwise. An account that Flow sends to
its public `/about` page (see [KNOWN_ISSUES](../KNOWN_ISSUES.md)) never finishes the labs sign-in,
so it needs `gflow auth login`. A profile counts as renewed only when labs.google was seen to
refresh it: if labs is unreachable or answers unreadably (a network that blocks `labs.google`,
say), the row is `labs_unverified` and the command exits `1` even though the Flow session
verifies. A profile another gflow process is using is skipped, not failed. It cannot bring back a session Google has **revoked**
(a password change, "sign out of all devices", an identity re-check). That still needs
`gflow auth login`. `gflow credits user|list --refresh` runs the same renewal automatically for
a profile whose credits read fails with an expired session. Over MCP, that case is reached
through `gflow_get_credits(refresh=true)`; `auth renew` itself has no MCP tool.

### `gflow auth list`

Same output as bare `gflow auth` when profiles exist — useful when you want the table even from a script (no auto-login fallback).
Expand Down Expand Up @@ -417,7 +446,24 @@ ERROR: Auth expired for profile 'default'.
Run: gflow auth login --profile default
```

Re-running `auth login` refreshes the cookies in place — no other state is lost.
Two cases look the same at that point and are fixed differently:

- **Stale, not revoked.** This is the common one: the profile has simply not been opened for a
while. Google's cookies are still good, but labs' NextAuth token has lapsed
(`ACCESS_TOKEN_REFRESH_NEEDED`), and labs only renews it when a browser visits. Run
[`gflow auth renew`](#gflow-auth-renew), or add `--refresh` to `gflow credits user|list`. It
renews the session headlessly with no sign-in.
- **Revoked.** A password change, a removed device, or Google asking to "Confirm it's you" cannot
be renewed from cookies. `auth renew` reports "could not be renewed" (or names Flow's public
`/about` page). Re-running `gflow auth login` refreshes the cookies in place, and no other state
is lost.

> **Renewal signs in headlessly.** The labs re-sign-in runs Google's OAuth hop in a headless
> browser, which identifies itself as `HeadlessChrome` (only `navigator.webdriver` is hidden).
> Generation runs headed by default, so this is the one Google sign-in gflow does headless.
> It worked on 7 of 7 recoverable profiles on 2026-10-14. If Google ever starts challenging it,
> fall back to `gflow auth login`. Note that `gflow_get_credits(refresh=true)` lets an agent
> trigger a renewal on every expired profile.

## Chromium downgrade guard

Expand Down
4 changes: 4 additions & 0 deletions docs/E2E_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ test, not a mocked proxy — the Bug Lane's step 5.
| `GFLOW_CLI_E2E_BATCH_MANIFEST` | `test_assets/sample_batch.tsv` | TSV manifest for the batch image e2e. |
| `GFLOW_CLI_E2E_BATCH_JITTER` | `"1"` | Set to `"0"` to disable inter-request jitter in batch tests. |
| `GFLOW_CLI_E2E_PROMPT` | *(safe default)* | Prompt override for the smoke test. |
| `GFLOW_CLI_E2E_STALE_PROFILES` | *(unset)* | Comma-separated profiles whose labs session has **lapsed** (the credits fast path fails with an expired session). Used by the session-renewal e2e (`e2e_auth`): index 0 drives `gflow_get_credits(refresh=true)`, index 1 drives `gflow credits user --refresh`, and index 2 drives `gflow auth renew`. Each is consumed by its first successful renewal, because a renewed labs token lasts about 24 h. A test whose profile is fresh **skips**; it never passes on a read that needed no renewal. `GFLOW_CLI_E2E_PROFILE` must also be set. |
| `GFLOW_CLI_E2E_ABOUT_PROFILE` | *(unset)* | A profile Flow holds at its public `/about` page (see [KNOWN_ISSUES](../KNOWN_ISSUES.md)). `tests/e2e/test_auth_renew_e2e.py` asserts that `gflow auth renew` does **not** report it renewed. It skips if the profile reads a balance. |

---

Expand Down Expand Up @@ -399,6 +401,8 @@ tests/
├── conftest.py # e2e_profile_dir, e2e_nosession_profile,
│ # e2e_env (shared fixtures)
├── test_auth_verification_e2e.py # [e2e, e2e_auth]
├── test_auth_renew_e2e.py # [e2e, e2e_auth] `gflow auth renew` leaf — 0 credits
├── test_credits_refresh_e2e.py # [e2e, e2e_auth] `--refresh` on CLI + MCP — 0 credits
├── test_transports_e2e.py # [e2e, e2e_{auth,image,batch,video}]
├── test_image_batch_e2e.py # [e2e, e2e_batch]
├── test_video_t2v_e2e.py # [e2e, e2e_video]
Expand Down
Loading
Loading