Skip to main content

a2a_agents/core/
runtime.rs

1//! Agent runtime for managing server lifecycle
2//!
3//! The runtime handles starting HTTP/WebSocket servers, wiring components,
4//! and managing the agent lifecycle based on configuration.
5
6#[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
25/// Agent runtime that manages the server lifecycle
26pub 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    /// Create a new runtime
40    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    /// Create a new runtime with MCP client
51    #[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    /// Get the MCP client manager (if enabled)
67    #[cfg(feature = "mcp-client")]
68    pub fn mcp_client(&self) -> Option<&McpClientManager> {
69        self.mcp_client.as_ref()
70    }
71
72    /// Build agent info from configuration
73    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        // Map AuthConfig into AgentCard security schemes
93        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        // Add features
175        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        // Add extensions
192        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        // Add skills
218        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    /// Start HTTP server
242    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                    // Authorization code flow
394                    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    /// Start the appropriate server(s) based on configuration
430    pub async fn run(self) -> Result<(), RuntimeError>
431    where
432        S: AsyncStreamingHandler,
433    {
434        // Check if MCP server mode is enabled
435        if self.config.features.mcp_server.enabled {
436            return self.run_as_mcp_server().await;
437        }
438
439        // Normal A2A server mode
440        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    /// Run agent as MCP server.
450    ///
451    /// The MCP bridge calls the configured [`AsyncMessageHandler`] in-process,
452    /// so no HTTP server is spawned. HTTP-only concerns like
453    /// [`AuthConfig`] are not applicable here — if you also want a secured
454    /// HTTP surface, run a normal `start_http()` instance in a separate
455    /// process or task.
456    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        // `base_url` is what gets stamped onto the agent card (and therefore
463        // into the MCP tool namespace). MCP stdio has no listener of its own,
464        // so this is purely advertising — it doesn't need to be reachable.
465        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    /// Print agent information
483    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/// Runtime errors
505#[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}