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
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
31pub 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(¤t_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(¤t_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
168pub struct MemoryServiceImpl {
174 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#[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#[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 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 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 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 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 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 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}