mj_controller/server/api/
routes.rs1use super::*;
2
3pub(in crate::server) fn router(state: ServerState) -> Router<ServerState> {
4 Router::new()
5 .route(
6 "/sessions/{session_id}/native-agents/{child_id}/history",
7 get(native_agent_history),
8 )
9 .route("/events", get(events::events))
10 .route("/profiles/{profile_id}/config", get(profile_config))
11 .route(
12 "/sessions/{session_id}/config",
13 axum::routing::patch(set_config),
14 )
15 .route("/workspaces", get(list_workspaces).post(create_workspace))
16 .route("/sessions", get(list_sessions).post(start_session))
17 .route("/sessions/{session_id}", get(get_session))
18 .route(
19 "/sessions/{session_id}/subagents",
20 get(list_subagents).post(spawn_subagent),
21 )
22 .route("/sessions/{session_id}/prompt", post(prompt))
23 .route("/sessions/{session_id}/transcript", get(transcript))
24 .route("/sessions/{session_id}/usage", get(usage))
25 .route("/sessions/{session_id}/wait", post(wait))
26 .route("/sessions/{session_id}/suspend", post(suspend))
27 .route("/sessions/{session_id}/destroy", post(destroy))
28 .route("/sessions/{session_id}/resume", post(resume))
29 .route(
30 "/sessions/{session_id}/interrupt-turn",
31 post(interrupt_turn),
32 )
33 .route("/sessions/{session_id}/diff", get(diff))
34 .route(
35 "/sessions/{session_id}/files",
36 get(read_file)
37 .put(write_file)
38 .layer(axum::extract::DefaultBodyLimit::max(
39 mj_checkpoint::archive::MAX_SESSION_FILE_BYTES as usize,
40 )),
41 )
42 .route("/sessions/{session_id}/elicitations", get(elicitations))
43 .route(
44 "/sessions/{session_id}/elicitations/{elicitation_id}",
45 post(respond_elicitation),
46 )
47 .route("/sessions/{session_id}/export", post(export))
48 .route("/wiki/search", get(wiki_search))
49 .route("/wiki/sessions/{wiki_id}", get(wiki_session))
50 .route("/wiki/sessions/{wiki_id}/brief", get(wiki_brief))
51 .route("/wiki/sessions/{wiki_id}/hits", get(wiki_hits))
52 .route("/wiki/sessions/{wiki_id}/restore", post(wiki_restore))
53 .route_layer(axum::middleware::from_fn_with_state(
54 state,
55 require_api_auth,
56 ))
57 .layer(axum::middleware::from_fn(api_response_headers))
60}
61
62pub(super) async fn require_api_auth(
68 State(state): State<ServerState>,
69 request: HttpRequest<axum::body::Body>,
70 next: Next,
71) -> Result<Response, ApiFailure> {
72 let bearer = request
73 .headers()
74 .get(AUTHORIZATION)
75 .and_then(|value| value.to_str().ok())
76 .and_then(|value| value.strip_prefix("Bearer "))
77 .map(str::trim);
78 if bearer.is_some_and(|token| {
79 constant_time_eq(state.api_token.as_bytes(), token.as_bytes()) && !token.is_empty()
80 }) {
81 return Ok(next.run(request).await);
82 }
83 let cookie =
84 super::super::authenticated_viewer(&state, request.headers()).map_err(|error| {
85 if error.status == StatusCode::UNAUTHORIZED {
86 ApiFailure::new(
87 StatusCode::UNAUTHORIZED,
88 "supply the API token from the api-token file as a bearer token",
89 )
90 } else {
91 ApiFailure::from(error)
92 }
93 })?;
94 let mut response = next.run(request).await;
95 super::super::renew_viewer_response(&state, &cookie, &mut response)?;
96 Ok(response)
97}
98
99pub(super) async fn api_response_headers(
102 request: HttpRequest<axum::body::Body>,
103 next: Next,
104) -> Response {
105 let mut response = next.run(request).await;
106 let headers = response.headers_mut();
107 headers.insert(API_VERSION_HEADER, HeaderValue::from_static(API_VERSION));
108 headers.insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
109 response
110}
111
112#[derive(Debug, Default, Clone, Serialize, Deserialize)]
117#[serde(deny_unknown_fields)]
118pub struct SessionListQuery {
119 pub workspace_id: Option<String>,
120}
121
122#[derive(Debug, Default, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub(super) struct ProfileConfigQuery {
125 pub(super) model: Option<String>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[serde(deny_unknown_fields)]
130pub struct SetConfigRequest {
131 pub key: String,
132 pub value: String,
133}