agentmux_srv/server/service/
memory_adopt.rs1use serde::Deserialize;
18
19use crate::backend::memory_adopt::{self, AdoptError};
20use crate::backend::service::{get_arg, WebCallType, WebReturnType};
21
22use super::super::AppState;
23use super::credential::host_caller_allowed;
24
25pub(super) async fn handle_memory_adopt_service(state: &AppState, call: &WebCallType) -> WebReturnType {
26 let supplied = call.args.get(1).and_then(|v| v.as_str()).unwrap_or("");
27 if !host_caller_allowed(state.host_reg_secret.as_deref(), supplied) {
28 tracing::error!(
29 "[memoryadopt] REJECTED a {} call — args[1] is not this srv instance's host-registration \
30 secret. Only the paired host, after a human confirmed in its window, may adopt memory.",
31 call.method
32 );
33 return WebReturnType::error("memoryadopt: host-only");
34 }
35 match call.method.as_str() {
36 "Adopt" => handle_adopt(state, call).await,
37 "Release" => handle_release(call).await,
38 other => WebReturnType::error(format!("memoryadopt: unknown method {other}")),
39 }
40}
41
42#[derive(Deserialize)]
43struct Choice {
44 index: usize,
45 dir_hash: String,
46}
47
48#[derive(Deserialize)]
49struct AdoptArgs {
50 agent_id: String,
51 list_id: String,
52 choices: Vec<Choice>,
53}
54
55async fn handle_adopt(state: &AppState, call: &WebCallType) -> WebReturnType {
56 let args: AdoptArgs = match get_arg(&call.args, 0) {
57 Ok(a) => a,
58 Err(e) => return WebReturnType::error(format!("memoryadopt.Adopt: {e}")),
59 };
60 let Some(fs) = crate::backend::agent_session::global_transcript_store() else {
61 return WebReturnType::error("memoryadopt.Adopt: memory record store unavailable");
62 };
63 let mstore = state.mstore.clone();
64 let choices: Vec<(usize, String)> = args.choices.into_iter().map(|c| (c.index, c.dir_hash)).collect();
65 let result = tokio::task::spawn_blocking(move || {
66 memory_adopt::adopt(fs, &mstore, &args.agent_id, &args.list_id, &choices)
67 })
68 .await;
69 match result {
70 Ok(Ok(report)) => match serde_json::to_value(&report) {
71 Ok(v) => WebReturnType::success(v),
72 Err(e) => WebReturnType::error(format!("memoryadopt.Adopt: {e}")),
73 },
74 Ok(Err(AdoptError::Changed { index })) => {
76 WebReturnType::error(format!("changed: folder {index} changed since the list was issued; list again"))
77 }
78 Ok(Err(AdoptError::UnknownList)) => WebReturnType::error("unknown-list: the list expired or is not this agent's; list again"),
79 Ok(Err(AdoptError::BadChoice)) => WebReturnType::error("bad-choice"),
80 Ok(Err(AdoptError::Busy)) => WebReturnType::error("busy: the agent's memory is being synced; try again"),
81 Ok(Err(AdoptError::Store(e))) => {
82 tracing::warn!("[memoryadopt] Adopt failed: {e}");
83 WebReturnType::error("store error")
84 }
85 Err(e) => WebReturnType::error(format!("memoryadopt.Adopt: {e}")),
86 }
87}
88
89#[derive(Deserialize)]
90struct ReleaseArgs {
91 agent_id: String,
92 list_id: String,
93 index: usize,
94}
95
96async fn handle_release(call: &WebCallType) -> WebReturnType {
99 use crate::backend::memory_release::{self, ReleaseError};
100 let args: ReleaseArgs = match get_arg(&call.args, 0) {
101 Ok(a) => a,
102 Err(e) => return WebReturnType::error(format!("memoryadopt.Release: {e}")),
103 };
104 let Some(fs) = crate::backend::agent_session::global_transcript_store() else {
105 return WebReturnType::error("memoryadopt.Release: memory record store unavailable");
106 };
107 let result =
108 tokio::task::spawn_blocking(move || memory_release::release(fs, &args.agent_id, &args.list_id, args.index)).await;
109 match result {
110 Ok(Ok(released)) => WebReturnType::success(serde_json::json!({ "released": released })),
111 Ok(Err(ReleaseError::UnknownList)) => WebReturnType::error("unknown-list: the list expired or is not this agent's; list again"),
112 Ok(Err(ReleaseError::BadChoice)) => WebReturnType::error("bad-choice"),
113 Ok(Err(ReleaseError::Store(e))) => {
114 tracing::warn!("[memoryadopt] Release failed: {e}");
115 WebReturnType::error("store error")
116 }
117 Err(e) => WebReturnType::error(format!("memoryadopt.Release: {e}")),
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 fn call(secret: Option<&str>) -> WebCallType {
126 let mut args = vec![serde_json::json!({ "agent_id": "a", "list_id": "l", "choices": [] })];
127 if let Some(s) = secret {
128 args.push(serde_json::json!(s));
129 }
130 WebCallType { service: "memoryadopt".into(), method: "Adopt".into(), uicontext: None, args }
131 }
132
133 #[tokio::test]
137 async fn an_agent_without_the_host_secret_cannot_adopt() {
138 let state = crate::server::tests::test_state();
139 for secret in [None, Some(""), Some("not-the-secret")] {
140 let r = handle_memory_adopt_service(&state, &call(secret)).await;
141 assert_eq!(r.error.as_deref(), Some("memoryadopt: host-only"), "{secret:?}");
142 }
143 }
144
145 #[tokio::test]
146 async fn an_agent_without_the_host_secret_cannot_release_a_folder() {
147 let state = crate::server::tests::test_state();
148 let call = WebCallType {
149 service: "memoryadopt".into(),
150 method: "Release".into(),
151 uicontext: None,
152 args: vec![serde_json::json!({ "agent_id": "a", "list_id": "l", "index": 0 })],
153 };
154 let r = handle_memory_adopt_service(&state, &call).await;
155 assert_eq!(r.error.as_deref(), Some("memoryadopt: host-only"));
156 }
157
158 #[tokio::test]
159 async fn the_host_reaches_adopt() {
160 let state = crate::server::tests::test_state();
161 let r = handle_memory_adopt_service(&state, &call(Some("test-host-reg-secret"))).await;
162 assert_ne!(r.error.as_deref(), Some("memoryadopt: host-only"));
163 }
164}