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