Skip to main content

mentedb_server/
grpc.rs

1//! gRPC service implementations for MenteDB.
2//!
3//! Provides a CognitionService with bidirectional streaming that wraps the
4//! CognitionStream engine, and a MemoryService with standard CRUD operations.
5
6use std::collections::HashMap;
7use std::pin::Pin;
8use std::sync::Arc;
9
10use mentedb_cognitive::stream::{CognitionStream, StreamAlert};
11use mentedb_core::edge::EdgeType;
12use mentedb_core::memory::{AttributeValue, MemoryType};
13use mentedb_core::{MemoryEdge, MemoryNode};
14use tokio::sync::mpsc;
15use tokio_stream::{Stream, StreamExt, wrappers::ReceiverStream};
16use tonic::{Request, Response, Status, Streaming};
17use tracing::{error, info};
18use uuid::Uuid;
19
20use crate::auth::AuthenticatedAgent;
21use crate::state::AppState;
22
23// Generated by tonic-build, not hand written: lints on it are tonic's to fix, and
24// each new clippy release adds some (large `tonic::Status` errors, doubled
25// `#[must_use]`), breaking CI on code nobody here writes.
26#[allow(clippy::all)]
27pub mod pb {
28    tonic::include_proto!("mentedb");
29}
30
31use mentedb_core::types::{AgentId, MemoryId, SpaceId, UserId};
32use pb::cognition_service_server::CognitionService;
33use pb::memory_service_server::MemoryService;
34
35// ---------------------------------------------------------------------------
36// CognitionService
37// ---------------------------------------------------------------------------
38
39/// gRPC service implementation for cognitive memory operations.
40pub struct CognitionServiceImpl {
41    #[allow(dead_code)]
42    pub state: Arc<AppState>,
43}
44
45type CognitionStream_ =
46    Pin<Box<dyn Stream<Item = Result<pb::CognitionAlert, Status>> + Send + 'static>>;
47
48#[tonic::async_trait]
49impl CognitionService for CognitionServiceImpl {
50    type StreamCognitionStream = CognitionStream_;
51
52    async fn stream_cognition(
53        &self,
54        request: Request<Streaming<pb::CognitionRequest>>,
55    ) -> Result<Response<Self::StreamCognitionStream>, Status> {
56        authenticate_grpc_streaming(&self.state, request.metadata(), false)?;
57        info!("gRPC cognition stream opened");
58
59        let stream_engine = Arc::new(CognitionStream::new(1000));
60        let known_facts: Arc<std::sync::Mutex<Vec<(MemoryId, String)>>> =
61            Arc::new(std::sync::Mutex::new(Vec::new()));
62
63        let (tx, rx) = mpsc::channel(128);
64        let mut inbound = request.into_inner();
65
66        let engine = stream_engine.clone();
67        let facts = known_facts.clone();
68
69        tokio::spawn(async move {
70            while let Some(result) = inbound.next().await {
71                let req = match result {
72                    Ok(r) => r,
73                    Err(e) => {
74                        error!("cognition stream receive error: {e}");
75                        break;
76                    }
77                };
78
79                let payload = match req.payload {
80                    Some(p) => p,
81                    None => continue,
82                };
83
84                match payload {
85                    pb::cognition_request::Payload::Token(t) => {
86                        engine.feed_token(&t.text);
87
88                        let current_facts = facts.lock().unwrap().clone();
89                        let alerts = engine.check_alerts(&current_facts);
90                        for alert in alerts {
91                            let proto_alert = stream_alert_to_proto(alert);
92                            if tx.send(Ok(proto_alert)).await.is_err() {
93                                return;
94                            }
95                        }
96                    }
97                    pb::cognition_request::Payload::KnownFact(kf) => {
98                        let mid = kf
99                            .memory_id
100                            .parse::<MemoryId>()
101                            .unwrap_or_else(|_| MemoryId::nil());
102                        facts.lock().unwrap().push((mid, kf.content));
103                    }
104                    pb::cognition_request::Payload::EndOfTurn(_) => {
105                        let current_facts = facts.lock().unwrap().clone();
106                        let alerts = engine.check_alerts(&current_facts);
107                        for alert in alerts {
108                            let proto_alert = stream_alert_to_proto(alert);
109                            if tx.send(Ok(proto_alert)).await.is_err() {
110                                return;
111                            }
112                        }
113                    }
114                    pb::cognition_request::Payload::Flush(_) => {
115                        let text = engine.drain_buffer();
116                        let alert = pb::CognitionAlert {
117                            alert: Some(pb::cognition_alert::Alert::BufferFlushed(
118                                pb::BufferFlushedAlert {
119                                    accumulated_text: text,
120                                },
121                            )),
122                        };
123                        if tx.send(Ok(alert)).await.is_err() {
124                            return;
125                        }
126                    }
127                }
128            }
129            info!("gRPC cognition stream closed");
130        });
131
132        let output = ReceiverStream::new(rx);
133        Ok(Response::new(Box::pin(output)))
134    }
135}
136
137fn stream_alert_to_proto(alert: StreamAlert) -> pb::CognitionAlert {
138    let inner = match alert {
139        StreamAlert::Contradiction {
140            memory_id,
141            ai_said,
142            stored,
143        } => pb::cognition_alert::Alert::Contradiction(pb::ContradictionAlert {
144            memory_id: memory_id.to_string(),
145            ai_said,
146            stored,
147        }),
148        StreamAlert::Forgotten { memory_id, summary } => {
149            pb::cognition_alert::Alert::Forgotten(pb::ForgottenAlert {
150                memory_id: memory_id.to_string(),
151                summary,
152            })
153        }
154        StreamAlert::Correction {
155            memory_id,
156            old,
157            new,
158        } => pb::cognition_alert::Alert::Correction(pb::CorrectionAlert {
159            memory_id: memory_id.to_string(),
160            old_content: old,
161            new_content: new,
162        }),
163        StreamAlert::Reinforcement { memory_id } => {
164            pb::cognition_alert::Alert::Reinforcement(pb::ReinforcementAlert {
165                memory_id: memory_id.to_string(),
166            })
167        }
168    };
169    pb::CognitionAlert { alert: Some(inner) }
170}
171
172// ---------------------------------------------------------------------------
173// MemoryService
174// ---------------------------------------------------------------------------
175
176/// gRPC service implementation for basic memory CRUD operations.
177pub struct MemoryServiceImpl {
178    /// Shared application state containing the database handle.
179    pub state: Arc<AppState>,
180}
181
182#[tonic::async_trait]
183impl MemoryService for MemoryServiceImpl {
184    async fn store(
185        &self,
186        request: Request<pb::StoreRequest>,
187    ) -> Result<Response<pb::StoreResponse>, Status> {
188        let caller = authenticate_grpc_request(&self.state, &request, true)?;
189        let req = request.into_inner();
190
191        let agent_id = AgentId(parse_uuid_field(&req.agent_id, "agent_id")?);
192        if let Some(ref ta) = caller
193            && let Some(tu) = ta
194                .agent()
195                .map_err(|_| Status::unauthenticated("bad token agent_id"))?
196            && tu != agent_id
197        {
198            return Err(Status::permission_denied("agent_id mismatch"));
199        }
200        let memory_type = parse_memory_type_str(&req.memory_type)?;
201        let space_id = if req.space_id.is_empty() {
202            SpaceId::nil()
203        } else {
204            SpaceId(parse_uuid_field(&req.space_id, "space_id")?)
205        };
206
207        let salience = if req.salience == 0.0 {
208            0.5
209        } else {
210            req.salience
211        };
212        let confidence = if req.confidence == 0.0 {
213            1.0
214        } else {
215            req.confidence
216        };
217
218        let now = std::time::SystemTime::now()
219            .duration_since(std::time::UNIX_EPOCH)
220            .unwrap_or_default()
221            .as_micros() as u64;
222
223        let id = MemoryId::new();
224
225        let attributes = req
226            .attributes
227            .into_iter()
228            .map(|(k, v)| (k, AttributeValue::String(v)))
229            .collect::<HashMap<String, AttributeValue>>();
230
231        let node = MemoryNode {
232            id,
233            agent_id,
234            user_id: UserId::nil(),
235            memory_type,
236            embedding: req.embedding,
237            content: req.content,
238            created_at: now,
239            accessed_at: now,
240            access_count: 0,
241            salience,
242            confidence,
243            space_id,
244            attributes,
245            tags: req.tags,
246            valid_from: req.valid_from,
247            valid_until: req.valid_until,
248            context: None,
249        };
250
251        let db = &*self.state.db;
252        let outcome = db.store_with_outcome(node).map_err(|e| {
253            error!("gRPC store failed: {e}");
254            Status::internal(format!("store failed: {e}"))
255        })?;
256
257        let (id, status) = match outcome {
258            mentedb::StoreOutcome::Stored => (id, "stored"),
259            mentedb::StoreOutcome::Duplicate { of } => (of, "duplicate"),
260        };
261        Ok(Response::new(pb::StoreResponse {
262            id: id.to_string(),
263            status: status.into(),
264        }))
265    }
266
267    async fn recall(
268        &self,
269        request: Request<pb::RecallRequest>,
270    ) -> Result<Response<pb::RecallResponse>, Status> {
271        authenticate_grpc_request(&self.state, &request, false)?;
272        let req = request.into_inner();
273
274        let db = &*self.state.db;
275        let window = db.recall(&req.query).map_err(|e| {
276            error!("gRPC recall failed: {e}");
277            Status::internal(format!("recall failed: {e}"))
278        })?;
279
280        let memory_count: usize = window.blocks.iter().map(|b| b.memories.len()).sum();
281
282        Ok(Response::new(pb::RecallResponse {
283            context: window.format,
284            total_tokens: window.total_tokens as u64,
285            memory_count: memory_count as u64,
286        }))
287    }
288
289    async fn search(
290        &self,
291        request: Request<pb::SearchRequest>,
292    ) -> Result<Response<pb::SearchResponse>, Status> {
293        authenticate_grpc_request(&self.state, &request, false)?;
294        let req = request.into_inner();
295        let k = if req.k == 0 { 10 } else { req.k as usize };
296
297        if req.embedding.is_empty() {
298            return Err(Status::invalid_argument("missing embedding vector"));
299        }
300
301        let db = &*self.state.db;
302        let results = db.recall_similar(&req.embedding, k).map_err(|e| {
303            error!("gRPC search failed: {e}");
304            Status::internal(format!("search failed: {e}"))
305        })?;
306
307        let items: Vec<pb::SearchResult> = results
308            .iter()
309            .map(|(id, score)| pb::SearchResult {
310                id: id.to_string(),
311                score: *score,
312            })
313            .collect();
314
315        Ok(Response::new(pb::SearchResponse { results: items }))
316    }
317
318    async fn forget(
319        &self,
320        request: Request<pb::ForgetRequest>,
321    ) -> Result<Response<pb::ForgetResponse>, Status> {
322        authenticate_grpc_request(&self.state, &request, true)?;
323        let req = request.into_inner();
324        let id = MemoryId(parse_uuid_field(&req.id, "id")?);
325
326        let db = &*self.state.db;
327        db.forget(id).map_err(|e| {
328            error!("gRPC forget failed: {e}");
329            Status::internal(format!("forget failed: {e}"))
330        })?;
331
332        Ok(Response::new(pb::ForgetResponse {
333            status: "deleted".into(),
334        }))
335    }
336
337    async fn relate(
338        &self,
339        request: Request<pb::RelateRequest>,
340    ) -> Result<Response<pb::RelateResponse>, Status> {
341        authenticate_grpc_request(&self.state, &request, true)?;
342        let req = request.into_inner();
343        let source = MemoryId(parse_uuid_field(&req.source, "source")?);
344        let target = MemoryId(parse_uuid_field(&req.target, "target")?);
345        let edge_type = parse_edge_type_str(&req.edge_type)?;
346        let weight = if req.weight == 0.0 { 1.0 } else { req.weight };
347
348        let now = std::time::SystemTime::now()
349            .duration_since(std::time::UNIX_EPOCH)
350            .unwrap_or_default()
351            .as_micros() as u64;
352
353        let edge = MemoryEdge {
354            source,
355            target,
356            edge_type,
357            weight,
358            created_at: now,
359            valid_from: req.valid_from,
360            valid_until: req.valid_until,
361            label: None,
362        };
363
364        let db = &*self.state.db;
365        db.relate(edge).map_err(|e| {
366            error!("gRPC relate failed: {e}");
367            Status::internal(format!("relate failed: {e}"))
368        })?;
369
370        Ok(Response::new(pb::RelateResponse {
371            status: "created".into(),
372        }))
373    }
374}
375
376#[allow(clippy::result_large_err)]
377fn authenticate_grpc_request<T>(
378    state: &AppState,
379    request: &Request<T>,
380    write: bool,
381) -> Result<Option<AuthenticatedAgent>, Status> {
382    authenticate_grpc_streaming(state, request.metadata(), write)
383}
384
385/// Check the bearer token on a gRPC call the same way the REST API does: a
386/// valid signature, the token policy, and then the token's scope and `dbs`
387/// against the database gRPC serves (the default one).
388#[allow(clippy::result_large_err)]
389fn authenticate_grpc_streaming(
390    state: &AppState,
391    metadata: &tonic::metadata::MetadataMap,
392    write: bool,
393) -> Result<Option<AuthenticatedAgent>, Status> {
394    let verifier = match &state.verifier {
395        Some(v) => v,
396        None => return Ok(None),
397    };
398    let token = metadata
399        .get("authorization")
400        .and_then(|v| v.to_str().ok())
401        .and_then(|v| v.strip_prefix("Bearer "))
402        .ok_or_else(|| Status::unauthenticated("missing or invalid authorization metadata"))?;
403    let agent = verifier
404        .authenticate(token)
405        .map_err(|e| Status::unauthenticated(format!("invalid token: {e}")))?;
406    agent
407        .authorize_db(crate::databases::DEFAULT_DB, write)
408        .map_err(|e| Status::permission_denied(e.to_string()))?;
409    Ok(Some(agent))
410}
411
412// ---------------------------------------------------------------------------
413// Helpers
414// ---------------------------------------------------------------------------
415
416#[allow(clippy::result_large_err)]
417fn parse_uuid_field(s: &str, field: &str) -> Result<Uuid, Status> {
418    Uuid::parse_str(s)
419        .map_err(|_| Status::invalid_argument(format!("invalid UUID for '{field}': {s}")))
420}
421
422#[allow(clippy::result_large_err)]
423fn parse_memory_type_str(s: &str) -> Result<MemoryType, Status> {
424    match s.to_lowercase().as_str() {
425        "episodic" => Ok(MemoryType::Episodic),
426        "semantic" => Ok(MemoryType::Semantic),
427        "procedural" => Ok(MemoryType::Procedural),
428        "antipattern" | "anti_pattern" => Ok(MemoryType::AntiPattern),
429        "reasoning" => Ok(MemoryType::Reasoning),
430        "correction" => Ok(MemoryType::Correction),
431        _ => Err(Status::invalid_argument(format!(
432            "unknown memory_type: {s}"
433        ))),
434    }
435}
436
437#[allow(clippy::result_large_err)]
438fn parse_edge_type_str(s: &str) -> Result<EdgeType, Status> {
439    match s.to_lowercase().as_str() {
440        "caused" => Ok(EdgeType::Caused),
441        "before" => Ok(EdgeType::Before),
442        "related" => Ok(EdgeType::Related),
443        "contradicts" => Ok(EdgeType::Contradicts),
444        "supports" => Ok(EdgeType::Supports),
445        "supersedes" => Ok(EdgeType::Supersedes),
446        "derived" => Ok(EdgeType::Derived),
447        "partof" | "part_of" => Ok(EdgeType::PartOf),
448        _ => Err(Status::invalid_argument(format!("unknown edge_type: {s}"))),
449    }
450}
451
452// ---------------------------------------------------------------------------
453// Tests
454// ---------------------------------------------------------------------------
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use std::time::Instant;
460    use tempfile::TempDir;
461
462    fn make_test_state() -> (Arc<AppState>, TempDir) {
463        let tmp = TempDir::new().unwrap();
464        let databases = Arc::new(
465            crate::databases::Databases::open(
466                tmp.path(),
467                None,
468                Default::default(),
469                Default::default(),
470            )
471            .unwrap(),
472        );
473        let state = Arc::new(AppState {
474            db: databases.default_db(),
475            databases: databases.clone(),
476            verifier: None,
477            spaces: Arc::new(tokio::sync::RwLock::new(mentedb_core::SpaceManager::new())),
478            jwt_secret: None,
479            admin_key: None,
480            start_time: Instant::now(),
481            extraction_config: None,
482            auto_extract: false,
483            extraction_tx: None,
484            cluster: None,
485            turn_trackers: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
486        });
487        (state, tmp)
488    }
489
490    #[tokio::test]
491    async fn test_grpc_memory_store_and_recall() {
492        let (state, _tmp) = make_test_state();
493        let svc = MemoryServiceImpl {
494            state: state.clone(),
495        };
496
497        let agent_id = AgentId::new().to_string();
498        let store_req = Request::new(pb::StoreRequest {
499            agent_id: agent_id.clone(),
500            memory_type: "episodic".into(),
501            content: "The user prefers dark mode".into(),
502            embedding: vec![],
503            tags: vec!["preference".into()],
504            attributes: HashMap::new(),
505            space_id: String::new(),
506            salience: 0.8,
507            confidence: 1.0,
508            valid_from: None,
509            valid_until: None,
510        });
511
512        let resp = svc.store(store_req).await.unwrap();
513        let inner = resp.into_inner();
514        assert_eq!(inner.status, "stored");
515        assert!(!inner.id.is_empty());
516
517        let recall_req = Request::new(pb::RecallRequest {
518            query: "RECALL memories LIMIT 100".into(),
519        });
520        let resp = svc.recall(recall_req).await.unwrap();
521        let inner = resp.into_inner();
522        // Recall succeeded without error; count may be 0 if the engine has not
523        // indexed the memory yet, so we just verify the response is valid.
524        let _ = inner.memory_count;
525    }
526
527    #[tokio::test]
528    async fn test_grpc_memory_forget() {
529        let (state, _tmp) = make_test_state();
530        let svc = MemoryServiceImpl {
531            state: state.clone(),
532        };
533
534        let agent_id = AgentId::new().to_string();
535        let store_resp = svc
536            .store(Request::new(pb::StoreRequest {
537                agent_id,
538                memory_type: "semantic".into(),
539                content: "Temporary memory".into(),
540                embedding: vec![],
541                tags: vec![],
542                attributes: HashMap::new(),
543                space_id: String::new(),
544                salience: 0.5,
545                confidence: 1.0,
546                valid_from: None,
547                valid_until: None,
548            }))
549            .await
550            .unwrap();
551        let stored_id = store_resp.into_inner().id;
552
553        let forget_resp = svc
554            .forget(Request::new(pb::ForgetRequest {
555                id: stored_id.clone(),
556            }))
557            .await
558            .unwrap();
559        assert_eq!(forget_resp.into_inner().status, "deleted");
560    }
561
562    #[tokio::test]
563    async fn test_grpc_memory_relate() {
564        let (state, _tmp) = make_test_state();
565        let svc = MemoryServiceImpl {
566            state: state.clone(),
567        };
568
569        let agent_id = AgentId::new().to_string();
570        let id1 = svc
571            .store(Request::new(pb::StoreRequest {
572                agent_id: agent_id.clone(),
573                memory_type: "episodic".into(),
574                content: "Event A".into(),
575                embedding: vec![],
576                tags: vec![],
577                attributes: HashMap::new(),
578                space_id: String::new(),
579                salience: 0.5,
580                confidence: 1.0,
581                valid_from: None,
582                valid_until: None,
583            }))
584            .await
585            .unwrap()
586            .into_inner()
587            .id;
588
589        let id2 = svc
590            .store(Request::new(pb::StoreRequest {
591                agent_id,
592                memory_type: "episodic".into(),
593                content: "Event B".into(),
594                embedding: vec![],
595                tags: vec![],
596                attributes: HashMap::new(),
597                space_id: String::new(),
598                salience: 0.5,
599                confidence: 1.0,
600                valid_from: None,
601                valid_until: None,
602            }))
603            .await
604            .unwrap()
605            .into_inner()
606            .id;
607
608        let relate_resp = svc
609            .relate(Request::new(pb::RelateRequest {
610                source: id1,
611                target: id2,
612                edge_type: "caused".into(),
613                weight: 0.9,
614                valid_from: None,
615                valid_until: None,
616            }))
617            .await
618            .unwrap();
619        assert_eq!(relate_resp.into_inner().status, "created");
620    }
621
622    #[tokio::test]
623    async fn test_grpc_cognition_stream() {
624        use pb::cognition_service_server::CognitionServiceServer;
625        use tokio::net::TcpListener;
626        use tonic::transport::{Channel, Server};
627
628        let (state, _tmp) = make_test_state();
629
630        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
631        let addr = listener.local_addr().unwrap();
632
633        let svc = CognitionServiceServer::new(CognitionServiceImpl {
634            state: state.clone(),
635        });
636
637        tokio::spawn(async move {
638            let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
639            Server::builder()
640                .add_service(svc)
641                .serve_with_incoming(incoming)
642                .await
643                .unwrap();
644        });
645
646        // Small delay to let the server start
647        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
648
649        let channel = Channel::from_shared(format!("http://{addr}"))
650            .unwrap()
651            .connect()
652            .await
653            .unwrap();
654
655        let mut client = pb::cognition_service_client::CognitionServiceClient::new(channel);
656
657        let (tx, rx) = mpsc::channel(32);
658        let outbound = ReceiverStream::new(rx);
659
660        let response = client.stream_cognition(outbound).await.unwrap();
661        let mut inbound = response.into_inner();
662
663        // Register a known fact
664        let mid = MemoryId::new();
665        tx.send(pb::CognitionRequest {
666            payload: Some(pb::cognition_request::Payload::KnownFact(pb::KnownFact {
667                memory_id: mid.to_string(),
668                content: "The system uses PostgreSQL for storage".into(),
669            })),
670        })
671        .await
672        .unwrap();
673
674        // Feed tokens that contradict the known fact
675        tx.send(pb::CognitionRequest {
676            payload: Some(pb::cognition_request::Payload::Token(pb::TokenPayload {
677                text: "The system does not use PostgreSQL, actually it uses MySQL".into(),
678            })),
679        })
680        .await
681        .unwrap();
682
683        // We should get a contradiction alert
684        if let Some(Ok(alert)) = inbound.next().await {
685            match alert.alert {
686                Some(pb::cognition_alert::Alert::Contradiction(c)) => {
687                    assert_eq!(c.memory_id, mid.to_string());
688                    assert!(!c.ai_said.is_empty());
689                    assert!(!c.stored.is_empty());
690                }
691                other => panic!("expected contradiction alert, got: {:?}", other),
692            }
693        } else {
694            panic!("expected an alert from the cognition stream");
695        }
696
697        // Test flush
698        tx.send(pb::CognitionRequest {
699            payload: Some(pb::cognition_request::Payload::Flush(pb::FlushBuffer {})),
700        })
701        .await
702        .unwrap();
703
704        if let Some(Ok(alert)) = inbound.next().await {
705            match alert.alert {
706                Some(pb::cognition_alert::Alert::BufferFlushed(f)) => {
707                    assert!(!f.accumulated_text.is_empty());
708                }
709                other => panic!("expected buffer_flushed alert, got: {:?}", other),
710            }
711        }
712
713        drop(tx);
714    }
715
716    #[tokio::test]
717    async fn test_grpc_invalid_uuid() {
718        let (state, _tmp) = make_test_state();
719        let svc = MemoryServiceImpl { state };
720
721        let result = svc
722            .store(Request::new(pb::StoreRequest {
723                agent_id: "not-a-uuid".into(),
724                memory_type: "episodic".into(),
725                content: "test".into(),
726                embedding: vec![],
727                tags: vec![],
728                attributes: HashMap::new(),
729                space_id: String::new(),
730                salience: 0.5,
731                confidence: 1.0,
732                valid_from: None,
733                valid_until: None,
734            }))
735            .await;
736
737        assert!(result.is_err());
738        let status = result.unwrap_err();
739        assert_eq!(status.code(), tonic::Code::InvalidArgument);
740    }
741
742    #[tokio::test]
743    async fn test_grpc_invalid_memory_type() {
744        let (state, _tmp) = make_test_state();
745        let svc = MemoryServiceImpl { state };
746
747        let result = svc
748            .store(Request::new(pb::StoreRequest {
749                agent_id: AgentId::new().to_string(),
750                memory_type: "nonexistent".into(),
751                content: "test".into(),
752                embedding: vec![],
753                tags: vec![],
754                attributes: HashMap::new(),
755                space_id: String::new(),
756                salience: 0.5,
757                confidence: 1.0,
758                valid_from: None,
759                valid_until: None,
760            }))
761            .await;
762
763        assert!(result.is_err());
764        let status = result.unwrap_err();
765        assert_eq!(status.code(), tonic::Code::InvalidArgument);
766    }
767
768    #[tokio::test]
769    async fn test_grpc_search_missing_embedding() {
770        let (state, _tmp) = make_test_state();
771        let svc = MemoryServiceImpl { state };
772
773        let result = svc
774            .search(Request::new(pb::SearchRequest {
775                embedding: vec![],
776                k: 10,
777            }))
778            .await;
779
780        assert!(result.is_err());
781        assert_eq!(result.unwrap_err().code(), tonic::Code::InvalidArgument);
782    }
783
784    fn make_auth_test_state(secret: &str) -> (Arc<AppState>, TempDir) {
785        let tmp = TempDir::new().unwrap();
786        let databases = Arc::new(
787            crate::databases::Databases::open(
788                tmp.path(),
789                None,
790                Default::default(),
791                Default::default(),
792            )
793            .unwrap(),
794        );
795        (
796            Arc::new(AppState {
797                db: databases.default_db(),
798                databases: databases.clone(),
799                verifier: Some(Arc::new(crate::auth::TokenVerifier::shared_secret(secret))),
800                spaces: Arc::new(tokio::sync::RwLock::new(mentedb_core::SpaceManager::new())),
801                jwt_secret: Some(secret.into()),
802                admin_key: Some("ak".into()),
803                start_time: Instant::now(),
804                extraction_config: None,
805                auto_extract: false,
806                extraction_tx: None,
807                cluster: None,
808                turn_trackers: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
809            }),
810            tmp,
811        )
812    }
813    #[tokio::test]
814    async fn test_grpc_auth_required_when_secret_set() {
815        let (s, _t) = make_auth_test_state("s");
816        let svc = MemoryServiceImpl { state: s };
817        let r = svc
818            .recall(Request::new(pb::RecallRequest {
819                query: "RECALL memories LIMIT 10".into(),
820            }))
821            .await;
822        assert!(r.is_err());
823        assert_eq!(r.unwrap_err().code(), tonic::Code::Unauthenticated);
824    }
825    #[tokio::test]
826    async fn test_grpc_auth_succeeds_with_valid_token() {
827        let (s, _t) = make_auth_test_state("s");
828        let svc = MemoryServiceImpl { state: s };
829        let a = AgentId::new();
830        let tok = crate::auth::create_token("s", &a.to_string(), false, 1);
831        let mut r = Request::new(pb::StoreRequest {
832            agent_id: a.to_string(),
833            memory_type: "episodic".into(),
834            content: "t".into(),
835            embedding: vec![],
836            tags: vec![],
837            attributes: HashMap::new(),
838            space_id: String::new(),
839            salience: 0.5,
840            confidence: 1.0,
841            valid_from: None,
842            valid_until: None,
843        });
844        r.metadata_mut()
845            .insert("authorization", format!("Bearer {tok}").parse().unwrap());
846        assert_eq!(svc.store(r).await.unwrap().into_inner().status, "stored");
847    }
848    #[tokio::test]
849    async fn test_grpc_auth_rejects_wrong_agent_id() {
850        let (s, _t) = make_auth_test_state("s");
851        let svc = MemoryServiceImpl { state: s };
852        let ta = AgentId::new();
853        let ra = AgentId::new();
854        let tok = crate::auth::create_token("s", &ta.to_string(), false, 1);
855        let mut r = Request::new(pb::StoreRequest {
856            agent_id: ra.to_string(),
857            memory_type: "episodic".into(),
858            content: "t".into(),
859            embedding: vec![],
860            tags: vec![],
861            attributes: HashMap::new(),
862            space_id: String::new(),
863            salience: 0.5,
864            confidence: 1.0,
865            valid_from: None,
866            valid_until: None,
867        });
868        r.metadata_mut()
869            .insert("authorization", format!("Bearer {tok}").parse().unwrap());
870        assert_eq!(
871            svc.store(r).await.unwrap_err().code(),
872            tonic::Code::PermissionDenied
873        );
874    }
875    fn store_request(agent: AgentId, token: &str) -> Request<pb::StoreRequest> {
876        let mut r = Request::new(pb::StoreRequest {
877            agent_id: agent.to_string(),
878            memory_type: "episodic".into(),
879            content: "t".into(),
880            embedding: vec![],
881            tags: vec![],
882            attributes: HashMap::new(),
883            space_id: String::new(),
884            salience: 0.5,
885            confidence: 1.0,
886            valid_from: None,
887            valid_until: None,
888        });
889        r.metadata_mut()
890            .insert("authorization", format!("Bearer {token}").parse().unwrap());
891        r
892    }
893
894    #[tokio::test]
895    async fn test_grpc_enforces_scope_and_dbs() {
896        use crate::auth::{Scope, TokenOptions, create_token_with};
897        let (s, _t) = make_auth_test_state("s");
898        let svc = MemoryServiceImpl { state: s };
899        let a = AgentId::new();
900        let read = create_token_with(
901            "s",
902            "",
903            false,
904            1,
905            TokenOptions {
906                scope: Some(Scope::Read),
907                ..Default::default()
908            },
909        );
910        assert_eq!(
911            svc.store(store_request(a, &read)).await.unwrap_err().code(),
912            tonic::Code::PermissionDenied,
913            "read scope cannot store"
914        );
915        let mut recall = Request::new(pb::RecallRequest {
916            query: "RECALL memories LIMIT 1".into(),
917        });
918        recall
919            .metadata_mut()
920            .insert("authorization", format!("Bearer {read}").parse().unwrap());
921        assert!(svc.recall(recall).await.is_ok(), "read scope can recall");
922
923        let other_db = create_token_with(
924            "s",
925            "",
926            false,
927            1,
928            TokenOptions {
929                dbs: Some(vec!["tenant.alice.notes".into()]),
930                ..Default::default()
931            },
932        );
933        assert_eq!(
934            svc.store(store_request(a, &other_db))
935                .await
936                .unwrap_err()
937                .code(),
938            tonic::Code::PermissionDenied,
939            "a token for another database cannot use the default one"
940        );
941        let default_db = create_token_with(
942            "s",
943            "",
944            false,
945            1,
946            TokenOptions {
947                dbs: Some(vec!["default".into()]),
948                ..Default::default()
949            },
950        );
951        assert_eq!(
952            svc.store(store_request(a, &default_db))
953                .await
954                .unwrap()
955                .into_inner()
956                .status,
957            "stored"
958        );
959    }
960
961    #[tokio::test]
962    async fn test_grpc_auth_invalid_token() {
963        let (s, _t) = make_auth_test_state("s");
964        let svc = MemoryServiceImpl { state: s };
965        let mut r = Request::new(pb::RecallRequest {
966            query: "RECALL memories LIMIT 10".into(),
967        });
968        r.metadata_mut()
969            .insert("authorization", "Bearer bad.tok".parse().unwrap());
970        assert_eq!(
971            svc.recall(r).await.unwrap_err().code(),
972            tonic::Code::Unauthenticated
973        );
974    }
975}