1pub(crate) mod cli_handlers;
5mod files;
6mod app_api;
7mod agent_handlers;
8mod editor_handlers;
9mod identity_handlers;
10pub mod install_handlers;
11mod lsp_handlers;
12mod messagebus;
13mod reactive;
14pub(crate) mod service;
15mod shell_handlers;
16mod tool_handlers;
17mod voice;
18pub(crate) mod wave_obj_bridge;
19mod websocket;
20mod drone_handlers;
21mod muxbus_handlers;
22mod native_memory_handlers;
23
24#[cfg(test)]
25pub(crate) mod tests;
26
27use std::sync::Arc;
28
29use axum::{
30 body::Body,
31 extract::{Query, Request, State},
32 http::{header, Method, StatusCode},
33 middleware::{self, Next},
34 response::{IntoResponse, Json, Response},
35 routing::{get, post},
36 Router,
37};
38use serde_json::json;
39use tower_http::cors::{Any, CorsLayer};
40
41use crate::backend::eventbus::EventBus;
42use crate::backend::lan_discovery::LanDiscoveryController;
43use crate::backend::lsp::LspSupervisor;
44use crate::backend::messagebus::MessageBus;
45use crate::backend::reactive::{Poller, ReactiveHandler};
46use crate::backend::storage::filestore::FileStore;
47use crate::backend::storage::store::Store;
48use crate::backend::history::HistoryService;
49use crate::backend::subagent_watcher::SubagentWatcher;
50use crate::backend::wconfig;
51use crate::backend::wps::Broker;
52use agentmux_common::api_types::{
53 PaneTitleRequest, ShellCreateRequest, ShellCreateResponse, ShellStopRequest,
54 TabActivateRequest, TabNameRequest, TabNewRequest, WindowFocusRequest, WindowNameRequest,
55 WorkspaceNameRequest, WpsPublishRequest,
56};
57
58#[derive(Clone)]
61pub struct AppState {
62 pub auth_key: String,
63 pub version: String,
64 pub app_path: String,
65 pub wstore: Arc<Store>,
66 pub filestore: Arc<FileStore>,
67 pub global_transcript_store: Option<Arc<FileStore>>,
74 pub event_bus: Arc<EventBus>,
75 pub broker: Arc<Broker>,
76 pub reactive_handler: &'static ReactiveHandler,
77 pub poller: Arc<Poller>,
78 pub config_watcher: Arc<wconfig::ConfigWatcher>,
79 pub messagebus: Arc<MessageBus>,
80 pub subagent_watcher: Arc<SubagentWatcher>,
81 pub history_service: Arc<HistoryService>,
82 pub process_tracker: Arc<crate::backend::process_tracker::registry::AgentProcessRegistry>,
88 pub lan_discovery: Arc<LanDiscoveryController>,
93 pub lsp_supervisor: Arc<LspSupervisor>,
98 pub local_web_url: String,
101 pub http_client: reqwest::Client,
103 pub srv_state: std::sync::Arc<tokio::sync::Mutex<crate::state::State>>,
110 pub srv_events_tx: tokio::sync::broadcast::Sender<agentmux_common::ipc::Event>,
115 pub saga_id_alloc: std::sync::Arc<std::sync::atomic::AtomicU64>,
122 pub saga_log: std::sync::Arc<crate::sagas::log::SagaLog>,
129 pub auth_session_manager: std::sync::Arc<crate::identity::auth_session::AuthSessionManager>,
133
134 pub install_sessions: std::sync::Arc<crate::server::install_handlers::InstallSessionRegistry>,
140 pub container_manager: Option<std::sync::Arc<crate::backend::container::ContainerManager>>,
144 pub shell_sessions: std::sync::Arc<crate::backend::shell_node::ShellSessionRegistry>,
148}
149
150pub fn build_router(state: AppState) -> Router {
152 use tower_http::cors::AllowOrigin;
169 let cors = CorsLayer::new()
170 .allow_origin(AllowOrigin::predicate(|origin, _req| {
171 let Ok(s) = origin.to_str() else { return false };
172 s.starts_with("http://127.0.0.1:")
173 || s.starts_with("http://localhost:")
174 || s == "http://127.0.0.1"
175 || s == "http://localhost"
176 }))
177 .allow_methods(Any)
178 .allow_headers(vec![
179 header::CONTENT_TYPE,
180 header::AUTHORIZATION,
181 header::ACCEPT,
182 "X-Session-Id".parse().unwrap(),
183 "X-AuthKey".parse().unwrap(),
184 "X-Requested-With".parse().unwrap(),
185 "x-vercel-ai-ui-message-stream".parse().unwrap(),
186 ]);
187
188 let reactive_routes = Router::new()
196 .route("/agentmux/reactive/inject", post(reactive::handle_reactive_inject))
197 .route("/agentmux/reactive/agents", get(reactive::handle_reactive_agents))
198 .route("/agentmux/reactive/agent", get(reactive::handle_reactive_agent))
199 .route("/agentmux/reactive/audit", get(reactive::handle_reactive_audit))
200 .route("/agentmux/reactive/register", post(reactive::handle_reactive_register))
201 .route(
202 "/agentmux/reactive/unregister",
203 post(reactive::handle_reactive_unregister),
204 )
205 .route(
206 "/agentmux/reactive/poller/stats",
207 get(reactive::handle_reactive_poller_stats),
208 )
209 .route(
210 "/agentmux/reactive/poller/config",
211 post(reactive::handle_reactive_poller_config),
212 )
213 .route(
214 "/agentmux/reactive/poller/status",
215 get(reactive::handle_reactive_poller_status),
216 );
217
218 let bus_routes = Router::new()
220 .route("/api/bus/register", post(messagebus::handle_register))
221 .route("/api/bus/send", post(messagebus::handle_send))
222 .route("/api/bus/inject", post(messagebus::handle_inject))
223 .route("/api/bus/broadcast", post(messagebus::handle_broadcast))
224 .route("/api/bus/messages", get(messagebus::handle_read_messages))
225 .route("/api/bus/messages/delete", post(messagebus::handle_delete_messages))
226 .route("/api/bus/agents", get(messagebus::handle_list_agents));
227
228 let authed_routes = Router::new()
229 .route("/ws", get(websocket::handle_ws))
230 .route("/agentmux/service", post(service::handle_service))
231 .route("/agentmux/file", get(files::handle_wave_file))
232 .route("/agentmux/stream-file", get(stub_501))
233 .route("/agentmux/stream-file/*path", get(stub_501))
234 .route("/agentmux/stream-local-file", get(stub_501))
235 .route("/api/post-chat-message", get(stub_501).post(stub_501))
236 .route("/docsite/*path", get(files::handle_docsite))
237 .route("/schema/*path", get(files::handle_schema))
238 .route("/api/lan-instances", get(handle_lan_instances))
239 .route("/agentmux/discovery", get(handle_discovery))
240 .route("/agentmux/diag/sagas", get(handle_diag_sagas))
241 .route("/agentmux/wps/publish", post(handle_wps_publish))
247 .route("/api/v1/shell/create", post(handle_shell_create))
252 .route("/api/v1/shell/stop", post(handle_shell_stop))
255 .route("/api/v1/pane/open", post(handle_pane_open))
260 .route("/api/v1/voice/transcribe", post(voice::handle_voice_transcribe))
265 .route("/api/v1/self", get(handle_self))
270 .route("/api/v1/window/name", post(handle_window_name))
271 .route("/api/v1/tab/name", post(handle_tab_name))
275 .route("/api/v1/pane/title", post(handle_pane_title))
276 .route("/api/v1/workspace/name", post(handle_workspace_name))
277 .route("/api/v1/layout", get(handle_layout))
281 .route("/api/v1/windows", get(handle_list_windows))
282 .route("/api/v1/workspaces", get(handle_list_workspaces))
283 .route("/api/v1/tabs", get(handle_list_tabs))
284 .route("/api/v1/tab/activate", post(handle_tab_activate))
288 .route("/api/v1/tab/new", post(handle_tab_new))
289 .route("/api/v1/window/focus", post(handle_window_focus))
290 .merge(bus_routes)
291 .merge(reactive_routes)
292 .route_layer(middleware::from_fn_with_state(
293 state.clone(),
294 auth_middleware,
295 ));
296
297 let health = Router::new().route("/", get(health_handler));
299
300 Router::new()
301 .merge(health)
302 .merge(authed_routes)
303 .layer(cors)
304 .with_state(state)
305}
306
307async fn health_handler(State(state): State<AppState>) -> Json<serde_json::Value> {
310 Json(json!({
311 "status": "ok",
312 "version": state.version,
313 }))
314}
315
316async fn handle_diag_sagas(State(state): State<AppState>) -> Json<serde_json::Value> {
346 const LIMIT: u32 = 50;
347 let recent = match state.saga_log.snapshot_recent(LIMIT) {
348 Ok(rows) => rows,
349 Err(e) => {
350 return Json(json!({
351 "error": format!("snapshot_recent failed: {}", e),
352 }));
353 }
354 };
355 let in_flight = match state.saga_log.unresolved_sagas() {
356 Ok(rows) => rows.len(),
357 Err(e) => {
358 tracing::warn!("[diag/sagas] unresolved_sagas failed: {}", e);
359 0
360 }
361 };
362 let recently_failed = recent
363 .iter()
364 .filter(|s| s.state == "failed" || s.state == "failed_compensation")
365 .count();
366 Json(json!({
367 "recent": recent,
368 "in_flight_count": in_flight,
369 "recently_failed_count": recently_failed,
370 "total_returned": recent.len(),
371 }))
372}
373
374async fn handle_lan_instances(State(state): State<AppState>) -> Json<serde_json::Value> {
375 Json(json!(state.lan_discovery.get_instances()))
376}
377
378async fn handle_discovery(State(state): State<AppState>) -> Json<serde_json::Value> {
391 let reachable = state.reactive_handler.list_agents();
395 let reachable_block: std::collections::HashMap<String, String> = reachable
396 .iter()
397 .map(|a| (a.agent_id.to_lowercase(), a.block_id.clone()))
398 .collect();
399
400 let instances = state.wstore.instance_list(None, None).unwrap_or_default();
406 let agents: Vec<serde_json::Value> = instances
407 .into_iter()
408 .map(|i| {
409 let live_block = if i.instance_name.is_empty() {
410 None
411 } else {
412 reachable_block.get(&i.instance_name.to_lowercase()).cloned()
413 };
414 json!({
415 "name": i.instance_name,
416 "id": i.id,
417 "definition_id": i.definition_id,
418 "working_directory": i.working_directory,
419 "addressable": live_block.is_some(),
420 "block_id": live_block,
421 })
422 })
423 .collect();
424
425 let wan_agents = crate::muxbus::cloud_subscriber::get_global_subscriber()
426 .map(|s| s.subscribed_agents())
427 .unwrap_or_default();
428
429 let lan = state.lan_discovery.get_instances();
430 let version = state.version.clone();
431 let local_url = state.local_web_url.clone();
432
433 Json(json!({
434 "host": {
435 "version": version,
436 "local_url": local_url,
437 "addressable": reachable,
438 "agents": agents,
439 },
440 "lan": lan,
441 "wan": { "subscribed_agents": wan_agents },
442 }))
443}
444
445async fn stub_501() -> impl IntoResponse {
446 (
447 StatusCode::NOT_IMPLEMENTED,
448 Json(json!({"error": "not implemented"})),
449 )
450}
451
452async fn handle_wps_publish(
457 State(state): State<AppState>,
458 Json(req): Json<WpsPublishRequest>,
459) -> impl IntoResponse {
460 let event = crate::backend::wps::WaveEvent {
461 event: req.event,
462 scopes: req.scopes,
463 sender: String::new(),
464 persist: req.persist,
465 data: Some(req.data),
466 };
467 state.broker.publish(event);
468 (StatusCode::OK, Json(json!({"ok": true})))
469}
470
471async fn handle_shell_create(
477 State(state): State<AppState>,
478 Json(req): Json<ShellCreateRequest>,
479) -> impl IntoResponse {
480 let shell_id = uuid::Uuid::new_v4().to_string();
481 let title = req.title.as_deref().unwrap_or(&req.cmd).to_string();
482 let now_ms = std::time::SystemTime::now()
483 .duration_since(std::time::UNIX_EPOCH)
484 .unwrap_or_default()
485 .as_millis() as u64;
486
487 let agent_block = state.wstore
489 .get::<crate::backend::obj::Block>(&req.agent_block_id)
490 .ok()
491 .flatten();
492
493 let effective_cwd = req.cwd.or_else(|| {
498 agent_block.as_ref().and_then(|block| {
499 let cwd = crate::backend::obj::meta_get_string(&block.meta, "cmd:cwd", "");
500 if cwd.is_empty() { None } else { Some(cwd.to_string()) }
501 })
502 });
503
504 let effective_cwd =
509 effective_cwd.and_then(|c| crate::backend::base::normalize_working_dir(&c));
510
511 let mut effective_env: std::collections::HashMap<String, String> = agent_block
525 .as_ref()
526 .and_then(|block| match block.meta.get("cmd:env") {
527 Some(serde_json::Value::Object(obj)) => Some(
528 obj.iter()
529 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
530 .collect(),
531 ),
532 _ => None,
533 })
534 .unwrap_or_default();
535 if let Some(req_env) = req.env {
536 effective_env.extend(req_env);
537 }
538
539 tracing::info!(
540 block_id = %req.agent_block_id,
541 shell_id = %shell_id,
542 cmd = %req.cmd,
543 cwd = ?effective_cwd,
544 "shell.create"
545 );
546
547 state.broker.publish(crate::backend::wps::WaveEvent {
556 event: crate::backend::wps::EVENT_SHELL_NODE_CREATE.to_string(),
557 scopes: vec![format!("block:{}", req.agent_block_id)],
558 sender: String::new(),
559 persist: 64,
560 data: Some(json!({
561 "shell_id": shell_id,
562 "cmd": req.cmd,
563 "cwd": effective_cwd,
564 "title": title,
565 "timestamp": now_ms,
566 })),
567 });
568
569 let runner = crate::backend::shell_node::ShellNodeRunner {
571 shell_id: shell_id.clone(),
572 block_id: req.agent_block_id,
573 cmd: req.cmd,
574 cwd: effective_cwd,
575 extra_env: effective_env,
576 broker: Arc::clone(&state.broker),
577 registry: Arc::clone(&state.shell_sessions),
578 };
579 tokio::spawn(runner.run());
580
581 (StatusCode::OK, Json(ShellCreateResponse { shell_id }))
582}
583
584async fn handle_shell_stop(
591 State(state): State<AppState>,
592 Json(req): Json<ShellStopRequest>,
593) -> impl IntoResponse {
594 let stopped = state.shell_sessions.stop(&req.shell_id);
595 tracing::info!(shell_id = %req.shell_id, stopped, "shell.stop");
596 (StatusCode::OK, Json(json!({ "stopped": stopped })))
597}
598
599async fn handle_pane_open(
606 State(state): State<AppState>,
607 Json(req): Json<crate::backend::rpc_types::CommandPaneOpenData>,
608) -> impl IntoResponse {
609 match app_api::open_pane(&state, req).await {
610 Ok(result) => (StatusCode::OK, Json(json!(result))).into_response(),
611 Err(e) => {
612 let status = if e.starts_with("MISSING_ARG") || e.starts_with("INVALID_VIEW") {
615 StatusCode::BAD_REQUEST
616 } else {
617 StatusCode::INTERNAL_SERVER_ERROR
618 };
619 (status, Json(json!({ "error": e }))).into_response()
620 }
621 }
622}
623
624#[derive(serde::Deserialize)]
625struct SelfQuery {
626 block_id: Option<String>,
628}
629
630async fn handle_self(
636 State(state): State<AppState>,
637 Query(q): Query<SelfQuery>,
638) -> impl IntoResponse {
639 let block_id = q.block_id.unwrap_or_default();
640 if block_id.is_empty() {
641 return (StatusCode::BAD_REQUEST, Json(json!({ "error": "missing block_id" }))).into_response();
642 }
643 match service::resolve_agent_context(&state.wstore, &block_id) {
644 Ok(ctx) => Json(serde_json::to_value(&ctx).unwrap_or_default()).into_response(),
645 Err(e) => (StatusCode::NOT_FOUND, Json(json!({ "error": e }))).into_response(),
646 }
647}
648
649async fn handle_window_name(
656 State(state): State<AppState>,
657 Json(req): Json<WindowNameRequest>,
658) -> impl IntoResponse {
659 let name: String = req.name.trim().chars().take(64).collect();
661 if name.is_empty() {
662 return (StatusCode::BAD_REQUEST, Json(json!({ "error": "name must not be empty" }))).into_response();
663 }
664
665 let window_id = match req.window_id.filter(|w| !w.is_empty()) {
666 Some(w) => w,
667 None => {
668 let block_id = req.block_id.unwrap_or_default();
669 if block_id.is_empty() {
670 return (StatusCode::BAD_REQUEST, Json(json!({ "error": "provide window_id or block_id" }))).into_response();
671 }
672 match service::resolve_agent_context(&state.wstore, &block_id) {
673 Ok(ctx) => match ctx.window_id {
674 Some(w) => w,
675 None => {
676 return (
677 StatusCode::NOT_FOUND,
678 Json(json!({ "error": "no live window for this agent (tab not attached to a window)" })),
679 )
680 .into_response()
681 }
682 },
683 Err(e) => return (StatusCode::NOT_FOUND, Json(json!({ "error": e }))).into_response(),
684 }
685 }
686 };
687
688 let call = crate::backend::service::WebCallType {
689 service: "object".to_string(),
690 method: "UpdateObjectMeta".to_string(),
691 uicontext: None,
692 args: vec![
693 json!(format!("window:{window_id}")),
694 json!({ "window:displayname": name }),
695 ],
696 };
697 let result = service::run_service_call(&state, &call).await;
698 if let Some(err) = result.error {
699 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": err }))).into_response();
700 }
701 Json(json!({ "success": true, "window_id": window_id, "name": name })).into_response()
702}
703
704fn clean_name(raw: &str, max: usize) -> Option<String> {
706 let n: String = raw.trim().chars().take(max).collect();
707 if n.is_empty() {
708 None
709 } else {
710 Some(n)
711 }
712}
713
714async fn handle_tab_name(
717 State(state): State<AppState>,
718 Json(req): Json<TabNameRequest>,
719) -> impl IntoResponse {
720 let name = match clean_name(&req.name, 128) {
721 Some(n) => n,
722 None => return (StatusCode::BAD_REQUEST, Json(json!({ "error": "name must not be empty" }))).into_response(),
723 };
724 let tab_id = match req.tab_id.filter(|t| !t.is_empty()) {
725 Some(t) => t,
726 None => match resolve_own(&state, req.block_id, |c| Some(c.tab_id.clone())) {
727 Ok(t) => t,
728 Err(resp) => return resp,
729 },
730 };
731 let call = crate::backend::service::WebCallType {
732 service: "object".to_string(),
733 method: "UpdateTabName".to_string(),
734 uicontext: None,
735 args: vec![json!(tab_id), json!(name)],
736 };
737 finish_name_call(&state, call, json!({ "success": true, "tab_id": tab_id, "name": name })).await
738}
739
740async fn handle_pane_title(
744 State(state): State<AppState>,
745 Json(req): Json<PaneTitleRequest>,
746) -> impl IntoResponse {
747 let title = match clean_name(&req.title, 128) {
748 Some(t) => t,
749 None => return (StatusCode::BAD_REQUEST, Json(json!({ "error": "title must not be empty" }))).into_response(),
750 };
751 let block_id = match req.block_id.filter(|b| !b.is_empty()) {
752 Some(b) => b,
753 None => return (StatusCode::BAD_REQUEST, Json(json!({ "error": "missing block_id" }))).into_response(),
754 };
755 let call = crate::backend::service::WebCallType {
756 service: "object".to_string(),
757 method: "UpdateObjectMeta".to_string(),
758 uicontext: None,
759 args: vec![
760 json!(format!("block:{block_id}")),
761 json!({ "frame:title": title }),
762 ],
763 };
764 finish_name_call(&state, call, json!({ "success": true, "block_id": block_id, "title": title })).await
765}
766
767async fn handle_workspace_name(
771 State(state): State<AppState>,
772 Json(req): Json<WorkspaceNameRequest>,
773) -> impl IntoResponse {
774 let name = match clean_name(&req.name, 128) {
775 Some(n) => n,
776 None => return (StatusCode::BAD_REQUEST, Json(json!({ "error": "name must not be empty" }))).into_response(),
777 };
778 let workspace_id = match req.workspace_id.filter(|w| !w.is_empty()) {
779 Some(w) => w,
780 None => match resolve_own(&state, req.block_id, |c| c.workspace_id.clone()) {
781 Ok(w) => w,
782 Err(resp) => return resp,
783 },
784 };
785 let call = crate::backend::service::WebCallType {
786 service: "workspace".to_string(),
787 method: "UpdateWorkspace".to_string(),
788 uicontext: None,
789 args: vec![json!(workspace_id), json!(name)],
790 };
791 finish_name_call(&state, call, json!({ "success": true, "workspace_id": workspace_id, "name": name })).await
792}
793
794fn resolve_own(
798 state: &AppState,
799 block_id: Option<String>,
800 pick: impl Fn(&service::AgentContext) -> Option<String>,
801) -> Result<String, Response> {
802 let block_id = block_id.unwrap_or_default();
803 if block_id.is_empty() {
804 return Err((StatusCode::BAD_REQUEST, Json(json!({ "error": "provide an explicit target id or block_id" }))).into_response());
805 }
806 let ctx = service::resolve_agent_context(&state.wstore, &block_id)
807 .map_err(|e| (StatusCode::NOT_FOUND, Json(json!({ "error": e }))).into_response())?;
808 pick(&ctx).filter(|s| !s.is_empty()).ok_or_else(|| {
809 (
810 StatusCode::NOT_FOUND,
811 Json(json!({ "error": "no such target resolved for this agent" })),
812 )
813 .into_response()
814 })
815}
816
817async fn finish_name_call(
819 state: &AppState,
820 call: crate::backend::service::WebCallType,
821 ok_body: serde_json::Value,
822) -> Response {
823 let result = service::run_service_call(state, &call).await;
824 if let Some(err) = result.error {
825 return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": err }))).into_response();
826 }
827 Json(ok_body).into_response()
828}
829
830async fn handle_layout(State(state): State<AppState>) -> impl IntoResponse {
832 Json(service::agent_layout(&state.wstore))
833}
834
835async fn handle_list_windows(State(state): State<AppState>) -> impl IntoResponse {
837 Json(service::agent_windows(&state.wstore))
838}
839
840async fn handle_list_workspaces(State(state): State<AppState>) -> impl IntoResponse {
842 Json(service::agent_workspaces(&state.wstore))
843}
844
845#[derive(serde::Deserialize)]
846struct ListTabsQuery {
847 #[serde(default)]
849 workspace_id: Option<String>,
850 #[serde(default)]
853 block_id: Option<String>,
854}
855
856async fn handle_list_tabs(
859 State(state): State<AppState>,
860 Query(q): Query<ListTabsQuery>,
861) -> impl IntoResponse {
862 let ws_id = q.workspace_id.filter(|w| !w.is_empty()).or_else(|| {
863 q.block_id
864 .filter(|b| !b.is_empty())
865 .and_then(|b| service::resolve_agent_context(&state.wstore, &b).ok())
866 .and_then(|ctx| ctx.workspace_id)
867 });
868 Json(service::agent_tabs(&state.wstore, ws_id.as_deref()))
869}
870
871async fn handle_tab_activate(
874 State(state): State<AppState>,
875 Json(req): Json<TabActivateRequest>,
876) -> impl IntoResponse {
877 let tab_id = req.tab_id.trim().to_string();
878 if tab_id.is_empty() {
879 return (StatusCode::BAD_REQUEST, Json(json!({ "error": "missing tab_id" }))).into_response();
880 }
881 let ws_id = match service::workspace_id_for_tab(&state.wstore, &tab_id) {
882 Some(w) => w,
883 None => return (StatusCode::NOT_FOUND, Json(json!({ "error": format!("no workspace owns tab {tab_id}") }))).into_response(),
884 };
885 let call = crate::backend::service::WebCallType {
886 service: "workspace".to_string(),
887 method: "SetActiveTab".to_string(),
888 uicontext: None,
889 args: vec![json!(ws_id), json!(tab_id)],
890 };
891 finish_name_call(&state, call, json!({ "success": true, "tab_id": tab_id })).await
892}
893
894async fn handle_tab_new(
898 State(state): State<AppState>,
899 Json(req): Json<TabNewRequest>,
900) -> impl IntoResponse {
901 let name = req.name.map(|n| n.trim().chars().take(128).collect::<String>()).unwrap_or_default();
902 let workspace_id = match req.workspace_id.filter(|w| !w.is_empty()) {
903 Some(w) => w,
904 None => match resolve_own(&state, req.block_id, |c| c.workspace_id.clone()) {
905 Ok(w) => w,
906 Err(resp) => return resp,
907 },
908 };
909 let call = crate::backend::service::WebCallType {
910 service: "workspace".to_string(),
911 method: "CreateTab".to_string(),
912 uicontext: None,
913 args: vec![json!(workspace_id), json!(name), json!(true)],
915 };
916 finish_name_call(&state, call, json!({ "success": true, "workspace_id": workspace_id })).await
917}
918
919async fn handle_window_focus(
922 State(state): State<AppState>,
923 Json(req): Json<WindowFocusRequest>,
924) -> impl IntoResponse {
925 let window_id = match req.window_id.filter(|w| !w.is_empty()) {
926 Some(w) => w,
927 None => match resolve_own(&state, req.block_id, |c| c.window_id.clone()) {
928 Ok(w) => w,
929 Err(resp) => return resp,
930 },
931 };
932 let call = crate::backend::service::WebCallType {
933 service: "client".to_string(),
934 method: "FocusWindow".to_string(),
935 uicontext: None,
936 args: vec![json!(window_id)],
937 };
938 finish_name_call(&state, call, json!({ "success": true, "window_id": window_id })).await
939}
940
941async fn auth_middleware(
945 State(state): State<AppState>,
946 req: Request<Body>,
947 next: Next,
948) -> Response {
949 if req.method() == Method::OPTIONS {
950 return next.run(req).await;
951 }
952
953 let auth_key = req
954 .headers()
955 .get("X-AuthKey")
956 .and_then(|v| v.to_str().ok())
957 .map(|s| s.to_string());
958
959 let auth_key = auth_key.or_else(|| {
967 if req.uri().path() != "/ws" {
968 return None;
969 }
970 req.uri().query().and_then(|q| {
971 q.split('&')
972 .filter_map(|pair| pair.split_once('='))
973 .find(|(k, _)| *k == "authkey")
974 .map(|(_, v)| v.to_string())
975 })
976 });
977
978 match auth_key {
979 Some(key) if key == state.auth_key => next.run(req).await,
980 _ => (
981 StatusCode::UNAUTHORIZED,
982 Json(json!({"error": "unauthorized"})),
983 )
984 .into_response(),
985 }
986}