mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
hanzo-tools-browser: vendor ZAP wire constants, advertise on bind host
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]>
This commit is contained in:
@@ -19,8 +19,6 @@ keywords = ["mcp", "claude", "hanzo", "code", "agent"]
|
||||
dependencies = [
|
||||
# Hanzo AI core SDK (canonical types, config, session, MCP client)
|
||||
"hanzoai>=2.2.0",
|
||||
# ZAP native protocol
|
||||
"zap-protocol>=0.2.1",
|
||||
# Core MCP dependencies
|
||||
"mcp>=1.25.0",
|
||||
"fastmcp>=2.14.4",
|
||||
|
||||
@@ -24,22 +24,48 @@ from typing import Any, Awaitable, Callable, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Wire format — single source of truth is the `zap-protocol` package
|
||||
# (~/work/zap/zap-py). Import the constants instead of redefining them so
|
||||
# any future change (e.g. MSG_STREAM, MAX_MESSAGE_SIZE) lands here for free.
|
||||
from zap.protocol import (
|
||||
ZAP_MAGIC,
|
||||
HEADER_SIZE,
|
||||
MAX_MESSAGE_SIZE,
|
||||
MSG_HANDSHAKE,
|
||||
MSG_HANDSHAKE_OK,
|
||||
MSG_REQUEST,
|
||||
MSG_RESPONSE,
|
||||
MSG_PING,
|
||||
MSG_PONG,
|
||||
encode,
|
||||
decode,
|
||||
)
|
||||
# Wire format — vendored here. Mirrors the TypeScript reference at
|
||||
# extension/packages/browser/src/shared/zap.ts. Kept self-contained so a
|
||||
# vanilla `pip install hanzo-tools-browser` works without external state
|
||||
# (an earlier `zap-protocol` PyPI package was a `zap-schema` stub that
|
||||
# doesn't expose `zap.protocol`; rather than chase that, the wire spec is
|
||||
# small enough to inline). If you change anything here, also update the
|
||||
# TS reference and `extension/packages/mcp/src/zap-server.ts`.
|
||||
import json as _json
|
||||
import struct as _struct
|
||||
from typing import Tuple as _Tuple
|
||||
|
||||
ZAP_MAGIC = b"\x5a\x41\x50\x01"
|
||||
HEADER_SIZE = 9 # 4 magic + 1 type + 4 length BE
|
||||
MAX_MESSAGE_SIZE = 16 * 1024 * 1024 # 16 MiB
|
||||
|
||||
MSG_HANDSHAKE = 0x01
|
||||
MSG_HANDSHAKE_OK = 0x02
|
||||
MSG_REQUEST = 0x10
|
||||
MSG_RESPONSE = 0x11
|
||||
MSG_PING = 0xFE
|
||||
MSG_PONG = 0xFF
|
||||
|
||||
|
||||
def encode(msg_type: int, payload) -> bytes:
|
||||
body = _json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
if len(body) > MAX_MESSAGE_SIZE:
|
||||
raise ValueError(f"ZAP payload exceeds MAX_MESSAGE_SIZE ({len(body)} > {MAX_MESSAGE_SIZE})")
|
||||
return ZAP_MAGIC + _struct.pack("!BL", msg_type & 0xFF, len(body)) + body
|
||||
|
||||
|
||||
def decode(frame: bytes):
|
||||
if len(frame) < HEADER_SIZE or frame[:4] != ZAP_MAGIC:
|
||||
return None
|
||||
msg_type = frame[4]
|
||||
(length,) = _struct.unpack("!L", frame[5:9])
|
||||
if length > MAX_MESSAGE_SIZE or len(frame) < HEADER_SIZE + length:
|
||||
return None
|
||||
try:
|
||||
payload = _json.loads(frame[HEADER_SIZE : HEADER_SIZE + length].decode("utf-8"))
|
||||
except (_json.JSONDecodeError, UnicodeDecodeError):
|
||||
return None
|
||||
return msg_type, payload
|
||||
|
||||
# Browser-resolution preference (mirror cdp-bridge-server.ts)
|
||||
DEFAULT_BROWSER_PREFERENCE: list[str] = ["firefox", "safari", "edge", "chrome"]
|
||||
@@ -195,6 +221,11 @@ class ZapServer:
|
||||
agent_label=self.agent_label or "",
|
||||
version="zap/1",
|
||||
capabilities=["mcp", "browser-bridge"],
|
||||
# Advertise the bind address so the URL clients receive
|
||||
# actually reaches this server. zap-mdns defaults to the
|
||||
# outbound LAN IP via _local_ip(), which is wrong when we
|
||||
# bind loopback (browser extension dials LAN IP → ECONNREFUSED).
|
||||
host=self.host,
|
||||
)
|
||||
logger.info("mDNS published %s on :%d", zap_mdns.SERVICE_TYPE, self._port)
|
||||
except Exception as e:
|
||||
|
||||
@@ -18,7 +18,6 @@ dependencies = [
|
||||
"aiohttp>=3.9.0",
|
||||
"websockets>=12.0",
|
||||
"zap-mdns>=0.1.0",
|
||||
"zap-protocol>=0.2.1",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
Reference in New Issue
Block a user