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