The hanzo-tools-ui docs-RAG client targets api.hanzo.ai (host already
canonicalized on main). Move the paths off the residual /api/ prefix to
top-level /v1/, matching the cloud-api route rename and the openapi
v1.0.0 lock-in (no /api/ prefix):
/api/search-docs -> /v1/search-docs
/api/chat-docs -> /v1/chat-docs
/api/index-docs -> /v1/index-docs
/api/search-docs/stats -> /v1/search-docs/stats
Hard cutover, coordinated with hanzoai/cloud (productsvc + ai serve these
at /v1), universe cloud-api-v1 AUTH_PUBLIC_PATHS, hanzo-docs RAG clients,
and the openapi cloud spec.
Co-authored-by: Hanzo Dev <[email protected]>
The hanzo-tools-ui docs-RAG client targets api.hanzo.ai (host already
canonicalized on main). Move the paths off the residual /api/ prefix to
top-level /v1/, matching the cloud-api route rename and the openapi
v1.0.0 lock-in (no /api/ prefix):
/api/search-docs -> /v1/search-docs
/api/chat-docs -> /v1/chat-docs
/api/index-docs -> /v1/index-docs
/api/search-docs/stats -> /v1/search-docs/stats
Hard cutover, coordinated with hanzoai/cloud (productsvc + ai serve these
at /v1), universe cloud-api-v1 AUTH_PUBLIC_PATHS, hanzo-docs RAG clients,
and the openapi cloud spec.
The cloud API now sits behind one front door (ingress -> gateway -> cloud).
api.cloud.hanzo.ai / cloud-api.hanzo.ai / functions.hanzo.ai are redundant
aliases routing to the same backend, so migrate clients to api.hanzo.ai.
- hanzo-tools-ui: HANZO_CLOUD_API default cloud-api.hanzo.ai -> api.hanzo.ai
(docs RAG paths /api/search-docs|chat-docs|index-docs are served at /api on
the cloud backend per AUTH_PUBLIC_PATHS; kept as-is)
- hanzo base status: Functions remote host functions.hanzo.ai ->
api.hanzo.ai/v1/functions (the canonical gateway path)
Non-breaking: the aliases still resolve to the same backend.
Co-authored-by: Hanzo Dev <[email protected]>
Packaging metadata (twine check was PASSED-with-warnings -> clean PASS):
- config/llm/mcp/vector: add missing 'readme = README.md' ([project] key);
README files existed but were never wired into the dist metadata.
- net/plan/vcs: fill empty (0-byte) README.md files referenced by the
readme key, so long_description is no longer missing.
Tests (hanzo-tools-core/test_all_tools.py drifted from current sources):
- browser: 1 -> 3 tools (browser, cdp, playwright after the zapd split)
- mcp_tools: 4 -> 5 tools
- required-tool total: 21 -> 23
Patch release carrying the HIP-0111 IAM endpoint migration in
hanzoai.resources.iam (/iam/api/get-account + /iam/api/userinfo ->
canonical /v1/iam/oauth/userinfo), already in-tree since be917695 but
unreleased. 2.2.1 is taken on PyPI; bump to 2.2.2 (x.x.x+1).
hanzo-tools-iam built URLs as {base}/api/{path} — forbidden per HIP-0111
(no /api/ prefix; one way). Now {base}/v1/iam/{path}; health probe is
/v1/iam/healthz. Asserts JSON shape, not bare 200, to dodge the IAM SPA
catch-all that serves 200 text/html for unregistered paths. Adds a test
for _iam_url + IAM_BASE_URL. (Test harness wiring for this non-member
package is a separate packaging task — code verified: 0 /api/ remain.)
hanzoai/s3 (SeaweedFS fork w/ consensus+ZAP) is the canonical object
store; the minio pip package and its branding are gone. hanzo-s3 is now
a thin native boto3 adapter exposing a stable surface (S3Client + Bucket/
Object/Stat) so the hanzo s3 CLI and s3 MCP tool are unchanged. Removed
the dead MinioAdmin re-export (no consumer) and all MinIO naming.
CLI (7) + MCP S3Tool (14) tests green; ruff clean.
Closes the last gap in the one-way Hanzo control surface. IAM/KMS/PaaS
already had both CLI subcommands and MCP tools; S3 had neither.
- hanzo s3: buckets/mb/rb/ls/stat/rm/presign over hanzo_s3 (MinIO) client,
creds from env (inject from KMS, never plaintext). Registered in the
unified hanzo CLI alongside iam/kms/paas.
- hanzo-tools-s3: S3Tool MCP tool (same action-dispatch pattern as PaaSTool),
exported via the hanzo.tools entry point; s3 added to loader prefix map.
- uv workspace: map hanzo-cli/iam/kms/s3 + tools-s3 to local sources so the
unified CLI resolves on-disk packages (fixes hanzo_iam ImportError from a
stale published 1.29.0 wheel shadowing local 1.30.0).
Tests: 7 CLI + 14 MCP, all green.
Root cause of dead shell-exec: stale installs froze hanzo-tools-core 0.2.0 (no ToolError)
before 0.3.0 published, shadowing hanzo_tools.core and crashing the shell package import.
Floors now exclude it; a fresh/upgraded install always lands the consistent set.
hanzo-tools-browser: the zapd router envelope (frame.rs mirror) is no
longer reimplemented — zapd_consumer now uses zap.frame + zap.ZapClient
(dep zap-proto>=1.2.0). The end-to-end browser command codec stays local
and untagged to match the extension's decodeCmd (frame.encode_cmd is
tagged and would corrupt it); a new test proves the byte layout.
hanzo-zap: wire.py now delegates the zero-copy codec (Message/Builder/
ObjectBuilder/object readers) to canonical zap.wire instead of a 3rd
hand-rolled copy; the hanzo cloud-service schema + handshake + frame I/O
remain as a thin layer. dep zap-proto>=1.2.0. 114 tests pass.
When the firefox backend is targeted and Firefox exposes a WebDriver BiDi remote agent (--remote-debugging-port=9222), drive it directly via bidi_client (real navigation + trusted input) instead of the half-implemented extension path or a dead headless Playwright. Additive + gated: only fires for target firefox with live BiDi and a BiDi-mapped action; otherwise falls through to the existing extension/Playwright routing unchanged. Takes effect after the MCP server reloads.
Firefox 129+ runs WebDriver BiDi-only by default (CDP off), so /json/version 404s and the socket is served directly at /session. Four fixes make the BiDi client actually work against it:
- probe(): fall back to a TCP check + ws://host:port/session when /json/version isn't advertised (don't open a probe WebSocket — that establishes session state and conflicts with session.new).
- websockets>=13 compat: ClientConnection dropped .closed; _ws_open() reads .state/.close_code across versions.
- session lifecycle: session.end on close() so a session never orphans (Firefox keeps one at a time, and an orphan blocks every later session.new); retry session.new once after ending.
- add create_context/close_context/get_url so automation runs in a fresh tab without disturbing the user's tabs.
* fix(browser-mcp): migrate CDP bridge -> zapd consumer; make disk authoritative
The MCP runtime kept pulling the buggy published wheel (hanzo-tools-browser
0.5.7) from PyPI instead of the migrated on-disk source, so every restart
re-broke the `cdp` tool. Reinstate `cdp` as a zapd-native tool, finish the
bridge->zapd migration (delete dead bridge/server modules + their tests), bump
to 0.5.8 and pin hanzo-mcp >=0.5.8, add the package to the uv workspace/sources
so disk is authoritative. 9/9 browser tests pass.
Pairs with the hanzoai/extension bridge->zapd migration (separate, still
uncommitted). The ~/.claude.json MCP invocation was also pointed at the local
editable source (machine config, not in this repo).
(cherry picked from commit 11b4d4ea8a5570d6fb0e51079fa445dba3713f71)
* fix(browser): drop dead lifecycle.py (in-process zap_server/cdp_bridge are gone)
lifecycle.py only wrapped the removed in-process ZAP server + CDP bridge; nothing
imports it under the zapd-consumer model. One way: transport is zapd_consumer,
tools are browser/cdp/playwright.
---------
Co-authored-by: hanzo-dev <[email protected]>
Co-authored-by: Antje Worring <[email protected]>
Co-authored-by: Hanzo Dev <[email protected]>
The `memory` service never set REDIS_URL and never wired itself to the
Redis container. Runtime config (src/hanzo_memory/config.py) defaults
redis_url=None — caching via Redis is optional and unconfigured. The
`redis:` block was a dead, profile-gated (with-cache) declaration with
no consumer.
Per "no redis, no kv" policy: remove the orphaned block and its volume.
No consumer migration needed (nothing consumed it).
Published 0.2.0 predates 2cb7eb03: its ConfigTool passed
permission_manager positionally into BaseTool.__init__(self), so the
'config' tool failed registration on every server start (and 0.2.0
lacked WorkspaceTool entirely). The fix has sat unreleased on main —
same stranded-fix pattern as hanzoai.protocols. hanzo-mcp now floors
hanzo-tools-config>=0.2.1.
Floor hanzoai at 2.2.1 — the first release that actually ships
hanzoai.protocols, which permissions.py imports.
Drop the stderr→devnull redirect in stdio mode. The MCP stdio protocol
rides stdout alone; stderr is the channel clients log for diagnostics.
Silencing it turned the missing-module crash into a zero-byte mystery —
the client log literally suggests printing to stderr while the server
was throwing tracebacks into devnull. Run-loop failures also log with
exc_info so the traceback reaches the client log.
protocols.py landed 2026-03-31 (200cef23) but 2.2.0 was published before
it, so the module never reached PyPI. hanzo-mcp imports
hanzoai.protocols at startup; resolving against the published 2.2.0
crashed every stdio launch with ModuleNotFoundError before the
initialize response — surfacing as 'Failed to connect' in Claude
Desktop/Code.
Adds bidi_client.py — a WebDriver BiDi client over WebSocket to
Firefox 129+ / Chrome 124+ at --remote-debugging-port=9222.
This is the "trusted input" backend that complements the existing
WebExtension scripting backend (the cdp_bridge_server WS to the
extension). The extension produces synthetic events with
isTrusted=false, which strict frameworks (Drupal AJAX, certain React
libraries, security-aware sites) reject. BiDi via input.performActions
produces real browser input with isTrusted=true that ALL frameworks
honor because they ARE the real browser events.
Methods implemented:
- probe() auto-detect at /json/version
- connect() / close() WebSocket session lifecycle
- list_contexts() browsingContext.getTree
- find_context_by_url() locate tab by URL substring
- navigate(ctx, url) browsingContext.navigate
- capture_screenshot(ctx) browsingContext.captureScreenshot
- script_evaluate(ctx, js) script.evaluate in page context
- input_mouse_click(ctx,x,y) input.performActions — TRUSTED click
- input_double_click double-click composite
- input_key_press input.performActions key
- input_insert_text trusted keyboard typing
- click_selector(ctx, sel) composite: eval rect + trusted click
Architecture: decomplected, three layers. Layer 1 = wire transport
(WebSocket JSON-RPC). Layer 2 = canonical primitives
(input.performActions / browsingContext.* / script.evaluate). Layer 3
= ergonomic aliases (click_selector composite).
Integration into cdp_bridge_server.py — auto-detect on startup, route
BiDi.* methods, optionally route Input.dispatchMouseEvent and
hanzo.click to BiDi backend when available — is the next step.
To enable BiDi in a Firefox session:
open -a "Firefox Developer Edition" --args --remote-debugging-port=9222
The bridge will probe http://localhost:9222/json/version on startup;
if BiDi is available it advertises BiDi.* in its capabilities.
0.5.6 of hanzo-tools-browser shipped with a missing register() method
on CdpTool that crashed MCP startup. 0.5.7 fixes it. Floor bumped to
0.5.7 so pip/uv refuse to resolve the broken intermediate version.
0.5.6 shipped CdpTool inheriting BaseTool but missing the required
`register()` abstract method, so any host that registered the tool
crashed with `TypeError: Can't instantiate abstract class CdpTool`.
The browser package never reached MCP startup, taking the whole hanzo
MCP server with it.
This commit adds CdpTool.register() with the cdp-specific param surface
(action / method / params / tab_id / target_browser / client_id / timeout)
matching the BrowserTool registration pattern.
Yanking 0.5.6 from PyPI alongside this release; 0.5.7 is the canonical
0.5.x for the three-tool surface.
Decomplects the browser surface. Previously: BrowserTool with three
braided transports (ZAP, CDP bridge, Playwright) plus a CdpTool alias
that was just a name change. Now: three peer tools, one transport
strategy each, env-gated registration.
Tools (each default-on, each disable-able):
- `browser` — high-level action surface, auto-routes transport.
Disable: HANZO_BROWSER_TOOL_DISABLED=1
- `cdp` — raw Chrome DevTools Protocol method dispatch
(`method=`, `params=`). No Playwright fallback.
Disable: HANZO_CDP_TOOL_DISABLED=1
- `playwright` — same action surface as `browser` but pinned to
backend=playwright (no extension / CDP-bridge).
Disable: HANZO_PLAYWRIGHT_TOOL_DISABLED=1
Transport knobs stay orthogonal to tool selection:
- HANZO_ZAP_DISABLED=1 — don't auto-start the in-process ZAP server
- HANZO_CDP_BRIDGE_ENABLED=1 — opt back into the legacy HTTP bridge
- BROWSER_TRANSPORT=zap|http|auto
- BROWSER_BACKEND=firefox|chrome|extension|playwright|auto
Lifecycle (ZAP server + CDP bridge background threads) moved out of
__init__.py into lifecycle.py. __init__.py is now a pure re-export
surface that resolves TOOLS at import time based on env flags.
CdpTool is now a genuine peer of BrowserTool — method-oriented, not
action-oriented — replacing the alias-subclass from 0.5.5.
Removes three console scripts that should never have been declared at the
top-level shell namespace from hanzo-mcp:
hanzo-dev — name-collided with the canonical Rust Codex-fork CLI
at /opt/homebrew/bin/hanzo-dev (~/work/hanzo/dev).
hanzo-plugin — unscoped; if needed, belongs in its own package.
hanzo-unified — vague + unscoped.
Modules remain importable (hanzo_mcp.dev_tools, .cli_plugin,
.unified_backend) so any user-facing entry point can ship from its own
package later. Only hanzo-mcp + hanzo-mcp-dev stay as scripts here.
Also bumps the hanzo-tools-browser floor to >=0.5.5 so the CdpTool alias
(mcp__hanzo__cdp) is guaranteed alongside mcp__hanzo__browser on every
hanzo-mcp install.
Adds CdpTool — a subclass of BrowserTool with `name = "cdp"` — so MCP
clients can reach the same browser control surface under both
`mcp__hanzo__browser` and `mcp__hanzo__cdp`. One implementation
(BrowserTool.execute is shared), two registered names. ZAP server,
CDP-bridge fallback, and Playwright transport stack are all shared.
Two fixes for the canonical ZAP server:
1. Vendor the wire format (ZAP_MAGIC, MSG_*, MAX_MESSAGE_SIZE,
encode/decode) inside hanzo_tools/browser/zap_server.py instead of
importing from the external ``zap.protocol`` module — the published
``zap-protocol`` package on PyPI is now a ``zap-schema`` stub that
doesn't expose those symbols, so the import chain breaks for any
fresh install. Inlining ~40 lines keeps the spec self-contained and
matches the TS reference at extension/packages/browser/src/shared/zap.ts.
2. Pass ``host=self.host`` to ``zap_mdns.publish`` so the advertised
address matches the bound address. Default behaviour advertised the
outbound LAN IP via ``_local_ip()`` while the WebSocket was bound to
127.0.0.1; clients dialled the LAN IP and got ECONNREFUSED.
Drops the broken ``zap-protocol>=0.2.1`` dependency from both
hanzo-mcp and hanzo-tools-browser pyprojects — no longer needed.
Co-authored-by: Hanzo Dev <[email protected]>
Replaces the legacy per-operation tool list (read/write/edit/tree/dag/
zsh/shell/open) in PersonalityRegistry.ESSENTIAL_TOOLS with the six
HIP-0300 axes:
fs bytes + paths (read, write, edit, list, stat, apply_patch, search_text)
exec execution (run, background, ps, kill, logs)
code symbols (parse, search, transform, summarize)
git diffs + history
fetch network (get, post, download)
plan orchestration
Action-routed dispatch on top, not split tools — one tool per axis.
Keeps memory/think/critic/agent/config/mode/tool unchanged.
Follow-up to 9c377be (hanzo-iam 1.30.0). Consumer packages now read only
the canonical IAM_ prefix — no upstream-brand alias chain, matching the
new IAMConfig.ENV_PREFIX contract enforced by test_env.py.
Touched modules:
- hanzo-cli: hanzo_cli/auth.py, hanzo_cli/bot/commands.py
- hanzo-tools-auth: hanzo_tools/auth/session.py
- hanzo-tools-iam: hanzo_tools/iam/iam_tool.py
- hanzo: src/hanzo/commands/{auth,iam}.py
- hanzoai: auth.py, llm_client.py
- tests: test_auth_pkce.py (import IAM_CLIENT_ID)
Also adds the generated pkg/hanzo-iam/uv.lock — matches the convention
of the other 17 axis pkgs that track their lockfiles.
Bumps hanzo-iam from 1.29.0 (older Casdoor SDK port) to 1.30.0 with a
purpose-built IAM SDK aligned to the canonical IAM_ env contract.
- ENV_PREFIX="IAM_" only — no HANZO_IAM_ / {ORG}_IAM_ fallback chain.
- IAMConfig.from_env reads IAM_ENDPOINT, IAM_CLIENT_ID, IAM_CLIENT_SECRET,
IAM_ORG, IAM_APP, IAM_CERT, IAM_USERINFO_URL.
- New FastAPI middleware module wraps Casdoor sign-in flow with
request-state user injection.
- 19 unit tests covering env-read, validation, and FastAPI integration.
Consumer packages (hanzo-cli, hanzo-tools-auth, hanzo-tools-paas) bump
their hanzo-iam dep to >=1.30.0.
Tag: hanzo-iam-1.30.0
The earlier patch-bump regex passed through a shell+python double-escape
that dropped the leading 'version = ' on each match and inserted a stray
\x01 byte, leaving '\x01"0.3.3"' instead of 'version = "0.3.3"'.
TOML parse fails (line 7 invalid statement) → CI publish-pypi 1/1 jobs
exit 1 → none of the 7 axis pkgs published.
Each TOML now parses with tomllib; ready for fresh tags.
(the prior tags pointed to commits CI didn't re-evaluate after the
black format-fix landed; bumping again so the new tags have unique
SHAs and the publish-pypi workflow fires cleanly)
The MCP wire (what tools/list reports) ships exactly one tool per axis:
fs, exec, code, git, fetch, plan — action-routed. The per-action classes
(ReadTool, WriteTool, EditTool, …) stay importable for in-process
read-only sandboxing (swarm sub-agents) but are NOT on the wire.
PyPI 0.3.1 of hanzo-tools-fs shipped the legacy split (7 tools) while
the local 0.3.1 source had already migrated to TOOLS=[FsTool]. Same
version number, two different contracts — the source of the
'Python MCP surfaces 32 tools, Node MCP surfaces 13' divergence
that breaks contract parity. Bumping to 0.3.2 publishes the
HIP-0300 surface for the first time.
- pkg/hanzo-tools-fs 0.3.1 → 0.3.2: TOOLS=[FsTool]; legacy classes
importable; dropped 'Edit = EditTool' alias; cleaner __init__.
- pkg/hanzo-tools-shell 0.6.3 → 0.6.4: bump only (source already
TOOLS=_get_detected_shell_tools()).
- pkg/hanzo-tools-code 0.1.0 → 0.1.1: bump only (TOOLS=[CodeTool]).
- pkg/hanzo-tools-vcs 0.1.0 → 0.1.1: bump only (TOOLS=[GitTool]).
- pkg/hanzo-tools-net 0.1.0 → 0.1.1: bump only (TOOLS=[FetchTool]).
- pkg/hanzo-tools-plan 0.1.0 → 0.1.1: bump only (TOOLS=[PlanTool]).
- pkg/hanzo-tools-agent 0.3.1 → 0.3.2: 'Edit' alias is gone — call
EditTool by its real name in swarm_tool.py.
- pkg/hanzo-mcp 0.15.3 → 0.15.4: gets the HIP-0300-clean wire when
consumed via 'uvx --refresh-package hanzo-tools-fs hanzo-mcp serve'.
Two byte-equivalence bugs caught by cross-runtime parity check:
1. _blake3() was falling back to BLAKE2b when hashlib lacked BLAKE3 (it
always does on stock CPython). Wallet addresses depend on the SAME
hash across all five brain runtimes. Required the `blake3` pip
package; no fallback. Python addresses now match the TS canonical
(@noble/hashes/blake3): encode_address(32x0x00) → "hanzo:UFC8qCW8...".
2. render_vtt joined lines with "\n" and appended one "" at the end,
producing a single trailing newline. Every other runtime emits "\n\n"
after each segment so cues are separated by a blank line per WebVTT
spec. Switched to explicit f-string append; render_srt fixed the same
way.
53 tests still pass.
Adds the zero-LLM typed-link extractor + YAML recipe loader to the
existing hanzo-memory Python SDK so a single brain.db file can be
written by the TS bot and read by the Python SDK and vice versa.
graph_links.py — Edge dataclass, extract_edges(), reconcile(),
slugify(). Same six edge types
(mentions/attended/works_at/invested_in/founded/
advises), same regex + role inference, same code-
fence stripping. Pure — no I/O, no LLM.
recipes.py — list_recipes() + load_recipe(name). Reads built-in
recipes/ dir + optional HANZO_BRAIN_RECIPES path.
recipes/email.yaml — flagship swipe-to-reply recipe, byte-identical
to the TS pack so cross-runtime brains agree.
tests/test_graph_links.py — mirror of TS suite. 12 assertions cover
slugify, every edge type, code-fence
stripping, dedup, bare slugs, reconcile,
and recipe load.
hanzo-memory already had pluggable backends (SQLite + sqlite-vec via
.db.factory) and an MCP server; this fills in the gbrain-shape graph +
recipe surfaces so the Python SDK reaches feature parity with the bot
extensions in TS.
hanzo-mcp (0.15.1 -> 0.15.3):
- _normalize_mcp_result() collapses FastMCP list[TextContent] to parsed JSON
for ZAP MSG_RESPONSE serialization
- Closes the "TextContent not JSON serializable" bug surfaced when driving
the Hanzo extension from agent via tools/call
- Bump hanzo-tools-browser[playwright] floor to 0.5.4
hanzo-tools-browser (0.5.2 -> 0.5.4):
- mDNS-only ZAP discovery via _hanzo._tcp.local. (HIP-0069); no port pool,
no lockfile, OS-assigned ports advertised by hanzo-zap-mdns
- zap_server.py: refactor for unique server_id + retract on stop
- Test suite trimmed to the new wire contract
Patch-bump both packages.
The custom encode/decode and message-type constants in zap_server.py
were a byte-identical duplicate of zap-protocol (~/work/zap/zap-py).
Drop the duplicates, import from canonical:
from zap.protocol import (
ZAP_MAGIC, HEADER_SIZE, MAX_MESSAGE_SIZE,
MSG_HANDSHAKE, MSG_HANDSHAKE_OK,
MSG_REQUEST, MSG_RESPONSE,
MSG_PING, MSG_PONG,
ZAP_PORTS as DEFAULT_ZAP_PORTS,
encode, decode,
)
Single source of truth for the wire — any change (MSG_STREAM, frame
versioning, future Cap'n Proto migration) lands once and propagates.
zap-protocol added as a dependency. Verified at install:
ZAP encode unified: True
ZAP decode unified: True
Every ZapServer.start() now publishes its bound port + agent_label as a
`_hanzo-zap._tcp.local.` mDNS record (best-effort — silently skipped
when `hanzo-zap-mdns` isn't installed). Browser extensions and any
LAN-wide agent can discover the server via `hanzo_zap_mdns.browse()`
without hard-coded ports or fragile lockfile registries.
Publish runs on a worker thread (asyncio.to_thread) so zeroconf's
internal multicast-socket setup doesn't block our event loop and
trigger EventLoopBlocked on zeroconf >= 0.140.
stop() retracts the announcement before closing the WS.
When an extension reuses its client_id across consecutive ZAP connections
(probe followed by connectZap, both with mgr.state.extensionId), the
older websocket's finally clause was unconditionally popping the
client_id from _clients — deleting the new connection's freshly-installed
registration. The extension would then immediately disconnect because the
server-side state no longer had it, hammering reconnect loops.
Now the finally clause only removes the entry when it still points at
the closing websocket. Verified end-to-end: Firefox 1.9.x ZAP-native
connection holds steady; tabs/screenshot/evaluate round-trip in
11/73/5 ms.
Each hanzo-mcp now hosts a ZAP server directly on the lowest free port
from [9999..9995], using a POSIX flock for cross-MCP arbitration. The
browser extension discovers and connects to this server without any
node-side bridge in the critical path.
- New hanzo_tools/browser/zap_server.py: wire format, server, lifecycle,
client registry, leases, cluster-visibility config registry.
- _extension_command tries ZAP first (sub-1ms median round-trip on
loopback), falls back to legacy HTTP bridge on :9224 only when no
ZAP-connected client matches. Pinnable via BROWSER_TRANSPORT.
- New tool actions: list_mcp_instances, claim_browser, release_browser
for sub-agent coordination across multiple parallel MCPs.
- Legacy CDP HTTP bridge no longer auto-starts (opt in with
HANZO_CDP_BRIDGE_ENABLED=1). It remains available for non-ZAP MCP
clients.
Tests: 30 new unit tests cover wire format, port arbitration,
multi-MCP coexistence, lease semantics, and cluster registry.
Bench shows 0.35ms median round-trip on local loopback.
Total tests: 4 baseline -> 35 passing (30 zap_server + 1 bench + 4 imports).
The extension's bridge server (cdp-bridge-server.ts v1.9.0) added two
new actions for dynamic browser switching:
set_default_browser — persist a default-target browser
use_browser — alias of set_default_browser
Both accept the browser name (firefox / chrome / safari / edge or a
specific instance ID like 'firefox-2') as the 'browser' field. The
bridge writes the choice to ~/.hanzo/extension/config.json so it
survives bridge restarts.
This commit:
- Adds the two action names to the Action Literal so the MCP tool
accepts them.
- Adds them to extension_actions so they route through the bridge
rather than falling to Playwright.
- The 'target_browser' parameter (already exposed) carries the
browser name through to the bridge as 'browser', which is exactly
what set_default_browser expects.
Bumps hanzo-tools-browser 0.4.5 → 0.4.6.