1mod agents;
7mod auth;
8mod blueprints;
9mod config;
10mod config_types;
11mod cursor;
12mod doctor;
13mod fs;
14mod interactions;
15mod mcp;
16mod polling;
17mod runs;
18mod scripts;
19mod search;
20#[cfg(test)]
21mod testutil;
22mod tls;
23mod tools;
24mod tree;
25mod types;
26mod websocket;
27
28use types::ServeLimits;
29pub use types::{AppState, ServeArgs, ServerEvent};
30
31use std::net::SocketAddr;
32use std::sync::Arc;
33
34use axum::Router;
35use axum::routing::{delete, get, post, put};
36use tokio::sync::broadcast;
37use tower_http::cors::{Any, CorsLayer};
38
39use crate::config::Config;
40
41struct AbortOnDrop<T>(tokio::task::JoinHandle<T>);
47
48impl<T> Drop for AbortOnDrop<T> {
49 fn drop(&mut self) {
50 self.0.abort();
51 }
52}
53
54pub async fn execute(
56 args: ServeArgs,
57 control: leviath_runtime::control_socket::ControlClient,
58) -> anyhow::Result<()> {
59 execute_with_shutdown(args, control, Box::pin(std::future::pending()), None).await
60}
61
62fn api_router() -> Router<AppState> {
68 Router::new()
69 .route(
71 "/api/blueprints",
72 get(blueprints::list_blueprints).post(blueprints::create_blueprint),
73 )
74 .route(
75 "/api/blueprints/validate",
76 post(blueprints::validate_blueprint),
77 )
78 .route(
79 "/api/blueprints/{name}",
80 get(blueprints::get_blueprint)
81 .put(blueprints::update_blueprint)
82 .delete(blueprints::delete_blueprint),
83 )
84 .route("/api/runs", get(runs::list_runs))
87 .route(
89 "/api/agents",
90 get(agents::list_agents).post(agents::spawn_agent),
91 )
92 .route("/api/agents/tree", get(tree::agents_tree))
93 .route(
94 "/api/agents/{id}",
95 get(agents::get_agent).delete(agents::kill_agent),
96 )
97 .route("/api/agents/{id}/children", get(agents::agent_children))
98 .route("/api/agents/{id}/context", get(agents::agent_context))
99 .route(
100 "/api/agents/{id}/context/history",
101 get(agents::agent_context_history),
102 )
103 .route("/api/agents/{id}/files", get(agents::agent_file))
104 .route("/api/agents/{id}/logs", get(agents::agent_logs))
105 .route("/api/agents/{id}/result", get(agents::agent_result))
106 .route("/api/agents/{id}/stages", get(agents::agent_stages))
107 .route("/api/agents/{id}/tree-status", get(tree::agent_tree_status))
108 .route("/api/agents/{id}/pause", post(agents::pause_agent))
109 .route("/api/agents/{id}/resume", post(agents::resume_agent))
110 .route("/api/agents/{id}/message", post(interactions::send_message))
112 .route(
114 "/api/agents/{id}/interaction",
115 get(interactions::get_interaction).post(interactions::submit_interaction),
116 )
117 .route("/api/mcp/servers", get(mcp::list_servers))
120 .route("/api/mcp/servers/{name}/status", get(mcp::status))
121 .route("/api/mcp/servers/{name}/login", post(mcp::login))
122 .route("/api/mcp/servers/{name}/test", post(mcp::test_server))
123 .route("/api/doctor", get(doctor::run_doctor))
125 .route("/api/fs/dirs", get(fs::list_dirs))
127 .route("/api/tools", get(tools::list_tools))
131 .route("/api/scripts", get(scripts::list_scripts))
134 .route("/api/scripts/validate", post(scripts::validate_script))
135 .route("/api/scripts/{kind}/{name}", get(scripts::get_script))
136 .route("/api/config", get(config::get_config))
138 .route("/api/config/validate", post(config::validate_config_key))
139 .route("/api/models", get(config::get_models))
140 .route("/ws", get(websocket::ws_global))
142 .route("/ws/agents/{id}", get(websocket::ws_agent))
143}
144
145#[cfg(test)]
153fn declared_routes() -> Vec<(String, String)> {
154 const SOURCE: &str = include_str!("mod.rs");
155 let production = SOURCE.split("\nmod tests {").next().unwrap_or(SOURCE);
159 routes_in(production)
160}
161
162#[cfg(test)]
167fn routes_in(source: &str) -> Vec<(String, String)> {
168 let mut routes = Vec::new();
169 for chunk in source.split(".route(").skip(1) {
172 let mut depth = 1usize;
176 let mut body = String::new();
177 for ch in chunk.chars() {
178 match ch {
179 '(' => depth += 1,
180 ')' => {
181 depth -= 1;
182 if depth == 0 {
183 break;
184 }
185 }
186 _ => {}
187 }
188 body.push(ch);
189 }
190 let Some(path) = body
191 .split_once('"')
192 .and_then(|(_, rest)| rest.split_once('"'))
193 .map(|(path, _)| path)
194 else {
195 continue;
196 };
197 if !path.starts_with('/') {
201 continue;
202 }
203 for method in ["get", "post", "put", "delete", "patch"] {
204 if body.contains(&format!("{method}(")) {
205 routes.push((path.to_string(), method.to_uppercase()));
206 }
207 }
208 }
209 routes
210}
211
212async fn execute_with_shutdown(
233 args: ServeArgs,
234 control: leviath_runtime::control_socket::ControlClient,
235 shutdown: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
236 ready: Option<tokio::sync::oneshot::Sender<SocketAddr>>,
237) -> anyhow::Result<()> {
238 let auth_token = std::sync::Arc::new(auth::resolve_token(args.token.as_deref())?);
240 if args.host != "127.0.0.1" && args.host != "localhost" && args.host != "::1" {
242 tracing::warn!(
243 host = %args.host,
244 "serving the agent API on a non-local address - anyone who can reach \
245 this host and holds the token can spawn agents"
246 );
247 }
248
249 let cfg = Config::load()?;
250 let allow_local_network = cfg.security.allow_local_network;
252 for warning in cfg.validate_keys() {
253 tracing::warn!("{}", warning);
254 }
255
256 let (event_tx, _) = broadcast::channel::<ServerEvent>(256);
259
260 let state = AppState {
261 config: Arc::new(cfg),
262 event_tx: event_tx.clone(),
263 control,
264 mcp: mcp::McpAdmin::default(),
265 limits: Arc::new(ServeLimits {
266 workdir_root: args.workdir_root.clone(),
267 no_remote_yolo: args.no_remote_yolo,
268 allow_local_network,
269 }),
270 };
271
272 let event_state = state.clone();
281 let _event_guard = AbortOnDrop(tokio::spawn(polling::event_loop(
282 event_state,
283 polling::RECONNECT_BACKOFF,
284 )));
285
286 let cors = match args.cors.as_deref() {
290 None => None,
291 Some("*") => Some(
292 CorsLayer::new()
293 .allow_origin(Any)
294 .allow_methods(Any)
295 .allow_headers([
300 axum::http::header::AUTHORIZATION,
301 axum::http::header::CONTENT_TYPE,
302 ]),
303 ),
304 Some(origin) => {
305 let value = origin.parse::<axum::http::HeaderValue>().map_err(|_| {
309 anyhow::anyhow!("--cors value '{origin}' is not a valid origin header")
310 })?;
311 Some(
312 CorsLayer::new()
313 .allow_origin(value)
314 .allow_methods(Any)
315 .allow_headers([
320 axum::http::header::AUTHORIZATION,
321 axum::http::header::CONTENT_TYPE,
322 ]),
323 )
324 }
325 };
326
327 let app = api_router();
328
329 let app = match args.allow_admin {
337 true => app
338 .route("/api/mcp/servers", post(mcp::add_server))
339 .route("/api/mcp/servers/{name}", delete(mcp::remove_server))
340 .route("/api/config", put(config::put_config))
343 .route(
349 "/api/scripts/{kind}/{name}",
350 put(scripts::put_script).delete(scripts::delete_script),
351 ),
352 false => app,
353 };
354
355 let app = app
356 .layer(axum::middleware::from_fn_with_state(
359 auth_token,
360 auth::require_auth,
361 ))
362 .with_state(state);
363
364 let app = app.merge(Router::new().route("/", get(status_page)));
374 let app = match cors {
378 Some(layer) => app.layer(layer),
379 None => app,
380 };
381
382 let tls = tls::resolve(args.tls_cert.clone(), args.tls_key.clone())?;
386 let tls_config = match &tls {
387 Some(paths) => Some(tls::load(paths).await?),
388 None => None,
389 };
390
391 let addr: SocketAddr = format!("{}:{}", args.host, args.port).parse()?;
392 let scheme = tls::scheme(tls.as_ref());
393 tracing::info!("Listening on {}://{}", scheme, addr);
394 println!("Leviath API server listening on {scheme}://{addr}");
395
396 let listener = tokio::net::TcpListener::bind(addr).await?;
397 if let Some(ready) = ready {
398 let local_addr = listener
401 .local_addr()
402 .expect("infallible: a freshly bound TcpListener always has a local address");
403 let _ = ready.send(local_addr);
404 }
405
406 match tls_config {
407 None => {
410 let _ = axum::serve(listener, app)
411 .with_graceful_shutdown(shutdown)
412 .await;
413 }
414 Some(config) => serve_tls(listener, app, config, shutdown).await,
415 }
416
417 Ok(())
418}
419
420async fn serve_tls(
431 listener: tokio::net::TcpListener,
432 app: Router,
433 config: axum_server::tls_rustls::RustlsConfig,
434 shutdown: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
435) {
436 let handle = axum_server::Handle::new();
437 let signal = handle.clone();
438 tokio::spawn(async move {
439 shutdown.await;
440 signal.graceful_shutdown(Some(std::time::Duration::from_secs(5)));
444 });
445 let std_listener = listener
451 .into_std()
452 .expect("infallible: a bound tokio listener always converts back");
453 let server = axum_server::from_tcp_rustls(std_listener, config)
454 .expect("infallible: the listener is bound and non-blocking, which is all this checks");
455 let _ = server.handle(handle).serve(app.into_make_service()).await;
459}
460
461async fn status_page() -> axum::response::Html<&'static str> {
469 axum::response::Html(
470 "<!doctype html><meta charset=utf-8><title>Leviath</title>\
471 <body style=\"font:16px system-ui;margin:4rem auto;max-width:30rem\">\
472 <h1>Leviath is running.</h1>\
473 <p>The API needs a token; this page does not serve it.</p>",
474 )
475}
476
477#[cfg(test)]
480mod tests {
481 use super::*;
482 use axum::body::Body;
483 use axum::http::{Request, StatusCode};
484 use tower::ServiceExt;
485
486 use crate::runstate::RunMeta;
487 use crate::test_support::with_tracing;
488
489 const OPENAPI: &str = include_str!("../../../../../docs/schema/openapi.json");
491
492 #[test]
500 fn the_api_version_matches_the_spec_it_names() {
501 let spec: serde_json::Value = serde_json::from_str(OPENAPI).expect("the spec is JSON");
502 let documented = spec["info"]["version"]
503 .as_str()
504 .expect("the spec declares a version");
505 assert_eq!(documented, types::API_VERSION);
506 }
507
508 fn documented_routes() -> Vec<(String, String)> {
510 let spec: serde_json::Value = serde_json::from_str(OPENAPI).expect("the spec is JSON");
511 let paths = spec["paths"].as_object().expect("the spec has paths");
512 let mut routes = Vec::new();
513 for (path, item) in paths {
514 let operations = item.as_object().expect("a path item is an object");
515 for method in ["get", "post", "put", "delete", "patch"] {
516 if operations.contains_key(method) {
517 routes.push((path.clone(), method.to_uppercase()));
518 }
519 }
520 }
521 routes
522 }
523
524 type Routes = Vec<(String, String)>;
526
527 fn spec_drift() -> (Routes, Routes) {
530 let declared = declared_routes();
531 let documented = documented_routes();
532 let missing = declared
533 .iter()
534 .filter(|r| !documented.contains(r))
535 .cloned()
536 .collect();
537 let extra = documented
538 .iter()
539 .filter(|r| !declared.contains(r))
540 .cloned()
541 .collect();
542 (missing, extra)
543 }
544
545 #[test]
546 fn the_openapi_spec_documents_exactly_the_routes_this_router_serves() {
547 let (missing, extra) = spec_drift();
558 assert!(missing.is_empty());
559 assert!(extra.is_empty());
560 }
561
562 #[test]
563 fn the_route_reader_finds_the_routes_that_are_actually_there() {
564 let declared = declared_routes();
568 assert!(declared.len() > 25);
569 assert!(declared.contains(&("/api/agents".to_string(), "POST".to_string())));
570 assert!(declared.contains(&("/api/agents/{id}".to_string(), "DELETE".to_string())));
571 assert!(declared.contains(&("/ws".to_string(), "GET".to_string())));
572 }
573
574 #[test]
575 fn the_route_reader_ignores_text_that_is_not_a_route() {
576 let source = concat!(
580 "let x = source.split(\".route(\").skip(1);\n",
581 ".route(\"not a path\", get(h))\n",
582 ".route(\"/real\", get(h).post(h))\n"
583 );
584 assert_eq!(
585 routes_in(source),
586 vec![
587 ("/real".to_string(), "GET".to_string()),
588 ("/real".to_string(), "POST".to_string()),
589 ]
590 );
591 }
592
593 #[test]
594 fn the_route_reader_reads_nothing_out_of_source_with_no_routes() {
595 assert_eq!(routes_in("fn main() {}"), Vec::new());
596 }
597
598 fn assert_execute_failed_on_malformed_config(result: &anyhow::Result<()>) {
603 assert!(
604 result.is_err(),
605 "execute should fail when config is malformed"
606 );
607 }
608
609 #[test]
610 #[should_panic(expected = "execute should fail when config is malformed")]
611 fn assert_execute_failed_on_malformed_config_panics_when_ok() {
612 assert_execute_failed_on_malformed_config(&Ok(()));
613 }
614
615 fn assert_connected_with_bad_api_key(connected: bool) {
618 assert!(connected, "server should start even with a bad API key");
619 }
620
621 #[test]
622 #[should_panic(expected = "server should start even with a bad API key")]
623 fn assert_connected_with_bad_api_key_panics_when_not_connected() {
624 assert_connected_with_bad_api_key(false);
625 }
626
627 fn assert_execute_returned_ok_after_shutdown(result: &Result<(), anyhow::Error>) {
630 assert!(
631 result.is_ok(),
632 "execute should return Ok after graceful shutdown"
633 );
634 }
635
636 #[test]
637 #[should_panic(expected = "execute should return Ok after graceful shutdown")]
638 fn assert_execute_returned_ok_after_shutdown_panics_when_err() {
639 assert_execute_returned_ok_after_shutdown(&Err(anyhow::anyhow!("boom")));
640 }
641
642 fn assert_execute_failed_on_port_in_use(result: &anyhow::Result<()>) {
645 assert!(
646 result.is_err(),
647 "execute should fail when port is already in use"
648 );
649 }
650
651 #[test]
652 #[should_panic(expected = "execute should fail when port is already in use")]
653 fn assert_execute_failed_on_port_in_use_panics_when_ok() {
654 assert_execute_failed_on_port_in_use(&Ok(()));
655 }
656
657 fn assert_execute_with_shutdown_returned_ok(result: &Result<(), anyhow::Error>) {
661 assert!(
662 result.is_ok(),
663 "execute_with_shutdown should return Ok(()) after graceful shutdown"
664 );
665 }
666
667 #[test]
668 #[should_panic(expected = "execute_with_shutdown should return Ok(()) after graceful shutdown")]
669 fn assert_execute_with_shutdown_returned_ok_panics_when_err() {
670 assert_execute_with_shutdown_returned_ok(&Err(anyhow::anyhow!("boom")));
671 }
672
673 fn assert_response_ok(resp_str: &str) {
676 assert!(resp_str.starts_with("HTTP/1.1 200"), "got: {resp_str}");
677 }
678
679 #[test]
680 #[should_panic(expected = "got: HTTP/1.1 404 Not Found")]
681 fn assert_response_ok_panics_when_not_200() {
682 assert_response_ok("HTTP/1.1 404 Not Found\r\n\r\n");
683 }
684
685 fn no_daemon_control() -> leviath_runtime::control_socket::ControlClient {
688 leviath_runtime::control_socket::ControlClient::new(
689 leviath_runtime::control_socket::control_id(std::path::Path::new("/no/such/leviath")),
690 )
691 }
692
693 fn test_state() -> AppState {
694 let (tx, _) = broadcast::channel(64);
695 AppState {
696 config: Arc::new(Config::default()),
697 event_tx: tx,
698 control: no_daemon_control(),
699 mcp: crate::commands::serve::mcp::McpAdmin::default(),
700 limits: Default::default(),
701 }
702 }
703
704 fn test_app() -> Router {
707 api_router().with_state(test_state())
708 }
709
710 #[tokio::test]
711 async fn test_list_blueprints() {
712 let app = test_app();
713 let req = Request::builder()
714 .uri("/api/blueprints")
715 .body(Body::empty())
716 .unwrap();
717 let resp = app.oneshot(req).await.unwrap();
718 assert_eq!(resp.status(), StatusCode::OK);
719 }
720
721 #[tokio::test]
722 async fn test_router_serves_routes_the_old_hand_copy_missed() {
723 let app = test_app();
727 let req = Request::builder()
728 .uri("/api/mcp/servers")
729 .body(Body::empty())
730 .unwrap();
731 let resp = app.oneshot(req).await.unwrap();
732 assert_eq!(resp.status(), StatusCode::OK);
733 }
734
735 #[tokio::test]
736 async fn test_pause_and_resume_routes_are_mounted() {
737 for action in ["pause", "resume"] {
741 let app = test_app();
742 let req = Request::builder()
743 .method("POST")
744 .uri(format!("/api/agents/some-run/{action}"))
745 .body(Body::empty())
746 .unwrap();
747 let resp = app.oneshot(req).await.unwrap();
748 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
749 }
750 }
751
752 #[tokio::test]
753 async fn test_agent_files_route_is_mounted() {
754 let app = test_app();
764 let req = Request::builder()
765 .uri("/api/agents/some-run/files")
766 .body(Body::empty())
767 .unwrap();
768 let resp = app.oneshot(req).await.unwrap();
769 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
770 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
771 .await
772 .unwrap();
773 let error = serde_json::from_slice::<serde_json::Value>(&body)
774 .ok()
775 .and_then(|v| v["error"].as_str().map(str::to_string))
776 .unwrap_or_default();
777 assert!(error.contains("some-run"));
778 }
779
780 #[tokio::test]
781 async fn test_fs_dirs_route_is_mounted() {
782 let app = test_app();
787 let req = Request::builder()
788 .uri("/api/fs/dirs?path=not/absolute")
789 .body(Body::empty())
790 .unwrap();
791 let resp = app.oneshot(req).await.unwrap();
792 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
793 }
794
795 #[tokio::test]
796 async fn test_get_blueprint_not_found() {
797 let app = test_app();
798 let req = Request::builder()
799 .uri("/api/blueprints/nonexistent-agent-xyz")
800 .body(Body::empty())
801 .unwrap();
802 let resp = app.oneshot(req).await.unwrap();
803 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
804 }
805
806 #[tokio::test]
807 async fn test_validate_blueprint_valid() {
808 let app = test_app();
809 let manifest = r#"
810[agent]
811name = "test-agent"
812version = "0.1.0"
813description = "A test"
814
815[stages.main]
816mode = "autonomous"
817[stages.main.model]
818provider = "anthropic"
819model = "claude-sonnet-4-6"
820"#;
821 let body = serde_json::json!({ "manifest": manifest });
822 let req = Request::builder()
823 .method("POST")
824 .uri("/api/blueprints/validate")
825 .header("content-type", "application/json")
826 .body(Body::from(serde_json::to_string(&body).unwrap()))
827 .unwrap();
828 let resp = app.oneshot(req).await.unwrap();
829 assert_eq!(resp.status(), StatusCode::OK);
830
831 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
832 .await
833 .unwrap();
834 let val: types::ValidateResponse = serde_json::from_slice(&body).unwrap();
835 assert!(val.valid);
836 }
837
838 #[tokio::test]
839 async fn test_validate_blueprint_invalid() {
840 let app = test_app();
841 let body = serde_json::json!({ "manifest": "not valid toml {{{{" });
842 let req = Request::builder()
843 .method("POST")
844 .uri("/api/blueprints/validate")
845 .header("content-type", "application/json")
846 .body(Body::from(serde_json::to_string(&body).unwrap()))
847 .unwrap();
848 let resp = app.oneshot(req).await.unwrap();
849 assert_eq!(resp.status(), StatusCode::OK);
850
851 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
852 .await
853 .unwrap();
854 let val: types::ValidateResponse = serde_json::from_slice(&body).unwrap();
855 assert!(!val.valid);
856 assert!(val.errors.is_some());
857 }
858
859 #[tokio::test]
860 async fn test_list_agents() {
861 let app = test_app();
862 let req = Request::builder()
863 .uri("/api/agents")
864 .body(Body::empty())
865 .unwrap();
866 let resp = app.oneshot(req).await.unwrap();
867 assert_eq!(resp.status(), StatusCode::OK);
868 }
869
870 #[tokio::test]
871 async fn test_agents_tree() {
872 let app = test_app();
873 let req = Request::builder()
874 .uri("/api/agents/tree")
875 .body(Body::empty())
876 .unwrap();
877 let resp = app.oneshot(req).await.unwrap();
878 assert_eq!(resp.status(), StatusCode::OK);
879 }
880
881 #[tokio::test]
882 async fn test_get_agent_not_found() {
883 let app = test_app();
884 let req = Request::builder()
885 .uri("/api/agents/nonexistent-run-id-xyz")
886 .body(Body::empty())
887 .unwrap();
888 let resp = app.oneshot(req).await.unwrap();
889 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
890 }
891
892 #[tokio::test]
893 async fn test_agent_children_empty() {
894 let app = test_app();
895 let req = Request::builder()
896 .uri("/api/agents/nonexistent/children")
897 .body(Body::empty())
898 .unwrap();
899 let resp = app.oneshot(req).await.unwrap();
900 assert_eq!(resp.status(), StatusCode::OK);
902 }
903
904 #[tokio::test]
905 async fn test_agent_context_not_found() {
906 let app = test_app();
907 let req = Request::builder()
908 .uri("/api/agents/nonexistent/context")
909 .body(Body::empty())
910 .unwrap();
911 let resp = app.oneshot(req).await.unwrap();
912 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
913 }
914
915 #[tokio::test]
916 async fn test_agent_logs_not_found() {
917 let app = test_app();
918 let req = Request::builder()
919 .uri("/api/agents/nonexistent/logs")
920 .body(Body::empty())
921 .unwrap();
922 let resp = app.oneshot(req).await.unwrap();
923 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
924 }
925
926 #[tokio::test]
927 async fn test_agent_result_not_found() {
928 let app = test_app();
929 let req = Request::builder()
930 .uri("/api/agents/nonexistent/result")
931 .body(Body::empty())
932 .unwrap();
933 let resp = app.oneshot(req).await.unwrap();
934 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
935 }
936
937 #[tokio::test]
938 async fn test_agent_tree_status_not_found() {
939 let app = test_app();
940 let req = Request::builder()
941 .uri("/api/agents/nonexistent/tree-status")
942 .body(Body::empty())
943 .unwrap();
944 let resp = app.oneshot(req).await.unwrap();
945 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
946 }
947
948 #[tokio::test]
949 async fn test_interaction_route_reaches_daemon() {
950 let app = test_app();
953 let req = Request::builder()
954 .uri("/api/agents/nonexistent/interaction")
955 .body(Body::empty())
956 .unwrap();
957 let resp = app.oneshot(req).await.unwrap();
958 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
959 }
960
961 #[tokio::test]
962 async fn test_get_config() {
963 let app = test_app();
964 let req = Request::builder()
965 .uri("/api/config")
966 .body(Body::empty())
967 .unwrap();
968 let resp = app.oneshot(req).await.unwrap();
969 assert_eq!(resp.status(), StatusCode::OK);
970
971 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
972 .await
973 .unwrap();
974 let val: types::RedactedConfig = serde_json::from_slice(&body).unwrap();
975 assert_eq!(val.default_provider, "anthropic");
976 assert!(!val.has_anthropic_key);
978 assert!(!val.has_openai_key);
979 }
980
981 #[tokio::test]
982 async fn test_tree_building() {
983 let runs = vec![
985 RunMeta::new(
986 "parent-1".to_string(),
987 "agent-a".to_string(),
988 "/path".to_string(),
989 "task".to_string(),
990 None,
991 "/work".to_string(),
992 1,
993 ),
994 {
995 let mut child = RunMeta::new(
996 "child-1".to_string(),
997 "agent-b".to_string(),
998 "/path".to_string(),
999 "sub-task".to_string(),
1000 None,
1001 "/work".to_string(),
1002 1,
1003 );
1004 child.parent_run_id = Some("parent-1".to_string());
1005 child.prompt_tokens = 100;
1006 child.completion_tokens = 50;
1007 child
1008 },
1009 ];
1010
1011 let tree = tree::build_tree_status(&runs, None);
1012 assert_eq!(tree.len(), 1);
1013 assert_eq!(tree[0].run_id, "parent-1");
1014 assert_eq!(tree[0].children.len(), 1);
1015 assert_eq!(tree[0].subtree_prompt_tokens, 100); assert_eq!(tree[0].subtree_completion_tokens, 50);
1017 }
1018
1019 #[tokio::test]
1020 async fn test_delete_blueprint_not_found() {
1021 let app = test_app();
1022 let req = Request::builder()
1023 .method("DELETE")
1024 .uri("/api/blueprints/nonexistent-agent-xyz")
1025 .body(Body::empty())
1026 .unwrap();
1027 let resp = app.oneshot(req).await.unwrap();
1028 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1029 }
1030
1031 #[tokio::test]
1032 async fn test_server_event_serialization() {
1033 let event = ServerEvent::AgentStatus {
1034 agent_id: "coder".to_string(),
1035 run_id: "run-123".to_string(),
1036 status: "running".to_string(),
1037 stage: "implement".to_string(),
1038 iteration: 5,
1039 tool_calls: 0,
1040 accepts_messages: true,
1041 wait_reason: None,
1042 };
1043 let json = serde_json::to_string(&event).unwrap();
1044 assert!(json.contains("\"type\":\"agent_status\""));
1045 assert!(json.contains("\"agent_id\":\"coder\""));
1046
1047 let event2 = ServerEvent::Tokens {
1048 agent_id: "coder".to_string(),
1049 run_id: "run-123".to_string(),
1050 prompt_tokens: 5000,
1051 completion_tokens: 1200,
1052 cached_tokens: 0,
1053 cache_write_tokens: 0,
1054 };
1055 let json2 = serde_json::to_string(&event2).unwrap();
1056 assert!(json2.contains("\"type\":\"tokens\""));
1057 assert!(json2.contains("\"prompt_tokens\":5000"));
1058 }
1059
1060 #[tokio::test]
1061 async fn test_full_router_create_blueprint_invalid() {
1062 let app = test_app();
1063 let body = serde_json::json!({
1064 "name": "bad-agent",
1065 "manifest": "not valid toml {{{"
1066 });
1067 let req = Request::builder()
1068 .method("POST")
1069 .uri("/api/blueprints")
1070 .header("content-type", "application/json")
1071 .body(Body::from(serde_json::to_string(&body).unwrap()))
1072 .unwrap();
1073 let resp = app.oneshot(req).await.unwrap();
1074 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1075 }
1076
1077 #[tokio::test]
1078 async fn test_full_router_update_blueprint_not_found() {
1079 let app = test_app();
1080 let body = serde_json::json!({
1081 "manifest": r#"
1082[agent]
1083name = "no-such-agent"
1084version = "1.0.0"
1085description = "Missing"
1086
1087[stages.run]
1088system_prompt = "Run"
1089"#
1090 });
1091 let req = Request::builder()
1092 .method("PUT")
1093 .uri("/api/blueprints/no-such-agent-xyz-99999")
1094 .header("content-type", "application/json")
1095 .body(Body::from(serde_json::to_string(&body).unwrap()))
1096 .unwrap();
1097 let resp = app.oneshot(req).await.unwrap();
1098 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1099 }
1100
1101 #[tokio::test]
1102 async fn test_full_router_kill_agent_reaches_daemon() {
1103 let app = test_app();
1104 let req = Request::builder()
1105 .method("DELETE")
1106 .uri("/api/agents/nonexistent-kill-id-xyz")
1107 .body(Body::empty())
1108 .unwrap();
1109 let resp = app.oneshot(req).await.unwrap();
1110 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
1111 }
1112
1113 #[tokio::test]
1114 async fn test_full_router_send_message_reaches_daemon() {
1115 let app = test_app();
1116 let body = serde_json::json!({"message": "hello"});
1117 let req = Request::builder()
1118 .method("POST")
1119 .uri("/api/agents/nonexistent-msg-id-xyz/message")
1120 .header("content-type", "application/json")
1121 .body(Body::from(serde_json::to_string(&body).unwrap()))
1122 .unwrap();
1123 let resp = app.oneshot(req).await.unwrap();
1124 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
1125 }
1126
1127 #[tokio::test]
1128 async fn test_full_router_get_models() {
1129 let app = test_app();
1130 let req = Request::builder()
1131 .uri("/api/models")
1132 .body(Body::empty())
1133 .unwrap();
1134 let resp = app.oneshot(req).await.unwrap();
1135 assert_eq!(resp.status(), StatusCode::OK);
1136 }
1137
1138 #[tokio::test]
1139 async fn test_full_router_spawn_agent_blueprint_not_found() {
1140 let app = test_app();
1141 let body = serde_json::json!({
1142 "blueprint": "nonexistent-blueprint-xyz",
1143 "task": "do something"
1144 });
1145 let req = Request::builder()
1146 .method("POST")
1147 .uri("/api/agents")
1148 .header("content-type", "application/json")
1149 .body(Body::from(serde_json::to_string(&body).unwrap()))
1150 .unwrap();
1151 let resp = app.oneshot(req).await.unwrap();
1152 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1153 }
1154
1155 #[test]
1156 fn test_serve_args_defaults() {
1157 let args = ServeArgs {
1158 port: 3000,
1159 host: "127.0.0.1".to_string(),
1160 cors: None,
1161 token: Some("test-token".to_string()),
1162 allow_admin: false,
1163 workdir_root: None,
1164 no_remote_yolo: false,
1165 tls_cert: None,
1166 tls_key: None,
1167 };
1168 assert_eq!(args.port, 3000);
1169 assert_eq!(args.host, "127.0.0.1");
1170 assert_eq!(args.cors, None);
1171 }
1172
1173 #[test]
1174 fn test_app_state_clone() {
1175 let state = test_state();
1176 let cloned = state.clone();
1177 let _ = cloned.config.default_provider.clone();
1179 }
1180
1181 #[test]
1182 fn test_cors_wildcard_vs_specific() {
1183 let wildcard = "*";
1185 let specific = "https://example.com";
1186
1187 let is_wildcard = wildcard == "*";
1188 assert!(is_wildcard);
1189
1190 let is_specific = specific != "*";
1191 assert!(is_specific);
1192
1193 let parsed = specific.parse::<axum::http::HeaderValue>();
1195 assert!(parsed.is_ok());
1196 }
1197
1198 #[test]
1199 fn test_cors_invalid_origin_falls_back() {
1200 let invalid_cors = "not a valid header value \x00";
1201 let result = invalid_cors.parse::<axum::http::HeaderValue>();
1202 assert!(result.is_err());
1204 }
1205
1206 #[tokio::test]
1207 async fn test_submit_interaction_full_router_reaches_daemon() {
1208 let app = test_app();
1212 let body = serde_json::json!({"request_id": "req-1", "value": "do it", "scope": "once"});
1213 let req = Request::builder()
1214 .method("POST")
1215 .uri("/api/agents/any/interaction")
1216 .header("content-type", "application/json")
1217 .body(Body::from(serde_json::to_string(&body).unwrap()))
1218 .unwrap();
1219 let resp = app.oneshot(req).await.unwrap();
1220 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
1221 }
1222
1223 #[tokio::test]
1243 async fn execute_binds_and_serves_with_wildcard_cors() {
1244 crate::config::with_isolated_config_path_async(
1245 "serve-mod-wildcard-cors",
1246 |_fake_dir| async move {
1247 with_tracing(|| {});
1248 let args = ServeArgs {
1258 port: 0,
1259 host: "127.0.0.1".to_string(),
1260 cors: None,
1261 token: Some("test-token".to_string()),
1262 allow_admin: false,
1263 workdir_root: None,
1264 no_remote_yolo: false,
1265 tls_cert: None,
1266 tls_key: None,
1267 };
1268 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1269 let handle = tokio::spawn(execute_with_shutdown(
1270 args,
1271 no_daemon_control(),
1272 Box::pin(std::future::pending()),
1273 Some(ready_tx),
1274 ));
1275 let addr = ready_rx
1276 .await
1277 .expect("server should report its bound address");
1278
1279 let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
1281 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1282 stream
1283 .write_all(
1284 b"GET /api/config HTTP/1.1\r\nHost: localhost\r\n\
1285 Authorization: Bearer test-token\r\nConnection: close\r\n\r\n",
1286 )
1287 .await
1288 .unwrap();
1289 let mut resp = Vec::new();
1290 stream.read_to_end(&mut resp).await.unwrap();
1291 let resp_str = String::from_utf8_lossy(&resp);
1292 assert_response_ok(&resp_str);
1293
1294 let mut unauth = tokio::net::TcpStream::connect(addr).await.unwrap();
1296 unauth
1297 .write_all(
1298 b"GET /api/config HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
1299 )
1300 .await
1301 .unwrap();
1302 let mut resp2 = Vec::new();
1303 unauth.read_to_end(&mut resp2).await.unwrap();
1304 assert!(
1305 String::from_utf8_lossy(&resp2).starts_with("HTTP/1.1 401"),
1306 "unauthenticated request should be 401"
1307 );
1308
1309 handle.abort();
1310 },
1311 )
1312 .await;
1313 }
1314
1315 #[tokio::test]
1323 async fn execute_serves_https_and_the_status_page_needs_no_token() {
1324 crate::config::with_isolated_config_path_async("serve-mod-tls", |_fake_dir| async move {
1325 with_tracing(|| {});
1326 let dir = tempfile::tempdir().expect("tempdir");
1327 let cert = dir.path().join("cert.pem");
1328 let key = dir.path().join("key.pem");
1329 std::fs::write(&cert, tls::tests::TEST_CERT).expect("write cert");
1330 std::fs::write(&key, tls::tests::TEST_KEY).expect("write key");
1331
1332 let args = ServeArgs {
1333 port: 0,
1334 host: "127.0.0.1".to_string(),
1335 cors: None,
1336 token: Some("test-token".to_string()),
1337 allow_admin: false,
1338 workdir_root: None,
1339 no_remote_yolo: false,
1340 tls_cert: Some(cert),
1341 tls_key: Some(key),
1342 };
1343 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1344 let handle = tokio::spawn(execute_with_shutdown(
1345 args,
1346 no_daemon_control(),
1347 Box::pin(std::future::pending()),
1348 Some(ready_tx),
1349 ));
1350 let addr = ready_rx.await.expect("server reports its address");
1351
1352 let mut roots = tokio_rustls::rustls::RootCertStore::empty();
1356 use rustls_pki_types::pem::PemObject;
1357 for der in
1358 rustls_pki_types::CertificateDer::pem_slice_iter(tls::tests::TEST_CA.as_bytes())
1359 {
1360 roots
1361 .add(der.expect("a parseable certificate"))
1362 .expect("add to the root store");
1363 }
1364 let client_config = tokio_rustls::rustls::ClientConfig::builder()
1365 .with_root_certificates(roots)
1366 .with_no_client_auth();
1367 let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(client_config));
1368
1369 let stream = tokio::net::TcpStream::connect(addr).await.expect("connect");
1370 let server_name = tokio_rustls::rustls::pki_types::ServerName::try_from("localhost")
1371 .expect("a valid name");
1372 let mut tls_stream = connector
1373 .connect(server_name, stream)
1374 .await
1375 .expect("the TLS handshake succeeds against the served certificate");
1376
1377 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1378 tls_stream
1379 .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
1380 .await
1381 .expect("write");
1382 let mut resp = Vec::new();
1383 tls_stream.read_to_end(&mut resp).await.expect("read");
1384 let text = String::from_utf8_lossy(&resp).into_owned();
1385
1386 assert!(text.starts_with("HTTP/1.1 200"), "{text}");
1389 assert!(text.contains("Leviath is running."), "{text}");
1390
1391 handle.abort();
1392 })
1393 .await;
1394 }
1395
1396 #[tokio::test]
1400 async fn a_bad_tls_configuration_stops_the_server_before_it_binds() {
1401 crate::config::with_isolated_config_path_async(
1402 "serve-mod-tls-bad",
1403 |_fake_dir| async move {
1404 with_tracing(|| {});
1405 let dir = tempfile::tempdir().expect("tempdir");
1406 let cert = dir.path().join("cert.pem");
1407 std::fs::write(&cert, "not a certificate").expect("write");
1408
1409 let base = ServeArgs {
1410 port: 0,
1411 host: "127.0.0.1".to_string(),
1412 cors: None,
1413 token: Some("test-token".to_string()),
1414 allow_admin: false,
1415 workdir_root: None,
1416 no_remote_yolo: false,
1417 tls_cert: None,
1418 tls_key: None,
1419 };
1420
1421 let lone = ServeArgs {
1423 tls_cert: Some(cert.clone()),
1424 ..base.clone()
1425 };
1426 let err = execute_with_shutdown(
1427 lone,
1428 no_daemon_control(),
1429 Box::pin(std::future::pending()),
1430 None,
1431 )
1432 .await
1433 .expect_err("one TLS flag alone is refused");
1434 let message = format!("{err:#}");
1435 assert!(message.contains("--tls-key"), "{message}");
1436
1437 let key = dir.path().join("key.pem");
1439 std::fs::write(&key, tls::tests::TEST_KEY).expect("write");
1440 let unreadable = ServeArgs {
1441 tls_cert: Some(cert),
1442 tls_key: Some(key),
1443 ..base
1444 };
1445 let err = execute_with_shutdown(
1446 unreadable,
1447 no_daemon_control(),
1448 Box::pin(std::future::pending()),
1449 None,
1450 )
1451 .await
1452 .expect_err("a malformed certificate is refused");
1453 let message = format!("{err:#}");
1454 assert!(message.contains("cert.pem"), "{message}");
1455 },
1456 )
1457 .await;
1458 }
1459
1460 #[tokio::test]
1467 async fn https_shuts_down_when_its_signal_resolves() {
1468 crate::config::with_isolated_config_path_async(
1469 "serve-mod-tls-shutdown",
1470 |_fake_dir| async move {
1471 with_tracing(|| {});
1472 let dir = tempfile::tempdir().expect("tempdir");
1473 let cert = dir.path().join("cert.pem");
1474 let key = dir.path().join("key.pem");
1475 std::fs::write(&cert, tls::tests::TEST_CERT).expect("write cert");
1476 std::fs::write(&key, tls::tests::TEST_KEY).expect("write key");
1477
1478 let args = ServeArgs {
1479 port: 0,
1480 host: "127.0.0.1".to_string(),
1481 cors: None,
1482 token: Some("test-token".to_string()),
1483 allow_admin: false,
1484 workdir_root: None,
1485 no_remote_yolo: false,
1486 tls_cert: Some(cert),
1487 tls_key: Some(key),
1488 };
1489 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel::<()>();
1490 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1491 let server = tokio::spawn(execute_with_shutdown(
1492 args,
1493 no_daemon_control(),
1494 Box::pin(async move {
1495 let _ = stop_rx.await;
1496 }),
1497 Some(ready_tx),
1498 ));
1499 ready_rx.await.expect("server reports its address");
1500
1501 stop_tx.send(()).expect("the server is listening for this");
1502 let finished = tokio::time::timeout(std::time::Duration::from_secs(10), server)
1506 .await
1507 .expect("the server should stop on its own");
1508 finished
1509 .expect("the task should not panic")
1510 .expect("a clean shutdown is not an error");
1511 },
1512 )
1513 .await;
1514 }
1515
1516 #[tokio::test]
1522 async fn execute_cors_preflight_allows_authorization_header() {
1523 crate::config::with_isolated_config_path_async(
1524 "serve-mod-cors-preflight",
1525 |_fake_dir| async move {
1526 with_tracing(|| {});
1527 let args = ServeArgs {
1528 port: 0,
1529 host: "127.0.0.1".to_string(),
1530 cors: Some("*".to_string()),
1531 token: Some("test-token".to_string()),
1532 allow_admin: false,
1533 workdir_root: None,
1534 no_remote_yolo: false,
1535 tls_cert: None,
1536 tls_key: None,
1537 };
1538 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1539 let handle = tokio::spawn(execute_with_shutdown(
1540 args,
1541 no_daemon_control(),
1542 Box::pin(std::future::pending()),
1543 Some(ready_tx),
1544 ));
1545 let addr = ready_rx
1546 .await
1547 .expect("server should report its bound address");
1548
1549 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1550 let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
1551 stream
1552 .write_all(
1553 b"OPTIONS /api/config HTTP/1.1\r\nHost: localhost\r\n\
1554 Origin: https://leviath.dev\r\n\
1555 Access-Control-Request-Method: GET\r\n\
1556 Access-Control-Request-Headers: authorization\r\n\
1557 Connection: close\r\n\r\n",
1558 )
1559 .await
1560 .unwrap();
1561 let mut resp = Vec::new();
1562 stream.read_to_end(&mut resp).await.unwrap();
1563 let lower = String::from_utf8_lossy(&resp).to_lowercase();
1564 assert!(
1565 lower.contains("access-control-allow-headers")
1566 && lower.contains("authorization"),
1567 "preflight must allow the Authorization header, got:\n{lower}"
1568 );
1569
1570 handle.abort();
1571 },
1572 )
1573 .await;
1574 }
1575
1576 #[tokio::test]
1577 async fn execute_with_specific_cors_origin_serves() {
1578 crate::config::with_isolated_config_path_async(
1579 "serve-mod-specific-cors",
1580 |_fake_dir| async move {
1581 let args = ServeArgs {
1582 port: 0,
1583 host: "127.0.0.1".to_string(),
1584 cors: Some("https://example.com".to_string()),
1585 token: Some("test-token".to_string()),
1586 allow_admin: false,
1587 workdir_root: None,
1588 no_remote_yolo: false,
1589 tls_cert: None,
1590 tls_key: None,
1591 };
1592 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1593 let handle = tokio::spawn(execute_with_shutdown(
1594 args,
1595 no_daemon_control(),
1596 Box::pin(std::future::pending()),
1597 Some(ready_tx),
1598 ));
1599 let addr = ready_rx
1600 .await
1601 .expect("server should report its bound address");
1602 assert!(tokio::net::TcpStream::connect(addr).await.is_ok());
1603
1604 handle.abort();
1605 },
1606 )
1607 .await;
1608 }
1609
1610 #[tokio::test]
1611 async fn execute_with_unparseable_addr_returns_err() {
1612 crate::config::with_isolated_config_path_async("serve-badaddr", |_fake_dir| async move {
1615 let args = ServeArgs {
1618 port: 0,
1619 host: "not a valid host".to_string(),
1620 cors: None,
1621 token: Some("test-token".to_string()),
1622 allow_admin: false,
1623 workdir_root: None,
1624 no_remote_yolo: false,
1625 tls_cert: None,
1626 tls_key: None,
1627 };
1628 let result = execute(args, no_daemon_control()).await;
1629 assert!(result.is_err());
1630 })
1631 .await;
1632 }
1633
1634 #[tokio::test]
1635 async fn test_agent_list_with_status_filter_full_router() {
1636 let app = test_app();
1637 let req = Request::builder()
1638 .uri("/api/agents?status=running,complete")
1639 .body(Body::empty())
1640 .unwrap();
1641 let resp = app.oneshot(req).await.unwrap();
1642 assert_eq!(resp.status(), StatusCode::OK);
1643 }
1644
1645 #[tokio::test]
1648 async fn execute_with_malformed_config_returns_err() {
1649 crate::config::with_isolated_config_path_async(
1650 "serve-mod-malformed",
1651 |_fake_dir| async move {
1652 std::fs::write(Config::config_path(), "not valid toml [[[").unwrap();
1654
1655 let args = ServeArgs {
1656 port: 0,
1657 host: "127.0.0.1".to_string(),
1658 cors: None,
1659 token: Some("test-token".to_string()),
1660 allow_admin: false,
1661 workdir_root: None,
1662 no_remote_yolo: false,
1663 tls_cert: None,
1664 tls_key: None,
1665 };
1666 let result = execute(args, no_daemon_control()).await;
1667 assert_execute_failed_on_malformed_config(&result);
1668 },
1669 )
1670 .await;
1671 }
1672
1673 #[tokio::test]
1677 async fn execute_with_bad_api_key_logs_warning_and_serves() {
1678 with_tracing(|| {});
1679 crate::config::with_isolated_config_path_async("serve-mod-badkey", |_fake_dir| async move {
1680 std::fs::write(
1682 Config::config_path(),
1683 "default_provider = \"anthropic\"\nagent_paths = []\n[providers]\nanthropic_api_key = \"bad-key-not-sk-ant\"\n",
1684 )
1685 .unwrap();
1686
1687 let args = ServeArgs {
1688 port: 0,
1689 host: "127.0.0.1".to_string(),
1690 cors: None,
1691 token: Some("test-token".to_string()),
1692 allow_admin: false,
1693 workdir_root: None,
1694 no_remote_yolo: false,
1695 tls_cert: None,
1696 tls_key: None,
1697 };
1698
1699 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
1700 let shutdown_fut = async move {
1701 let _ = shutdown_rx.await;
1702 };
1703 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1704
1705 let handle = tokio::spawn(execute_with_shutdown(
1706 args,
1707 no_daemon_control(),
1708 Box::pin(shutdown_fut),
1709 Some(ready_tx),
1710 ));
1711 let addr = ready_rx
1712 .await
1713 .expect("server should report its bound address");
1714 let connected = tokio::net::TcpStream::connect(addr).await.is_ok();
1715 assert_connected_with_bad_api_key(connected);
1716
1717 let _ = shutdown_tx.send(());
1719 let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
1720 .await
1721 .expect("timed out waiting for execute to return")
1722 .expect("task panicked");
1723 assert_execute_returned_ok_after_shutdown(&result);
1724 }).await;
1725 }
1726
1727 #[tokio::test]
1734 async fn execute_with_unbindable_address_returns_bind_error() {
1735 crate::config::with_isolated_config_path_async(
1738 "serve-unbindable",
1739 |_fake_dir| async move {
1740 let args = ServeArgs {
1741 port: 8080,
1742 host: "192.0.2.1".to_string(),
1743 cors: None,
1744 token: Some("test-token".to_string()),
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_execute_failed_on_port_in_use(&result);
1753 },
1754 )
1755 .await;
1756 }
1757
1758 #[tokio::test]
1759 async fn execute_refuses_to_start_without_a_token() {
1760 temp_env::async_with_vars([("LEVIATH_API_TOKEN", None::<&str>)], async {
1762 let args = ServeArgs {
1763 port: 0,
1764 host: "127.0.0.1".to_string(),
1765 cors: None,
1766 token: None,
1767 allow_admin: false,
1768 workdir_root: None,
1769 no_remote_yolo: false,
1770 tls_cert: None,
1771 tls_key: None,
1772 };
1773 let result = execute(args, no_daemon_control()).await;
1774 assert!(result.is_err(), "must refuse to start unauthenticated");
1775 })
1776 .await;
1777 }
1778
1779 #[tokio::test]
1782 async fn execute_with_shutdown_signal_returns_ok() {
1783 crate::config::with_isolated_config_path_async(
1784 "serve-mod-shutdown-signal",
1785 |_fake_dir| async move {
1786 let args = ServeArgs {
1787 port: 0,
1788 host: "127.0.0.1".to_string(),
1789 cors: None,
1790 token: Some("test-token".to_string()),
1791 allow_admin: false,
1792 workdir_root: None,
1793 no_remote_yolo: false,
1794 tls_cert: None,
1795 tls_key: None,
1796 };
1797
1798 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
1799 let shutdown_fut = async move {
1800 let _ = shutdown_rx.await;
1801 };
1802 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1803
1804 let handle = tokio::spawn(execute_with_shutdown(
1805 args,
1806 no_daemon_control(),
1807 Box::pin(shutdown_fut),
1808 Some(ready_tx),
1809 ));
1810 ready_rx
1811 .await
1812 .expect("server should report its bound address");
1813
1814 let _ = shutdown_tx.send(());
1816 let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
1817 .await
1818 .expect("timed out waiting for execute_with_shutdown to return")
1819 .expect("task panicked");
1820 assert_execute_with_shutdown_returned_ok(&result);
1821 },
1822 )
1823 .await;
1824 }
1825
1826 #[tokio::test]
1832 async fn execute_with_shutdown_no_ready_observer_returns_ok() {
1833 crate::config::with_isolated_config_path_async(
1834 "serve-mod-no-ready",
1835 |_fake_dir| async move {
1836 let args = ServeArgs {
1837 port: 0,
1838 host: "127.0.0.1".to_string(),
1839 cors: None,
1840 token: Some("test-token".to_string()),
1841 allow_admin: false,
1842 workdir_root: None,
1843 no_remote_yolo: false,
1844 tls_cert: None,
1845 tls_key: None,
1846 };
1847
1848 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
1849 let shutdown_fut = async move {
1850 let _ = shutdown_rx.await;
1851 };
1852
1853 let handle = tokio::spawn(execute_with_shutdown(
1854 args,
1855 no_daemon_control(),
1856 Box::pin(shutdown_fut),
1857 None,
1858 ));
1859 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1861 let _ = shutdown_tx.send(());
1862 let result = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
1863 .await
1864 .expect("timed out waiting for execute_with_shutdown to return")
1865 .expect("task panicked");
1866 assert_execute_with_shutdown_returned_ok(&result);
1867 },
1868 )
1869 .await;
1870 }
1871 #[tokio::test]
1875 async fn cors_is_off_by_default_explicit_when_asked_and_fatal_when_malformed() {
1876 crate::config::with_isolated_config_path_async("serve-mod-cors", |_fake_dir| async move {
1882 fn args_with(cors: Option<&str>) -> ServeArgs {
1883 ServeArgs {
1884 port: 0,
1885 host: "127.0.0.1".to_string(),
1886 cors: cors.map(str::to_string),
1887 token: Some("t".to_string()),
1888 allow_admin: false,
1889 workdir_root: None,
1890 no_remote_yolo: false,
1891 tls_cert: None,
1892 tls_key: None,
1893 }
1894 }
1895
1896 async fn starts(cors: Option<&str>) {
1899 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1900 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
1901 let server = tokio::spawn(execute_with_shutdown(
1902 args_with(cors),
1903 no_daemon_control(),
1904 Box::pin(async move {
1905 let _ = stop_rx.await;
1906 }),
1907 Some(ready_tx),
1908 ));
1909 ready_rx.await.expect("the server bound");
1915 let _ = stop_tx.send(());
1916 server.await.expect("join").expect("clean shutdown");
1917 }
1918
1919 starts(None).await;
1920 starts(Some("*")).await;
1921 starts(Some("https://ok.example")).await;
1922
1923 let err = execute_with_shutdown(
1926 args_with(Some("not a valid\nheader")),
1927 no_daemon_control(),
1928 Box::pin(std::future::pending()),
1929 None,
1930 )
1931 .await
1932 .expect_err("a malformed origin must refuse to start");
1933 assert!(
1936 err.to_string().contains("not a valid origin header"),
1937 "expected the CORS parse to be what refused, got: {err}"
1938 );
1939 })
1940 .await;
1941 }
1942
1943 #[tokio::test]
1946 async fn the_mcp_admin_routes_are_mounted_only_with_allow_admin() {
1947 crate::config::with_isolated_config_path_async("serve-mod-admin", |_fake_dir| async move {
1949 for allow_admin in [false, true] {
1950 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
1951 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
1952 let args = ServeArgs {
1953 port: 0,
1954 host: "127.0.0.1".to_string(),
1955 cors: None,
1956 token: Some("t".to_string()),
1957 allow_admin,
1958 workdir_root: None,
1959 no_remote_yolo: false,
1960 tls_cert: None,
1961 tls_key: None,
1962 };
1963 let server = tokio::spawn(execute_with_shutdown(
1964 args,
1965 no_daemon_control(),
1966 Box::pin(async move {
1967 let _ = stop_rx.await;
1968 }),
1969 Some(ready_tx),
1970 ));
1971 let addr = ready_rx.await.expect("bound");
1972
1973 let status = reqwest::Client::new()
1974 .post(format!("http://{addr}/api/mcp/servers"))
1975 .bearer_auth("t")
1976 .json(&serde_json::json!({}))
1977 .send()
1978 .await
1979 .expect("request")
1980 .status()
1981 .as_u16();
1982 match allow_admin {
1987 false => assert_eq!(status, 405, "the admin route must not be mounted"),
1988 true => assert_ne!(status, 405, "the admin route must be mounted"),
1989 }
1990
1991 let _ = stop_tx.send(());
1992 let _ = server.await;
1993 }
1994 })
1995 .await;
1996 }
1997
1998 #[tokio::test]
2009 async fn the_script_write_routes_are_mounted_only_with_allow_admin() {
2010 let home = tempfile::tempdir().expect("a temp dir");
2011 let root = home.path().to_path_buf();
2012 let mut vars = crate::config::config_isolation_vars(&root);
2013 vars.push(("LEVIATH_HOME", Some(root.clone().into_os_string())));
2014
2015 temp_env::async_with_vars(vars, async move {
2016 for allow_admin in [false, true] {
2017 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
2018 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
2019 let args = ServeArgs {
2020 port: 0,
2021 host: "127.0.0.1".to_string(),
2022 cors: None,
2023 token: Some("t".to_string()),
2024 allow_admin,
2025 workdir_root: None,
2026 no_remote_yolo: false,
2027 tls_cert: None,
2028 tls_key: None,
2029 };
2030 let server = tokio::spawn(execute_with_shutdown(
2031 args,
2032 no_daemon_control(),
2033 Box::pin(async move {
2034 let _ = stop_rx.await;
2035 }),
2036 Some(ready_tx),
2037 ));
2038 let addr = ready_rx.await.expect("bound");
2039 let client = reqwest::Client::new();
2040
2041 let write = client
2042 .put(format!("http://{addr}/api/scripts/tool/gate"))
2043 .bearer_auth("t")
2044 .json(&serde_json::json!({ "content": "// @tool gate\n1" }))
2045 .send()
2046 .await
2047 .expect("request")
2048 .status()
2049 .as_u16();
2050 match allow_admin {
2053 false => assert_eq!(write, 405, "the write route must not be mounted"),
2054 true => assert_ne!(write, 405, "the write route must be mounted"),
2055 }
2056
2057 let read = client
2060 .get(format!("http://{addr}/api/scripts"))
2061 .bearer_auth("t")
2062 .send()
2063 .await
2064 .expect("request")
2065 .status()
2066 .as_u16();
2067 assert_eq!(read, 200, "the read routes are never gated");
2068
2069 let _ = stop_tx.send(());
2070 let _ = server.await;
2071 }
2072 })
2073 .await;
2074 }
2075}