From 73148da6c624af080c791780fde76198fb888e6e Mon Sep 17 00:00:00 2001 From: Flavio Oliva Date: Tue, 10 Nov 2026 14:50:13 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20direct=20credits=20read=20=E2=80=94?= =?UTF-8?q?=20claims,=20architecture=20and=20the=20session-cookie=20risk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The credits read now asks flow.google.com directly when the saved session is accepted and falls back to the profile's browser (#1236). Sweep every claim that it always opens the browser, add the ARCHITECTURE section 'Reads without a browser', and a KNOWN_ISSUES entry for using the saved Google session outside Chrome: measured rotation result, dropped Set-Cookie, the locked jar while Chrome runs, the kill switch, and what is still unmeasured. Refs #1229 --- AGENTS.md | 2 +- CHANGELOG.md | 14 ++++--- KNOWN_ISSUES.md | 55 ++++++++++++++++++++++++- README.md | 2 +- docs/ARCHITECTURE.md | 26 +++++++++++- docs/E2E_TESTING.md | 1 + docs/MCP.md | 2 +- docs/USAGE.md | 18 +++++--- docs/USER_GUIDE.md | 9 ++-- llms.txt | 5 ++- plugins/gflow/skills/gflow-cli/SKILL.md | 4 +- skills/gflow-cli/SKILL.md | 4 +- website/docs/ARCHITECTURE.md | 26 +++++++++++- website/docs/E2E_TESTING.md | 1 + website/docs/KNOWN_ISSUES.md | 55 ++++++++++++++++++++++++- website/docs/MCP.md | 2 +- website/docs/USAGE.md | 18 +++++--- website/docs/USER_GUIDE.md | 9 ++-- 18 files changed, 217 insertions(+), 36 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b2eb62d0d..cc4ddc7b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 ` 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 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 ` 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) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7726298..610644e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,14 @@ 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 `GFLOW_CLI_BROWSER_PROXY` is set (#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, 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 (exit 4 or 6) + 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 `GFLOW_CLI_BROWSER_PROXY` is set. + The direct read uses the saved Google session outside Chrome; what that risks and what is + still unmeasured is in KNOWN_ISSUES (#1229). ### Fixed diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 49bf73c3f..0825fcb94 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -14,6 +14,53 @@ 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, gflow stops trying the direct read for that profile until the process exits. If +it answers HTTP 429 or 503, that profile is left alone for the throttle window and the command +reports the error (exit 4 or 6) rather than retrying in the browser. + +--- + ### 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. @@ -898,11 +945,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 diff --git a/README.md b/README.md index 5208a3cce..7f52a2177 100644 --- a/README.md +++ b/README.md @@ -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 saved session is accepted; spends nothing) # 3 · Generate gflow image t2i "a hot air balloon over Tokyo at sunrise" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6f892aa69..4da184994 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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) +- a 3 s connect and 5 s overall deadline +- 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=` 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, because a browser read would send the same requests to a throttled account. Window length and 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 until the process exits, even after a `--refresh` renewal, so a long-running MCP server reads that profile in the browser until it restarts. 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 `GFLOW_CLI_BROWSER_PROXY` is set, 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. diff --git a/docs/E2E_TESTING.md b/docs/E2E_TESTING.md index 987c2ba92..c51fab35b 100644 --- a/docs/E2E_TESTING.md +++ b/docs/E2E_TESTING.md @@ -391,6 +391,7 @@ tests/ ├── test_migrated_selector_drift_e2e.py # [e2e, e2e_auth] registry vs flow.google.com — 0 credits ├── 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_credits_wire_bdd.py # [e2e, e2e_auth] credits read with no browser, fallback (#1229) — 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] diff --git a/docs/MCP.md b/docs/MCP.md index 41f65966f..a2b90ae68 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -127,7 +127,7 @@ The server registers three protocol surfaces: * `gflow_auth_status(profile)`: Credit-free, non-interactive Flow session probe (#497) — wraps the same fail-closed `verify_flow_profile` check as `gflow auth status`. Returns `{"status": "authenticated", "profile", "user_email"}` or a problem-details error with a `remediation_hint`; a `verification_error` outcome (HTTP 503, `retryable: true`) means a network/endpoint problem, which re-login does not fix, and a `profile_marker_missing` outcome (HTTP 409, `retryable: false`, #796) means the profile lost its `.gflow_browser_strategy` marker so its cookies cannot be read — a retry does not help, and neither does a plain `gflow auth login`; re-run `gflow auth login --browser chrome` for that profile. A `cookie_store_unreadable` outcome (HTTP 409, `retryable: false`, #796) means the profile's Chrome cookie database is corrupt or unreadable — a local file fault, not the network; close any Chrome window using that profile, then re-run `gflow auth login` for it. Any other unauthenticated outcome is a dead session (HTTP 401). Call it before a generation tool to fail fast on dead auth, before a long blocking generation call. Login/logout remain CLI-only (genuinely interactive). -* `gflow_get_credits(profile, all_profiles, refresh)`: Current Flow balance query; spends no credits. With `all_profiles=true`, inspects every saved profile sequentially and preserves successful balances plus `total_credits` when another profile fails. The balance is read from flow.google.com in the profile's browser: the number the app itself loads, which is the one Flow's account menu shows (#795). That is one browser launch per profile, and the tier fields (`sku`, `service_tier`, `user_paygate_tier`, `subscription_credits`) are always `null`. With `refresh=true`, a profile that lands on a sign-in page is renewed in a headless browser and read again: one more Chrome launch per such profile, no credits. This is the MCP twin of `gflow credits user|list --refresh`. It runs the same renewal as `gflow auth renew`, but only for a profile whose credits read lands on a sign-in page. `auth renew` itself has no MCP tool; see the exemption in `tests/mcp/test_cli_parity.py`. Without `refresh` the call is read-only; with it, an expired profile's saved cookies are rewritten. This tool spends no credits and returns no cookies, bearer tokens, or browser API keys. The balance funds Veo video generation; image generation uses separate per-model daily quotas. A page that loads but never fetches the balance is a `FlowAppError` (exit 31), never an auth error. +* `gflow_get_credits(profile, all_profiles, refresh)`: Current Flow balance query; spends no credits. With `all_profiles=true`, inspects every saved profile sequentially and preserves successful balances plus `total_credits` when another profile fails. The balance is the number flow.google.com itself loads, which is the one Flow's account menu shows (#795). It is read directly, with no browser, when Flow accepts the profile's saved session; otherwise it is read in the profile's browser, one browser launch per profile (#1229; when the direct read runs, and how to turn it off with `GFLOW_CLI_WIRE_READS=0`: [CONFIGURATION](CONFIGURATION.md#gflow_cli_wire_reads)). When Flow throttles the account (HTTP 429/503) the read for that profile fails with the rate-limit or network error rather than falling back to the browser, which would send the same requests. The tier fields (`sku`, `service_tier`, `user_paygate_tier`, `subscription_credits`) are always `null`. With `refresh=true`, a profile that lands on a sign-in page is renewed in a headless browser and read again: one more Chrome launch per such profile, no credits. This is the MCP twin of `gflow credits user|list --refresh`. It runs the same renewal as `gflow auth renew`, but only for a profile whose credits read lands on a sign-in page. `auth renew` itself has no MCP tool; see the exemption in `tests/mcp/test_cli_parity.py`. Without `refresh` the call is read-only; with it, an expired profile's saved cookies are rewritten. This tool spends no credits and returns no cookies, bearer tokens, or browser API keys. The balance funds Veo video generation; image generation uses separate per-model daily quotas. A page that loads but never fetches the balance is a `FlowAppError` (exit 31), never an auth error. * `gflow_character_list(project, profile)`: Lists a project's saved Flow CHARACTER entities with their **entity ids** — read-only, spends no credits, drives a browser session. This is how an agent discovers what it can attach: an `entity_id` goes to `reference_entities`, a `display_name` can be used as an `@Name` mention or goes to `reference_entity_names` at the id's position. On flow.google.com it cannot read the project yet and returns a `wire-format` error (the CLI's exit 7, [#875](https://github.com/ffroliva/gflow-cli/issues/875)): ask the user for the name. An empty list means the project genuinely has none. * `gflow_character_show(project, entity_id, name, profile)`: Shows one character by id or exact display name (exactly one selector required). An ambiguous name is refused rather than resolved arbitrarily — which is the reason to prefer the id. Read-only; drives a browser session. * `gflow_character_voices()`: Lists the preset voices available for Character TTS (name, description, sample URL). A static in-process lookup — no network, no browser, no profile, no cost. Call it to pick a valid voice name. diff --git a/docs/USAGE.md b/docs/USAGE.md index 5da2c7ff3..51cc1a4a7 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -129,11 +129,19 @@ Read the current Google Flow balance through an existing authenticated profile. read-only request: it does not generate media or spend credits. The displayed balance funds Veo video generation; image generation uses separate per-model daily quotas. -The balance is read from flow.google.com: gflow opens the app in the profile's browser and reads -the number the app itself loads, which is the one Flow's account menu shows -([#795](https://github.com/ffroliva/gflow-cli/issues/795)). Nothing is clicked. It costs one -browser launch per profile, and the profile must not be in use by another gflow process -(`ProfileLockedError`, exit 11). Before #795 the balance came from a labs.google token; labs' +The balance is the number flow.google.com itself loads, which is the one Flow's account menu +shows ([#795](https://github.com/ffroliva/gflow-cli/issues/795)). When Flow accepts the +profile's saved session, gflow reads it directly with that session's cookies: two requests, no +browser ([#1229](https://github.com/ffroliva/gflow-cli/issues/1229)). Otherwise (signed out, +Chrome open on the profile, a network error, an unexpected reply) gflow opens the app in the +profile's browser and reads it there. Nothing is clicked. The browser read costs one browser +launch per profile, and the profile must not be in use by another gflow process +(`ProfileLockedError`, exit 11). When Flow throttles the account (HTTP 429 or 503) gflow does not +open the browser, which would send the same requests: `credits user` exits 4 (`RateLimitError`) +or 6 (`NetworkError`), and `credits list` records that error for the profile. The direct read +is off by default on macOS and while `GFLOW_CLI_BROWSER_PROXY` is set; `GFLOW_CLI_WIRE_READS=0` +turns it off everywhere ([CONFIGURATION](CONFIGURATION.md#gflow_cli_wire_reads), +[KNOWN_ISSUES](../KNOWN_ISSUES.md)). Before #795 the balance came from a labs.google token; labs' session URL has answered HTTP 410 to every account since 2026-10-22 ([#1023](https://github.com/ffroliva/gflow-cli/issues/1023)). diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 4405c0ca8..c169421d3 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -422,9 +422,12 @@ Your Google plan includes a finite Veo credit allowance — the video journeys s ### 10.1 Check the balance -Check the current Veo video-credit balance for the selected profile. gflow opens flow.google.com -in the profile's browser and reads the balance the app itself loads, the number in Flow's -account menu ([#795](https://github.com/ffroliva/gflow-cli/issues/795)). Nothing is clicked and +Check the current Veo video-credit balance for the selected profile. gflow reads the balance +flow.google.com itself loads, the number in Flow's account menu +([#795](https://github.com/ffroliva/gflow-cli/issues/795)): directly, with no browser, when Flow +accepts the profile's saved session, and otherwise in the profile's browser +([#1229](https://github.com/ffroliva/gflow-cli/issues/1229); off by default on macOS, see +[`GFLOW_CLI_WIRE_READS`](CONFIGURATION.md#gflow_cli_wire_reads)). Nothing is clicked and nothing is spent: ```bash diff --git a/llms.txt b/llms.txt index d4a3098a7..f41d56c75 100644 --- a/llms.txt +++ b/llms.txt @@ -3,8 +3,9 @@ > Python CLI and MCP server for Google Flow — drives Veo (image-to-video, text-to-video, reference-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 (#639), that frontend's batchexecute wire: text-to-video, image-to-video from a local `--initial-frame` (optionally a local `--end-frame` (#639), no UUID frame), reference-to-video from local `--ref` files, text-to-image, image-to-image from local files, and a single-segment `video extend` (Flow's own Extend control; chains, `--scene`, `--seed` and a foreign `--aspect` exit 36) today, `--project` required there; image prompts may tag a character by name (`@Name`, #723) and reference-to-video takes a character by id plus its name (`--reference-entity` + `--reference-entity-name`, #723); an agent-only composer (#799) takes text-to-image and text-to-video only, other forms exit 36; references by UUID (except an image `--ref ` the catalog records in the same `--project`, #995), an entity id without a name, characters on t2v/i2v, Agent instructions, Imagen 4 and image batch exit 36 (non-retryable; all five aspect ratios, 3:4 included, are served since #864); `GFLOW_CLI_FLOW_HOST` = `auto` (default: flow.google.com for every request it can serve) | `flow.google.com` | `labs.google` (kill switch). Works with any Google account that has Flow access (a paid AI Pro/Ultra plan only affects credit allowances and tier-gated features such as 4K upscale). Uses Playwright Chromium under the hood (one-time real-Chrome auth, then a persistent Chrome session driven via ui_automation). Production-stable for image generation and video T2V/I2V/R2V with `--model`/`--duration`/`--count` pickers and a `--ui-mode` UI-arm gate (labs video has only a classic driver, so `auto` ≡ `classic`, and an agent-only flow.google.com composer is routed to its own driver before this gate; an explicit `--ui-mode agentic` is refused with exit 2 before any browser work, and an unreachable classic editor aborts pre-submit with exit 28 and zero credits); there is no `batch` video subcommand — the nonfunctional stub was removed in v0.41.0; loop `gflow video t2v`/`i2v` from the shell for multi-clip runs. Also ships `gflow auth renew` (refreshes saved sessions headlessly), `gflow character` (create/list/show/rm/voices — reusable, project-scoped Flow Character entities for consistent subjects across generations), `gflow scene` (create/show — compose ordered clips into a scene; `create --output` renders a credit-free server-side extended video), `gflow video chain` (last-frame I2V chaining from a JSONL manifest), and `gflow video extend` (continues an existing clip by chained 8s Veo extensions, seeded server-side from the source clip rather than an extracted still, landing as a Scene that `--output` renders to one file on labs.google; on flow.google.com it runs one segment and `-o` saves the extension clip; note a segment carries ~7s of content though Flow bills 8s, so multi-segment renders hold a frozen second at each internal seam — see KNOWN_ISSUES). `gflow run --config ` runs a JSON image batch whose rows may generate from an earlier row (`"ref": "batch:N"`, referenced in place, no upload) or from a local image file (uploaded once per run); `gflow image batch` refuses any row ref. Keeps a local SQLite catalog of every generation, queryable via `gflow data list {projects,images,videos,profiles}`, and local usage statistics via `gflow data stats` (#1164); local usage counters in `/telemetry.db` are inspectable via `gflow telemetry status|show` (strictly local, no upload). Ships an MCP server (`gflow mcp run` stdio, `gflow serve` Streamable HTTP) with generate/list/instructions tools, a credit-free non-interactive `gflow_auth_status` session probe to call before spending credits, and a `--no-spend` mode (or `GFLOW_MCP_NO_SPEND=1`) that never registers the credit-spending tools. Operational guards (v0.56.0): refuses to open a profile with an older bundled Chromium major than last wrote it (exit 11, protects the session store); `GFLOW_CLI_LEASE_WAIT_SECONDS=N` lets a second same-profile invocation wait for the lease instead of failing fast; a once-a-day stderr banner reports newer PyPI versions (`GFLOW_CLI_UPDATE_CHECK=0` silences it — do so when piping stderr into `jq`), and `gflow update [--check] [--json]` upgrades in place through the installer that put it here (uv tool / pipx / pip; source installs refused, exit 11); `gflow doctor` is the read-only pre-flight diagnostic (exit 33 = findings present). `gflow docs` makes the bundled documentation reachable from the terminal — `gflow docs` lists the topics, `gflow docs ` prints one as raw Markdown, and `gflow docs --search ` returns the matching lines with their `docs/FILE.md:LINE` positions; the pages ship inside the wheel, so it needs no checkout, no network and no account. MIT licensed, not affiliated with Google. Credit inspection is exposed through `gflow credits user|list` and the `gflow_get_credits` MCP -tool. It opens flow.google.com in the profile's browser and reads the balance the app itself -loads (the number in Flow's account menu, #795); the tier fields are always null there. The balance funds Veo video generation; image generation uses +tool. It reads the balance flow.google.com itself loads (the number in Flow's account menu, +#795): directly, with no browser, when Flow accepts the profile's saved session, otherwise in +the profile's browser (#1229; `GFLOW_CLI_WIRE_READS=0` always uses the browser); the tier fields are always null there. The balance funds Veo video generation; image generation uses separate per-model daily quotas. Log in with `gflow auth login --browser chrome`: a bundled- Chromium login (`--browser internal`, or `auto` without Chrome) works again (verified on Windows; macOS not measured), but generation still needs a chrome profile (#1026). diff --git a/plugins/gflow/skills/gflow-cli/SKILL.md b/plugins/gflow/skills/gflow-cli/SKILL.md index a4b076b6f..3c903805c 100644 --- a/plugins/gflow/skills/gflow-cli/SKILL.md +++ b/plugins/gflow/skills/gflow-cli/SKILL.md @@ -61,7 +61,9 @@ gflow auth # bare: list profiles gflow auth logout # delete a saved session # Veo video-credit balance (read-only; image models use separate daily quotas). -# Opens flow.google.com in the profile's browser and reads the balance Flow itself shows. +# Reads the balance Flow itself shows: directly (no browser) when the saved session is accepted, +# otherwise in the profile's browser. GFLOW_CLI_WIRE_READS=0 always uses the browser. +# Exit 4/6 = Flow is throttling the account (429/503): wait, do not retry in a loop. # Exit 3 = sign-in page (fix: --refresh); 31 = /about or no balance reply (NOT an auth # problem, do not loop on login); 11 = profile in use; 7 = unrecognised reply. Tier fields are null. gflow credits user [--profile NAME] [--refresh] [--json] diff --git a/skills/gflow-cli/SKILL.md b/skills/gflow-cli/SKILL.md index a4b076b6f..3c903805c 100644 --- a/skills/gflow-cli/SKILL.md +++ b/skills/gflow-cli/SKILL.md @@ -61,7 +61,9 @@ gflow auth # bare: list profiles gflow auth logout # delete a saved session # Veo video-credit balance (read-only; image models use separate daily quotas). -# Opens flow.google.com in the profile's browser and reads the balance Flow itself shows. +# Reads the balance Flow itself shows: directly (no browser) when the saved session is accepted, +# otherwise in the profile's browser. GFLOW_CLI_WIRE_READS=0 always uses the browser. +# Exit 4/6 = Flow is throttling the account (429/503): wait, do not retry in a loop. # Exit 3 = sign-in page (fix: --refresh); 31 = /about or no balance reply (NOT an auth # problem, do not loop on login); 11 = profile in use; 7 = unrecognised reply. Tier fields are null. gflow credits user [--profile NAME] [--refresh] [--json] diff --git a/website/docs/ARCHITECTURE.md b/website/docs/ARCHITECTURE.md index 6f892aa69..4da184994 100644 --- a/website/docs/ARCHITECTURE.md +++ b/website/docs/ARCHITECTURE.md @@ -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) +- a 3 s connect and 5 s overall deadline +- 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=` 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, because a browser read would send the same requests to a throttled account. Window length and 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 until the process exits, even after a `--refresh` renewal, so a long-running MCP server reads that profile in the browser until it restarts. 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 `GFLOW_CLI_BROWSER_PROXY` is set, 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. diff --git a/website/docs/E2E_TESTING.md b/website/docs/E2E_TESTING.md index 987c2ba92..c51fab35b 100644 --- a/website/docs/E2E_TESTING.md +++ b/website/docs/E2E_TESTING.md @@ -391,6 +391,7 @@ tests/ ├── test_migrated_selector_drift_e2e.py # [e2e, e2e_auth] registry vs flow.google.com — 0 credits ├── 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_credits_wire_bdd.py # [e2e, e2e_auth] credits read with no browser, fallback (#1229) — 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] diff --git a/website/docs/KNOWN_ISSUES.md b/website/docs/KNOWN_ISSUES.md index 204d4fc97..a13db06fc 100644 --- a/website/docs/KNOWN_ISSUES.md +++ b/website/docs/KNOWN_ISSUES.md @@ -14,6 +14,53 @@ 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](CONFIGURATION.md#gflow_cli_wire_reads)). If Flow ever answers signed out +or HTTP 401, gflow stops trying the direct read for that profile until the process exits. If +it answers HTTP 429 or 503, that profile is left alone for the throttle window and the command +reports the error (exit 4 or 6) rather than retrying in the browser. + +--- + ### 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 (`my-profile`, 2026-09-23). Other `/about` occurrences are not yet shown to share the cause. @@ -898,11 +945,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](https://github.com/ffroliva/gflow-cli/blob/main/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 diff --git a/website/docs/MCP.md b/website/docs/MCP.md index 41f65966f..a2b90ae68 100644 --- a/website/docs/MCP.md +++ b/website/docs/MCP.md @@ -127,7 +127,7 @@ The server registers three protocol surfaces: * `gflow_auth_status(profile)`: Credit-free, non-interactive Flow session probe (#497) — wraps the same fail-closed `verify_flow_profile` check as `gflow auth status`. Returns `{"status": "authenticated", "profile", "user_email"}` or a problem-details error with a `remediation_hint`; a `verification_error` outcome (HTTP 503, `retryable: true`) means a network/endpoint problem, which re-login does not fix, and a `profile_marker_missing` outcome (HTTP 409, `retryable: false`, #796) means the profile lost its `.gflow_browser_strategy` marker so its cookies cannot be read — a retry does not help, and neither does a plain `gflow auth login`; re-run `gflow auth login --browser chrome` for that profile. A `cookie_store_unreadable` outcome (HTTP 409, `retryable: false`, #796) means the profile's Chrome cookie database is corrupt or unreadable — a local file fault, not the network; close any Chrome window using that profile, then re-run `gflow auth login` for it. Any other unauthenticated outcome is a dead session (HTTP 401). Call it before a generation tool to fail fast on dead auth, before a long blocking generation call. Login/logout remain CLI-only (genuinely interactive). -* `gflow_get_credits(profile, all_profiles, refresh)`: Current Flow balance query; spends no credits. With `all_profiles=true`, inspects every saved profile sequentially and preserves successful balances plus `total_credits` when another profile fails. The balance is read from flow.google.com in the profile's browser: the number the app itself loads, which is the one Flow's account menu shows (#795). That is one browser launch per profile, and the tier fields (`sku`, `service_tier`, `user_paygate_tier`, `subscription_credits`) are always `null`. With `refresh=true`, a profile that lands on a sign-in page is renewed in a headless browser and read again: one more Chrome launch per such profile, no credits. This is the MCP twin of `gflow credits user|list --refresh`. It runs the same renewal as `gflow auth renew`, but only for a profile whose credits read lands on a sign-in page. `auth renew` itself has no MCP tool; see the exemption in `tests/mcp/test_cli_parity.py`. Without `refresh` the call is read-only; with it, an expired profile's saved cookies are rewritten. This tool spends no credits and returns no cookies, bearer tokens, or browser API keys. The balance funds Veo video generation; image generation uses separate per-model daily quotas. A page that loads but never fetches the balance is a `FlowAppError` (exit 31), never an auth error. +* `gflow_get_credits(profile, all_profiles, refresh)`: Current Flow balance query; spends no credits. With `all_profiles=true`, inspects every saved profile sequentially and preserves successful balances plus `total_credits` when another profile fails. The balance is the number flow.google.com itself loads, which is the one Flow's account menu shows (#795). It is read directly, with no browser, when Flow accepts the profile's saved session; otherwise it is read in the profile's browser, one browser launch per profile (#1229; when the direct read runs, and how to turn it off with `GFLOW_CLI_WIRE_READS=0`: [CONFIGURATION](CONFIGURATION.md#gflow_cli_wire_reads)). When Flow throttles the account (HTTP 429/503) the read for that profile fails with the rate-limit or network error rather than falling back to the browser, which would send the same requests. The tier fields (`sku`, `service_tier`, `user_paygate_tier`, `subscription_credits`) are always `null`. With `refresh=true`, a profile that lands on a sign-in page is renewed in a headless browser and read again: one more Chrome launch per such profile, no credits. This is the MCP twin of `gflow credits user|list --refresh`. It runs the same renewal as `gflow auth renew`, but only for a profile whose credits read lands on a sign-in page. `auth renew` itself has no MCP tool; see the exemption in `tests/mcp/test_cli_parity.py`. Without `refresh` the call is read-only; with it, an expired profile's saved cookies are rewritten. This tool spends no credits and returns no cookies, bearer tokens, or browser API keys. The balance funds Veo video generation; image generation uses separate per-model daily quotas. A page that loads but never fetches the balance is a `FlowAppError` (exit 31), never an auth error. * `gflow_character_list(project, profile)`: Lists a project's saved Flow CHARACTER entities with their **entity ids** — read-only, spends no credits, drives a browser session. This is how an agent discovers what it can attach: an `entity_id` goes to `reference_entities`, a `display_name` can be used as an `@Name` mention or goes to `reference_entity_names` at the id's position. On flow.google.com it cannot read the project yet and returns a `wire-format` error (the CLI's exit 7, [#875](https://github.com/ffroliva/gflow-cli/issues/875)): ask the user for the name. An empty list means the project genuinely has none. * `gflow_character_show(project, entity_id, name, profile)`: Shows one character by id or exact display name (exactly one selector required). An ambiguous name is refused rather than resolved arbitrarily — which is the reason to prefer the id. Read-only; drives a browser session. * `gflow_character_voices()`: Lists the preset voices available for Character TTS (name, description, sample URL). A static in-process lookup — no network, no browser, no profile, no cost. Call it to pick a valid voice name. diff --git a/website/docs/USAGE.md b/website/docs/USAGE.md index 5a9b76fff..959b1341d 100644 --- a/website/docs/USAGE.md +++ b/website/docs/USAGE.md @@ -129,11 +129,19 @@ Read the current Google Flow balance through an existing authenticated profile. read-only request: it does not generate media or spend credits. The displayed balance funds Veo video generation; image generation uses separate per-model daily quotas. -The balance is read from flow.google.com: gflow opens the app in the profile's browser and reads -the number the app itself loads, which is the one Flow's account menu shows -([#795](https://github.com/ffroliva/gflow-cli/issues/795)). Nothing is clicked. It costs one -browser launch per profile, and the profile must not be in use by another gflow process -(`ProfileLockedError`, exit 11). Before #795 the balance came from a labs.google token; labs' +The balance is the number flow.google.com itself loads, which is the one Flow's account menu +shows ([#795](https://github.com/ffroliva/gflow-cli/issues/795)). When Flow accepts the +profile's saved session, gflow reads it directly with that session's cookies: two requests, no +browser ([#1229](https://github.com/ffroliva/gflow-cli/issues/1229)). Otherwise (signed out, +Chrome open on the profile, a network error, an unexpected reply) gflow opens the app in the +profile's browser and reads it there. Nothing is clicked. The browser read costs one browser +launch per profile, and the profile must not be in use by another gflow process +(`ProfileLockedError`, exit 11). When Flow throttles the account (HTTP 429 or 503) gflow does not +open the browser, which would send the same requests: `credits user` exits 4 (`RateLimitError`) +or 6 (`NetworkError`), and `credits list` records that error for the profile. The direct read +is off by default on macOS and while `GFLOW_CLI_BROWSER_PROXY` is set; `GFLOW_CLI_WIRE_READS=0` +turns it off everywhere ([CONFIGURATION](CONFIGURATION.md#gflow_cli_wire_reads), +[KNOWN_ISSUES](../KNOWN_ISSUES.md)). Before #795 the balance came from a labs.google token; labs' session URL has answered HTTP 410 to every account since 2026-10-22 ([#1023](https://github.com/ffroliva/gflow-cli/issues/1023)). diff --git a/website/docs/USER_GUIDE.md b/website/docs/USER_GUIDE.md index 4405c0ca8..c169421d3 100644 --- a/website/docs/USER_GUIDE.md +++ b/website/docs/USER_GUIDE.md @@ -422,9 +422,12 @@ Your Google plan includes a finite Veo credit allowance — the video journeys s ### 10.1 Check the balance -Check the current Veo video-credit balance for the selected profile. gflow opens flow.google.com -in the profile's browser and reads the balance the app itself loads, the number in Flow's -account menu ([#795](https://github.com/ffroliva/gflow-cli/issues/795)). Nothing is clicked and +Check the current Veo video-credit balance for the selected profile. gflow reads the balance +flow.google.com itself loads, the number in Flow's account menu +([#795](https://github.com/ffroliva/gflow-cli/issues/795)): directly, with no browser, when Flow +accepts the profile's saved session, and otherwise in the profile's browser +([#1229](https://github.com/ffroliva/gflow-cli/issues/1229); off by default on macOS, see +[`GFLOW_CLI_WIRE_READS`](CONFIGURATION.md#gflow_cli_wire_reads)). Nothing is clicked and nothing is spent: ```bash From 37c39a0c9b38ec0b3e804adc105da2f8366bed7a Mon Sep 17 00:00:00 2001 From: Flavio Oliva Date: Tue, 10 Nov 2026 18:27:56 +0000 Subject: [PATCH 2/2] docs: CodeRabbit round on the direct credits read; mirror links/anchors fixed in the generator - Chrome open on the profile ends in ProfileLockedError on the browser read too (CHANGELOG, USAGE); credits user exits 4/6 on 429/503, credits list records it per profile (CHANGELOG, KNOWN_ISSUES) - ARCHITECTURE: the cookie read and the requests have separate 5 s limits - USER_GUIDE/README/AGENTS: the direct read is the one off by default on macOS - KNOWN_ISSUES/ARCHITECTURE: the signed-out mark is lifted by a verified --refresh renewal or an accepted doctor/gflow_auth_status probe (#1238) - FLOW_WIRE_RPCS: nzlxg is read directly by api/flow_wire.py, browser fallback - generate_website_docs: ../ links to a mirrored root page become sibling links, and relative anchors take the MkDocs slug (hyphen runs collapsed); tests added Refs #1229 --- AGENTS.md | 2 +- CHANGELOG.md | 9 ++-- KNOWN_ISSUES.md | 8 +-- README.md | 2 +- docs/ARCHITECTURE.md | 4 +- docs/FLOW_WIRE_RPCS.md | 2 +- docs/USAGE.md | 5 +- docs/USER_GUIDE.md | 9 ++-- scripts/ci/generate_website_docs.py | 14 +++++ tests/scripts/test_generate_website_docs.py | 18 +++++++ website/docs/ACCOUNT_SAFETY.md | 2 +- website/docs/ARCHITECTURE.md | 14 ++--- website/docs/AUTHENTICATION.md | 6 +-- website/docs/CHARACTER.md | 10 ++-- website/docs/CONFIGURATION.md | 6 +-- website/docs/DATA_LAYER.md | 2 +- website/docs/DEBUGGING.md | 10 ++-- website/docs/E2E_TESTING.md | 6 +-- website/docs/KNOWN_ISSUES.md | 10 ++-- website/docs/USAGE.md | 23 ++++---- website/docs/USER_GUIDE.md | 59 +++++++++++---------- 21 files changed, 131 insertions(+), 90 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cc4ddc7b0..f6394ebda 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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, #795 — directly, with no browser, when 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 ` 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 ` 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) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd7a5adf..1a4b6e1e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,11 @@ 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: two HTTP - requests and 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 (exit 4 or 6) - instead, since the browser would send the same requests. `GFLOW_CLI_WIRE_READS=0` turns the + 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 diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 61a41785a..f900c0ec7 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -55,9 +55,11 @@ in use (`ProfileLockedError`, exit 11) as before. Linux is unmeasured. **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, gflow stops trying the direct read for that profile until the process exits. If -it answers HTTP 429 or 503, that profile is left alone for the throttle window (`Retry-After`, capped at 60 s) and the command -reports the error (exit 4 or 6) rather than retrying in the browser. +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. --- diff --git a/README.md b/README.md index 7f52a2177..4b62949f7 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ gflow auth login --browser chrome # Check the session (spends nothing) gflow auth status -gflow credits user # Veo credit balance (no browser when the saved session is accepted; 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" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3c9109c18..8694a7083 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -427,7 +427,7 @@ Since [#1229](https://github.com/ffroliva/gflow-cli/issues/1229), one read, the - 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) -- a 3 s connect and 5 s overall deadline +- 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 @@ -435,7 +435,7 @@ Since [#1229](https://github.com/ffroliva/gflow-cli/issues/1229), one read, the **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 until the process exits, even after a `--refresh` renewal, so a long-running MCP server reads that profile in the browser until it restarts. One lock per profile keeps concurrent reads to one shell GET. One profile's state never affects another's. +**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). diff --git a/docs/FLOW_WIRE_RPCS.md b/docs/FLOW_WIRE_RPCS.md index 368e907cd..723da121b 100644 --- a/docs/FLOW_WIRE_RPCS.md +++ b/docs/FLOW_WIRE_RPCS.md @@ -45,7 +45,7 @@ a run that sent it cost nothing. | `maseQ` | Local file upload into the project | Toolbar `add` menu, `upload` item, file chooser | no | unknown | `[, , …]` | `UPLOAD_RPC`, `api/transports/migrated_composer.py:231` | no | 2026-09-05 | 2026-10-27 | live | - | yes (2026-11-10) | [frames spike](superpowers/spikes/2026-09-05-migrated-frames-attach.md) ($0 run), [upload spike](superpowers/spikes/2026-09-08-migrated-upload-fails-two-ways.md), [lost submit spike](superpowers/spikes/2026-10-27-lost-video-submit-recovery.md) | | `jHPbke` | Create a project | Projects page floating `add` button | no | no (captured `f.req` has no token slot) | `[, []]` | `CREATE_PROJECT_RPC`, `api/transports/migrated_composer.py:4282` | no | 2026-09-17 | 2026-09-17 | live | - | yes (2026-11-10) | [project spike](superpowers/spikes/2026-09-17-project-create-on-flow-google-com.md), [v0.78.0](LIVE_VERIFICATION_v0.78.0.md) | | `o8DA4` | Rename a project | Edit the title in the project header, then Enter | no | no (captured `f.req` has no token slot) | `[<title>]` | `RENAME_PROJECT_RPC`, `api/transports/migrated_composer.py:4284` | no | 2026-09-17 | 2026-09-17 | live | - | yes (2026-11-10) | [project spike](superpowers/spikes/2026-09-17-project-create-on-flow-google-com.md), [v0.78.0](LIVE_VERIFICATION_v0.78.0.md) | -| `nzlxg` | Credit balance and its daily refresh time | Page load | no | no (page-free read with cookies and `at`) | Balance at `[0]`, refresh time at `[6][0]` | `CREDITS_RPC`, `api/transports/migrated_composer.py:4570` | yes (2026-11-09) | 2026-09-05 | 2026-11-09 | live | - | yes (2026-11-10) | [wire spike](superpowers/spikes/2026-09-05-migrated-host-wire-protocol.md) (first seen), [balance spike](superpowers/spikes/2026-10-22-credits-balance-on-nzlxg.md), [v0.87.1](LIVE_VERIFICATION_v0.87.1.md); page-free: PR #1205 spike doc `2026-11-09-page-free-reads.md` | +| `nzlxg` | Credit balance and its daily refresh time | Page load | no | no (page-free read with cookies and `at`) | Balance at `[0]`, refresh time at `[6][0]` | `CREDITS_RPC`, `api/transports/migrated_composer.py:4570` (browser read); `_ALLOWED_RPCIDS`, `api/flow_wire.py:51` (read directly by `gflow credits` / `gflow_get_credits` with the profile's saved cookies, falling back to the browser read, #1229) | yes (2026-11-09) | 2026-09-05 | 2026-11-09 | live | - | yes (2026-11-10) | [wire spike](superpowers/spikes/2026-09-05-migrated-host-wire-protocol.md) (first seen), [balance spike](superpowers/spikes/2026-10-22-credits-balance-on-nzlxg.md), [v0.87.1](LIVE_VERIFICATION_v0.87.1.md); page-free: PR #1205 spike doc `2026-11-09-page-free-reads.md` | | `fZytfe` | Video extend submit | Extend prompt box submit on a clip | yes | yes (token in `f.req`) | unknown | `EXTEND_SUBMIT_RPC`, `api/transports/migrated_extend.py:67` | no | 2026-09-19 | 2026-11-08 | live | - | yes (2026-11-10) | [extend wire spike](superpowers/spikes/2026-09-19-migrated-video-extend-wire.md), [extend UI spike](superpowers/spikes/2026-10-17-migrated-extend-ui-path.md), [Omni extend spike](superpowers/spikes/2026-11-08-extend-no-scene-create-omni-key.md) | | `rqZuUc` | Create a scene from a clip's media id, sent before an extend | Extend submit on a clip (not sent on the 2026-11-08 run) | no | no (captured body has no token slot) | Scene record `[<scene id>, "<title>", …]` with one clip | `SCENE_CREATE_RPC`, `api/transports/migrated_extend.py:68` | no | 2026-10-17 | 2026-10-17 | live | - | yes (2026-11-10) | [extend UI spike](superpowers/spikes/2026-10-17-migrated-extend-ui-path.md) ($0), [v0.86.0](LIVE_VERIFICATION_v0.86.0.md), [Omni extend spike](superpowers/spikes/2026-11-08-extend-no-scene-create-omni-key.md) | | `uwAyfb` | Scene read, sent before the extend submit on the 2026-10-17 path; the 2026-11-08 extend submit went out without it | Extend submit on a clip, after `rqZuUc` (not sent on the 2026-11-08 run) | no | no (captured body has no token slot) | The scene's clips and the project's generation records | none (named in the docstring, `api/transports/migrated_extend.py:11`) | no | 2026-10-17 | 2026-10-17 | live | - | yes (2026-11-10) | [extend UI spike](superpowers/spikes/2026-10-17-migrated-extend-ui-path.md) ($0), [Omni extend spike](superpowers/spikes/2026-11-08-extend-no-scene-create-omni-key.md) | diff --git a/docs/USAGE.md b/docs/USAGE.md index 8243a95fc..63d8acd47 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -135,8 +135,9 @@ profile's saved session, gflow reads it directly with that session's cookies: tw browser ([#1229](https://github.com/ffroliva/gflow-cli/issues/1229)). Otherwise (signed out, Chrome open on the profile, a network error, an unexpected reply) gflow opens the app in the profile's browser and reads it there. Nothing is clicked. The browser read costs one browser -launch per profile, and the profile must not be in use by another gflow process -(`ProfileLockedError`, exit 11). When Flow throttles the account (HTTP 429 or 503) gflow does not +launch per profile, and the profile must not be in use by another gflow process or a Chrome +window (`ProfileLockedError`, exit 11): with Chrome open on the profile, both reads fail, so +close it and retry. When Flow throttles the account (HTTP 429 or 503) gflow does not open the browser, which would send the same requests: `credits user` exits 4 (`RateLimitError`) or 6 (`NetworkError`), and `credits list` records that error for the profile. The direct read is off by default on macOS and while any proxy is configured (`GFLOW_CLI_BROWSER_PROXY`, or an diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index c169421d3..8f5177995 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -424,10 +424,11 @@ Your Google plan includes a finite Veo credit allowance — the video journeys s Check the current Veo video-credit balance for the selected profile. gflow reads the balance flow.google.com itself loads, the number in Flow's account menu -([#795](https://github.com/ffroliva/gflow-cli/issues/795)): directly, with no browser, when Flow -accepts the profile's saved session, and otherwise in the profile's browser -([#1229](https://github.com/ffroliva/gflow-cli/issues/1229); off by default on macOS, see -[`GFLOW_CLI_WIRE_READS`](CONFIGURATION.md#gflow_cli_wire_reads)). Nothing is clicked and +([#795](https://github.com/ffroliva/gflow-cli/issues/795)): directly, with no browser, when the +direct read is on and Flow accepts the profile's saved session, and otherwise in the profile's +browser ([#1229](https://github.com/ffroliva/gflow-cli/issues/1229)). The direct read is off by +default on macOS, where the browser read is used; see +[`GFLOW_CLI_WIRE_READS`](CONFIGURATION.md#gflow_cli_wire_reads). Nothing is clicked and nothing is spent: ```bash diff --git a/scripts/ci/generate_website_docs.py b/scripts/ci/generate_website_docs.py index 4434b5076..3238f4fd8 100644 --- a/scripts/ci/generate_website_docs.py +++ b/scripts/ci/generate_website_docs.py @@ -141,6 +141,20 @@ def render(dest_name: str, source_text: str) -> str: text = text.replace(old, new) if _source_for(dest_name) == _REPO / dest_name: text = _rewrite_root_doc_links(text) + else: + # docs/X.md links a mirrored root file as ../NAME.md; on the site it is a sibling. + text = re.sub( + r"\]\(\.\./([^/)#?]+\.md)", + lambda m: f"]({m.group(1)}" if (_WEB / m.group(1)).is_file() else m.group(0), + text, + ) + # MkDocs' toc slug collapses a run of hyphens that GitHub keeps (`Ports & adapters` is + # `ports--adapters` on GitHub, `ports-adapters` on the site). Relative links only. + text = re.sub( + r"\]\((?!https?:)([^)#\s]*)#([^)\s]+)\)", + lambda m: f"]({m.group(1)}#{re.sub('-{2,}', '-', m.group(2))})", + text, + ) for pattern, repl in TOKEN_SUBS: text = pattern.sub(repl, text) return text diff --git a/tests/scripts/test_generate_website_docs.py b/tests/scripts/test_generate_website_docs.py index 5133323cb..184a05ef3 100644 --- a/tests/scripts/test_generate_website_docs.py +++ b/tests/scripts/test_generate_website_docs.py @@ -95,3 +95,21 @@ def test_nav_orphan_detection_flags_unlinked_pages( (web / "ORPHAN.md").write_text("x", encoding="utf-8") assert _gen._nav_orphans() == ["ORPHAN.md"] + + +def test_a_docs_link_to_a_mirrored_root_file_becomes_a_sibling_link() -> None: + """`docs/X.md` links `../KNOWN_ISSUES.md`; on the site KNOWN_ISSUES.md is a sibling page, + so `../` would leave the docs tree (PR #1237 review). Fragment and query survive.""" + out = _gen.render("USAGE.md", "see [KI](../KNOWN_ISSUES.md#a-b) and [C](../CHANGELOG.md)") + assert "](KNOWN_ISSUES.md#a-b)" in out + assert "](../CHANGELOG.md)" in out # not mirrored: left as it was + + +def test_a_relative_anchor_takes_the_mkdocs_slug() -> None: + """GitHub slugs `Ports & adapters` as `ports--adapters`; MkDocs' toc collapses the run to + `ports-adapters` (PR #1237 review). External links keep GitHub's anchor.""" + text = "[P](#ports--adapters) [U](USER_GUIDE.md#a--b) [G](https://github.com/x#a--b)" + out = _gen.render("ARCHITECTURE.md", text) + assert "](#ports-adapters)" in out + assert "](USER_GUIDE.md#a-b)" in out + assert "](https://github.com/x#a--b)" in out diff --git a/website/docs/ACCOUNT_SAFETY.md b/website/docs/ACCOUNT_SAFETY.md index 0a58bface..d4fc7e332 100644 --- a/website/docs/ACCOUNT_SAFETY.md +++ b/website/docs/ACCOUNT_SAFETY.md @@ -129,7 +129,7 @@ profile of the same account has succeeded the same day. 5. **Widen the jitter** when you resume. Full entry, including the structlog signature to confirm it: -[KNOWN_ISSUES § batchGenerateImages HTTP 403](../KNOWN_ISSUES.md). +[KNOWN_ISSUES § batchGenerateImages HTTP 403](KNOWN_ISSUES.md). ## What we cannot tell you diff --git a/website/docs/ARCHITECTURE.md b/website/docs/ARCHITECTURE.md index 3c9109c18..03cdfd1f2 100644 --- a/website/docs/ARCHITECTURE.md +++ b/website/docs/ARCHITECTURE.md @@ -94,7 +94,7 @@ class AuthStrategy(Protocol): - `mode="internal"` — explicit `InternalChromiumStrategy`. **RealChromeStrategy stealth design** (`real_chrome.py`): -- **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. +- **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 falls back to a headless `launch_persistent_context` when cookie decryption fails (DPAPI on Windows, keychain on macOS, libsecret on Linux). On flow.google.com a headless probe then asks My Account and, only if that request cannot complete, reads Flow's rendered workspace. 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. @@ -291,7 +291,7 @@ def generate(prompt: str) -> None: - **Single-process, single-event-loop** (`asyncio`). No multiprocessing, no application-level threads (only what Playwright uses internally). - **Page pool:** `FlowApiClient.__aenter__` opens `Settings.concurrency` Playwright Pages inside **one shared persistent `BrowserContext`**. Operations check out a Page via an `asyncio.Queue` (FIFO, bounded by `maxsize=N`); a double-checkin raises `QueueFull` loudly rather than corrupting the pool. See `gflow_cli.api.client.FlowApiClient._checkout_page` / `_checkin_page`. **No current CLI command drives more than one generation concurrently through this pool** — the one caller that used to fan out manifest entries via `asyncio.gather` (a manifest-driven video batch runner) never worked end-to-end and was removed as a nonfunctional stub; `gflow image batch` processes its manifest sequentially. -- **Across profiles:** parallelism by spawning one shell per profile. Chromium refuses two persistent contexts on the same `user-data-dir`, so cross-profile parallelism is a process-level concern, not a coroutine one. See [KNOWN_ISSUES § Same profile can't be used in parallel](../KNOWN_ISSUES.md#same-profile-cant-be-used-in-parallel). +- **Across profiles:** parallelism by spawning one shell per profile. Chromium refuses two persistent contexts on the same `user-data-dir`, so cross-profile parallelism is a process-level concern, not a coroutine one. See [KNOWN_ISSUES § Same profile can't be used in parallel](KNOWN_ISSUES.md#same-profile-cant-be-used-in-parallel). - **Why a Page pool and not a `Semaphore`?** A semaphore over a single shared Page would serialize at `page.request.post` anyway (Playwright doesn't make a Page thread-safe). N Pages inside one Context let Chromium pipeline the requests while still sharing the auth cookies. T0 of the Phase 4 spike measured ~45 ms per Page creation at N=16 — well under the 200 ms/Page threshold. - **What's backlog (post-v0.5):** a concurrent caller that actually exercises the Page pool above `N=1`, account-pool aware scheduling across multiple signed-in profiles (round-robin), and a separate-process driver so two concurrent invocations against the same profile can serialize properly via OS file lock. @@ -308,7 +308,7 @@ Process-boundary contextvars bind once: `correlation_id` (UUID4 per CLI invocati Format defaults to **human-readable on TTY**, **JSON when piped or `GFLOW_CLI_LOG_FORMAT=json`**. The exception renderer is constructed as `structlog.processors.ExceptionRenderer(structlog.tracebacks.ExceptionDictTransformer(show_locals=False))` — the verbose form makes the privacy guarantee visible at the call site (frame locals may contain auth cookies and signed URLs, never log them). -Pipes cleanly into `jq` / Loki / Datadog without configuration. See [`docs/USER_GUIDE.md` § Journey 6](USER_GUIDE.md#journey-6--read-the-structured-logs) for `jq` recipes. +Pipes cleanly into `jq` / Loki / Datadog without configuration. See [`docs/USER_GUIDE.md` § Journey 6](USER_GUIDE.md#journey-6-read-the-structured-logs) for `jq` recipes. ## Incident diagnostics (shipped, v0.43.0) @@ -427,7 +427,7 @@ Since [#1229](https://github.com/ffroliva/gflow-cli/issues/1229), one read, the - 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) -- a 3 s connect and 5 s overall deadline +- 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 @@ -435,11 +435,11 @@ Since [#1229](https://github.com/ffroliva/gflow-cli/issues/1229), one read, the **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 until the process exits, even after a `--refresh` renewal, so a long-running MCP server reads that profile in the browser until it restarts. One lock per profile keeps concurrent reads to one shell GET. One profile's state never affects another's. +**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). +**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. +**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 diff --git a/website/docs/AUTHENTICATION.md b/website/docs/AUTHENTICATION.md index a19e6a7bf..0b0783f08 100644 --- a/website/docs/AUTHENTICATION.md +++ b/website/docs/AUTHENTICATION.md @@ -218,7 +218,7 @@ 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. +[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 @@ -308,7 +308,7 @@ 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, +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 @@ -456,7 +456,7 @@ gflow image t2i "client work" --profile client-a Each profile is fully isolated (its own cookies, its own Flow project history). **Different profiles run fully in parallel** — each acquires its own cross-process `ProfileLease` on its own canonical directory, so concurrent `gflow` calls across profiles never contend. **The same profile can only ever have one active holder.** A cross-process advisory lock (`ProfileLease`, kernel `flock`/`msvcrt.locking`) enforces this fail-fast: a second call against an already-leased profile — whether it's another `gflow` invocation, the `gflow serve` daemon, or an MCP tool call — is rejected immediately with `ProfileLockedError` (exit code 11); it never blocks or waits for the lease to free up. -For automated multi-account work: a per-profile Page pool (`GFLOW_CLI_CONCURRENCY=N`, 1–16, shipped in v0.4.0a2) opens N Playwright Pages on one shared BrowserContext within a single profile, but no current CLI command drives more than one generation concurrently through it — the one caller that used to (a manifest-driven video batch runner) never worked and was removed. Cross-profile parallel runs remain "one shell per profile"; same-profile contention is a fast, typed rejection rather than a Chromium crash (see [KNOWN_ISSUES § Same profile can't be used in parallel](../KNOWN_ISSUES.md#same-profile-cant-be-used-in-parallel)). +For automated multi-account work: a per-profile Page pool (`GFLOW_CLI_CONCURRENCY=N`, 1–16, shipped in v0.4.0a2) opens N Playwright Pages on one shared BrowserContext within a single profile, but no current CLI command drives more than one generation concurrently through it — the one caller that used to (a manifest-driven video batch runner) never worked and was removed. Cross-profile parallel runs remain "one shell per profile"; same-profile contention is a fast, typed rejection rather than a Chromium crash (see [KNOWN_ISSUES § Same profile can't be used in parallel](KNOWN_ISSUES.md#same-profile-cant-be-used-in-parallel)). ## Refresh / expiry diff --git a/website/docs/CHARACTER.md b/website/docs/CHARACTER.md index 3b4cf3e17..230a7bc3b 100644 --- a/website/docs/CHARACTER.md +++ b/website/docs/CHARACTER.md @@ -3,7 +3,7 @@ > **Status:** SHIPPED (live-verified 2026-06-02) · **Issue:** [#145](https://github.com/ffroliva/gflow-cli/issues/145) · **Branch:** `feature/character-creation` > **Living document.** Evolve this as the feature and Flow's API evolve. Raw capture provenance lives in > [`CHARACTER_RECON.md`](CHARACTER_RECON.md); the cross-feature decision lens is -> [the REST-path capability matrix](#capability--cost). +> [the REST-path capability matrix](#capability-cost). > **Credit:** pattern-references @kittinan's avatar PR #123 (attach-flow idiom + CLI scaffolding shape) — but > built **greenfield off `develop`, NOT stacked on #123** (that branch is CONFLICTING and likeness-specific; > see [Design decisions](#11-design-decisions-post-predict)). @@ -12,14 +12,14 @@ > `@Name` prompt mentions is **shipped** (v0.40.0, [#344](https://github.com/ffroliva/gflow-cli/issues/344)) > for both `image` and `video` paths — see [REFERENCE_STRATEGIES](REFERENCE_STRATEGIES.md). An explicit > `--character <id>` **flag** on `gflow video` (a separate, repeatable-ref delivery mechanism) remains -> **not yet implemented** — see [Backlog](#14-backlog--not-yet-implemented). +> **not yet implemented** — see [Backlog](#14-backlog-not-yet-implemented). > **Status (2026-06-02, LIVE-VERIFIED):** the create saga + character-editor UI generation are **shipped** > and verified end-to-end on my-profile (face + front/side/back triptych body, both reference slots bound, > images downloaded, character read back). This document's selectors, editor URL, generation protocol > (Option B), `flow/entities` PATCH, and the CLI surface are **VERIFIED against shipped code**, not guesses. > The `nanopro` model picker is wired best-effort (non-fatal) and pending an explicit live confirmation — see -> [Backlog](#14-backlog--not-yet-implemented). +> [Backlog](#14-backlog-not-yet-implemented). ## 1. What it is @@ -282,7 +282,7 @@ both normalize to the canonical `Charon`) and sets `audioReferences[].presetVoic > performance on a `video t2v` / `r2v` call — put the direction in the prompt instead. > > The voice list is currently a **hardcoded constant**; fetching the live list from Flow's -> voice API is tracked in the [Backlog](#14-backlog--not-yet-implemented). A "create new voice" +> voice API is tracked in the [Backlog](#14-backlog-not-yet-implemented). A "create new voice" > flow ("Criar nova voz") exists in the UI but is **not yet implemented**. ## 8. CLI surface (shipped v0.12.0) @@ -337,7 +337,7 @@ never hard-code wire names in user-facing help — see [[cli-model-aliases-verif ## 10. Open items -See [§14 Backlog — not yet implemented](#14-backlog--not-yet-implemented) for the consolidated list. In +See [§14 Backlog — not yet implemented](#14-backlog-not-yet-implemented) for the consolidated list. In short: an explicit `--character <id>` flag on `gflow video` reuse (Phase 3; `@Name` prompt mentions already ship this on both image and video, see #344), live voice listing + `presetVoiceId` wire-case confirmation, custom-voice creation, `nanopro` picker live confirmation, diff --git a/website/docs/CONFIGURATION.md b/website/docs/CONFIGURATION.md index c38344722..398ecd4d3 100644 --- a/website/docs/CONFIGURATION.md +++ b/website/docs/CONFIGURATION.md @@ -331,7 +331,7 @@ Until the #1041 fix this setting was ignored and every command logged at `INFO` **What:** How long a command waits for another gflow process to release the profile lease before giving up with `ProfileLockedError` (exit 11). The default `0` keeps the historical fail-fast behavior. With a positive value, the waiter polls the kernel lock (0.5 s cadence) and simply takes over when the current holder — a CLI command or a `gflow serve` daemon task, both of which release at their natural end — finishes. Holders are never interrupted or asked to release early. **Values:** `0`–`3600` seconds (fractions allowed) **Default:** `0` (fail fast) for CLI commands; `180` under `gflow mcp run` / `gflow serve`, where no human is there to retry — unless you set it yourself, in the environment or a `.env` file. -**Note:** Same-process contention always fails fast regardless of this setting — the holder is the same process, so waiting would deadlock. See [KNOWN_ISSUES § Same profile can't be used in parallel](../KNOWN_ISSUES.md#same-profile-cant-be-used-in-parallel). +**Note:** Same-process contention always fails fast regardless of this setting — the holder is the same process, so waiting would deadlock. See [KNOWN_ISSUES § Same profile can't be used in parallel](KNOWN_ISSUES.md#same-profile-cant-be-used-in-parallel). **Shipped in:** #478; MCP default #864. ### `GFLOW_CLI_DB_PATH` @@ -405,7 +405,7 @@ consent or verification screen, the error tells you to run can see it, and it waits for you. A generation run does not: after clicking the chooser row it gives up within 30 seconds, so clearing a screen in a visible generation window is a race. (Flow's one-time *rights to use this image* confirmation -has its own remedy: see [KNOWN_ISSUES](../KNOWN_ISSUES.md).) +has its own remedy: see [KNOWN_ISSUES](KNOWN_ISSUES.md).) **What it does not do:** it does not stop Chrome **taking keyboard focus** when it launches: the `--no-focus-on-init` switch we tried made no measurable difference. If you are typing when a run starts, the @@ -545,7 +545,7 @@ per-command `--proxy` flags (the setting covers one-off runs: **Default:** `en-US` **Shipped in:** post-v0.8.1 develop (PR #51). **When to set it:** capturing locale-invariant DOM via `scripts/dev/capture_locale_invariants.py`, or live-verifying a generation under a non-EN account language. -**Important:** Chrome's *UI* language is independently forced to `en-US` via the `--lang=en-US` launch arg (so Flow keeps serving `/fx/tools/flow/` and the editor's localized text selectors keep working). This env var only affects request headers — not the editor UI you see. See [KNOWN_ISSUES § issue #24](../KNOWN_ISSUES.md) for the path to dropping `--lang=en-US`. +**Important:** Chrome's *UI* language is independently forced to `en-US` via the `--lang=en-US` launch arg (so Flow keeps serving `/fx/tools/flow/` and the editor's localized text selectors keep working). This env var only affects request headers — not the editor UI you see. See [KNOWN_ISSUES § issue #24](KNOWN_ISSUES.md) for the path to dropping `--lang=en-US`. ### `GFLOW_CLI_INCIDENT_CAPTURE` diff --git a/website/docs/DATA_LAYER.md b/website/docs/DATA_LAYER.md index 8f4ec04ce..5de6f7c11 100644 --- a/website/docs/DATA_LAYER.md +++ b/website/docs/DATA_LAYER.md @@ -223,7 +223,7 @@ async worker (which mirrors its `generation_queue.error_json` failure into - `failed` rows have **no automatic cap** by design — there is no background pruning. Use `gflow data errors prune --older-than <age>` for explicit retention and `gflow data errors export` to archive history first (see - [`data errors`](#gflow-data-errors--bounded-retention--export-345) below, + [`data errors`](#gflow-data-errors-bounded-retention-export-345) below, [#345](https://github.com/ffroliva/gflow-cli/issues/345)). Note `gflow data prune` is unrelated: it targets stale `local_files`, not failure history. diff --git a/website/docs/DEBUGGING.md b/website/docs/DEBUGGING.md index b98fbb445..660e62683 100644 --- a/website/docs/DEBUGGING.md +++ b/website/docs/DEBUGGING.md @@ -10,11 +10,11 @@ | Symptom | First thing to try | Read | |---|---|---| | Any operational failure (crash, drift, timeout, WAF, lock) | Open the incident bundle path printed with the error | [Automatic incident bundles](#automatic-incident-bundles) | -| `gflow image t2i` hangs ≥ 3 min then fails with `TimeoutError` | Re-run with `--verbose` and grep for `batch_response_seen` | [Listener log keys](#listener--http-layer-debugging) | +| `gflow image t2i` hangs ≥ 3 min then fails with `TimeoutError` | Re-run with `--verbose` and grep for `batch_response_seen` | [Listener log keys](#listener-http-layer-debugging) | | `aspect_ratio_set_failed` warning then wrong-aspect output | The aspect-tab selector cascade missed; capture a DOM snapshot of the gen-settings panel | [Inspecting Flow's live UI](#inspecting-flows-live-ui) | | `UnicodeEncodeError: 'charmap' codec can't encode` on Windows | Set `PYTHONUTF8=1` (PowerShell: `$env:PYTHONUTF8="1"`) before any `gflow` invocation | [Windows console](#windows-console-encoding) | | `AuthBrowserRejectedError` / exit 14 | Re-run `gflow auth login` (the `chrome` strategy retries automatically) | [`AUTHENTICATION.md`](AUTHENTICATION.md), `/gflow:known-issues` | -| `BrowserSessionClosedError` / exit 15 in a long-lived worker | Recreate the `FlowApiClient` via its async context manager | [Lifecycle errors](#lifecycle--browser-state) | +| `BrowserSessionClosedError` / exit 15 in a long-lived worker | Recreate the `FlowApiClient` via its async context manager | [Lifecycle errors](#lifecycle-browser-state) | | Test suite OOMs / sandbox crashes | Run dirs separately (`tests/api`, `tests/auth tests/cli`, `tests/features`, then the rest with `--ignore`) | [Test suite memory](#test-suite-memory) | | New Flow UI label breaks a selector | Add a candidate to `_ASPECT_TAB_CANDIDATES` (or the relevant cascade) and live-verify | [Selector cascades](#selector-cascades) | @@ -324,7 +324,7 @@ requests. Field data (issue [#241](https://github.com/ffroliva/gflow-cli/issues/ - A burst of ~14 image submissions in ~10 min ended in a **403** (`Flow returned 403; blocked by WAF or fingerprint check`, `PUBLIC_ERROR_UNUSUAL_ACTIVITY` — see - [KNOWN_ISSUES § batchGenerateImages HTTP 403](../KNOWN_ISSUES.md)). + [KNOWN_ISSUES § batchGenerateImages HTTP 403](KNOWN_ISSUES.md)). - After ~30 min of cooldown, individually-paced calls (45–120 s apart) on the same profile and project passed 4/4. @@ -349,7 +349,7 @@ Practical guidance: ## Common failure modes -See [`KNOWN_ISSUES.md`](../KNOWN_ISSUES.md) for the canonical list and +See [`KNOWN_ISSUES.md`](KNOWN_ISSUES.md) for the canonical list and remediations. The most-hit categories: - **Flow UI drift** → selector cascade needs a new candidate ([selector-cascades](#selector-cascades)) @@ -361,7 +361,7 @@ remediations. The most-hit categories: ## See also -- [`KNOWN_ISSUES.md`](../KNOWN_ISSUES.md) — canonical bug list +- [`KNOWN_ISSUES.md`](KNOWN_ISSUES.md) — canonical bug list - [`AUTHENTICATION.md`](AUTHENTICATION.md) — sign-in flow details - [`DEVELOPMENT.md`](DEVELOPMENT.md) — branching, PRs, quality gates - [`USAGE.md`](USAGE.md) — CLI surface diff --git a/website/docs/E2E_TESTING.md b/website/docs/E2E_TESTING.md index 4195d7051..9a662d7ce 100644 --- a/website/docs/E2E_TESTING.md +++ b/website/docs/E2E_TESTING.md @@ -166,7 +166,7 @@ test, not a mocked proxy — the Bug Lane's step 5. | `GFLOW_CLI_E2E_RPC_IMAGE` | *(unset)* | Path to an image you own, for `tests/e2e/test_migrated_rpc_readback_bdd.py` (#1009: a lost reply body is read back). Unset, that test **skips**. It creates one project, renames it and uploads this file; it spends no credits and no quota. | | `GFLOW_CLI_E2E_PROMPT` | *(safe default)* | Prompt override for the smoke test. | | `GFLOW_CLI_E2E_STALE_PROFILES` | *(unset)* | Comma-separated profiles whose session has **lapsed** (the credits read lands on a Google sign-in page, exit 3). 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 session stays fresh for a while. 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. | +| `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. | --- @@ -229,7 +229,7 @@ export GFLOW_CLI_E2E_PROFILE=<profile-name> GFLOW_CLI_E2E_RUN_VIDEO=1 uv run pytest -m e2e -v -p no:cov ``` -See [DEVELOPMENT.md § E2e gate](DEVELOPMENT.md#e2e-gate-before-merging-develop--main). +See [DEVELOPMENT.md § E2e gate](DEVELOPMENT.md#e2e-gate-before-merging-develop-main). --- @@ -637,6 +637,6 @@ flow.google.com clip lands in that bucket (#1003). ## See also - [CONTRIBUTING.md § Test categories](../CONTRIBUTING.md#test-categories) -- [DEVELOPMENT.md § E2e gate](DEVELOPMENT.md#e2e-gate-before-merging-develop--main) +- [DEVELOPMENT.md § E2e gate](DEVELOPMENT.md#e2e-gate-before-merging-develop-main) - [docs/DATA_LAYER.md](DATA_LAYER.md) - [docs/ARCHITECTURE.md § Testing topology](ARCHITECTURE.md#testing-topology) diff --git a/website/docs/KNOWN_ISSUES.md b/website/docs/KNOWN_ISSUES.md index fe6e53081..df906bbb5 100644 --- a/website/docs/KNOWN_ISSUES.md +++ b/website/docs/KNOWN_ISSUES.md @@ -55,9 +55,11 @@ in use (`ProfileLockedError`, exit 11) as before. Linux is unmeasured. **Remedy / opt-out:** set `GFLOW_CLI_WIRE_READS=0` to always read in the browser ([CONFIGURATION](CONFIGURATION.md#gflow_cli_wire_reads)). If Flow ever answers signed out -or HTTP 401, gflow stops trying the direct read for that profile until the process exits. If -it answers HTTP 429 or 503, that profile is left alone for the throttle window (`Retry-After`, capped at 60 s) and the command -reports the error (exit 4 or 6) rather than retrying in the browser. +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. --- @@ -856,7 +858,7 @@ Re-running `auth login` reuses the existing profile dir (you typically just clic **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](AUTHENTICATION.md#refresh--expiry). +and `auth renew` reports them as not renewed. See [docs/AUTHENTICATION.md § Refresh / expiry](AUTHENTICATION.md#refresh-expiry). --- diff --git a/website/docs/USAGE.md b/website/docs/USAGE.md index 560eec1ad..306bfdf33 100644 --- a/website/docs/USAGE.md +++ b/website/docs/USAGE.md @@ -74,7 +74,7 @@ Machine-readable output: the generation commands (`image t2i` / `image i2i`, `video t2v` / `i2v` / `r2v`), `auth list`, `credits user` / `credits list`, and `gflow models` accept `--json` to emit a single parseable object on stdout instead of Rich tables. See -[§ JSON output](#json-output---json). +[§ JSON output](#json-output-json). Note: `--profile NAME` is **per-subcommand**, not global — pass it after the subcommand name (e.g. `gflow image t2i "..." --profile experiments`, not `gflow --profile experiments image t2i ...`). @@ -135,14 +135,15 @@ profile's saved session, gflow reads it directly with that session's cookies: tw browser ([#1229](https://github.com/ffroliva/gflow-cli/issues/1229)). Otherwise (signed out, Chrome open on the profile, a network error, an unexpected reply) gflow opens the app in the profile's browser and reads it there. Nothing is clicked. The browser read costs one browser -launch per profile, and the profile must not be in use by another gflow process -(`ProfileLockedError`, exit 11). When Flow throttles the account (HTTP 429 or 503) gflow does not +launch per profile, and the profile must not be in use by another gflow process or a Chrome +window (`ProfileLockedError`, exit 11): with Chrome open on the profile, both reads fail, so +close it and retry. When Flow throttles the account (HTTP 429 or 503) gflow does not open the browser, which would send the same requests: `credits user` exits 4 (`RateLimitError`) or 6 (`NetworkError`), and `credits list` records that error for the profile. The direct read is off by default on macOS and while any proxy is configured (`GFLOW_CLI_BROWSER_PROXY`, or an environment/system proxy); `GFLOW_CLI_WIRE_READS=0` turns it off everywhere ([CONFIGURATION](CONFIGURATION.md#gflow_cli_wire_reads), -[KNOWN_ISSUES](../KNOWN_ISSUES.md)). Before #795 the balance came from a labs.google token; labs' +[KNOWN_ISSUES](KNOWN_ISSUES.md)). Before #795 the balance came from a labs.google token; labs' session URL has answered HTTP 410 to every account since 2026-10-22 ([#1023](https://github.com/ffroliva/gflow-cli/issues/1023)). @@ -158,7 +159,7 @@ then reads the balance again. That costs one more Chrome launch per signed-out p credits. A session Google has revoked cannot be renewed, so the row keeps its original diagnosis. A profile another gflow process is using is recorded as `ProfileLockedError` and the list carries on. `--refresh` does not clear Flow's public `/about` -page (see [KNOWN_ISSUES](../KNOWN_ISSUES.md)); that row keeps its original diagnosis. +page (see [KNOWN_ISSUES](KNOWN_ISSUES.md)); that row keeps its original diagnosis. `credits user` applies the normal profile precedence chain. `credits list` inspects every saved profile sequentially and returns successful balances even when another profile is expired or @@ -1041,7 +1042,7 @@ costs credits but never quality. > are concatenated, each internal seam is preceded by ~1 second of **frozen frame > and silence** as the shorter clip is padded into its 8s slot. A single-segment > extend is unaffected. See -> [KNOWN_ISSUES](../KNOWN_ISSUES.md#a-veo-extend-segment-is-7-seconds-not-the-8-flow-advertises--so-concat-pads-a-frozen-second) +> [KNOWN_ISSUES](KNOWN_ISSUES.md#a-veo-extend-segment-is-7-seconds-not-the-8-flow-advertises-so-concat-pads-a-frozen-second) > — render without `-o` and trim in post if the seam matters. ### What it produces @@ -1107,7 +1108,7 @@ previous clip**, giving visual continuity with no server-side stitching. > Without it, `gflow video chain` fails up front with exit `20` > (`FrameExtractionError`) before reading the manifest or prompting for cost — > on `gflow-cli` ≤ 0.74.0 it was a generic exit `1` instead, and Pillow had to be -> installed by hand (see [KNOWN_ISSUES.md](../KNOWN_ISSUES.md)). Install it with: +> installed by hand (see [KNOWN_ISSUES.md](KNOWN_ISSUES.md)). Install it with: > > ```bash > pip install 'gflow-cli[chain]' @@ -1146,7 +1147,7 @@ Options: > on the Veo 3.1 family pending a chain-scale verification. The chain also > aborts a link loudly (rather than reporting a fake success) if a generation > is observed routing to the text-only endpoint — see -> [KNOWN_ISSUES](../KNOWN_ISSUES.md). +> [KNOWN_ISSUES](KNOWN_ISSUES.md). ### JSONL manifest format @@ -1179,7 +1180,7 @@ between links) under `--out-dir` (or the default output dir). **Stitching the clips into a single video is a separate step — use `gflow scene` to concatenate them server-side.** Chain does not auto-concat: see the *deferred auto-concat* entry in -[KNOWN_ISSUES](../KNOWN_ISSUES.md) for why. +[KNOWN_ISSUES](KNOWN_ISSUES.md) for why. **Examples:** @@ -2233,9 +2234,9 @@ shell scripts can branch on the failure mode without parsing stderr. | `4` | `RateLimitError` | Quota / rate limit hit, exhausted retries | Wait + reduce `GFLOW_CLI_CONCURRENCY` | | `5` | `ContentPolicyError` | Flow rejected the prompt (200 + empty `media[]`) | Soften prompt wording | | `6` | `NetworkError` | Network failure persisted across 3 attempts. A failed download of media Flow already made (`MediaDownloadError`) is `retryable: false` ([#941](https://github.com/ffroliva/gflow-cli/issues/941)): use `gflow data download` instead of re-running — including a signed-media download whose connection dropped on every attempt ([#895](https://github.com/ffroliva/gflow-cli/issues/895)), or whose signed URL answered an HTTP error such as an expired link (exit 7 until #941 part 2) | Check connectivity — **but read the error's own `remediation_hint` first.** When a *generation* download fails this way the clip was already produced and billed: the hint names it, and `gflow data download <media_id>` recovers it for free. Re-generating pays twice | -| `7` | `WireFormatError` | Unexpected response shape — Flow API changed. **Also a refused video submit** (flow.google.com, [#1178](https://github.com/ffroliva/gflow-cli/issues/1178)): the submit was not the run asked for (a dropped frame, another asset) and gflow aborted it before it was sent — the hint then says *billed nothing*; without that hint it was already sent and may be billed, so check the project before re-running. **Also `RpcResponseUnavailableError`** (flow.google.com): Chrome lost the reply body of a project create, rename or upload ([#1009](https://github.com/ffroliva/gflow-cli/issues/1009)), or a project create or rename got no reply within 120 s ([#1037](https://github.com/ffroliva/gflow-cli/issues/1037)), and gflow could not confirm the outcome by reading the project back | File a bug (do NOT include captured tokens or signed URLs). **For `RpcResponseUnavailableError` the write may have saved:** open the project and check its title or the uploaded file before re-running, or you may get a duplicate (see [KNOWN_ISSUES](../KNOWN_ISSUES.md)) | +| `7` | `WireFormatError` | Unexpected response shape — Flow API changed. **Also a refused video submit** (flow.google.com, [#1178](https://github.com/ffroliva/gflow-cli/issues/1178)): the submit was not the run asked for (a dropped frame, another asset) and gflow aborted it before it was sent — the hint then says *billed nothing*; without that hint it was already sent and may be billed, so check the project before re-running. **Also `RpcResponseUnavailableError`** (flow.google.com): Chrome lost the reply body of a project create, rename or upload ([#1009](https://github.com/ffroliva/gflow-cli/issues/1009)), or a project create or rename got no reply within 120 s ([#1037](https://github.com/ffroliva/gflow-cli/issues/1037)), and gflow could not confirm the outcome by reading the project back | File a bug (do NOT include captured tokens or signed URLs). **For `RpcResponseUnavailableError` the write may have saved:** open the project and check its title or the uploaded file before re-running, or you may get a duplicate (see [KNOWN_ISSUES](KNOWN_ISSUES.md)) | | `8` | `AuthMissingError` | Required auth credential is absent from profile | `gflow auth login --profile <name>` | -| `9` | `TransportTimeoutError` | Browser/API operation exceeded its timeout (incl. an i2v frame UUID not found in the media picker, #287; or a wedged submission stage — the error names the stage and your Playwright version) | Retry; raise the relevant timeout. **If the detail says `clicking submit`, `not terminal`, `unreadable` or `was lost`, or `--json` shows `retryable: false`, the submit already went out ([#1010](https://github.com/ffroliva/gflow-cli/issues/1010)): check the project in Flow before re-running, because a re-run bills again.** A lost **image** reply is recovered from the project grid since v0.89.1 ([#1075](https://github.com/ffroliva/gflow-cli/issues/1075)), so exit 9 there means the grid did not show exactly the requested images. A lost **video** reply is followed to the clip when the run uploaded a file (`--ref`, `--initial-frame`, `--end-frame`; [#1112](https://github.com/ffroliva/gflow-cli/issues/1112)), so exit 9 with `unreadable` there means no clip, or more than one, carried the upload. A one-clip t2v run is followed by its exact prompt and model ([#1141](https://github.com/ffroliva/gflow-cli/issues/1141)), so `was lost` there means the project listing could not name exactly one clip; t2v with `--count` above 1 and character-only r2v still exit 9. For a frame-UUID miss verify the UUID belongs to the `--project` passed; for a `stage_stalled` abort check your Playwright is in range (see [KNOWN_ISSUES](../KNOWN_ISSUES.md)) | +| `9` | `TransportTimeoutError` | Browser/API operation exceeded its timeout (incl. an i2v frame UUID not found in the media picker, #287; or a wedged submission stage — the error names the stage and your Playwright version) | Retry; raise the relevant timeout. **If the detail says `clicking submit`, `not terminal`, `unreadable` or `was lost`, or `--json` shows `retryable: false`, the submit already went out ([#1010](https://github.com/ffroliva/gflow-cli/issues/1010)): check the project in Flow before re-running, because a re-run bills again.** A lost **image** reply is recovered from the project grid since v0.89.1 ([#1075](https://github.com/ffroliva/gflow-cli/issues/1075)), so exit 9 there means the grid did not show exactly the requested images. A lost **video** reply is followed to the clip when the run uploaded a file (`--ref`, `--initial-frame`, `--end-frame`; [#1112](https://github.com/ffroliva/gflow-cli/issues/1112)), so exit 9 with `unreadable` there means no clip, or more than one, carried the upload. A one-clip t2v run is followed by its exact prompt and model ([#1141](https://github.com/ffroliva/gflow-cli/issues/1141)), so `was lost` there means the project listing could not name exactly one clip; t2v with `--count` above 1 and character-only r2v still exit 9. For a frame-UUID miss verify the UUID belongs to the `--project` passed; for a `stage_stalled` abort check your Playwright is in range (see [KNOWN_ISSUES](KNOWN_ISSUES.md)) | | `10` | `WafRejectionError` | Flow security layer rejected the request | Change prompt/request and retry | | `11` | `ConfigurationError` | Local configuration or browser mode is invalid, including a malformed row in a `gflow run` config, an `image batch` manifest, a `movie` manifest or a `video chain` manifest (#935: `image batch` reported these as exit 2 before); a `--project` id Flow answers with its `/404?reason=project` page, meaning not found or not accessible to this account ([#974](https://github.com/ffroliva/gflow-cli/issues/974), [#1053](https://github.com/ffroliva/gflow-cli/issues/1053)); an output folder gflow cannot write to (`--out`, `GFLOW_CLI_OUTPUT_DIR`, `GFLOW_CLI_STORAGE_URI`), after a generation that succeeded: the asset is still in the Flow project ([#1091](https://github.com/ffroliva/gflow-cli/issues/1091); a generated video clip keeps exit 6); a cloud `GFLOW_CLI_STORAGE_URI` combined with `-o`, `video chain` or `movie run --stitch`, refused before billing ([#1008](https://github.com/ffroliva/gflow-cli/issues/1008)) — on the migrated `flow.google.com` host also a request the host cannot take as given (no `--project`, a model its menu does not offer, a `--duration` its settings pane renders no control for); includes `ProfileLockedError` (same-profile lease contention: another `gflow`/daemon/MCP call already owns this profile; also a Chrome window you opened yourself holding the profile, [#984](https://github.com/ffroliva/gflow-cli/issues/984) — close it or use another `--profile`), `ProfileAccessError` (Windows Chrome cannot write the profile directory; raised at the generation client's launch), and `ProfileEngineDowngradeError` (the profile was last written by a newer Chromium major than the bundled engine about to open it — see [AUTHENTICATION § Chromium downgrade guard](AUTHENTICATION.md#chromium-downgrade-guard)) | Fix the option/env var shown in the error; for lease contention wait, use a different `--profile`, or set `GFLOW_CLI_LEASE_WAIT_SECONDS=N` to wait bounded; for profile access denial grant write access to the complete profile directory or use a writable profile; upgrade gflow-cli/Playwright or re-run `gflow auth login` for a downgrade refusal | | `12` | `AuthLoginTimeoutError` | Browser sign-in was not completed in time | Re-run login or raise `GFLOW_CLI_AUTH_LOGIN_TIMEOUT` | diff --git a/website/docs/USER_GUIDE.md b/website/docs/USER_GUIDE.md index c169421d3..93206abda 100644 --- a/website/docs/USER_GUIDE.md +++ b/website/docs/USER_GUIDE.md @@ -13,22 +13,22 @@ ## Table of contents -- [Journey 1 — First-time setup (10 minutes)](#journey-1--first-time-setup-10-minutes) -- [Journey 2 — Your first video (single t2v)](#journey-2--your-first-video-single-t2v) -- [Journey 3 — Batch video from a prompt list (shell loop)](#journey-3--batch-video-from-a-prompt-list-shell-loop) -- [Journey 4 — Multi-image text-to-image fan-out](#journey-4--multi-image-text-to-image-fan-out) -- [Journey 5 — Image-to-image with reference images](#journey-5--image-to-image-with-reference-images) -- [Journey 6 — Reading structured logs (`jq` recipes)](#journey-6--reading-structured-logs-jq-recipes) -- [Journey 7 — Recovering from an `AuthExpiredError` mid-run](#journey-7--recovering-from-an-authexpirederror-mid-run) -- [Journey 8 — Switching between Google accounts (profiles)](#journey-8--switching-between-google-accounts-profiles) -- [Journey 9 — Migrating from `FLOW_CLI_*` to `GFLOW_CLI_*` (v0.3.x → v0.4.x)](#journey-9--migrating-from-flow_cli_-to-gflow_cli_-v03x--v04x) -- [Journey 10 — Budgeting credits before a batch run](#journey-10--budgeting-credits-before-a-batch-run) -- [Journey 11 — Wiring gflow outputs into a downstream pipeline](#journey-11--wiring-gflow-outputs-into-a-downstream-pipeline) -- [Journey 12 — Recovering from `ContentPolicyError` or `RateLimitError`](#journey-12--recovering-from-contentpolicyerror-or-ratelimiterror) -- [Journey 13 — Diagnosing a persistent `AuthExpiredError` after a successful re-login](#journey-13--diagnosing-a-persistent-authexpirederror-after-a-successful-re-login) -- [Journey 14 — Embedding `FlowApiClient` in a long-lived worker](#journey-14--embedding-flowapiclient-in-a-long-lived-worker) -- [Journey 15 — Sending generated assets to S3, MinIO, or GCS](#journey-15--sending-generated-assets-to-s3-minio-or-gcs) -- [Journey 16 — Consistent characters across a video (`@Name` mentions)](#journey-16--consistent-characters-across-a-video-name-mentions) +- [Journey 1 — First-time setup (10 minutes)](#journey-1-first-time-setup-10-minutes) +- [Journey 2 — Your first video (single t2v)](#journey-2-your-first-video-single-t2v) +- [Journey 3 — Batch video from a prompt list (shell loop)](#journey-3-batch-video-from-a-prompt-list-shell-loop) +- [Journey 4 — Multi-image text-to-image fan-out](#journey-4-multi-image-text-to-image-fan-out) +- [Journey 5 — Image-to-image with reference images](#journey-5-image-to-image-with-reference-images) +- [Journey 6 — Reading structured logs (`jq` recipes)](#journey-6-reading-structured-logs-jq-recipes) +- [Journey 7 — Recovering from an `AuthExpiredError` mid-run](#journey-7-recovering-from-an-authexpirederror-mid-run) +- [Journey 8 — Switching between Google accounts (profiles)](#journey-8-switching-between-google-accounts-profiles) +- [Journey 9 — Migrating from `FLOW_CLI_*` to `GFLOW_CLI_*` (v0.3.x → v0.4.x)](#journey-9-migrating-from-flow_cli_-to-gflow_cli_-v03x-v04x) +- [Journey 10 — Budgeting credits before a batch run](#journey-10-budgeting-credits-before-a-batch-run) +- [Journey 11 — Wiring gflow outputs into a downstream pipeline](#journey-11-wiring-gflow-outputs-into-a-downstream-pipeline) +- [Journey 12 — Recovering from `ContentPolicyError` or `RateLimitError`](#journey-12-recovering-from-contentpolicyerror-or-ratelimiterror) +- [Journey 13 — Diagnosing a persistent `AuthExpiredError` after a successful re-login](#journey-13-diagnosing-a-persistent-authexpirederror-after-a-successful-re-login) +- [Journey 14 — Embedding `FlowApiClient` in a long-lived worker](#journey-14-embedding-flowapiclient-in-a-long-lived-worker) +- [Journey 15 — Sending generated assets to S3, MinIO, or GCS](#journey-15-sending-generated-assets-to-s3-minio-or-gcs) +- [Journey 16 — Consistent characters across a video (`@Name` mentions)](#journey-16-consistent-characters-across-a-video-name-mentions) - [Common errors quick-reference](#common-errors-quick-reference) --- @@ -94,7 +94,7 @@ gflow auth status Expected: `Flow session verified as <your account>.` and exit code 0 — the command probes the live Flow session (no browser, no credits). If it exits 1, follow the printed hint (usually re-run `gflow auth login`). -You're done. Skip to [Journey 2](#journey-2--your-first-video-single-t2v). +You're done. Skip to [Journey 2](#journey-2-your-first-video-single-t2v). --- @@ -132,8 +132,8 @@ There is no CLI seed knob — an identical prompt + model can still vary between You want to render 20 clips overnight. `gflow video` has no `batch` subcommand — a manifest-driven runner was scaffolded early on but never worked and has been removed. (`gflow image batch` is a different, working -command — see [Journey 4](#journey-4--multi-image-text-to-image-fan-out) and -[Journey 10](#journey-10--budgeting-credits-before-a-batch-run).) For video, +command — see [Journey 4](#journey-4-multi-image-text-to-image-fan-out) and +[Journey 10](#journey-10-budgeting-credits-before-a-batch-run).) For video, drive each generation from a plain shell loop instead — one `gflow` invocation per prompt, sequential, so a bad row doesn't torpedo the rest. @@ -174,7 +174,7 @@ In-flight requests that Flow had already accepted when a failure occurs **may still consume credits** — Flow's private API does not give us a "cancel-and-refund" handshake. Inspect the output directory before rerunning; skip prompts whose expected `output_path` already exists (recipe -in [Journey 11](#journey-11--wiring-gflow-outputs-into-a-downstream-pipeline)). +in [Journey 11](#journey-11-wiring-gflow-outputs-into-a-downstream-pipeline)). By default each call opens its own Flow project; pass the same `--project <id>` to every call if you want the clips to land in one project instead @@ -312,7 +312,7 @@ jq -c 'select(.event == "error_unhandled") | .message_hash' events.jsonl | sort ## Journey 7 — Recovering from an `AuthExpiredError` mid-run -You started an overnight 50-clip shell loop (see [Journey 3](#journey-3--batch-video-from-a-prompt-list-shell-loop)). You wake up. Prompt 23 onward all failed with exit code 3. +You started an overnight 50-clip shell loop (see [Journey 3](#journey-3-batch-video-from-a-prompt-list-shell-loop)). You wake up. Prompt 23 onward all failed with exit code 3. ### 7.1 Diagnose @@ -424,10 +424,11 @@ Your Google plan includes a finite Veo credit allowance — the video journeys s Check the current Veo video-credit balance for the selected profile. gflow reads the balance flow.google.com itself loads, the number in Flow's account menu -([#795](https://github.com/ffroliva/gflow-cli/issues/795)): directly, with no browser, when Flow -accepts the profile's saved session, and otherwise in the profile's browser -([#1229](https://github.com/ffroliva/gflow-cli/issues/1229); off by default on macOS, see -[`GFLOW_CLI_WIRE_READS`](CONFIGURATION.md#gflow_cli_wire_reads)). Nothing is clicked and +([#795](https://github.com/ffroliva/gflow-cli/issues/795)): directly, with no browser, when the +direct read is on and Flow accepts the profile's saved session, and otherwise in the profile's +browser ([#1229](https://github.com/ffroliva/gflow-cli/issues/1229)). The direct read is off by +default on macOS, where the browser read is used; see +[`GFLOW_CLI_WIRE_READS`](CONFIGURATION.md#gflow_cli_wire_reads). Nothing is clicked and nothing is spent: ```bash @@ -468,7 +469,7 @@ Costs are not published by Google. The numbers below are **rough observations fr ### 10.3 Batch-cost math -For a 50-line `prompts.txt` of `gflow video i2v` clips (driven from the shell loop in [Journey 3](#journey-3--batch-video-from-a-prompt-list-shell-loop)) at ~1 credit / clip: +For a 50-line `prompts.txt` of `gflow video i2v` clips (driven from the shell loop in [Journey 3](#journey-3-batch-video-from-a-prompt-list-shell-loop)) at ~1 credit / clip: ```text 50 clips × ~1 credit/clip = ~50 credits. @@ -519,7 +520,7 @@ Silent overwrite semantics: `-o` overwrites any existing file at the target path When `-o` is omitted, default outputs and `--out-dir` flags produce **predictable paths**: If `GFLOW_CLI_STORAGE_URI` is set, generated asset bytes go to the configured -bucket prefix instead of local disk. Use [Journey 15](#journey-15--sending-generated-assets-to-s3-minio-or-gcs) +bucket prefix instead of local disk. Use [Journey 15](#journey-15-sending-generated-assets-to-s3-minio-or-gcs) for the storage setup, then use `gflow data media <media-id>` to retrieve the recorded `cloud_uri_N` values. @@ -549,7 +550,7 @@ ffmpeg -f concat -safe 0 -i "$DIR/concat.txt" -c copy "$DIR/_compiled.mp4" ### 11.5 Trim the prompt list to unrendered prompts only (skip-existing workaround) -The shell loop from [Journey 3](#journey-3--batch-video-from-a-prompt-list-shell-loop) doesn't skip-existing yet. To rerun only the prompts whose expected output doesn't exist yet, keep a sidecar mapping of prompt → expected filename and filter against it: +The shell loop from [Journey 3](#journey-3-batch-video-from-a-prompt-list-shell-loop) doesn't skip-existing yet. To rerun only the prompts whose expected output doesn't exist yet, keep a sidecar mapping of prompt → expected filename and filter against it: ```bash awk -F'\t' 'NR==FNR { done[$1]; next } !($0 in done)' rendered.txt prompts.txt > prompts.remaining.txt @@ -1053,6 +1054,6 @@ gflow video t2v "@Zoro alone on the cliff at night, rain" --project "$PROJECT" - [AUTHENTICATION.md](AUTHENTICATION.md) — full auth flow, session storage, multi-account details, refresh strategy. - [ARCHITECTURE.md](ARCHITECTURE.md) — modular monolith layout, per-worker Page pool, RFC 9457 Problem Details, retry layer. - [SECURITY.md](SECURITY.md) — threat model, redaction strategy, what's on disk. -- [KNOWN_ISSUES.md](../KNOWN_ISSUES.md) — workarounds for current limitations. +- [KNOWN_ISSUES.md](KNOWN_ISSUES.md) — workarounds for current limitations. _Built for Flow users who'd rather automate than click. Same Veo model, same quality, same billing — without the browser._