Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 27 additions & 78 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -837,89 +837,38 @@ jobs:
- name: Test (buzz-dev-mcp)
# The Windows-only bash resolver lives in buzz-dev-mcp; its unit tests
# only gate if this crate is tested ON Windows.
run: cargo test -p buzz-dev-mcp --target $env:TARGET
# The bundled-bash staging (PortableGit download + SFX extract of the WHOLE
# tree, including mingw64/) runs only in release.yml on tag — so without this
# step the agent's only Windows transport would ship UNEXERCISED until a
# tagged release hits users. Stage the tree and spawn the LAUNCHER bash
# (bin/bash.exe, the entry the resolver now uses) to prove all of: the SFX
# `-o` POSIX-path extraction works; the launcher spawns and sets up
# MSYSTEM/PATH; the lazily-loaded MSYS DLL closure survives; and — the whole
# point of bundling the full toolchain — git/jq/curl resolve from the bundled
# mingw64/bin and git is functional (a real commit round-trips). The launcher
# prepending mingw64/bin to PATH under `-c` is the load-bearing behavior that
# only a real Windows host can confirm; this gate is where it gets proven.
# (End-to-end nostr-SIGNED commits compose build_git_env's GIT_CONFIG_* with
# the git-sign-nostr helper — covered by buzz-dev-mcp unit tests + a bare-host
# check, not here, since this job stages only empty sidecar placeholders.)
- name: Smoke-test bundled bash + git toolchain staging
# Serial: windows_resolver_tests mutate process-global env
# (BUZZ_SHELL/GIT_BASH/SystemRoot) that SharedState::new reads.
run: cargo test -p buzz-dev-mcp --target $env:TARGET -- --test-threads=1
# Smoke-test the new host-prereq contract: Git for Windows (which provides
# bash) is available on the runner, a shell command round-trips, and bash
# does NOT resolve from System32 (so WSL's launcher is never picked up).
# windows-latest runners have Git for Windows pre-installed; the unit tests
# above exercise the MCP resolver itself. This step verifies the host env.
- name: Smoke-test host Git Bash prereq (host env check)
shell: bash
run: |
set -euo pipefail
stage_dir="$RUNNER_TEMP/git-bash"
scripts/stage-windows-bash.sh "$stage_dir"
launcher="$stage_dir/bin/bash.exe"
[[ -f "$launcher" ]] || { echo "launcher bash missing: $launcher" >&2; exit 1; }
# Git for Windows ships bash.exe under its bin/ directory; confirm it
# resolves from the standard location the runtime resolver probes first.
bash_path=$(command -v bash 2>/dev/null || true)
[[ -n "$bash_path" ]] || { echo "ERROR: bash not found on PATH — host Git for Windows missing" >&2; exit 1; }
echo "Resolved bash: $bash_path"
[[ "$bash_path" != *System32* ]] || { echo "ERROR: resolved bash is WSL's System32 launcher" >&2; exit 1; }

# Faithfulness to the agent: shell.rs spawns the launcher with PATH set
# wholesale to the shim PATH (shim tempdir + the MCP process's inherited
# PATH). We strip PATH down to just the Windows system dir before
# spawning — stricter than a bare host (ambient PATH stripped to force
# resolution through the launcher). This stops the runner's ambient
# git/jq from masking the launcher's own mingw64/bin prepend, so anything
# that resolves had to come from the launcher itself. The fidelity gap is
# benign: the real shim tempdir only ever holds rg/tree/buzz and the two
# nostr helpers, never git/jq/curl, so a richer real PATH cannot shadow
# mingw64/bin for these three tools.
win_root="${SYSTEMROOT:-${SystemRoot:-C:\\Windows}}"
bare_path="$win_root\\System32;$win_root"
# Run a basic pipeline through the resolved bash (same invocation the
# agent uses: bash -c '...').
out=$(bash -c 'echo hello | tr a-z A-Z')
[[ "$out" == "HELLO" ]] || { echo "bash pipeline failed: got '$out'" >&2; exit 1; }

# Coreutils pipeline through the launcher (the resolver's entry point).
out=$(PATH="$bare_path" "$launcher" -c 'echo hello | tr a-z A-Z')
[[ "$out" == "HELLO" ]] || { echo "staged bash pipeline failed: got '$out'" >&2; exit 1; }

# The full-toolchain payoff: git/jq/curl must resolve from the bundle's
# mingw64/bin or usr/bin. Use NON-login `-c` to match the agent's actual
# invocation (shell.rs spawns `bash -c`): if the launcher only fixes PATH
# under a login shell, `-lc` would pass here while the real agent stays
# broken — the exact green-but-broken trap this gate guards.
#
# Anchor to the ACTUAL staged tree, not a mount-shape: command -v reports
# the MSYS mount path (/mingw64/bin/git), so we cygpath -m it back to the
# real Windows location and assert it sits under $stage_dir. A bare
# `/mingw64/*` shape check would pass for any /mingw64-rooted mount; the
# cygpath round-trip proves the tool is literally inside the bundle we
# just staged. cygpath ships in the bundle's usr/bin (MSYS2 core, same
# tier as the `tr` proven above), so it resolves under the launcher.
#
# curl is the one tool of the three with a System32 twin
# (C:\Windows\System32\curl.exe, on $bare_path) — if a future PortableGit
# ever drops curl from mingw64/bin, the launcher would resolve the
# System32 twin and this REDs on a non-bug; git/jq have no such twin.
stage_m=$("$launcher" -c "cygpath -m '$stage_dir'")
for tool in git jq curl; do
located=$(PATH="$bare_path" "$launcher" -c "command -v $tool >/dev/null 2>&1 && cygpath -m \"\$(command -v $tool)\"" || true)
[[ -n "$located" ]] || { echo "$tool did not resolve in bundled bash" >&2; exit 1; }
case "$located" in
"$stage_m"/*) ;;
*) echo "$tool resolved outside the staged bundle: $located (stage: $stage_m)" >&2; exit 1 ;;
esac
echo "$tool -> $located"
done

# git is not just present but functional: a real commit round-trips,
# again through non-login `-c` with the bare PATH. Single-quoted on
# purpose — this script body runs in the launcher's shell, not ours.
# shellcheck disable=SC2016
PATH="$bare_path" "$launcher" -c '
set -euo pipefail
repo=$(mktemp -d)
cd "$repo"
git init -q
git -c user.name=ci -c user.email=ci@example.com commit -q --allow-empty -m smoke
git log -1 --format=%s | grep -qx smoke
' || { echo "bundled git failed a commit round-trip" >&2; exit 1; }
echo "staged launcher bash spawned; git/jq/curl resolve from the bundle; git commit works"
# Confirm git itself works — agents run git commands frequently.
git --version
repo=$(mktemp -d)
cd "$repo"
git init -q
git -c user.name=ci -c user.email=ci@example.com commit -q --allow-empty -m smoke
git log -1 --format=%s | grep -qx smoke
echo "Host bash resolved and functional; git commit round-trip passed"
- name: Check (Tauri crate)
run: cargo check --manifest-path desktop/src-tauri/Cargo.toml --target $env:TARGET
env:
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ For agents, set `BUZZ_PRIVATE_KEY` and use [`buzz-cli`](crates/buzz-cli) — JSO

---

## Windows prerequisites

The agent shell tool runs commands under bash. On macOS and Linux that's already there; on Windows you need to bring it.

Install [Git for Windows](https://git-scm.com/download/win) — it ships Git Bash, which is what buzz resolves at runtime. Once it's installed, everything works the same as on other platforms.

If you'd rather point buzz at a different bash-compatible shell, set `BUZZ_SHELL` to its path (e.g. `BUZZ_SHELL=C:\path\to\bash.exe`). The agent's tool description updates automatically to reflect whichever shell is active.

---

## Architecture

```
Expand Down
2 changes: 1 addition & 1 deletion crates/buzz-acp/src/base_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ You are operating inside the Buzz platform — a Nostr-based messaging platform

## Buzz CLI

The `buzz` CLI is your primary interface. Auth env vars: `BUZZ_RELAY_URL`, `BUZZ_PRIVATE_KEY`, `BUZZ_AUTH_TAG`. Exit codes: 0 ok, 1 user error, 2 network, 3 auth, 4 other. Output is structured JSON — pipe through `jq` as needed.
The `buzz` CLI is your primary interface. Auth env vars: `BUZZ_RELAY_URL`, `BUZZ_PRIVATE_KEY`, `BUZZ_AUTH_TAG`. Exit codes: 0 ok, 1 user error, 2 network, 3 auth, 4 other. Output is structured JSON.

| Group | Key commands |
|-------|-------------|
Expand Down
2 changes: 1 addition & 1 deletion crates/buzz-dev-mcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl DevMcp {

#[tool(
name = "shell",
description = "Run a bash command. Ephemeral process per call. Output tail-truncated to ~8KB for the LLM; full output (first 10MB) saved to artifact file. timeout_ms defaults to 120000 (2 min) if omitted; capped at 600000 (10 min). For long-running commands (git push with hooks, cargo build, test suites), use 300000+. On PATH: rg (prefer over grep; flags: -n -i -l -g <glob> -C <n> --files), tree (flags: -d <depth>; shows line counts), and buzz (Buzz relay CLI — run buzz --help for commands)."
description = "Run a shell command (bash by default; set `BUZZ_SHELL` to use cmd, PowerShell, or another shell). Ephemeral process per call. Output tail-truncated to ~8KB for the LLM; full output (first 10MB) saved to artifact file. timeout_ms defaults to 120000 (2 min) if omitted; capped at 600000 (10 min). For long-running commands (git push with hooks, cargo build, test suites), use 300000+. On PATH: rg (prefer over grep; flags: -n -i -l -g <glob> -C <n> --files), tree (flags: -d <depth>; shows line counts), and buzz (Buzz relay CLI — run buzz --help for commands)."
)]
async fn shell(
&self,
Expand Down
12 changes: 6 additions & 6 deletions crates/buzz-dev-mcp/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ pub(crate) fn resolve_path(root: &Path, path: &str) -> Result<PathBuf, String> {
/// - UNC: `//server/share/x` -> `\\server\share\x`.
///
/// A third form — root-anchored `/tmp`, `/usr/...`, `/bin` — maps under the
/// MSYS install root (the bundled `git-bash` dir), which this process does not
/// reliably know. We deliberately do NOT guess it: such a path falls through
/// untranslated and fails with the clear `path not accessible` error rather than
/// being silently mis-mapped to the wrong location. Resolving it correctly would
/// require shelling out to the bundled `cygpath`; that is out of scope here and
/// these paths are not a normal target for agent file I/O.
/// MSYS install root (from the host's Git for Windows install), which this
/// process does not reliably know. We deliberately do NOT guess it: such a path
/// falls through untranslated and fails with the clear `path not accessible`
/// error rather than being silently mis-mapped to the wrong location. Resolving
/// it correctly would require shelling out to `cygpath`; that is out of scope
/// here and these paths are not a normal target for agent file I/O.
#[cfg(windows)]
fn msys_to_windows(path: &str) -> String {
// UNC: exactly two leading slashes then a non-empty host segment.
Expand Down
Loading
Loading