Register the native host with Firefox as well

Firefox reads its host manifests from ~/.mozilla/native-messaging-hosts
(Mozilla/NativeMessagingHosts on macOS) and names the extension by its
gecko id in allowed_extensions, not an origin. With the extension now
asking for nativeMessaging, Firefox joins the router without a pairing
code, as Chromium does. hanzo-tools-browser 0.5.14.
This commit is contained in:
2026-09-30 12:32:10 -07:00
parent e629d5e539
commit 58801c6ccc
3 changed files with 55 additions and 7 deletions
@@ -29,6 +29,8 @@ from pathlib import Path
NAME = "ai.hanzo.zap"
EXTENSIONS = ("biingenefmanpecedoafkfajbnlgdmbl",)
# Firefox names an extension by its gecko id (manifest-firefox.json), not an origin.
GECKO = ("[email protected]",)
# Where each Chromium-family browser looks for a user-level host manifest.
_LINUX = (
@@ -48,6 +50,9 @@ _DARWIN = (
"~/Library/Application Support/Microsoft Edge",
"~/Library/Application Support/Vivaldi",
)
# Where Firefox looks: the host directory itself, not a profile root.
_FIREFOX_LINUX = ("~/.mozilla/native-messaging-hosts",)
_FIREFOX_DARWIN = ("~/Library/Application Support/Mozilla/NativeMessagingHosts",)
def _executable() -> str | None:
@@ -73,16 +78,23 @@ def install() -> list[str]:
"description": "Hanzo ZAP: the browser's seat on this machine's router",
"path": exe,
"type": "stdio",
"allowed_origins": [f"chrome-extension://{e}/" for e in EXTENSIONS],
}
body = json.dumps(manifest, indent=2) + "\n"
roots = _DARWIN if sys.platform == "darwin" else _LINUX
chromium = json.dumps({**manifest, "allowed_origins": [f"chrome-extension://{e}/" for e in EXTENSIONS]}, indent=2) + "\n"
firefox = json.dumps({**manifest, "allowed_extensions": list(GECKO)}, indent=2) + "\n"
darwin = sys.platform == "darwin"
targets = [
(Path(r).expanduser(), Path(r).expanduser() / "NativeMessagingHosts", chromium)
for r in (_DARWIN if darwin else _LINUX)
] + [
# Firefox's host directory: present once Firefox has a profile there.
(Path(r).expanduser().parent, Path(r).expanduser(), firefox)
for r in (_FIREFOX_DARWIN if darwin else _FIREFOX_LINUX)
]
written = []
for root in roots:
base = Path(root).expanduser()
for base, hosts, body in targets:
if not base.is_dir():
continue
target = base / "NativeMessagingHosts" / f"{NAME}.json"
target = hosts / f"{NAME}.json"
try:
if target.exists() and target.read_text() == body:
continue
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "hanzo-tools-browser"
version = "0.5.13"
version = "0.5.14"
description = "Browser automation tools: drive the user's own browser through the Hanzo extension, over the ZAP router every ZAP process embeds."
readme = "README.md"
requires-python = ">=3.12"
@@ -0,0 +1,36 @@
import json
import sys
import pytest
from hanzo_tools.browser import native_host
@pytest.fixture
def home(tmp_path, monkeypatch):
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setattr(sys, "platform", "linux")
exe = tmp_path / ".local" / "bin" / "hanzo-zap-host"
exe.parent.mkdir(parents=True)
exe.write_text("")
monkeypatch.setattr(native_host, "_executable", lambda: str(exe))
return tmp_path
def test_every_installed_engine_gets_its_own_manifest(home):
(home / ".config" / "chromium").mkdir(parents=True)
(home / ".mozilla").mkdir()
written = native_host.install()
chromium = json.loads((home / ".config/chromium/NativeMessagingHosts/ai.hanzo.zap.json").read_text())
firefox = json.loads((home / ".mozilla/native-messaging-hosts/ai.hanzo.zap.json").read_text())
assert chromium["allowed_origins"] == ["chrome-extension://biingenefmanpecedoafkfajbnlgdmbl/"]
assert "allowed_extensions" not in chromium
assert firefox["allowed_extensions"] == ["[email protected]"]
assert "allowed_origins" not in firefox
assert len(written) == 2
assert native_host.install() == [] # idempotent
def test_an_engine_that_is_not_installed_is_left_alone(home):
assert native_host.install() == []
assert not (home / ".mozilla").exists()