From 1c0437dfc5bbbf12b4e312a597137b13ebbe7154 Mon Sep 17 00:00:00 2001 From: "Zak B. Elep" Date: Thu, 4 Jun 2026 15:14:36 +0800 Subject: [PATCH] fix(install): scope npm installs/audits to avoid pulling in apps/desktop Root package.json uses apps/* workspaces glob which unconditionally includes apps/desktop (Electron + node-pty@1.1.0, ~200MB, requires make/g++ to build) in every unscoped npm command run from the repo root. This commit addresses the core problem by adding explicit workspace scoping to all internal npm calls: hermes_cli/main.py (_build_web_ui): - Add --workspace web to the npm install call so only the web workspace deps are resolved, never apps/desktop. hermes_cli/tools_config.py: - Add --workspaces=false to agent-browser and Camofox root installs so only root-level deps (agent-browser, @streamdown/math) are installed, bypassing the workspace graph entirely. hermes_cli/doctor.py (run_doctor npm audit): - Replace the single unscoped 'npm audit --json' at PROJECT_ROOT with three scoped invocations: * --workspaces=false for root deps (Browser tools) * --workspace web for the web workspace * --workspace ui-tui for the TUI workspace - Update remediation hints to use matching scoped 'npm audit fix' commands so users don't accidentally trigger a desktop rebuild. package.json: - Add convenience scripts for scoped operations: npm run install:root / install:web / install:tui / install:desktop npm run audit:root / audit:web / audit:tui npm run audit:fix:root / audit:fix:web / audit:fix:tui These give developers and CI a safe, explicit interface for the most common per-workspace tasks without accidentally pulling desktop. Fixes #38772 --- hermes_cli/doctor.py | 33 ++++++++++++++++++++++++++------- hermes_cli/main.py | 6 +++++- hermes_cli/tools_config.py | 12 ++++++++---- package.json | 12 +++++++++++- 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 910130ac45b..ae0477d103d 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -1454,18 +1454,29 @@ def run_doctor(args): # npm audit for all Node.js packages _npm_bin = _safe_which("npm") if _npm_bin: - npm_dirs = [ - (PROJECT_ROOT, "Browser tools (agent-browser)"), - (PROJECT_ROOT / "scripts" / "whatsapp-bridge", "WhatsApp bridge"), + # Each entry: (cwd, label, extra_audit_args) + # PROJECT_ROOT is audited with --workspaces=false so that the apps/* + # glob (which pulls in Electron, node-pty, etc.) is never resolved + # for a routine security check. The web and ui-tui workspaces are + # audited separately via --workspace flags. See #38772. + npm_audit_targets = [ + (PROJECT_ROOT, "Browser tools (agent-browser)", ["--workspaces=false"]), + (PROJECT_ROOT, "web workspace", ["--workspace", "web"]), + (PROJECT_ROOT, "ui-tui workspace", ["--workspace", "ui-tui"]), + (PROJECT_ROOT / "scripts" / "whatsapp-bridge", "WhatsApp bridge", []), ] - for npm_dir, label in npm_dirs: - if not (npm_dir / "node_modules").exists(): + for npm_dir, label, audit_extra in npm_audit_targets: + # For workspace-scoped audits run from PROJECT_ROOT the + # node_modules check must use the workspace root; standalone dirs + # (whatsapp-bridge) check their own node_modules. + check_dir = npm_dir if audit_extra else npm_dir + if not (check_dir / "node_modules").exists(): continue try: # Use resolved absolute path so Windows can execute # npm.cmd (CreateProcessW can't run bare .cmd names). audit_result = subprocess.run( - [_npm_bin, "audit", "--json"], + [_npm_bin, "audit", "--json", *audit_extra], cwd=str(npm_dir), capture_output=True, text=True, timeout=30, ) @@ -1476,12 +1487,20 @@ def run_doctor(args): high = vuln_count.get("high", 0) moderate = vuln_count.get("moderate", 0) total = critical + high + moderate + # Determine a scoped fix command for the remediation hint. + if audit_extra and audit_extra[0] == "--workspace": + fix_scope = " ".join(audit_extra) + fix_cmd = f"cd {npm_dir} && npm audit fix {fix_scope}" + elif audit_extra == ["--workspaces=false"]: + fix_cmd = f"cd {npm_dir} && npm audit fix --workspaces=false" + else: + fix_cmd = f"cd {npm_dir} && npm audit fix" if total == 0: check_ok(f"{label} deps", "(no known vulnerabilities)") elif critical > 0 or high > 0: check_warn( f"{label} deps", - f"({critical} critical, {high} high, {moderate} moderate — run: cd {npm_dir} && npm audit fix)" + f"({critical} critical, {high} high, {moderate} moderate — run: {fix_cmd})" ) issues.append( f"{label} has {total} npm " diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 38271f64ef7..60f82adf2b1 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -7090,7 +7090,11 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool: _say(text) npm_cwd = _workspace_root(web_dir) - npm_workspace_args: tuple[str, ...] = () + # Scope the install to the web workspace only so that the full workspace + # graph (including apps/desktop with its Electron + node-pty deps) is never + # resolved here. Without --workspace the root package.json's apps/* glob + # would pull in desktop on every web build. See #38772. + npm_workspace_args: tuple[str, ...] = ("--workspace", "web") if _is_termux_startup_environment(): npm_cwd, npm_workspace_args = _termux_workspace_install_context(web_dir) r1 = _run_npm_install_deterministic( diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 72fb824a560..ae97dbf54a2 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -846,14 +846,17 @@ def _run_post_setup(post_setup_key: str): # batch shims). On POSIX npm_bin is the plain path — same # behaviour as before. result = subprocess.run( - [npm_bin, "install", "--silent"], + # --workspaces=false restricts the install to the repo root + # only, avoiding the apps/* glob which would pull in + # apps/desktop (Electron + node-pty) unnecessarily. See #38772. + [npm_bin, "install", "--silent", "--workspaces=false"], capture_output=True, text=True, cwd=str(PROJECT_ROOT) ) if result.returncode == 0: _print_success(" Node.js dependencies installed") else: from hermes_constants import display_hermes_home - _print_warning(f" npm install failed - run manually: cd {display_hermes_home()}/hermes-agent && npm install") + _print_warning(f" npm install failed - run manually: cd {display_hermes_home()}/hermes-agent && npm install --workspaces=false") if result.stderr: _print_info(f" {result.stderr.strip()[:200]}") elif not node_modules.exists(): @@ -951,13 +954,14 @@ def _run_post_setup(post_setup_key: str): import subprocess # Absolute npm path so .cmd shim executes on Windows. result = subprocess.run( - [_npm_bin, "install", "--silent"], + # --workspaces=false avoids resolving apps/desktop. See #38772. + [_npm_bin, "install", "--silent", "--workspaces=false"], 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") + _print_warning(" npm install failed - run manually: npm install --workspaces=false") if camofox_dir.exists(): _print_info(" Start the Camofox server:") _print_info(" npx @askjo/camofox-browser") diff --git a/package.json b/package.json index 41bb08a41a2..13689e75c08 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,17 @@ "web" ], "scripts": { - "postinstall": "echo '✅ Browser tools ready. Run: python run_agent.py --help'" + "postinstall": "echo '✅ Browser tools ready. Run: python run_agent.py --help'", + "install:root": "npm install --workspaces=false", + "install:web": "npm install --workspace web", + "install:tui": "npm install --workspace ui-tui", + "install:desktop": "npm install --workspace apps/desktop", + "audit:root": "npm audit --workspaces=false", + "audit:web": "npm audit --workspace web", + "audit:tui": "npm audit --workspace ui-tui", + "audit:fix:root": "npm audit fix --workspaces=false", + "audit:fix:web": "npm audit fix --workspace web", + "audit:fix:tui": "npm audit fix --workspace ui-tui" }, "repository": { "type": "git",