Skip to content

fix(desktop): harden Windows installs against Defender block and orphaned Node - #4382

Merged
wesbillman merged 2 commits into
mainfrom
duncan/windows-install-hardening
Aug 3, 2026
Merged

fix(desktop): harden Windows installs against Defender block and orphaned Node#4382
wesbillman merged 2 commits into
mainfrom
duncan/windows-install-hardening

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 2, 2026

Copy link
Copy Markdown
Member

This PR fixes two Windows-specific install failures: Windows Defender blocking the bare irm|iex PowerShell install command, and managed Node shims pointing at a version-bumped (now-absent) Node directory.

The Defender block (Trojan:Win32/Commando.A!ml) fires before PowerShell runs and is not clearable via Allow. The Node orphaning means shims in the managed npm prefix resolve but fail at runtime with 'node not recognized' because they reference the deleted old Node path.

  • Replace all three Windows CLI install commands (Goose, Claude, Codex) with a two-step shape — Invoke-RestMethod to a named temp file, then execute — to eliminate the dropper signature; a new windows_install_command! macro in discovery/windows_install.rs generates all three strings at compile time so the shape cannot drift between runtimes
  • $ErrorActionPreference='Stop' aborts on download failure instead of falling through to a missing-file exit-0; exit $LASTEXITCODE propagates the vendor script's own exit code
  • Add probe_node(executable, expected_version, timeout) as a bounded seam: stdout goes to a temp file (not a pipe) so no exit path can block on an inherited handle; the child runs in its own process group on Unix so an unconditional group SIGKILL on every exit path terminates all descendants; on Windows taskkill /T /F provides the same tree-wide cleanup; managed_node_runtime_ready() is a thin wrapper that resolves the managed Node path and calls the seam
  • Add resolve_adapter_path() in managed_node.rs: resolves the candidate first, then calls should_invalidate_adapter() — a pure predicate that returns true only when the resolved path is under buzz_managed_npm_bin_dir() AND the managed Node runtime is orphaned; external adapters outside the managed prefix are always preserved

Note: CI cannot reproduce the Defender block (no live Defender ML classifier). Proof of fix is structural — the command shape no longer matches the dropper signature. Canary validation on a real Windows machine with Defender enabled is the definitive check.

@wpfleger96
wpfleger96 requested a review from a team as a code owner August 2, 2026 18:44
…two-step shape

Windows Defender's ML classifier (Trojan:Win32/Commando.A!ml) flags the bare
`irm <url> | iex` command line as a dropper signature and denies the spawn
with 'Access is denied. (os error 5)' before PowerShell runs. The block is
sticky — Allow does not clear it.

Replace all three Windows CLI install commands (Goose, Claude, Codex) with a
two-step shape: download the vendor script to a named temp file with
Invoke-RestMethod, then execute the file. Two invariants guard against the
#2892 regression (success-on-download-failure):

- $ErrorActionPreference='Stop' aborts on a failed download instead of
  falling through to a missing file and exiting 0.
- exit $LASTEXITCODE propagates the vendor script's own exit code so a
  vendor failure of 3 does not flatten to 1.

A single macro (windows_install_command!) in a new discovery/windows_install.rs
submodule generates all three strings at compile time. One definition means the
security shape cannot drift between runtimes as URLs change; a per-runtime
literal would let one entry silently regress to irm|iex.

Goose and Claude escaped by scoring under the classifier threshold — that is
luck, not design. All three are hardened here.

The three agent_discovery.rs test fixtures that pinned catalog command strings
are updated to the new two-step shape. The routing and argv-parsing tests
(is_powershell_command, install_powershell_command) that use irm|iex as
representative PS input are unchanged — they test the routing function, not
the catalog.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the duncan/windows-install-hardening branch 6 times, most recently from c86aae3 to 12ae0ee Compare August 3, 2026 00:05
@Chessing234

Copy link
Copy Markdown
Contributor

defender eating the windows install is a real footgun — hardening that path is overdue. will be curious how the orphaned-node cleanup behaves on a dirty upgrade.

After a Node version pin bump (v24.11.0 → v24.18.0), existing installs keep
the old directory on disk. The shims in the managed npm prefix remain as files,
so resolve_command finds them and treats the adapter as installed. The shims
then fail at run time with 'node not recognized' because they reference the
now-gone old Node path.

Two changes address this:

1. managed_node_runtime_ready() now delegates to probe_node(), a bounded seam
   that runs node --version with a 3-second process deadline (spawn + try_wait
   loop) using a temp-file stdout transport. The temp-file transport removes
   any dependency on inherited pipe handles: a descendant retaining a stdout
   write-end cannot cause probe_node to hang. The child runs in its own process
   group (Unix) or is terminated via taskkill /T /F (Windows) so all
   descendants are killed unconditionally on every exit path.

2. A new resolve_adapter_path() helper replaces the inline resolve_command call
   in install_acp_runtime_blocking. It resolves the candidate path first, then
   calls should_invalidate_adapter() — a pure seam that returns true only when
   the resolved path is under the Buzz-managed npm prefix AND the managed Node
   runtime is orphaned. External adapters outside the managed prefix are always
   preserved; only managed-prefix shims are invalidated when Node is orphaned.
   This forces plan_adapter_install to schedule a reinstall, which triggers
   ensure_managed_node_runtime_blocking to re-download the correct Node version
   before npm reinstalls the adapter shims.

The re-download path already existed in ensure_managed_node_runtime_blocking;
the gap was that it was only reachable when the adapter appeared missing. This
change makes a stale Node dir the sufficient condition for triggering it, not
only a missing adapter shim.

Absence detection (the file-existence check in managed_node_runtime_ready)
heals an orphaned Node dir; run-verification (the node --version probe) heals
shims that exist but point at the wrong binary.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the duncan/windows-install-hardening branch from 12ae0ee to 67d68e8 Compare August 3, 2026 15:22
@wesbillman
wesbillman merged commit 80315ac into main Aug 3, 2026
26 checks passed
@wesbillman
wesbillman deleted the duncan/windows-install-hardening branch August 3, 2026 16:37
wpfleger96 pushed a commit that referenced this pull request Aug 3, 2026
…ed-unread

* origin/main: (44 commits)
  chore(release): release Buzz Desktop version 0.5.4 (#4562)
  test(mobile): assert follow boundary semantics (#4559)
  docs(release): align desktop handoff instructions (#3988)
  fix: report agent usage per provider round, not once per turn (#4545)
  fix(desktop): harden Windows installs against Defender block and orphaned Node (#4382)
  feat(desktop): improve channel template discovery (#4549)
  fix(desktop): save key backups to authorized path (#4022)
  Add channel activity hover menu (#3935)
  feat(desktop): show saved Run on settings when editing an agent (#4539)
  fix(desktop): disambiguate provider API key labels and annotate mint key (#4406)
  fix(desktop): make OpenAI key re-enterable after first save in card mint dialog (#4140)
  fix(config-bridge): add harness-definition env tier and fix equal-value model override (#3580)
  Polish mobile composer and messaging UI (#3918)
  ci(linux): enable mesh-llm feature in Linux release and canary builds (#4524)
  fix(desktop): stop the create-agent provider config probe from erasing keystrokes (#4411)
  fix(mobile): recover and pace live subscriptions (#3053)
  feat(acp): deliver system prompt via _meta.systemPrompt for claude-agent-acp (#4395)
  fix(security): bump nostr crates for RUSTSEC-2026-0225..0232 + default sprig image to published digest (#4392)
  fix(desktop): back/forward via keyboard chords, mouse X1/X2 buttons, and swipe gestures (#3778)
  feat(k8s): Kubernetes backend plugin + desktop deploy path (#4289)
  ...

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96 added a commit that referenced this pull request Aug 3, 2026
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

* origin/main: (40 commits)
  fix(mobile): recover stale relay sessions (#4372)
  chore(release): release Buzz Desktop version 0.5.4 (#4562)
  test(mobile): assert follow boundary semantics (#4559)
  docs(release): align desktop handoff instructions (#3988)
  fix: report agent usage per provider round, not once per turn (#4545)
  fix(desktop): harden Windows installs against Defender block and orphaned Node (#4382)
  feat(desktop): improve channel template discovery (#4549)
  fix(desktop): save key backups to authorized path (#4022)
  Add channel activity hover menu (#3935)
  feat(desktop): show saved Run on settings when editing an agent (#4539)
  fix(desktop): disambiguate provider API key labels and annotate mint key (#4406)
  fix(desktop): make OpenAI key re-enterable after first save in card mint dialog (#4140)
  fix(config-bridge): add harness-definition env tier and fix equal-value model override (#3580)
  Polish mobile composer and messaging UI (#3918)
  ci(linux): enable mesh-llm feature in Linux release and canary builds (#4524)
  fix(desktop): stop the create-agent provider config probe from erasing keystrokes (#4411)
  fix(mobile): recover and pace live subscriptions (#3053)
  feat(acp): deliver system prompt via _meta.systemPrompt for claude-agent-acp (#4395)
  fix(security): bump nostr crates for RUSTSEC-2026-0225..0232 + default sprig image to published digest (#4392)
  fix(desktop): back/forward via keyboard chords, mouse X1/X2 buttons, and swipe gestures (#3778)
  ...
wpfleger96 added a commit that referenced this pull request Aug 3, 2026
Syncs Cargo.lock and ci.yml updates from origin/main via the umbrella, which
resolves the RUSTSEC-2026-0225..0229 nostr advisory failures in the Security gate.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

* commit 'ad9ae1dbb0c2d9b6839c1e18d6fd6a78238b61fb': (40 commits)
  fix(mobile): recover stale relay sessions (#4372)
  chore(release): release Buzz Desktop version 0.5.4 (#4562)
  test(mobile): assert follow boundary semantics (#4559)
  docs(release): align desktop handoff instructions (#3988)
  fix: report agent usage per provider round, not once per turn (#4545)
  fix(desktop): harden Windows installs against Defender block and orphaned Node (#4382)
  feat(desktop): improve channel template discovery (#4549)
  fix(desktop): save key backups to authorized path (#4022)
  Add channel activity hover menu (#3935)
  feat(desktop): show saved Run on settings when editing an agent (#4539)
  fix(desktop): disambiguate provider API key labels and annotate mint key (#4406)
  fix(desktop): make OpenAI key re-enterable after first save in card mint dialog (#4140)
  fix(config-bridge): add harness-definition env tier and fix equal-value model override (#3580)
  Polish mobile composer and messaging UI (#3918)
  ci(linux): enable mesh-llm feature in Linux release and canary builds (#4524)
  fix(desktop): stop the create-agent provider config probe from erasing keystrokes (#4411)
  fix(mobile): recover and pace live subscriptions (#3053)
  feat(acp): deliver system prompt via _meta.systemPrompt for claude-agent-acp (#4395)
  fix(security): bump nostr crates for RUSTSEC-2026-0225..0232 + default sprig image to published digest (#4392)
  fix(desktop): back/forward via keyboard chords, mouse X1/X2 buttons, and swipe gestures (#3778)
  ...
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.

3 participants