Skip to main content

adk_server/rest/
mod.rs

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
99/// Build CORS layer based on security configuration
100fn 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
223/// Create the server application with optional A2A support
224pub fn create_app(config: ServerConfig) -> Router {
225    create_app_with_a2a(config, None)
226}
227
228/// Start hot reload watchers for configured YAML agent directories.
229///
230/// For each directory in `config.yaml_agent_dirs`, creates an
231/// [`AgentConfigLoader`](crate::yaml_agent::AgentConfigLoader) and
232/// [`HotReloadWatcher`](crate::yaml_agent::HotReloadWatcher), performs
233/// the initial load, and spawns a background watcher task.
234///
235/// Returns the list of active watchers so route handlers can look up
236/// YAML-defined agents.
237#[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        // Create a minimal tool registry (no pre-registered tools) and a
247        // placeholder model factory. Real deployments should configure these
248        // via ServerConfig extensions; for now we use empty defaults so the
249        // watcher can start and load YAML definitions.
250        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                // Detach the watcher task — it runs until the server shuts down.
259                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/// Empty tool registry used as default when no tools are pre-registered.
272#[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/// Placeholder model factory that returns an error for any provider.
287///
288/// Real deployments should provide a proper `ModelFactory` via
289/// `ServerConfig` extensions. This exists so the watcher can start
290/// even when no model factory is explicitly configured.
291#[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
310/// Create the server application with A2A support at the specified base URL
311pub 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    // Start YAML agent hot reload watchers if configured.
320    #[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                // Watchers are kept alive for the lifetime of this task.
327                // They run background filesystem watch loops internally.
328                // We hold them here so they aren't dropped.
329                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        // These routes mutate and read shared bridge and resource state, so they
367        // carry the same authentication as the session, artifact, and debug routers.
368        .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        // Discovery stays public — an agent card is meant to be fetched by unknown peers.
451        // The JSON-RPC routes execute agent and tool work, so they carry the same
452        // authentication as every other mutation surface. They were previously merged at the
453        // root, outside the layer applied to `/api`, so anyone who could reach the port could
454        // drive the agent and incur its costs.
455        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
503// ---------------------------------------------------------------------------
504// ServerBuilder — extensible server construction with custom routes
505// ---------------------------------------------------------------------------
506
507/// Builder for constructing an ADK server with custom routes.
508///
509/// `ServerBuilder` allows registering additional Axum routers alongside the
510/// built-in REST, A2A, and UI routes. Custom routes benefit from the same
511/// middleware stack (auth, CORS, tracing, timeout, security headers) as the
512/// built-in routes.
513///
514/// # Example
515///
516/// ```rust,ignore
517/// use adk_server::{ServerBuilder, ServerConfig};
518/// use axum::{Router, routing::get};
519///
520/// let config = ServerConfig::new(agent, session_service);
521///
522/// let app = ServerBuilder::new(config)
523///     .add_api_routes(
524///         Router::new()
525///             .route("/projects", get(list_projects))
526///             .route("/projects/{id}", get(get_project))
527///     )
528///     .add_api_routes(
529///         Router::new()
530///             .route("/automations", get(list_automations))
531///     )
532///     .with_a2a("http://localhost:8080")
533///     .build();
534///
535/// let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
536/// axum::serve(listener, app).await?;
537/// ```
538pub 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    /// Create a new server builder with the given configuration.
556    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    /// Add custom routes nested under `/api`.
575    ///
576    /// These routes are merged into the API router and benefit from the auth
577    /// middleware layer. Multiple calls accumulate routes.
578    ///
579    /// # Example
580    ///
581    /// ```rust,ignore
582    /// builder.add_api_routes(
583    ///     Router::new()
584    ///         .route("/projects", get(list_projects))
585    ///         .route("/projects/{id}", get(get_project))
586    /// )
587    /// ```
588    pub fn add_api_routes(mut self, routes: Router) -> Self {
589        self.api_routes.push(routes);
590        self
591    }
592
593    /// Add custom routes at the root level (not nested under `/api`).
594    ///
595    /// These routes are merged at the top level of the application, alongside
596    /// the UI and A2A routes. They receive the full middleware stack (CORS,
597    /// tracing, timeout, security headers) but NOT the auth middleware.
598    ///
599    /// Use this for routes that need their own auth handling or public endpoints.
600    pub fn add_root_routes(mut self, routes: Router) -> Self {
601        self.root_routes.push(routes);
602        self
603    }
604
605    /// Enable A2A protocol support at the specified base URL.
606    ///
607    /// The base URL is used to construct the agent card's endpoint URL.
608    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    /// Expose the skills in `skill_index` on the A2A agent card.
614    ///
615    /// When A2A is enabled via [`with_a2a`](Self::with_a2a), the card served at
616    /// `/.well-known/agent.json` appends one `skills[]` entry per indexed
617    /// skill, mapped by [`agent_skills_from_index`](crate::a2a::agent_skills_from_index).
618    /// Has no effect without `with_a2a`.
619    ///
620    /// # Example
621    ///
622    /// ```rust,ignore
623    /// use adk_server::{ServerBuilder, ServerConfig};
624    /// use std::sync::Arc;
625    ///
626    /// let index = adk_skill::load_skill_index(".")?;
627    /// let app = ServerBuilder::new(config)
628    ///     .with_a2a("http://localhost:8080")
629    ///     .with_skill_index(Arc::new(index))
630    ///     .build();
631    /// ```
632    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    /// Bound how many finished A2A task records the server keeps, and for how long.
638    ///
639    /// Defaults to [`A2aTaskRetention::default`]: the newest 1,000 records, each for
640    /// at most one hour. Has no effect without [`with_a2a`](Self::with_a2a).
641    ///
642    /// # Example
643    ///
644    /// ```rust,ignore
645    /// use adk_server::{A2aTaskRetention, ServerBuilder};
646    /// use std::time::Duration;
647    ///
648    /// let app = ServerBuilder::new(config)
649    ///     .with_a2a("http://localhost:8080")
650    ///     .with_a2a_task_retention(A2aTaskRetention::default().with_ttl(Duration::from_secs(300)))
651    ///     .build();
652    /// ```
653    pub fn with_a2a_task_retention(mut self, retention: A2aTaskRetention) -> Self {
654        self.a2a_task_retention = Some(retention);
655        self
656    }
657
658    /// Mount the background run endpoints under `/api`, behind the auth middleware.
659    ///
660    /// Serves `POST /api/runs`, `GET /api/runs/{run_id}`, and
661    /// `DELETE /api/runs/{run_id}` from `state`. When a `RequestContextExtractor`
662    /// is configured, every request must authenticate. Mounting
663    /// [`background_runs_router_with_state`](crate::background::background_runs_router_with_state)
664    /// yourself skips that layer.
665    ///
666    /// # Example
667    ///
668    /// ```rust,ignore
669    /// use adk_server::background::BackgroundState;
670    ///
671    /// let app = ServerBuilder::new(config)
672    ///     .with_background_runs(BackgroundState::new().with_executor(executor))
673    ///     .build();
674    /// ```
675    #[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    /// Mount the cron job endpoints under `/api`, behind the auth middleware.
682    ///
683    /// Serves `POST /api/cron`, `GET /api/cron`, and `GET`/`PATCH`/`DELETE
684    /// /api/cron/{job_id}` from `state`. The scheduler is not started here — call
685    /// [`start_cron_scheduler`](crate::background::start_cron_scheduler) with a
686    /// clone of the same state.
687    ///
688    /// # Example
689    ///
690    /// ```rust,ignore
691    /// use adk_server::background::{BackgroundState, CronState, start_cron_scheduler};
692    ///
693    /// let cron_state = CronState::new(BackgroundState::new());
694    /// start_cron_scheduler(cron_state.clone());
695    /// let app = ServerBuilder::new(config).with_cron_jobs(cron_state).build();
696    /// ```
697    #[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    /// Mount the Agent Engine dispatch endpoints alongside the built-in routes.
704    ///
705    /// When enabled, `POST /api/reasoning_engine` and
706    /// `POST /api/stream_reasoning_engine` serve the Gemini Enterprise Agent
707    /// Platform runtime contract for the loader's root agent, using the
708    /// configured session and artifact services. The routes carry the shared
709    /// middleware stack but **not** the auth middleware — a deployed engine is
710    /// fronted by the platform, which authenticates callers before they reach
711    /// the container. The memory class methods report `Unsupported`; use
712    /// [`serve_agent_engine`](crate::agent_engine::serve_agent_engine) with
713    /// [`AgentEngineOptions`](crate::agent_engine::AgentEngineOptions) to
714    /// configure a memory service.
715    ///
716    /// # Panics
717    ///
718    /// [`build`](Self::build) panics when the root agent's name is not a
719    /// valid app name, mirroring the builder's other misuse panics.
720    #[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    /// Enable the `POST /api/shutdown` endpoint for graceful shutdown.
727    ///
728    /// When enabled, the server exposes a shutdown endpoint that triggers
729    /// graceful shutdown: stops accepting new connections, completes in-flight
730    /// requests, and then exits. Use [`build_with_shutdown`](Self::build_with_shutdown)
731    /// to get the [`ShutdownHandle`] for wiring into `axum::serve().with_graceful_shutdown()`.
732    ///
733    /// The endpoint is protected by the auth middleware when a
734    /// `RequestContextExtractor` is configured.
735    pub fn enable_shutdown_endpoint(mut self) -> Self {
736        self.shutdown_endpoint = true;
737        self
738    }
739
740    /// Build the final Axum router with all routes and middleware applied.
741    pub fn build(self) -> Router {
742        self.build_inner().0
743    }
744
745    /// Build the final Axum router and return a [`ShutdownHandle`].
746    ///
747    /// Use this when [`enable_shutdown_endpoint()`](Self::enable_shutdown_endpoint) is set.
748    /// Pass the handle's signal to `axum::serve().with_graceful_shutdown()`.
749    ///
750    /// # Example
751    ///
752    /// ```rust,ignore
753    /// let (app, shutdown_handle) = ServerBuilder::new(config)
754    ///     .enable_shutdown_endpoint()
755    ///     .build_with_shutdown();
756    ///
757    /// let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
758    /// axum::serve(listener, app)
759    ///     .with_graceful_shutdown(shutdown_handle.signal())
760    ///     .await?;
761    /// ```
762    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        // Start YAML agent hot reload watchers if configured.
777        #[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            // These routes mutate and read shared bridge and resource state, so they
821            // carry the same authentication as the session, artifact, and debug routers.
822            .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        // Assemble the API router with built-in + custom routes
887        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        // Merge custom API routes — these get the same /api prefix and auth middleware
896        for custom_routes in self.api_routes {
897            api_router = api_router.merge(custom_routes.layer(auth_layer.clone()));
898        }
899
900        // Background runs and cron jobs submit and schedule work, so they carry the
901        // same authentication as every other mutation surface.
902        #[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        // Add shutdown endpoint if enabled
917        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        // Merge custom root routes
939        for custom_routes in self.root_routes {
940            app = app.merge(custom_routes);
941        }
942
943        // Agent Engine dispatch surface: root-level merge (its routes carry
944        // their own /api/... paths) so the platform host reaches it without
945        // the local auth middleware — the platform authenticates callers
946        // before they reach the container.
947        #[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            // Same split as `create_app_with_a2a`: discovery is public, RPC is authenticated.
979            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
1031/// Wait for a process shutdown signal.
1032pub 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// ---------------------------------------------------------------------------
1056// ShutdownHandle — programmatic graceful shutdown trigger
1057// ---------------------------------------------------------------------------
1058
1059/// Handle for triggering graceful server shutdown.
1060///
1061/// Returned by [`ServerBuilder::build_with_shutdown`]. Pass the future from
1062/// [`ShutdownHandle::signal()`] to `axum::serve(...).with_graceful_shutdown()`
1063/// to enable both OS signal-based and HTTP endpoint-based shutdown.
1064///
1065/// # Example
1066///
1067/// ```rust,ignore
1068/// use adk_server::{ServerBuilder, ServerConfig};
1069///
1070/// let (app, shutdown_handle) = ServerBuilder::new(config)
1071///     .enable_shutdown_endpoint()
1072///     .build_with_shutdown();
1073///
1074/// let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
1075/// axum::serve(listener, app)
1076///     .with_graceful_shutdown(shutdown_handle.signal())
1077///     .await?;
1078/// ```
1079#[derive(Clone)]
1080pub struct ShutdownHandle {
1081    token: CancellationToken,
1082}
1083
1084impl ShutdownHandle {
1085    /// Create a new shutdown handle.
1086    fn new() -> Self {
1087        Self { token: CancellationToken::new() }
1088    }
1089
1090    /// Trigger graceful shutdown programmatically.
1091    ///
1092    /// This has the same effect as calling `POST /api/shutdown` — the server
1093    /// stops accepting new connections and completes in-flight requests.
1094    pub fn shutdown(&self) {
1095        tracing::info!("graceful shutdown triggered programmatically");
1096        self.token.cancel();
1097    }
1098
1099    /// Returns a future that resolves when shutdown is triggered.
1100    ///
1101    /// Combines OS signals (Ctrl+C, SIGTERM) with the programmatic/HTTP trigger.
1102    /// Pass this to `axum::serve(...).with_graceful_shutdown()`.
1103    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                // Shutdown triggered via POST /api/shutdown or programmatic call
1131            }
1132        }
1133    }
1134
1135    /// Returns whether shutdown has been triggered.
1136    pub fn is_shutdown(&self) -> bool {
1137        self.token.is_cancelled()
1138    }
1139}
1140
1141/// Handler for `POST /api/shutdown`.
1142///
1143/// Triggers graceful shutdown: the server stops accepting new connections,
1144/// completes in-flight requests, and then exits.
1145async 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}