Skip to content

fix(desktop): preflight npm prefix writability in doctor installs#1732

Merged
wpfleger96 merged 4 commits into
mainfrom
wpfleger/doctor-npm-eacces-preflight
Jul 10, 2026
Merged

fix(desktop): preflight npm prefix writability in doctor installs#1732
wpfleger96 merged 4 commits into
mainfrom
wpfleger/doctor-npm-eacces-preflight

Conversation

@wpfleger96

Copy link
Copy Markdown
Collaborator

The doctor's adapter step was running npm install -g blind — on machines where npm's global prefix is root-owned (nodejs.org pkg installer leaves /usr/local owned by root; version managers initialized only in ~/.zshrc are invisible to the login shells Buzz uses), users hit a raw truncated EACCES dump with no path forward.

Before running any npm install -g adapter command, the doctor now resolves npm prefix -g in the same hermit-stripped login shell the install would use. If the prefix isn't writable it fails immediately with actionable remediation steps; if npm is missing it says that with a pointer to install instructions. An EACCES stderr classifier catches failures that slip past the preflight. A new optional hint on InstallStepResult carries the guidance to the frontend, rendered above the raw output in both the Doctor settings panel and onboarding setup.

  • resolve_npm_prefix() spawns the same zsh/bash login shell as run_install_command, strips NPM_CONFIG_PREFIX/NPM_CONFIG_CACHE/COREPACK_HOME, and takes the last non-empty stdout line to skip version manager banners
  • npm_install_target_is_writable() probes prefix/lib/node_modulesprefix/libprefix via POSIX access(2); non-unix always returns true (the EACCES classifier still applies)
  • npm_eacces_hint() matches both old (npm ERR! EACCES: permission denied) and new (npm error EACCES) npm stderr formats
  • new installError.ts shared helper consolidates the error-message logic from both DoctorSettingsPanel and SetupStep; error elements gain whitespace-pre-line so the multi-line guidance renders correctly

The doctor's adapter step ran npm install -g blind; on machines where
npm's global prefix is root-owned (nodejs.org pkg installs, or version
managers initialized only in .zshrc which login shells never source),
users got a raw truncated EACCES dump with no way forward.

The install now resolves npm's effective prefix in the same stripped-env
login shell, fails fast when the target isn't writable, and classifies
EACCES stderr as a fallback. A new optional hint field on
InstallStepResult carries the remediation steps, rendered above the raw
output in both the Doctor panel and onboarding setup.
@wpfleger96 wpfleger96 requested a review from a team as a code owner July 10, 2026 20:30
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 3 commits July 10, 2026 18:09
Three follow-ups from code review:

1. Add a 30-second timeout to resolve_npm_prefix() using the same
   spawn+channel+SIGTERM pattern already in run_install_command. A
   wedged login shell (e.g. a blocking version-manager init) would
   have hung the preflight indefinitely; now it falls through to the
   stderr classifier path on timeout.

2. Extract install_shell_command() as the single source of truth for
   shell selection (/bin/zsh vs /bin/bash), hermit env-var strip
   (NPM_CONFIG_PREFIX / NPM_CONFIG_CACHE / COREPACK_HOME), PATH
   injection, and the setsid pre_exec. Both run_install_command and
   resolve_npm_prefix now call it, so the hermit-strip list cannot
   drift between the two paths.

3. Add a comment at the Phase-1 CLI-install loop noting that the npm
   EACCES preflight and classifier run only in Phase 2 because every
   cli_install_commands entry today is a curl-pipe; a future npm-
   global CLI install would need to add the guard there explicitly.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The previous commit returned None from resolve_npm_prefix() on timeout,
but npm_preflight_check maps None to a hard abort with NPM_MISSING_HINT
('Node.js / npm was not found'). A wedged-shell timeout would therefore
surface a misleading error and never reach the stderr classifier.

Introduce a NpmPrefix enum with three variants:
- Found(PathBuf): npm responded with a parseable prefix
- Unavailable: spawn failed, non-zero exit, or unparseable output
- TimedOut: 30s deadline exceeded

npm_preflight_check now maps:
- Unavailable → hard abort with NPM_MISSING_HINT (unchanged)
- Found + unwritable → EACCES guidance abort (unchanged)
- Found + writable or TimedOut → None (proceed to install)

On timeout the preflight logs a single eprintln so the fall-through is
not silent, then returns TimedOut, allowing run_install_command to run
and the stderr classifier to serve as the backstop.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Four scenarios covering the NpmPrefix enum paths added in the preceding
commits, all #[ignore]d so normal cargo test / CI skips them:

  (a) writable prefix → npm_preflight_check returns None (proceed)
  (b) read-only prefix → aborts with EACCES guidance hint
  (c) npm not on PATH → aborts with NPM_MISSING_HINT (regression guard
      for the byte-for-byte-unchanged branch Will was most concerned about)
  (d) wedged login shell (npm shim sleeps 60s) → 30s timeout fires,
      NpmPrefix::TimedOut → npm_preflight_check returns None (proceed)

Each test runs in a fresh subprocess (separate docker run) so the
login_shell_path() OnceLock is initialised from the correct HOME for
that scenario. The container entrypoint creates four isolated HOME dirs
under /tmp with crafted shell init files and optional ~/.npmrc before
invoking the pre-compiled test binary.

Harness files:
  desktop/src-tauri/e2e/Dockerfile.preflight-e2e  — builds on
    rust:1.95-slim-bookworm with Node.js + zsh; non-root testuser;
    no host volume mounts (host npm/PATH untouched)
  desktop/src-tauri/e2e/run-e2e-scenarios.sh      — entrypoint
  desktop/src-tauri/e2e/Dockerfile.preflight-e2e.dockerignore
    — overrides root .dockerignore so desktop/src-tauri/ is included

just target: desktop-preflight-e2e (docker build + docker run --rm)

Normal cargo test surface unchanged: 16 passed, 4 ignored.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 merged commit 58aee8f into main Jul 10, 2026
25 checks passed
@wpfleger96 wpfleger96 deleted the wpfleger/doctor-npm-eacces-preflight branch July 10, 2026 23:26
tlongwell-block pushed a commit that referenced this pull request Jul 11, 2026
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>

Signed-off-by: Tyler Longwell <tlongwell@block.xyz>

* origin/main:
  fix(desktop): preserve archived observer history (#1752)
  fix(dev): install Lefthook hooks into shared .git/hooks dir (#1751)
  fix(desktop): surface codex config-parse failures and -32603 internal errors clearly (#1745)
  fix(desktop): fix workspace rail badge disappearance and persist mark-as-unread (#1747)
  chore(desktop): remove container-only npm-preflight E2E harness (#1749)
  fix(desktop): preflight npm prefix writability in doctor installs (#1732)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant