diff --git a/src/llm/mod.rs b/src/llm/mod.rs index b896c80d..aa0aeccf 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -43,7 +43,7 @@ pub use config::{ }; pub use error::LlmError; pub use failover::{CooldownConfig, FailoverProvider}; -pub use nearai_auth::resolve_nearai_bearer_token; +pub use nearai_auth::{resolve_nearai_bearer_token, resolve_nearai_bearer_token_if_available}; pub use nearai_chat::{ModelInfo, NearAiChatProvider}; pub use provider::{ ChatMessage, CompletionRequest, CompletionResponse, ContentPart, FinishReason, ImageUrl, diff --git a/src/llm/nearai_auth.rs b/src/llm/nearai_auth.rs index cbd4cbee..f049bd4d 100644 --- a/src/llm/nearai_auth.rs +++ b/src/llm/nearai_auth.rs @@ -3,6 +3,32 @@ use secrecy::{ExposeSecret, SecretString}; use crate::llm::LlmError; use crate::llm::session::SessionManager; +/// Resolve the active NEAR AI bearer token only if already available. +/// +/// Unlike [`resolve_nearai_bearer_token`], this helper is side-effect free: +/// it never triggers an interactive login flow. +pub async fn resolve_nearai_bearer_token_if_available( + api_key: Option<&SecretString>, + session: &SessionManager, +) -> Result, LlmError> { + if let Some(api_key) = api_key { + return Ok(Some(api_key.expose_secret().to_string())); + } + + if session.has_token().await { + let token = session.get_token().await?; + return Ok(Some(token.expose_secret().to_string())); + } + + if let Ok(key) = std::env::var("NEARAI_API_KEY") + && !key.is_empty() + { + return Ok(Some(key)); + } + + Ok(None) +} + /// Resolve the active NEAR AI bearer token. /// /// Priority order: @@ -14,26 +40,14 @@ pub async fn resolve_nearai_bearer_token( api_key: Option<&SecretString>, session: &SessionManager, ) -> Result { - if let Some(api_key) = api_key { - return Ok(api_key.expose_secret().to_string()); - } - - if session.has_token().await { - let token = session.get_token().await?; - return Ok(token.expose_secret().to_string()); + if let Some(token) = resolve_nearai_bearer_token_if_available(api_key, session).await? { + return Ok(token); } session.ensure_authenticated().await?; - if session.has_token().await { - let token = session.get_token().await?; - return Ok(token.expose_secret().to_string()); - } - - if let Ok(key) = std::env::var("NEARAI_API_KEY") - && !key.is_empty() - { - return Ok(key); + if let Some(token) = resolve_nearai_bearer_token_if_available(api_key, session).await? { + return Ok(token); } Err(LlmError::AuthFailed { diff --git a/src/tools/mcp/client.rs b/src/tools/mcp/client.rs index 50efd62f..65d1d8b1 100644 --- a/src/tools/mcp/client.rs +++ b/src/tools/mcp/client.rs @@ -308,12 +308,11 @@ impl McpClient { )); }; - crate::llm::resolve_nearai_bearer_token( + crate::llm::resolve_nearai_bearer_token_if_available( self.nearai_api_key.as_ref(), session_manager, ) .await - .map(Some) .map_err(|e| { ToolError::ExternalService(format!( "Failed to resolve NEAR AI token for MCP server '{}': {}", @@ -885,6 +884,27 @@ mod tests { ); } + #[tokio::test] + async fn test_build_request_headers_without_nearai_auth_does_not_trigger_login() { + use crate::llm::{ + SessionConfig as NearAiSessionConfig, SessionManager as NearAiSessionManager, + }; + + let config = McpServerConfig::new("chat_api", "http://localhost:3000/mcp") + .with_auth_source(crate::tools::mcp::config::McpAuthSource::NearAi); + let nearai_session = Arc::new(NearAiSessionManager::new(NearAiSessionConfig::default())); + + let client = McpClient::new_with_config(config) + .expect("valid MCP config") + .with_nearai_session_manager(nearai_session); + let headers = client.build_request_headers().await.expect("headers"); + + assert!( + !headers.contains_key("Authorization"), + "runtime auth should stay absent when no token is available" + ); + } + #[test] fn test_next_request_id_monotonically_increasing() { let client = McpClient::new("http://localhost:1234");