Skip to content

Windows: .cmd/.bat backend providers pass discovery and info but always fail deploy — stage_provider renames the staged copy to provider.exe #7729

Description

@phamvietloi

Summary

On Windows, a backend provider that is a .cmd or .bat file is accepted by discovery, resolves to a valid provider id, and answers the info request successfully — but every deploy fails, because stage_provider copies the provider into a temp directory and unconditionally names the copy provider.exe. CreateProcess then refuses the batch script it finds behind that .exe name with os error 216.

The two halves of the code disagree with each other. provider_id_from_filename deliberately strips .exe, .bat and .cmd, and its doc comment says so explicitly — so script providers are intended to be discoverable. stage_provider then destroys the extension that Windows needs to dispatch them.

Environment

  • Buzz Desktop v0.5.23
  • Windows 11
  • Provider installed as a .cmd file on PATH (e.g. C:\Users\<user>\.local\bin\)

Steps to reproduce

  1. Create buzz-backend-example.cmd somewhere on PATH (for example %USERPROFILE%\.local\bin\). A minimal one that satisfies the strict info schema is enough:

    @echo off
    echo {"ok":true,"name":"Example","version":"0.0.1","protocol_version":1,"description":"Example provider","config_schema":{}}

    (The info response is validated against a closed allowlist of exactly these six keys, with protocol_version = 1 and config_schema an object.)

  2. Restart Buzz Desktop and open the agent creation flow. The provider appears in the Where to run dropdown as example, and the info request against it succeeds — the entry looks healthy.

  3. Create an agent using that provider and deploy it.

Expected

Either the deploy works — the provider is discoverable, resolvable and answers info, so nothing in the UI suggests it cannot be used — or the provider is rejected at discovery time with an explicit reason.

Actual

The deploy fails at spawn:

failed to spawn C:\Users\<user>\AppData\Local\Temp\buzz-provider-XXXXXX\provider.exe: This version of %1 is not compatible with the version of Windows you're running. Check your computer's system information and then contact the software publisher. (os error 216)

The provider is never given a chance to run. The failure is in process creation, before any op is written to stdin.

Root cause

All citations are against desktop/src-tauri/src/managed_agents/backend.rs at commit 779af8886caae1317b4de962082429867ab61503.

1. Script providers are intentionally discoverable. provider_id_from_filename strips all three Windows extensions, and its doc comment states the reason:

/// Derive a provider id from the filename Tauri stages at runtime. Tauri
/// removes its target-triple suffix while copying an external binary, but on
/// Windows leaves the executable/script extension, which is not part of the
/// provider id.
fn provider_id_from_filename(name: &str) -> Option<&str> {
    let raw = name.strip_prefix("buzz-backend-")?;
    let id = [".exe", ".bat", ".cmd"]
        .into_iter()
        // ...

So buzz-backend-example.cmd yields the id example, which passes the [a-z0-9][a-z0-9_-]* check in resolve_provider_binary and resolves normally.

2. Nothing filters it out. is_executable returns true unconditionally on non-unix, so the .cmd passes the executability gate in discover_provider_candidates (which scans PATH, the app exe directory, and ~/.local/bin).

3. Staging discards the extension. stage_provider hard-codes the suffix:

fn stage_provider(
    binary: &Path,
) -> Result<(tempfile::TempDir, PathBuf, String, std::fs::File), String> {
    let directory = tempfile::Builder::new()
        .prefix("buzz-provider-")
        .tempdir()
        .map_err(|error| format!("failed to create provider staging directory: {error}"))?;
    let suffix = if cfg!(windows) { ".exe" } else { "" };
    let staged_path = directory.path().join(format!("provider{suffix}"));

The copy is then made read-only and held open under a share-mode guard before either invocation. Whatever the source file was, the thing that actually gets spawned is named provider.exe — and CreateProcess dispatches on the extension, so a batch script behind an .exe name is rejected as a malformed PE image.

Why it is confusing

The info path and the deploy path do not agree on what they execute.

  • The info request issued for the Where to run dropdown calls invoke_provider on the original file (desktop/src-tauri/src/commands/agent_providers.rs:42). buzz-backend-example.cmd still has its extension there, so Windows dispatches it correctly and it answers.
  • provider_deploy calls stage_provider first and runs everything — including its own info negotiation — against the staged provider.exe.

So the provider appears healthy in the UI right up to the moment a user commits to creating an agent with it, and the error that finally surfaces names a temp path and a file the user never created.

There is also a standing disagreement inside main about which way this should go. #4289 added the extension strip that makes script providers discoverable; the still-open #3310 argues the opposite direction — narrow the Windows allowlist to exe/com and reject script-shaped providers outright, on the grounds that routing them through cmd.exe puts a shell-quoting surface in front of a code path that pipes an agent's private key over stdin. Neither of those touches stage_provider, which is where the current contradiction actually bites.

Suggested fixes

Offered as options, not a prescription:

  • (a) Preserve the original extension when staging on Windows. Take the suffix from binary instead of hard-coding ".exe". CreateProcess already routes .cmd/.bat through cmd.exe when the extension is present, so this alone is sufficient to make script providers work.
  • (b) Invoke staged script providers explicitly through cmd.exe /c rather than relying on implicit dispatch — still with the extension preserved, since cmd.exe /c against a file named provider.exe fails the same way.
  • (c) If only PE binaries are supportable on Windows, reject .cmd/.bat at discovery time with an explicit error, instead of stripping their extensions and letting them appear to work. This is the direction fix(desktop): make backend providers runnable on Windows #3310 proposes, with the stdin/secret-handling rationale noted above; it would also mean removing .bat/.cmd from provider_id_from_filename so the two decisions stop contradicting each other.

Whichever direction is chosen, the useful invariant is that discovery and deploy should agree: a provider that reaches the Where to run dropdown and answers info should not be one that can never be spawned.

Workaround

For anyone hitting this now: ship a small native .exe shim named buzz-backend-<id>.exe that re-executes the real script with inherited stdio. The shim must reference the script by absolute path — the staged copy lives alone in a fresh temp directory, so nothing resolves relative to the shim's own location at runtime, and neither does anything else that was sitting next to the original file.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions