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