Add preferred runtime onboarding (#2040)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-07-17 14:19:29 -06:00
committed by GitHub
parent 68e670e001
commit c19e0ce2d4
26 changed files with 1072 additions and 60 deletions

View File

@@ -3,6 +3,9 @@ use std::{
process::{Command, Stdio},
};
#[cfg(target_os = "macos")]
use std::{fs, io::Write, os::unix::fs::PermissionsExt};
use serde_json::Value;
use serde::{Deserialize, Serialize};
@@ -84,8 +87,12 @@ fn connect_acp_runtime_blocking(
.find(|candidate| candidate.id == request.method_id)
.ok_or_else(|| "auth method is no longer advertised by this adapter".to_string())?;
if method.method_type.as_deref() == Some("terminal") {
launch_terminal_auth(&request.runtime_id, method)?;
if uses_terminal_auth(method)? {
if method.id == "claude-login" && terminal_auth_meta_command(method)?.is_some() {
run_claude_login(&request.runtime_id, method)?;
} else {
launch_terminal_auth(&request.runtime_id, method)?;
}
return Ok(ConnectAcpRuntimeResult { launched: true });
}
@@ -208,6 +215,29 @@ fn command_error(label: &str, output: &std::process::Output) -> String {
}
}
fn run_claude_login(runtime_id: &str, method: &AcpAuthMethod) -> Result<(), String> {
let runtime = known_acp_runtime_exact(runtime_id)
.ok_or_else(|| format!("unknown ACP runtime: {runtime_id}"))?;
let argv = adapter_terminal_argv(runtime.label, method, "")?;
let (command, args) = argv
.split_first()
.ok_or_else(|| "Claude login command is empty".to_string())?;
let status = Command::new(command)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map_err(|error| format!("failed to run Claude login: {error}"))?;
if !status.success() {
return Err(format!(
"Claude login failed (exit {})",
status.code().unwrap_or(-1)
));
}
Ok(())
}
fn launch_terminal_auth(runtime_id: &str, method: &AcpAuthMethod) -> Result<(), String> {
let runtime = known_acp_runtime_exact(runtime_id)
.ok_or_else(|| format!("unknown ACP runtime: {runtime_id}"))?;
@@ -248,9 +278,17 @@ fn adapter_terminal_argv(
.unwrap_or_else(|| command.to_string());
let mut argv = vec![command_path];
argv.extend(args.iter().cloned());
if method.id == "claude-login" && terminal_auth_meta_command(method)?.is_some() {
argv.extend(["auth".to_string(), "login".to_string()]);
}
Ok(argv)
}
fn uses_terminal_auth(method: &AcpAuthMethod) -> Result<bool, String> {
Ok(method.method_type.as_deref() == Some("terminal")
|| terminal_auth_meta_command(method)?.is_some())
}
fn terminal_auth_meta_command(method: &AcpAuthMethod) -> Result<Option<Vec<String>>, String> {
let Some(meta) = method.meta.as_ref() else {
return Ok(None);
@@ -303,6 +341,7 @@ fn terminal_auth_meta_command(method: &AcpAuthMethod) -> Result<Option<Vec<Strin
Ok((!argv.is_empty()).then_some(argv))
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
fn spawn_without_stdio(mut command: Command) -> Result<(), String> {
command
.stdin(Stdio::null())
@@ -315,14 +354,36 @@ fn spawn_without_stdio(mut command: Command) -> Result<(), String> {
#[cfg(target_os = "macos")]
fn launch_visible_terminal(argv: &[String]) -> Result<(), String> {
let command = shell_join(argv);
let script = format!(
"tell application \"Terminal\"\n activate\n do script {}\nend tell",
applescript_string(&command)
);
let mut command = Command::new("osascript");
command.arg("-e").arg(script);
spawn_without_stdio(command)
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{}",
shell_join(argv)
)
.map_err(|error| format!("failed to write terminal login script: {error}"))?;
fs::set_permissions(script.path(), fs::Permissions::from_mode(0o700))
.map_err(|error| format!("failed to prepare terminal login script: {error}"))?;
let script = script
.into_temp_path()
.keep()
.map_err(|error| format!("failed to preserve terminal login script: {error}"))?;
let status = Command::new("open")
.arg(&script)
.status()
.map_err(|error| format!("failed to open terminal: {error}"))?;
if !status.success() {
let _ = fs::remove_file(&script);
return Err(format!(
"failed to open terminal (exit {})",
status.code().unwrap_or(-1)
));
}
Ok(())
}
#[cfg(target_os = "linux")]
@@ -389,16 +450,11 @@ fn shell_escape(arg: &str) -> String {
format!("'{}'", arg.replace('\'', "'\\''"))
}
#[cfg(target_os = "macos")]
fn applescript_string(value: &str) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
}
#[cfg(test)]
mod tests {
use super::{
adapter_terminal_argv, append_inherited_path, run_buzz_acp_auth_command_with_paths,
shell_escape, shell_join, windows_terminal_args, AcpAuthMethod,
shell_escape, shell_join, uses_terminal_auth, windows_terminal_args, AcpAuthMethod,
};
/// Windows regression: the augmented PATH there holds only Buzz-managed
@@ -532,6 +588,32 @@ mod tests {
assert_eq!(method.command[0], "claude");
}
#[test]
fn claude_terminal_meta_routes_around_unimplemented_authenticate() {
let raw = r#"{"_meta":{"terminal-auth":{"args":["/tmp/claude-cli.js"],"command":"node","label":"Claude Login"}},"description":"Run `claude /login` in the terminal","id":"claude-login","name":"Log in with Claude"}"#;
let method: AcpAuthMethod = serde_json::from_str(raw).unwrap();
assert_eq!(method.method_type, None);
assert!(uses_terminal_auth(&method).unwrap());
}
#[test]
fn claude_terminal_meta_starts_login_flow() {
let _guard = crate::managed_agents::lock_path_mutex();
let method: AcpAuthMethod = serde_json::from_str(
r#"{"_meta":{"terminal-auth":{"args":["/tmp/claude-cli.js"],"command":"definitely-not-on-path-node"}},"id":"claude-login","name":"Log in with Claude"}"#,
)
.unwrap();
assert_eq!(
adapter_terminal_argv("Claude Code", &method, "unused").unwrap(),
vec![
"definitely-not-on-path-node".to_string(),
"/tmp/claude-cli.js".to_string(),
"auth".to_string(),
"login".to_string(),
]
);
}
#[test]
fn terminal_argv_uses_adapter_declared_command() {
let _guard = crate::managed_agents::lock_path_mutex();

View File

@@ -63,6 +63,10 @@ pub struct GlobalAgentConfig {
/// a model. `None` = no global default.
#[serde(default)]
pub model: Option<String>,
/// Preferred ACP runtime for definitions without an explicit runtime.
#[serde(default)]
pub preferred_runtime: Option<String>,
}
/// Validate a `GlobalAgentConfig` before persisting it.

View File

@@ -266,6 +266,7 @@ fn roundtrip_serialization() {
env_vars: BTreeMap::from([("ANTHROPIC_API_KEY".to_string(), "sk-test".to_string())]),
provider: Some("anthropic".to_string()),
model: Some("claude-opus-4".to_string()),
preferred_runtime: Some("claude".to_string()),
};
let json = serde_json::to_string(&config).expect("serialize");
let back: GlobalAgentConfig = serde_json::from_str(&json).expect("deserialize");
@@ -582,6 +583,7 @@ fn populated_global_config_round_trips() {
.collect(),
provider: Some("anthropic".to_string()),
model: Some("claude-opus-4-5".to_string()),
preferred_runtime: None,
};
let json = serde_json::to_string(&original).expect("serialization must not fail");
let decoded: GlobalAgentConfig =