1mod agents;
7mod auth;
8mod blueprints;
9mod config;
10mod cursor;
11mod doctor;
12mod fs;
13mod interactions;
14mod mcp;
15mod polling;
16mod runs;
17mod search;
18#[cfg(test)]
19mod testutil;
20mod tls;
21mod tree;
22mod types;
23mod websocket;
24
25use types::ServeLimits;
26pub use types::{AppState, ServeArgs, ServerEvent};
27
28use std::net::SocketAddr;
29use std::sync::Arc;
30
31use axum::Router;
32use axum::routing::{delete, get, post, put};
33use tokio::sync::broadcast;
34use tower_http::cors::{Any, CorsLayer};
35
36use crate::config::Config;
37
38struct AbortOnDrop<T>(tokio::task::JoinHandle<T>);
44
45impl<T> Drop for AbortOnDrop<T> {
46 fn drop(&mut self) {
47 self.0.abort();
48 }
49}
50
51pub async fn execute(
53 args: ServeArgs,
54 control: leviath_runtime::control_socket::ControlClient,
55) -> anyhow::Result<()> {
56 execute_with_shutdown(args, control, Box::pin(std::future::pending()), None).await
57}
58
59fn api_router() -> Router<AppState> {
65 Router::new()
66 .route(
68 "/api/blueprints",
69 get(blueprints::list_blueprints).post(blueprints::create_blueprint),
70 )
71 .route(
72 "/api/blueprints/validate",
73 post(blueprints::validate_blueprint),
74 )
75 .route(
76 "/api/blueprints/{name}",
77 get(blueprints::get_blueprint)
78 .put(blueprints::update_blueprint)
79 .delete(blueprints::delete_blueprint),
80 )
81 .route("/api/runs", get(runs::list_runs))
84 .route(
86 "/api/agents",
87 get(agents::list_agents).post(agents::spawn_agent),
88 )
89 .route("/api/agents/tree", get(tree::agents_tree))
90 .route(
91 "/api/agents/{id}",
92 get(agents::get_agent).delete(agents::kill_agent),
93 )
94 .route("/api/agents/{id}/children", get(agents::agent_children))
95 .route("/api/agents/{id}/context", get(agents::agent_context))
96 .route(
97 "/api/agents/{id}/context/history",
98 get(agents::agent_context_history),
99 )
100 .route("/api/agents/{id}/files", get(agents::agent_file))
101 .route("/api/agents/{id}/logs", get(agents::agent_logs))
102 .route("/api/agents/{id}/result", get(agents::agent_result))
103 .route("/api/agents/{id}/stages", get(agents::agent_stages))
104 .route("/api/agents/{id}/tree-status", get(tree::agent_tree_status))
105 .route("/api/agents/{id}/pause", post(agents::pause_agent))
106 .route("/api/agents/{id}/resume", post(agents::resume_agent))
107 .route("/api/agents/{id}/message", post(interactions::send_message))
109 .route(
111 "/api/agents/{id}/interaction",
112 get(interactions::get_interaction).post(interactions::submit_interaction),
113 )
114 .route("/api/mcp/servers", get(mcp::list_servers))
117 .route("/api/mcp/servers/{name}/status", get(mcp::status))
118 .route("/api/mcp/servers/{name}/login", post(mcp::login))
119 .route("/api/mcp/servers/{name}/test", post(mcp::test_server))
120 .route("/api/doctor", get(doctor::run_doctor))
122 .route("/api/fs/dirs", get(fs::list_dirs))
124 .route("/api/config", get(config::get_config))
126 .route("/api/config/validate", post(config::validate_config_key))
127 .route("/api/models", get(config::get_models))
128 .route("/ws", get(websocket::ws_global))
130 .route("/ws/agents/{id}", get(websocket::ws_agent))
131}
132
133#[cfg(test)]
141fn declared_routes() -> Vec<(String, String)> {
142 const SOURCE: &str = include_str!("mod.rs");
143 let production = SOURCE.split("\nmod tests {").next().unwrap_or(SOURCE);
147 routes_in(production)
148}
149
150#[cfg(test)]
155fn routes_in(source: &str) -> Vec<(String, String)> {
156 let mut routes = Vec::new();
157 for chunk in source.split(".route(").skip(1) {
160 let mut depth = 1usize;
164 let mut body = String::new();
165 for ch in chunk.chars() {
166 match ch {
167 '(' => depth += 1,
168 ')' => {
169 depth -= 1;
170 if depth == 0 {
171 break;
172 }
173 }
174 _ => {}
175 }
176 body.push(ch);
177 }
178 let Some(path) = body
179 .split_once('"')
180 .and_then(|(_, rest)| rest.split_once('"'))
181 .map(|(path, _)| path)
182 else {
183 continue;
184 };
185 if !path.starts_with('/') {
189 continue;
190 }
191 for method in ["get", "post", "put", "delete", "patch"] {
192 if body.contains(&format!("{method}(")) {
193 routes.push((path.to_string(), method.to_uppercase()));
194 }
195 }
196 }
197 routes
198}
199
200async fn execute_with_shutdown(
221 args: ServeArgs,
222 control: leviath_runtime::control_socket::ControlClient,
223 shutdown: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
224 ready: Option<tokio::sync::oneshot::Sender<SocketAddr>>,
225) -> anyhow::Result<()> {
226 let auth_token = std::sync::Arc::new(auth::resolve_token(args.token.as_deref())?);
228 if args.host != "127.0.0.1" && args.host != "localhost" && args.host != "::1" {
230 tracing::warn!(
231 host = %args.host,
232 "serving the agent API on a non-local address - anyone who can reach \
233 this host and holds the token can spawn agents"
234 );
235 }
236
237 let cfg = Config::load()?;
238 let allow_local_network = cfg.security.allow_local_network;
240 for warning in cfg.validate_keys() {
241 tracing::warn!("{}", warning);
242 }
243
244 let (event_tx, _) = broadcast::channel::<ServerEvent>(256);
247
248 let state = AppState {
249 config: Arc::new(cfg),
250 event_tx: event_tx.clone(),
251 control,
252 mcp: mcp::McpAdmin::default(),
253 limits: Arc::new(ServeLimits {
254 workdir_root: args.workdir_root.clone(),
255 no_remote_yolo: args.no_remote_yolo,
256 allow_local_network,
257 }),
258 };
259
260 let event_state = state.clone();
269 let _event_guard = AbortOnDrop(tokio::spawn(polling::event_loop(
270 event_state,
271 polling::RECONNECT_BACKOFF,
272 )));
273
274 let cors = match args.cors.as_deref() {
278 None => None,
279 Some("*") => Some(
280 CorsLayer::new()
281 .allow_origin(Any)
282 .allow_methods(Any)
283 .allow_headers([
288 axum::http::header::AUTHORIZATION,
289 axum::http::header::CONTENT_TYPE,
290 ]),
291 ),
292 Some(origin) => {
293 let value = origin.parse::<axum::http::HeaderValue>().map_err(|_| {
297 anyhow::anyhow!("--cors value '{origin}' is not a valid origin header")
298 })?;
299 Some(
300 CorsLayer::new()
301 .allow_origin(value)
302 .allow_methods(Any)
303 .allow_headers([
308 axum::http::header::AUTHORIZATION,
309 axum::http::header::CONTENT_TYPE,
310 ]),
311 )
312 }
313 };
314
315 let app = api_router();
316
317 let app = match args.allow_admin {
325 true => app
326 .route("/api/mcp/servers", post(mcp::add_server))
327 .route("/api/mcp/servers/{name}", delete(mcp::remove_server))
328 .route("/api/config", put(config::put_config)),
331 false => app,
332 };
333
334 let app = app
335 .layer(axum::middleware::from_fn_with_state(
338 auth_token,
339 auth::require_auth,
340 ))
341 .with_state(state);
342
343 let app = app.merge(Router::new().route("/", get(status_page)));
353 let app = match cors {
357 Some(layer) => app.layer(layer),
358 None => app,
359 };
360
361 let tls = tls::resolve(args.tls_cert.clone(), args.tls_key.clone())?;
365 let tls_config = match &tls {
366 Some(paths) => Some(tls::load(paths).await?),
367 None => None,
368 };
369
370 let addr: SocketAddr = format!("{}:{}", args.host, args.port).parse()?;
371 let scheme = tls::scheme(tls.as_ref());
372 tracing::info!("Listening on {}://{}", scheme, addr);
373 println!("Leviath API server listening on {scheme}://{addr}");
374
375 let listener = tokio::net::TcpListener::bind(addr).await?;
376 if let Some(ready) = ready {
377 let local_addr = listener
380 .local_addr()
381 .expect("infallible: a freshly bound TcpListener always has a local address");
382 let _ = ready.send(local_addr);
383 }
384
385 match tls_config {
386 None => {
389 let _ = axum::serve(listener, app)
390 .with_graceful_shutdown(shutdown)
391 .await;
392 }
393 Some(config) => serve_tls(listener, app, config, shutdown).await,
394 }
395
396 Ok(())
397}
398
399async fn serve_tls(
410 listener: tokio::net::TcpListener,
411 app: Router,
412 config: axum_server::tls_rustls::RustlsConfig,
413 shutdown: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
414) {
415 let handle = axum_server::Handle::new();
416 let signal = handle.clone();
417 tokio::spawn(async move {
418 shutdown.await;
419 signal.graceful_shutdown(Some(std::time::Duration::from_secs(5)));
423 });
424 let std_listener = listener
430 .into_std()
431 .expect("infallible: a bound tokio listener always converts back");
432 let server = axum_server::from_tcp_rustls(std_listener, config)
433 .expect("infallible: the listener is bound and non-blocking, which is all this checks");
434 let _ = server.handle(handle).serve(app.into_make_service()).await;
438}
439
440async fn status_page() -> axum::response::Html<&'static str> {
448 axum::response::Html(
449 "<!doctype html><meta charset=utf-8><title>Leviath</title>\
450 <body style=\"font:16px system-ui;margin:4rem auto;max-width:30rem\">\
451 <h1>Leviath is running.</h1>\
452 <p>The API needs a token; this page does not serve it.</p>",
453 )
454}
455
456#[cfg(test)]
459mod tests {
460 use super::*;
461 use axum::body::Body;
462 use axum::http::{Request, StatusCode};
463 use tower::ServiceExt;
464
465 use crate::runstate::RunMeta;
466 use crate::test_support::with_tracing;
467
468 const OPENAPI: &str = include_str!("../../../../../docs/schema/openapi.json");
470
471 #[test]
479 fn the_api_version_matches_the_spec_it_names() {
480 let spec: serde_json::Value = serde_json::from_str(OPENAPI).expect("the spec is JSON");
481 let documented = spec["info"]["version"]
482 .as_str()
483 .expect("the spec declares a version");
484 assert_eq!(documented, types::API_VERSION);
485 }
486
487 fn documented_routes() -> Vec<(String, String)> {
489 let spec: serde_json::Value = serde_json::from_str(OPENAPI).expect("the spec is JSON");
490 let paths = spec["paths"].as_object().expect("the spec has paths");
491 let mut routes = Vec::new();
492 for (path, item) in paths {
493 let operations = item.as_object().expect("a path item is an object");
494 for method in ["get", "post", "put", "delete", "patch"] {
495 if operations.contains_key(method) {
496 routes.push((path.clone(), method.to_uppercase()));
497 }
498 }
499 }
500 routes
501 }
502
503 type Routes = Vec<(String, String)>;
505
506 fn spec_drift() -> (Routes, Routes) {
509 let declared = declared_routes();
510 let documented = documented_routes();
511 let missing = declared
512 .iter()
513 .filter(|r| !documented.contains(r))
514 .cloned()
515 .collect();
516 let extra = documented
517 .iter()
518 .filter(|r| !declared.contains(r))
519 .cloned()
520 .collect();
521 (missing, extra)
522 }
523
524 #[test]
525 fn the_openapi_spec_documents_exactly_the_routes_this_router_serves() {
526 let (missing, extra) = spec_drift();
537 assert!(missing.is_empty());
538 assert!(extra.is_empty());
539 }
540
541 #[test]
542 fn the_route_reader_finds_the_routes_that_are_actually_there() {
543 let declared = declared_routes();
547 assert!(declared.len() > 25);
548 assert!(declared.contains(&("/api/agents".to_string(), "POST".to_string())));
549 assert!(declared.contains(&("/api/agents/{id}".to_string(), "DELETE".to_string())));
550 assert!(declared.contains(&("/ws".to_string(), "GET".to_string())));
551 }
552
553 #[test]
554 fn the_route_reader_ignores_text_that_is_not_a_route() {
555 let source = concat!(
559 "let x = source.split(\".route(\").skip(1);\n",
560 ".route(\"not a path\", get(h))\n",
561 ".route(\"/real\", get(h).post(h))\n"
562 );
563 assert_eq!(
564 routes_in(source),
565 vec![
566 ("/real".to_string(), "GET".to_string()),
567 ("/real".to_string(), "POST".to_string()),
568 ]
569 );
570 }
571
572 #[test]
573 fn the_route_reader_reads_nothing_out_of_source_with_no_routes() {
574 assert_eq!(routes_in("fn main() {}"), Vec::new());
575 }
576
577 fn assert_execute_failed_on_malformed_config(result: &anyhow::Result<()>) {
582 assert!(
583 result.is_err(),
584 "execute should fail when config is malformed"
585 );
586 }
587
588 #[test]
589 #[should_panic(expected = "execute should fail when config is malformed")]
590 fn assert_execute_failed_on_malformed_config_panics_when_ok() {
591 assert_execute_failed_on_malformed_config(&Ok(()));
592 }
593
594 fn assert_connected_with_bad_api_key(connected: bool) {
597 assert!(connected, "server should start even with a bad API key");
598 }
599
600 #[test]
601 #[should_panic(expected = "server should start even with a bad API key")]
602 fn assert_connected_with_bad_api_key_panics_when_not_connected() {
603 assert_connected_with_bad_api_key(false);
604 }
605
606 fn assert_execute_returned_ok_after_shutdown(result: &Result<(), anyhow::Error>) {
609 assert!(
610 result.is_ok(),
611 "execute should return Ok after graceful shutdown"
612 );
613 }
614
615 #[test]
616 #[should_panic(expected = "execute should return Ok after graceful shutdown")]
617 fn assert_execute_returned_ok_after_shutdown_panics_when_err() {
618 assert_execute_returned_ok_after_shutdown(&Err(anyhow::anyhow!("boom")));
619 }
620
621 fn assert_execute_failed_on_port_in_use(result: &anyhow::Result<()>) {
624 assert!(
625 result.is_err(),
626 "execute should fail when port is already in use"
627 );
628 }
629
630 #[test]
631 #[should_panic(expected = "execute should fail when port is already in use")]
632 fn assert_execute_failed_on_port_in_use_panics_when_ok() {
633 assert_execute_failed_on_port_in_use(&Ok(()));
634 }
635
636 fn assert_execute_with_shutdown_returned_ok(result: &Result<(), anyhow::Error>) {
640 assert!(
641 result.is_ok(),
642 "execute_with_shutdown should return Ok(()) after graceful shutdown"
643 );
644 }
645
646 #[test]
647 #[should_panic(expected = "execute_with_shutdown should return Ok(()) after graceful shutdown")]
648 fn assert_execute_with_shutdown_returned_ok_panics_when_err() {
649 assert_execute_with_shutdown_returned_ok(&Err(anyhow::anyhow!("boom")));
650 }
651
652 fn assert_response_ok(resp_str: &str) {
655 assert!(resp_str.starts_with("HTTP/1.1 200"), "got: {resp_str}");
656 }
657
658 #[test]
659 #[should_panic(expected = "got: HTTP/1.1 404 Not Found")]
660 fn assert_response_ok_panics_when_not_200() {
661 assert_response_ok("HTTP/1.1 404 Not Found\r\n\r\n");
662 }
663
664 fn no_daemon_control() -> leviath_runtime::control_socket::ControlClient {
667 leviath_runtime::control_socket::ControlClient::new(
668 leviath_runtime::control_socket::control_id(std::path::Path::new("/no/such/leviath")),
669 )
670 }
671
672 fn test_state() -> AppState {
673 let (tx, _) = broadcast::channel(64);
674 AppState {
675 config: Arc::new(Config::default()),
676 event_tx: tx,
677 control: no_daemon_control(),
678 mcp: crate::commands::serve::mcp::McpAdmin::default(),
679 limits: Default::default(),
680 }
681 }
682
683 fn test_app() -> Router {
686 api_router().with_state(test_state())
687 }
688
689 #[tokio::test]
690 async fn test_list_blueprints() {
691 let app = test_app();
692 let req = Request::builder()
693 .uri("/api/blueprints")
694 .body(Body::empty())
695 .unwrap();
696 let resp = app.oneshot(req).await.unwrap();
697 assert_eq!(resp.status(), StatusCode::OK);
698 }
699
700 #[tokio::test]
701 async fn test_router_serves_routes_the_old_hand_copy_missed() {
702 let app = test_app();
706 let req = Request::builder()
707 .uri("/api/mcp/servers")
708 .body(Body::empty())
709 .unwrap();
710 let resp = app.oneshot(req).await.unwrap();
711 assert_eq!(resp.status(), StatusCode::OK);
712 }
713
714 #[tokio::test]
715 async fn test_pause_and_resume_routes_are_mounted() {
716 for action in ["pause", "resume"] {
720 let app = test_app();
721 let req = Request::builder()
722 .method("POST")
723 .uri(format!("/api/agents/some-run/{action}"))
724 .body(Body::empty())
725 .unwrap();
726 let resp = app.oneshot(req).await.unwrap();
727 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
728 }
729 }
730
731 #[tokio::test]
732 async fn test_agent_files_route_is_mounted() {
733 let app = test_app();
743 let req = Request::builder()
744 .uri("/api/agents/some-run/files")
745 .body(Body::empty())
746 .unwrap();
747 let resp = app.oneshot(req).await.unwrap();
748 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
749 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
750 .await
751 .unwrap();
752 let error = serde_json::from_slice::<serde_json::Value>(&body)
753 .ok()
754 .and_then(|v| v["error"].as_str().map(str::to_string))
755 .unwrap_or_default();
756 assert!(error.contains("some-run"));
757 }
758
759 #[tokio::test]
760 async fn test_fs_dirs_route_is_mounted() {
761 let app = test_app();
766 let req = Request::builder()
767 .uri("/api/fs/dirs?path=not/absolute")
768 .body(Body::empty())
769 .unwrap();
770 let resp = app.oneshot(req).await.unwrap();
771 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
772 }
773
774 #[tokio::test]
775 async fn test_get_blueprint_not_found() {
776 let app = test_app();
777 let req = Request::builder()
778 .uri("/api/blueprints/nonexistent-agent-xyz")
779 .body(Body::empty())
780 .unwrap();
781 let resp = app.oneshot(req).await.unwrap();
782 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
783 }
784
785 #[tokio::test]
786 async fn test_validate_blueprint_valid() {
787 let app = test_app();
788 let manifest = r#"
789[agent]
790name = "test-agent"
791version = "0.1.0"
792description = "A test"
793
794[stages.main]
795mode = "autonomous"
796[stages.main.model]
797provider = "anthropic"
798model = "claude-sonnet-4-6"
799"#;
800 let body = serde_json::json!({ "manifest": manifest });
801 let req = Request::builder()
802 .method("POST")
803 .uri("/api/blueprints/validate")
804 .header("content-type", "application/json")
805 .body(Body::from(serde_json::to_string(&body).unwrap()))
806 .unwrap();
807 let resp = app.oneshot(req).await.unwrap();
808 assert_eq!(resp.status(), StatusCode::OK);
809
810 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
811 .await
812 .unwrap();
813 let val: types::ValidateResponse = serde_json::from_slice(&body).unwrap();
814 assert!(val.valid);
815 }
816
817 #[tokio::test]
818 async fn test_validate_blueprint_invalid() {
819 let app = test_app();
820 let body = serde_json::json!({ "manifest": "not valid toml {{{{" });
821 let req = Request::builder()
822 .method("POST")
823 .uri("/api/blueprints/validate")
824 .header("content-type", "application/json")
825 .body(Body::from(serde_json::to_string(&body).unwrap()))
826 .unwrap();
827 let resp = app.oneshot(req).await.unwrap();
828 assert_eq!(resp.status(), StatusCode::OK);
829
830 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
831 .await
832 .unwrap();
833 let val: types::ValidateResponse = serde_json::from_slice(&body).unwrap();
834 assert!(!val.valid);
835 assert!(val.errors.is_some());
836 }
837
838 #[tokio::test]
839 async fn test_list_agents() {
840 let app = test_app();
841 let req = Request::builder()
842 .uri("/api/agents")
843 .body(Body::empty())
844 .unwrap();
845 let resp = app.oneshot(req).await.unwrap();
846 assert_eq!(resp.status(), StatusCode::OK);
847 }
848
849 #[tokio::test]
850 async fn test_agents_tree() {
851 let app = test_app();
852 let req = Request::builder()
853 .uri("/api/agents/tree")
854 .body(Body::empty())
855 .unwrap();
856 let resp = app.oneshot(req).await.unwrap();
857 assert_eq!(resp.status(), StatusCode::OK);
858 }
859
860 #[tokio::test]
861 async fn test_get_agent_not_found() {
862 let app = test_app();
863 let req = Request::builder()
864 .uri("/api/agents/nonexistent-run-id-xyz")
865 .body(Body::empty())
866 .unwrap();
867 let resp = app.oneshot(req).await.unwrap();
868 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
869 }
870
871 #[tokio::test]
872 async fn test_agent_children_empty() {
873 let app = test_app();
874 let req = Request::builder()
875 .uri("/api/agents/nonexistent/children")
876 .body(Body::empty())
877 .unwrap();
878 let resp = app.oneshot(req).await.unwrap();
879 assert_eq!(resp.status(), StatusCode::OK);
881 }
882
883 #[tokio::test]
884 async fn test_agent_context_not_found() {
885 let app = test_app();
886 let req = Request::builder()
887 .uri("/api/agents/nonexistent/context")
888 .body(Body::empty())
889 .unwrap();
890 let resp = app.oneshot(req).await.unwrap();
891 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
892 }
893
894 #[tokio::test]
895 async fn test_agent_logs_not_found() {
896 let app = test_app();
897 let req = Request::builder()
898 .uri("/api/agents/nonexistent/logs")
899 .body(Body::empty())
900 .unwrap();
901 let resp = app.oneshot(req).await.unwrap();
902 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
903 }
904
905 #[tokio::test]
906 async fn test_agent_result_not_found() {
907 let app = test_app();
908 let req = Request::builder()
909 .uri("/api/agents/nonexistent/result")
910 .body(Body::empty())
911 .unwrap();
912 let resp = app.oneshot(req).await.unwrap();
913 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
914 }
915
916 #[tokio::test]
917 async fn test_agent_tree_status_not_found() {
918 let app = test_app();
919 let req = Request::builder()
920 .uri("/api/agents/nonexistent/tree-status")
921 .body(Body::empty())
922 .unwrap();
923 let resp = app.oneshot(req).await.unwrap();
924 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
925 }
926
927 #[tokio::test]
928 async fn test_interaction_route_reaches_daemon() {
929 let app = test_app();
932 let req = Request::builder()
933 .uri("/api/agents/nonexistent/interaction")
934 .body(Body::empty())
935 .unwrap();
936 let resp = app.oneshot(req).await.unwrap();
937 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
938 }
939
940 #[tokio::test]
941 async fn test_get_config() {
942 let app = test_app();
943 let req = Request::builder()
944 .uri("/api/config")
945 .body(Body::empty())
946 .unwrap();
947 let resp = app.oneshot(req).await.unwrap();
948 assert_eq!(resp.status(), StatusCode::OK);
949
950 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
951 .await
952 .unwrap();
953 let val: types::RedactedConfig = serde_json::from_slice(&body).unwrap();
954 assert_eq!(val.default_provider, "anthropic");
955 assert!(!val.has_anthropic_key);
957 assert!(!val.has_openai_key);
958 }
959
960 #[tokio::test]
961 async fn test_tree_building() {
962 let runs = vec![
964 RunMeta::new(
965 "parent-1".to_string(),
966 "agent-a".to_string(),
967 "/path".to_string(),
968 "task".to_string(),
969 None,
970 "/work".to_string(),
971 1,
972 ),
973 {
974 let mut child = RunMeta::new(
975 "child-1".to_string(),
976 "agent-b".to_string(),
977 "/path".to_string(),
978 "sub-task".to_string(),
979 None,
980 "/work".to_string(),
981 1,
982 );
983 child.parent_run_id = Some("parent-1".to_string());
984 child.prompt_tokens = 100;
985 child.completion_tokens = 50;
986 child
987 },
988 ];
989
990 let tree = tree::build_tree_status(&runs, None);
991 assert_eq!(tree.len(), 1);
992 assert_eq!(tree[0].run_id, "parent-1");
993 assert_eq!(tree[0].children.len(), 1);
994 assert_eq!(tree[0].subtree_prompt_tokens, 100); assert_eq!(tree[0].subtree_completion_tokens, 50);
996 }
997
998 #[tokio::test]
999 async fn test_delete_blueprint_not_found() {
1000 let app = test_app();
1001 let req = Request::builder()
1002 .method("DELETE")
1003 .uri("/api/blueprints/nonexistent-agent-xyz")
1004 .body(Body::empty())
1005 .unwrap();
1006 let resp = app.oneshot(req).await.unwrap();
1007 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1008 }
1009
1010 #[tokio::test]
1011 async fn test_server_event_serialization() {
1012 let event = ServerEvent::AgentStatus {
1013 agent_id: "coder".to_string(),
1014 run_id: "run-123".to_string(),
1015 status: "running".to_string(),
1016 stage: "implement".to_string(),
1017 iteration: 5,
1018 tool_calls: 0,
1019 accepts_messages: true,
1020 };
1021 let json = serde_json::to_string(&event).unwrap();
1022 assert!(json.contains("\"type\":\"agent_status\""));
1023 assert!(json.contains("\"agent_id\":\"coder\""));
1024
1025 let event2 = ServerEvent::Tokens {
1026 agent_id: "coder".to_string(),
1027 run_id: "run-123".to_string(),
1028 prompt_tokens: 5000,
1029 completion_tokens: 1200,
1030 cached_tokens: 0,
1031 cache_write_tokens: 0,
1032 };
1033 let json2 = serde_json::to_string(&event2).unwrap();
1034 assert!(json2.contains("\"type\":\"tokens\""));
1035 assert!(json2.contains("\"prompt_tokens\":5000"));
1036 }
1037
1038 #[tokio::test]
1039 async fn test_full_router_create_blueprint_invalid() {
1040 let app = test_app();
1041 let body = serde_json::json!({
1042 "name": "bad-agent",
1043 "manifest": "not valid toml {{{"
1044 });
1045 let req = Request::builder()
1046 .method("POST")
1047 .uri("/api/blueprints")
1048 .header("content-type", "application/json")
1049 .body(Body::from(serde_json::to_string(&body).unwrap()))
1050 .unwrap();
1051 let resp = app.oneshot(req).await.unwrap();
1052 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1053 }
1054
1055 #[tokio::test]
1056 async fn test_full_router_update_blueprint_not_found() {
1057 let app = test_app();
1058 let body = serde_json::json!({
1059 "manifest": r#"
1060[agent]
1061name = "no-such-agent"
1062version = "1.0.0"
1063description = "Missing"
1064
1065[stages.run]
1066system_prompt = "Run"
1067"#
1068 });
1069 let req = Request::builder()
1070 .method("PUT")
1071 .uri("/api/blueprints/no-such-agent-xyz-99999")
1072 .header("content-type", "application/json")
1073 .body(Body::from(serde_json::to_string(&body).unwrap()))
1074 .unwrap();
1075 let resp = app.oneshot(req).await.unwrap();
1076 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1077 }
1078
1079 #[tokio::test]
1080 async fn test_full_router_kill_agent_reaches_daemon() {
1081 let app = test_app();
1082 let req = Request::builder()
1083 .method("DELETE")
1084 .uri("/api/agents/nonexistent-kill-id-xyz")
1085 .body(Body::empty())
1086 .unwrap();
1087 let resp = app.oneshot(req).await.unwrap();
1088 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
1089 }
1090
1091 #[tokio::test]
1092 async fn test_full_router_send_message_reaches_daemon() {
1093 let app = test_app();
1094 let body = serde_json::json!({"message": "hello"});
1095 let req = Request::builder()
1096 .method("POST")
1097 .uri("/api/agents/nonexistent-msg-id-xyz/message")
1098 .header("content-type", "application/json")
1099 .body(Body::from(serde_json::to_string(&body).unwrap()))
1100 .unwrap();
1101 let resp = app.oneshot(req).await.unwrap();
1102 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
1103 }
1104
1105 #[tokio::test]
1106 async fn test_full_router_get_models() {
1107 let app = test_app();
1108 let req = Request::builder()
1109 .uri("/api/models")
1110 .body(Body::empty())
1111 .unwrap();
1112 let resp = app.oneshot(req).await.unwrap();
1113 assert_eq!(resp.status(), StatusCode::OK);
1114 }
1115
1116 #[tokio::test]
1117 async fn test_full_router_spawn_agent_blueprint_not_found() {
1118 let app = test_app();
1119 let body = serde_json::json!({
1120 "blueprint": "nonexistent-blueprint-xyz",
1121 "task": "do something"
1122 });
1123 let req = Request::builder()
1124 .method("POST")
1125 .uri("/api/agents")
1126 .header("content-type", "application/json")
1127 .body(Body::from(serde_json::to_string(&body).unwrap()))
1128 .unwrap();
1129 let resp = app.oneshot(req).await.unwrap();
1130 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1131 }
1132
1133 #[test]
1134 fn test_serve_args_defaults() {
1135 let args = ServeArgs {
1136 port: 3000,
1137 host: "127.0.0.1".to_string(),
1138 cors: None,
1139 token: Some("test-token".to_string()),
1140 allow_admin: false,
1141 workdir_root: None,
1142 no_remote_yolo: false,
1143 tls_cert: None,
1144 tls_key: None,
1145 };
1146 assert_eq!(args.port, 3000);
1147 assert_eq!(args.host, "127.0.0.1");
1148 assert_eq!(args.cors, None);
1149 }
1150
1151 #[test]
1152 fn test_app_state_clone() {
1153 let state = test_state();
1154 let cloned = state.clone();
1155 let _ = cloned.config.default_provider.clone();
1157 }
1158
1159 #[test]
1160 fn test_cors_wildcard_vs_specific() {
1161 let wildcard = "*";
1163 let specific = "https://example.com";
1164
1165 let is_wildcard = wildcard == "*";
1166 assert!(is_wildcard);
1167
1168 let is_specific = specific != "*";
1169 assert!(is_specific);
1170
1171 let parsed = specific.parse::<axum::http::HeaderValue>();
1173 assert!(parsed.is_ok());
1174 }
1175
1176 #[test]
1177 fn test_cors_invalid_origin_falls_back() {
1178 let invalid_cors = "not a valid header value \x00";
1179 let result = invalid_cors.parse::<axum::http::HeaderValue>();
1180 assert!(result.is_err());
1182 }
1183
1184 #[tokio::test]
1185 async fn test_submit_interaction_full_router_reaches_daemon() {
1186 let app = test_app();
1190 let body = serde_json::json!({"request_id": "req-1", "value": "do it", "scope": "once"});
1191 let req = Request::builder()
1192 .method("POST")
1193 .uri("/api/agents/any/interaction")
1194 .header("content-type", "application/json")
1195 .body(Body::from(serde_json::to_string(&body).unwrap()))
1196 .unwrap();
1197 let resp = app.oneshot(req).await.unwrap();
1198 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
1199 }
1200
1201 #[tokio::test]
1221 async fn execute_binds_and_serves_with_wildcard_cors() {
1222 crate::config::with_isolated_config_path_async(
1223 "serve-mod-wildcard-cors",
1224 |_fake_dir| async move {
1225 with_tracing(|| {});
1226 let args = ServeArgs {
1236 port: 0,
1237 host: "127.0.0.1".to_string(),
1238 cors: None,
1239 token: Some("test-token".to_string()),
1240 allow_admin: false,
1241 workdir_root: None,
1242 no_remote_yolo: false,
1243 tls_cert: None,
1244 tls_key: None,
1245 };
1246 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1247 let handle = tokio::spawn(execute_with_shutdown(
1248 args,
1249 no_daemon_control(),
1250 Box::pin(std::future::pending()),
1251 Some(ready_tx),
1252 ));
1253 let addr = ready_rx
1254 .await
1255 .expect("server should report its bound address");
1256
1257 let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
1259 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1260 stream
1261 .write_all(
1262 b"GET /api/config HTTP/1.1\r\nHost: localhost\r\n\
1263 Authorization: Bearer test-token\r\nConnection: close\r\n\r\n",
1264 )
1265 .await
1266 .unwrap();
1267 let mut resp = Vec::new();
1268 stream.read_to_end(&mut resp).await.unwrap();
1269 let resp_str = String::from_utf8_lossy(&resp);
1270 assert_response_ok(&resp_str);
1271
1272 let mut unauth = tokio::net::TcpStream::connect(addr).await.unwrap();
1274 unauth
1275 .write_all(
1276 b"GET /api/config HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
1277 )
1278 .await
1279 .unwrap();
1280 let mut resp2 = Vec::new();
1281 unauth.read_to_end(&mut resp2).await.unwrap();
1282 assert!(
1283 String::from_utf8_lossy(&resp2).starts_with("HTTP/1.1 401"),
1284 "unauthenticated request should be 401"
1285 );
1286
1287 handle.abort();
1288 },
1289 )
1290 .await;
1291 }
1292
1293 #[tokio::test]
1301 async fn execute_serves_https_and_the_status_page_needs_no_token() {
1302 crate::config::with_isolated_config_path_async("serve-mod-tls", |_fake_dir| async move {
1303 with_tracing(|| {});
1304 let dir = tempfile::tempdir().expect("tempdir");
1305 let cert = dir.path().join("cert.pem");
1306 let key = dir.path().join("key.pem");
1307 std::fs::write(&cert, tls::tests::TEST_CERT).expect("write cert");
1308 std::fs::write(&key, tls::tests::TEST_KEY).expect("write key");
1309
1310 let args = ServeArgs {
1311 port: 0,
1312 host: "127.0.0.1".to_string(),
1313 cors: None,
1314 token: Some("test-token".to_string()),
1315 allow_admin: false,
1316 workdir_root: None,
1317 no_remote_yolo: false,
1318 tls_cert: Some(cert),
1319 tls_key: Some(key),
1320 };
1321 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1322 let handle = tokio::spawn(execute_with_shutdown(
1323 args,
1324 no_daemon_control(),
1325 Box::pin(std::future::pending()),
1326 Some(ready_tx),
1327 ));
1328 let addr = ready_rx.await.expect("server reports its address");
1329
1330 let mut roots = tokio_rustls::rustls::RootCertStore::empty();
1334 use rustls_pki_types::pem::PemObject;
1335 for der in
1336 rustls_pki_types::CertificateDer::pem_slice_iter(tls::tests::TEST_CA.as_bytes())
1337 {
1338 roots
1339 .add(der.expect("a parseable certificate"))
1340 .expect("add to the root store");
1341 }
1342 let client_config = tokio_rustls::rustls::ClientConfig::builder()
1343 .with_root_certificates(roots)
1344 .with_no_client_auth();
1345 let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(client_config));
1346
1347 let stream = tokio::net::TcpStream::connect(addr).await.expect("connect");
1348 let server_name = tokio_rustls::rustls::pki_types::ServerName::try_from("localhost")
1349 .expect("a valid name");
1350 let mut tls_stream = connector
1351 .connect(server_name, stream)
1352 .await
1353 .expect("the TLS handshake succeeds against the served certificate");
1354
1355 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1356 tls_stream
1357 .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
1358 .await
1359 .expect("write");
1360 let mut resp = Vec::new();
1361 tls_stream.read_to_end(&mut resp).await.expect("read");
1362 let text = String::from_utf8_lossy(&resp).into_owned();
1363
1364 assert!(text.starts_with("HTTP/1.1 200"), "{text}");
1367 assert!(text.contains("Leviath is running."), "{text}");
1368
1369 handle.abort();
1370 })
1371 .await;
1372 }
1373
1374 #[tokio::test]
1378 async fn a_bad_tls_configuration_stops_the_server_before_it_binds() {
1379 crate::config::with_isolated_config_path_async(
1380 "serve-mod-tls-bad",
1381 |_fake_dir| async move {
1382 with_tracing(|| {});
1383 let dir = tempfile::tempdir().expect("tempdir");
1384 let cert = dir.path().join("cert.pem");
1385 std::fs::write(&cert, "not a certificate").expect("write");
1386
1387 let base = ServeArgs {
1388 port: 0,
1389 host: "127.0.0.1".to_string(),
1390 cors: None,
1391 token: Some("test-token".to_string()),
1392 allow_admin: false,
1393 workdir_root: None,
1394 no_remote_yolo: false,
1395 tls_cert: None,
1396 tls_key: None,
1397 };
1398
1399 let lone = ServeArgs {
1401 tls_cert: Some(cert.clone()),
1402 ..base.clone()
1403 };
1404 let err = execute_with_shutdown(
1405 lone,
1406 no_daemon_control(),
1407 Box::pin(std::future::pending()),
1408 None,
1409 )
1410 .await
1411 .expect_err("one TLS flag alone is refused");
1412 let message = format!("{err:#}");
1413 assert!(message.contains("--tls-key"), "{message}");
1414
1415 let key = dir.path().join("key.pem");
1417 std::fs::write(&key, tls::tests::TEST_KEY).expect("write");
1418 let unreadable = ServeArgs {
1419 tls_cert: Some(cert),
1420 tls_key: Some(key),
1421 ..base
1422 };
1423 let err = execute_with_shutdown(
1424 unreadable,
1425 no_daemon_control(),
1426 Box::pin(std::future::pending()),
1427 None,
1428 )
1429 .await
1430 .expect_err("a malformed certificate is refused");
1431 let message = format!("{err:#}");
1432 assert!(message.contains("cert.pem"), "{message}");
1433 },
1434 )
1435 .await;
1436 }
1437
1438 #[tokio::test]
1445 async fn https_shuts_down_when_its_signal_resolves() {
1446 crate::config::with_isolated_config_path_async(
1447 "serve-mod-tls-shutdown",
1448 |_fake_dir| async move {
1449 with_tracing(|| {});
1450 let dir = tempfile::tempdir().expect("tempdir");
1451 let cert = dir.path().join("cert.pem");
1452 let key = dir.path().join("key.pem");
1453 std::fs::write(&cert, tls::tests::TEST_CERT).expect("write cert");
1454 std::fs::write(&key, tls::tests::TEST_KEY).expect("write key");
1455
1456 let args = ServeArgs {
1457 port: 0,
1458 host: "127.0.0.1".to_string(),
1459 cors: None,
1460 token: Some("test-token".to_string()),
1461 allow_admin: false,
1462 workdir_root: None,
1463 no_remote_yolo: false,
1464 tls_cert: Some(cert),
1465 tls_key: Some(key),
1466 };
1467 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel::<()>();
1468 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1469 let server = tokio::spawn(execute_with_shutdown(
1470 args,
1471 no_daemon_control(),
1472 Box::pin(async move {
1473 let _ = stop_rx.await;
1474 }),
1475 Some(ready_tx),
1476 ));
1477 ready_rx.await.expect("server reports its address");
1478
1479 stop_tx.send(()).expect("the server is listening for this");
1480 let finished = tokio::time::timeout(std::time::Duration::from_secs(10), server)
1484 .await
1485 .expect("the server should stop on its own");
1486 finished
1487 .expect("the task should not panic")
1488 .expect("a clean shutdown is not an error");
1489 },
1490 )
1491 .await;
1492 }
1493
1494 #[tokio::test]
1500 async fn execute_cors_preflight_allows_authorization_header() {
1501 crate::config::with_isolated_config_path_async(
1502 "serve-mod-cors-preflight",
1503 |_fake_dir| async move {
1504 with_tracing(|| {});
1505 let args = ServeArgs {
1506 port: 0,
1507 host: "127.0.0.1".to_string(),
1508 cors: Some("*".to_string()),
1509 token: Some("test-token".to_string()),
1510 allow_admin: false,
1511 workdir_root: None,
1512 no_remote_yolo: false,
1513 tls_cert: None,
1514 tls_key: None,
1515 };
1516 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1517 let handle = tokio::spawn(execute_with_shutdown(
1518 args,
1519 no_daemon_control(),
1520 Box::pin(std::future::pending()),
1521 Some(ready_tx),
1522 ));
1523 let addr = ready_rx
1524 .await
1525 .expect("server should report its bound address");
1526
1527 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1528 let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
1529 stream
1530 .write_all(
1531 b"OPTIONS /api/config HTTP/1.1\r\nHost: localhost\r\n\
1532 Origin: https://leviath.dev\r\n\
1533 Access-Control-Request-Method: GET\r\n\
1534 Access-Control-Request-Headers: authorization\r\n\
1535 Connection: close\r\n\r\n",
1536 )
1537 .await
1538 .unwrap();
1539 let mut resp = Vec::new();
1540 stream.read_to_end(&mut resp).await.unwrap();
1541 let lower = String::from_utf8_lossy(&resp).to_lowercase();
1542 assert!(
1543 lower.contains("access-control-allow-headers")
1544 && lower.contains("authorization"),
1545 "preflight must allow the Authorization header, got:\n{lower}"
1546 );
1547
1548 handle.abort();
1549 },
1550 )
1551 .await;
1552 }
1553
1554 #[tokio::test]
1555 async fn execute_with_specific_cors_origin_serves() {
1556 crate::config::with_isolated_config_path_async(
1557 "serve-mod-specific-cors",
1558 |_fake_dir| async move {
1559 let args = ServeArgs {
1560 port: 0,
1561 host: "127.0.0.1".to_string(),
1562 cors: Some("https://example.com".to_string()),
1563 token: Some("test-token".to_string()),
1564 allow_admin: false,
1565 workdir_root: None,
1566 no_remote_yolo: false,
1567 tls_cert: None,
1568 tls_key: None,
1569 };
1570 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1571 let handle = tokio::spawn(execute_with_shutdown(
1572 args,
1573 no_daemon_control(),
1574 Box::pin(std::future::pending()),
1575 Some(ready_tx),
1576 ));
1577 let addr = ready_rx
1578 .await
1579 .expect("server should report its bound address");
1580 assert!(tokio::net::TcpStream::connect(addr).await.is_ok());
1581
1582 handle.abort();
1583 },
1584 )
1585 .await;
1586 }
1587
1588 #[tokio::test]
1589 async fn execute_with_unparseable_addr_returns_err() {
1590 crate::config::with_isolated_config_path_async("serve-badaddr", |_fake_dir| async move {
1593 let args = ServeArgs {
1596 port: 0,
1597 host: "not a valid host".to_string(),
1598 cors: None,
1599 token: Some("test-token".to_string()),
1600 allow_admin: false,
1601 workdir_root: None,
1602 no_remote_yolo: false,
1603 tls_cert: None,
1604 tls_key: None,
1605 };
1606 let result = execute(args, no_daemon_control()).await;
1607 assert!(result.is_err());
1608 })
1609 .await;
1610 }
1611
1612 #[tokio::test]
1613 async fn test_agent_list_with_status_filter_full_router() {
1614 let app = test_app();
1615 let req = Request::builder()
1616 .uri("/api/agents?status=running,complete")
1617 .body(Body::empty())
1618 .unwrap();
1619 let resp = app.oneshot(req).await.unwrap();
1620 assert_eq!(resp.status(), StatusCode::OK);
1621 }
1622
1623 #[tokio::test]
1626 async fn execute_with_malformed_config_returns_err() {
1627 crate::config::with_isolated_config_path_async(
1628 "serve-mod-malformed",
1629 |_fake_dir| async move {
1630 std::fs::write(Config::config_path(), "not valid toml [[[").unwrap();
1632
1633 let args = ServeArgs {
1634 port: 0,
1635 host: "127.0.0.1".to_string(),
1636 cors: None,
1637 token: Some("test-token".to_string()),
1638 allow_admin: false,
1639 workdir_root: None,
1640 no_remote_yolo: false,
1641 tls_cert: None,
1642 tls_key: None,
1643 };
1644 let result = execute(args, no_daemon_control()).await;
1645 assert_execute_failed_on_malformed_config(&result);
1646 },
1647 )
1648 .await;
1649 }
1650
1651 #[tokio::test]
1655 async fn execute_with_bad_api_key_logs_warning_and_serves() {
1656 with_tracing(|| {});
1657 crate::config::with_isolated_config_path_async("serve-mod-badkey", |_fake_dir| async move {
1658 std::fs::write(
1660 Config::config_path(),
1661 "default_provider = \"anthropic\"\nagent_paths = []\n[providers]\nanthropic_api_key = \"bad-key-not-sk-ant\"\n",
1662 )
1663 .unwrap();
1664
1665 let args = ServeArgs {
1666 port: 0,
1667 host: "127.0.0.1".to_string(),
1668 cors: None,
1669 token: Some("test-token".to_string()),
1670 allow_admin: false,
1671 workdir_root: None,
1672 no_remote_yolo: false,
1673 tls_cert: None,
1674 tls_key: None,
1675 };
1676
1677 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
1678 let shutdown_fut = async move {
1679 let _ = shutdown_rx.await;
1680 };
1681 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1682
1683 let handle = tokio::spawn(execute_with_shutdown(
1684 args,
1685 no_daemon_control(),
1686 Box::pin(shutdown_fut),
1687 Some(ready_tx),
1688 ));
1689 let addr = ready_rx
1690 .await
1691 .expect("server should report its bound address");
1692 let connected = tokio::net::TcpStream::connect(addr).await.is_ok();
1693 assert_connected_with_bad_api_key(connected);
1694
1695 let _ = shutdown_tx.send(());
1697 let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
1698 .await
1699 .expect("timed out waiting for execute to return")
1700 .expect("task panicked");
1701 assert_execute_returned_ok_after_shutdown(&result);
1702 }).await;
1703 }
1704
1705 #[tokio::test]
1712 async fn execute_with_unbindable_address_returns_bind_error() {
1713 crate::config::with_isolated_config_path_async(
1716 "serve-unbindable",
1717 |_fake_dir| async move {
1718 let args = ServeArgs {
1719 port: 8080,
1720 host: "192.0.2.1".to_string(),
1721 cors: None,
1722 token: Some("test-token".to_string()),
1723 allow_admin: false,
1724 workdir_root: None,
1725 no_remote_yolo: false,
1726 tls_cert: None,
1727 tls_key: None,
1728 };
1729 let result = execute(args, no_daemon_control()).await;
1730 assert_execute_failed_on_port_in_use(&result);
1731 },
1732 )
1733 .await;
1734 }
1735
1736 #[tokio::test]
1737 async fn execute_refuses_to_start_without_a_token() {
1738 temp_env::async_with_vars([("LEVIATH_API_TOKEN", None::<&str>)], async {
1740 let args = ServeArgs {
1741 port: 0,
1742 host: "127.0.0.1".to_string(),
1743 cors: None,
1744 token: None,
1745 allow_admin: false,
1746 workdir_root: None,
1747 no_remote_yolo: false,
1748 tls_cert: None,
1749 tls_key: None,
1750 };
1751 let result = execute(args, no_daemon_control()).await;
1752 assert!(result.is_err(), "must refuse to start unauthenticated");
1753 })
1754 .await;
1755 }
1756
1757 #[tokio::test]
1760 async fn execute_with_shutdown_signal_returns_ok() {
1761 crate::config::with_isolated_config_path_async(
1762 "serve-mod-shutdown-signal",
1763 |_fake_dir| async move {
1764 let args = ServeArgs {
1765 port: 0,
1766 host: "127.0.0.1".to_string(),
1767 cors: None,
1768 token: Some("test-token".to_string()),
1769 allow_admin: false,
1770 workdir_root: None,
1771 no_remote_yolo: false,
1772 tls_cert: None,
1773 tls_key: None,
1774 };
1775
1776 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
1777 let shutdown_fut = async move {
1778 let _ = shutdown_rx.await;
1779 };
1780 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1781
1782 let handle = tokio::spawn(execute_with_shutdown(
1783 args,
1784 no_daemon_control(),
1785 Box::pin(shutdown_fut),
1786 Some(ready_tx),
1787 ));
1788 ready_rx
1789 .await
1790 .expect("server should report its bound address");
1791
1792 let _ = shutdown_tx.send(());
1794 let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
1795 .await
1796 .expect("timed out waiting for execute_with_shutdown to return")
1797 .expect("task panicked");
1798 assert_execute_with_shutdown_returned_ok(&result);
1799 },
1800 )
1801 .await;
1802 }
1803
1804 #[tokio::test]
1810 async fn execute_with_shutdown_no_ready_observer_returns_ok() {
1811 crate::config::with_isolated_config_path_async(
1812 "serve-mod-no-ready",
1813 |_fake_dir| async move {
1814 let args = ServeArgs {
1815 port: 0,
1816 host: "127.0.0.1".to_string(),
1817 cors: None,
1818 token: Some("test-token".to_string()),
1819 allow_admin: false,
1820 workdir_root: None,
1821 no_remote_yolo: false,
1822 tls_cert: None,
1823 tls_key: None,
1824 };
1825
1826 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
1827 let shutdown_fut = async move {
1828 let _ = shutdown_rx.await;
1829 };
1830
1831 let handle = tokio::spawn(execute_with_shutdown(
1832 args,
1833 no_daemon_control(),
1834 Box::pin(shutdown_fut),
1835 None,
1836 ));
1837 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1839 let _ = shutdown_tx.send(());
1840 let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
1841 .await
1842 .expect("timed out waiting for execute_with_shutdown to return")
1843 .expect("task panicked");
1844 assert_execute_with_shutdown_returned_ok(&result);
1845 },
1846 )
1847 .await;
1848 }
1849 #[tokio::test]
1853 async fn cors_is_off_by_default_explicit_when_asked_and_fatal_when_malformed() {
1854 crate::config::with_isolated_config_path_async("serve-mod-cors", |_fake_dir| async move {
1860 fn args_with(cors: Option<&str>) -> ServeArgs {
1861 ServeArgs {
1862 port: 0,
1863 host: "127.0.0.1".to_string(),
1864 cors: cors.map(str::to_string),
1865 token: Some("t".to_string()),
1866 allow_admin: false,
1867 workdir_root: None,
1868 no_remote_yolo: false,
1869 tls_cert: None,
1870 tls_key: None,
1871 }
1872 }
1873
1874 async fn starts(cors: Option<&str>) {
1877 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1878 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
1879 let server = tokio::spawn(execute_with_shutdown(
1880 args_with(cors),
1881 no_daemon_control(),
1882 Box::pin(async move {
1883 let _ = stop_rx.await;
1884 }),
1885 Some(ready_tx),
1886 ));
1887 ready_rx.await.expect("the server bound");
1893 let _ = stop_tx.send(());
1894 server.await.expect("join").expect("clean shutdown");
1895 }
1896
1897 starts(None).await;
1898 starts(Some("*")).await;
1899 starts(Some("https://ok.example")).await;
1900
1901 let err = execute_with_shutdown(
1904 args_with(Some("not a valid\nheader")),
1905 no_daemon_control(),
1906 Box::pin(std::future::pending()),
1907 None,
1908 )
1909 .await
1910 .expect_err("a malformed origin must refuse to start");
1911 assert!(
1914 err.to_string().contains("not a valid origin header"),
1915 "expected the CORS parse to be what refused, got: {err}"
1916 );
1917 })
1918 .await;
1919 }
1920
1921 #[tokio::test]
1924 async fn the_mcp_admin_routes_are_mounted_only_with_allow_admin() {
1925 crate::config::with_isolated_config_path_async("serve-mod-admin", |_fake_dir| async move {
1927 for allow_admin in [false, true] {
1928 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1929 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
1930 let args = ServeArgs {
1931 port: 0,
1932 host: "127.0.0.1".to_string(),
1933 cors: None,
1934 token: Some("t".to_string()),
1935 allow_admin,
1936 workdir_root: None,
1937 no_remote_yolo: false,
1938 tls_cert: None,
1939 tls_key: None,
1940 };
1941 let server = tokio::spawn(execute_with_shutdown(
1942 args,
1943 no_daemon_control(),
1944 Box::pin(async move {
1945 let _ = stop_rx.await;
1946 }),
1947 Some(ready_tx),
1948 ));
1949 let addr = ready_rx.await.expect("bound");
1950
1951 let status = reqwest::Client::new()
1952 .post(format!("http://{addr}/api/mcp/servers"))
1953 .bearer_auth("t")
1954 .json(&serde_json::json!({}))
1955 .send()
1956 .await
1957 .expect("request")
1958 .status()
1959 .as_u16();
1960 match allow_admin {
1965 false => assert_eq!(status, 405, "the admin route must not be mounted"),
1966 true => assert_ne!(status, 405, "the admin route must be mounted"),
1967 }
1968
1969 let _ = stop_tx.send(());
1970 let _ = server.await;
1971 }
1972 })
1973 .await;
1974 }
1975}