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
13 changes: 13 additions & 0 deletions desktop/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,21 @@ fn main() {
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_RELAY_RECONNECT_CMD");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_CANDIDATE_ID");
println!("cargo:rustc-check-cfg=cfg(buzz_updater_enabled)");

if let Ok(candidate_id) = std::env::var("BUZZ_BUILD_CANDIDATE_ID") {
let valid = !candidate_id.is_empty()
&& candidate_id.len() <= 48
&& candidate_id.chars().all(|character| {
character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-'
});
if !valid {
panic!("BUZZ_BUILD_CANDIDATE_ID must match [a-z0-9-] and be at most 48 characters");
}
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_CANDIDATE_ID={candidate_id}");
}

// Explicit owner-only agent-access capability. Release packaging sets this
// presence-only marker; OSS/custom builds leave agent access configurable.
if std::env::var("BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY").is_ok() {
Expand Down
7 changes: 6 additions & 1 deletion desktop/src-tauri/src/app_state_keyring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ fn dev_keyring_service(configured: Option<String>) -> String {
}

pub(crate) fn keyring_service() -> &'static str {
if cfg!(debug_assertions) {
if let Some(candidate_id) = option_env!("BUZZ_DESKTOP_BUILD_CANDIDATE_ID") {
static CANDIDATE_SERVICE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
CANDIDATE_SERVICE
.get_or_init(|| format!("buzz-desktop-candidate.{candidate_id}"))
.as_str()
} else if cfg!(debug_assertions) {
static DEV_SERVICE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
DEV_SERVICE
.get_or_init(|| dev_keyring_service(std::env::var("BUZZ_DEV_KEYRING_SERVICE").ok()))
Expand Down
99 changes: 84 additions & 15 deletions desktop/src-tauri/src/commands/agent_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,34 @@ fn run_buzz_acp_auth_command<const N: usize>(
) -> Result<std::process::Output, String> {
let runtime = known_acp_runtime_exact(runtime_id)
.ok_or_else(|| format!("unknown ACP runtime: {runtime_id}"))?;
let adapter_command = runtime
.commands
.iter()
.find_map(|command| resolve_command(command).map(|path| (*command, path)))
.ok_or_else(|| format!("{} ACP adapter is not installed", runtime.label))?;
let (adapter_name, adapter_path, runtime_plan) = if runtime.id == "codex" && !cfg!(windows) {
let mut planned_runtime = None;
let mut last_plan_error = None;
for adapter_name in runtime.commands {
match crate::managed_agents::runtime_plan::resolve_runtime_execution_plan(adapter_name)
{
Ok(Some(plan)) => {
planned_runtime = Some((*adapter_name, plan));
break;
}
Ok(None) => {}
Err(error) => last_plan_error = Some(error),
}
}
let (adapter_name, plan) = planned_runtime.ok_or_else(|| {
last_plan_error
.unwrap_or_else(|| format!("{} ACP adapter is not installed", runtime.label))
})?;
let adapter_path = plan.harness_path()?.to_path_buf();
(adapter_name, adapter_path, Some(plan))
} else {
let (adapter_name, adapter_path) = runtime
.commands
.iter()
.find_map(|command| resolve_command(command).map(|path| (*command, path)))
.ok_or_else(|| format!("{} ACP adapter is not installed", runtime.label))?;
(adapter_name, adapter_path, None)
};

let acp_path = std::env::current_exe()
.map(|path| path.with_file_name(format!("buzz-acp{}", std::env::consts::EXE_SUFFIX)))
Expand All @@ -129,10 +152,11 @@ fn run_buzz_acp_auth_command<const N: usize>(
let augmented_path = auth_command_path();
run_buzz_acp_auth_command_with_paths(
&acp_path,
adapter_command.0,
&adapter_command.1,
adapter_name,
&adapter_path,
args,
augmented_path.as_deref(),
runtime_plan.as_ref(),
)
}

Expand Down Expand Up @@ -179,6 +203,7 @@ fn run_buzz_acp_auth_command_with_paths<const N: usize>(
adapter_path: &Path,
args: [&str; N],
augmented_path: Option<&str>,
runtime_plan: Option<&crate::managed_agents::runtime_plan::RuntimeExecutionPlan>,
) -> Result<std::process::Output, String> {
let agent_args = normalize_agent_args(adapter_name, Vec::new());
let mut command = Command::new(acp_path);
Expand All @@ -194,6 +219,10 @@ fn run_buzz_acp_auth_command_with_paths<const N: usize>(
if let Some(path) = augmented_path {
command.env("PATH", path);
}
if let Some(plan) = runtime_plan {
plan.verify()?;
plan.apply_environment(&mut command);
}
crate::util::configure_no_window(&mut command);

command
Expand Down Expand Up @@ -254,9 +283,47 @@ fn launch_terminal_auth(runtime_id: &str, method: &AcpAuthMethod) -> Result<(),
.iter()
.find_map(|command| resolve_command(command).map(|path| (*command, path)))
.ok_or_else(|| format!("{} ACP adapter is not installed", runtime.label))?;
let runtime_plan = if runtime.id == "codex" {
crate::managed_agents::runtime_plan::resolve_runtime_execution_plan(adapter_command.0)?
} else {
None
};
let fallback_command = adapter_command.1.display().to_string();
let argv = adapter_terminal_argv(runtime.label, method, &fallback_command)?;
launch_visible_terminal(&argv)
let mut argv = adapter_terminal_argv(runtime.label, method, &fallback_command)?;
if let Some(plan) = runtime_plan.as_ref() {
let provider_path = plan
.provider_cli_path()
.ok_or_else(|| "Codex runtime plan has no provider CLI".to_string())?;
let command = argv
.first_mut()
.ok_or_else(|| "Codex terminal login command is empty".to_string())?;
*command = provider_path.display().to_string();
plan.verify()?;
}
let terminal_prelude = runtime_plan
.as_ref()
.map(runtime_plan_shell_prelude)
.unwrap_or_default();
launch_visible_terminal(&argv, &terminal_prelude)
}

fn runtime_plan_shell_prelude(
plan: &crate::managed_agents::runtime_plan::RuntimeExecutionPlan,
) -> String {
let mut prelude = String::new();
for key in plan.denied_environment() {
prelude.push_str("unset ");
prelude.push_str(key);
prelude.push('\n');
}
for (key, value) in plan.generated_environment_entries() {
prelude.push_str("export ");
prelude.push_str(key);
prelude.push('=');
prelude.push_str(&shell_escape(value));
prelude.push('\n');
}
prelude
}

fn adapter_terminal_argv(
Expand Down Expand Up @@ -361,15 +428,16 @@ fn spawn_without_stdio(mut command: Command) -> Result<(), String> {
}

#[cfg(target_os = "macos")]
fn launch_visible_terminal(argv: &[String]) -> Result<(), String> {
fn launch_visible_terminal(argv: &[String], shell_prelude: &str) -> Result<(), String> {
let mut script = tempfile::Builder::new()
.prefix("buzz-auth-")
.suffix(".command")
.tempfile()
.map_err(|error| format!("failed to create terminal login script: {error}"))?;
writeln!(
script,
"#!/bin/sh\ntrap 'rm -f -- \"$0\"' EXIT\n{}",
"#!/bin/sh\ntrap 'rm -f -- \"$0\"' EXIT\n{}{}",
shell_prelude,
shell_join(argv)
)
.map_err(|error| format!("failed to write terminal login script: {error}"))?;
Expand All @@ -395,8 +463,8 @@ fn launch_visible_terminal(argv: &[String]) -> Result<(), String> {
}

#[cfg(target_os = "linux")]
fn launch_visible_terminal(argv: &[String]) -> Result<(), String> {
let command = shell_join(argv);
fn launch_visible_terminal(argv: &[String], shell_prelude: &str) -> Result<(), String> {
let command = format!("{}{}", shell_prelude, shell_join(argv));
let candidates: [(&str, &[&str]); 4] = [
("x-terminal-emulator", &["-e", "sh", "-lc"]),
("gnome-terminal", &["--", "sh", "-lc"]),
Expand All @@ -414,7 +482,7 @@ fn launch_visible_terminal(argv: &[String]) -> Result<(), String> {
}

#[cfg(target_os = "windows")]
fn launch_visible_terminal(argv: &[String]) -> Result<(), String> {
fn launch_visible_terminal(argv: &[String], _shell_prelude: &str) -> Result<(), String> {
use std::os::windows::process::CommandExt;

const CREATE_NEW_CONSOLE: u32 = 0x0000_0010;
Expand All @@ -436,7 +504,7 @@ fn windows_terminal_args(argv: &[String]) -> Vec<String> {
}

#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn launch_visible_terminal(_argv: &[String]) -> Result<(), String> {
fn launch_visible_terminal(_argv: &[String], _shell_prelude: &str) -> Result<(), String> {
Err("opening a terminal is not supported on this platform".to_string())
}

Expand Down Expand Up @@ -542,6 +610,7 @@ mod tests {
&adapter_path,
["auth-methods", "--json"],
Some(&augmented_path),
None,
)
.expect("run auth command");

Expand Down
4 changes: 2 additions & 2 deletions desktop/src-tauri/src/commands/agent_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ pub async fn save_custom_harness(
// so concurrent saves never produce a stale registry snapshot (B-6).
custom_harnesses::save_and_warm(&custom_dir, &definition, rename_old_id.as_deref())?;

// Resolve availability for the returned catalog entry.
let (availability, command_opt, binary_path) =
match crate::managed_agents::find_command(&definition.command) {
Some(path) => (
Expand All @@ -148,7 +147,6 @@ pub async fn save_custom_harness(
),
None => (AcpAvailabilityStatus::NotInstalled, None, None),
};

let default_args =
crate::managed_agents::normalize_agent_args(&definition.command, definition.args.clone());

Expand All @@ -159,6 +157,8 @@ pub async fn save_custom_harness(
availability,
command: command_opt,
binary_path,
runtime_plan_id: None,
runtime_plan_source: None,
default_args,
mcp_command: None,
model_env_var: None,
Expand Down
17 changes: 16 additions & 1 deletion desktop/src-tauri/src/commands/agent_model_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ pub(super) async fn run_agent_models_command(
persisted_model: Option<String>,
merged_env: BTreeMap<String, String>,
) -> Result<AgentModelsResponse, String> {
let runtime_plan =
crate::managed_agents::runtime_plan::resolve_runtime_execution_plan(&agent_command)?;
let agent_command = match runtime_plan.as_ref() {
Some(plan) => plan.harness_path()?.display().to_string(),
None => agent_command,
};

// Clone the env map for redaction below — `merged_env` is moved
// into the spawn_blocking closure and we still need the values to
// scrub any user-supplied secrets that the child surfaces in stderr.
Expand Down Expand Up @@ -54,7 +61,15 @@ pub(super) async fn run_agent_models_command(
for (k, v) in &merged_env {
cmd.env(k, v);
}
crate::managed_agents::configure_runtime_cli(&mut cmd, known_acp_runtime(&agent_command));
if let Some(plan) = runtime_plan.as_ref() {
plan.verify()?;
plan.apply_environment(&mut cmd);
} else {
crate::managed_agents::configure_runtime_cli(
&mut cmd,
known_acp_runtime(&agent_command),
);
}
crate::util::configure_no_window(&mut cmd);
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
Expand Down
Loading