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