1pub mod controllers;
2mod routes;
3
4pub use controllers::{
5 A2aController, AppsController, ArtifactsController, DebugController, RuntimeController,
6 SessionController, a2a::A2aTaskRetention,
7};
8
9use crate::{
10 ServerConfig,
11 auth_bridge::{RequestContext, RequestContextError, RequestContextExtractor},
12 web_ui,
13};
14use axum::{
15 Json, Router,
16 body::Body,
17 extract::{DefaultBodyLimit, State},
18 http::{HeaderMap, HeaderName, HeaderValue, Method, Request, StatusCode, header},
19 middleware::{self, Next},
20 response::{IntoResponse, Response},
21 routing::{get, post},
22};
23use serde::Serialize;
24use std::sync::Arc;
25use tokio_util::sync::CancellationToken;
26use tower::ServiceBuilder;
27use tower_http::{
28 cors::{AllowOrigin, CorsLayer},
29 set_header::SetResponseHeaderLayer,
30 timeout::TimeoutLayer,
31 trace::TraceLayer,
32};
33
34const REQUEST_ID_HEADER: &str = "x-request-id";
35
36#[derive(Clone)]
37struct HealthController {
38 session_service: Arc<dyn adk_session::SessionService>,
39 artifact_service: Option<Arc<dyn adk_artifact::ArtifactService>>,
40 memory_service: Option<Arc<dyn adk_core::Memory>>,
41}
42
43impl HealthController {
44 fn new(config: &ServerConfig) -> Self {
45 Self {
46 session_service: config.session_service.clone(),
47 artifact_service: config.artifact_service.clone(),
48 memory_service: config.memory_service.clone(),
49 }
50 }
51}
52
53#[derive(Clone, Debug)]
54struct RequestId(String);
55
56impl RequestId {
57 fn as_str(&self) -> &str {
58 &self.0
59 }
60}
61
62#[derive(Serialize)]
63#[serde(rename_all = "camelCase")]
64struct HealthResponse {
65 status: &'static str,
66 components: HealthComponents,
67}
68
69#[derive(Serialize)]
70#[serde(rename_all = "camelCase")]
71struct HealthComponents {
72 session: ComponentHealth,
73 memory: ComponentHealth,
74 artifact: ComponentHealth,
75}
76
77#[derive(Serialize)]
78#[serde(rename_all = "camelCase")]
79struct ComponentHealth {
80 status: &'static str,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 error: Option<String>,
83}
84
85impl ComponentHealth {
86 fn healthy() -> Self {
87 Self { status: "healthy", error: None }
88 }
89
90 fn unhealthy(error: impl Into<String>) -> Self {
91 Self { status: "unhealthy", error: Some(error.into()) }
92 }
93
94 fn not_configured() -> Self {
95 Self { status: "not_configured", error: None }
96 }
97}
98
99fn build_cors_layer(config: &ServerConfig) -> CorsLayer {
101 let cors = CorsLayer::new()
102 .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
103 .allow_headers([
104 header::CONTENT_TYPE,
105 header::AUTHORIZATION,
106 HeaderName::from_static(REQUEST_ID_HEADER),
107 HeaderName::from_static("x-adk-ui-protocol"),
108 HeaderName::from_static("x-adk-ui-transport"),
109 ]);
110
111 if config.security.allowed_origins.is_empty() {
112 cors.allow_origin(AllowOrigin::any())
113 } else {
114 let origins: Vec<HeaderValue> = config
115 .security
116 .allowed_origins
117 .iter()
118 .filter_map(|origin| origin.parse().ok())
119 .collect();
120 cors.allow_origin(origins)
121 }
122}
123
124fn validate_request_id(headers: &HeaderMap) -> Option<String> {
125 let value = headers.get(REQUEST_ID_HEADER)?;
126 let raw = value.to_str().ok()?;
127 if raw.len() > 128 {
128 return None;
129 }
130 uuid::Uuid::parse_str(raw).ok()?;
131 Some(raw.to_string())
132}
133
134async fn request_id_middleware(mut request: Request<Body>, next: Next) -> Response {
135 let request_id =
136 validate_request_id(request.headers()).unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
137
138 request.extensions_mut().insert(RequestId(request_id.clone()));
139
140 let mut response = next.run(request).await;
141 if let Ok(value) = HeaderValue::from_str(&request_id) {
142 response.headers_mut().insert(HeaderName::from_static(REQUEST_ID_HEADER), value);
143 }
144 response
145}
146
147async fn auth_middleware(
148 request: Request<Body>,
149 next: Next,
150 extractor: Option<Arc<dyn RequestContextExtractor>>,
151) -> Response {
152 let (mut parts, body) = request.into_parts();
153
154 let request_context = match extractor {
155 Some(extractor) => match extractor.extract(&parts).await {
156 Ok(context) => Some(context),
157 Err(RequestContextError::MissingAuth) => {
158 return (
159 StatusCode::UNAUTHORIZED,
160 Json(serde_json::json!({ "error": "missing authorization" })),
161 )
162 .into_response();
163 }
164 Err(RequestContextError::InvalidToken(message)) => {
165 return (
166 StatusCode::UNAUTHORIZED,
167 Json(serde_json::json!({ "error": format!("invalid token: {message}") })),
168 )
169 .into_response();
170 }
171 Err(RequestContextError::ExtractionFailed(message)) => {
172 return (
173 StatusCode::INTERNAL_SERVER_ERROR,
174 Json(serde_json::json!({
175 "error": format!("auth extraction failed: {message}")
176 })),
177 )
178 .into_response();
179 }
180 },
181 None => None,
182 };
183
184 parts.extensions.insert::<Option<RequestContext>>(request_context);
185 next.run(Request::from_parts(parts, body)).await
186}
187
188async fn health_check(State(controller): State<HealthController>) -> impl IntoResponse {
189 let session = match controller.session_service.health_check().await {
190 Ok(()) => ComponentHealth::healthy(),
191 Err(error) => ComponentHealth::unhealthy(error.to_string()),
192 };
193
194 let memory = match controller.memory_service.as_ref() {
195 Some(service) => match service.health_check().await {
196 Ok(()) => ComponentHealth::healthy(),
197 Err(error) => ComponentHealth::unhealthy(error.to_string()),
198 },
199 None => ComponentHealth::not_configured(),
200 };
201
202 let artifact = match controller.artifact_service.as_ref() {
203 Some(service) => match service.health_check().await {
204 Ok(()) => ComponentHealth::healthy(),
205 Err(error) => ComponentHealth::unhealthy(error.to_string()),
206 },
207 None => ComponentHealth::not_configured(),
208 };
209
210 let healthy = session.status == "healthy"
211 && memory.status != "unhealthy"
212 && artifact.status != "unhealthy";
213
214 (
215 if healthy { StatusCode::OK } else { StatusCode::SERVICE_UNAVAILABLE },
216 Json(HealthResponse {
217 status: if healthy { "healthy" } else { "unhealthy" },
218 components: HealthComponents { session, memory, artifact },
219 }),
220 )
221}
222
223pub fn create_app(config: ServerConfig) -> Router {
225 create_app_with_a2a(config, None)
226}
227
228#[cfg(feature = "yaml-agent")]
238async fn start_yaml_agent_watchers(
239 dirs: &[std::path::PathBuf],
240) -> Vec<Arc<crate::yaml_agent::HotReloadWatcher>> {
241 use crate::yaml_agent::{AgentConfigLoader, HotReloadWatcher};
242
243 let mut watchers = Vec::new();
244
245 for dir in dirs {
246 let registry: Arc<dyn adk_core::ToolRegistry> = Arc::new(EmptyToolRegistry);
251 let factory: Arc<dyn crate::yaml_agent::ModelFactory> = Arc::new(NoOpModelFactory);
252 let loader = Arc::new(AgentConfigLoader::new(registry, factory));
253 let watcher = Arc::new(HotReloadWatcher::new(loader));
254
255 match watcher.watch(dir).await {
256 Ok(handle) => {
257 tracing::info!("started YAML agent hot reload watcher for {}", dir.display());
258 drop(handle);
260 watchers.push(watcher);
261 }
262 Err(e) => {
263 tracing::warn!("failed to start YAML agent watcher for {}: {e}", dir.display());
264 }
265 }
266 }
267
268 watchers
269}
270
271#[cfg(feature = "yaml-agent")]
273struct EmptyToolRegistry;
274
275#[cfg(feature = "yaml-agent")]
276impl adk_core::ToolRegistry for EmptyToolRegistry {
277 fn resolve(&self, _tool_name: &str) -> Option<Arc<dyn adk_core::Tool>> {
278 None
279 }
280
281 fn available_tools(&self) -> Vec<String> {
282 vec![]
283 }
284}
285
286#[cfg(feature = "yaml-agent")]
292struct NoOpModelFactory;
293
294#[cfg(feature = "yaml-agent")]
295#[async_trait::async_trait]
296impl crate::yaml_agent::ModelFactory for NoOpModelFactory {
297 async fn create_model(
298 &self,
299 provider: &str,
300 model_id: &str,
301 ) -> adk_core::Result<Arc<dyn adk_core::Llm>> {
302 Err(adk_core::AdkError::config(format!(
303 "no model factory configured for YAML agent loading \
304 (requested provider='{provider}', model_id='{model_id}'). \
305 Configure a ModelFactory on the server to enable YAML agent model creation."
306 )))
307 }
308}
309
310pub fn create_app_with_a2a(config: ServerConfig, a2a_base_url: Option<&str>) -> Router {
312 let session_controller = SessionController::new(config.session_service.clone());
313 let runtime_controller = RuntimeController::new(config.clone());
314 let apps_controller = AppsController::new(config.clone());
315 let artifacts_controller = ArtifactsController::new(config.clone());
316 let debug_controller = DebugController::new(config.clone());
317 let health_controller = HealthController::new(&config);
318
319 #[cfg(feature = "yaml-agent")]
321 {
322 let dirs = config.yaml_agent_dirs.clone();
323 if !dirs.is_empty() {
324 tokio::spawn(async move {
325 let _watchers = start_yaml_agent_watchers(&dirs).await;
326 std::future::pending::<()>().await;
330 });
331 }
332 }
333
334 let auth_layer = middleware::from_fn({
335 let extractor = config.request_context_extractor.clone();
336 move |request: Request<Body>, next: Next| {
337 let extractor = extractor.clone();
338 async move { auth_middleware(request, next, extractor).await }
339 }
340 });
341
342 let health_router =
343 Router::new().route("/health", get(health_check)).with_state(health_controller);
344
345 let ui_api_router = Router::new()
346 .route("/apps", get(controllers::apps::list_apps))
347 .route("/list-apps", get(controllers::apps::list_apps_compat))
348 .route("/ui/agents/{name}", get(controllers::apps::get_agent_details))
349 .with_state(apps_controller)
350 .route("/ui/capabilities", get(controllers::ui::ui_capabilities))
351 .route("/ui/initialize", post(controllers::ui::ui_initialize))
352 .route("/ui/message", post(controllers::ui::ui_message))
353 .route("/ui/update-model-context", post(controllers::ui::ui_update_model_context))
354 .route("/ui/notifications/poll", post(controllers::ui::ui_poll_notifications))
355 .route(
356 "/ui/notifications/resources-list-changed",
357 post(controllers::ui::ui_notify_resources_list_changed),
358 )
359 .route(
360 "/ui/notifications/tools-list-changed",
361 post(controllers::ui::ui_notify_tools_list_changed),
362 )
363 .route("/ui/resources", get(controllers::ui::list_ui_resources))
364 .route("/ui/resources/read", get(controllers::ui::read_ui_resource))
365 .route("/ui/resources/register", post(controllers::ui::register_ui_resource))
366 .layer(auth_layer.clone());
369
370 let session_router = Router::new()
371 .route("/sessions", post(controllers::session::create_session))
372 .route(
373 "/sessions/{app_name}/{user_id}/{session_id}",
374 get(controllers::session::get_session).delete(controllers::session::delete_session),
375 )
376 .route(
377 "/apps/{app_name}/users/{user_id}/sessions",
378 get(controllers::session::list_sessions)
379 .post(controllers::session::create_session_from_path),
380 )
381 .route(
382 "/apps/{app_name}/users/{user_id}/sessions/{session_id}",
383 get(controllers::session::get_session_from_path)
384 .post(controllers::session::create_session_from_path)
385 .delete(controllers::session::delete_session_from_path),
386 )
387 .with_state(session_controller)
388 .layer(auth_layer.clone());
389
390 let runtime_router = Router::new()
391 .route("/run", post(controllers::runtime::run_collect))
392 .route("/run/{app_name}/{user_id}/{session_id}", post(controllers::runtime::run_sse))
393 .route("/run_sse", post(controllers::runtime::run_sse_compat))
394 .with_state(runtime_controller);
395
396 let artifacts_router = Router::new()
397 .route(
398 "/sessions/{app_name}/{user_id}/{session_id}/artifacts",
399 get(controllers::artifacts::list_artifacts),
400 )
401 .route(
402 "/sessions/{app_name}/{user_id}/{session_id}/artifacts/{artifact_name}",
403 get(controllers::artifacts::get_artifact),
404 )
405 .with_state(artifacts_controller)
406 .layer(auth_layer.clone());
407
408 let mut debug_router = Router::new()
409 .route("/debug/trace/session/{session_id}", get(controllers::debug::get_session_traces))
410 .route(
411 "/debug/graph/{app_name}/{user_id}/{session_id}/{event_id}",
412 get(controllers::debug::get_graph),
413 )
414 .route(
415 "/apps/{app_name}/users/{user_id}/sessions/{session_id}/events/{event_id}/graph",
416 get(controllers::debug::get_graph),
417 )
418 .route("/apps/{app_name}/eval_sets", get(controllers::debug::get_eval_sets))
419 .route(
420 "/apps/{app_name}/users/{user_id}/sessions/{session_id}/events/{event_id}",
421 get(controllers::debug::get_event),
422 );
423
424 if config.request_context_extractor.is_none() || config.security.expose_admin_debug {
425 debug_router = debug_router
426 .route("/debug/trace/{event_id}", get(controllers::debug::get_trace_by_event_id));
427 }
428
429 let debug_router = debug_router.with_state(debug_controller.clone()).layer(auth_layer.clone());
430
431 let api_router = Router::new()
432 .merge(health_router)
433 .merge(ui_api_router)
434 .merge(session_router)
435 .merge(runtime_router)
436 .merge(artifacts_router)
437 .merge(debug_router);
438
439 let ui_router = Router::new()
440 .route("/", get(web_ui::root_redirect))
441 .route("/ui/", get(web_ui::serve_ui_index))
442 .route("/ui/assets/config/runtime-config.json", get(web_ui::serve_runtime_config))
443 .with_state(config.clone())
444 .route("/ui/{*path}", get(web_ui::serve_ui_assets));
445
446 let mut app = Router::new().nest("/api", api_router).merge(ui_router);
447
448 if let Some(base_url) = a2a_base_url {
449 let a2a_controller = A2aController::new(config.clone(), base_url);
450 let a2a_discovery = Router::new()
456 .route("/.well-known/agent.json", get(controllers::a2a::get_agent_card))
457 .with_state(a2a_controller.clone());
458 let a2a_rpc = Router::new()
459 .route("/a2a", post(controllers::a2a::handle_jsonrpc_for_caller))
460 .route("/a2a/stream", post(controllers::a2a::handle_jsonrpc_stream_for_caller))
461 .with_state(a2a_controller)
462 .layer(auth_layer.clone());
463 app = app.merge(a2a_discovery).merge(a2a_rpc);
464 }
465
466 let cors_layer = build_cors_layer(&config);
467 let trace_layer = TraceLayer::new_for_http().make_span_with(|request: &Request<Body>| {
468 let request_id =
469 request.extensions().get::<RequestId>().map(RequestId::as_str).unwrap_or("");
470 tracing::info_span!(
471 "http.request",
472 request.id = %request_id,
473 http.method = %request.method(),
474 http.path = %request.uri().path()
475 )
476 });
477
478 app.layer(
479 ServiceBuilder::new()
480 .layer(middleware::from_fn(request_id_middleware))
481 .layer(trace_layer)
482 .layer(TimeoutLayer::with_status_code(
483 StatusCode::REQUEST_TIMEOUT,
484 config.security.request_timeout,
485 ))
486 .layer(DefaultBodyLimit::max(config.security.max_body_size))
487 .layer(cors_layer)
488 .layer(SetResponseHeaderLayer::if_not_present(
489 header::X_CONTENT_TYPE_OPTIONS,
490 HeaderValue::from_static("nosniff"),
491 ))
492 .layer(SetResponseHeaderLayer::if_not_present(
493 header::X_FRAME_OPTIONS,
494 HeaderValue::from_static("DENY"),
495 ))
496 .layer(SetResponseHeaderLayer::if_not_present(
497 header::X_XSS_PROTECTION,
498 HeaderValue::from_static("1; mode=block"),
499 )),
500 )
501}
502
503pub struct ServerBuilder {
539 config: ServerConfig,
540 a2a_base_url: Option<String>,
541 api_routes: Vec<Router>,
542 root_routes: Vec<Router>,
543 shutdown_endpoint: bool,
544 skill_index: Option<Arc<adk_skill::SkillIndex>>,
545 a2a_task_retention: Option<A2aTaskRetention>,
546 #[cfg(feature = "agent-engine")]
547 agent_engine: bool,
548 #[cfg(feature = "background")]
549 background_state: Option<crate::background::BackgroundState>,
550 #[cfg(feature = "background")]
551 cron_state: Option<crate::background::CronState>,
552}
553
554impl ServerBuilder {
555 pub fn new(config: ServerConfig) -> Self {
557 Self {
558 config,
559 a2a_base_url: None,
560 api_routes: Vec::new(),
561 root_routes: Vec::new(),
562 shutdown_endpoint: false,
563 skill_index: None,
564 a2a_task_retention: None,
565 #[cfg(feature = "agent-engine")]
566 agent_engine: false,
567 #[cfg(feature = "background")]
568 background_state: None,
569 #[cfg(feature = "background")]
570 cron_state: None,
571 }
572 }
573
574 pub fn add_api_routes(mut self, routes: Router) -> Self {
589 self.api_routes.push(routes);
590 self
591 }
592
593 pub fn add_root_routes(mut self, routes: Router) -> Self {
601 self.root_routes.push(routes);
602 self
603 }
604
605 pub fn with_a2a(mut self, base_url: impl Into<String>) -> Self {
609 self.a2a_base_url = Some(base_url.into());
610 self
611 }
612
613 pub fn with_skill_index(mut self, skill_index: Arc<adk_skill::SkillIndex>) -> Self {
633 self.skill_index = Some(skill_index);
634 self
635 }
636
637 pub fn with_a2a_task_retention(mut self, retention: A2aTaskRetention) -> Self {
654 self.a2a_task_retention = Some(retention);
655 self
656 }
657
658 #[cfg(feature = "background")]
676 pub fn with_background_runs(mut self, state: crate::background::BackgroundState) -> Self {
677 self.background_state = Some(state);
678 self
679 }
680
681 #[cfg(feature = "background")]
698 pub fn with_cron_jobs(mut self, state: crate::background::CronState) -> Self {
699 self.cron_state = Some(state);
700 self
701 }
702
703 #[cfg(feature = "agent-engine")]
721 pub fn with_agent_engine(mut self, enabled: bool) -> Self {
722 self.agent_engine = enabled;
723 self
724 }
725
726 pub fn enable_shutdown_endpoint(mut self) -> Self {
736 self.shutdown_endpoint = true;
737 self
738 }
739
740 pub fn build(self) -> Router {
742 self.build_inner().0
743 }
744
745 pub fn build_with_shutdown(self) -> (Router, ShutdownHandle) {
763 let (router, handle) = self.build_inner();
764 (router, handle.expect("build_with_shutdown requires enable_shutdown_endpoint()"))
765 }
766
767 fn build_inner(self) -> (Router, Option<ShutdownHandle>) {
768 let config = &self.config;
769 let session_controller = SessionController::new(config.session_service.clone());
770 let runtime_controller = RuntimeController::new(config.clone());
771 let apps_controller = AppsController::new(config.clone());
772 let artifacts_controller = ArtifactsController::new(config.clone());
773 let debug_controller = DebugController::new(config.clone());
774 let health_controller = HealthController::new(config);
775
776 #[cfg(feature = "yaml-agent")]
778 {
779 let dirs = config.yaml_agent_dirs.clone();
780 if !dirs.is_empty() {
781 tokio::spawn(async move {
782 let _watchers = start_yaml_agent_watchers(&dirs).await;
783 std::future::pending::<()>().await;
784 });
785 }
786 }
787
788 let auth_layer = middleware::from_fn({
789 let extractor = config.request_context_extractor.clone();
790 move |request: Request<Body>, next: Next| {
791 let extractor = extractor.clone();
792 async move { auth_middleware(request, next, extractor).await }
793 }
794 });
795
796 let health_router =
797 Router::new().route("/health", get(health_check)).with_state(health_controller);
798
799 let ui_api_router = Router::new()
800 .route("/apps", get(controllers::apps::list_apps))
801 .route("/list-apps", get(controllers::apps::list_apps_compat))
802 .route("/ui/agents/{name}", get(controllers::apps::get_agent_details))
803 .with_state(apps_controller)
804 .route("/ui/capabilities", get(controllers::ui::ui_capabilities))
805 .route("/ui/initialize", post(controllers::ui::ui_initialize))
806 .route("/ui/message", post(controllers::ui::ui_message))
807 .route("/ui/update-model-context", post(controllers::ui::ui_update_model_context))
808 .route("/ui/notifications/poll", post(controllers::ui::ui_poll_notifications))
809 .route(
810 "/ui/notifications/resources-list-changed",
811 post(controllers::ui::ui_notify_resources_list_changed),
812 )
813 .route(
814 "/ui/notifications/tools-list-changed",
815 post(controllers::ui::ui_notify_tools_list_changed),
816 )
817 .route("/ui/resources", get(controllers::ui::list_ui_resources))
818 .route("/ui/resources/read", get(controllers::ui::read_ui_resource))
819 .route("/ui/resources/register", post(controllers::ui::register_ui_resource))
820 .layer(auth_layer.clone());
823
824 let session_router = Router::new()
825 .route("/sessions", post(controllers::session::create_session))
826 .route(
827 "/sessions/{app_name}/{user_id}/{session_id}",
828 get(controllers::session::get_session).delete(controllers::session::delete_session),
829 )
830 .route(
831 "/apps/{app_name}/users/{user_id}/sessions",
832 get(controllers::session::list_sessions)
833 .post(controllers::session::create_session_from_path),
834 )
835 .route(
836 "/apps/{app_name}/users/{user_id}/sessions/{session_id}",
837 get(controllers::session::get_session_from_path)
838 .post(controllers::session::create_session_from_path)
839 .delete(controllers::session::delete_session_from_path),
840 )
841 .with_state(session_controller)
842 .layer(auth_layer.clone());
843
844 let runtime_router = Router::new()
845 .route("/run", post(controllers::runtime::run_collect))
846 .route("/run/{app_name}/{user_id}/{session_id}", post(controllers::runtime::run_sse))
847 .route("/run_sse", post(controllers::runtime::run_sse_compat))
848 .with_state(runtime_controller);
849
850 let artifacts_router = Router::new()
851 .route(
852 "/sessions/{app_name}/{user_id}/{session_id}/artifacts",
853 get(controllers::artifacts::list_artifacts),
854 )
855 .route(
856 "/sessions/{app_name}/{user_id}/{session_id}/artifacts/{artifact_name}",
857 get(controllers::artifacts::get_artifact),
858 )
859 .with_state(artifacts_controller)
860 .layer(auth_layer.clone());
861
862 let mut debug_router = Router::new()
863 .route("/debug/trace/session/{session_id}", get(controllers::debug::get_session_traces))
864 .route(
865 "/debug/graph/{app_name}/{user_id}/{session_id}/{event_id}",
866 get(controllers::debug::get_graph),
867 )
868 .route(
869 "/apps/{app_name}/users/{user_id}/sessions/{session_id}/events/{event_id}/graph",
870 get(controllers::debug::get_graph),
871 )
872 .route("/apps/{app_name}/eval_sets", get(controllers::debug::get_eval_sets))
873 .route(
874 "/apps/{app_name}/users/{user_id}/sessions/{session_id}/events/{event_id}",
875 get(controllers::debug::get_event),
876 );
877
878 if config.request_context_extractor.is_none() || config.security.expose_admin_debug {
879 debug_router = debug_router
880 .route("/debug/trace/{event_id}", get(controllers::debug::get_trace_by_event_id));
881 }
882
883 let debug_router =
884 debug_router.with_state(debug_controller.clone()).layer(auth_layer.clone());
885
886 let mut api_router = Router::new()
888 .merge(health_router)
889 .merge(ui_api_router)
890 .merge(session_router)
891 .merge(runtime_router)
892 .merge(artifacts_router)
893 .merge(debug_router);
894
895 for custom_routes in self.api_routes {
897 api_router = api_router.merge(custom_routes.layer(auth_layer.clone()));
898 }
899
900 #[cfg(feature = "background")]
903 if let Some(state) = self.background_state {
904 api_router = api_router.merge(
905 crate::background::background_runs_router_with_state(state)
906 .layer(auth_layer.clone()),
907 );
908 }
909 #[cfg(feature = "background")]
910 if let Some(state) = self.cron_state {
911 api_router = api_router.merge(
912 crate::background::cron_jobs_router_with_state(state).layer(auth_layer.clone()),
913 );
914 }
915
916 let shutdown_handle = if self.shutdown_endpoint {
918 let handle = ShutdownHandle::new();
919 let shutdown_router = Router::new()
920 .route("/shutdown", post(handle_shutdown))
921 .with_state(handle.token.clone())
922 .layer(auth_layer.clone());
923 api_router = api_router.merge(shutdown_router);
924 Some(handle)
925 } else {
926 None
927 };
928
929 let ui_router = Router::new()
930 .route("/", get(web_ui::root_redirect))
931 .route("/ui/", get(web_ui::serve_ui_index))
932 .route("/ui/assets/config/runtime-config.json", get(web_ui::serve_runtime_config))
933 .with_state(config.clone())
934 .route("/ui/{*path}", get(web_ui::serve_ui_assets));
935
936 let mut app = Router::new().nest("/api", api_router).merge(ui_router);
937
938 for custom_routes in self.root_routes {
940 app = app.merge(custom_routes);
941 }
942
943 #[cfg(feature = "agent-engine")]
948 if self.agent_engine {
949 let root_agent = config.agent_loader.root_agent();
950 let mut runner_builder = adk_runner::Runner::builder()
951 .app_name(root_agent.name())
952 .agent(root_agent.clone())
953 .session_service(config.session_service.clone());
954 if let Some(artifact_service) = &config.artifact_service {
955 runner_builder = runner_builder.artifact_service(artifact_service.clone());
956 }
957 let runner = runner_builder
958 .build()
959 .expect("with_agent_engine requires the root agent's name to be a valid app name");
960 let mut state = crate::agent_engine::AgentEngineState::new(Arc::new(runner));
961 if let Some(artifact_service) = &config.artifact_service {
962 state = state.with_artifact_service(artifact_service.clone());
963 }
964 app = app.merge(crate::agent_engine::agent_engine_router(state));
965 }
966
967 if let Some(base_url) = &self.a2a_base_url {
968 let a2a_controller = match &self.skill_index {
969 Some(skill_index) => {
970 A2aController::with_skill_index(config.clone(), base_url, skill_index.clone())
971 }
972 None => A2aController::new(config.clone(), base_url),
973 };
974 let a2a_controller = match self.a2a_task_retention {
975 Some(retention) => a2a_controller.with_task_retention(retention),
976 None => a2a_controller,
977 };
978 let a2a_discovery = Router::new()
980 .route("/.well-known/agent.json", get(controllers::a2a::get_agent_card))
981 .with_state(a2a_controller.clone());
982 let a2a_rpc = Router::new()
983 .route("/a2a", post(controllers::a2a::handle_jsonrpc_for_caller))
984 .route("/a2a/stream", post(controllers::a2a::handle_jsonrpc_stream_for_caller))
985 .with_state(a2a_controller)
986 .layer(auth_layer.clone());
987 app = app.merge(a2a_discovery).merge(a2a_rpc);
988 }
989
990 let cors_layer = build_cors_layer(config);
991 let trace_layer = TraceLayer::new_for_http().make_span_with(|request: &Request<Body>| {
992 let request_id =
993 request.extensions().get::<RequestId>().map(RequestId::as_str).unwrap_or("");
994 tracing::info_span!(
995 "http.request",
996 request.id = %request_id,
997 http.method = %request.method(),
998 http.path = %request.uri().path()
999 )
1000 });
1001
1002 (
1003 app.layer(
1004 ServiceBuilder::new()
1005 .layer(middleware::from_fn(request_id_middleware))
1006 .layer(trace_layer)
1007 .layer(TimeoutLayer::with_status_code(
1008 StatusCode::REQUEST_TIMEOUT,
1009 config.security.request_timeout,
1010 ))
1011 .layer(DefaultBodyLimit::max(config.security.max_body_size))
1012 .layer(cors_layer)
1013 .layer(SetResponseHeaderLayer::if_not_present(
1014 header::X_CONTENT_TYPE_OPTIONS,
1015 HeaderValue::from_static("nosniff"),
1016 ))
1017 .layer(SetResponseHeaderLayer::if_not_present(
1018 header::X_FRAME_OPTIONS,
1019 HeaderValue::from_static("DENY"),
1020 ))
1021 .layer(SetResponseHeaderLayer::if_not_present(
1022 header::X_XSS_PROTECTION,
1023 HeaderValue::from_static("1; mode=block"),
1024 )),
1025 ),
1026 shutdown_handle,
1027 )
1028 }
1029}
1030
1031pub async fn shutdown_signal() {
1033 let ctrl_c = async {
1034 let _ = tokio::signal::ctrl_c().await;
1035 };
1036
1037 #[cfg(unix)]
1038 let terminate = async {
1039 if let Ok(mut signal) =
1040 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
1041 {
1042 let _ = signal.recv().await;
1043 }
1044 };
1045
1046 #[cfg(not(unix))]
1047 let terminate = std::future::pending::<()>();
1048
1049 tokio::select! {
1050 _ = ctrl_c => {}
1051 _ = terminate => {}
1052 }
1053}
1054
1055#[derive(Clone)]
1080pub struct ShutdownHandle {
1081 token: CancellationToken,
1082}
1083
1084impl ShutdownHandle {
1085 fn new() -> Self {
1087 Self { token: CancellationToken::new() }
1088 }
1089
1090 pub fn shutdown(&self) {
1095 tracing::info!("graceful shutdown triggered programmatically");
1096 self.token.cancel();
1097 }
1098
1099 pub async fn signal(self) {
1104 let token = self.token.clone();
1105
1106 let ctrl_c = async {
1107 let _ = tokio::signal::ctrl_c().await;
1108 };
1109
1110 #[cfg(unix)]
1111 let terminate = async {
1112 if let Ok(mut signal) =
1113 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
1114 {
1115 let _ = signal.recv().await;
1116 }
1117 };
1118
1119 #[cfg(not(unix))]
1120 let terminate = std::future::pending::<()>();
1121
1122 tokio::select! {
1123 _ = ctrl_c => {
1124 tracing::info!("received Ctrl+C, initiating graceful shutdown");
1125 }
1126 _ = terminate => {
1127 tracing::info!("received SIGTERM, initiating graceful shutdown");
1128 }
1129 _ = token.cancelled() => {
1130 }
1132 }
1133 }
1134
1135 pub fn is_shutdown(&self) -> bool {
1137 self.token.is_cancelled()
1138 }
1139}
1140
1141async fn handle_shutdown(State(token): State<CancellationToken>) -> impl IntoResponse {
1146 tracing::info!("POST /api/shutdown received, initiating graceful shutdown");
1147 token.cancel();
1148 (StatusCode::OK, Json(serde_json::json!({ "status": "shutting_down" })))
1149}