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("/github-token", get(github_token))
11 .route("/profiles/{profile_id}/config", get(profile_config))
12 .route(
13 "/profiles/{profile_id}/subagent-options",
14 get(subagent_options),
15 )
16 .route("/options", get(options))
17 .route("/usage/report", get(usage_report))
18 .route(
19 "/sessions/{session_id}/config",
20 axum::routing::patch(set_config),
21 )
22 .route("/workspaces", get(list_workspaces).post(create_workspace))
23 .route("/sessions", get(list_sessions).post(start_session))
24 .route("/sessions/{session_id}", get(get_session))
25 .route(
26 "/sessions/{session_id}/subagents",
27 get(list_subagents).post(spawn_subagent),
28 )
29 .route("/sessions/{session_id}/prompt", post(prompt))
30 .route(
31 "/sessions/{session_id}/message",
32 post(mailbox::send_message),
33 )
34 .route(
35 "/sessions/{session_id}/queued-prompts/clear",
36 post(clear_queue),
37 )
38 .route(
39 "/sessions/{session_id}/events",
40 post(mailbox::enqueue_event),
41 )
42 .route("/sessions/{session_id}/transcript", get(transcript))
43 .route("/sessions/{session_id}/history", get(transcript_history))
44 .route("/sessions/{session_id}/usage", get(usage))
45 .route("/sessions/{session_id}/usage/tree", get(usage_tree))
46 .route("/sessions/{session_id}/wait", post(wait))
47 .route("/sessions/{session_id}/suspend", post(suspend))
48 .route("/sessions/{session_id}/destroy", post(destroy))
49 .route("/sessions/{session_id}/resume", post(resume))
50 .route(
51 "/sessions/{session_id}/background-tasks/stop",
52 post(stop_background_task),
53 )
54 .route(
55 "/sessions/{session_id}/interrupt-turn",
56 post(interrupt_turn),
57 )
58 .route(
59 "/sessions/{session_id}/review",
60 get(review_status).post(start_review),
61 )
62 .route(
63 "/sessions/{session_id}/review/{resolution}",
64 post(resolve_review),
65 )
66 .route("/sessions/{session_id}/diff", get(diff))
67 .route(
68 "/sessions/{session_id}/files",
69 get(read_file)
70 .put(write_file)
71 .layer(axum::extract::DefaultBodyLimit::max(
72 mj_checkpoint::archive::MAX_SESSION_FILE_BYTES as usize,
73 )),
74 )
75 .route("/sessions/{session_id}/elicitations", get(elicitations))
76 .route(
77 "/sessions/{session_id}/elicitations/{elicitation_id}",
78 post(respond_elicitation),
79 )
80 .route("/sessions/{session_id}/export", post(export))
81 .route("/wiki/search", get(wiki_search))
82 .route("/wiki/sessions/{wiki_id}", get(wiki_session))
83 .route("/wiki/sessions/{wiki_id}/brief", get(wiki_brief))
84 .route("/wiki/sessions/{wiki_id}/hits", get(wiki_hits))
85 .route("/wiki/sessions/{wiki_id}/restore", post(wiki_restore))
86 .route_layer(axum::middleware::from_fn_with_state(
87 state,
88 require_api_auth,
89 ))
90 .layer(axum::middleware::from_fn(api_response_headers))
93}
94
95pub(super) async fn require_api_auth(
101 State(state): State<ServerState>,
102 request: HttpRequest<axum::body::Body>,
103 next: Next,
104) -> Result<Response, ApiFailure> {
105 let bearer = request
106 .headers()
107 .get(AUTHORIZATION)
108 .and_then(|value| value.to_str().ok())
109 .and_then(|value| value.strip_prefix("Bearer "))
110 .map(str::trim);
111 if bearer.is_some_and(|token| {
112 constant_time_eq(state.api_token.as_bytes(), token.as_bytes()) && !token.is_empty()
113 }) {
114 return Ok(next.run(request).await);
115 }
116 let cookie =
117 super::super::authenticated_viewer(&state, request.headers()).map_err(|error| {
118 if error.status == StatusCode::UNAUTHORIZED {
119 ApiFailure::new(
120 StatusCode::UNAUTHORIZED,
121 "supply the API token from the api-token file as a bearer token",
122 )
123 } else {
124 ApiFailure::from(error)
125 }
126 })?;
127 let mut response = next.run(request).await;
128 super::super::renew_viewer_response(&state, &cookie, &mut response)?;
129 Ok(response)
130}
131
132pub(super) async fn api_response_headers(
135 request: HttpRequest<axum::body::Body>,
136 next: Next,
137) -> Response {
138 let mut response = next.run(request).await;
139 let headers = response.headers_mut();
140 headers.insert(API_VERSION_HEADER, HeaderValue::from_static(API_VERSION));
141 headers.insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
142 response
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
150#[serde(deny_unknown_fields)]
151pub struct SessionListQuery {
152 pub workspace_id: Option<String>,
153 #[serde(default = "default_all_sessions")]
155 pub all: bool,
156}
157
158fn default_all_sessions() -> bool {
159 true
160}
161
162impl Default for SessionListQuery {
163 fn default() -> Self {
164 Self {
165 workspace_id: None,
166 all: true,
167 }
168 }
169}
170
171#[derive(Debug, Default, Deserialize)]
172#[serde(deny_unknown_fields)]
173pub(super) struct ProfileConfigQuery {
174 pub(super) model: Option<String>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct SetConfigRequest {
180 pub key: String,
181 pub value: String,
182}