Skip to content

feat(desktop): discover and spawn ACP harnesses installed in WSL on Windows - #3679

Open
spfcraze wants to merge 3 commits into
block:mainfrom
spfcraze:feat/windows-wsl-harness-fallback
Open

feat(desktop): discover and spawn ACP harnesses installed in WSL on Windows#3679
spfcraze wants to merge 3 commits into
block:mainfrom
spfcraze:feat/windows-wsl-harness-fallback

Conversation

@spfcraze

@spfcraze spfcraze commented Jul 30, 2026

Copy link
Copy Markdown

Problem

On Windows, Buzz Desktop discovers ACP harnesses by resolving their command
on the Windows PATH. Several supported harnesses are commonly installed
inside WSL, not on the Windows host — Hermes Agent (hermes-acp) is the
canonical case: it is a Linux-first install, so on a Windows machine running
Hermes in WSL, Doctor reports Not installed and the harness cannot be
started, even though it works perfectly inside the distro.

There was no WSL story anywhere in the harness discovery/spawn path (the only
WSL awareness in the tree is is_windows_apps_alias, which exists to avoid
WindowsApps stub launchers, issue #2328).

What this PR does

Buzz now falls back to the default WSL distribution when a preset or
custom harness command is missing from the Windows PATH. This directly
addresses the WSL breakages catalogued in
#3122 (env severed at the
wsl.exe boundary, untranslated cwd, harness invisible to discovery):

  1. Discovery (managed_agents/wsl.rs, new) — probes the default distro
    via wsl.exe -e sh -c …, checking ~/.local/bin, ~/bin,
    /usr/local/bin, then the distro PATH (command -v). Positive results
    mark the harness Available with a wsl://… binary path in Doctor.
    Resolutions are cached for the app lifetime (mirrors resolve_command's
    cache contract). Only bare command names are ever probed — paths are
    rejected up front (no injection surface, no nonsense lookups).

  2. Spawn contract (runtime.rsbuzz-acp) — when the host PATH
    resolution failed but the WSL probe succeeded, Desktop sets
    BUZZ_ACP_AGENT_WSL_PATH (absolute in-distro path) and optionally
    BUZZ_ACP_AGENT_WSL_DISTRO on the buzz-acp child.
    BUZZ_ACP_AGENT_COMMAND keeps the original bare identity
    (hermes-acp), so all per-runtime defaults keyed on the command name
    (default_agent_env, default_agent_args) apply unchanged.

  3. Spawn wrapping (buzz-acp/src/acp.rs) — AcpClient::spawn reads the
    contract and launches wsl.exe [-d distro] -e <linux_path> <args> instead
    of the bare command. WSL interop bridges the stdio pipes; the ACP/NDJSON
    channel is byte-transparent. Contract values are validated (absolute POSIX
    path required); a set contract on a non-Windows host is ignored with a
    warning.

  4. Environment forwarding — wsl.exe children only import Windows-side
    variables listed in WSLENV. buzz-acp builds the forward list from
    every agent-intended key (per-runtime defaults like
    HERMES_ACP_SKIP_CONFIGURED_MCP=1, persona env like GOOSE_PROVIDER,
    merged CODEX_CONFIG), merging into any pre-existing WSLENV without
    duplicating flagged entries (GOPATH/p style). Without this, agents in
    WSL silently lose their env — for Hermes specifically this re-introduces
    the [Bug] Hermes model discovery times out in Buzz Desktop v0.5.0 #3355 startup-stall the skip flag exists to prevent.

  5. Session cwd translationbuzz-acp is spawned from ~/.buzz, so
    session/new would otherwise carry C:\Users\<u>\.buzz into the distro.
    For WSL-targeted agents, AcpClient::spawn-adjacent session_cwd_for_wsl
    translates drive-letter paths to /mnt/<drive>/… before the cwd reaches
    the [Workspace] grounding prompt and the agent's session state (Hermes
    uses it as the edit-approval root). UNC, relative, and POSIX paths are
    left untouched. This is BYOH cannot reach a WSL-hosted harness on Windows: env is severed at the wsl.exe boundary and cwd is never translated #3122's "first failure after initialize".

Known remaining follow-ups from #3122 (deliberately out of scope here, noted
in the issue's item list): skill/nest placement for a distro $HOME and
teardown that reaps the in-distro process rather than only the wsl.exe
shim.

Verification (real, on a Windows 11 + WSL2 machine)

Environment: Windows 11, WSL2 Ubuntu, Hermes Agent v0.19.0 at
/home/rat/.local/bin/hermes-acp, Buzz Desktop installed at
C:\Users\ratz\AppData\Local\Buzz\.

  1. Baseline bug: hermes-acp absent from Windows PATH → Doctor shows
    Hermes Not installed.
  2. Stdio transparency: spawning
    C:\Windows\System32\wsl.exe -e /home/rat/.local/bin/hermes-acp from a
    Windows parent with piped stdio returns a byte-clean ACP initialize
    response (agentInfo: hermes-agent 0.19.0, protocolVersion: 1).
  3. Env forwarding: without WSLENV, hermes-acp hangs before
    initialize (the [Bug] Hermes model discovery times out in Buzz Desktop v0.5.0 #3355 MCP-startup stall). With
    HERMES_ACP_SKIP_CONFIGURED_MCP=1 set Windows-side and forwarded via
    WSLENV, initialize answers immediately.
  4. Zero-config workaround validated: the manual custom-harness JSON
    documented in docs/windows-wsl-harnesses.md (same mechanism this PR
    automates) loads and works on the current released build.

Tests

  • buzz-acp: 8 new unit tests (build_wsl_wrap validation, prefix_args,
    merge_wslenv order/dup/flag handling). cargo test -p buzz-acp: 648
    passed, 0 failed
    (full suite, Linux). cargo clippy -p buzz-acp --all-targets: clean.
  • Desktop wsl.rs: 6 unit tests (probe script contract pins ~/.local/bin
    before PATH, no login shell; stdout parsing; display path;
    probeable-command validation incl. metacharacter rejection; negative-result
    caching) — pass on Linux. Process-spawning probes are cfg(windows)-gated;
    the Windows-only code paths (resolve_wsl_exe, probe spawn) were
    compile-verified for x86_64-pc-windows-msvc.
  • cargo check (Linux) on desktop/src-tauri: green, zero warnings.
    cargo test managed_agents:: on desktop/src-tauri: 906 passed, 0
    failed
    (includes the 6 new wsl.rs tests + all pre-existing discovery,
    custom-harness, spawn-hash, and readiness tests).
  • Full x86_64-pc-windows-msvc workspace check was not runnable in the dev
    container (crypto -sys crates need a Windows C toolchain); Windows CI
    covers it. Every Windows-gated line added by this PR is either
    std-only Rust (verified via the standalone msvc-target compile described
    above) or mirrors existing patterns in git_bash.rs.
  • Existing discovery tests bind the resolver seam (preset_catalog_entry) —
    unchanged, still green.

Notes for reviewers

  • Scope: preset + custom harnesses only. Tier-1 builtins (auth probes,
    install scripts) still assume host-native installs — a follow-up could
    extend the same cached-resolution mechanism to their probes.
  • Perf: probes run once per missing command per app launch, cache-first
    on both the discovery and spawn paths. WSL-unavailable machines
    short-circuit on wsl.exe resolution.
  • Future: WslCommandResolution.distro is plumbed through the whole
    contract but always None today (default distro only) — multi-distro
    discovery needs no contract change.
  • New public API: none (all pub(crate) / private). Docs added:
    docs/windows-wsl-harnesses.md.

@spfcraze
spfcraze requested a review from a team as a code owner July 30, 2026 03:56
@spfcraze
spfcraze force-pushed the feat/windows-wsl-harness-fallback branch from d2afdfa to 0458c49 Compare July 30, 2026 12:35
@spfcraze

spfcraze commented Aug 7, 2026

Copy link
Copy Markdown
Author

This PR addresses #3122 — the canonical WSL-harness issue. To map its breakage list onto this change:

Verification is on a real Win11 + WSL2 box with Hermes Agent 0.19.0 (handshake through wsl.exe -e, env-forwarding confirmed required and working, custom-harness JSON path validated against the released build); full test suites: buzz-acp 650 passed, desktop managed_agents 906 passed, clippy clean.

@al-osokin

al-osokin commented Aug 31, 2026

Copy link
Copy Markdown

I tested this PR end-to-end on Windows 10 + WSL2 against Buzz Desktop 0.5.20, using @agentclientprotocol/codex-acp 1.1.10 behind a custom harness command (codex-wsl-acp). The WSL discovery/spawn path works, including env forwarding and the translated WSL cwd, but I found one additional readiness gate that prevents the discovered runtime from being used.

For an unknown/custom command, collect_missing_requirements in desktop/src-tauri/src/managed_agents/readiness.rs still checks only the Windows resolver:

if crate::managed_agents::resolve_command(&effective.effective_command).is_none()
    && crate::managed_agents::wsl::probe_wsl_command(&effective.effective_command).is_none()
{
    return vec![Requirement::MissingBinary {
        command: effective.effective_command.clone(),
    }];
}

With only that additional WSL fallback in readiness, I cross-built the Windows candidate and completed a real managed-agent turn:

  • Desktop discovered /home/avo/.local/bin/codex-wsl-acp in WSL.
  • buzz-acp.exe spawned C:\Windows\System32\wsl.exe -e /home/avo/.local/bin/codex-wsl-acp.
  • All 10 configured lazy-pool workers initialized as codex-acp 1.1.10.
  • A real mention produced two signed OK replies in the Buzz channel.
  • After a normal Desktop restart, the harness rediscovered the channel and subscribed successfully.

So this is an independent end-to-end confirmation that the approach works with Codex ACP too, once readiness uses the same WSL resolution as discovery/spawn.

One rebase follow-up: PR #6904 has since landed and adds the bounded command runner plus forced-discovery cache invalidation. This PR's wsl_cache currently retains negative results for the app lifetime and has no clear path, so Check again cannot discover a WSL command installed after the first miss. I suggest wiring a clear_wsl_cache() into the same forced-discovery invalidation path and moving the WSL probe onto the bounded runner while resolving the current conflicts.

I'm happy to submit the small readiness/cache/test changes to this branch if you accept contributions there, or help prepare a rebased successor against current main if that is easier.

…indows

When a preset or custom harness command is missing from the Windows PATH,
harness discovery now probes the default WSL distribution (~/.local/bin,
~/bin, /usr/local/bin, then the distro PATH) and marks the harness
Available with a wsl:// binary path. At spawn, Desktop hands the in-distro
path to buzz-acp (BUZZ_ACP_AGENT_WSL_PATH, optional
BUZZ_ACP_AGENT_WSL_DISTRO), which wraps the agent launch through
wsl.exe -e and forwards all agent-intended env vars across the boundary
via a computed WSLENV merge. BUZZ_ACP_AGENT_COMMAND keeps the bare command
identity so per-runtime defaults (e.g. HERMES_ACP_SKIP_CONFIGURED_MCP)
apply unchanged.

This unblocks running Hermes Agent (Linux-first install, hermes-acp) from
Buzz Desktop on Windows without a hand-authored custom harness.

Docs: docs/windows-wsl-harnesses.md
Signed-off-by: Frank Saunders <frank@users.noreply.github.com>
buzz-acp is spawned from ~/.buzz, so session/new carried a Windows drive
path (C:\Users\x\.buzz) into the distro, where it does not exist. Agents
store the session cwd and use it as the edit-approval root and workspace
grounding (block#3122's first failure after initialize). When the
agent is WSL-targeted, translate drive-letter paths to /mnt/<drive>/…
before the cwd leaves the process; UNC, relative, and POSIX paths pass
through untouched.

Signed-off-by: Frank Saunders <frank@users.noreply.github.com>
…edback

Address al-osokin's review on PR block#3679:

1. Readiness: collect_missing_requirements now also probes the default WSL
   distro (probe_wsl_command) before reporting a custom/unknown command as
   MissingBinary, so a discovered-but-wsl-only harness (e.g. hermes-acp in
   WSL) is no longer shown as Not installed. Mirrors the discovery + spawn
   path's WSL fallback.

2. Cache invalidation: add wsl::clear_wsl_cache() and call it on the same
   forced-discovery invalidation seams as clear_resolve_cache
   (commands/agent_discovery.rs install path + forced_single_flight.rs and
   the install path), so a harness installed inside the distro after a prior
   miss is found on the next forced run — fixing 'Check again' not
   discovering a recently-installed WSL command.

3. Bounded probe: route the WSL probe through the discovery bounded runner
   (bounded_command::output_with_timeout, re-exported under cfg(windows))
   with a 30s wall-clock deadline, so a hung distro boot cannot stall
   discovery forever, matching the other discovery spawns.

Author: spfcraze
@spfcraze
spfcraze force-pushed the feat/windows-wsl-harness-fallback branch from e5feb40 to 0bc8cfa Compare August 31, 2026 01:48
@github-actions

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is eed74bde2f4797714335ac10c56c0b0244c1def4...0bc8cfa59d7826609fb93d7c26c5b4d4225d809b.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review 0bc8cfa59d7826609fb93d7c26c5b4d4225d809b to authorize a new review.
Any previous review applies only to its recorded range.

@spfcraze

Copy link
Copy Markdown
Author

Rebased the branch onto current main and addressed the review feedback. Head is now 0bc8cfa5 (resolved the two conflicts with the resolve_command force/cached split and the cwd/session_cwd_for_wsl merge).

Updates included:

  • Readyness: collect_missing_requirements now also probes the default WSL distro for a custom/unknown command before reporting MissingBinary, so a discovered WSL-only harness is no longer shown as Not installed.
  • Cache: added wsl::clear_wsl_cache() and wired it into the forced-discovery invalidation seam (same places as clear_resolve_cache), so "Check again" finds a WSL command installed after the first miss.
  • Bounded probe: routed the WSL probe through bounded_command::output_with_timeout (30s deadline) so a hung distro boot can't stall discovery, matching the other discovery spawns.

Build + clippy clean; wsl 6/6, readiness 53/53, discovery 125/125 tests pass. Re-running the Codex security review on this exact range would be appreciated.

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.

2 participants