Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions flow-agent/flow_engine/generators/i2v.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
26 changes: 26 additions & 0 deletions flow-agent/tests/test_extension_flow_urls.py
Original file line number Diff line number Diff line change
@@ -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

56 changes: 56 additions & 0 deletions flow-agent/tests/test_media_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import importlib
import json
import os
import re
import subprocess
import sys
from pathlib import Path
Expand Down Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions flow-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,12 @@ Chrome bridge for [kodelyx/flow-agent](https://github.com/kodelyx/flow-agent). I
4. Open <https://labs.google/fx/tools/flow>, 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)
35 changes: 24 additions & 11 deletions flow-extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
);

Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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'],
Expand All @@ -251,7 +266,7 @@ async function _getOrOpenFlowTab() {
}

scheduleFlowTabClose();
return retryTabs[0];
return createdTab;
}

async function getOrOpenFlowTab() {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 });
Expand Down
3 changes: 3 additions & 0 deletions flow-extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/*",
Expand All @@ -33,6 +34,7 @@
"content_scripts": [
{
"matches": [
"https://flow.google.com/*",
"https://labs.google/fx/tools/flow*",
"https://labs.google/fx/*/tools/flow*"
],
Expand All @@ -48,6 +50,7 @@
"injected.js"
],
"matches": [
"https://flow.google.com/*",
"https://labs.google/*"
]
}
Expand Down