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, UserId};
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            user_id: UserId::nil(),
231            memory_type,
232            embedding: req.embedding,
233            content: req.content,
234            created_at: now,
235            accessed_at: now,
236            access_count: 0,
237            salience,
238            confidence,
239            space_id,
240            attributes,
241            tags: req.tags,
242            valid_from: req.valid_from,
243            valid_until: req.valid_until,
244            context: None,
245        };
246
247        let db = &*self.state.db;
248        db.store(node).map_err(|e| {
249            error!("gRPC store failed: {e}");
250            Status::internal(format!("store failed: {e}"))
251        })?;
252
253        Ok(Response::new(pb::StoreResponse {
254            id: id.to_string(),
255            status: "stored".into(),
256        }))
257    }
258
259    async fn recall(
260        &self,
261        request: Request<pb::RecallRequest>,
262    ) -> Result<Response<pb::RecallResponse>, Status> {
263        authenticate_grpc_request(&self.state, &request)?;
264        let req = request.into_inner();
265
266        let db = &*self.state.db;
267        let window = db.recall(&req.query).map_err(|e| {
268            error!("gRPC recall failed: {e}");
269            Status::internal(format!("recall failed: {e}"))
270        })?;
271
272        let memory_count: usize = window.blocks.iter().map(|b| b.memories.len()).sum();
273
274        Ok(Response::new(pb::RecallResponse {
275            context: window.format,
276            total_tokens: window.total_tokens as u64,
277            memory_count: memory_count as u64,
278        }))
279    }
280
281    async fn search(
282        &self,
283        request: Request<pb::SearchRequest>,
284    ) -> Result<Response<pb::SearchResponse>, Status> {
285        authenticate_grpc_request(&self.state, &request)?;
286        let req = request.into_inner();
287        let k = if req.k == 0 { 10 } else { req.k as usize };
288
289        if req.embedding.is_empty() {
290            return Err(Status::invalid_argument("missing embedding vector"));
291        }
292
293        let db = &*self.state.db;
294        let results = db.recall_similar(&req.embedding, k).map_err(|e| {
295            error!("gRPC search failed: {e}");
296            Status::internal(format!("search failed: {e}"))
297        })?;
298
299        let items: Vec<pb::SearchResult> = results
300            .iter()
301            .map(|(id, score)| pb::SearchResult {
302                id: id.to_string(),
303                score: *score,
304            })
305            .collect();
306
307        Ok(Response::new(pb::SearchResponse { results: items }))
308    }
309
310    async fn forget(
311        &self,
312        request: Request<pb::ForgetRequest>,
313    ) -> Result<Response<pb::ForgetResponse>, Status> {
314        authenticate_grpc_request(&self.state, &request)?;
315        let req = request.into_inner();
316        let id = MemoryId(parse_uuid_field(&req.id, "id")?);
317
318        let db = &*self.state.db;
319        db.forget(id).map_err(|e| {
320            error!("gRPC forget failed: {e}");
321            Status::internal(format!("forget failed: {e}"))
322        })?;
323
324        Ok(Response::new(pb::ForgetResponse {
325            status: "deleted".into(),
326        }))
327    }
328
329    async fn relate(
330        &self,
331        request: Request<pb::RelateRequest>,
332    ) -> Result<Response<pb::RelateResponse>, Status> {
333        authenticate_grpc_request(&self.state, &request)?;
334        let req = request.into_inner();
335        let source = MemoryId(parse_uuid_field(&req.source, "source")?);
336        let target = MemoryId(parse_uuid_field(&req.target, "target")?);
337        let edge_type = parse_edge_type_str(&req.edge_type)?;
338        let weight = if req.weight == 0.0 { 1.0 } else { req.weight };
339
340        let now = std::time::SystemTime::now()
341            .duration_since(std::time::UNIX_EPOCH)
342            .unwrap_or_default()
343            .as_micros() as u64;
344
345        let edge = MemoryEdge {
346            source,
347            target,
348            edge_type,
349            weight,
350            created_at: now,
351            valid_from: req.valid_from,
352            valid_until: req.valid_until,
353            label: None,
354        };
355
356        let db = &*self.state.db;
357        db.relate(edge).map_err(|e| {
358            error!("gRPC relate failed: {e}");
359            Status::internal(format!("relate failed: {e}"))
360        })?;
361
362        Ok(Response::new(pb::RelateResponse {
363            status: "created".into(),
364        }))
365    }
366}
367
368#[allow(clippy::result_large_err)]
369fn authenticate_grpc_request<T>(
370    state: &AppState,
371    request: &Request<T>,
372) -> Result<Option<String>, Status> {
373    let secret = match &state.jwt_secret {
374        Some(s) => s,
375        None => return Ok(None),
376    };
377    let token = request
378        .metadata()
379        .get("authorization")
380        .and_then(|v| v.to_str().ok())
381        .and_then(|v| v.strip_prefix("Bearer "))
382        .ok_or_else(|| Status::unauthenticated("missing or invalid authorization metadata"))?;
383    auth::validate_token(secret, token)
384        .map(|c| Some(c.agent_id))
385        .map_err(|e| Status::unauthenticated(format!("invalid token: {e}")))
386}
387#[allow(clippy::result_large_err)]
388fn authenticate_grpc_streaming(
389    state: &AppState,
390    metadata: &tonic::metadata::MetadataMap,
391) -> Result<Option<String>, Status> {
392    let secret = match &state.jwt_secret {
393        Some(s) => s,
394        None => return Ok(None),
395    };
396    let token = metadata
397        .get("authorization")
398        .and_then(|v| v.to_str().ok())
399        .and_then(|v| v.strip_prefix("Bearer "))
400        .ok_or_else(|| Status::unauthenticated("missing or invalid authorization metadata"))?;
401    auth::validate_token(secret, token)
402        .map(|c| Some(c.agent_id))
403        .map_err(|e| Status::unauthenticated(format!("invalid token: {e}")))
404}
405
406// ---------------------------------------------------------------------------
407// Helpers
408// ---------------------------------------------------------------------------
409
410#[allow(clippy::result_large_err)]
411fn parse_uuid_field(s: &str, field: &str) -> Result<Uuid, Status> {
412    Uuid::parse_str(s)
413        .map_err(|_| Status::invalid_argument(format!("invalid UUID for '{field}': {s}")))
414}
415
416#[allow(clippy::result_large_err)]
417fn parse_memory_type_str(s: &str) -> Result<MemoryType, Status> {
418    match s.to_lowercase().as_str() {
419        "episodic" => Ok(MemoryType::Episodic),
420        "semantic" => Ok(MemoryType::Semantic),
421        "procedural" => Ok(MemoryType::Procedural),
422        "antipattern" | "anti_pattern" => Ok(MemoryType::AntiPattern),
423        "reasoning" => Ok(MemoryType::Reasoning),
424        "correction" => Ok(MemoryType::Correction),
425        _ => Err(Status::invalid_argument(format!(
426            "unknown memory_type: {s}"
427        ))),
428    }
429}
430
431#[allow(clippy::result_large_err)]
432fn parse_edge_type_str(s: &str) -> Result<EdgeType, Status> {
433    match s.to_lowercase().as_str() {
434        "caused" => Ok(EdgeType::Caused),
435        "before" => Ok(EdgeType::Before),
436        "related" => Ok(EdgeType::Related),
437        "contradicts" => Ok(EdgeType::Contradicts),
438        "supports" => Ok(EdgeType::Supports),
439        "supersedes" => Ok(EdgeType::Supersedes),
440        "derived" => Ok(EdgeType::Derived),
441        "partof" | "part_of" => Ok(EdgeType::PartOf),
442        _ => Err(Status::invalid_argument(format!("unknown edge_type: {s}"))),
443    }
444}
445
446// ---------------------------------------------------------------------------
447// Tests
448// ---------------------------------------------------------------------------
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453    use mentedb::MenteDb;
454    use std::time::Instant;
455    use tempfile::TempDir;
456
457    fn make_test_state() -> (Arc<AppState>, TempDir) {
458        let tmp = TempDir::new().unwrap();
459        let db = MenteDb::open(tmp.path()).unwrap();
460        let state = Arc::new(AppState {
461            db: Arc::new(db),
462            spaces: Arc::new(tokio::sync::RwLock::new(mentedb_core::SpaceManager::new())),
463            jwt_secret: None,
464            admin_key: None,
465            start_time: Instant::now(),
466            extraction_config: None,
467            auto_extract: false,
468            extraction_tx: None,
469            cluster: None,
470            turn_trackers: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
471        });
472        (state, tmp)
473    }
474
475    #[tokio::test]
476    async fn test_grpc_memory_store_and_recall() {
477        let (state, _tmp) = make_test_state();
478        let svc = MemoryServiceImpl {
479            state: state.clone(),
480        };
481
482        let agent_id = AgentId::new().to_string();
483        let store_req = Request::new(pb::StoreRequest {
484            agent_id: agent_id.clone(),
485            memory_type: "episodic".into(),
486            content: "The user prefers dark mode".into(),
487            embedding: vec![],
488            tags: vec!["preference".into()],
489            attributes: HashMap::new(),
490            space_id: String::new(),
491            salience: 0.8,
492            confidence: 1.0,
493            valid_from: None,
494            valid_until: None,
495        });
496
497        let resp = svc.store(store_req).await.unwrap();
498        let inner = resp.into_inner();
499        assert_eq!(inner.status, "stored");
500        assert!(!inner.id.is_empty());
501
502        let recall_req = Request::new(pb::RecallRequest {
503            query: "RECALL memories LIMIT 100".into(),
504        });
505        let resp = svc.recall(recall_req).await.unwrap();
506        let inner = resp.into_inner();
507        // Recall succeeded without error; count may be 0 if the engine has not
508        // indexed the memory yet, so we just verify the response is valid.
509        let _ = inner.memory_count;
510    }
511
512    #[tokio::test]
513    async fn test_grpc_memory_forget() {
514        let (state, _tmp) = make_test_state();
515        let svc = MemoryServiceImpl {
516            state: state.clone(),
517        };
518
519        let agent_id = AgentId::new().to_string();
520        let store_resp = svc
521            .store(Request::new(pb::StoreRequest {
522                agent_id,
523                memory_type: "semantic".into(),
524                content: "Temporary memory".into(),
525                embedding: vec![],
526                tags: vec![],
527                attributes: HashMap::new(),
528                space_id: String::new(),
529                salience: 0.5,
530                confidence: 1.0,
531                valid_from: None,
532                valid_until: None,
533            }))
534            .await
535            .unwrap();
536        let stored_id = store_resp.into_inner().id;
537
538        let forget_resp = svc
539            .forget(Request::new(pb::ForgetRequest {
540                id: stored_id.clone(),
541            }))
542            .await
543            .unwrap();
544        assert_eq!(forget_resp.into_inner().status, "deleted");
545    }
546
547    #[tokio::test]
548    async fn test_grpc_memory_relate() {
549        let (state, _tmp) = make_test_state();
550        let svc = MemoryServiceImpl {
551            state: state.clone(),
552        };
553
554        let agent_id = AgentId::new().to_string();
555        let id1 = svc
556            .store(Request::new(pb::StoreRequest {
557                agent_id: agent_id.clone(),
558                memory_type: "episodic".into(),
559                content: "Event A".into(),
560                embedding: vec![],
561                tags: vec![],
562                attributes: HashMap::new(),
563                space_id: String::new(),
564                salience: 0.5,
565                confidence: 1.0,
566                valid_from: None,
567                valid_until: None,
568            }))
569            .await
570            .unwrap()
571            .into_inner()
572            .id;
573
574        let id2 = svc
575            .store(Request::new(pb::StoreRequest {
576                agent_id,
577                memory_type: "episodic".into(),
578                content: "Event B".into(),
579                embedding: vec![],
580                tags: vec![],
581                attributes: HashMap::new(),
582                space_id: String::new(),
583                salience: 0.5,
584                confidence: 1.0,
585                valid_from: None,
586                valid_until: None,
587            }))
588            .await
589            .unwrap()
590            .into_inner()
591            .id;
592
593        let relate_resp = svc
594            .relate(Request::new(pb::RelateRequest {
595                source: id1,
596                target: id2,
597                edge_type: "caused".into(),
598                weight: 0.9,
599                valid_from: None,
600                valid_until: None,
601            }))
602            .await
603            .unwrap();
604        assert_eq!(relate_resp.into_inner().status, "created");
605    }
606
607    #[tokio::test]
608    async fn test_grpc_cognition_stream() {
609        use pb::cognition_service_server::CognitionServiceServer;
610        use tokio::net::TcpListener;
611        use tonic::transport::{Channel, Server};
612
613        let (state, _tmp) = make_test_state();
614
615        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
616        let addr = listener.local_addr().unwrap();
617
618        let svc = CognitionServiceServer::new(CognitionServiceImpl {
619            state: state.clone(),
620        });
621
622        tokio::spawn(async move {
623            let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
624            Server::builder()
625                .add_service(svc)
626                .serve_with_incoming(incoming)
627                .await
628                .unwrap();
629        });
630
631        // Small delay to let the server start
632        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
633
634        let channel = Channel::from_shared(format!("http://{addr}"))
635            .unwrap()
636            .connect()
637            .await
638            .unwrap();
639
640        let mut client = pb::cognition_service_client::CognitionServiceClient::new(channel);
641
642        let (tx, rx) = mpsc::channel(32);
643        let outbound = ReceiverStream::new(rx);
644
645        let response = client.stream_cognition(outbound).await.unwrap();
646        let mut inbound = response.into_inner();
647
648        // Register a known fact
649        let mid = MemoryId::new();
650        tx.send(pb::CognitionRequest {
651            payload: Some(pb::cognition_request::Payload::KnownFact(pb::KnownFact {
652                memory_id: mid.to_string(),
653                content: "The system uses PostgreSQL for storage".into(),
654            })),
655        })
656        .await
657        .unwrap();
658
659        // Feed tokens that contradict the known fact
660        tx.send(pb::CognitionRequest {
661            payload: Some(pb::cognition_request::Payload::Token(pb::TokenPayload {
662                text: "The system does not use PostgreSQL, actually it uses MySQL".into(),
663            })),
664        })
665        .await
666        .unwrap();
667
668        // We should get a contradiction alert
669        if let Some(Ok(alert)) = inbound.next().await {
670            match alert.alert {
671                Some(pb::cognition_alert::Alert::Contradiction(c)) => {
672                    assert_eq!(c.memory_id, mid.to_string());
673                    assert!(!c.ai_said.is_empty());
674                    assert!(!c.stored.is_empty());
675                }
676                other => panic!("expected contradiction alert, got: {:?}", other),
677            }
678        } else {
679            panic!("expected an alert from the cognition stream");
680        }
681
682        // Test flush
683        tx.send(pb::CognitionRequest {
684            payload: Some(pb::cognition_request::Payload::Flush(pb::FlushBuffer {})),
685        })
686        .await
687        .unwrap();
688
689        if let Some(Ok(alert)) = inbound.next().await {
690            match alert.alert {
691                Some(pb::cognition_alert::Alert::BufferFlushed(f)) => {
692                    assert!(!f.accumulated_text.is_empty());
693                }
694                other => panic!("expected buffer_flushed alert, got: {:?}", other),
695            }
696        }
697
698        drop(tx);
699    }
700
701    #[tokio::test]
702    async fn test_grpc_invalid_uuid() {
703        let (state, _tmp) = make_test_state();
704        let svc = MemoryServiceImpl { state };
705
706        let result = svc
707            .store(Request::new(pb::StoreRequest {
708                agent_id: "not-a-uuid".into(),
709                memory_type: "episodic".into(),
710                content: "test".into(),
711                embedding: vec![],
712                tags: vec![],
713                attributes: HashMap::new(),
714                space_id: String::new(),
715                salience: 0.5,
716                confidence: 1.0,
717                valid_from: None,
718                valid_until: None,
719            }))
720            .await;
721
722        assert!(result.is_err());
723        let status = result.unwrap_err();
724        assert_eq!(status.code(), tonic::Code::InvalidArgument);
725    }
726
727    #[tokio::test]
728    async fn test_grpc_invalid_memory_type() {
729        let (state, _tmp) = make_test_state();
730        let svc = MemoryServiceImpl { state };
731
732        let result = svc
733            .store(Request::new(pb::StoreRequest {
734                agent_id: AgentId::new().to_string(),
735                memory_type: "nonexistent".into(),
736                content: "test".into(),
737                embedding: vec![],
738                tags: vec![],
739                attributes: HashMap::new(),
740                space_id: String::new(),
741                salience: 0.5,
742                confidence: 1.0,
743                valid_from: None,
744                valid_until: None,
745            }))
746            .await;
747
748        assert!(result.is_err());
749        let status = result.unwrap_err();
750        assert_eq!(status.code(), tonic::Code::InvalidArgument);
751    }
752
753    #[tokio::test]
754    async fn test_grpc_search_missing_embedding() {
755        let (state, _tmp) = make_test_state();
756        let svc = MemoryServiceImpl { state };
757
758        let result = svc
759            .search(Request::new(pb::SearchRequest {
760                embedding: vec![],
761                k: 10,
762            }))
763            .await;
764
765        assert!(result.is_err());
766        assert_eq!(result.unwrap_err().code(), tonic::Code::InvalidArgument);
767    }
768
769    fn make_auth_test_state(secret: &str) -> (Arc<AppState>, TempDir) {
770        let tmp = TempDir::new().unwrap();
771        let db = MenteDb::open(tmp.path()).unwrap();
772        (
773            Arc::new(AppState {
774                db: Arc::new(db),
775                spaces: Arc::new(tokio::sync::RwLock::new(mentedb_core::SpaceManager::new())),
776                jwt_secret: Some(secret.into()),
777                admin_key: Some("ak".into()),
778                start_time: Instant::now(),
779                extraction_config: None,
780                auto_extract: false,
781                extraction_tx: None,
782                cluster: None,
783                turn_trackers: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
784            }),
785            tmp,
786        )
787    }
788    #[tokio::test]
789    async fn test_grpc_auth_required_when_secret_set() {
790        let (s, _t) = make_auth_test_state("s");
791        let svc = MemoryServiceImpl { state: s };
792        let r = svc
793            .recall(Request::new(pb::RecallRequest {
794                query: "RECALL memories LIMIT 10".into(),
795            }))
796            .await;
797        assert!(r.is_err());
798        assert_eq!(r.unwrap_err().code(), tonic::Code::Unauthenticated);
799    }
800    #[tokio::test]
801    async fn test_grpc_auth_succeeds_with_valid_token() {
802        let (s, _t) = make_auth_test_state("s");
803        let svc = MemoryServiceImpl { state: s };
804        let a = AgentId::new();
805        let tok = crate::auth::create_token("s", &a.to_string(), false, 1);
806        let mut r = Request::new(pb::StoreRequest {
807            agent_id: a.to_string(),
808            memory_type: "episodic".into(),
809            content: "t".into(),
810            embedding: vec![],
811            tags: vec![],
812            attributes: HashMap::new(),
813            space_id: String::new(),
814            salience: 0.5,
815            confidence: 1.0,
816            valid_from: None,
817            valid_until: None,
818        });
819        r.metadata_mut()
820            .insert("authorization", format!("Bearer {tok}").parse().unwrap());
821        assert_eq!(svc.store(r).await.unwrap().into_inner().status, "stored");
822    }
823    #[tokio::test]
824    async fn test_grpc_auth_rejects_wrong_agent_id() {
825        let (s, _t) = make_auth_test_state("s");
826        let svc = MemoryServiceImpl { state: s };
827        let ta = AgentId::new();
828        let ra = AgentId::new();
829        let tok = crate::auth::create_token("s", &ta.to_string(), false, 1);
830        let mut r = Request::new(pb::StoreRequest {
831            agent_id: ra.to_string(),
832            memory_type: "episodic".into(),
833            content: "t".into(),
834            embedding: vec![],
835            tags: vec![],
836            attributes: HashMap::new(),
837            space_id: String::new(),
838            salience: 0.5,
839            confidence: 1.0,
840            valid_from: None,
841            valid_until: None,
842        });
843        r.metadata_mut()
844            .insert("authorization", format!("Bearer {tok}").parse().unwrap());
845        assert_eq!(
846            svc.store(r).await.unwrap_err().code(),
847            tonic::Code::PermissionDenied
848        );
849    }
850    #[tokio::test]
851    async fn test_grpc_auth_invalid_token() {
852        let (s, _t) = make_auth_test_state("s");
853        let svc = MemoryServiceImpl { state: s };
854        let mut r = Request::new(pb::RecallRequest {
855            query: "RECALL memories LIMIT 10".into(),
856        });
857        r.metadata_mut()
858            .insert("authorization", "Bearer bad.tok".parse().unwrap());
859        assert_eq!(
860            svc.recall(r).await.unwrap_err().code(),
861            tonic::Code::Unauthenticated
862        );
863    }
864}