mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Add managed agent avatars (#94)
This commit is contained in:
@@ -7,17 +7,18 @@ use crate::{
|
||||
managed_agents::{
|
||||
admin_command, build_managed_agent_summary, command_availability, default_token_scopes,
|
||||
discover_local_acp_providers, find_managed_agent_mut, load_managed_agents,
|
||||
managed_agent_log_path, read_log_tail, run_sprout_admin_mint_token, save_managed_agents,
|
||||
start_managed_agent_process, stop_managed_agent_process, sync_managed_agent_processes,
|
||||
AcpProviderInfo, CreateManagedAgentRequest, CreateManagedAgentResponse,
|
||||
DiscoverManagedAgentPrereqsRequest, ManagedAgentLogResponse, ManagedAgentPrereqsInfo,
|
||||
ManagedAgentSummary, MintManagedAgentTokenRequest, MintManagedAgentTokenResponse,
|
||||
RelayAgentInfo, DEFAULT_ACP_COMMAND, DEFAULT_AGENT_ARG, DEFAULT_AGENT_COMMAND,
|
||||
DEFAULT_AGENT_PARALLELISM, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS, DEFAULT_MCP_COMMAND,
|
||||
managed_agent_avatar_url, managed_agent_log_path, read_log_tail,
|
||||
run_sprout_admin_mint_token, save_managed_agents, start_managed_agent_process,
|
||||
stop_managed_agent_process, sync_managed_agent_processes, AcpProviderInfo,
|
||||
CreateManagedAgentRequest, CreateManagedAgentResponse, DiscoverManagedAgentPrereqsRequest,
|
||||
ManagedAgentLogResponse, ManagedAgentPrereqsInfo, ManagedAgentSummary,
|
||||
MintManagedAgentTokenRequest, MintManagedAgentTokenResponse, RelayAgentInfo,
|
||||
DEFAULT_ACP_COMMAND, DEFAULT_AGENT_ARG, DEFAULT_AGENT_COMMAND, DEFAULT_AGENT_PARALLELISM,
|
||||
DEFAULT_AGENT_TURN_TIMEOUT_SECONDS, DEFAULT_MCP_COMMAND,
|
||||
},
|
||||
relay::{
|
||||
build_authed_request, managed_agent_owner_pubkey, relay_ws_url, send_json_request,
|
||||
sync_managed_agent_profile_display_name,
|
||||
sync_managed_agent_profile,
|
||||
},
|
||||
util::now_iso,
|
||||
};
|
||||
@@ -269,13 +270,15 @@ pub async fn create_managed_agent(
|
||||
))
|
||||
}?;
|
||||
|
||||
let profile_sync_error = match sync_managed_agent_profile_display_name(
|
||||
let avatar_url = managed_agent_avatar_url(agent.agent_command.as_str());
|
||||
let profile_sync_error = match sync_managed_agent_profile(
|
||||
&state,
|
||||
&resolved_relay_url,
|
||||
&pubkey,
|
||||
api_token.as_deref(),
|
||||
&token_scopes,
|
||||
name,
|
||||
avatar_url.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -13,9 +13,15 @@ struct KnownAcpProvider {
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
command: &'static str,
|
||||
aliases: &'static [&'static str],
|
||||
default_args: &'static [&'static str],
|
||||
avatar_url: &'static str,
|
||||
}
|
||||
|
||||
const GOOSE_AVATAR_URL: &str = "https://block.github.io/goose/img/logo_dark.png";
|
||||
const CLAUDE_CODE_AVATAR_URL: &str = "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default";
|
||||
const CODEX_AVATAR_URL: &str = "https://openai.gallerycdn.vsassets.io/extensions/openai/chatgpt/26.5313.41514/1773706730621/Microsoft.VisualStudio.Services.Icons.Default";
|
||||
|
||||
const COMMON_BINARY_PATHS: &[&str] = &[
|
||||
"/opt/homebrew/bin",
|
||||
"/usr/local/bin",
|
||||
@@ -28,19 +34,25 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[
|
||||
id: "goose",
|
||||
label: "Goose",
|
||||
command: "goose",
|
||||
aliases: &[],
|
||||
default_args: &[DEFAULT_AGENT_ARG],
|
||||
avatar_url: GOOSE_AVATAR_URL,
|
||||
},
|
||||
KnownAcpProvider {
|
||||
id: "claude",
|
||||
label: "Claude Code",
|
||||
command: "claude-agent-acp",
|
||||
aliases: &["claude-code", "claudecode"],
|
||||
default_args: &[],
|
||||
avatar_url: CLAUDE_CODE_AVATAR_URL,
|
||||
},
|
||||
KnownAcpProvider {
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
command: "codex-acp",
|
||||
aliases: &[],
|
||||
default_args: &[],
|
||||
avatar_url: CODEX_AVATAR_URL,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -62,6 +74,42 @@ fn executable_basename(command: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_command_identity(command: &str) -> String {
|
||||
let normalized = command.trim().replace('\\', "/");
|
||||
let basename = normalized.rsplit('/').next().unwrap_or(normalized.as_str());
|
||||
let lower = basename
|
||||
.chars()
|
||||
.map(|character| match character {
|
||||
' ' | '_' => '-',
|
||||
_ => character.to_ascii_lowercase(),
|
||||
})
|
||||
.collect::<String>();
|
||||
let lower = lower.strip_suffix(".exe").unwrap_or(&lower).to_string();
|
||||
|
||||
if let Some(suffix) = std::env::consts::EXE_SUFFIX.strip_prefix('.') {
|
||||
return lower.strip_suffix(&format!(".{suffix}")).unwrap_or(&lower).to_string();
|
||||
}
|
||||
|
||||
if !std::env::consts::EXE_SUFFIX.is_empty() {
|
||||
return lower
|
||||
.strip_suffix(std::env::consts::EXE_SUFFIX)
|
||||
.unwrap_or(&lower)
|
||||
.to_string();
|
||||
}
|
||||
|
||||
lower
|
||||
}
|
||||
|
||||
fn known_acp_provider(command: &str) -> Option<&'static KnownAcpProvider> {
|
||||
let normalized = normalize_command_identity(command);
|
||||
|
||||
KNOWN_ACP_PROVIDERS.iter().find(|provider| {
|
||||
normalized == provider.id
|
||||
|| normalized == normalize_command_identity(provider.command)
|
||||
|| provider.aliases.iter().any(|alias| normalized == *alias)
|
||||
})
|
||||
}
|
||||
|
||||
fn command_search_dirs(app: Option<&AppHandle>) -> Vec<PathBuf> {
|
||||
let mut dirs = vec![
|
||||
workspace_root_dir().join("target/release"),
|
||||
@@ -212,6 +260,11 @@ pub fn discover_local_acp_providers() -> Vec<AcpProviderInfo> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn managed_agent_avatar_url(command: &str) -> Option<String> {
|
||||
let provider = known_acp_provider(command)?;
|
||||
Some(provider.avatar_url.to_string())
|
||||
}
|
||||
|
||||
pub fn admin_command() -> String {
|
||||
std::env::var("SPROUT_ADMIN_COMMAND").unwrap_or_else(|_| "sprout-admin".to_string())
|
||||
}
|
||||
@@ -224,6 +277,42 @@ pub fn default_token_scopes() -> Vec<String> {
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
managed_agent_avatar_url, CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn resolves_known_avatar_for_bare_command() {
|
||||
let avatar_url =
|
||||
managed_agent_avatar_url("goose").expect("goose avatar should resolve");
|
||||
|
||||
assert_eq!(avatar_url, GOOSE_AVATAR_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_known_avatar_for_command_paths_and_aliases() {
|
||||
assert_eq!(
|
||||
managed_agent_avatar_url("/usr/local/bin/codex-acp"),
|
||||
Some(CODEX_AVATAR_URL.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
managed_agent_avatar_url("Claude Code"),
|
||||
Some(CLAUDE_CODE_AVATAR_URL.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
managed_agent_avatar_url(r"C:\Tools\claude-agent-acp.exe"),
|
||||
Some(CLAUDE_CODE_AVATAR_URL.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_for_unknown_commands() {
|
||||
assert!(managed_agent_avatar_url("custom-agent").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
fn run_sprout_admin_command(
|
||||
program: &Path,
|
||||
pubkey: &str,
|
||||
|
||||
@@ -81,13 +81,14 @@ fn token_supports_scope(scopes: &[String], required_scope: &str) -> bool {
|
||||
scopes.iter().any(|scope| scope == required_scope)
|
||||
}
|
||||
|
||||
pub async fn sync_managed_agent_profile_display_name(
|
||||
pub async fn sync_managed_agent_profile(
|
||||
state: &AppState,
|
||||
relay_url: &str,
|
||||
pubkey: &str,
|
||||
api_token: Option<&str>,
|
||||
token_scopes: &[String],
|
||||
display_name: &str,
|
||||
avatar_url: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let url = format!(
|
||||
"{}{}",
|
||||
@@ -105,7 +106,7 @@ pub async fn sync_managed_agent_profile_display_name(
|
||||
|
||||
let request = request.json(&UpdateProfileBody {
|
||||
display_name: Some(display_name),
|
||||
avatar_url: None,
|
||||
avatar_url,
|
||||
about: None,
|
||||
nip05_handle: None,
|
||||
});
|
||||
@@ -113,13 +114,13 @@ pub async fn sync_managed_agent_profile_display_name(
|
||||
send_empty_request(request).await.map_err(|error| {
|
||||
if api_token.is_some() && !use_bearer_token {
|
||||
format!(
|
||||
"Created the agent, but could not sync its profile display name. The minted token does not include `users:write`, and the relay rejected dev-mode pubkey auth: {error}"
|
||||
"Created the agent, but could not sync its profile metadata. The minted token does not include `users:write`, and the relay rejected dev-mode pubkey auth: {error}"
|
||||
)
|
||||
} else if api_token.is_some() {
|
||||
format!("Created the agent, but could not sync its profile display name: {error}")
|
||||
format!("Created the agent, but could not sync its profile metadata: {error}")
|
||||
} else {
|
||||
format!(
|
||||
"Created the agent, but could not sync its profile display name without a token: {error}"
|
||||
"Created the agent, but could not sync its profile metadata without a token: {error}"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user