1use super::*;
8
9pub(super) fn app_api_error_status(e: &str) -> StatusCode {
27 if e.starts_with("FORBIDDEN") {
28 StatusCode::FORBIDDEN
29 } else if e.contains("not found") {
30 StatusCode::NOT_FOUND
31 } else if e.contains("provide ")
32 || e.contains("not a regular file")
33 || e.contains("too large")
34 || e.contains("invalid")
35 {
36 StatusCode::BAD_REQUEST
37 } else {
38 StatusCode::INTERNAL_SERVER_ERROR
39 }
40}
41
42pub(super) fn app_api_response<T: serde::Serialize>(result: Result<T, String>) -> axum::response::Response {
43 match result {
44 Ok(v) => (StatusCode::OK, Json(v)).into_response(),
45 Err(e) => (app_api_error_status(&e), Json(json!({ "error": e }))).into_response(),
46 }
47}
48
49#[derive(serde::Deserialize)]
50pub(super) struct AgentMemoryListQuery {
51 pub(super) agent_id: String,
52}
53
54pub(super) async fn handle_agent_memory_list(
57 State(state): State<AppState>,
58 caller: Option<Extension<caller::Caller>>,
59 Query(q): Query<AgentMemoryListQuery>,
60) -> impl IntoResponse {
61 actor::check_actor(
62 &state,
63 caller.as_deref(),
64 actor::ActorSite::MemoryList,
65 Some(&q.agent_id),
66 );
67 app_api_response(app_api::memory_list_impl(&state, app_api::SelfOwner::of(caller.as_deref(), &q.agent_id, "m4c.memory_owner_by_name")))
68}
69
70#[derive(serde::Deserialize)]
71pub(super) struct AgentMemoryReadQuery {
72 pub(super) agent_id: String,
73 pub(super) filename: String,
74}
75
76pub(super) async fn handle_agent_memory_read(
79 State(state): State<AppState>,
80 caller: Option<Extension<caller::Caller>>,
81 Query(q): Query<AgentMemoryReadQuery>,
82) -> impl IntoResponse {
83 actor::check_actor(
84 &state,
85 caller.as_deref(),
86 actor::ActorSite::MemoryRead,
87 Some(&q.agent_id),
88 );
89 app_api_response(app_api::memory_read_impl(&state, app_api::SelfOwner::of(caller.as_deref(), &q.agent_id, "m4c.memory_owner_by_name"), &q.filename))
90}
91
92#[derive(serde::Deserialize)]
93pub(super) struct AgentMemoryWriteRequest {
94 pub(super) agent_id: String,
95 pub(super) filename: String,
96 pub(super) content: String,
97 #[serde(default)]
100 pub(super) provenance: Option<crate::backend::rpc_types::NativeMemoryWriteProvenance>,
101}
102
103pub(super) async fn handle_agent_memory_write(
110 State(state): State<AppState>,
111 caller: Option<Extension<caller::Caller>>,
112 Json(req): Json<AgentMemoryWriteRequest>,
113) -> impl IntoResponse {
114 actor::check_actor(
115 &state,
116 caller.as_deref(),
117 actor::ActorSite::MemoryWrite,
118 Some(&req.agent_id),
119 );
120 let detail_str;
121 let provenance = if let Some(p) = req.provenance.as_ref() {
122 detail_str = p.detail.to_string();
123 Some(app_api::MemoryWriteProvenance { source: &p.source, detail: &detail_str })
124 } else {
125 None
126 };
127 match app_api::memory_write_impl(&state, app_api::SelfOwner::of(caller.as_deref(), &req.agent_id, "m4c.memory_owner_by_name"), &req.filename, &req.content, provenance) {
128 Ok(()) => (StatusCode::OK, Json(json!({ "ok": true }))).into_response(),
129 Err(e) => (app_api_error_status(&e), Json(json!({ "error": e }))).into_response(),
130 }
131}
132
133pub(super) async fn handle_fleet_bulk_stop(
142 State(state): State<AppState>,
143 caller: Option<axum::Extension<crate::server::caller::Caller>>,
144 Json(body): Json<serde_json::Value>,
145) -> impl IntoResponse {
146 let req: crate::backend::rpc_types::CommandFleetBulkStopData = match serde_json::from_value(body.clone()) {
147 Ok(r) => r,
148 Err(e) => return (StatusCode::UNPROCESSABLE_ENTITY, Json(json!({ "error": e.to_string() }))).into_response(),
149 };
150 let by = body
151 .get("auth")
152 .and_then(|a| serde_json::from_value::<agentmux_common::api_types::UiAutomationAuth>(a.clone()).ok())
153 .filter(|a| ui_handlers::verified_block_id(&state, caller.as_deref(), a).is_ok())
154 .map(|a| a.agent_id)
155 .unwrap_or_else(|| "an agent".to_string());
156 let (result, pending) =
157 app_api::fleet::fleet_bulk_stop_with_override(&state, &by, req.targets, req.signal.as_deref(), req.staged).await;
158 if pending.is_empty() {
159 return (StatusCode::OK, Json(result)).into_response();
160 }
161 (
162 StatusCode::ACCEPTED,
163 Json(json!({
164 "status": "pending_user_override",
165 "wait_at_least_ms": crate::sagas::pending_shutdown::OVERRIDE_WINDOW.as_millis() as u64,
166 "deadline_ms": pending.iter().map(|p| p.deadline_ms).max(),
167 "pending": pending.iter().map(|p| json!({ "block_id": p.block_id, "request_id": p.request_id })).collect::<Vec<_>>(),
168 "succeeded": result.succeeded,
169 "failed": result.failed,
170 "aborted_early": result.aborted_early,
171 })),
172 )
173 .into_response()
174}
175
176#[derive(serde::Deserialize)]
177pub(super) struct AgentStopForwardRequest {
178 pub(super) block_id: String,
179 pub(super) signal: Option<String>,
180}
181
182#[derive(serde::Deserialize)]
183pub(super) struct AgentBroadcastForwardRequest {
184 pub(super) block_id: String,
185 pub(super) body: String,
186 pub(super) msg_id: String,
187 pub(super) recipients: usize,
188}
189
190pub(super) async fn handle_agent_broadcast_forward(
195 State(state): State<AppState>,
196 Json(req): Json<AgentBroadcastForwardRequest>,
197) -> impl IntoResponse {
198 match app_api::fleet::deliver_forwarded_broadcast(&state, &req.block_id, &req.body, &req.msg_id, req.recipients).await {
199 Ok(()) => (StatusCode::OK, Json(serde_json::json!({ "success": true }))).into_response(),
200 Err(e) => (StatusCode::OK, Json(serde_json::json!({ "success": false, "error": e }))).into_response(),
201 }
202}
203
204#[derive(serde::Deserialize)]
205pub(super) struct AgentStopPendingForwardRequest {
206 pub(super) block_id: String,
207 pub(super) signal: Option<String>,
208 #[serde(default)]
211 pub(super) by: String,
212}
213
214pub(super) async fn handle_agent_stop_pending_forward(
220 State(state): State<AppState>,
221 Json(req): Json<AgentStopPendingForwardRequest>,
222) -> impl IntoResponse {
223 if crate::backend::blockcontroller::get_controller(&req.block_id).is_none() {
224 let error = format!("NOT_RUNNING: no controller for block {}", req.block_id);
225 return (StatusCode::OK, Json(json!({ "success": false, "error": error }))).into_response();
226 }
227 let by = if req.by.trim().is_empty() { "an agent on another AgentMux" } else { req.by.trim() };
228 let v = crate::sagas::pending_shutdown::request(
229 &state,
230 &req.block_id,
231 by,
232 "FleetBulkStop",
233 "",
234 crate::sagas::pending_shutdown::Action::Stop { signal: req.signal },
235 );
236 state.reactive_handler.log_fleet_action_audit(
237 Some(by),
238 &v.target,
239 &req.block_id,
240 "fleet.bulk-stop",
241 true,
242 None,
243 &v.request_id,
244 Some(&format!("{} (forwarded from another channel)", crate::sagas::pending_shutdown::audit_note(&v, by, "FleetBulkStop"))),
245 );
246 let mut pending = serde_json::to_value(&v).unwrap_or_default();
247 pending["joined"] = json!(v.joined);
248 (StatusCode::OK, Json(json!({ "success": true, "pending": pending }))).into_response()
249}
250
251pub(super) async fn handle_agent_stop_forward(
258 State(state): State<AppState>,
259 Json(req): Json<AgentStopForwardRequest>,
260) -> impl IntoResponse {
261 match app_api::agent_io::stop_one_agent_block(&state, &req.block_id, req.signal.as_deref(), None).await {
262 Ok(result) => (StatusCode::OK, Json(serde_json::json!({ "success": true, "result": result }))).into_response(),
263 Err(e) => (StatusCode::OK, Json(serde_json::json!({ "success": false, "error": e }))).into_response(),
264 }
265}
266
267#[derive(serde::Deserialize)]
268pub(super) struct AgentMemoryHistoryQuery {
269 pub(super) agent_id: String,
270 pub(super) filename: String,
271}
272
273pub(super) async fn handle_agent_memory_history(
277 State(state): State<AppState>,
278 caller: Option<Extension<caller::Caller>>,
279 Query(q): Query<AgentMemoryHistoryQuery>,
280) -> impl IntoResponse {
281 actor::check_actor(
282 &state,
283 caller.as_deref(),
284 actor::ActorSite::MemoryHistory,
285 Some(&q.agent_id),
286 );
287 app_api_response(app_api::memory_history_impl(&state, app_api::SelfOwner::of(caller.as_deref(), &q.agent_id, "m4c.memory_owner_by_name"), &q.filename))
288}
289
290#[derive(serde::Deserialize)]
291pub(super) struct AgentMemoryDiffQuery {
292 pub(super) agent_id: String,
293 pub(super) from_version_id: String,
294 pub(super) to_version_id: String,
295}
296
297pub(super) async fn handle_agent_memory_diff(
302 State(state): State<AppState>,
303 caller: Option<Extension<caller::Caller>>,
304 Query(q): Query<AgentMemoryDiffQuery>,
305) -> impl IntoResponse {
306 actor::check_actor(
307 &state,
308 caller.as_deref(),
309 actor::ActorSite::MemoryDiff,
310 Some(&q.agent_id),
311 );
312 app_api_response(app_api::memory_diff_impl(&state, app_api::SelfOwner::of(caller.as_deref(), &q.agent_id, "m4c.memory_owner_by_name"), &q.from_version_id, &q.to_version_id))
313}
314
315#[derive(serde::Deserialize)]
316pub(super) struct AgentMemoryRevertRequest {
317 pub(super) agent_id: String,
318 pub(super) filename: String,
319 pub(super) target_version_id: String,
320}
321
322pub(super) async fn handle_agent_memory_revert(
327 State(state): State<AppState>,
328 caller: Option<Extension<caller::Caller>>,
329 Json(req): Json<AgentMemoryRevertRequest>,
330) -> impl IntoResponse {
331 actor::check_actor(
332 &state,
333 caller.as_deref(),
334 actor::ActorSite::MemoryRevert,
335 Some(&req.agent_id),
336 );
337 app_api_response(app_api::memory_revert_impl(&state, app_api::SelfOwner::of(caller.as_deref(), &req.agent_id, "m4c.memory_owner_by_name"), &req.filename, &req.target_version_id))
338}
339
340#[derive(serde::Deserialize)]
341pub(super) struct AgentGlobalMemoryWriteProvenanceReq {
342 pub(super) source: String,
343 #[serde(default = "default_agent_globalmemory_write_detail")]
346 pub(super) detail: serde_json::Value,
347}
348
349pub(super) fn default_agent_globalmemory_write_detail() -> serde_json::Value {
350 serde_json::json!({})
351}
352
353#[derive(serde::Deserialize)]
354pub(super) struct AgentGlobalMemoryWriteRequest {
355 pub(super) agent_id: String,
356 #[serde(default)]
357 pub(super) id: Option<String>,
358 pub(super) name: String,
359 pub(super) content: String,
360 #[serde(default)]
361 pub(super) provenance: Option<AgentGlobalMemoryWriteProvenanceReq>,
362}
363
364pub(super) async fn handle_agent_globalmemory_write(
369 State(state): State<AppState>,
370 caller: Option<Extension<caller::Caller>>,
371 Json(req): Json<AgentGlobalMemoryWriteRequest>,
372) -> impl IntoResponse {
373 actor::check_actor(
374 &state,
375 caller.as_deref(),
376 actor::ActorSite::GlobalMemoryWrite,
377 Some(&req.agent_id),
378 );
379 let detail_str;
380 let provenance = if let Some(p) = req.provenance.as_ref() {
381 detail_str = p.detail.to_string();
382 Some(app_api::GlobalMemoryWriteProvenance { source: &p.source, detail: &detail_str })
383 } else {
384 None
385 };
386 let written_by_uid = caller::attributed_uid(caller.as_deref());
389 app_api_response(app_api::global_memory_write_impl(
390 &state,
391 &req.agent_id,
392 &written_by_uid,
393 req.id.as_deref(),
394 &req.name,
395 &req.content,
396 provenance,
397 ))
398}
399
400pub(super) async fn handle_agent_globalmemory_list(State(state): State<AppState>) -> impl IntoResponse {
406 app_api_response(app_api::global_memory_list_impl(&state))
407}
408
409#[derive(serde::Deserialize)]
410pub(super) struct AgentGlobalMemoryReadQuery {
411 pub(super) id: String,
412}
413
414pub(super) async fn handle_agent_globalmemory_read(
417 State(state): State<AppState>,
418 Query(q): Query<AgentGlobalMemoryReadQuery>,
419) -> impl IntoResponse {
420 app_api_response(app_api::global_memory_read_impl(&state, &q.id))
421}
422
423#[derive(serde::Deserialize)]
424pub(super) struct AgentGlobalMemoryRemoveRequest {
425 pub(super) id: String,
426}
427
428pub(super) async fn handle_agent_globalmemory_remove(
432 State(state): State<AppState>,
433 Json(req): Json<AgentGlobalMemoryRemoveRequest>,
434) -> impl IntoResponse {
435 app_api_response(app_api::global_memory_remove_impl(&state, &req.id))
436}
437
438#[derive(serde::Deserialize)]
439pub(super) struct AgentGlobalMemoryHistoryQuery {
440 pub(super) id: String,
441}
442
443pub(super) async fn handle_agent_globalmemory_history(
449 State(state): State<AppState>,
450 Query(q): Query<AgentGlobalMemoryHistoryQuery>,
451) -> impl IntoResponse {
452 app_api_response(app_api::global_memory_history_impl(&state, &q.id))
453}
454
455#[derive(serde::Deserialize)]
456pub(super) struct AgentGlobalMemoryDiffQuery {
457 pub(super) id: String,
458 pub(super) from_version_id: String,
459 pub(super) to_version_id: String,
460}
461
462pub(super) async fn handle_agent_globalmemory_diff(
468 State(state): State<AppState>,
469 Query(q): Query<AgentGlobalMemoryDiffQuery>,
470) -> impl IntoResponse {
471 app_api_response(app_api::global_memory_diff_impl(&state, &q.id, &q.from_version_id, &q.to_version_id))
472}
473
474#[derive(serde::Deserialize)]
475pub(super) struct AgentGlobalMemoryRevertRequest {
476 pub(super) agent_id: String,
477 pub(super) id: String,
478 pub(super) version_id: String,
479}
480
481pub(super) async fn handle_agent_globalmemory_revert(
488 State(state): State<AppState>,
489 caller: Option<Extension<caller::Caller>>,
490 Json(req): Json<AgentGlobalMemoryRevertRequest>,
491) -> impl IntoResponse {
492 actor::check_actor(
493 &state,
494 caller.as_deref(),
495 actor::ActorSite::GlobalMemoryRevert,
496 Some(&req.agent_id),
497 );
498 let written_by_uid = caller::attributed_uid(caller.as_deref());
500 app_api_response(app_api::global_memory_revert_impl(
501 &state,
502 &req.agent_id,
503 &written_by_uid,
504 &req.id,
505 &req.version_id,
506 ))
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
516 fn app_api_error_status_classifies_each_kind_once() {
517 assert_eq!(app_api_error_status("FORBIDDEN: not your memory"), StatusCode::FORBIDDEN);
518 assert_eq!(app_api_error_status("globalmemory.read: not found id=x"), StatusCode::NOT_FOUND);
519 assert_eq!(app_api_error_status("tab not found: tab-1"), StatusCode::NOT_FOUND);
520 for bad in ["provide id or name", "not a regular file", "file too large", "invalid filename"] {
521 assert_eq!(app_api_error_status(bad), StatusCode::BAD_REQUEST, "{bad}");
522 }
523 assert_eq!(app_api_error_status("database is locked"), StatusCode::INTERNAL_SERVER_ERROR);
524 }
525}