1.9.25 shipped the unregistered app-hanzo client_id, so the PKCE token
exchange failed invalid_client — login has been dead since v1.8.0. The
canonical registered client is hanzo-browser (universe IAM init_data;
redirectUris include https://hanzo.ai/callback). Contracts pinning the
registered client land in shared-config + cross-browser-parity tests.
Adopted from fix/dxt-login-and-ci-guards: the DXT auth tool offered a
password-grant login defaulting to the unregistered app-hanzo client —
removed; whoami/token/account read /v1/iam/oauth/userinfo, refresh
exchanges at /v1/iam/oauth/token, issuance is authorization-code+PKCE
only. dxt build script now actually builds (mcp cli → server.js) instead
of echoing success. PR #6's test-suite restorations are not taken — main
already restored those suites.
Migrate the last llm.hanzo.ai references (vscode MCP llm tool provider URL
+ gateway fallback, auth-chat e2e doc comments) to the canonical
https://api.hanzo.ai/v1 (OpenAI-compatible, IAM JWT at the gateway — see
hanzoai/ai). The browser extension chat paths already pointed at
API_BASE_URL = api.hanzo.ai.
model-hub's HuggingFace tests are real live-API integration tests, which
means they fail on HF's weather: repeated runs rate-limit and the 5s
default timeout reads as 6 test failures with nothing of ours broken.
Probe HF once at module load and describe.skipIf with the status logged
when HF itself refuses service; give live calls a 15s budget. A failure
while HF answers 200 is still a real failure. 243/243 across repeated runs.
Three specs still tested the pre-1.9.3 popup (#open-page-overlay FAB,
'Open Chat Panel' label) — the Extension E2E jobs have been red on them
since the FAB was killed. Repointed at the current contract
(#overlay-enabled + #open-panel, 'Open Chat').
The interaction specs (overlay show, picker tool, console tool) all
funnel through the popup's active-tab lookup, which cannot work when the
harness hosts the popup as a tab: the popup IS the active tab, so the
privileged-tab guard always takes the fallback. Each spec now sends the
exact message its button sends (page.overlay.show / tool.picker.start /
tool.console.start), explicitly targeted at the example.com tab, so the
background relay and content-script surfaces are still exercised end to
end. CI's file set (popup, popup-tools, sidebar): 30/30 locally.
Fix the two failures the 1.9.24 tag surfaced:
- test/claude-integration.test.ts bound a fixed port 3002 in beforeEach
and raced the previous test's teardown — EADDRINUSE as an unhandled
'error', a 10s hook timeout, then ECONNREFUSED (flaked 3 of 4 local
runs). BrowserExtensionServer now exposes ready() (resolves on listen,
rejects on bind failure instead of crashing the process) and a port
getter; the test binds port 0 (OS-assigned), awaits ready(), and
terminates the client so wss.close() reliably fires. Six consecutive
green runs.
- CI Build and Publish packaged with the `zip` binary the arc runner
image doesn't have (Publish red since 1.9.23); Publish also wrote the
zips one directory above where its artifact globs and the Chrome Web
Store upload (which additionally pointed at an unversioned filename)
looked, and CI hardcoded the version from the root package.json.
Package with `npx web-ext build` (already in the job for linting),
version from the browser package.json everywhere, store upload path
fixed, and the Linux job no longer ships a junk safari zip — the real
bundle comes from the macOS job.
Root and browser package.json move in lockstep to 1.9.25 (root was left
at 1.9.23 by the 1.9.24 bump). npm + VS Code Marketplace already carry
1.9.24; the stores' first complete release of this line is 1.9.25.
shared/evaluable.ts is the single cross-browser rule for caller-supplied
JS: pickEvaluable accepts every code-param alias (expression/code/script/
function/js); wrapEvaluable passes bare expressions through and wraps
statement bodies in an async IIFE with the trailing value auto-returned.
Both dispatch paths consume it — browser-dispatch.ts (chrome.debugger
Runtime.evaluate, which also gains the plain 'evaluate' method alias
Firefox already accepted) and background-firefox.ts, whose previous
`return (${code})` wrap was a SyntaxError on any multi-statement body
and silently returned undefined for `expr;`. The Firefox path now always
settles promises in-page and routes rejections over the __hanzo_error
channel so callers see the real page-side error.
Tests: behavioral suite for the wrapper (evaluates wrapped output, not
string-matching); revive the three suites dead since the node bridge was
deleted — parity contracts repointed at browser-dispatch.ts, the IAM
contracts updated to the HIP-0111 canonical /v1/iam/oauth/token shape,
hub-wiring trimmed to the live background.ts surface, and the node-bridge
routing suite removed with its subject. 243 passing (was 186 + 3 dead
files).
connectNativeZap had no singleton guard: state.port was overwritten without closing the
prior port, and every onDisconnect scheduled another connect. Called from startup + the
3s reconnect, ports (+ their native hosts) accumulated into a host-spawn storm once the
router stopped rejecting duplicates. Now: tear down any prior port on entry; a replaced
port (state.port !== port) never reconnects. Exactly one native host per browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build.js stamps the package.json version into every manifest (chrome/firefox/safari)
so the three can never drift. Syncs src manifests to 1.9.21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
connectNativeZap (shared/native-zap.ts) registers the browser as a zapd provider
over native messaging and dispatches inbound ROUTE commands. Renames cdp-bridge.ts
-> browser-dispatch.ts; deletes the cdp-bridge-server.ts WS bridge. No ws://localhost,
no port roulette, no CDP fallback in the default boot path. Patch bump (no 2.0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ZAP discovery is unreliable for local hanzo-mcp: the server binds ephemeral
ports (not the probed [9999..9995]) and mDNS needs the unshipped
ai.hanzo.zap_mdns native helper. Re-enable ENABLE_LEGACY_TRANSPORTS so the
extension connects to the running CDP bridge (ws://localhost:9223/cdp +
HTTP :9224) for driving a logged-in Chrome profile. Builds on the 1.9.16-1.9.19
dispatch/CSP fixes. Bumps chrome+firefox manifests + package.json to 1.9.20.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hanzo.click: removed framework-specific hacks (Drupal AJAX lookup, jQuery
fallback). Those don't generalize — every new framework would need its
own hack. Replaced with a clean realistic-event-sequence dispatch:
mouseover → mouseenter → mousemove → mousedown → focus → mouseup → click
with proper clientX/Y/button/buttons/composed. Events still have
isTrusted=false (a WebExtension can't change that), so frameworks that
explicitly check isTrusted (Drupal AJAX behaviors, some React libs)
will still reject — those need the v1.10.0 BiDi backend on the Python
bridge to produce real trusted browser input via Firefox's
--remote-debugging-port WebDriver BiDi endpoint.
Target.activateTarget: now ALSO calls windows.update(focused:true) so
cross-Firefox-window tab switching actually brings the OS window to
the foreground (not just the tab within its current window).
hanzo.switchTab: new Layer-3 ergonomic alias. Same implementation as
Target.activateTarget but with a human-readable name. Recommended for
everyday tab switching in scripts and the MCP browser_tool wrapper.
Architectural note: hanzo.click is the synthetic path (best-effort for
sites that don't check isTrusted). When the v1.10.0 Python bridge BiDi
backend lands, calls to hanzo.click will auto-route to Input.bidi
.dispatchMouseEvent for trusted browser-input events. That is the
proper decomplected design — backend per trustedness level, ergonomic
alias on top. No per-framework hacks in the extension.
Two bugs caught while driving the SEC TCR form:
1) runFunc silently swallowed page-side errors.
scripting.executeScript returns [{result, error, frameId}] per frame.
We were mapping to r?.result which dropped the `error` field — when a
page-side function threw, callers saw null instead of an error.
Fix: keep the full InjectionResult shape and throw on first.error.
2) hanzo.fill called HTMLInputElement's value setter on a TEXTAREA.
That throws TypeError "Illegal invocation" silently (now visible
thanks to fix#1). The chained `|| HTMLTextAreaElement.prototype...`
never executed because the first ?.set was truthy.
Fix: per-tag dispatch — pick the matching prototype's native setter
(INPUT/TEXTAREA/SELECT each get their own). Wrapped in try/catch with
plain `el.value = value` as last resort.
Verified on www.sec.gov/forms/tcr-external-form:
- hanzo.fill on textarea[name^="in_your_own_words"] now sets the value
- hanzo.clear on same textarea now works
handleExtensionRequest() was returning "Unknown extension action" for
every Page.* / DOM.* / Input.* / hanzo.* method added in 1.9.16. The
handler had only routed monitor.*/audit.* and fell off the end for
everything else.
Fix: fall back to executeMethod (the canonical CDP-shaped dispatcher)
before returning Unknown. Strictly additive — all previously-working
actions continue through their existing branches.
Verified locally on www.sec.gov/forms/tcr-external-form: hanzo.listForm
returns 183 elements with full label association; hanzo.scroll +
hanzo.clickByText + hanzo.fillByLabel are now routable.
Next: a v1.10.x multi-backend Python bridge that adds WebDriver BiDi
(Firefox 129+), CDP (Chrome 124+), and safaridriver (Safari 17+) as
secondary backends for network interception, log streams, and other
capabilities the WebExtension scripting API cannot provide. Same wire
surface, prefix-based routing (BiDi.* / CDP.* / WD.*).
Decomplected three orthogonal layers:
L3: hanzo.* ergonomic aliases
L2: CDP-shape canonical primitives (Page.* DOM.* Input.* ...)
L1: runFunc(tabId, fn, args) → scripting.executeScript({world: MAIN, func: ref})
Every DOM op now passes a real function reference to scripting.executeScript
instead of constructing Function(codeStr)(). The Function() path is blocked
by strict page CSP on sec.gov / github.com / any default-src 'self' SPA.
Direct function refs are exempt from page CSP per the WebExtension spec, so
the new path works everywhere.
Added (all CSP-safe):
• hanzo.clickByText fillByLabel findByText listForm submitForm
• hanzo.press scroll waitForText waitForMutation
• hanzo.uploadFile dialogAccept observe/observeRead/observeStop
• Input.dispatchMouseEvent Input.scrollWheel
• DOM.querySelector DOM.scrollIntoView DOM.focus
• Page.printToPDF
Rewrote with runFunc (CSP-safe):
• hanzo.click fill check uncheck clear getText getHTML
• hanzo.querySelectorAll
Capabilities handshake bumped to advertise the new method names.
Driver-side mapping in hanzo-tools-browser/browser_tool.py updated with
~100 snake_case action → wire-method entries (Python MCP surface).
API reference: dist/browser-extension/firefox/API.md
Reverts violations of the durable rule that current-state docs belong
in LLM.md and history belongs in git log. Removed files were session
handoffs, agent-style "complete success" / "1000%" reports, dated
audit dumps, and stub NOTES.
Fixes the browser ↔ hanzo-mcp ZAP path end to end:
- shared/zap.ts: vendor ZAP_PORTS constant and restore localhost
port-probe fallback in discoverZapServers when the mDNS native
helper isn't available. Sequential probe (stops on first hit) so
the Chrome console stays quiet on the happy path.
- src/manifest.json: add deterministic ``key`` (extension ID
``biingenefmanpecedoafkfajbnlgdmbl``) + ``nativeMessaging``
permission so the ``ai.hanzo.zap_mdns`` helper can be invoked.
- background.ts: configurable HF model source for the WebGPU
runtime via chrome.storage (``hanzo_model_url`` or
``hanzo_model_hf={repo,file,revision}``); falls back through HF →
bundled artifact → remote providers.
- webgpu-ai.ts: cache-first fetch backed by the service-worker
Cache API + progress logs, ``WebGPUAI.huggingFaceURL`` helper, and
``WebGPUAI.evictCache`` for rotating model artefacts. Drop-in for
the existing single-blob loader; lays groundwork for sparse-load
MoE work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns the standalone MCP server's auth path with the canonical IAM_
env contract enforced by hanzo-iam 1.30.0 (python-sdk 3cb6a34) — there
is exactly one prefix, IAM_, with no upstream-brand alias chain.
- HANZO_IAM_ENDPOINT → IAM_ENDPOINT
- HANZO_IAM_LOGIN_URL → IAM_LOGIN_URL
- HANZO_IAM_CLIENT_ID → IAM_CLIENT_ID
- HANZO_IAM_CLIENT_SECRET → IAM_CLIENT_SECRET
Updates docs/BUILD.md and docs/MCP_INSTALLATION.md to match.
Module-load WS connections to ws://localhost:3001/browser-extension
(legacy MCP) and ws://localhost:9223/cdp (legacy CDP bridge) spam
ERR_CONNECTION_REFUSED on every reload when no local hanzo-mcp / CDP
server is running.
ZAP mDNS discovery (startZapDiscoveryLoop / discoverZapServers, added
in 1.9.13–1.9.15) is now the canonical transport, so the legacy paths
are dead-on-load by default.
Adds ENABLE_LEGACY_TRANSPORTS = false (top of background.ts) and gates:
- connectToMCP() body (early-return when off)
- The module-load call site for connectToMCP()
- The module-load call site for cdpBridge.startWebSocketServer(cdpPort)
Flip the flag to true when developing against a local WS server.
ZAP path is untouched.
Firefox 151 warns on `data_collection_permissions` — that key is not
yet in Firefox's accepted MV3 manifest schema. The warning was
sufficient to block clean loading of native-messaging permissions in
some contexts (visible in about:debugging as "Reading manifest:
Warning processing data_collection_permissions"), which broke our
mDNS discovery via runtime.sendNativeMessage with an opaque
"unexpected error".
Removed the field. No data-collection semantics depend on it on the
Firefox side; Chrome MV3 still ignores unknown keys.
Per HIP-0069 — extension discovers MCPs via mDNS only, no port-probe
fallback. The legacy [9999..9995] DEFAULT_ZAP_PORTS array is gone;
non-compliant deployments (no mDNS responder, no native-messaging
helper installed) fail loudly so the operator can fix the install
rather than the extension silently picking the wrong server.
shared/zap.ts:
- Replaced DEFAULT_ZAP_PORTS export with HANZO_SERVICE_TYPE constant
('_hanzo._tcp.local.')
- discoverZapServers: mDNS-only, retries every 10s when the helper
is unavailable or the browse comes back empty
- discoverViaMdns: throws on missing native-messaging API instead of
swallowing — surfaces the install gap
background.ts:
- Removed DEFAULT_ZAP_PORTS / SHARED_ZAP_PORTS imports + zapPorts
storage key from getPortConfig
- Both discovery callsites pass undefined for ports (mDNS routes them)
The 1.9.11 alarms-based wake-up worked but felt fragile — the background
was still idling out between alarms. Switching primary keep-alive to
the canonical MV3 Port pattern: every content script in every loaded
http(s) tab opens a `zap-keepalive` chrome.runtime port back to the
background and pings it every 10s. As long as ANY tab holds a port,
the background event-page (Firefox) / service worker (Chrome) stays
loaded indefinitely. The WebSocket survives without polling.
Alarms remain as belt-and-suspenders for the no-tab edge case (fresh
browser window, only privileged pages open).
Package metadata corrected: monorepo root is now `@hanzo/extension`
with description "Hanzo AI Extension" — preparing the surface for
publishing the bits people actually want (zap shared module,
gui-primitives shim, mDNS package).
Root cause of the "connect once, never reconnect" pattern: Firefox MV3
treats `"background.scripts"` as event pages and Chrome MV3 service
workers idle out after ~30s. Once the background unloads, every
setTimeout (including our connectZap retry) silently dies. The first
connect happens on initial load; once that WS closes the script
suspends and never comes back.
Fix: register a `chrome.alarms` / `browser.alarms` 30s alarm. Alarm
events wake the worker/page even when suspended. The handler is a
no-op when the ZAP connection is healthy and triggers
discoverZapServers() when it isn't — closing the gap that the
WebSocket reconnect chain alone could not.
Adds `alarms` permission to both Firefox and Chrome manifests.
ZAP is a long-lived WebSocket. The OS delivers FIN/RST on real
disconnects, which fires ws.onclose → the reconnect chain shipped in
1.9.7. The 20s/8s PING/PONG dance added in 1.9.9 was both unnecessary
and broken: it overwrote ws.onmessage on every tick, stacking closures
indefinitely until the socket closed.
Reverted. The transport stays simple: connect, stream, on close
reconnect. No polling, no wrapper chain.
shared/zap.ts:
Every successful connectZap now arms a 20-second PING heartbeat. If no
PONG arrives within 8s the socket is force-closed so the existing
reconnect chain (1.9.7) takes over. This catches the "half-open"
failure mode where neither side gets ws.onclose — typical when the
agent process is killed -9, a laptop sleeps mid-WS, or NAT keep-alive
drops a stale connection between LAN hops.
ws.onclose now also clears the heartbeat interval, so we never PING a
socket whose owner is in the middle of being torn down.
Combined with 1.9.7's onerror retry and 1.9.8's MAIN→ISOLATED CSP
fallback + 30s evaluate timeout, the 1.9.x line is rock-solid for
low-latency tab driving across MCP restarts, page CSP, and any
hand-off between agents.
executeScript now tries world: 'MAIN' first (so callers reading
page-internal state still work) and silently falls back to ISOLATED on
the recognisable CSP-blocked-Function() error string. This rescues every
strict 'script-src self' page (banking sites, Porkbun account pages,
GitHub, Google Docs) without forcing the caller to know which world it
needs.
Default Runtime.evaluate timeout bumped from 10s → 30s. Page-walks
against jQuery-rendered domain-management UIs with 1.5k+ DOM nodes
routinely need more than 10s for the first selector pass; caller can
still override via params.timeout for short queries.
Together with 1.9.7's reconnect-chain fix the extension can now drive
any Firefox tab end-to-end: stays glued to its ZAP server through
restarts, evaluates JS regardless of page CSP, and exposes the result
on a clean shape (1.9.2's null-coercion + exception-details wiring).
connectZap previously chained a 3s reconnect on ws.onclose only. When the
WebSocket failed BEFORE a successful handshake (typical case: agent
process restarting and rebinding port 9999), Firefox hit ws.onerror →
resolve(null) → the reconnect chain died after a single attempt.
Now both onclose AND onerror schedule the same idempotent retry (guarded
by a one-shot flag so we never double-fire). Firefox stays glued to its
ZAP server across MCP restarts, server upgrades, and any transient bind
window — exactly what we need for low-latency tab driving.
Discovery (shared/zap.ts):
discoverZapServers now tries mDNS via the native-messaging helper
`ai.hanzo.zap_mdns` first (op=browse, 1.5s timeout), falling back to
the legacy `[9999..9995]` port-probe only when the helper isn't
installed or returns no services. With the helper in place we get
collision-free LAN-wide discovery — the same agent can be found
whether it's on this host or another machine on the network.
See ~/work/zap/mdns/ for the helper + manifest:
python/zap_mdns.py publish() / browse() (zeroconf-backed, no-op
fallback when zeroconf isn't installed)
python/omni.py roles: mcp / browser / host / gateway over
shared `_hanzo._tcp.local.` service-type
ext/helper.py stdio native-messaging host
ext/native-messaging-host.json manifest for Firefox NMH path
Manifest:
Adds `nativeMessaging` permission so the helper can be reached.
Popup:
Footer now displays "Hanzo AI v1.9.6" (read from manifest at runtime
so the chip always tracks the actual loaded build).
The extension no longer depends on @hanzo/ui. The single React consumer
(chat-widget.tsx) now imports from `@hanzo/gui`, which the build aliases
to a small local primitives shim at `src/gui-primitives.tsx`. The shim is
strictly monochrome plain DOM (Button, Textarea, Input, Card*, XStack,
YStack, Text) — sized for content-script bundles. When the full tamagui-
based @hanzo/gui adoption ships in a feature branch, swap the alias
target in build.js and remove the shim.
Why the local shim and not the upstream @hanzo/gui (npm) right now:
- the upstream is tamagui-flavoured: react-native-web + @hanzogui/*
packages + the static compiler — far too heavy for the per-page
content-script + background bundles we ship today;
- @hanzo/brand is strict monochrome anyway, so primitives carry no
colour tokens — sibling CSS (popup.css/sidebar.css) does the styling
with the white-luminance scale committed in 1.9.4.
Surface change: every `@hanzo/ui/primitives-common` import is now
`@hanzo/gui`, build.js no longer probes a sibling @hanzo/ui repo, and
`primitives-common-fallback.tsx` is replaced by the canonical
`gui-primitives.tsx`. Bundle drops ~200KB across firefox + chrome from
not pulling the @hanzo/ui dist.
Also includes the in-flight ZAP-bridge / Firefox auth / sidebar
auto-mount tweaks that were already in working-tree.
@hanzo/brand is strictly monochrome (primaryColor #FFFFFF, no chromatic
accents). Stripped every blue/violet/emerald/amber/red literal across
the in-page overlay, sidebar, and popup; replaced with a luminance scale
keyed off white. Status states (success/warn/error) now distinguished
by opacity, not hue.
On-page overlay (content-script):
- Replaces the brittle `looksLikePromiseResult` blue button with a white
primary action (#fff bg / #0a0a0b text) per brand guidelines.
- Composer is now sticky-bottom: messages flex/scroll, composer
flex-shrink: 0, panel min-height: 0 lets the layout collapse cleanly.
- Adds a pin-mode toggle in the header. `mode=push` reflows page content
(sets html.hanzo-overlay-push-{side} → margin-{side}: var(width));
`mode=overlay` floats over the page (default).
- Adds setMode / setWidth / setEnabled message handlers and persists all
three in chrome.storage.sync alongside the existing overlaySide.
- Picks up overlayWidth (compact 320 / default 420 / wide 560) via
--hanzo-overlay-width CSS var, used by both the panel itself and the
push-mode html margin.
Popup:
- "Open Chat" honours overlayEnabled — falls back to standalone tab
when disabled (or when active tab is privileged).
- Adds On-page chat panel checkbox + Width picker (Compact/Default/Wide)
alongside the existing Pin Left/Right.
CSS / brand cleanup:
- sidebar.css usage bars (.bar-blue/.bar-violet/...) → white opacity scale.
- model-hub badges (.badge-cloud/.badge-local/.badge-hub) → mono-luminance.
- inspector-result element-{tag,id,class} → mono-luminance instead of
blue/violet/green syntax tints.
- --success / --warning / --error in both stylesheets → white shades.
- Remove the round "AI" FAB launcher from the in-page overlay. It rendered
as a 54px circle bottom-right and looked out of place; once toggled on
it was sticky because hide() never reset data-enabled. Removed the
button + CSS + listener entirely.
- Reposition the overlay as a full-height edge-pinned sidebar panel
(top:0; bottom:0; right:0; width:min(420px,100vw)) instead of the old
floating bottom-right card. Slides in via translateX rather than
translateY+scale so it reads as a sidebar.
- Add data-side="left|right" with persisted overlaySide setting in
chrome.storage.sync. Popup gains a Pin Left | Pin Right picker that
flips the side on the fly and stores the preference for next page load.
- Unify the popup's two old buttons (Open Chat Panel + Toggle On-Page
Overlay — both did the same thing in different words) into a single
Open Chat button.
- Register sidebar_action in manifest-firefox.json so Firefox users who
prefer the OS-native sidebar can reach the same chat surface via
View → Sidebar → Hanzo AI.
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.
The Python hanzo-mcp now hosts the ZAP server directly. This extension
extends the shared ZAP module so Python servers can dispatch browser
RPCs back to the extension over the same socket — closing the loop on
the 2-process architecture.
- shared/zap.ts: handle inbound MSG_REQUEST and respond with
MSG_RESPONSE. New setZapRequestHandler(mgr, fn) installs the
dispatcher.
- background.ts (Chrome): wire cdpBridge.dispatchMethod as the inbound
ZAP handler.
- background-firefox.ts: expose dispatchMethod and wire it as the
inbound ZAP handler. Reorder so discoverZapServers fires AFTER the
handler is installed.
- cdp-bridge.ts: add public dispatchMethod() helper for ZAP-server
initiated requests.
- cdp-bridge-server.ts: deprecation header. No longer in the critical
path; kept only for legacy non-ZAP clients.
- mcp/src/zap-server.ts: fix wire-constant mismatches with
shared/zap.ts (MSG_RESPONSE was 0x12, must be 0x11; PING/PONG were
0xf0/0xf1, must be 0xFE/0xFF).
Tests: 250 vitest cases pass (was 247, +3 for setZapRequestHandler).
shared-zap.test.ts now covers 31 cases (was 28).
Manifest version 2.0.0 reflects the architecture pivot; wire format
itself is unchanged from 1.9.x (still
[0x5A 0x41 0x50 0x01][type:1][length:4 BE][JSON]).
The CDP bridge now supports multiple connected browsers concurrently
(it always did; this fixes the routing) and prefers Firefox when no
explicit target is specified. Order: firefox > safari > edge > chrome.
User explicitly said personal browser is Firefox — Chrome was
silently winning whenever both extensions were active because the
bridge picked the first-registered client. Now Chrome only wins
when it's the only one connected.
- resolveClient(): walks the DEFAULT_BROWSER_PREFERENCE list to find
the first connected match. Last-resort fallback is the
first-registered client.
- defaultBrowser config: persisted to ~/.hanzo/extension/config.json
so the preference survives bridge restarts. Read on construct.
- New 'set_default_browser' / 'use_browser' actions: agent can flip
the default at runtime without restarting the bridge.
- list_browsers now returns defaultBrowser + preferenceOrder so
callers can see what would be picked.
The agent can now talk to multiple browsers simultaneously by passing
'browser' as a per-call target ('firefox', 'chrome', 'safari', or a
specific instance ID like 'firefox-2'). Without a target, the
configured default (or Firefox by preference) wins.
- popup.ts: 'Open Chat Panel' now opens the in-page right-anchored
overlay instead of the browser-native sidebar. Firefox's sidebar_action
always rendered on the user's preferred side (default left) wrapped in
the native sidebar-icon strip — both unwanted. Chrome's sidePanel was
inconsistent with the overlay's behaviour. Single right-anchored
surface across every browser.
- manifest-firefox.json: drop sidebar_action entirely. No more native
Firefox sidebar; the floating right-side overlay (already at
right:20px;bottom:20px in content-script.ts) is the only chat surface.
- cdp-bridge.ts: add awaitPromise:true to Runtime.evaluate so async
IIFEs (the standard pattern for fetch+credentials:include in MCP
evaluate calls) resolve to the actual value instead of returning {}.
Bumps 1.8.7 → 1.8.8.
The Hanzo gateway exposes Casdoor's
at (verified via probe — POST
returns 401 JSON with proper error semantics). The bare /oauth/token
listed in iam.hanzo.ai/.well-known/openid-configuration returns 404
in prod — the gateway has no rewrite for it.
Token exchange + refresh now hit the working path and use proper
application/x-www-form-urlencoded body per RFC 6749. JSON bodies were
silently rejected by Casdoor's token endpoint (it only accepts form-
encoded for the OAuth path).
Bumps 1.8.6 → 1.8.7 in root + browser package.json + both manifests.
Firefox MV3 removed `browser.tabs.executeScript` (the string-based
MV2 API the bridge was still calling). Every evaluate from a Firefox
client returned `{}` because the call rejected silently. Replaced
with `browser.scripting.executeScript({ target, world: 'MAIN', func,
args })`, with the legacy `tabs.executeScript` retained as a fallback
for older Firefox builds.
Also fixed runInPage's wrappedCode: when injected via Function(body)()
the body must `return` explicitly. The previous IIFE-as-expression
trampoline discarded the result and the caller saw {} for every
evaluation.
Screenshot fix: Firefox's `tabs.captureVisibleTab(opts)` is interpreted
as `(windowId)` when called with a single object — the polymorphic
signature accepts only `(windowId, opts)`. Now resolves the target
tab's windowId, briefly activates the tab if needed, captures, then
restores focus. Also returns `{data, format, size}` so the Python
side has all three fields it expects.
Bumps 1.8.5 → 1.8.6 in root + browser package.json and both manifests.