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::authoring::compile_source;
13use super::awl::{
14 bind_run, check, create_document, deploy_authoring, edit, emit, format, get_document,
15 get_layout, get_revision, get_run_status, list_documents, put_document, put_layout, scaffold,
16 worker_availability,
17};
18use super::awl_deployed::{get_deployed_document, list_deployed};
19use super::build::build_identity;
20use super::changelog::changelog;
21use super::children::list_children;
22use super::cluster_command::cluster_command;
23use super::deploy::{list_versions, route_version, unload_version, upload_package};
24use super::describe_live::describe_live;
25use super::dev_ui::{dev_register_mock, dev_replay_run, dev_trigger_run};
26use super::events::subscribe_events_socket;
27use super::history::{fetch_event, fetch_history};
28use super::intervene::{intervene, list_attempts};
29use super::managed_workers::{
30 list_managed_workers, restart_managed_worker, start_managed_worker, stop_managed_worker,
31};
32use super::outbox::list_dead_letters;
33use super::queues::list_unserved_queues;
34use super::schedules::{
35 create_schedule, delete_schedule, describe_schedule, list_schedules, pause_schedule,
36 resume_schedule, update_schedule,
37};
38use super::transcripts::{fetch_transcript, list_transcript_streams};
39use super::unrecoverable::list_unrecoverable_runs;
40use super::update_status::update_status;
41use super::whoami::whoami;
42use super::worker_deployments::{
43 delete_worker_deployment, get_worker_deployment, list_worker_deployments,
44 put_worker_deployment, set_worker_deployment_desired_state,
45};
46use super::workers::{drain_worker, stop_worker};
47use super::workflows::{
48 cancel_workflow, count_workflows, describe_workflow, get_workflows, list_namespace_records,
49 list_namespaces, post_list_workflows, post_namespace, query_workflow, rename_workflow,
50 reopen_workflow, set_namespace_placement, signal_workflow, start_workflow,
51};
52use crate::mcp::{McpRuntime, mcp_disabled_router, mcp_router};
53use crate::{ServerError, ServerState, observability, ops_console::assets};
54
55pub fn http_router(state: ServerState) -> Result<Router, ServerError> {
64 let ops_console = assets::ops_console_router(&state.runtime_config().ops_console)?;
65 let cors = cors_layer(&state.runtime_config().cors_allowed_origins)?;
66 let metrics = state.metrics().cloned();
67 let health = state.health().cloned();
68 let mut router = workflow_router(state.clone());
69 router = router.merge(mcp_family(&state)?.with_state(state));
74 if let Some(metrics) = metrics {
75 router = router.merge(Router::new().route(
76 "/metrics",
77 get(observability::metrics::metrics_handler).with_state(metrics),
78 ));
79 }
80 if let Some(health) = health {
81 router = router.merge(
82 Router::new()
83 .route("/health/live", get(observability::health::live))
84 .route(
85 "/health/ready",
86 get(observability::health::ready).with_state(health),
87 ),
88 );
89 }
90 let router = router.merge(ops_console);
91 Ok(match cors {
97 Some(cors) => router.layer(cors),
98 None => router,
99 })
100}
101
102fn cors_layer(allowed_origins: &[String]) -> Result<Option<CorsLayer>, ServerError> {
118 if allowed_origins.is_empty() {
119 return Ok(None);
120 }
121 let mut origins = Vec::with_capacity(allowed_origins.len());
122 for origin in allowed_origins {
123 let value = origin
124 .parse::<HeaderValue>()
125 .map_err(|source| ServerError::Config {
126 message: format!("invalid CORS origin `{origin}`: {source}"),
127 })?;
128 origins.push(value);
129 }
130 let layer = CorsLayer::new()
131 .allow_origin(origins)
132 .allow_methods([
133 Method::GET,
134 Method::POST,
135 Method::PUT,
136 Method::DELETE,
137 Method::OPTIONS,
138 ])
139 .allow_headers([
140 header::CONTENT_TYPE,
141 header::AUTHORIZATION,
142 HeaderName::from_static("x-aion-namespaces"),
143 HeaderName::from_static("x-aion-subject"),
144 ]);
145 Ok(Some(layer))
146}
147
148async fn deploy_disabled() -> StatusCode {
151 StatusCode::NOT_FOUND
152}
153
154async fn authoring_disabled() -> StatusCode {
158 StatusCode::NOT_FOUND
159}
160
161async fn dev_disabled() -> StatusCode {
165 StatusCode::NOT_FOUND
166}
167
168async fn deploy_surface_disabled() -> StatusCode {
173 StatusCode::NOT_FOUND
174}
175
176fn deployed_awl_router(deploy_enabled: bool) -> Router<ServerState> {
184 if deploy_enabled {
185 Router::new()
186 .route("/awl/deployed", get(list_deployed))
187 .route(
188 "/awl/deployed/{workflow_type}/{content_hash}",
189 get(get_deployed_document),
190 )
191 } else {
192 Router::new()
193 .route("/awl/deployed", any(deploy_surface_disabled))
194 .route("/awl/deployed/{*rest}", any(deploy_surface_disabled))
195 }
196}
197
198fn worker_deployment_router(deploy_enabled: bool) -> Router<ServerState> {
199 if deploy_enabled {
200 Router::new()
201 .route("/worker-deployments", get(list_worker_deployments))
202 .route(
203 "/worker-deployments/{name}",
204 get(get_worker_deployment)
205 .put(put_worker_deployment)
206 .delete(delete_worker_deployment),
207 )
208 .route(
209 "/worker-deployments/{name}/desired-state",
210 post(set_worker_deployment_desired_state),
211 )
212 } else {
213 Router::new()
214 .route("/worker-deployments", any(deploy_surface_disabled))
215 .route("/worker-deployments/{*rest}", any(deploy_surface_disabled))
216 }
217}
218
219fn managed_worker_router(deploy_enabled: bool) -> Router<ServerState> {
233 let read = Router::new().route("/workers/managed", get(list_managed_workers));
234 if deploy_enabled {
235 read.route("/workers/managed/{name}/start", post(start_managed_worker))
236 .route("/workers/managed/{name}/stop", post(stop_managed_worker))
237 .route(
238 "/workers/managed/{name}/restart",
239 post(restart_managed_worker),
240 )
241 } else {
242 read.route("/workers/managed/{*rest}", any(deploy_surface_disabled))
243 }
244}
245
246fn mcp_family(state: &ServerState) -> Result<Router<ServerState>, ServerError> {
262 if !state.runtime_config().mcp.enabled {
263 return Ok(mcp_disabled_router());
264 }
265 let runtime = McpRuntime::build(state.clone(), &state.runtime_config().mcp)?;
266 Ok(mcp_router(std::sync::Arc::new(runtime)))
267}
268
269fn dev_router(enabled: bool) -> Router<ServerState> {
271 if enabled {
272 Router::new()
273 .route("/dev/runs", post(dev_trigger_run))
274 .route("/dev/mocks", post(dev_register_mock))
275 .route("/dev/replay", post(dev_replay_run))
276 } else {
277 Router::new().route("/dev/{*rest}", any(dev_disabled))
278 }
279}
280
281pub fn workflow_router(state: ServerState) -> Router {
283 let deploy = if state.runtime_config().deploy.enabled {
291 Router::new()
292 .route(
293 "/deploy/packages",
294 post(upload_package).layer(DefaultBodyLimit::disable()),
295 )
296 .route("/deploy/versions", get(list_versions))
297 .route("/deploy/route", post(route_version))
298 .route("/deploy/unload", post(unload_version))
299 } else {
300 Router::new().route("/deploy/{*rest}", any(deploy_disabled))
301 };
302 let authoring = if state.runtime_config().authoring.gleam_path.is_some() {
309 Router::new().route("/authoring/compile", post(compile_source))
310 } else {
311 Router::new().route("/authoring/{*rest}", any(authoring_disabled))
312 };
313 let awl_documents = Router::new()
317 .route("/awl/documents", get(list_documents).post(create_document))
318 .route(
319 "/awl/documents/{*path}",
320 get(get_document).put(put_document),
321 )
322 .route("/awl/layout/{*path}", get(get_layout).put(put_layout));
323 let awl_deployed = deployed_awl_router(state.runtime_config().deploy.enabled);
324 let awl = Router::new()
325 .route("/awl/check", post(check))
326 .route("/awl/emit", post(emit))
327 .route("/awl/deploy", post(deploy_authoring))
328 .route("/awl/revisions/{hash}", get(get_revision))
329 .route("/awl/workers/availability", post(worker_availability))
330 .route("/awl/runs/{deployment_id}", get(get_run_status))
331 .route("/awl/runs/{deployment_id}/binding", post(bind_run))
332 .route("/awl/edit", post(edit))
333 .route("/awl/fmt", post(format))
334 .route("/awl/scaffold", post(scaffold))
335 .merge(awl_documents)
336 .merge(awl_deployed);
337 let dev = dev_router(state.runtime_config().dev.enabled);
342 deploy
343 .merge(authoring)
344 .merge(awl)
345 .merge(dev)
346 .merge(worker_deployment_router(
347 state.runtime_config().deploy.enabled,
348 ))
349 .route("/whoami", get(whoami))
350 .route("/build", get(build_identity))
351 .route("/update-status", get(update_status))
355 .route("/changelog", get(changelog))
358 .route("/assistant", get(assistant_descriptor))
362 .route("/assistant/document", get(assistant_document))
363 .route("/namespaces", get(list_namespaces).post(post_namespace))
364 .route("/namespaces/records", get(list_namespace_records))
365 .route(
366 "/namespaces/{name}/placement",
367 axum::routing::put(set_namespace_placement),
368 )
369 .route("/workflows", get(get_workflows))
370 .route("/workflows/count", get(count_workflows))
371 .route("/workflows/start", post(start_workflow))
372 .route("/workflows/signal", post(signal_workflow))
373 .route("/workflows/query", post(query_workflow))
374 .route("/workflows/cancel", post(cancel_workflow))
375 .route("/workflows/rename", post(rename_workflow))
376 .route("/workflows/reopen", post(reopen_workflow))
377 .route("/workflows/list", post(post_list_workflows))
378 .route("/workflows/describe", post(describe_workflow))
379 .route("/workflows/describe-live", post(describe_live))
380 .route("/workflows/children", post(list_children))
381 .route("/workflows/history", post(fetch_history))
382 .route("/workflows/event", post(fetch_event))
383 .route("/workflows/intervene", post(intervene))
384 .route("/workflows/attempts", post(list_attempts))
385 .route("/workflows/transcript", post(fetch_transcript))
386 .route("/workflows/transcripts", post(list_transcript_streams))
387 .route("/workflows/unrecoverable", get(list_unrecoverable_runs))
388 .route("/events/stream", get(subscribe_events_socket))
389 .route("/cluster/command", post(cluster_command))
390 .route("/outbox/dead-letters", post(list_dead_letters))
391 .route("/queues/unserved", get(list_unserved_queues))
392 .merge(managed_worker_router(state.runtime_config().deploy.enabled))
393 .route("/workers/{worker_id}/drain", post(drain_worker))
394 .route("/workers/{worker_id}/stop", post(stop_worker))
395 .route("/schedules", post(create_schedule).get(list_schedules))
396 .route(
397 "/schedules/{id}",
398 get(describe_schedule)
399 .put(update_schedule)
400 .delete(delete_schedule),
401 )
402 .route("/schedules/{id}/pause", post(pause_schedule))
403 .route("/schedules/{id}/resume", post(resume_schedule))
404 .with_state(state)
405}
406
407#[cfg(test)]
408#[path = "router_tests.rs"]
409mod tests;