diff --git a/flow-agent/flow_engine/generators/i2v.py b/flow-agent/flow_engine/generators/i2v.py index 2c09d97..63aa7c3 100644 --- a/flow-agent/flow_engine/generators/i2v.py +++ b/flow-agent/flow_engine/generators/i2v.py @@ -11,6 +11,14 @@ log = logging.getLogger("flow_engine.generators.i2v") +def _safe_error_text(value) -> str | None: + """Return a bounded scalar error without serializing arbitrary responses.""" + if not isinstance(value, (str, int, float)): + return None + text = " ".join(str(value).split()) + return text[:500] or None + + async def upload_image( bridge, image_path: str, @@ -52,11 +60,20 @@ async def upload_image( log.info("Uploading image: %s", os.path.basename(image_path)) result = await bridge.api_request(ENDPOINTS["upload_image"], body) - status = result.get("status", 0) - data = result.get("data", {}) + status = result.get("status", 0) if isinstance(result, dict) else 0 + data = result.get("data", {}) if isinstance(result, dict) else {} if status != 200: - err = data.get("error", {}).get("message", "Unknown") if isinstance(data, dict) else str(data) - err_msg = f"Image upload failed: {err}" + nested = data.get("error", {}) if isinstance(data, dict) else {} + nested = nested if isinstance(nested, dict) else {} + message = _safe_error_text(nested.get("message")) + google_status = _safe_error_text(nested.get("status")) + top_level_error = _safe_error_text(result.get("error")) if isinstance(result, dict) else None + detail = message or top_level_error or google_status or "Unknown error" + if message and google_status and google_status not in message: + detail = f"{message} ({google_status})" + status_text = _safe_error_text(status) + status_suffix = f" (status {status_text})" if status_text and status_text != "0" else "" + err_msg = f"Image upload failed{status_suffix}: {detail}" log.error("%s", err_msg) raise ValueError(err_msg) diff --git a/flow-agent/tests/test_extension_flow_urls.py b/flow-agent/tests/test_extension_flow_urls.py new file mode 100644 index 0000000..ad704e0 --- /dev/null +++ b/flow-agent/tests/test_extension_flow_urls.py @@ -0,0 +1,26 @@ +import json +from pathlib import Path + + +EXTENSION_DIR = Path(__file__).resolve().parents[2] / "flow-extension" + + +def test_manifest_allows_new_and_legacy_flow_pages(): + manifest = json.loads((EXTENSION_DIR / "manifest.json").read_text(encoding="utf-8")) + + assert "https://flow.google.com/*" in manifest["host_permissions"] + assert "https://flow.google.com/*" in manifest["content_scripts"][0]["matches"] + assert "https://flow.google.com/*" in manifest["web_accessible_resources"][0]["matches"] + assert "https://labs.google/fx/tools/flow*" in manifest["content_scripts"][0]["matches"] + + +def test_background_uses_precise_flow_page_eligibility_and_shared_tab_patterns(): + source = (EXTENSION_DIR / "background.js").read_text(encoding="utf-8") + + assert "'https://flow.google.com/*'" in source + assert "parsed.hostname === 'flow.google.com'" in source + assert "parsed.hostname !== 'labs.google'" in source + assert "return createdTab;" in source + assert "url: '*://labs.google/*'" not in source + assert source.count("url: FLOW_TAB_URLS") >= 3 + diff --git a/flow-agent/tests/test_media_storage.py b/flow-agent/tests/test_media_storage.py index 97dc02c..812c2f5 100644 --- a/flow-agent/tests/test_media_storage.py +++ b/flow-agent/tests/test_media_storage.py @@ -2,6 +2,7 @@ import importlib import json import os +import re import subprocess import sys from pathlib import Path @@ -216,6 +217,61 @@ async def api_request(self, endpoint, body, **kwargs): assert media_store.get_for_file(image_path, project_id="project-a")["media_id"] == "fresh-id" +@pytest.mark.parametrize( + ("response", "expected"), + [ + ( + {"status": 403, "error": "CAPTCHA_FAILED"}, + "Image upload failed (status 403): CAPTCHA_FAILED", + ), + ({"error": "Request timed out"}, "Image upload failed: Request timed out"), + ( + {"status": 500, "error": {"debug": {"internal": "sensitive-context"}}}, + "Image upload failed (status 500): Unknown error", + ), + ( + { + "status": 400, + "data": { + "error": { + "message": "The image could not be processed", + "status": "INVALID_ARGUMENT", + } + }, + }, + "Image upload failed (status 400): The image could not be processed (INVALID_ARGUMENT)", + ), + ], +) +def test_upload_image_preserves_safe_actionable_errors( + monkeypatch, tmp_path, response, expected +): + _configure_store(monkeypatch, tmp_path) + image_path = tmp_path / "reference.png" + image_path.write_bytes(PNG_BYTES) + + class Bridge: + async def api_request(self, endpoint, body, **kwargs): + return response + + with pytest.raises(ValueError, match=re.escape(expected)): + asyncio.run(upload_image(Bridge(), str(image_path), "project-a")) + + +def test_upload_image_still_accepts_successful_media_response(monkeypatch, tmp_path): + _configure_store(monkeypatch, tmp_path) + image_path = tmp_path / "reference.png" + image_path.write_bytes(PNG_BYTES) + + class Bridge: + async def api_request(self, endpoint, body, **kwargs): + return {"status": 200, "data": {"media": {"name": "uploaded-media-id"}}} + + media_id = asyncio.run(upload_image(Bridge(), str(image_path), "project-a")) + + assert media_id == "uploaded-media-id" + + def test_upload_is_reused_as_image_to_video_reference_without_duplicates(monkeypatch, tmp_path): history_path = _configure_store(monkeypatch, tmp_path) image_path = tmp_path / "start-frame.png" diff --git a/flow-extension/README.md b/flow-extension/README.md index 446470f..b9e1c84 100644 --- a/flow-extension/README.md +++ b/flow-extension/README.md @@ -19,4 +19,12 @@ Chrome bridge for [kodelyx/flow-agent](https://github.com/kodelyx/flow-agent). I 4. Open , sign in, and keep the tab open. 5. Click the extension icon to open Flow Agent in Chrome's side panel. +## Flow site compatibility + +The extension recognizes both the current `https://flow.google.com/` site and +the legacy `https://labs.google/fx/tools/flow` route. This is partial issue #10 +compatibility: authentication and CAPTCHA behavior, plus the existing REST and +upload calls on the new site, have not been verified end to end and may still +require follow-up changes. + Main documentation: [Flow Agent README](../README.md) diff --git a/flow-extension/background.js b/flow-extension/background.js index 3cfc6df..d1911d2 100644 --- a/flow-extension/background.js +++ b/flow-extension/background.js @@ -153,7 +153,7 @@ chrome.webRequest.onBeforeSendHeaders.addListener( // Notify whichever transport is active. sendToAgent({ type: 'token_captured', flowKey, clientId: extensionClientId }); }, - { urls: ['https://aisandbox-pa.googleapis.com/*', 'https://labs.google/*'] }, + { urls: ['https://aisandbox-pa.googleapis.com/*', 'https://labs.google/*', 'https://flow.google.com/*'] }, ['requestHeaders', 'extraHeaders'], ); @@ -162,7 +162,11 @@ let _openingFlowTab = false; // ─── On-demand tab lifecycle ──────────────────────────────── // Open the Flow tab only when real work needs it (token capture or captcha). // Keep it available in the background so user tabs are never redirected. -const FLOW_TAB_URLS = ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*']; +const FLOW_TAB_URLS = [ + 'https://flow.google.com/*', + 'https://labs.google/fx/tools/flow*', + 'https://labs.google/fx/*/tools/flow*', +]; const FLOW_URL = 'https://labs.google/fx/tools/flow'; let workTabId = null; let flowTabOpening = null; @@ -189,7 +193,16 @@ async function closeIdleFlowTab() { } function isFlowUrl(url) { - return !!url && FLOW_TAB_URLS.some((p) => new RegExp(p.replace(/\./g, '\\.').replace(/\*/g, '.*')).test(url)); + if (!url) return false; + try { + const parsed = new URL(url); + if (parsed.protocol !== 'https:') return false; + if (parsed.hostname === 'flow.google.com') return true; + if (parsed.hostname !== 'labs.google') return false; + return /^\/fx\/(?:[^/]+\/)?tools\/flow(?:\/|$)/.test(parsed.pathname); + } catch { + return false; + } } async function waitForTabComplete(tabId, maxWaitMs = 10000) { @@ -242,6 +255,8 @@ async function _getOrOpenFlowTab() { // Inject content script to make sure reCAPTCHA bridge is ready try { + const readyTab = await chrome.tabs.get(workTabId); + if (!isFlowUrl(readyTab?.url)) throw new Error('INVALID_FLOW_TAB'); await chrome.scripting.executeScript({ target: { tabId: workTabId }, files: ['content.js'], @@ -251,7 +266,7 @@ async function _getOrOpenFlowTab() { } scheduleFlowTabClose(); - return retryTabs[0]; + return createdTab; } async function getOrOpenFlowTab() { @@ -395,9 +410,7 @@ async function connectToAgent() { sendToAgent({ type: 'token_captured', flowKey, clientId: extensionClientId }); } else { console.log('[Flow Agent] open_flow_tab: token missing/expired, opening tab'); - const tabs = await chrome.tabs.query({ - url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'], - }); + const tabs = await chrome.tabs.query({ url: FLOW_TAB_URLS }); if (tabs.length) { await chrome.tabs.reload(tabs[0].id); console.log('[Flow Agent] Refreshed existing Flow tab'); @@ -696,6 +709,8 @@ async function requestCaptchaFromTab(tabId, requestId, pageAction) { if (!shouldInject) throw error; // Inject content script and retry + const tab = await chrome.tabs.get(tabId); + if (!isFlowUrl(tab?.url)) throw new Error('INVALID_FLOW_TAB'); await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'], @@ -783,7 +798,7 @@ async function handleUploadVideo(msg) { const { videoBase64, projectId, videoSize } = params; try { - const tabs = await chrome.tabs.query({ url: '*://labs.google/*' }); + const tabs = await chrome.tabs.query({ url: FLOW_TAB_URLS }); if (!tabs.length) { sendToAgent({ id, error: 'NO_FLOW_TAB' }); return; @@ -1091,9 +1106,7 @@ chrome.runtime.onMessage.addListener((msg, _, reply) => { } if (msg.type === 'OPEN_FLOW_TAB') { - chrome.tabs.query({ - url: ['https://labs.google/fx/tools/flow*', 'https://labs.google/fx/*/tools/flow*'], - }).then((tabs) => { + chrome.tabs.query({ url: FLOW_TAB_URLS }).then((tabs) => { if (tabs.length) { chrome.tabs.update(tabs[0].id, { active: true }); reply({ ok: true, tabId: tabs[0].id }); diff --git a/flow-extension/manifest.json b/flow-extension/manifest.json index 3436cac..afe70a7 100644 --- a/flow-extension/manifest.json +++ b/flow-extension/manifest.json @@ -19,6 +19,7 @@ ], "host_permissions": [ "https://labs.google/*", + "https://flow.google.com/*", "https://aisandbox-pa.googleapis.com/*", "https://aisandbox-pa.sandbox.googleapis.com/*", "https://storage.googleapis.com/*", @@ -33,6 +34,7 @@ "content_scripts": [ { "matches": [ + "https://flow.google.com/*", "https://labs.google/fx/tools/flow*", "https://labs.google/fx/*/tools/flow*" ], @@ -48,6 +50,7 @@ "injected.js" ], "matches": [ + "https://flow.google.com/*", "https://labs.google/*" ] }