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 @@ -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/or a named character, and text-to-image/local-file image-to-image with `@Character` mentions by name, and a single-segment `video extend`, today — an agent-only composer, #799, takes text-to-image and text-to-video only; `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_telemetry.py`, `telemetry_upload.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`, `telemetry.py`.
- 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 from flow.google.com's own balance reply in the profile's browser, #795; read-only unless `--refresh`, which renews signed-out 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, and `gflow data stats`), `gflow telemetry` (on/off/status/show; uploads weekly usage totals after the notice), `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 from flow.google.com's own balance reply, #795 — directly, with no browser, when the direct read is on (`GFLOW_CLI_WIRE_READS`; off by default on macOS) and the profile's saved session is accepted, otherwise in the profile's browser, #1229; read-only unless `--refresh`, which renews signed-out 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, and `gflow data stats`), `gflow telemetry` (on/off/status/show; uploads weekly usage totals after the notice), `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
16 changes: 10 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **`gflow credits` reads the balance without starting a browser when the saved session is
accepted.** `credits user`, `credits list` and the MCP `gflow_get_credits` tool now ask
flow.google.com for the balance directly, with the profile's saved cookies: one page load
and one request, no Chrome. When that fails (signed out, Chrome open on the profile, a
network error, an unexpected reply) they read it in the browser as before. When Flow
throttles the account (HTTP 429 or 503) the command reports the rate-limit or network error
instead, since the browser would send the same requests. `GFLOW_CLI_WIRE_READS=0` turns the direct read off; it is off by default on
macOS and while any proxy is configured (#1229).
flow.google.com for the balance directly, with the profile's saved cookies: two HTTP
requests and no Chrome. When that fails (signed out, a network error, an unexpected reply)
they read it in the browser as before; with Chrome open on the profile both reads fail and
report the profile in use (exit 11, close Chrome and retry). When Flow throttles the account
(HTTP 429 or 503) nothing falls back to the browser, which would send the same requests:
`credits user` exits 4 or 6, and `credits list` records the error for that profile. `GFLOW_CLI_WIRE_READS=0` turns the
direct read off; it is off by default on macOS and while any proxy is configured
(`GFLOW_CLI_BROWSER_PROXY`, or an environment/system proxy). The direct read uses the saved
Google session outside Chrome; what that risks and what is still unmeasured is in
KNOWN_ISSUES (#1229).

- **`gflow doctor` and the MCP `gflow_auth_status` tool report whether that direct read works.**
Doctor's new `auth.wire_read` check (default profile) and the tool's new `wire` field (on every session result) give
Expand Down
57 changes: 55 additions & 2 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,55 @@ Living list of behaviour that's broken, surprising, or limited by design — alo

## Open

### The direct credits read uses the profile's saved Google session outside Chrome

- **Status:** Open (a risk under watch, not a failure seen) · **Severity:** Low as measured; the long-run effect on the account is unmeasured · **Affected:** `gflow credits user` / `list` and MCP `gflow_get_credits`, on Windows and Linux by default (off by default on macOS)
- **Tracked:** [#1229](https://github.com/ffroliva/gflow-cli/issues/1229) · [#1222](https://github.com/ffroliva/gflow-cli/issues/1222)
- **Evidence:** cookie-jar spike, [PR #1234](https://github.com/ffroliva/gflow-cli/pull/1234); live proof in [PR #1236](https://github.com/ffroliva/gflow-cli/pull/1236)

To read the balance without starting Chrome, gflow takes the profile's saved Google session
cookies from the profile's own cookie file and sends them to flow.google.com from its own HTTP
client: one GET of the app page and one request for the balance. Flow's batchexecute wire is a
private, undocumented API. Google has not said it accepts this use, and an account that sends
its session from something other than Chrome takes that risk. gflow limits what it sends to what
the app itself sends for this read, and asks for nothing else (an allow-list holding one rpc).

**Rotation.** Chrome rotates some of these cookies (`*SIDCC`, `*PSIDRTS`, `*PSIDTS`) while it
runs, and a copy taken outside Chrome does not follow. Measured on one Windows profile: a copy
taken before Chrome opened was still accepted after Chrome had rotated 7 of them, and the
browser session was still valid afterwards (`auth status` passed and a browser balance read
succeeded). After a run of direct reads, the e2e suite checks that a browser read still works.

**Cookies Flow sends back are dropped.** Every direct reply sets `SIDCC`, `__Secure-1PSIDCC`
and `__Secure-3PSIDCC`. gflow keeps none of them and never writes to the profile's cookie
file; in the measured run every later read passed with the original values.

**While Chrome runs on the profile** (Windows, measured), Chrome holds the cookie file
exclusively, so the direct read cannot open it. gflow does not work around that lock; it
falls back to the browser read (reason `store_unreadable`), which then reports the profile as
in use (`ProfileLockedError`, exit 11) as before. Linux is unmeasured.

**Not measured yet:**

- How long a copied session stays accepted beyond the measured minutes, and whether dropping
the re-minted `*SIDCC` cookies over many reads eventually makes Google ask for a re-sign-in.
- How long a fetched `at` token stays valid inside a long-running process
([#1222](https://github.com/ffroliva/gflow-cli/issues/1222)). A stale one is expected to
be refused (HTTP 400) and to fall back to the browser read; that path is tested offline only.
- A profile written by a Chrome build that uses app-bound (`v20`) cookie encryption. None was
present on the measured profile; such a profile is expected to fail to decrypt and fall back,
but that is not measured.

**Remedy / opt-out:** set `GFLOW_CLI_WIRE_READS=0` to always read in the browser
([CONFIGURATION](docs/CONFIGURATION.md#gflow_cli_wire_reads)). If Flow ever answers signed out
or HTTP 401, that process stops trying the direct read for that profile until a `--refresh`
renewal that Flow verified, or a `gflow doctor` / `gflow_auth_status` probe that Flow accepts.
If it answers HTTP 429 or 503, that profile is left alone for the throttle window
(`Retry-After`, capped at 60 s) and nothing retries in the browser: `credits user` exits 4 or
6, and `credits list` records the error for that profile and keeps the other balances.

---

### Every command lands on `flow.google.com/about` (exit 31) while `gflow auth status` says the session is fine

- **Status:** Open · **Severity:** High for the affected account (nothing runs) · **Affected:** measured on one account (`denon82`, 2026-09-23). Other `/about` occurrences are not yet shown to share the cause.
Expand Down Expand Up @@ -936,11 +985,15 @@ A block that survives dismissal now aborts pre-submit with exit 23 (probe `overl

- **Status:** **Resolved** in v0.87.1 ([#795](https://github.com/ffroliva/gflow-cli/issues/795)); open on every account from 2026-10-22 until then

**Fixed.** `gflow credits` now opens flow.google.com in the profile's browser and reads the
**Fixed.** In v0.87.1 `gflow credits` began opening flow.google.com in the profile's browser and reading the
balance from the app's own `nzlxg` reply, the number Flow's account menu shows (measured 5/5,
including after a spend:
[spike](docs/superpowers/spikes/2026-10-22-credits-balance-on-nzlxg.md)). It no longer needs a
labs.google token. It costs one browser launch per profile, and the tier fields (`sku`,
labs.google token. It cost one browser launch per profile; since
[#1229](https://github.com/ffroliva/gflow-cli/issues/1229) the same reply is first asked for
directly, with the profile's saved cookies and no browser, and the browser read is the fallback
(see "The direct credits read uses the profile's saved Google session outside Chrome" below).
The tier fields (`sku`,
`service_tier`, `user_paygate_tier`, `subscription_credits`) are always `null`, because
flow.google.com does not report them. It was verified on two accounts, neither on a paid plan; a paid plan may put a different
balance in the field gflow reads, which would show a wrong number without an error. A page that never sends the reply fails with exit 31
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ gflow auth login --browser chrome

# Check the session (spends nothing)
gflow auth status
gflow credits user # Veo credit balance (opens Flow in the browser; spends nothing)
gflow credits user # Veo credit balance (no browser when the direct read is on and the saved session is accepted; spends nothing)

# 3 · Generate
gflow image t2i "a hot air balloon over Tokyo at sunrise"
Expand Down
26 changes: 25 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,10 +413,34 @@ The `Provider` interface keeps backends interchangeable. v0.1 shipped `FlowProvi
- macOS: `~/Library/Application Support/gflow-cli/profile_default/`
- Linux: `~/.local/share/gflow-cli/profile_default/`

Subsequent commands launch a Playwright context using that profile and call REST endpoints via Playwright's HTTP client — which auto-attaches the cookies. No tokens to refresh manually, no SSO scraping. Auth is the only browser interaction users see, and it is a one-time event.
Subsequent commands launch a Playwright context using that profile and call REST endpoints via Playwright's HTTP client — which auto-attaches the cookies. No tokens to refresh manually, no SSO scraping. Auth is the only browser interaction users see, and it is a one-time event. One read skips the browser when it can: the credit balance (see [Reads without a browser](#reads-without-a-browser)).

See [docs/AUTHENTICATION.md](AUTHENTICATION.md) for the full lifecycle, multi-account workflows, and recovery paths.

## Reads without a browser

Since [#1229](https://github.com/ffroliva/gflow-cli/issues/1229), one read, the credit balance, is first made without starting Chrome. Everything else still goes through the browser: generation submits, uploads, reCAPTCHA, and every other read.

**Where it sits.** `services/credits.py::_fetch_once` calls `api/flow_wire.py::read_credits` **before** it builds a `FlowApiClient`, because building one launches Chrome. Putting the choice lower, inside the client, would launch the browser anyway. The kill switch (`GFLOW_CLI_WIRE_READS`) is checked at the same spot.

**What it sends.** The profile's saved cookies, read from its own cookie file (`auth/cookies.py::read_cookies_for`: the profile's own cookie file, and on Windows its own key file, named explicitly so the user's personal Chrome jar is never read; only cookies a browser would send to `https://flow.google.com/`, kept in memory, never written back). With them: one GET of the app shell, for the page's `at` token, session id and build label, then one batchexecute POST of `nzlxg`, whose reply `CreditsInfo.from_nzlxg` already decodes for the browser path. The rules in code:

- an rpcid allow-list that holds only `nzlxg`
- no redirect is followed and no 4xx is retried (one retry only for a connect error, when nothing reached Flow)
- two time limits in sequence: 5 s for the cookie read, then 5 s for both requests (3 s to connect), so a read that hangs gives way to the browser after about 10 s at most
- a reject-all cookie jar, so every cookie Flow sets in a reply is dropped
- the User-Agent of the profile's own Chrome version

**Fallback.** Any failure raises `WireReadUnavailable` carrying one `WireFallbackReason` value and nothing else (no body, no exception text: replies on this wire can carry signed URLs). The service logs `credits.wire_fallback reason=<value>` and reads in the browser as before. The reasons are `signed_out_shell`, `http_401`, `http_400`, `http_error`, `unparseable`, `store_unreadable` (the cookie file cannot be opened, which is what happens while Chrome runs on the profile), `no_chrome_version`, `proxy_configured`, `network`, `redirect`, `disabled`, and `unexpected` (a bug in the direct read, logged with its class name). Which path answered is logged as `credits.read source=wire|page`; the JSON result has no such field.

**Not a fallback.** A profile outside `GFLOW_CLI_HOME` raises `SecurityError` before anything is read. HTTP 429 and 503 are back-pressure: they raise the browser path's own `RateLimitError` / `NetworkError` and open a throttle window for that profile (`Retry-After`, capped at 60 s; 60 s when Flow names none), because a browser read would send the same requests to a throttled account. The other knobs: [CONFIGURATION § `GFLOW_CLI_WIRE_READS`](CONFIGURATION.md#gflow_cli_wire_reads).

**Per-profile state, per process.** The shell's tokens are cached, so a second read in the same process sends only the POST; any non-200 or unparseable reply drops them. A profile Flow answered signed out (no `at` in the shell, or a 401) is not tried again in that process until a `--refresh` renewal that Flow verified (`flow_wire.session_recovered`) or a `gflow doctor` / `gflow_auth_status` probe that Flow accepts lifts the mark; a browser read that succeeds does not, because the cookies the direct read sends are unchanged. One lock per profile keeps concurrent reads to one shell GET. One profile's state never affects another's.

**Where it is off.** On macOS by default, where reading Chrome's cookies can raise a Keychain prompt; while any proxy is configured (`GFLOW_CLI_BROWSER_PROXY`, or an environment/system proxy found by `urllib.request.getproxies()`), so the cookies never leave from a different address than the browser's; and wherever `GFLOW_CLI_WIRE_READS=0`. The account risk of using the saved session outside Chrome is written up in [KNOWN_ISSUES](../KNOWN_ISSUES.md#the-direct-credits-read-uses-the-profiles-saved-google-session-outside-chrome).

**No port yet.** `flow_wire` is plain functions with one caller. A `FlowReader` port (a `Protocol`, per [Ports & adapters](#ports--adapters)) is introduced together with its second consumer, the job-status reads of [#1194](https://github.com/ffroliva/gflow-cli/issues/1194), not before.

## Headed-browser dependency — current limitation

This is the project's defining trade-off and the most valuable place an external contributor could move the needle.
Expand Down
Loading
Loading