1use std::sync::Arc;
18
19use tonic::{Request, Response, Status};
20use uuid::Uuid;
21
22use mnemo_core::model::acl::Permission;
23use mnemo_core::model::delegation::{Delegation, DelegationScope};
24use mnemo_core::model::memory::{MemoryType, Scope, SourceType};
25use mnemo_core::query::MnemoEngine;
26use mnemo_core::query::branch::BranchRequest as CoreBranchRequest;
27use mnemo_core::query::checkpoint::CheckpointRequest as CoreCheckpointRequest;
28use mnemo_core::query::consolidate::ConsolidateRequest as CoreConsolidateRequest;
29use mnemo_core::query::forget::{
30 ForgetRequest as CoreForgetRequest, ForgetStrategy,
31 ForgetSubjectRequest as CoreForgetSubjectRequest,
32};
33use mnemo_core::query::merge::{MergeRequest as CoreMergeRequest, MergeStrategy};
34use mnemo_core::query::recall::RecallRequest as CoreRecallRequest;
35use mnemo_core::query::remember::RememberRequest as CoreRememberRequest;
36use mnemo_core::query::replay::ReplayRequest as CoreReplayRequest;
37use mnemo_core::query::share::ShareRequest as CoreShareRequest;
38
39pub mod proto {
44 tonic::include_proto!("mnemo.v1");
45}
46
47use proto::mnemo_service_server::{MnemoService, MnemoServiceServer};
48use proto::{
49 BranchRequest as ProtoBranchRequest, BranchResponse as ProtoBranchResponse,
50 CheckpointRequest as ProtoCheckpointRequest, CheckpointResponse as ProtoCheckpointResponse,
51 ConsolidateRequest as ProtoConsolidateRequest, ConsolidateResponse as ProtoConsolidateResponse,
52 DelegateRequest as ProtoDelegateRequest, DelegateResponse as ProtoDelegateResponse,
53 ForgetError as ProtoForgetError, ForgetRequest as ProtoForgetRequest,
54 ForgetResponse as ProtoForgetResponse, ForgetSubjectRequest as ProtoForgetSubjectRequest,
55 ForgetSubjectResponse as ProtoForgetSubjectResponse, HealthRequest, HealthResponse,
56 MergeRequest as ProtoMergeRequest, MergeResponse as ProtoMergeResponse,
57 RecallRequest as ProtoRecallRequest, RecallResponse as ProtoRecallResponse,
58 RememberRequest as ProtoRememberRequest, RememberResponse as ProtoRememberResponse,
59 ReplayMemory as ProtoReplayMemory, ReplayRequest as ProtoReplayRequest,
60 ReplayResponse as ProtoReplayResponse, ScoredMemory as ProtoScoredMemory,
61 ShareRequest as ProtoShareRequest, ShareResponse as ProtoShareResponse,
62 TrajectoryAuditRequest as ProtoTrajectoryAuditRequest,
63 TrajectoryAuditResponse as ProtoTrajectoryAuditResponse,
64 TrajectoryFinding as ProtoTrajectoryFinding, VerifyRequest as ProtoVerifyRequest,
65 VerifyResponse as ProtoVerifyResponse,
66};
67
68#[derive(Clone)]
74pub struct MnemoGrpcServer {
75 engine: Arc<MnemoEngine>,
76}
77
78impl MnemoGrpcServer {
79 pub fn new(engine: Arc<MnemoEngine>) -> Self {
81 Self { engine }
82 }
83}
84
85#[tonic::async_trait]
86impl MnemoService for MnemoGrpcServer {
87 async fn remember(
90 &self,
91 request: Request<ProtoRememberRequest>,
92 ) -> Result<Response<ProtoRememberResponse>, Status> {
93 let req = request.into_inner();
94
95 let memory_type = match req.memory_type {
96 Some(ref s) => match s.parse::<MemoryType>() {
97 Ok(mt) => Some(mt),
98 Err(_) => {
99 return Err(Status::invalid_argument(format!(
100 "invalid memory_type '{}': expected one of: episodic, semantic, procedural, working",
101 s
102 )));
103 }
104 },
105 None => None,
106 };
107
108 let scope = match req.scope {
109 Some(ref s) => match s.parse::<Scope>() {
110 Ok(sc) => Some(sc),
111 Err(_) => {
112 return Err(Status::invalid_argument(format!(
113 "invalid scope '{}': expected one of: private, shared, public, global",
114 s
115 )));
116 }
117 },
118 None => None,
119 };
120
121 let source_type = match req.source_type {
122 Some(ref s) => match s.parse::<SourceType>() {
123 Ok(st) => Some(st),
124 Err(_) => {
125 return Err(Status::invalid_argument(format!(
126 "invalid source_type '{}': expected one of: agent, human, system, user_input, tool_output, model_response, retrieval, consolidation, import",
127 s
128 )));
129 }
130 },
131 None => None,
132 };
133
134 let metadata: Option<serde_json::Value> = match req.metadata {
135 Some(ref s) => match serde_json::from_str(s) {
136 Ok(v) => Some(v),
137 Err(e) => {
138 return Err(Status::invalid_argument(format!(
139 "invalid metadata JSON: {}",
140 e
141 )));
142 }
143 },
144 None => None,
145 };
146
147 let tags = if req.tags.is_empty() {
148 None
149 } else {
150 Some(req.tags)
151 };
152
153 let related_to = if req.related_to.is_empty() {
154 None
155 } else {
156 Some(req.related_to)
157 };
158
159 let core_req = CoreRememberRequest {
160 content: req.content,
161 agent_id: req.agent_id,
162 memory_type,
163 scope,
164 importance: req.importance,
165 tags,
166 metadata,
167 source_type,
168 source_id: req.source_id,
169 org_id: req.org_id,
170 thread_id: req.thread_id,
171 ttl_seconds: req.ttl_seconds,
172 related_to,
173 decay_rate: req.decay_rate,
174 created_by: req.created_by,
175 };
176
177 let result = self
178 .engine
179 .remember(core_req)
180 .await
181 .map_err(core_error_to_status)?;
182
183 Ok(Response::new(ProtoRememberResponse {
184 id: result.id.to_string(),
185 content_hash: result.content_hash,
186 }))
187 }
188
189 async fn recall(
192 &self,
193 request: Request<ProtoRecallRequest>,
194 ) -> Result<Response<ProtoRecallResponse>, Status> {
195 let req = request.into_inner();
196
197 let memory_type = match req.memory_type {
198 Some(ref s) => match s.parse::<MemoryType>() {
199 Ok(mt) => Some(mt),
200 Err(_) => {
201 return Err(Status::invalid_argument(format!(
202 "invalid memory_type '{}': expected one of: episodic, semantic, procedural, working",
203 s
204 )));
205 }
206 },
207 None => None,
208 };
209
210 let scope = match req.scope {
211 Some(ref s) => match s.parse::<Scope>() {
212 Ok(sc) => Some(sc),
213 Err(_) => {
214 return Err(Status::invalid_argument(format!(
215 "invalid scope '{}': expected one of: private, shared, public, global",
216 s
217 )));
218 }
219 },
220 None => None,
221 };
222
223 let tags = if req.tags.is_empty() {
224 None
225 } else {
226 Some(req.tags)
227 };
228
229 let hybrid_weights = if req.hybrid_weights.is_empty() {
230 None
231 } else {
232 Some(req.hybrid_weights)
233 };
234
235 let orientation_cache_cfg = req.orientation_cache.map(|o| {
236 mnemo_core::query::orientation_cache::OrientationCacheConfig {
237 namespace: o.namespace,
238 token_budget: o.token_budget,
239 include_in_response: o.include_in_response.unwrap_or(true),
240 distill: o.distill.unwrap_or(true),
241 }
242 });
243 let core_req = CoreRecallRequest {
244 query: req.query,
245 agent_id: req.agent_id,
246 limit: req.limit.map(|l| l as usize),
247 memory_type,
248 memory_types: None,
249 scope,
250 min_importance: req.min_importance,
251 tags,
252 org_id: req.org_id,
253 strategy: req.strategy,
254 temporal_range: None,
255 recency_half_life_hours: None,
256 hybrid_weights,
257 rrf_k: req.rrf_k,
258 as_of: req.as_of,
259 explain: req.explain,
260 with_provenance: None,
261 mode: None,
262 current_fact_resolver: None,
263 orientation_cache: orientation_cache_cfg,
264 evidence_budget: None,
265 retained_token_budget: None,
266 domain_scope: None,
267 reasoning_trust: None,
268 };
269
270 let result = self
271 .engine
272 .recall(core_req)
273 .await
274 .map_err(core_error_to_status)?;
275
276 let memories: Vec<ProtoScoredMemory> = result
277 .memories
278 .into_iter()
279 .map(|m| ProtoScoredMemory {
280 id: m.id.to_string(),
281 content: m.content,
282 memory_type: format!("{:?}", m.memory_type),
283 importance: m.importance,
284 score: m.score,
285 created_at: m.created_at,
286 agent_id: m.agent_id,
287 scope: format!("{:?}", m.scope),
288 tags: m.tags,
289 metadata: m.metadata.to_string(),
290 access_count: m.access_count,
291 updated_at: m.updated_at,
292 score_breakdown: m.score_breakdown.map(|b| proto::ScoreBreakdown {
293 vector: b.vector,
294 bm25: b.bm25,
295 graph: b.graph,
296 recency: b.recency,
297 rrf_rank: b.rrf_rank,
298 }),
299 })
300 .collect();
301
302 let total = result.total as u32;
303
304 let orientation_cache = result
305 .orientation_cache
306 .map(|r| proto::OrientationCacheResponse {
307 namespace: r.namespace,
308 entities: r
309 .entities
310 .into_iter()
311 .map(|e| proto::OrientationEntry {
312 key: e.key,
313 value: e.value,
314 freq: e.freq,
315 token_estimate: e.token_estimate,
316 })
317 .collect(),
318 constants: r
319 .constants
320 .into_iter()
321 .map(|e| proto::OrientationEntry {
322 key: e.key,
323 value: e.value,
324 freq: e.freq,
325 token_estimate: e.token_estimate,
326 })
327 .collect(),
328 schemas: r
329 .schemas
330 .into_iter()
331 .map(|e| proto::OrientationEntry {
332 key: e.key,
333 value: e.value,
334 freq: e.freq,
335 token_estimate: e.token_estimate,
336 })
337 .collect(),
338 token_estimate: r.token_estimate,
339 budget: r.budget,
340 hit_count: r.hit_count,
341 });
342
343 let reconstruction = result.reconstruction.map(|b| proto::Reconstruction {
346 cue: b.cue,
347 summary: b.summary,
348 source_ids: b.source_ids.iter().map(|id| id.to_string()).collect(),
349 linked_context_ids: b
350 .linked_context_ids
351 .iter()
352 .map(|id| id.to_string())
353 .collect(),
354 confidence: b.confidence,
355 });
356
357 Ok(Response::new(ProtoRecallResponse {
358 memories,
359 total,
360 orientation_cache,
361 reconstruction,
362 }))
363 }
364
365 async fn forget(
368 &self,
369 request: Request<ProtoForgetRequest>,
370 ) -> Result<Response<ProtoForgetResponse>, Status> {
371 let req = request.into_inner();
372
373 let memory_ids: Vec<Uuid> = req
374 .memory_ids
375 .iter()
376 .map(|s| {
377 Uuid::parse_str(s)
378 .map_err(|e| Status::invalid_argument(format!("invalid UUID '{s}': {e}")))
379 })
380 .collect::<Result<Vec<_>, _>>()?;
381
382 let strategy = match req.strategy {
383 Some(ref s) => {
384 let st = match s.as_str() {
385 "soft_delete" => ForgetStrategy::SoftDelete,
386 "hard_delete" => ForgetStrategy::HardDelete,
387 "decay" => ForgetStrategy::Decay,
388 "consolidate" => ForgetStrategy::Consolidate,
389 "archive" => ForgetStrategy::Archive,
390 "redact" => ForgetStrategy::Redact,
391 _ => {
392 return Err(Status::invalid_argument(format!(
393 "invalid forget strategy '{}': expected one of: soft_delete, hard_delete, decay, consolidate, archive, redact",
394 s
395 )));
396 }
397 };
398 Some(st)
399 }
400 None => None,
401 };
402
403 let core_req = CoreForgetRequest {
404 memory_ids,
405 agent_id: req.agent_id,
406 strategy,
407 criteria: None,
408 };
409
410 let result = self
411 .engine
412 .forget(core_req)
413 .await
414 .map_err(core_error_to_status)?;
415
416 let forgotten: Vec<String> = result.forgotten.iter().map(|id| id.to_string()).collect();
417
418 let errors: Vec<ProtoForgetError> = result
419 .errors
420 .into_iter()
421 .map(|e| ProtoForgetError {
422 id: e.id.to_string(),
423 error: e.error,
424 })
425 .collect();
426
427 Ok(Response::new(ProtoForgetResponse { forgotten, errors }))
428 }
429
430 async fn health(
433 &self,
434 _request: Request<HealthRequest>,
435 ) -> Result<Response<HealthResponse>, Status> {
436 Ok(Response::new(HealthResponse {
437 status: "ok".to_string(),
438 version: env!("CARGO_PKG_VERSION").to_string(),
439 }))
440 }
441
442 async fn share(
445 &self,
446 request: Request<ProtoShareRequest>,
447 ) -> Result<Response<ProtoShareResponse>, Status> {
448 let req = request.into_inner();
449 let memory_id = Uuid::parse_str(&req.memory_id)
450 .map_err(|e| Status::invalid_argument(format!("invalid UUID: {e}")))?;
451 let permission = match req.permission {
452 Some(ref s) => match s.parse::<Permission>() {
453 Ok(p) => Some(p),
454 Err(_) => {
455 return Err(Status::invalid_argument(format!(
456 "invalid permission '{}': expected one of: read, write, delete, share, delegate, admin",
457 s
458 )));
459 }
460 },
461 None => None,
462 };
463 let target_agent_ids = if req.target_agent_ids.is_empty() {
464 None
465 } else {
466 Some(req.target_agent_ids)
467 };
468
469 let core_req = CoreShareRequest {
470 memory_id,
471 agent_id: req.agent_id,
472 target_agent_id: req.target_agent_id,
473 target_agent_ids,
474 permission,
475 expires_in_hours: req.expires_in_hours,
476 };
477 let result = self
478 .engine
479 .share(core_req)
480 .await
481 .map_err(core_error_to_status)?;
482
483 Ok(Response::new(ProtoShareResponse {
484 acl_id: result.acl_id.to_string(),
485 acl_ids: result.acl_ids.iter().map(|id| id.to_string()).collect(),
486 memory_id: result.memory_id.to_string(),
487 shared_with: result.shared_with,
488 shared_with_all: result.shared_with_all,
489 permission: result.permission.to_string(),
490 }))
491 }
492
493 async fn checkpoint(
496 &self,
497 request: Request<ProtoCheckpointRequest>,
498 ) -> Result<Response<ProtoCheckpointResponse>, Status> {
499 let req = request.into_inner();
500 let state_snapshot: serde_json::Value = serde_json::from_str(&req.state_snapshot)
501 .map_err(|e| Status::invalid_argument(format!("invalid JSON state_snapshot: {e}")))?;
502 let metadata: Option<serde_json::Value> = match req.metadata {
503 Some(ref s) => match serde_json::from_str(s) {
504 Ok(v) => Some(v),
505 Err(e) => {
506 return Err(Status::invalid_argument(format!(
507 "invalid metadata JSON: {}",
508 e
509 )));
510 }
511 },
512 None => None,
513 };
514
515 let core_req = CoreCheckpointRequest {
516 thread_id: req.thread_id,
517 agent_id: req.agent_id,
518 branch_name: req.branch_name,
519 state_snapshot,
520 label: req.label,
521 metadata,
522 };
523 let result = self
524 .engine
525 .checkpoint(core_req)
526 .await
527 .map_err(core_error_to_status)?;
528
529 Ok(Response::new(ProtoCheckpointResponse {
530 checkpoint_id: result.id.to_string(),
531 parent_id: result.parent_id.map(|id| id.to_string()),
532 branch_name: result.branch_name,
533 }))
534 }
535
536 async fn consolidate(
537 &self,
538 request: Request<ProtoConsolidateRequest>,
539 ) -> Result<Response<ProtoConsolidateResponse>, Status> {
540 let req = request.into_inner();
541
542 let mut memory_ids = Vec::with_capacity(req.memory_ids.len());
543 for s in &req.memory_ids {
544 memory_ids.push(
545 Uuid::parse_str(s).map_err(|e| {
546 Status::invalid_argument(format!("invalid memory id '{s}': {e}"))
547 })?,
548 );
549 }
550 let supersede = match req.supersede {
551 Some(ref s) => Some(Uuid::parse_str(s).map_err(|e| {
552 Status::invalid_argument(format!("invalid supersede id '{s}': {e}"))
553 })?),
554 None => None,
555 };
556 let metadata: Option<serde_json::Value> = match req.metadata {
557 Some(ref s) => Some(
558 serde_json::from_str(s)
559 .map_err(|e| Status::invalid_argument(format!("invalid metadata JSON: {e}")))?,
560 ),
561 None => None,
562 };
563
564 let mut core_req = CoreConsolidateRequest::new(memory_ids, req.topic_name);
565 core_req.agent_id = req.agent_id;
566 core_req.summary = req.summary;
567 core_req.supersede = supersede;
568 core_req.thread_id = req.thread_id;
569 core_req.metadata = metadata;
570
571 let result = self
572 .engine
573 .consolidate(core_req)
574 .await
575 .map_err(core_error_to_status)?;
576
577 Ok(Response::new(ProtoConsolidateResponse {
578 topic_document_id: result.topic_document_id.to_string(),
579 topic_name: result.topic_name,
580 source_count: result.source_count as u64,
581 version: result.version,
582 superseded_id: result.superseded_id.map(|id| id.to_string()),
583 member_ids: result.member_ids.iter().map(|id| id.to_string()).collect(),
584 content_hash: result.content_hash,
585 consolidation_event_id: result.consolidation_event_id.to_string(),
586 revision_event_id: result.revision_event_id.map(|id| id.to_string()),
587 }))
588 }
589
590 async fn branch(
593 &self,
594 request: Request<ProtoBranchRequest>,
595 ) -> Result<Response<ProtoBranchResponse>, Status> {
596 let req = request.into_inner();
597 let source_checkpoint_id = match req.source_checkpoint_id {
598 Some(ref s) => match Uuid::parse_str(s) {
599 Ok(id) => Some(id),
600 Err(e) => {
601 return Err(Status::invalid_argument(format!(
602 "invalid source_checkpoint_id '{}': {}",
603 s, e
604 )));
605 }
606 },
607 None => None,
608 };
609
610 let core_req = CoreBranchRequest {
611 thread_id: req.thread_id,
612 agent_id: req.agent_id,
613 new_branch_name: req.new_branch_name,
614 source_checkpoint_id,
615 source_branch: req.source_branch,
616 };
617 let result = self
618 .engine
619 .branch(core_req)
620 .await
621 .map_err(core_error_to_status)?;
622
623 Ok(Response::new(ProtoBranchResponse {
624 checkpoint_id: result.checkpoint_id.to_string(),
625 branch_name: result.branch_name,
626 source_checkpoint_id: result.source_checkpoint_id.to_string(),
627 }))
628 }
629
630 async fn merge(
633 &self,
634 request: Request<ProtoMergeRequest>,
635 ) -> Result<Response<ProtoMergeResponse>, Status> {
636 let req = request.into_inner();
637 let strategy = match req.strategy {
638 Some(ref s) => {
639 let st = match s.as_str() {
640 "full_merge" => MergeStrategy::FullMerge,
641 "cherry_pick" => MergeStrategy::CherryPick,
642 "squash" => MergeStrategy::Squash,
643 _ => {
644 return Err(Status::invalid_argument(format!(
645 "invalid merge strategy '{}': expected one of: full_merge, cherry_pick, squash",
646 s
647 )));
648 }
649 };
650 Some(st)
651 }
652 None => None,
653 };
654 let cherry_pick_ids = if req.cherry_pick_ids.is_empty() {
655 None
656 } else {
657 let ids: Result<Vec<Uuid>, _> = req
658 .cherry_pick_ids
659 .iter()
660 .map(|s| Uuid::parse_str(s))
661 .collect();
662 Some(ids.map_err(|e| Status::invalid_argument(format!("invalid UUID: {e}")))?)
663 };
664
665 let core_req = CoreMergeRequest {
666 thread_id: req.thread_id,
667 agent_id: req.agent_id,
668 source_branch: req.source_branch,
669 target_branch: req.target_branch,
670 strategy,
671 cherry_pick_ids,
672 };
673 let result = self
674 .engine
675 .merge(core_req)
676 .await
677 .map_err(core_error_to_status)?;
678
679 Ok(Response::new(ProtoMergeResponse {
680 checkpoint_id: result.checkpoint_id.to_string(),
681 target_branch: result.target_branch,
682 merged_memory_count: result.merged_memory_count as u32,
683 }))
684 }
685
686 async fn replay(
689 &self,
690 request: Request<ProtoReplayRequest>,
691 ) -> Result<Response<ProtoReplayResponse>, Status> {
692 let req = request.into_inner();
693 let checkpoint_id = match req.checkpoint_id {
694 Some(ref s) => match Uuid::parse_str(s) {
695 Ok(id) => Some(id),
696 Err(e) => {
697 return Err(Status::invalid_argument(format!(
698 "invalid checkpoint_id '{}': {}",
699 s, e
700 )));
701 }
702 },
703 None => None,
704 };
705
706 let core_req = CoreReplayRequest {
707 thread_id: req.thread_id,
708 agent_id: req.agent_id,
709 checkpoint_id,
710 branch_name: req.branch_name,
711 as_of: req.as_of,
712 };
713 let result = self
714 .engine
715 .replay(core_req)
716 .await
717 .map_err(core_error_to_status)?;
718
719 let checkpoint_json =
720 serde_json::to_string(&result.checkpoint).unwrap_or_else(|_| "{}".to_string());
721 let memories: Vec<ProtoReplayMemory> = result
722 .memories
723 .iter()
724 .map(|m| ProtoReplayMemory {
725 id: m.id.to_string(),
726 content: m.content.clone(),
727 memory_type: format!("{:?}", m.memory_type),
728 created_at: m.created_at.clone(),
729 })
730 .collect();
731
732 let (chain_valid, chain_total, chain_verified) =
733 if let Some(ref cv) = result.chain_verification {
734 (
735 Some(cv.valid),
736 Some(cv.total_records as u32),
737 Some(cv.verified_records as u32),
738 )
739 } else {
740 (None, None, None)
741 };
742
743 Ok(Response::new(ProtoReplayResponse {
744 checkpoint_json,
745 memories,
746 event_count: result.events.len() as u32,
747 chain_valid,
748 chain_total,
749 chain_verified,
750 }))
751 }
752
753 async fn delegate(
756 &self,
757 request: Request<ProtoDelegateRequest>,
758 ) -> Result<Response<ProtoDelegateResponse>, Status> {
759 let req = request.into_inner();
760 let permission: Permission = req
761 .permission
762 .parse()
763 .map_err(|e: mnemo_core::error::Error| Status::invalid_argument(e.to_string()))?;
764
765 let scope = if !req.memory_ids.is_empty() {
766 let ids: Vec<Uuid> = req
767 .memory_ids
768 .iter()
769 .map(|s| {
770 Uuid::parse_str(s)
771 .map_err(|e| Status::invalid_argument(format!("invalid UUID: {e}")))
772 })
773 .collect::<Result<Vec<_>, _>>()?;
774 DelegationScope::ByMemoryId(ids)
775 } else if !req.tags.is_empty() {
776 DelegationScope::ByTag(req.tags)
777 } else {
778 DelegationScope::AllMemories
779 };
780
781 let now = chrono::Utc::now();
782 let expires_at = req
783 .expires_in_hours
784 .map(|h| (now + chrono::Duration::seconds((h * 3600.0) as i64)).to_rfc3339());
785
786 let delegation = Delegation {
787 id: Uuid::now_v7(),
788 delegator_id: req.delegator_id,
789 delegate_id: req.delegate_id,
790 permission,
791 scope,
792 max_depth: req.max_depth.unwrap_or(0),
793 current_depth: 0,
794 parent_delegation_id: None,
795 created_at: now.to_rfc3339(),
796 expires_at,
797 revoked_at: None,
798 };
799
800 self.engine
801 .storage
802 .insert_delegation(&delegation)
803 .await
804 .map_err(core_error_to_status)?;
805
806 Ok(Response::new(ProtoDelegateResponse {
807 delegation_id: delegation.id.to_string(),
808 }))
809 }
810
811 async fn verify(
814 &self,
815 request: Request<ProtoVerifyRequest>,
816 ) -> Result<Response<ProtoVerifyResponse>, Status> {
817 let req = request.into_inner();
818 let result = self
819 .engine
820 .verify_integrity(req.agent_id, req.thread_id.as_deref())
821 .await
822 .map_err(core_error_to_status)?;
823
824 Ok(Response::new(ProtoVerifyResponse {
825 valid: result.valid,
826 total_records: result.total_records as u32,
827 verified_records: result.verified_records as u32,
828 first_broken_at: result.first_broken_at.map(|id| id.to_string()),
829 error_message: result.error_message,
830 }))
831 }
832
833 async fn trajectory_audit(
842 &self,
843 request: Request<ProtoTrajectoryAuditRequest>,
844 ) -> Result<Response<ProtoTrajectoryAuditResponse>, Status> {
845 let req = request.into_inner();
846 let agent_id = req
847 .agent_id
848 .clone()
849 .unwrap_or_else(|| self.engine.default_agent_id.clone());
850
851 let mut events = self
855 .engine
856 .storage
857 .list_events(&agent_id, mnemo_core::query::MAX_BATCH_QUERY_LIMIT, 0)
858 .await
859 .map_err(core_error_to_status)?;
860 events.reverse();
861
862 let mut audit_req = mnemo_compliance::trajectory::TrajectoryAuditRequest {
863 agent_id: Some(agent_id),
864 thread_id: req.thread_id.clone(),
865 ..Default::default()
866 };
867 if let Some(c) = req.active_bank_ceiling {
868 audit_req.active_bank_ceiling = c as usize;
869 }
870 if let Some(k) = req.fact_key {
871 audit_req.fact_key = k;
872 }
873 if !req.named_forget_strategies.is_empty() {
874 audit_req.named_forget_strategies = req.named_forget_strategies;
875 }
876
877 let report = mnemo_compliance::trajectory::trajectory_audit(&events, &audit_req)
878 .map_err(|e| Status::invalid_argument(e.to_string()))?;
879 let report_json = serde_json::to_string(&report)
880 .map_err(|e| Status::internal(format!("report serialisation: {e}")))?;
881
882 Ok(Response::new(ProtoTrajectoryAuditResponse {
883 scope_label: report.scope_label.clone(),
884 event_count: report.event_count as u32,
885 all_ok: report.all_ok(),
886 unregulated_growth: Some(ProtoTrajectoryFinding {
887 severity: severity_to_str(report.unregulated_growth.severity).to_string(),
888 count: report.unregulated_growth.breach_count as u32,
889 }),
890 missing_semantic_revision: Some(ProtoTrajectoryFinding {
891 severity: severity_to_str(report.missing_semantic_revision.severity).to_string(),
892 count: report.missing_semantic_revision.stale_facts.len() as u32,
893 }),
894 capacity_driven_forgetting: Some(ProtoTrajectoryFinding {
895 severity: severity_to_str(report.capacity_driven_forgetting.severity).to_string(),
896 count: report
897 .capacity_driven_forgetting
898 .unlabelled_forget_event_ids
899 .len() as u32,
900 }),
901 read_only_retrieval: Some(ProtoTrajectoryFinding {
902 severity: severity_to_str(report.read_only_retrieval.severity).to_string(),
903 count: report.read_only_retrieval.read_only_scopes.len() as u32,
904 }),
905 report_json,
906 }))
907 }
908
909 async fn forget_subject(
912 &self,
913 request: Request<ProtoForgetSubjectRequest>,
914 ) -> Result<Response<ProtoForgetSubjectResponse>, Status> {
915 let req = request.into_inner();
916
917 let strategy = match req.strategy.as_deref().unwrap_or("redact") {
918 "redact" => ForgetStrategy::Redact,
919 "hard_delete" => ForgetStrategy::HardDelete,
920 "soft_delete" => ForgetStrategy::SoftDelete,
921 other => {
922 return Err(Status::invalid_argument(format!(
923 "invalid forget_subject strategy '{}': expected one of: redact, hard_delete, soft_delete",
924 other
925 )));
926 }
927 };
928
929 let core_req = CoreForgetSubjectRequest {
930 subject_id: req.subject_id,
931 agent_id: req.agent_id,
932 strategy,
933 };
934
935 let result = self
936 .engine
937 .forget_subject(core_req)
938 .await
939 .map_err(core_error_to_status)?;
940
941 let errors: Vec<ProtoForgetError> = result
942 .errors
943 .into_iter()
944 .map(|e| ProtoForgetError {
945 id: e.id.to_string(),
946 error: e.error,
947 })
948 .collect();
949
950 let strategy_str = match result.strategy {
951 ForgetStrategy::SoftDelete => "soft_delete",
952 ForgetStrategy::HardDelete => "hard_delete",
953 ForgetStrategy::Decay => "decay",
954 ForgetStrategy::Consolidate => "consolidate",
955 ForgetStrategy::Archive => "archive",
956 ForgetStrategy::Redact => "redact",
957 }
958 .to_string();
959
960 Ok(Response::new(ProtoForgetSubjectResponse {
961 subject_id: result.subject_id,
962 strategy: strategy_str,
963 matched: result.matched as u32,
964 forgotten: result.forgotten.iter().map(|id| id.to_string()).collect(),
965 cascaded_events: result.cascaded_events as u32,
966 errors,
967 }))
968 }
969}
970
971pub fn router(engine: Arc<MnemoEngine>) -> tonic::transport::server::Router {
999 let token = std::env::var("MNEMO_AUTH_TOKEN")
1000 .ok()
1001 .filter(|s| !s.is_empty());
1002 router_with_auth(engine, token)
1003}
1004
1005pub fn router_with_auth(
1009 engine: Arc<MnemoEngine>,
1010 auth_token: Option<String>,
1011) -> tonic::transport::server::Router {
1012 let svc = MnemoGrpcServer::new(engine);
1013 match auth_token {
1014 Some(token) if !token.is_empty() => {
1015 tracing::info!(
1016 "gRPC bearer-token auth ENABLED (authorization metadata = MNEMO_AUTH_TOKEN)"
1017 );
1018 let expected = Arc::new(token);
1019 let interceptor = move |req: Request<()>| -> Result<Request<()>, Status> {
1020 let provided = req
1021 .metadata()
1022 .get("authorization")
1023 .and_then(|v| v.to_str().ok());
1024 if mnemo_core::auth::bearer_token_matches(provided, &expected) {
1025 Ok(req)
1026 } else {
1027 Err(Status::unauthenticated(
1028 "missing or invalid bearer token (set `authorization` metadata)",
1029 ))
1030 }
1031 };
1032 tonic::transport::Server::builder()
1033 .add_service(MnemoServiceServer::with_interceptor(svc, interceptor))
1034 }
1035 _ => {
1036 tracing::warn!(
1037 "gRPC API running WITHOUT authentication — set MNEMO_AUTH_TOKEN to require a \
1038 bearer token. Do not expose an unauthenticated memory server."
1039 );
1040 tonic::transport::Server::builder().add_service(MnemoServiceServer::new(svc))
1041 }
1042 }
1043}
1044
1045fn core_error_to_status(err: mnemo_core::error::Error) -> Status {
1051 use mnemo_core::error::Error;
1052
1053 match err {
1054 Error::Validation(msg) => Status::invalid_argument(msg),
1055 Error::PermissionDenied(msg) => Status::permission_denied(msg),
1056 Error::NotFound(msg) => Status::not_found(msg),
1057 other => Status::internal(other.to_string()),
1058 }
1059}
1060
1061fn severity_to_str(s: mnemo_compliance::Severity) -> &'static str {
1064 match s {
1065 mnemo_compliance::Severity::Ok => "ok",
1066 mnemo_compliance::Severity::Warn => "warn",
1067 mnemo_compliance::Severity::Fail => "fail",
1068 }
1069}
1070
1071#[cfg(test)]
1076mod tests {
1077 use super::*;
1078
1079 #[test]
1080 fn core_error_maps_correctly() {
1081 let validation =
1082 core_error_to_status(mnemo_core::error::Error::Validation("bad input".into()));
1083 assert_eq!(validation.code(), tonic::Code::InvalidArgument);
1084
1085 let perm = core_error_to_status(mnemo_core::error::Error::PermissionDenied(
1086 "forbidden".into(),
1087 ));
1088 assert_eq!(perm.code(), tonic::Code::PermissionDenied);
1089
1090 let not_found = core_error_to_status(mnemo_core::error::Error::NotFound("missing".into()));
1091 assert_eq!(not_found.code(), tonic::Code::NotFound);
1092 }
1093}