mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
The `@askjo/camofox-browser` npm package was a top-level entry in the root `package.json` `dependencies` block, so `hermes update` ran its postinstall on every user, every update. That postinstall calls `npx camoufox-js fetch`, which silently downloads a ~300MB Firefox-fork browser binary from GitHub Releases — multi-minute on fast connections, and a hard block for users on slow / restricted networks (notably users in China running through a VPN). Camofox is an explicit opt-in browser backend. The runtime check in `tools/browser_tool.py` only routes through Camofox when the user has set `CAMOFOX_URL` (selected via `hermes tools` → Browser Automation → Camofox). Users who never opted in never touched the package at runtime, yet every `hermes update` paid for the binary fetch anyway. This change: * Removes `@askjo/camofox-browser` from root `package.json` dependencies (and the regenerated `package-lock.json` drops Camofox's entire transitive tree, ~2.6k lines). * Updates the Camofox `post_setup` handler in `hermes_cli/tools_config.py` to install `@askjo/camofox-browser@^1.5.2` explicitly when the user selects Camofox, and streams npm output (no `--silent`, no `capture_output`) so the ~300MB download is visible rather than appearing frozen. * Adds `tests/test_package_json_lazy_deps.py` as a regression guard so future PRs can't silently re-add Camofox (or any binary-postinstall package) to eager root dependencies. `agent-browser` stays eager — it is the default Chromium-driving backend used by every session that does not have a cloud browser provider configured, and its postinstall is small. Validation: | | Before | After | |---|---|---| | `hermes update` time on slow network | multi-minute hang at `→ Updating Node.js dependencies...` | seconds (no binary fetch) | | Camofox opt-in install visibility | silent, looked frozen | streamed npm output | | Regression guard against re-adding | none | `test_package_json_lazy_deps.py` | Tests: - `tests/test_package_json_lazy_deps.py`: 3/3 pass - `tests/tools/test_browser_camofox*`: 92/92 pass - `tests/hermes_cli/test_tools_config.py`: 66/66 pass - `tests/hermes_cli/test_cmd_update.py` + adjacent: green Reported by lulu (Discord, May 2026) — `hermes update` hangs at `→ Updating Node.js dependencies...` in China. Related: #18840, #18869.
85 lines
3.6 KiB
Python
85 lines
3.6 KiB
Python
"""Invariants for what is eager vs lazy in the root ``package.json``.
|
|
|
|
The root ``package.json`` is installed by ``hermes update`` on every user,
|
|
including users who never opted into a given browser backend. Anything
|
|
listed in ``dependencies`` therefore runs its npm postinstall script for
|
|
everyone — including binary-fetching backends, on every update.
|
|
|
|
The contract:
|
|
|
|
* ``agent-browser`` IS eager. It is the default Chromium-driving backend
|
|
used whenever the agent makes a browser call without a cloud provider
|
|
configured, so it must already be installed before any session starts.
|
|
Its postinstall is also small.
|
|
|
|
* ``@askjo/camofox-browser`` is NOT eager. It is an explicit opt-in
|
|
alternative browser backend, selected by the user via
|
|
``hermes tools`` → Browser Automation → Camofox, and only used at
|
|
runtime when ``CAMOFOX_URL`` is set. Its postinstall fetches a ~300MB
|
|
Firefox-fork binary, which silently blocked ``hermes update`` for
|
|
multi-minute stretches on slow / network-restricted connections
|
|
(notably users in China running through a VPN). The package is
|
|
installed on demand by ``tools_config.py`` ``post_setup_key ==
|
|
"camofox"`` when the user actually selects Camofox.
|
|
|
|
If a future PR re-adds Camofox (or any other binary-postinstall package)
|
|
to root ``dependencies``, this test fails — read the lazy-install
|
|
guidance in the ``hermes-agent-dev`` skill before changing the
|
|
expectations.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def _root_package_json() -> dict:
|
|
with (REPO_ROOT / "package.json").open("r", encoding="utf-8") as fh:
|
|
return json.load(fh)
|
|
|
|
|
|
def test_camofox_is_not_in_root_dependencies() -> None:
|
|
"""Camofox must be opt-in, installed lazily by its post_setup handler."""
|
|
deps = _root_package_json().get("dependencies", {})
|
|
assert "@askjo/camofox-browser" not in deps, (
|
|
"Camofox is a ~300MB binary-postinstall backend that must stay "
|
|
"out of root package.json dependencies. It belongs in the "
|
|
"Camofox post_setup handler in hermes_cli/tools_config.py so it "
|
|
"only installs when the user explicitly selects Camofox via "
|
|
"`hermes tools` → Browser Automation → Camofox."
|
|
)
|
|
|
|
|
|
def test_agent_browser_stays_eager() -> None:
|
|
"""agent-browser is the default backend; it must remain eager."""
|
|
deps = _root_package_json().get("dependencies", {})
|
|
assert "agent-browser" in deps, (
|
|
"agent-browser is the default browser-tool backend used by every "
|
|
"session that doesn't have a cloud browser provider configured. "
|
|
"It must stay in root package.json dependencies so it is present "
|
|
"after `hermes setup` / `hermes update` without an explicit "
|
|
"post_setup step."
|
|
)
|
|
|
|
|
|
def test_root_lockfile_has_no_camofox_entries() -> None:
|
|
"""Regenerated lockfiles should not contain Camofox tree entries."""
|
|
lock_path = REPO_ROOT / "package-lock.json"
|
|
if not lock_path.exists():
|
|
# Some CI matrix shards skip lockfile materialization.
|
|
return
|
|
text = lock_path.read_text(encoding="utf-8")
|
|
assert "@askjo/camofox-browser" not in text, (
|
|
"package-lock.json still references @askjo/camofox-browser. "
|
|
"Regenerate the lockfile after removing the dep: "
|
|
"`rm package-lock.json && npm install --package-lock-only "
|
|
"--ignore-scripts --no-fund --no-audit`."
|
|
)
|
|
assert "camoufox-js" not in text, (
|
|
"package-lock.json still references camoufox-js (transitive of "
|
|
"@askjo/camofox-browser). Regenerate the lockfile."
|
|
)
|