1use axum::{
4 Router,
5 extract::DefaultBodyLimit,
6 http::{HeaderName, HeaderValue, Method, StatusCode, header},
7 routing::{any, get, post},
8};
9use tower_http::cors::CorsLayer;
10
11use super::assistant::{assistant_descriptor, assistant_document};
12use super::assistant_sessions::{
13 cancel_assistant_turn, create_assistant_session, current_assistant_session,
14 delete_assistant_session, list_assistant_sessions, push_assistant_context,
15 read_assistant_session, resume_assistant_session, submit_assistant_turn,
16};
17use super::assistant_socket::assistant_session_socket;
18use super::authoring::compile_source;
19use super::awl::{
20 bind_run, check, create_document, deploy_authoring, doc as awl_doc, edit, emit, format,
21 get_document, get_layout, get_revision, get_run_status, list_documents, put_document,
22 put_layout, scaffold, worker_availability,
23};
24use super::awl_deployed::{get_deployed_doc, get_deployed_document, list_deployed};
25use super::build::build_identity;
26use super::changelog::changelog;
27use super::children::list_children;
28use super::cluster_command::cluster_command;
29use super::deploy::{list_versions, route_version, unload_version, upload_package};
30use super::describe_live::describe_live;
31use super::dev_ui::{dev_register_mock, dev_replay_run, dev_trigger_run};
32use super::events::subscribe_events_socket;
33use super::history::{fetch_event, fetch_history};
34use super::intervene::{intervene, list_attempts};
35use super::managed_workers::{
36 list_managed_workers, restart_managed_worker, start_managed_worker, stop_managed_worker,
37};
38use super::outbox::list_dead_letters;
39use super::queues::list_unserved_queues;
40use super::schedules::{
41 create_schedule, delete_schedule, describe_schedule, list_schedules, pause_schedule,
42 resume_schedule, update_schedule,
43};
44use super::transcripts::{fetch_transcript, list_transcript_streams};
45use super::unrecoverable::list_unrecoverable_runs;
46use super::update_status::update_status;
47use super::whoami::whoami;
48use super::worker_deployments::{
49 delete_worker_deployment, get_worker_deployment, list_worker_deployments,
50 put_worker_deployment, set_worker_deployment_desired_state,
51};
52use super::workers::{drain_worker, stop_worker};
53use super::workflow_document::get_run_document;
54use super::workflows::{
55 cancel_workflow, describe_workflow, list_namespace_records, list_namespaces,
56 post_list_workflows, post_namespace, query_workflow, rename_workflow, reopen_workflow,
57 retire_workloop, set_namespace_placement, signal_workflow, start_workflow,
58};
59use crate::assistant::mcp::{AssistantMcpRuntime, assistant_mcp_router};
60use crate::mcp::{McpRuntime, mcp_disabled_router, mcp_router};
61use crate::{ServerError, ServerState, observability, ops_console::assets};
62
63pub fn http_router(state: ServerState) -> Result<Router, ServerError> {
72 let ops_console = assets::ops_console_router(&state.runtime_config().ops_console)?;
73 let cors = cors_layer(&state.runtime_config().cors_allowed_origins)?;
74 let metrics = state.metrics().cloned();
75 let health = state.health().cloned();
76 let mut router = workflow_router(state.clone());
77 let assistant_mcp = assistant_mcp_family(&state)?;
86 router = router
87 .merge(assistant_mcp.with_state(state.clone()))
88 .merge(mcp_family(&state)?.with_state(state));
89 if let Some(metrics) = metrics {
90 router = router.merge(Router::new().route(
91 "/metrics",
92 get(observability::metrics::metrics_handler).with_state(metrics),
93 ));
94 }
95 if let Some(health) = health {
96 router = router.merge(
97 Router::new()
98 .route("/health/live", get(observability::health::live))
99 .route(
100 "/health/ready",
101 get(observability::health::ready).with_state(health),
102 ),
103 );
104 }
105 let router = router.merge(ops_console);
106 Ok(match cors {
112 Some(cors) => router.layer(cors),
113 None => router,
114 })
115}
116
117fn cors_layer(allowed_origins: &[String]) -> Result<Option<CorsLayer>, ServerError> {
133 if allowed_origins.is_empty() {
134 return Ok(None);
135 }
136 let mut origins = Vec::with_capacity(allowed_origins.len());
137 for origin in allowed_origins {
138 let value = origin
139 .parse::<HeaderValue>()
140 .map_err(|source| ServerError::Config {
141 message: format!("invalid CORS origin `{origin}`: {source}"),
142 })?;
143 origins.push(value);
144 }
145 let layer = CorsLayer::new()
146 .allow_origin(origins)
147 .allow_methods([
148 Method::GET,
149 Method::POST,
150 Method::PUT,
151 Method::DELETE,
152 Method::OPTIONS,
153 ])
154 .allow_headers([
155 header::CONTENT_TYPE,
156 header::AUTHORIZATION,
157 HeaderName::from_static("x-aion-namespaces"),
158 HeaderName::from_static("x-aion-subject"),
159 ]);
160 Ok(Some(layer))
161}
162
163async fn deploy_disabled() -> StatusCode {
166 StatusCode::NOT_FOUND
167}
168
169async fn authoring_disabled() -> StatusCode {
173 StatusCode::NOT_FOUND
174}
175
176async fn dev_disabled() -> StatusCode {
180 StatusCode::NOT_FOUND
181}
182
183async fn deploy_surface_disabled() -> StatusCode {
188 StatusCode::NOT_FOUND
189}
190
191fn deployed_awl_router(deploy_enabled: bool) -> Router<ServerState> {
199 if deploy_enabled {
200 Router::new()
201 .route("/awl/deployed", get(list_deployed))
202 .route(
203 "/awl/deployed/{workflow_type}/{content_hash}",
204 get(get_deployed_document),
205 )
206 .route(
207 "/awl/deployed/{workflow_type}/{content_hash}/doc",
208 get(get_deployed_doc),
209 )
210 } else {
211 Router::new()
212 .route("/awl/deployed", any(deploy_surface_disabled))
213 .route("/awl/deployed/{*rest}", any(deploy_surface_disabled))
214 }
215}
216
217fn worker_deployment_router(deploy_enabled: bool) -> Router<ServerState> {
218 if deploy_enabled {
219 Router::new()
220 .route("/worker-deployments", get(list_worker_deployments))
221 .route(
222 "/worker-deployments/{name}",
223 get(get_worker_deployment)
224 .put(put_worker_deployment)
225 .delete(delete_worker_deployment),
226 )
227 .route(
228 "/worker-deployments/{name}/desired-state",
229 post(set_worker_deployment_desired_state),
230 )
231 } else {
232 Router::new()
233 .route("/worker-deployments", any(deploy_surface_disabled))
234 .route("/worker-deployments/{*rest}", any(deploy_surface_disabled))
235 }
236}
237
238fn managed_worker_router(deploy_enabled: bool) -> Router<ServerState> {
252 let read = Router::new().route("/workers/managed", get(list_managed_workers));
253 if deploy_enabled {
254 read.route("/workers/managed/{name}/start", post(start_managed_worker))
255 .route("/workers/managed/{name}/stop", post(stop_managed_worker))
256 .route(
257 "/workers/managed/{name}/restart",
258 post(restart_managed_worker),
259 )
260 } else {
261 read.route("/workers/managed/{*rest}", any(deploy_surface_disabled))
262 }
263}
264
265fn mcp_family(state: &ServerState) -> Result<Router<ServerState>, ServerError> {
281 if !state.runtime_config().mcp.enabled {
282 return Ok(mcp_disabled_router());
283 }
284 let runtime = McpRuntime::build(state.clone(), &state.runtime_config().mcp)?;
285 Ok(mcp_router(std::sync::Arc::new(runtime)))
286}
287
288fn assistant_mcp_family(state: &ServerState) -> Result<Router<ServerState>, ServerError> {
299 let runtime = AssistantMcpRuntime::build(state.clone(), &state.runtime_config().mcp)?;
300 Ok(assistant_mcp_router(std::sync::Arc::new(runtime)))
301}
302
303fn assistant_session_router() -> Router<ServerState> {
317 Router::new()
318 .route(
319 "/assistant/sessions",
320 get(list_assistant_sessions).post(create_assistant_session),
321 )
322 .route(
327 "/assistant/sessions/current",
328 get(current_assistant_session),
329 )
330 .route(
331 "/assistant/sessions/{id}",
332 get(read_assistant_session).delete(delete_assistant_session),
333 )
334 .route(
335 "/assistant/sessions/{id}/turns",
336 post(submit_assistant_turn),
337 )
338 .route(
339 "/assistant/sessions/{id}/context",
340 axum::routing::put(push_assistant_context),
341 )
342 .route(
343 "/assistant/sessions/{id}/cancel",
344 post(cancel_assistant_turn),
345 )
346 .route(
347 "/assistant/sessions/{id}/resume",
348 post(resume_assistant_session),
349 )
350 .route(
351 "/assistant/sessions/{id}/events",
352 get(assistant_session_socket),
353 )
354}
355
356fn dev_router(enabled: bool) -> Router<ServerState> {
358 if enabled {
359 Router::new()
360 .route("/dev/runs", post(dev_trigger_run))
361 .route("/dev/mocks", post(dev_register_mock))
362 .route("/dev/replay", post(dev_replay_run))
363 } else {
364 Router::new().route("/dev/{*rest}", any(dev_disabled))
365 }
366}
367
368fn deploy_routes(state: &ServerState) -> Router<ServerState> {
376 if state.runtime_config().deploy.enabled {
377 Router::new()
378 .route(
379 "/deploy/packages",
380 post(upload_package).layer(DefaultBodyLimit::disable()),
381 )
382 .route("/deploy/versions", get(list_versions))
383 .route("/deploy/route", post(route_version))
384 .route("/deploy/unload", post(unload_version))
385 } else {
386 Router::new().route("/deploy/{*rest}", any(deploy_disabled))
387 }
388}
389
390pub fn workflow_router(state: ServerState) -> Router {
393 let deploy = deploy_routes(&state);
401 let authoring = if state.runtime_config().authoring.gleam_path.is_some() {
408 Router::new().route("/authoring/compile", post(compile_source))
409 } else {
410 Router::new().route("/authoring/{*rest}", any(authoring_disabled))
411 };
412 let awl_documents = Router::new()
416 .route("/awl/documents", get(list_documents).post(create_document))
417 .route(
418 "/awl/documents/{*path}",
419 get(get_document).put(put_document),
420 )
421 .route("/awl/layout/{*path}", get(get_layout).put(put_layout));
422 let awl_deployed = deployed_awl_router(state.runtime_config().deploy.enabled);
423 let awl = Router::new()
424 .route("/awl/check", post(check))
425 .route("/awl/doc", post(awl_doc))
426 .route("/awl/emit", post(emit))
427 .route("/awl/deploy", post(deploy_authoring))
428 .route("/awl/revisions/{hash}", get(get_revision))
429 .route("/awl/workers/availability", post(worker_availability))
430 .route("/awl/runs/{deployment_id}", get(get_run_status))
431 .route("/awl/runs/{deployment_id}/binding", post(bind_run))
432 .route("/awl/edit", post(edit))
433 .route("/awl/fmt", post(format))
434 .route("/awl/scaffold", post(scaffold))
435 .merge(awl_documents)
436 .merge(awl_deployed);
437 let dev = dev_router(state.runtime_config().dev.enabled);
442 deploy
443 .merge(authoring)
444 .merge(awl)
445 .merge(dev)
446 .merge(worker_deployment_router(
447 state.runtime_config().deploy.enabled,
448 ))
449 .route("/whoami", get(whoami))
450 .route("/build", get(build_identity))
451 .route("/update-status", get(update_status))
455 .route("/changelog", get(changelog))
458 .route("/assistant", get(assistant_descriptor))
462 .route("/assistant/document", get(assistant_document))
463 .merge(assistant_session_router())
464 .route("/namespaces", get(list_namespaces).post(post_namespace))
465 .route("/namespaces/records", get(list_namespace_records))
466 .route(
467 "/namespaces/{name}/placement",
468 axum::routing::put(set_namespace_placement),
469 )
470 .route("/workflows/start", post(start_workflow))
471 .route("/workflows/signal", post(signal_workflow))
472 .route("/workflows/query", post(query_workflow))
473 .route("/workflows/cancel", post(cancel_workflow))
474 .route("/workflows/retire", post(retire_workloop))
475 .route("/workflows/rename", post(rename_workflow))
476 .route("/workflows/reopen", post(reopen_workflow))
477 .route("/workflows/list", post(post_list_workflows))
478 .route("/workflows/describe", post(describe_workflow))
479 .route("/workflows/describe-live", post(describe_live))
480 .route("/workflows/children", post(list_children))
481 .route("/workflows/history", post(fetch_history))
482 .route("/workflows/event", post(fetch_event))
483 .route("/workflows/intervene", post(intervene))
484 .route("/workflows/attempts", post(list_attempts))
485 .route("/workflows/transcript", post(fetch_transcript))
486 .route("/workflows/transcripts", post(list_transcript_streams))
487 .route("/workflows/unrecoverable", get(list_unrecoverable_runs))
488 .route(
491 "/workflows/{workflow_id}/document/{content_hash}",
492 get(get_run_document),
493 )
494 .route("/events/stream", get(subscribe_events_socket))
495 .route("/cluster/command", post(cluster_command))
496 .route("/outbox/dead-letters", post(list_dead_letters))
497 .route("/queues/unserved", get(list_unserved_queues))
498 .merge(managed_worker_router(state.runtime_config().deploy.enabled))
499 .route("/workers/{worker_id}/drain", post(drain_worker))
500 .route("/workers/{worker_id}/stop", post(stop_worker))
501 .route("/schedules", post(create_schedule).get(list_schedules))
502 .route(
503 "/schedules/{id}",
504 get(describe_schedule)
505 .put(update_schedule)
506 .delete(delete_schedule),
507 )
508 .route("/schedules/{id}/pause", post(pause_schedule))
509 .route("/schedules/{id}/resume", post(resume_schedule))
510 .with_state(state)
511}
512
513#[cfg(test)]
514#[path = "router_tests.rs"]
515mod tests;