fix(update): make Camofox lazy-installed instead of eager (#27055)

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.
This commit is contained in:
Teknium 2026-05-16 12:15:45 -07:00 committed by GitHub
parent 8a2b2b9f6f
commit 05af78c53d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 110 additions and 2642 deletions

View file

@ -810,21 +810,35 @@ def _run_post_setup(post_setup_key: str):
camofox_dir = PROJECT_ROOT / "node_modules" / "@askjo" / "camofox-browser"
_npm_bin = shutil.which("npm")
if not camofox_dir.exists() and _npm_bin:
_print_info(" Installing Camofox browser server...")
_print_info(" Installing Camofox browser package...")
_print_info(" First run downloads the Camoufox engine (~300MB) — this can take several minutes.")
import subprocess
# Absolute npm path so .cmd shim executes on Windows.
result = subprocess.run(
[_npm_bin, "install", "--silent"],
capture_output=True, text=True, cwd=str(PROJECT_ROOT)
)
if result.returncode == 0:
_print_success(" Camofox installed")
else:
_print_warning(" npm install failed - run manually: npm install")
# Install @askjo/camofox-browser on-demand. It is NOT in
# package.json so that `hermes update` does not silently pull
# the ~300MB Camoufox Firefox-fork binary for every user.
# Stream output (no capture, no --silent) so the long-running
# postinstall download is visible instead of looking frozen.
try:
result = subprocess.run(
[_npm_bin, "install", "@askjo/camofox-browser@^1.5.2",
"--no-fund", "--no-audit", "--progress=false"],
cwd=str(PROJECT_ROOT),
)
if result.returncode == 0:
_print_success(" Camofox installed")
else:
_print_warning(
" npm install failed — run manually: "
"npm install @askjo/camofox-browser"
)
except Exception as exc:
_print_warning(f" Camofox install failed: {exc}")
_print_info(
" Run manually: npm install @askjo/camofox-browser"
)
if camofox_dir.exists():
_print_info(" Start the Camofox server:")
_print_info(" npx @askjo/camofox-browser")
_print_info(" First run downloads the Camoufox engine (~300MB)")
_print_info(" Or use Docker: docker run -p 9377:9377 -e CAMOFOX_PORT=9377 jo-inc/camofox-browser")
elif not shutil.which("npm"):
_print_warning(" Node.js not found. Install Camofox via Docker:")

2630
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,6 @@
},
"homepage": "https://github.com/NousResearch/Hermes-Agent#readme",
"dependencies": {
"@askjo/camofox-browser": "^1.5.2",
"agent-browser": "^0.26.0"
},
"overrides": {

View file

@ -0,0 +1,85 @@
"""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."
)