返回 CodeWhale
memory_lens.rs
根目录 / crates / tui / src / runtime_api / memory_lens.rs
1 //! Merged inside the existing Runtime API auth layer. No new listener, token,
2 //! CORS policy, or client-constructed capability grant is introduced here.
3 use super::*;
4 use crate::native_memory::NativeMemoryStore;
5 use codewhale_memory::{
6 Access, Capability, Draft, Evidence, MemoryBackend, Scope, SourceKind, Status,
7 ValidationReceipt,
8 };
9
10 type LensError = (StatusCode, Json<Value>);
11 fn fail(error: codewhale_memory::Error) -> LensError {
12 let status = match &error {
13 codewhale_memory::Error::Denied => StatusCode::FORBIDDEN,
14 codewhale_memory::Error::NotFound => StatusCode::NOT_FOUND,
15 codewhale_memory::Error::RevisionConflict
16 | codewhale_memory::Error::IdempotencyConflict
17 | codewhale_memory::Error::KeyConflict => StatusCode::CONFLICT,
18 codewhale_memory::Error::Disabled => StatusCode::SERVICE_UNAVAILABLE,
19 codewhale_memory::Error::Sql(_) | codewhale_memory::Error::Io(_) => {
20 StatusCode::INTERNAL_SERVER_ERROR
21 }
22 _ => StatusCode::UNPROCESSABLE_ENTITY,
23 };
24 (status, Json(json!({"error":error.code()})))
25 }
26 fn internal() -> LensError {
27 (
28 StatusCode::INTERNAL_SERVER_ERROR,
29 Json(json!({"error":"memory_unavailable"})),
30 )
31 }
32 fn invalid() -> LensError {
33 fail(codewhale_memory::Error::Invalid("invalid request".into()))
34 }
35 #[derive(Debug, Default, Deserialize)]
36 #[serde(deny_unknown_fields)]
37 struct LensQuery {
38 thread_id: Option<String>,
39 after: Option<String>,
40 limit: Option<usize>,
41 }
42 #[derive(Debug, Default, Deserialize)]
43 #[serde(deny_unknown_fields)]
44 struct EventQuery {
45 after: Option<i64>,
46 limit: Option<usize>,
47 }
48 #[derive(Debug, Deserialize)]
49 #[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
50 enum Action {
51 Approve {
52 id: String,
53 revision: i64,
54 #[serde(default)]
55 validation: Option<ValidationReceipt>,
56 },
57 Reject {
58 id: String,
59 revision: i64,
60 },
61 Forget {
62 id: String,
63 revision: i64,
64 },
65 Preferences {
66 id: String,
67 revision: i64,
68 pinned: bool,
69 suppressed: bool,
70 },
71 Remember {
72 request_key: String,
73 title: String,
74 body: String,
75 #[serde(default)]
76 global: bool,
77 },
78 Replace {
79 id: String,
80 revision: i64,
81 request_key: String,
82 title: String,
83 body: String,
84 #[serde(default)]
85 validation: Option<ValidationReceipt>,
86 },
87 }
88 #[derive(Debug, Deserialize)]
89 #[serde(deny_unknown_fields)]
90 struct ActionRequest {
91 thread_id: Option<String>,
92 command: Action,
93 }
94 fn valid_revision(revision: i64) -> Result<(), LensError> {
95 if !(0..i64::MAX).contains(&revision) {
96 return Err(invalid());
97 }
98 Ok(())
99 }
100 // This object is created exclusively by the authenticated server, not deserialized.
101 struct BoundMemory {
102 store: codewhale_memory::Store,
103 access: Access,
104 workspace_scope: Option<Scope>,
105 snapshot: codewhale_memory::Snapshot,
106 }
107 fn bind(state: &RuntimeApiState, thread_id: Option<&str>) -> Result<BoundMemory, LensError> {
108 let anchor = {
109 let config = state.config.read();
110 if !config.memory_enabled() {
111 return Err(fail(codewhale_memory::Error::Disabled));
112 }
113 config.memory_path()
114 };
115 let native = NativeMemoryStore::from_memory_anchor(&anchor);
116 let workspace_id = NativeMemoryStore::workspace_id(&state.workspace).map_err(|_| internal())?;
117 let owner = NativeMemoryStore::owner_scope();
118 let mut scopes = vec![owner.clone()];
119 let workspace_scope = workspace_id
120 .as_deref()
121 .map(NativeMemoryStore::workspace_scope)
122 .transpose()
123 .map_err(|_| internal())?;
124 if let Some(scope) = &workspace_scope {
125 scopes.push(scope.clone());
126 }
127 if let Some(id) = thread_id {
128 if id.is_empty()
129 || id.len() > 128
130 || !id
131 .bytes()
132 .all(|b| b.is_ascii_alphanumeric() || b"_-.:".contains(&b))
133 {
134 return Err(invalid());
135 }
136 // The server token already authorizes this local runtime. This only
137 // narrows reads; the request cannot specify tenant/user/workspace grants.
138 scopes.push(workspace_scope.as_ref().unwrap_or(&owner).session(id));
139 }
140 let access = Access::operator(scopes).map_err(fail)?;
141 let store = native.open_structured().map_err(|_| internal())?;
142 let snapshot = codewhale_memory::workspace::snapshot(
143 &state.workspace,
144 store.dependency_paths(&access).map_err(fail)?,
145 )
146 .map_err(fail)?;
147 Ok(BoundMemory {
148 store,
149 access,
150 workspace_scope,
151 snapshot,
152 })
153 }
154 pub(super) fn routes() -> Router<RuntimeApiState> {
155 Router::new()
156 .route("/v1/memory/lens", get(read_lens))
157 .route("/v1/memory/lens/actions", post(act))
158 .route("/v1/memory/events", get(events))
159 }
160 async fn read_lens(
161 State(state): State<RuntimeApiState>,
162 Query(query): Query<LensQuery>,
163 ) -> Result<Json<Value>, LensError> {
164 tokio::task::spawn_blocking(move || {
165 let b = bind(&state, query.thread_id.as_deref())?;
166 let snapshot = b
167 .store
168 .lens_snapshot(
169 &b.access,
170 query.thread_id.as_deref(),
171 query.after.as_deref(),
172 query.limit.unwrap_or(100),
173 &b.snapshot,
174 )
175 .map_err(fail)?;
176 Ok(Json(
177 serde_json::to_value(snapshot).map_err(|_| internal())?,
178 ))
179 })
180 .await
181 .map_err(|_| internal())?
182 }
183 async fn events(
184 State(state): State<RuntimeApiState>,
185 Query(query): Query<EventQuery>,
186 ) -> Result<Json<Value>, LensError> {
187 tokio::task::spawn_blocking(move || {
188 let b = bind(&state, None)?;
189 let page = b
190 .store
191 .event_page(
192 &b.access,
193 query.after.unwrap_or(0),
194 query.limit.unwrap_or(200),
195 )
196 .map_err(fail)?;
197 Ok(Json(serde_json::to_value(page).map_err(|_| internal())?))
198 })
199 .await
200 .map_err(|_| internal())?
201 }
202 async fn act(
203 State(state): State<RuntimeApiState>,
204 Json(request): Json<ActionRequest>,
205 ) -> Result<Json<Value>, LensError> {
206 tokio::task::spawn_blocking(move||{
207 let mut b=bind(&state,request.thread_id.as_deref())?;
208 // Mutations here are operator controls. No dispatch acknowledgement is
209 // exposed over HTTP; only the engine can attest its own history/transport.
210 b.access.require(Capability::Review).map_err(fail)?;
211 match request.command {
212 Action::Approve{id,revision,validation}=>{
213 valid_revision(revision)?;
214 let m=b.store.get(&b.access,&id).map_err(fail)?;
215 if !(m.status==Status::Active&&m.revision==revision+1) {b.store.approve(&b.access,&id,revision,validation.as_ref(),&b.snapshot).map_err(fail)?;}
216 },
217 Action::Reject{id,revision}=>{
218 valid_revision(revision)?;
219 let m=b.store.get(&b.access,&id).map_err(fail)?;
220 if !(m.status==Status::Rejected&&m.revision==revision+1) {b.store.reject(&b.access,&id,revision).map_err(fail)?;}
221 },
222 Action::Forget{id,revision}=>{
223 valid_revision(revision)?;
224 match b.store.forget(&b.access,&id,revision) {
225 Ok(_)|Err(codewhale_memory::Error::NotFound)=>{},
226 Err(error)=>return Err(fail(error)),
227 }
228 },
229 Action::Preferences{id,revision,pinned,suppressed}=>{valid_revision(revision)?;b.store.set_preferences(&b.access,&id,revision,pinned,suppressed).map_err(fail)?;},
230 Action::Remember{request_key,title,body,global}=>{
231 let scope=if global {NativeMemoryStore::owner_scope()} else {b.workspace_scope.ok_or_else(invalid)?};
232 // Explicit human operation: candidate capture followed by trusted
233 // review. observation=0 makes retry identity stable; created_at is
234 // the actual local receipt time, not an invented source timestamp.
235 let draft=Draft::note(scope,title,body,Evidence{kind:SourceKind::User,uri:"codewhale:context-lens".into(),locator:"Explicit remember action".into(),sha256:None,observed_at:0});
236 let receipt=b.store.capture(&b.access,&request_key,draft).map_err(fail)?;
237 if receipt.memory.status==Status::Candidate {b.store.approve(&b.access,&receipt.memory.id,receipt.memory.revision,None,&b.snapshot).map_err(fail)?;}
238 },
239 Action::Replace{id,revision,request_key,title,body,validation}=>{
240 valid_revision(revision)?;
241 let old=b.store.get(&b.access,&id).map_err(fail)?;
242 let mut draft=old.draft.clone();draft.title=title;draft.body=body;
243 draft.evidence=vec![Evidence{kind:SourceKind::User,uri:format!("codewhale:memory:{id}"),locator:"Explicit correction".into(),sha256:None,observed_at:0}];
244 draft.parent_ids.clear();
245 let captured=b.store.capture(&b.access,&request_key,draft).map_err(fail)?;
246 if let Some(replacement)=b.store.replacement_of(&b.access,&id).map_err(fail)? {
247 if replacement.id!=captured.memory.id {return Err(fail(codewhale_memory::Error::RevisionConflict));}
248 } else {
249 b.store.supersede(&b.access,(&id,revision),(&captured.memory.id,captured.memory.revision),validation.as_ref(),&b.snapshot).map_err(fail)?;
250 }
251 },
252 }
253 Ok(Json(json!({"ok":true,"refresh_required":true,"already_sent_context_cannot_be_retracted":true})))
254 }).await.map_err(|_|internal())?
255 }
256
256 lines RUST