Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cbaeabf
fix(desktop): resolve bundled sidecar on cheap path and bound login-s…
Aug 26, 2026
06e0a3e
fix(desktop): harden bounded-command timeout against uncooperative ch…
Aug 26, 2026
2eacc23
fix(desktop): reap process tree on every bounded-command exit path
Aug 26, 2026
d759d31
fix(desktop): own bounded-command tree via Job Object; fix vacuous tests
Aug 26, 2026
e04c8a4
fix(desktop): own bounded-command tree atomically on Windows
Aug 27, 2026
fd3fbd0
fix(desktop): gate cheap runtime consumers on the launch boot-warm pass
Aug 27, 2026
2e6ce46
fix(desktop): preserve CREATE_NO_WINDOW on bounded Windows spawns
Aug 27, 2026
31eb8b3
fix(desktop): gate boot-warm on state, not catalog length
Aug 27, 2026
4fb6f1e
fix(desktop): bound bounded-command capture and de-race the Windows f…
Aug 27, 2026
589838b
fix(desktop): cancel the cheap runtime query before writing the force…
Aug 27, 2026
73c6e30
fix(desktop): enforce bounded-command capture in the drain sink
Aug 27, 2026
8aec606
fix(desktop): bound Unix capture drains without depending on writer d…
Aug 27, 2026
82827b8
fix(desktop): bound the drain Ok(n) path and de-quote Windows fixtures
Aug 27, 2026
6453432
test(managed-agents): surface transcripts on watchdog expiry in Windo…
Aug 27, 2026
4fa6f57
fix(desktop): guard login-shell PATH cache against stale-probe recache
Aug 28, 2026
78c9409
test(desktop): restore shared login-shell cache after race fixtures
Aug 28, 2026
9f815d3
test(desktop): make stale-probe race test exercise the generation guard
Aug 28, 2026
cc5a690
fix(desktop): return authoritative login-shell PATH after probe race
Aug 28, 2026
51efce3
docs(desktop): align bounded_command tree-termination claims with con…
Aug 28, 2026
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
2 changes: 1 addition & 1 deletion desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ user-idle = { version = "0.6", default-features = false }
plist = "1"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_Security", "Win32_Storage_FileSystem", "Win32_System_JobObjects", "Win32_System_Registry", "Win32_System_Threading", "Win32_Foundation"] }
windows-sys = { version = "0.61", features = ["Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Diagnostics_ToolHelp", "Win32_System_JobObjects", "Win32_System_Registry", "Win32_System_Threading", "Win32_Foundation"] }
keyring = { version = "3.6.3", default-features = false, features = ["windows-native", "vendored"], optional = true }
user-idle = { version = "0.6", default-features = false }

Expand Down
103 changes: 24 additions & 79 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use std::time::Duration;

use crate::managed_agents::{
buzz_managed_command_path, buzz_managed_node_bin_dir, buzz_managed_npm_bin_dir,
AcpAvailabilityStatus, AcpRuntimeCatalogEntry, AuthStatus, CommandAvailabilityInfo,
HarnessSource,
};
mod auth_status_cache;
mod bounded_command;
mod login_shell;
mod presets;
mod runtime_metadata;
Expand Down Expand Up @@ -593,6 +593,16 @@ pub fn resolve_command_cached(command: &str) -> Option<PathBuf> {
if let Some(managed) = resolve_buzz_managed_command(command) {
return Some(managed);
}
// Bundled sidecars (e.g. `buzz-agent`) ship next to the app executable, so
// `resolve_workspace_command` finds them with a filesystem stat and no
// login-shell spawn — the same class of work the managed-shim check above
// already performs. Without this the cheap path could never see the sidecar
// until a forced discovery warmed the resolve cache, so `buzz-agent` (which
// cannot legitimately be missing) reported "not installed" at every cold
// launch across the create/edit and agent-defaults surfaces.
if let Some(workspace) = resolve_workspace_command(command) {
return Some(workspace);
}
resolve_cache()
.lock()
.ok()
Expand Down Expand Up @@ -822,10 +832,9 @@ pub(crate) fn is_npm_global_install(cmd: &str) -> bool {

/// Run a CLI auth probe with a 10-second process-level timeout.
///
/// Spawns the probe CLI as a child process. Stdout and stderr are drained on
/// background threads to prevent pipe-buffer deadlock. On timeout the child is
/// killed and `Unknown` is returned; no orphaned threads or processes are left
/// behind. Returns `Unknown` on timeout.
/// On timeout or spawn failure the child is killed and `Unknown` is returned;
/// no orphaned threads or processes are left behind (see
/// [`bounded_command::output_with_timeout`]).
fn probe_auth_status(binary_path: &Path, probe_args: &[&str]) -> AuthStatus {
use crate::managed_agents::readiness::cli_probe;

Expand All @@ -836,81 +845,17 @@ fn probe_auth_status(binary_path: &Path, probe_args: &[&str]) -> AuthStatus {
if let Some(ref path) = augmented_path {
command.env("PATH", path);
}
command
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
crate::util::configure_no_window(&mut command);

let mut child = match command.spawn() {
Ok(c) => c,
Err(_) => return AuthStatus::Unknown,
// Window suppression is owned by `output_with_timeout`'s spawn
// (`BOUNDED_CREATION_FLAGS` carries `CREATE_NO_WINDOW`); a
// `configure_no_window` call here would be clobbered by that later
// `creation_flags` set, so it is deliberately omitted.

let Some(output) = bounded_command::output_with_timeout(command, Duration::from_secs(10))
else {
return AuthStatus::Unknown;
};

// Drain stdout/stderr on background threads to prevent pipe-buffer deadlock.
let stdout_pipe = child.stdout.take();
let stderr_pipe = child.stderr.take();

let stdout_thread = std::thread::spawn(move || {
let mut buf = Vec::new();
if let Some(mut pipe) = stdout_pipe {
let _ = pipe.read_to_end(&mut buf);
}
});
let stderr_thread = std::thread::spawn(move || {
let mut buf = Vec::new();
if let Some(mut pipe) = stderr_pipe {
let _ = pipe.read_to_end(&mut buf);
}
buf
});

// Save PID for kill-on-timeout before moving child into the wait thread.
let child_pid = child.id();
let (tx, rx) = std::sync::mpsc::channel();
let wait_thread = std::thread::spawn(move || {
let _ = tx.send(child.wait());
});

// 10-second timeout for auth probes.
let deadline = Instant::now() + Duration::from_secs(10);
let exit_status = loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
#[cfg(unix)]
unsafe {
libc::kill(child_pid as i32, libc::SIGTERM);
}
#[cfg(not(unix))]
let _ = child_pid;
drop(rx);
let _ = wait_thread.join();
let _ = stdout_thread.join();
let _ = stderr_thread.join();
return AuthStatus::Unknown;
}
match rx.recv_timeout(Duration::from_millis(100).min(remaining)) {
Ok(Ok(status)) => break status,
Ok(Err(_)) => {
let _ = wait_thread.join();
let _ = stdout_thread.join();
let _ = stderr_thread.join();
return AuthStatus::Unknown;
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
let _ = stdout_thread.join();
let _ = stderr_thread.join();
return AuthStatus::Unknown;
}
}
};

let _ = wait_thread.join();
let _ = stdout_thread.join();
let stderr_bytes = stderr_thread.join().unwrap_or_default();

match cli_probe::classify_probe_output(&stderr_bytes, exit_status.success()) {
match cli_probe::classify_probe_output(&output.stderr, output.status.success()) {
cli_probe::ProbeOutcome::LoggedIn => AuthStatus::LoggedIn,
cli_probe::ProbeOutcome::LoggedOut => AuthStatus::LoggedOut,
cli_probe::ProbeOutcome::ConfigInvalid { stderr_excerpt } => AuthStatus::ConfigInvalid {
Expand Down
Loading
Loading