1#[cfg(feature = "mcp-client")]
7use crate::core::McpClientManager;
8use crate::core::config::{AgentConfig, AuthConfig, StorageConfig};
9use a2a_rs::adapter::{
10 BearerTokenAuthenticator, DefaultRequestProcessor, HttpServer, SimpleAgentInfo,
11};
12use a2a_rs::port::{
13 AsyncMessageHandler, AsyncNotificationManager, AsyncStreamingHandler, AsyncTaskManager,
14};
15use std::sync::Arc;
16use tracing::{info, warn};
17
18#[cfg(feature = "auth")]
19use a2a_rs::adapter::{JwtAuthenticator, OAuth2Authenticator};
20#[cfg(feature = "auth")]
21use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
22#[cfg(feature = "auth")]
23use std::collections::HashMap;
24
25pub struct AgentRuntime<H, S> {
27 config: AgentConfig,
28 handler: Arc<H>,
29 storage: Arc<S>,
30 #[cfg(feature = "mcp-client")]
31 mcp_client: Option<McpClientManager>,
32}
33
34impl<H, S> AgentRuntime<H, S>
35where
36 H: AsyncMessageHandler + Clone + Send + Sync + 'static,
37 S: AsyncTaskManager + AsyncNotificationManager + Clone + Send + Sync + 'static,
38{
39 pub fn new(config: AgentConfig, handler: Arc<H>, storage: Arc<S>) -> Self {
41 Self {
42 config,
43 handler,
44 storage,
45 #[cfg(feature = "mcp-client")]
46 mcp_client: None,
47 }
48 }
49
50 #[cfg(feature = "mcp-client")]
52 pub fn with_mcp_client(
53 config: AgentConfig,
54 handler: Arc<H>,
55 storage: Arc<S>,
56 mcp_client: McpClientManager,
57 ) -> Self {
58 Self {
59 config,
60 handler,
61 storage,
62 mcp_client: Some(mcp_client),
63 }
64 }
65
66 #[cfg(feature = "mcp-client")]
68 pub fn mcp_client(&self) -> Option<&McpClientManager> {
69 self.mcp_client.as_ref()
70 }
71
72 fn build_agent_info(&self, base_url: String) -> SimpleAgentInfo {
74 let mut agent_info = SimpleAgentInfo::new(self.config.agent.name.clone(), base_url);
75
76 if let Some(ref description) = self.config.agent.description {
77 agent_info = agent_info.with_description(description.clone());
78 }
79
80 if let Some(ref provider) = self.config.agent.provider {
81 agent_info = agent_info.with_provider(provider.name.clone(), provider.url.clone());
82 }
83
84 if let Some(ref doc_url) = self.config.agent.documentation_url {
85 agent_info = agent_info.with_documentation_url(doc_url.clone());
86 }
87
88 if let Some(ref version) = self.config.agent.version {
89 agent_info = agent_info.with_version(version.clone());
90 }
91
92 let mut schemes = std::collections::HashMap::new();
94 match &self.config.server.auth {
95 crate::core::config::AuthConfig::None => {}
96 crate::core::config::AuthConfig::Bearer { format, .. } => {
97 schemes.insert(
98 "bearer".to_string(),
99 a2a_rs::domain::SecurityScheme::http(
100 "bearer".to_string(),
101 format.clone(),
102 Some("Bearer token authentication".to_string()),
103 ),
104 );
105 }
106 crate::core::config::AuthConfig::ApiKey { location, name, .. } => {
107 schemes.insert(
108 "api_key".to_string(),
109 a2a_rs::domain::SecurityScheme::api_key(
110 name.clone(),
111 location.clone(),
112 Some("API Key authentication".to_string()),
113 ),
114 );
115 }
116 crate::core::config::AuthConfig::Jwt {
117 issuer, audience, ..
118 } => {
119 schemes.insert(
120 "jwt".to_string(),
121 a2a_rs::domain::SecurityScheme::http(
122 "bearer".to_string(),
123 Some("JWT".to_string()),
124 Some(format!(
125 "JWT authentication (issuer: {:?}, audience: {:?})",
126 issuer, audience
127 )),
128 ),
129 );
130 }
131 crate::core::config::AuthConfig::OAuth2 {
132 flow,
133 token_url,
134 authorization_url,
135 scopes,
136 ..
137 } => {
138 let scopes_map: std::collections::HashMap<String, String> =
139 scopes.iter().map(|s| (s.clone(), s.clone())).collect();
140 let flows = if flow == "client_credentials" {
141 a2a_rs::domain::OAuthFlows::client_credentials(
142 a2a_rs::domain::ClientCredentialsOAuthFlow {
143 token_url: token_url.clone(),
144 refresh_url: String::new(),
145 scopes: scopes_map,
146 ..Default::default()
147 },
148 )
149 } else {
150 a2a_rs::domain::OAuthFlows::authorization_code(
151 a2a_rs::domain::AuthorizationCodeOAuthFlow {
152 authorization_url: authorization_url.clone(),
153 token_url: token_url.clone(),
154 refresh_url: String::new(),
155 scopes: scopes_map,
156 ..Default::default()
157 },
158 )
159 };
160 schemes.insert(
161 "oauth2".to_string(),
162 a2a_rs::domain::SecurityScheme::oauth2(
163 flows,
164 Some("OAuth2 authentication".to_string()),
165 None,
166 ),
167 );
168 }
169 }
170 if !schemes.is_empty() {
171 agent_info = agent_info.with_security_schemes(schemes);
172 }
173
174 if self.config.features.streaming {
176 agent_info = agent_info.with_streaming();
177 }
178
179 if self.config.features.push_notifications {
180 agent_info = agent_info.with_push_notifications();
181 }
182
183 if self.config.features.state_history {
184 agent_info = agent_info.with_state_transition_history();
185 }
186
187 if self.config.features.authenticated_card {
188 agent_info = agent_info.with_authenticated_extended_card();
189 }
190
191 if let Some(ref ap2_config) = self.config.features.extensions.ap2 {
193 let roles_json: Vec<serde_json::Value> = ap2_config
194 .roles
195 .iter()
196 .map(|r| serde_json::Value::String(r.clone()))
197 .collect();
198
199 let mut params = std::collections::HashMap::new();
200 params.insert("roles".to_string(), serde_json::Value::Array(roles_json));
201 let params_val = serde_json::Value::Object(params.into_iter().collect());
202 let params_struct: buffa_types::google::protobuf::Struct =
203 serde_json::from_value(params_val).unwrap_or_default();
204
205 let ext = a2a_rs::domain::AgentExtension {
206 uri: "https://github.com/google-agentic-commerce/ap2/tree/v0.1".to_string(),
207 description: "Agent Payments Protocol (AP2) v0.1".to_string(),
208 required: ap2_config.required,
209 params: buffa::MessageField::some(params_struct),
210 ..Default::default()
211 };
212
213 agent_info = agent_info.add_extension(ext);
214 info!("💳 AP2 extension enabled (roles: {:?})", ap2_config.roles);
215 }
216
217 for skill in &self.config.skills {
219 agent_info = agent_info.add_comprehensive_skill(
220 skill.id.clone(),
221 skill.name.clone(),
222 skill.description.clone(),
223 if skill.keywords.is_empty() {
224 None
225 } else {
226 Some(skill.keywords.clone())
227 },
228 if skill.examples.is_empty() {
229 None
230 } else {
231 Some(skill.examples.clone())
232 },
233 Some(skill.input_formats.clone()),
234 Some(skill.output_formats.clone()),
235 );
236 }
237
238 agent_info
239 }
240
241 pub async fn start_http(&self) -> Result<(), RuntimeError> {
243 if self.config.server.http_port == 0 {
244 return Err(RuntimeError::ServerNotConfigured(
245 "HTTP port is 0".to_string(),
246 ));
247 }
248
249 let base_url = format!(
250 "http://{}:{}",
251 self.config.server.host, self.config.server.http_port
252 );
253 let agent_info = self.build_agent_info(base_url);
254
255 let processor = DefaultRequestProcessor::new(
256 (*self.handler).clone(),
257 (*self.storage).clone(),
258 (*self.storage).clone(),
259 agent_info.clone(),
260 );
261
262 let bind_address = format!(
263 "{}:{}",
264 self.config.server.host, self.config.server.http_port
265 );
266
267 info!("🌐 Starting HTTP server on {}", bind_address);
268 self.print_agent_info("HTTP", &self.config.server.http_port.to_string());
269
270 match &self.config.server.auth {
271 AuthConfig::None => {
272 let server = HttpServer::new(processor, agent_info, bind_address);
273 server
274 .start()
275 .await
276 .map_err(|e| RuntimeError::ServerError(e.to_string()))
277 }
278 AuthConfig::Bearer { tokens, format } => {
279 info!(
280 "🔐 Authentication: Bearer token ({} token(s){})",
281 tokens.len(),
282 format
283 .as_ref()
284 .map(|f| format!(", format: {}", f))
285 .unwrap_or_default()
286 );
287 let authenticator = BearerTokenAuthenticator::new(tokens.clone());
288 let server =
289 HttpServer::with_auth(processor, agent_info, bind_address, authenticator);
290 server
291 .start()
292 .await
293 .map_err(|e| RuntimeError::ServerError(e.to_string()))
294 }
295 AuthConfig::ApiKey {
296 keys,
297 location,
298 name,
299 } => {
300 warn!(
301 "🔐 API key authentication configured ({} {}, {} key(s)) but not yet supported, using no auth",
302 location,
303 name,
304 keys.len()
305 );
306 let server = HttpServer::new(processor, agent_info, bind_address);
307 server
308 .start()
309 .await
310 .map_err(|e| RuntimeError::ServerError(e.to_string()))
311 }
312 #[cfg(feature = "auth")]
313 AuthConfig::Jwt {
314 secret,
315 rsa_pem_path,
316 algorithm,
317 issuer,
318 audience,
319 } => {
320 info!("🔐 Authentication: JWT (algorithm: {})", algorithm);
321
322 let mut authenticator = if let Some(secret) = secret {
323 JwtAuthenticator::new_with_secret(secret.as_bytes())
324 } else if let Some(pem_path) = rsa_pem_path {
325 let pem_data = std::fs::read(pem_path).map_err(|e| {
326 RuntimeError::ServerError(format!("Failed to read RSA PEM file: {}", e))
327 })?;
328 JwtAuthenticator::new_with_rsa_pem(&pem_data).map_err(|e| {
329 RuntimeError::ServerError(format!(
330 "Failed to create JWT authenticator: {}",
331 e
332 ))
333 })?
334 } else {
335 return Err(RuntimeError::ServerError(
336 "JWT authentication requires either 'secret' or 'rsa_pem_path'".to_string(),
337 ));
338 };
339
340 if let Some(iss) = issuer {
341 authenticator = authenticator.with_issuer(iss.clone());
342 info!(" Issuer: {}", iss);
343 }
344 if let Some(aud) = audience {
345 authenticator = authenticator.with_audience(aud.clone());
346 info!(" Audience: {}", aud);
347 }
348
349 let server =
350 HttpServer::with_auth(processor, agent_info, bind_address, authenticator);
351 server
352 .start()
353 .await
354 .map_err(|e| RuntimeError::ServerError(e.to_string()))
355 }
356 #[cfg(not(feature = "auth"))]
357 AuthConfig::Jwt { .. } => Err(RuntimeError::ServerError(
358 "JWT authentication requires the 'auth' feature to be enabled".to_string(),
359 )),
360 #[cfg(feature = "auth")]
361 AuthConfig::OAuth2 {
362 client_id,
363 client_secret,
364 authorization_url,
365 token_url,
366 redirect_url,
367 flow,
368 scopes,
369 } => {
370 info!("🔐 Authentication: OAuth2 (flow: {})", flow);
371 info!(" Authorization URL: {}", authorization_url);
372 info!(" Token URL: {}", token_url);
373
374 let client_id = ClientId::new(client_id.clone());
375 let client_secret = ClientSecret::new(client_secret.clone());
376 let auth_url = AuthUrl::new(authorization_url.clone()).map_err(|e| {
377 RuntimeError::ServerError(format!("Invalid authorization URL: {}", e))
378 })?;
379 let token_url = TokenUrl::new(token_url.clone())
380 .map_err(|e| RuntimeError::ServerError(format!("Invalid token URL: {}", e)))?;
381
382 let scopes_map: HashMap<String, String> =
383 scopes.iter().map(|s| (s.clone(), s.clone())).collect();
384
385 let authenticator = if flow == "client_credentials" {
386 OAuth2Authenticator::new_client_credentials(
387 client_id,
388 client_secret,
389 token_url,
390 scopes_map,
391 )
392 } else {
393 let redirect_url = RedirectUrl::new(
395 redirect_url
396 .clone()
397 .unwrap_or_else(|| "http://localhost:8080/callback".to_string()),
398 )
399 .map_err(|e| {
400 RuntimeError::ServerError(format!("Invalid redirect URL: {}", e))
401 })?;
402
403 info!(" Redirect URL: {}", redirect_url.as_str());
404
405 OAuth2Authenticator::new_authorization_code(
406 client_id,
407 Some(client_secret),
408 auth_url,
409 token_url,
410 redirect_url,
411 scopes_map,
412 )
413 };
414
415 let server =
416 HttpServer::with_auth(processor, agent_info, bind_address, authenticator);
417 server
418 .start()
419 .await
420 .map_err(|e| RuntimeError::ServerError(e.to_string()))
421 }
422 #[cfg(not(feature = "auth"))]
423 AuthConfig::OAuth2 { .. } => Err(RuntimeError::ServerError(
424 "OAuth2 authentication requires the 'auth' feature to be enabled".to_string(),
425 )),
426 }
427 }
428
429 pub async fn run(self) -> Result<(), RuntimeError>
431 where
432 S: AsyncStreamingHandler,
433 {
434 if self.config.features.mcp_server.enabled {
436 return self.run_as_mcp_server().await;
437 }
438
439 if self.config.server.http_port > 0 {
441 self.start_http().await
442 } else {
443 Err(RuntimeError::ServerNotConfigured(
444 "No servers configured".to_string(),
445 ))
446 }
447 }
448
449 async fn run_as_mcp_server(self) -> Result<(), RuntimeError> {
457 use crate::core::mcp;
458 use a2a_rs::services::AgentInfoProvider;
459
460 info!("🔌 Running agent in MCP server mode");
461
462 let base_url = format!(
466 "http://{}:{}",
467 self.config.server.host, self.config.server.http_port
468 );
469
470 let agent_info = self.build_agent_info(base_url);
471 let agent_card = agent_info
472 .get_agent_card()
473 .await
474 .map_err(|e| RuntimeError::ServerError(format!("Failed to get agent card: {}", e)))?;
475
476 let handler = (*self.handler).clone();
477 mcp::run_mcp_server(&self.config.features.mcp_server, agent_card, handler)
478 .await
479 .map_err(|e| RuntimeError::ServerError(format!("MCP server error: {}", e)))
480 }
481
482 fn print_agent_info(&self, server_type: &str, port: &str) {
484 info!("📋 Agent: {}", self.config.agent.name);
485 if let Some(ref desc) = self.config.agent.description {
486 info!(" Description: {}", desc);
487 }
488 info!(" {} port: {}", server_type, port);
489
490 match &self.config.server.storage {
491 StorageConfig::InMemory => info!("💾 Storage: In-memory (non-persistent)"),
492 StorageConfig::Sqlx { url, .. } => info!("💾 Storage: SQLx ({})", url),
493 }
494
495 if !self.config.skills.is_empty() {
496 info!("🛠️ Skills: {}", self.config.skills.len());
497 for skill in &self.config.skills {
498 info!(" - {} ({})", skill.name, skill.id);
499 }
500 }
501 }
502}
503
504#[derive(Debug, thiserror::Error)]
506pub enum RuntimeError {
507 #[error("Server not configured: {0}")]
508 ServerNotConfigured(String),
509
510 #[error("Server error: {0}")]
511 ServerError(String),
512
513 #[error("Storage error: {0}")]
514 StorageError(String),
515}