fix(browser-extension): patch-bump to 1.9.2 + perfect FF evaluate path

Revert 2.0.0 to 1.9.2 — internal refactor doesn't warrant a major bump.

Firefox MV3 evaluate fixes (1.9.2):
- Detect privileged URLs (about:, moz-extension:, view-source:, resource:,
  file:, chrome:) up front; raise an actionable error instead of letting
  scripting.executeScript silently return zero frames.
- runInPage now throws when results is empty OR results[0] is undefined
  (the latter signals a frame errored outside the wrapped try/catch —
  almost always page CSP blocking Function() in MAIN world). Old behavior
  returned undefined which JSON-stripped to a confusing {result:{}}.
- Runtime.evaluate always returns {result:{type,value}, value} with
  value coerced from undefined to null and a CDP-style exceptionDetails
  surfaced when evaluation errored.
- Replace the looksLikePromiseResult heuristic with explicit
  awaitPromise / await_promise parameter handling. The previous heuristic
  re-evaluated any empty-object result, masking real {} values.
This commit is contained in:
Hanzo AI
2026-05-08 09:53:56 -07:00
parent 11862defbc
commit 4c5a0bb064
6 changed files with 105 additions and 27 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hanzo-ai-monorepo",
"version": "1.8.8",
"version": "1.9.2",
"private": true,
"description": "Hanzo AI Development Platform Monorepo",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hanzo/browser-extension",
"version": "2.0.0",
"version": "1.9.2",
"description": "Hanzo AI Browser Extension",
"main": "dist/background.js",
"scripts": {
+100 -22
View File
@@ -64,6 +64,29 @@ interface CDPResponse {
const BROWSER_NAME = 'firefox';
const VERSION = typeof browser !== 'undefined' ? browser.runtime.getManifest().version : '0';
// URLs the extension can never run scripts in. browser.scripting.executeScript
// silently returns no frames for these, which used to surface as a mystery
// `{}` to the caller. We detect upfront and raise an actionable error.
const PRIVILEGED_URL_PATTERNS: RegExp[] = [
/^about:/i, // about:debugging, about:addons, about:config…
/^moz-extension:/i, // any extension's own pages
/^chrome:/i, // chrome://… (Firefox accepts these too)
/^view-source:/i,
/^resource:/i,
/^file:/i, // local files unless explicit perm granted
/^https?:\/\/addons\.mozilla\.org\b/i, // AMO blocks WebExtensions by policy
];
function isPrivilegedTab(tab: chrome.tabs.Tab | undefined): boolean {
if (!tab) return false;
const url = tab.url || (tab as any).pendingUrl || '';
if (!url) {
// Some Firefox builds redact url for privileged tabs; treat as privileged.
return true;
}
return PRIVILEGED_URL_PATTERNS.some((rx) => rx.test(url));
}
// Shared ZAP manager (replaces inline ZAP protocol code)
const zapMgr: ZapManager = createZapManager();
@@ -262,21 +285,22 @@ class HanzoFirefoxExtension {
/**
* Run JS in the page via executeScript and return the serialized result.
* Wraps result to avoid undefined → {} JSON serialization issue.
* Bumped to throw on empty/missing results so undefined → {} can no
* longer hide a privileged-tab or CSP failure (1.9.2).
*/
/**
* Detect a serialized Promise from executeScript: an empty object {} that
* came from awaiting a thenable. We can't do better in MV3 / Firefox because
* executeScript drops Promise values.
*/
private looksLikePromiseResult(value: unknown): boolean {
return value !== null
&& typeof value === 'object'
&& Object.keys(value as Record<string, unknown>).length === 0
&& !Array.isArray(value);
}
private async runInPage(tabId: number, code: string, timeoutMs: number = 10000): Promise<any> {
// Privileged URLs (about:, moz-extension:, view-source:, …) silently
// refuse executeScript. Detect upfront with an actionable error rather
// than letting the caller see a mystery `{}` response.
let tab: chrome.tabs.Tab | undefined;
try { tab = await browser.tabs.get(tabId); } catch { /* best-effort */ }
if (tab && isPrivilegedTab(tab)) {
throw new Error(
`Cannot run JS in privileged tab ${tabId} (${tab.url}). ` +
`Activate a regular http(s) tab via tabId/tab_id parameter.`,
);
}
// Wrap the user's code in a function body that ALWAYS has a top-level
// `return`. Without this `Function(body)()` discards the IIFE result and
// hands back undefined → JSON serializes to {} → caller can't tell
@@ -291,7 +315,23 @@ class HanzoFirefoxExtension {
}
`;
const results = await this.executeScriptWithTimeout(tabId, wrappedCode, timeoutMs);
if (!Array.isArray(results) || results.length === 0) {
throw new Error(
`executeScript returned no frames for tab ${tabId} — page may be ` +
`privileged, in a process the extension can't access, or unloaded.`,
);
}
// results is the per-frame mapped array of `r?.result`. With world: MAIN
// an `undefined` slot means a frame errored *outside* our try/catch
// (typical CSP `script-src 'self'` blocking `Function()`). Surface that
// explicitly instead of returning a silent `undefined`.
const result = results[0];
if (result === undefined) {
throw new Error(
`executeScript ran but returned no value (frame errored outside the ` +
`wrapped try/catch — most often page CSP blocking Function()). Tab ${tabId}.`,
);
}
if (result && typeof result === 'object' && result.__hanzo_error) {
throw new Error(result.__hanzo_error);
}
@@ -478,6 +518,10 @@ class HanzoFirefoxExtension {
// Accept both `expression` (CDP) and `code` (MCP/SDK convention).
const expression = (params.expression as string) ?? (params.code as string) ?? '';
const timeout = (params.timeout as number) || 10000;
// Caller can opt into Promise-await semantics. CDP names this flag
// `awaitPromise`; the MCP/SDK callers pass `await_promise`.
const awaitPromise = Boolean(params.awaitPromise ?? params.await_promise);
// If the caller already wrote a `return` or wrote a function/IIFE,
// wrap differently so we don't double-wrap and break syntax.
const trimmed = expression.trim();
@@ -486,19 +530,53 @@ class HanzoFirefoxExtension {
: trimmed.endsWith(';')
? `${trimmed} return undefined;`
: `return (${trimmed});`;
let value = await this.runInPage(tabId, code, timeout);
// Auto-await Promises: the page may evaluate to a Promise, but
// executeScript serializes it as `{}`. Probe for a `.then` and
// resolve in-page so we return the resolved value instead.
if (this.looksLikePromiseResult(value)) {
value = await this.runInPage(tabId, `
// When awaitPromise is requested, resolve in-page so executeScript's
// structured-clone serialization sees the resolved value rather than
// a Promise (which it can't transfer).
const evalCode = awaitPromise
? `
return Promise.resolve((function(){ ${code} })()).then(function(__v){
try { return JSON.parse(JSON.stringify(__v)); } catch(e) { return String(__v); }
});
`, timeout);
`
: code;
let value: unknown;
let evalError: string | null = null;
try {
value = await this.runInPage(tabId, evalCode, timeout);
} catch (e: any) {
evalError = e?.message || String(e);
value = null;
}
// Match CDP shape (legacy callers) AND surface `value` directly.
return { result: { value }, value };
// CDP-shape: { result: { type, value } } plus a top-level `value`
// alias for ergonomic callers. Coerce `undefined` → `null` so the
// JSON-encoded ZAP frame keeps the keys. If evaluation errored,
// surface CDP-style `exceptionDetails` AND a top-level `error` so
// both flavors of caller can see what happened.
const safeValue = value === undefined ? null : value;
const cdpType = safeValue === null
? 'object'
: Array.isArray(safeValue)
? 'object'
: typeof safeValue;
const out: Record<string, unknown> = {
result: { type: cdpType, value: safeValue },
value: safeValue,
};
if (evalError !== null) {
out.error = evalError;
out.result = {
type: 'object',
value: null,
subtype: 'error',
description: evalError,
};
out.exceptionDetails = { text: evalError };
}
return out;
}
// =====================================================================
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* CDP Bridge Server — DEPRECATED in @hanzo/browser-extension 2.0.0.
* CDP Bridge Server — DEPRECATED in @hanzo/browser-extension 1.9.1.
*
* The canonical 2-process architecture removes this node bridge from the
* critical path. Each Python `hanzo-mcp` now hosts a ZAP server directly
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "2.0.0",
"version": "1.9.2",
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
"permissions": [
"activeTab",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "2.0.0",
"version": "1.9.2",
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
"permissions": [
"activeTab",