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