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