1#![forbid(unsafe_code)]
2
3mod admission;
4mod observability;
5
6use std::net::SocketAddr;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use admission::{
11 AdmissionClass, AdmissionConfig, AdmissionController, AdmissionError, RuntimeAdmissionStatus,
12};
13use axum::extract::{Path, Query, Request as AxumRequest};
14use axum::http::HeaderMap;
15use axum::http::StatusCode;
16use axum::middleware::{self, Next};
17use axum::response::IntoResponse;
18use axum::routing::{get, post};
19use axum::{Json, Router};
20use mnemara_core::{
21 AffectiveAnnotation, AffectiveAnnotationProvenance, ArchiveReceipt, ArchiveRequest,
22 ArtifactPointer, BatchUpsertRequest, CompactionReport, CompactionRequest, ConflictAnnotation,
23 ConflictResolutionKind, ConflictReviewState, DeleteReceipt, DeleteRequest,
24 EPISODE_SCHEMA_VERSION, EmbeddingProviderKind, EngineTuningInfo, EpisodeContext,
25 EpisodeContinuityState, EpisodeSalience, ExportRequest, GraphInspectionEdge,
26 GraphInspectionEdgeKind, GraphInspectionNode, GraphInspectionReport, GraphInspectionRequest,
27 ImportMode, ImportReport, ImportRequest, IntegrityCheckReport, IntegrityCheckRequest,
28 LineageLink, LineageRelationKind, MaintenanceRunReport, MaintenanceRunRequest,
29 MaintenanceStats, MemoryHistoricalState, MemoryQualityState, MemoryRecord, MemoryRecordKind,
30 MemoryScope, MemoryStore, MemoryTrustLevel, NamespaceStats, OperationTrace,
31 OperationTraceSummary, PortableRecord, PortableStorePackage, RecallCandidateSource,
32 RecallExplanation, RecallFilters, RecallHistoricalMode, RecallPlannerStage,
33 RecallPlanningProfile, RecallPlanningTrace, RecallPolicyProfile, RecallQuery, RecallResult,
34 RecallScorerKind, RecallScoringProfile, RecallTemporalOrder, RecallTraceCandidate,
35 RecoverReceipt, RecoverRequest, RepairReport, RepairRequest, SnapshotManifest,
36 SnapshotShipReport, SnapshotShipRequest, StoreStatsReport, StoreStatsRequest, SuppressReceipt,
37 SuppressRequest, TraceListRequest, TraceOperationKind, TraceStatus, UpsertReceipt,
38 UpsertRequest,
39};
40use mnemara_protocol::v1::memory_service_server::{MemoryService, MemoryServiceServer};
41use mnemara_protocol::v1::{
42 AffectiveAnnotation as ProtoAffectiveAnnotation, ArchiveReply,
43 ArchiveRequest as ProtoArchiveRequest, ArtifactPointer as ProtoArtifactPointer,
44 BatchUpsertMemoryRecordsReply, BatchUpsertMemoryRecordsRequest, CompactReply,
45 CompactRequest as ProtoCompactRequest, ConflictAnnotation as ProtoConflictAnnotation,
46 DeleteReply, DeleteRequest as ProtoDeleteRequest,
47 EmbeddingProviderKind as ProtoEmbeddingProviderKind, EngineTuningInfo as ProtoEngineTuningInfo,
48 EpisodeContext as ProtoEpisodeContext, EpisodeSalience as ProtoEpisodeSalience,
49 ExportReply as ProtoExportReply, ExportRequest as ProtoExportRequest,
50 GetTraceRequest as ProtoGetTraceRequest, GraphInspectionEdge as ProtoGraphInspectionEdge,
51 GraphInspectionNode as ProtoGraphInspectionNode, GraphInspectionReply,
52 GraphInspectionRequest as ProtoGraphInspectionRequest, ImportMode as ProtoImportMode,
53 ImportReply as ProtoImportReply, ImportRequest as ProtoImportRequest,
54 IntegrityCheckReply as ProtoIntegrityCheckReply,
55 IntegrityCheckRequest as ProtoIntegrityCheckRequest, LineageLink as ProtoLineageLink,
56 ListTracesReply as ProtoListTracesReply, ListTracesRequest as ProtoListTracesRequest,
57 MaintenanceRunReply, MaintenanceRunRequest as ProtoMaintenanceRunRequest,
58 MaintenanceStats as ProtoMaintenanceStats, MemoryRecord as ProtoMemoryRecord,
59 MemoryScope as ProtoMemoryScope, NamespaceStats as ProtoNamespaceStats,
60 OperationTrace as ProtoOperationTrace, OperationTraceSummary as ProtoOperationTraceSummary,
61 PortableRecord as ProtoPortableRecord, RecallExplanation as ProtoRecallExplanation,
62 RecallFilters as ProtoRecallFilters, RecallHit as ProtoRecallHit,
63 RecallPlanningTrace as ProtoRecallPlanningTrace,
64 RecallPolicyProfile as ProtoRecallPolicyProfile, RecallReply,
65 RecallRequest as ProtoRecallRequest, RecallScoreBreakdown as ProtoRecallScoreBreakdown,
66 RecallScorerKind as ProtoRecallScorerKind, RecallScoringProfile as ProtoRecallScoringProfile,
67 RecallTraceCandidate as ProtoRecallTraceCandidate, RecoverReply,
68 RecoverRequest as ProtoRecoverRequest, RepairReply as ProtoRepairReply,
69 RepairRequest as ProtoRepairRequest, SnapshotReply, SnapshotRequest, SnapshotShipReply,
70 SnapshotShipRequest as ProtoSnapshotShipRequest, StoreStatsReply as ProtoStoreStatsReply,
71 StoreStatsRequest as ProtoStoreStatsRequest, SuppressReply,
72 SuppressRequest as ProtoSuppressRequest, TraceOperationKind as ProtoTraceOperationKind,
73 TraceStatus as ProtoTraceStatus, UpsertMemoryRecordReply,
74 UpsertMemoryRecordRequest as ProtoUpsertMemoryRecordRequest,
75};
76use observability::{TraceRegistry, TraceRegistrySnapshot, now_unix_ms};
77use tonic::{Request, Response, Status};
78
79#[derive(Debug, Clone, serde::Serialize)]
80struct HealthStatus {
81 status: &'static str,
82}
83
84#[derive(Debug, Clone, serde::Serialize)]
85struct ReadyStatus {
86 status: &'static str,
87 record_count: u64,
88 namespaces: Vec<String>,
89}
90
91#[derive(Debug, Clone, serde::Serialize)]
92struct ServerRuntimeStatus {
93 backend: String,
94 admission: RuntimeAdmissionStatus,
95 traces: TraceRegistrySnapshot,
96}
97
98#[derive(Debug, Clone, serde::Serialize)]
99struct HttpErrorBody {
100 error: String,
101}
102
103#[derive(Debug, Default)]
104struct MethodMetrics {
105 started: AtomicU64,
106 ok: AtomicU64,
107 invalid_argument: AtomicU64,
108 conflict: AtomicU64,
109 unimplemented: AtomicU64,
110 internal: AtomicU64,
111}
112
113impl MethodMetrics {
114 fn record_started(&self) {
115 self.started.fetch_add(1, Ordering::Relaxed);
116 }
117
118 fn record_status(&self, status: &Status) {
119 match status.code() {
120 tonic::Code::Ok => {
121 self.ok.fetch_add(1, Ordering::Relaxed);
122 }
123 tonic::Code::InvalidArgument => {
124 self.invalid_argument.fetch_add(1, Ordering::Relaxed);
125 }
126 tonic::Code::AlreadyExists => {
127 self.conflict.fetch_add(1, Ordering::Relaxed);
128 }
129 tonic::Code::Unimplemented => {
130 self.unimplemented.fetch_add(1, Ordering::Relaxed);
131 }
132 _ => {
133 self.internal.fetch_add(1, Ordering::Relaxed);
134 }
135 }
136 }
137}
138
139#[derive(Debug, Default)]
140pub struct ServerMetrics {
141 grpc_upsert: MethodMetrics,
142 grpc_batch_upsert: MethodMetrics,
143 grpc_recall: MethodMetrics,
144 grpc_compact: MethodMetrics,
145 grpc_snapshot: MethodMetrics,
146 grpc_delete: MethodMetrics,
147 grpc_archive: MethodMetrics,
148 grpc_suppress: MethodMetrics,
149 grpc_recover: MethodMetrics,
150 grpc_stats: MethodMetrics,
151 grpc_integrity: MethodMetrics,
152 grpc_repair: MethodMetrics,
153 grpc_maintenance: MethodMetrics,
154 grpc_snapshot_ship: MethodMetrics,
155 http_healthz: AtomicU64,
156 http_readyz: AtomicU64,
157 http_snapshot: AtomicU64,
158 http_compact: AtomicU64,
159 http_delete: AtomicU64,
160 http_archive: AtomicU64,
161 http_suppress: AtomicU64,
162 http_recover: AtomicU64,
163 http_metrics: AtomicU64,
164 http_traces: AtomicU64,
165 http_runtime: AtomicU64,
166 http_export: AtomicU64,
167 http_import: AtomicU64,
168 http_maintenance: AtomicU64,
169 http_snapshot_ship: AtomicU64,
170 http_rejected_body_too_large: AtomicU64,
171 admission_rejected: AtomicU64,
172 admission_timed_out: AtomicU64,
173 trace_records: AtomicU64,
174 trace_evictions: AtomicU64,
175 trace_reads: AtomicU64,
176}
177
178impl ServerMetrics {
179 pub fn render(&self) -> String {
180 let mut output = String::new();
181 append_counter(
182 &mut output,
183 "mnemara_grpc_upsert_requests_started_total",
184 self.grpc_upsert.started.load(Ordering::Relaxed),
185 );
186 append_counter(
187 &mut output,
188 "mnemara_grpc_upsert_requests_ok_total",
189 self.grpc_upsert.ok.load(Ordering::Relaxed),
190 );
191 append_counter(
192 &mut output,
193 "mnemara_grpc_upsert_requests_invalid_argument_total",
194 self.grpc_upsert.invalid_argument.load(Ordering::Relaxed),
195 );
196 append_counter(
197 &mut output,
198 "mnemara_grpc_upsert_requests_conflict_total",
199 self.grpc_upsert.conflict.load(Ordering::Relaxed),
200 );
201 append_counter(
202 &mut output,
203 "mnemara_grpc_upsert_requests_unimplemented_total",
204 self.grpc_upsert.unimplemented.load(Ordering::Relaxed),
205 );
206 append_counter(
207 &mut output,
208 "mnemara_grpc_upsert_requests_internal_total",
209 self.grpc_upsert.internal.load(Ordering::Relaxed),
210 );
211 append_counter(
212 &mut output,
213 "mnemara_grpc_batch_upsert_requests_started_total",
214 self.grpc_batch_upsert.started.load(Ordering::Relaxed),
215 );
216 append_counter(
217 &mut output,
218 "mnemara_grpc_batch_upsert_requests_ok_total",
219 self.grpc_batch_upsert.ok.load(Ordering::Relaxed),
220 );
221 append_counter(
222 &mut output,
223 "mnemara_grpc_batch_upsert_requests_invalid_argument_total",
224 self.grpc_batch_upsert
225 .invalid_argument
226 .load(Ordering::Relaxed),
227 );
228 append_counter(
229 &mut output,
230 "mnemara_grpc_recall_requests_started_total",
231 self.grpc_recall.started.load(Ordering::Relaxed),
232 );
233 append_counter(
234 &mut output,
235 "mnemara_grpc_recall_requests_ok_total",
236 self.grpc_recall.ok.load(Ordering::Relaxed),
237 );
238 append_counter(
239 &mut output,
240 "mnemara_grpc_recall_requests_invalid_argument_total",
241 self.grpc_recall.invalid_argument.load(Ordering::Relaxed),
242 );
243 append_counter(
244 &mut output,
245 "mnemara_grpc_compact_requests_started_total",
246 self.grpc_compact.started.load(Ordering::Relaxed),
247 );
248 append_counter(
249 &mut output,
250 "mnemara_grpc_compact_requests_ok_total",
251 self.grpc_compact.ok.load(Ordering::Relaxed),
252 );
253 append_counter(
254 &mut output,
255 "mnemara_grpc_snapshot_requests_started_total",
256 self.grpc_snapshot.started.load(Ordering::Relaxed),
257 );
258 append_counter(
259 &mut output,
260 "mnemara_grpc_snapshot_requests_ok_total",
261 self.grpc_snapshot.ok.load(Ordering::Relaxed),
262 );
263 append_counter(
264 &mut output,
265 "mnemara_grpc_delete_requests_started_total",
266 self.grpc_delete.started.load(Ordering::Relaxed),
267 );
268 append_counter(
269 &mut output,
270 "mnemara_grpc_delete_requests_ok_total",
271 self.grpc_delete.ok.load(Ordering::Relaxed),
272 );
273 append_counter(
274 &mut output,
275 "mnemara_grpc_archive_requests_started_total",
276 self.grpc_archive.started.load(Ordering::Relaxed),
277 );
278 append_counter(
279 &mut output,
280 "mnemara_grpc_archive_requests_ok_total",
281 self.grpc_archive.ok.load(Ordering::Relaxed),
282 );
283 append_counter(
284 &mut output,
285 "mnemara_grpc_suppress_requests_started_total",
286 self.grpc_suppress.started.load(Ordering::Relaxed),
287 );
288 append_counter(
289 &mut output,
290 "mnemara_grpc_suppress_requests_ok_total",
291 self.grpc_suppress.ok.load(Ordering::Relaxed),
292 );
293 append_counter(
294 &mut output,
295 "mnemara_grpc_recover_requests_started_total",
296 self.grpc_recover.started.load(Ordering::Relaxed),
297 );
298 append_counter(
299 &mut output,
300 "mnemara_grpc_recover_requests_ok_total",
301 self.grpc_recover.ok.load(Ordering::Relaxed),
302 );
303 append_counter(
304 &mut output,
305 "mnemara_grpc_maintenance_requests_started_total",
306 self.grpc_maintenance.started.load(Ordering::Relaxed),
307 );
308 append_counter(
309 &mut output,
310 "mnemara_grpc_maintenance_requests_ok_total",
311 self.grpc_maintenance.ok.load(Ordering::Relaxed),
312 );
313 append_counter(
314 &mut output,
315 "mnemara_grpc_snapshot_ship_requests_started_total",
316 self.grpc_snapshot_ship.started.load(Ordering::Relaxed),
317 );
318 append_counter(
319 &mut output,
320 "mnemara_grpc_snapshot_ship_requests_ok_total",
321 self.grpc_snapshot_ship.ok.load(Ordering::Relaxed),
322 );
323 append_counter(
324 &mut output,
325 "mnemara_http_healthz_requests_total",
326 self.http_healthz.load(Ordering::Relaxed),
327 );
328 append_counter(
329 &mut output,
330 "mnemara_http_readyz_requests_total",
331 self.http_readyz.load(Ordering::Relaxed),
332 );
333 append_counter(
334 &mut output,
335 "mnemara_http_snapshot_requests_total",
336 self.http_snapshot.load(Ordering::Relaxed),
337 );
338 append_counter(
339 &mut output,
340 "mnemara_http_compact_requests_total",
341 self.http_compact.load(Ordering::Relaxed),
342 );
343 append_counter(
344 &mut output,
345 "mnemara_http_delete_requests_total",
346 self.http_delete.load(Ordering::Relaxed),
347 );
348 append_counter(
349 &mut output,
350 "mnemara_http_archive_requests_total",
351 self.http_archive.load(Ordering::Relaxed),
352 );
353 append_counter(
354 &mut output,
355 "mnemara_http_suppress_requests_total",
356 self.http_suppress.load(Ordering::Relaxed),
357 );
358 append_counter(
359 &mut output,
360 "mnemara_http_recover_requests_total",
361 self.http_recover.load(Ordering::Relaxed),
362 );
363 append_counter(
364 &mut output,
365 "mnemara_http_metrics_requests_total",
366 self.http_metrics.load(Ordering::Relaxed),
367 );
368 append_counter(
369 &mut output,
370 "mnemara_http_traces_requests_total",
371 self.http_traces.load(Ordering::Relaxed),
372 );
373 append_counter(
374 &mut output,
375 "mnemara_http_runtime_requests_total",
376 self.http_runtime.load(Ordering::Relaxed),
377 );
378 append_counter(
379 &mut output,
380 "mnemara_http_export_requests_total",
381 self.http_export.load(Ordering::Relaxed),
382 );
383 append_counter(
384 &mut output,
385 "mnemara_http_import_requests_total",
386 self.http_import.load(Ordering::Relaxed),
387 );
388 append_counter(
389 &mut output,
390 "mnemara_http_maintenance_requests_total",
391 self.http_maintenance.load(Ordering::Relaxed),
392 );
393 append_counter(
394 &mut output,
395 "mnemara_http_snapshot_ship_requests_total",
396 self.http_snapshot_ship.load(Ordering::Relaxed),
397 );
398 append_counter(
399 &mut output,
400 "mnemara_http_rejected_body_too_large_total",
401 self.http_rejected_body_too_large.load(Ordering::Relaxed),
402 );
403 append_counter(
404 &mut output,
405 "mnemara_admission_rejected_total",
406 self.admission_rejected.load(Ordering::Relaxed),
407 );
408 append_counter(
409 &mut output,
410 "mnemara_admission_timed_out_total",
411 self.admission_timed_out.load(Ordering::Relaxed),
412 );
413 append_counter(
414 &mut output,
415 "mnemara_trace_records_total",
416 self.trace_records.load(Ordering::Relaxed),
417 );
418 append_counter(
419 &mut output,
420 "mnemara_trace_evictions_total",
421 self.trace_evictions.load(Ordering::Relaxed),
422 );
423 append_counter(
424 &mut output,
425 "mnemara_trace_reads_total",
426 self.trace_reads.load(Ordering::Relaxed),
427 );
428 output
429 }
430}
431
432fn append_counter(output: &mut String, name: &str, value: u64) {
433 output.push_str("# TYPE ");
434 output.push_str(name);
435 output.push_str(" counter\n");
436 output.push_str(name);
437 output.push(' ');
438 output.push_str(&value.to_string());
439 output.push('\n');
440}
441
442#[derive(Debug, Clone)]
443pub struct ServerLimits {
444 pub max_http_body_bytes: usize,
445 pub max_batch_upsert_requests: usize,
446 pub max_recall_items: usize,
447 pub max_query_text_bytes: usize,
448 pub max_record_content_bytes: usize,
449 pub max_labels_per_scope: usize,
450 pub max_inflight_reads: usize,
451 pub max_inflight_writes: usize,
452 pub max_inflight_admin: usize,
453 pub max_queued_requests: usize,
454 pub max_tenant_inflight: usize,
455 pub queue_wait_timeout_ms: u64,
456 pub trace_retention: usize,
457}
458
459impl Default for ServerLimits {
460 fn default() -> Self {
461 Self {
462 max_http_body_bytes: 64 * 1024,
463 max_batch_upsert_requests: 128,
464 max_recall_items: 64,
465 max_query_text_bytes: 4 * 1024,
466 max_record_content_bytes: 32 * 1024,
467 max_labels_per_scope: 32,
468 max_inflight_reads: 64,
469 max_inflight_writes: 32,
470 max_inflight_admin: 8,
471 max_queued_requests: 256,
472 max_tenant_inflight: 16,
473 queue_wait_timeout_ms: 2_000,
474 trace_retention: 256,
475 }
476 }
477}
478
479#[derive(Debug, Clone, Default)]
480pub struct AuthConfig {
481 pub bearer_token: Option<String>,
482 pub protect_metrics: bool,
483 pub token_policies: Vec<TokenPolicy>,
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq)]
487pub enum AuthPermission {
488 Read,
489 Write,
490 Admin,
491 Metrics,
492}
493
494#[derive(Debug, Clone, PartialEq, Eq)]
495pub struct TokenPolicy {
496 pub token: String,
497 pub permissions: Vec<AuthPermission>,
498}
499
500impl AuthConfig {
501 fn has_authentication(&self) -> bool {
502 self.bearer_token.is_some() || !self.token_policies.is_empty()
503 }
504
505 fn authorize(&self, authorization_header: Option<&str>, permission: AuthPermission) -> bool {
506 if !self.has_authentication() {
507 return true;
508 }
509
510 let Some(token) = bearer_token_from_header(authorization_header) else {
511 return false;
512 };
513
514 if self.bearer_token.as_deref() == Some(token) {
515 return true;
516 }
517
518 self.token_policies
519 .iter()
520 .any(|policy| policy.token == token && policy.permissions.contains(&permission))
521 }
522}
523
524#[derive(Debug, Clone, serde::Deserialize)]
525pub struct DeleteHttpRequest {
526 pub tenant_id: String,
527 pub namespace: String,
528 pub record_id: String,
529 pub hard_delete: bool,
530 pub audit_reason: String,
531}
532
533#[derive(Debug, Clone, serde::Deserialize)]
534pub struct ArchiveHttpRequest {
535 pub tenant_id: String,
536 pub namespace: String,
537 pub record_id: String,
538 pub dry_run: bool,
539 pub audit_reason: String,
540}
541
542#[derive(Debug, Clone, serde::Deserialize)]
543pub struct SuppressHttpRequest {
544 pub tenant_id: String,
545 pub namespace: String,
546 pub record_id: String,
547 pub dry_run: bool,
548 pub audit_reason: String,
549}
550
551#[derive(Debug, Clone, serde::Deserialize)]
552pub struct RecoverHttpRequest {
553 pub tenant_id: String,
554 pub namespace: String,
555 pub record_id: String,
556 pub dry_run: bool,
557 pub audit_reason: String,
558 pub quality_state: MemoryQualityState,
559 pub historical_state: Option<MemoryHistoricalState>,
560}
561
562pub struct GrpcMemoryService<S> {
563 store: Arc<S>,
564 runtime: ServerRuntime,
565}
566
567impl<S> Clone for GrpcMemoryService<S> {
568 fn clone(&self) -> Self {
569 Self {
570 store: Arc::clone(&self.store),
571 runtime: self.runtime.clone(),
572 }
573 }
574}
575
576#[derive(Debug)]
577struct ServerRuntimeInner {
578 backend: Arc<String>,
579 limits: Arc<ServerLimits>,
580 metrics: Arc<ServerMetrics>,
581 auth: Arc<AuthConfig>,
582 traces: Arc<TraceRegistry>,
583 admission: Arc<AdmissionController>,
584}
585
586#[derive(Debug, Clone)]
587pub struct ServerRuntime {
588 inner: Arc<ServerRuntimeInner>,
589}
590
591impl ServerRuntime {
592 pub fn new(
593 backend: impl Into<String>,
594 limits: ServerLimits,
595 metrics: Arc<ServerMetrics>,
596 auth: AuthConfig,
597 ) -> Self {
598 let admission = AdmissionController::new(AdmissionConfig {
599 max_inflight_reads: limits.max_inflight_reads,
600 max_inflight_writes: limits.max_inflight_writes,
601 max_inflight_admin: limits.max_inflight_admin,
602 max_queued_requests: limits.max_queued_requests,
603 max_tenant_inflight: limits.max_tenant_inflight,
604 queue_wait_timeout_ms: limits.queue_wait_timeout_ms,
605 });
606 Self {
607 inner: Arc::new(ServerRuntimeInner {
608 backend: Arc::new(backend.into()),
609 traces: Arc::new(TraceRegistry::new(limits.trace_retention)),
610 admission: Arc::new(admission),
611 auth: Arc::new(auth),
612 metrics,
613 limits: Arc::new(limits),
614 }),
615 }
616 }
617
618 fn limits(&self) -> &Arc<ServerLimits> {
619 &self.inner.limits
620 }
621
622 fn metrics(&self) -> &Arc<ServerMetrics> {
623 &self.inner.metrics
624 }
625
626 fn backend(&self) -> &Arc<String> {
627 &self.inner.backend
628 }
629
630 fn auth(&self) -> &Arc<AuthConfig> {
631 &self.inner.auth
632 }
633
634 fn traces(&self) -> &Arc<TraceRegistry> {
635 &self.inner.traces
636 }
637
638 fn admission(&self) -> &Arc<AdmissionController> {
639 &self.inner.admission
640 }
641}
642
643impl<S: MemoryStore> GrpcMemoryService<S> {
644 pub fn new(store: Arc<S>) -> Self {
645 let backend = store.as_ref().backend_kind().to_string();
646 Self::with_runtime(
647 store,
648 ServerRuntime::new(
649 backend,
650 ServerLimits::default(),
651 Arc::new(ServerMetrics::default()),
652 AuthConfig::default(),
653 ),
654 )
655 }
656
657 pub fn with_limits(store: Arc<S>, limits: ServerLimits) -> Self {
658 let backend = store.as_ref().backend_kind().to_string();
659 Self::with_runtime(
660 store,
661 ServerRuntime::new(
662 backend,
663 limits,
664 Arc::new(ServerMetrics::default()),
665 AuthConfig::default(),
666 ),
667 )
668 }
669
670 pub fn with_observability(
671 store: Arc<S>,
672 limits: ServerLimits,
673 metrics: Arc<ServerMetrics>,
674 ) -> Self {
675 let backend = store.as_ref().backend_kind().to_string();
676 Self::with_runtime(
677 store,
678 ServerRuntime::new(backend, limits, metrics, AuthConfig::default()),
679 )
680 }
681
682 pub fn with_runtime_config(
683 store: Arc<S>,
684 limits: ServerLimits,
685 metrics: Arc<ServerMetrics>,
686 auth: AuthConfig,
687 ) -> Self {
688 let backend = store.as_ref().backend_kind().to_string();
689 Self::with_runtime(store, ServerRuntime::new(backend, limits, metrics, auth))
690 }
691
692 pub fn with_runtime(store: Arc<S>, runtime: ServerRuntime) -> Self {
693 Self { store, runtime }
694 }
695
696 pub fn into_service(self) -> MemoryServiceServer<Self>
697 where
698 Self: MemoryService,
699 {
700 MemoryServiceServer::new(self)
701 }
702}
703
704pub async fn serve<S>(
705 addr: SocketAddr,
706 store: Arc<S>,
707 limits: ServerLimits,
708 auth: AuthConfig,
709) -> Result<(), tonic::transport::Error>
710where
711 S: MemoryStore + 'static,
712{
713 let backend = store.as_ref().backend_kind().to_string();
714 serve_with_runtime(
715 addr,
716 store,
717 ServerRuntime::new(backend, limits, Arc::new(ServerMetrics::default()), auth),
718 )
719 .await
720}
721
722pub async fn serve_with_runtime<S>(
723 addr: SocketAddr,
724 store: Arc<S>,
725 runtime: ServerRuntime,
726) -> Result<(), tonic::transport::Error>
727where
728 S: MemoryStore + 'static,
729{
730 tonic::transport::Server::builder()
731 .add_service(GrpcMemoryService::with_runtime(store, runtime).into_service())
732 .serve(addr)
733 .await
734}
735
736pub fn http_app<S>(store: Arc<S>, limits: ServerLimits, auth: AuthConfig) -> Router
737where
738 S: MemoryStore + 'static,
739{
740 let backend = store.as_ref().backend_kind().to_string();
741 http_app_with_runtime(
742 store,
743 ServerRuntime::new(backend, limits, Arc::new(ServerMetrics::default()), auth),
744 )
745}
746
747pub fn http_app_with_metrics<S>(
748 store: Arc<S>,
749 limits: ServerLimits,
750 metrics: Arc<ServerMetrics>,
751 auth: AuthConfig,
752) -> Router
753where
754 S: MemoryStore + 'static,
755{
756 let backend = store.as_ref().backend_kind().to_string();
757 http_app_with_runtime(store, ServerRuntime::new(backend, limits, metrics, auth))
758}
759
760pub fn http_app_with_runtime<S>(store: Arc<S>, runtime: ServerRuntime) -> Router
761where
762 S: MemoryStore + 'static,
763{
764 let limits = Arc::clone(runtime.limits());
765 let ready_store = Arc::clone(&store);
766 let upsert_store = Arc::clone(&store);
767 let batch_upsert_store = Arc::clone(&store);
768 let recall_store = Arc::clone(&store);
769 let snapshot_store = Arc::clone(&store);
770 let stats_store = Arc::clone(&store);
771 let graph_store = Arc::clone(&store);
772 let integrity_store = Arc::clone(&store);
773 let repair_store = Arc::clone(&store);
774 let compact_store = Arc::clone(&store);
775 let delete_store = Arc::clone(&store);
776 let archive_store = Arc::clone(&store);
777 let suppress_store = Arc::clone(&store);
778 let recover_store = Arc::clone(&store);
779 let maintenance_store = Arc::clone(&store);
780 let export_store = Arc::clone(&store);
781 let ship_store = Arc::clone(&store);
782 let import_store = Arc::clone(&store);
783 let ready_runtime = runtime.clone();
784 let upsert_runtime = runtime.clone();
785 let batch_upsert_runtime = runtime.clone();
786 let recall_runtime = runtime.clone();
787 let snapshot_runtime = runtime.clone();
788 let stats_runtime = runtime.clone();
789 let graph_runtime = runtime.clone();
790 let integrity_runtime = runtime.clone();
791 let repair_runtime = runtime.clone();
792 let compact_runtime = runtime.clone();
793 let delete_runtime = runtime.clone();
794 let archive_runtime = runtime.clone();
795 let suppress_runtime = runtime.clone();
796 let recover_runtime = runtime.clone();
797 let maintenance_runtime = runtime.clone();
798 let traces_runtime = runtime.clone();
799 let trace_runtime = runtime.clone();
800 let export_runtime = runtime.clone();
801 let ship_runtime = runtime.clone();
802 let import_runtime = runtime.clone();
803 let runtime_status_runtime = runtime.clone();
804 let metrics_metrics = Arc::clone(runtime.metrics());
805 let middleware_limits = Arc::clone(&limits);
806 let middleware_metrics = Arc::clone(runtime.metrics());
807 let middleware_auth = Arc::clone(runtime.auth());
808
809 let health_metrics = Arc::clone(runtime.metrics());
810
811 Router::new()
812 .route(
813 "/healthz",
814 get(move || {
815 let metrics = Arc::clone(&health_metrics);
816 async move { healthz(metrics).await }
817 }),
818 )
819 .route(
820 "/readyz",
821 get(move || {
822 let store = Arc::clone(&ready_store);
823 let runtime = ready_runtime.clone();
824 async move { readyz(store, runtime).await }
825 }),
826 )
827 .route(
828 "/memory/upsert",
829 post(
830 move |headers: HeaderMap, Json(request): Json<UpsertRequest>| {
831 let store = Arc::clone(&upsert_store);
832 let runtime = upsert_runtime.clone();
833 async move { upsert_http(store, request, headers, runtime).await }
834 },
835 ),
836 )
837 .route(
838 "/memory/batch-upsert",
839 post(
840 move |headers: HeaderMap, Json(request): Json<BatchUpsertRequest>| {
841 let store = Arc::clone(&batch_upsert_store);
842 let runtime = batch_upsert_runtime.clone();
843 async move { batch_upsert_http(store, request, headers, runtime).await }
844 },
845 ),
846 )
847 .route(
848 "/memory/recall",
849 post(
850 move |headers: HeaderMap, Json(request): Json<RecallQuery>| {
851 let store = Arc::clone(&recall_store);
852 let runtime = recall_runtime.clone();
853 async move { recall_http(store, request, headers, runtime).await }
854 },
855 ),
856 )
857 .route(
858 "/admin/snapshot",
859 get(move |headers: HeaderMap| {
860 let store = Arc::clone(&snapshot_store);
861 let runtime = snapshot_runtime.clone();
862 async move { snapshot_http(store, headers, runtime).await }
863 }),
864 )
865 .route(
866 "/admin/stats",
867 get(
868 move |headers: HeaderMap, Query(request): Query<StoreStatsRequest>| {
869 let store = Arc::clone(&stats_store);
870 let runtime = stats_runtime.clone();
871 async move { stats_http(store, request, headers, runtime).await }
872 },
873 ),
874 )
875 .route(
876 "/admin/graph",
877 post(
878 move |headers: HeaderMap, Json(request): Json<GraphInspectionRequest>| {
879 let store = Arc::clone(&graph_store);
880 let runtime = graph_runtime.clone();
881 async move { graph_http(store, request, headers, runtime).await }
882 },
883 ),
884 )
885 .route(
886 "/admin/integrity",
887 get(
888 move |headers: HeaderMap, Query(request): Query<IntegrityCheckRequest>| {
889 let store = Arc::clone(&integrity_store);
890 let runtime = integrity_runtime.clone();
891 async move { integrity_http(store, request, headers, runtime).await }
892 },
893 ),
894 )
895 .route(
896 "/admin/repair",
897 post(
898 move |headers: HeaderMap, Json(request): Json<RepairRequest>| {
899 let store = Arc::clone(&repair_store);
900 let runtime = repair_runtime.clone();
901 async move { repair_http(store, request, headers, runtime).await }
902 },
903 ),
904 )
905 .route(
906 "/admin/compact",
907 post(
908 move |headers: HeaderMap, Json(request): Json<CompactionRequest>| {
909 let store = Arc::clone(&compact_store);
910 let runtime = compact_runtime.clone();
911 async move { compact_http(store, request, headers, runtime).await }
912 },
913 ),
914 )
915 .route(
916 "/admin/delete",
917 post(
918 move |headers: HeaderMap, Json(request): Json<DeleteHttpRequest>| {
919 let store = Arc::clone(&delete_store);
920 let runtime = delete_runtime.clone();
921 async move { delete_http(store, request, headers, runtime).await }
922 },
923 ),
924 )
925 .route(
926 "/admin/archive",
927 post(
928 move |headers: HeaderMap, Json(request): Json<ArchiveHttpRequest>| {
929 let store = Arc::clone(&archive_store);
930 let runtime = archive_runtime.clone();
931 async move { archive_http(store, request, headers, runtime).await }
932 },
933 ),
934 )
935 .route(
936 "/admin/suppress",
937 post(
938 move |headers: HeaderMap, Json(request): Json<SuppressHttpRequest>| {
939 let store = Arc::clone(&suppress_store);
940 let runtime = suppress_runtime.clone();
941 async move { suppress_http(store, request, headers, runtime).await }
942 },
943 ),
944 )
945 .route(
946 "/admin/recover",
947 post(
948 move |headers: HeaderMap, Json(request): Json<RecoverHttpRequest>| {
949 let store = Arc::clone(&recover_store);
950 let runtime = recover_runtime.clone();
951 async move { recover_http(store, request, headers, runtime).await }
952 },
953 ),
954 )
955 .route(
956 "/admin/maintenance/run",
957 post(
958 move |headers: HeaderMap, Json(request): Json<MaintenanceRunRequest>| {
959 let store = Arc::clone(&maintenance_store);
960 let runtime = maintenance_runtime.clone();
961 async move { maintenance_run_http(store, request, headers, runtime).await }
962 },
963 ),
964 )
965 .route(
966 "/admin/traces",
967 get(move |Query(request): Query<TraceListRequest>| {
968 let runtime = traces_runtime.clone();
969 async move { traces_http(request, runtime).await }
970 }),
971 )
972 .route(
973 "/admin/traces/{trace_id}",
974 get(move |Path(trace_id): Path<String>| {
975 let runtime = trace_runtime.clone();
976 async move { trace_http(trace_id, runtime).await }
977 }),
978 )
979 .route(
980 "/admin/runtime",
981 get(move || {
982 let runtime = runtime_status_runtime.clone();
983 async move { runtime_status_http(runtime).await }
984 }),
985 )
986 .route(
987 "/admin/export",
988 post(
989 move |headers: HeaderMap, Json(request): Json<ExportRequest>| {
990 let store = Arc::clone(&export_store);
991 let runtime = export_runtime.clone();
992 async move { export_http(store, request, headers, runtime).await }
993 },
994 ),
995 )
996 .route(
997 "/admin/replication/ship",
998 post(
999 move |headers: HeaderMap, Json(request): Json<SnapshotShipRequest>| {
1000 let store = Arc::clone(&ship_store);
1001 let runtime = ship_runtime.clone();
1002 async move { snapshot_ship_http(store, request, headers, runtime).await }
1003 },
1004 ),
1005 )
1006 .route(
1007 "/admin/import",
1008 post(
1009 move |headers: HeaderMap, Json(request): Json<ImportRequest>| {
1010 let store = Arc::clone(&import_store);
1011 let runtime = import_runtime.clone();
1012 async move { import_http(store, request, headers, runtime).await }
1013 },
1014 ),
1015 )
1016 .route(
1017 "/metrics",
1018 get(move || {
1019 let metrics = Arc::clone(&metrics_metrics);
1020 async move { metrics_http(metrics).await }
1021 }),
1022 )
1023 .layer(middleware::from_fn(move |request, next| {
1024 let limits = Arc::clone(&middleware_limits);
1025 let metrics = Arc::clone(&middleware_metrics);
1026 let auth = Arc::clone(&middleware_auth);
1027 async move { enforce_http_guardrails(request, next, limits, metrics, auth).await }
1028 }))
1029}
1030
1031pub async fn serve_http<S>(
1032 addr: SocketAddr,
1033 store: Arc<S>,
1034 limits: ServerLimits,
1035 auth: AuthConfig,
1036) -> std::io::Result<()>
1037where
1038 S: MemoryStore + 'static,
1039{
1040 let backend = store.as_ref().backend_kind().to_string();
1041 serve_http_with_runtime(
1042 addr,
1043 store,
1044 ServerRuntime::new(backend, limits, Arc::new(ServerMetrics::default()), auth),
1045 )
1046 .await
1047}
1048
1049pub async fn serve_http_with_runtime<S>(
1050 addr: SocketAddr,
1051 store: Arc<S>,
1052 runtime: ServerRuntime,
1053) -> std::io::Result<()>
1054where
1055 S: MemoryStore + 'static,
1056{
1057 let listener = tokio::net::TcpListener::bind(addr).await?;
1058 axum::serve(listener, http_app_with_runtime(store, runtime)).await
1059}
1060
1061async fn healthz(metrics: Arc<ServerMetrics>) -> Json<HealthStatus> {
1062 metrics.http_healthz.fetch_add(1, Ordering::Relaxed);
1063 Json(HealthStatus { status: "ok" })
1064}
1065
1066async fn readyz<S>(
1067 store: Arc<S>,
1068 runtime: ServerRuntime,
1069) -> Result<Json<ReadyStatus>, (StatusCode, Json<HttpErrorBody>)>
1070where
1071 S: MemoryStore + 'static,
1072{
1073 runtime
1074 .metrics()
1075 .http_readyz
1076 .fetch_add(1, Ordering::Relaxed);
1077 let manifest = store.snapshot().await.map_err(map_http_store_error)?;
1078 Ok(Json(ReadyStatus {
1079 status: "ready",
1080 record_count: manifest.record_count,
1081 namespaces: manifest.namespaces,
1082 }))
1083}
1084
1085async fn upsert_http<S>(
1086 store: Arc<S>,
1087 request: UpsertRequest,
1088 headers: HeaderMap,
1089 runtime: ServerRuntime,
1090) -> Result<Json<UpsertReceipt>, (StatusCode, Json<HttpErrorBody>)>
1091where
1092 S: MemoryStore + 'static,
1093{
1094 runtime
1095 .metrics()
1096 .http_maintenance
1097 .fetch_add(1, Ordering::Relaxed);
1098 let started_at_unix_ms = now_unix_ms();
1099 let correlation_id = runtime.traces().next_id("corr");
1100 validate_record_limits(&request.record, runtime.limits().as_ref())
1101 .map_err(map_http_validation_error)?;
1102 let _permit = runtime
1103 .admission()
1104 .acquire(AdmissionClass::Write, Some(&request.record.scope.tenant_id))
1105 .await
1106 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1107 let receipt = store
1108 .upsert(request.clone())
1109 .await
1110 .map_err(map_http_store_error)?;
1111 record_trace(
1112 &runtime,
1113 TraceOperationKind::Upsert,
1114 "http",
1115 Some(request.record.scope.tenant_id),
1116 Some(request.record.scope.namespace),
1117 http_principal(&headers),
1118 started_at_unix_ms,
1119 TraceStatus::Ok,
1120 None,
1121 OperationTraceSummary {
1122 record_id: Some(request.record.id),
1123 ..OperationTraceSummary::default()
1124 },
1125 None,
1126 correlation_id,
1127 );
1128 Ok(Json(receipt))
1129}
1130
1131async fn batch_upsert_http<S>(
1132 store: Arc<S>,
1133 request: BatchUpsertRequest,
1134 headers: HeaderMap,
1135 runtime: ServerRuntime,
1136) -> Result<Json<Vec<UpsertReceipt>>, (StatusCode, Json<HttpErrorBody>)>
1137where
1138 S: MemoryStore + 'static,
1139{
1140 runtime
1141 .metrics()
1142 .http_snapshot_ship
1143 .fetch_add(1, Ordering::Relaxed);
1144 let started_at_unix_ms = now_unix_ms();
1145 let correlation_id = runtime.traces().next_id("corr");
1146 let tenant_id = request
1147 .requests
1148 .first()
1149 .map(|item| item.record.scope.tenant_id.clone());
1150 let namespace = request
1151 .requests
1152 .first()
1153 .map(|item| item.record.scope.namespace.clone());
1154 if request.requests.len() > runtime.limits().max_batch_upsert_requests {
1155 return Err((
1156 StatusCode::BAD_REQUEST,
1157 Json(HttpErrorBody {
1158 error: format!(
1159 "batch upsert request count exceeds configured max of {}",
1160 runtime.limits().max_batch_upsert_requests
1161 ),
1162 }),
1163 ));
1164 }
1165 for item in &request.requests {
1166 validate_record_limits(&item.record, runtime.limits().as_ref())
1167 .map_err(map_http_validation_error)?;
1168 }
1169 let _permit = runtime
1170 .admission()
1171 .acquire(AdmissionClass::Write, tenant_id.as_deref())
1172 .await
1173 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1174 let receipts = store
1175 .batch_upsert(request.clone())
1176 .await
1177 .map_err(map_http_store_error)?;
1178 record_trace(
1179 &runtime,
1180 TraceOperationKind::BatchUpsert,
1181 "http",
1182 tenant_id,
1183 namespace,
1184 http_principal(&headers),
1185 started_at_unix_ms,
1186 TraceStatus::Ok,
1187 None,
1188 OperationTraceSummary {
1189 request_count: Some(request.requests.len() as u32),
1190 ..OperationTraceSummary::default()
1191 },
1192 None,
1193 correlation_id,
1194 );
1195 Ok(Json(receipts))
1196}
1197
1198async fn recall_http<S>(
1199 store: Arc<S>,
1200 request: RecallQuery,
1201 headers: HeaderMap,
1202 runtime: ServerRuntime,
1203) -> Result<Json<RecallResult>, (StatusCode, Json<HttpErrorBody>)>
1204where
1205 S: MemoryStore + 'static,
1206{
1207 let started_at_unix_ms = now_unix_ms();
1208 let correlation_id = runtime.traces().next_id("corr");
1209 validate_recall_limits(&request, runtime.limits().as_ref())
1210 .map_err(map_http_validation_error)?;
1211 let _permit = runtime
1212 .admission()
1213 .acquire(AdmissionClass::Read, Some(&request.scope.tenant_id))
1214 .await
1215 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1216 let mut result = store
1217 .recall(request.clone())
1218 .await
1219 .map_err(map_http_store_error)?;
1220 attach_correlation_id(&mut result, &correlation_id);
1221 record_trace(
1222 &runtime,
1223 TraceOperationKind::Recall,
1224 "http",
1225 Some(request.scope.tenant_id),
1226 Some(request.scope.namespace),
1227 http_principal(&headers),
1228 started_at_unix_ms,
1229 TraceStatus::Ok,
1230 None,
1231 OperationTraceSummary {
1232 query_text: Some(request.query_text),
1233 max_items: Some(request.max_items as u32),
1234 token_budget: request.token_budget.map(|value| value as u32),
1235 ..OperationTraceSummary::default()
1236 },
1237 result.explanation.clone(),
1238 correlation_id,
1239 );
1240 Ok(Json(result))
1241}
1242
1243async fn snapshot_http<S>(
1244 store: Arc<S>,
1245 headers: HeaderMap,
1246 runtime: ServerRuntime,
1247) -> Result<Json<SnapshotManifest>, (StatusCode, Json<HttpErrorBody>)>
1248where
1249 S: MemoryStore + 'static,
1250{
1251 runtime
1252 .metrics()
1253 .http_snapshot
1254 .fetch_add(1, Ordering::Relaxed);
1255 let started_at_unix_ms = now_unix_ms();
1256 let correlation_id = runtime.traces().next_id("corr");
1257 let _permit = runtime
1258 .admission()
1259 .acquire(AdmissionClass::Admin, None)
1260 .await
1261 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1262 let manifest = store.snapshot().await.map_err(map_http_store_error)?;
1263 record_trace(
1264 &runtime,
1265 TraceOperationKind::Snapshot,
1266 "http",
1267 None,
1268 None,
1269 http_principal(&headers),
1270 started_at_unix_ms,
1271 TraceStatus::Ok,
1272 None,
1273 OperationTraceSummary::default(),
1274 None,
1275 correlation_id,
1276 );
1277 Ok(Json(manifest))
1278}
1279
1280async fn stats_http<S>(
1281 store: Arc<S>,
1282 request: StoreStatsRequest,
1283 headers: HeaderMap,
1284 runtime: ServerRuntime,
1285) -> Result<Json<StoreStatsReport>, (StatusCode, Json<HttpErrorBody>)>
1286where
1287 S: MemoryStore + 'static,
1288{
1289 let started_at_unix_ms = now_unix_ms();
1290 let correlation_id = runtime.traces().next_id("corr");
1291 let _permit = runtime
1292 .admission()
1293 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1294 .await
1295 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1296 let report = store
1297 .stats(request.clone())
1298 .await
1299 .map_err(map_http_store_error)?;
1300 record_trace(
1301 &runtime,
1302 TraceOperationKind::Stats,
1303 "http",
1304 request.tenant_id,
1305 request.namespace,
1306 http_principal(&headers),
1307 started_at_unix_ms,
1308 TraceStatus::Ok,
1309 None,
1310 OperationTraceSummary::default(),
1311 None,
1312 correlation_id,
1313 );
1314 Ok(Json(report))
1315}
1316
1317async fn graph_http<S>(
1318 store: Arc<S>,
1319 request: GraphInspectionRequest,
1320 headers: HeaderMap,
1321 runtime: ServerRuntime,
1322) -> Result<Json<GraphInspectionReport>, (StatusCode, Json<HttpErrorBody>)>
1323where
1324 S: MemoryStore + 'static,
1325{
1326 let started_at_unix_ms = now_unix_ms();
1327 let correlation_id = runtime.traces().next_id("corr");
1328 let _permit = runtime
1329 .admission()
1330 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1331 .await
1332 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1333 let report = store
1334 .inspect_graph(request.clone())
1335 .await
1336 .map_err(map_http_store_error)?;
1337 record_trace(
1338 &runtime,
1339 TraceOperationKind::Stats,
1340 "http",
1341 request.tenant_id,
1342 request.namespace,
1343 http_principal(&headers),
1344 started_at_unix_ms,
1345 TraceStatus::Ok,
1346 None,
1347 OperationTraceSummary {
1348 request_count: Some(report.nodes.len() as u32),
1349 max_items: Some(report.edges.len() as u32),
1350 ..Default::default()
1351 },
1352 None,
1353 correlation_id,
1354 );
1355 Ok(Json(report))
1356}
1357
1358async fn integrity_http<S>(
1359 store: Arc<S>,
1360 request: IntegrityCheckRequest,
1361 headers: HeaderMap,
1362 runtime: ServerRuntime,
1363) -> Result<Json<IntegrityCheckReport>, (StatusCode, Json<HttpErrorBody>)>
1364where
1365 S: MemoryStore + 'static,
1366{
1367 let started_at_unix_ms = now_unix_ms();
1368 let correlation_id = runtime.traces().next_id("corr");
1369 let _permit = runtime
1370 .admission()
1371 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1372 .await
1373 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1374 let report = store
1375 .integrity_check(request.clone())
1376 .await
1377 .map_err(map_http_store_error)?;
1378 record_trace(
1379 &runtime,
1380 TraceOperationKind::IntegrityCheck,
1381 "http",
1382 request.tenant_id,
1383 request.namespace,
1384 http_principal(&headers),
1385 started_at_unix_ms,
1386 TraceStatus::Ok,
1387 None,
1388 OperationTraceSummary::default(),
1389 None,
1390 correlation_id,
1391 );
1392 Ok(Json(report))
1393}
1394
1395async fn repair_http<S>(
1396 store: Arc<S>,
1397 request: RepairRequest,
1398 headers: HeaderMap,
1399 runtime: ServerRuntime,
1400) -> Result<Json<RepairReport>, (StatusCode, Json<HttpErrorBody>)>
1401where
1402 S: MemoryStore + 'static,
1403{
1404 let started_at_unix_ms = now_unix_ms();
1405 let correlation_id = runtime.traces().next_id("corr");
1406 let _permit = runtime
1407 .admission()
1408 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1409 .await
1410 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1411 let report = store
1412 .repair(request.clone())
1413 .await
1414 .map_err(map_http_store_error)?;
1415 record_trace(
1416 &runtime,
1417 TraceOperationKind::Repair,
1418 "http",
1419 request.tenant_id,
1420 request.namespace,
1421 http_principal(&headers),
1422 started_at_unix_ms,
1423 TraceStatus::Ok,
1424 None,
1425 OperationTraceSummary {
1426 dry_run: Some(request.dry_run),
1427 ..OperationTraceSummary::default()
1428 },
1429 None,
1430 correlation_id,
1431 );
1432 Ok(Json(report))
1433}
1434
1435async fn compact_http<S>(
1436 store: Arc<S>,
1437 request: CompactionRequest,
1438 headers: HeaderMap,
1439 runtime: ServerRuntime,
1440) -> Result<Json<CompactionReport>, (StatusCode, Json<HttpErrorBody>)>
1441where
1442 S: MemoryStore + 'static,
1443{
1444 runtime
1445 .metrics()
1446 .http_compact
1447 .fetch_add(1, Ordering::Relaxed);
1448 let started_at_unix_ms = now_unix_ms();
1449 let correlation_id = runtime.traces().next_id("corr");
1450 let _permit = runtime
1451 .admission()
1452 .acquire(AdmissionClass::Admin, Some(&request.tenant_id))
1453 .await
1454 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1455 let report = store
1456 .compact(request.clone())
1457 .await
1458 .map_err(map_http_store_error)?;
1459 record_trace(
1460 &runtime,
1461 TraceOperationKind::Compact,
1462 "http",
1463 Some(request.tenant_id),
1464 request.namespace,
1465 http_principal(&headers),
1466 started_at_unix_ms,
1467 TraceStatus::Ok,
1468 None,
1469 OperationTraceSummary {
1470 dry_run: Some(request.dry_run),
1471 ..OperationTraceSummary::default()
1472 },
1473 None,
1474 correlation_id,
1475 );
1476 Ok(Json(report))
1477}
1478
1479async fn delete_http<S>(
1480 store: Arc<S>,
1481 request: DeleteHttpRequest,
1482 headers: HeaderMap,
1483 runtime: ServerRuntime,
1484) -> Result<Json<DeleteReceipt>, (StatusCode, Json<HttpErrorBody>)>
1485where
1486 S: MemoryStore + 'static,
1487{
1488 runtime
1489 .metrics()
1490 .http_delete
1491 .fetch_add(1, Ordering::Relaxed);
1492 let started_at_unix_ms = now_unix_ms();
1493 let correlation_id = runtime.traces().next_id("corr");
1494 let _permit = runtime
1495 .admission()
1496 .acquire(AdmissionClass::Admin, Some(&request.tenant_id))
1497 .await
1498 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1499 let receipt = store
1500 .delete(DeleteRequest {
1501 tenant_id: request.tenant_id.clone(),
1502 namespace: request.namespace.clone(),
1503 record_id: request.record_id,
1504 hard_delete: request.hard_delete,
1505 audit_reason: request.audit_reason,
1506 })
1507 .await
1508 .map_err(map_http_store_error)?;
1509 record_trace(
1510 &runtime,
1511 TraceOperationKind::Delete,
1512 "http",
1513 Some(request.tenant_id),
1514 Some(request.namespace),
1515 http_principal(&headers),
1516 started_at_unix_ms,
1517 TraceStatus::Ok,
1518 None,
1519 OperationTraceSummary {
1520 record_id: Some(receipt.record_id.clone()),
1521 ..OperationTraceSummary::default()
1522 },
1523 None,
1524 correlation_id,
1525 );
1526 Ok(Json(receipt))
1527}
1528
1529async fn archive_http<S>(
1530 store: Arc<S>,
1531 request: ArchiveHttpRequest,
1532 headers: HeaderMap,
1533 runtime: ServerRuntime,
1534) -> Result<Json<ArchiveReceipt>, (StatusCode, Json<HttpErrorBody>)>
1535where
1536 S: MemoryStore + 'static,
1537{
1538 runtime
1539 .metrics()
1540 .http_archive
1541 .fetch_add(1, Ordering::Relaxed);
1542 let started_at_unix_ms = now_unix_ms();
1543 let correlation_id = runtime.traces().next_id("corr");
1544 let _permit = runtime
1545 .admission()
1546 .acquire(AdmissionClass::Admin, Some(&request.tenant_id))
1547 .await
1548 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1549 let receipt = store
1550 .archive(ArchiveRequest {
1551 tenant_id: request.tenant_id.clone(),
1552 namespace: request.namespace.clone(),
1553 record_id: request.record_id,
1554 dry_run: request.dry_run,
1555 audit_reason: request.audit_reason,
1556 })
1557 .await
1558 .map_err(map_http_store_error)?;
1559 record_trace(
1560 &runtime,
1561 TraceOperationKind::Archive,
1562 "http",
1563 Some(request.tenant_id),
1564 Some(request.namespace),
1565 http_principal(&headers),
1566 started_at_unix_ms,
1567 TraceStatus::Ok,
1568 None,
1569 OperationTraceSummary {
1570 record_id: Some(receipt.record_id.clone()),
1571 dry_run: Some(receipt.dry_run),
1572 ..OperationTraceSummary::default()
1573 },
1574 None,
1575 correlation_id,
1576 );
1577 Ok(Json(receipt))
1578}
1579
1580async fn suppress_http<S>(
1581 store: Arc<S>,
1582 request: SuppressHttpRequest,
1583 headers: HeaderMap,
1584 runtime: ServerRuntime,
1585) -> Result<Json<SuppressReceipt>, (StatusCode, Json<HttpErrorBody>)>
1586where
1587 S: MemoryStore + 'static,
1588{
1589 runtime
1590 .metrics()
1591 .http_suppress
1592 .fetch_add(1, Ordering::Relaxed);
1593 let started_at_unix_ms = now_unix_ms();
1594 let correlation_id = runtime.traces().next_id("corr");
1595 let _permit = runtime
1596 .admission()
1597 .acquire(AdmissionClass::Admin, Some(&request.tenant_id))
1598 .await
1599 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1600 let receipt = store
1601 .suppress(SuppressRequest {
1602 tenant_id: request.tenant_id.clone(),
1603 namespace: request.namespace.clone(),
1604 record_id: request.record_id,
1605 dry_run: request.dry_run,
1606 audit_reason: request.audit_reason,
1607 })
1608 .await
1609 .map_err(map_http_store_error)?;
1610 record_trace(
1611 &runtime,
1612 TraceOperationKind::Suppress,
1613 "http",
1614 Some(request.tenant_id),
1615 Some(request.namespace),
1616 http_principal(&headers),
1617 started_at_unix_ms,
1618 TraceStatus::Ok,
1619 None,
1620 OperationTraceSummary {
1621 record_id: Some(receipt.record_id.clone()),
1622 dry_run: Some(receipt.dry_run),
1623 ..OperationTraceSummary::default()
1624 },
1625 None,
1626 correlation_id,
1627 );
1628 Ok(Json(receipt))
1629}
1630
1631async fn recover_http<S>(
1632 store: Arc<S>,
1633 request: RecoverHttpRequest,
1634 headers: HeaderMap,
1635 runtime: ServerRuntime,
1636) -> Result<Json<RecoverReceipt>, (StatusCode, Json<HttpErrorBody>)>
1637where
1638 S: MemoryStore + 'static,
1639{
1640 runtime
1641 .metrics()
1642 .http_recover
1643 .fetch_add(1, Ordering::Relaxed);
1644 let started_at_unix_ms = now_unix_ms();
1645 let correlation_id = runtime.traces().next_id("corr");
1646 let _permit = runtime
1647 .admission()
1648 .acquire(AdmissionClass::Admin, Some(&request.tenant_id))
1649 .await
1650 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1651 let receipt = store
1652 .recover(RecoverRequest {
1653 tenant_id: request.tenant_id.clone(),
1654 namespace: request.namespace.clone(),
1655 record_id: request.record_id,
1656 dry_run: request.dry_run,
1657 audit_reason: request.audit_reason,
1658 quality_state: request.quality_state,
1659 historical_state: request.historical_state,
1660 })
1661 .await
1662 .map_err(map_http_store_error)?;
1663 record_trace(
1664 &runtime,
1665 TraceOperationKind::Recover,
1666 "http",
1667 Some(request.tenant_id),
1668 Some(request.namespace),
1669 http_principal(&headers),
1670 started_at_unix_ms,
1671 TraceStatus::Ok,
1672 None,
1673 OperationTraceSummary {
1674 record_id: Some(receipt.record_id.clone()),
1675 dry_run: Some(receipt.dry_run),
1676 ..OperationTraceSummary::default()
1677 },
1678 None,
1679 correlation_id,
1680 );
1681 Ok(Json(receipt))
1682}
1683
1684async fn traces_http(
1685 request: TraceListRequest,
1686 runtime: ServerRuntime,
1687) -> Result<Json<Vec<OperationTrace>>, (StatusCode, Json<HttpErrorBody>)> {
1688 runtime
1689 .metrics()
1690 .http_traces
1691 .fetch_add(1, Ordering::Relaxed);
1692 runtime
1693 .metrics()
1694 .trace_reads
1695 .fetch_add(1, Ordering::Relaxed);
1696 Ok(Json(runtime.traces().list(&request)))
1697}
1698
1699async fn trace_http(
1700 trace_id: String,
1701 runtime: ServerRuntime,
1702) -> Result<Json<OperationTrace>, (StatusCode, Json<HttpErrorBody>)> {
1703 runtime
1704 .metrics()
1705 .http_traces
1706 .fetch_add(1, Ordering::Relaxed);
1707 runtime
1708 .metrics()
1709 .trace_reads
1710 .fetch_add(1, Ordering::Relaxed);
1711 runtime.traces().get(&trace_id).map(Json).ok_or_else(|| {
1712 (
1713 StatusCode::NOT_FOUND,
1714 Json(HttpErrorBody {
1715 error: format!("trace {trace_id} not found"),
1716 }),
1717 )
1718 })
1719}
1720
1721async fn runtime_status_http(runtime: ServerRuntime) -> Json<ServerRuntimeStatus> {
1722 runtime
1723 .metrics()
1724 .http_runtime
1725 .fetch_add(1, Ordering::Relaxed);
1726 Json(ServerRuntimeStatus {
1727 backend: runtime.backend().as_ref().clone(),
1728 admission: runtime.admission().snapshot(),
1729 traces: runtime.traces().snapshot(),
1730 })
1731}
1732
1733async fn maintenance_run_http<S>(
1734 store: Arc<S>,
1735 request: MaintenanceRunRequest,
1736 headers: HeaderMap,
1737 runtime: ServerRuntime,
1738) -> Result<Json<MaintenanceRunReport>, (StatusCode, Json<HttpErrorBody>)>
1739where
1740 S: MemoryStore + 'static,
1741{
1742 let started_at_unix_ms = now_unix_ms();
1743 let correlation_id = runtime.traces().next_id("corr");
1744 let _permit = runtime
1745 .admission()
1746 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1747 .await
1748 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1749 let report = store
1750 .run_maintenance(request.clone())
1751 .await
1752 .map_err(map_http_store_error)?;
1753 record_trace(
1754 &runtime,
1755 TraceOperationKind::MaintenanceRun,
1756 "http",
1757 request.tenant_id,
1758 request.namespace,
1759 http_principal(&headers),
1760 started_at_unix_ms,
1761 TraceStatus::Ok,
1762 None,
1763 OperationTraceSummary {
1764 dry_run: Some(report.dry_run),
1765 ..Default::default()
1766 },
1767 None,
1768 correlation_id,
1769 );
1770 Ok(Json(report))
1771}
1772
1773async fn export_http<S>(
1774 store: Arc<S>,
1775 request: ExportRequest,
1776 headers: HeaderMap,
1777 runtime: ServerRuntime,
1778) -> Result<Json<PortableStorePackage>, (StatusCode, Json<HttpErrorBody>)>
1779where
1780 S: MemoryStore + 'static,
1781{
1782 runtime
1783 .metrics()
1784 .http_export
1785 .fetch_add(1, Ordering::Relaxed);
1786 let started_at_unix_ms = now_unix_ms();
1787 let correlation_id = runtime.traces().next_id("corr");
1788 let _permit = runtime
1789 .admission()
1790 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1791 .await
1792 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1793 let package = store
1794 .export(request.clone())
1795 .await
1796 .map_err(map_http_store_error)?;
1797 record_trace(
1798 &runtime,
1799 TraceOperationKind::Export,
1800 "http",
1801 request.tenant_id,
1802 request.namespace,
1803 http_principal(&headers),
1804 started_at_unix_ms,
1805 TraceStatus::Ok,
1806 None,
1807 OperationTraceSummary::default(),
1808 None,
1809 correlation_id,
1810 );
1811 Ok(Json(package))
1812}
1813
1814async fn snapshot_ship_http<S>(
1815 store: Arc<S>,
1816 request: SnapshotShipRequest,
1817 headers: HeaderMap,
1818 runtime: ServerRuntime,
1819) -> Result<Json<SnapshotShipReport>, (StatusCode, Json<HttpErrorBody>)>
1820where
1821 S: MemoryStore + 'static,
1822{
1823 let started_at_unix_ms = now_unix_ms();
1824 let correlation_id = runtime.traces().next_id("corr");
1825 let _permit = runtime
1826 .admission()
1827 .acquire(AdmissionClass::Admin, request.tenant_id.as_deref())
1828 .await
1829 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1830
1831 let package = store
1832 .export(ExportRequest {
1833 tenant_id: request.tenant_id.clone(),
1834 namespace: request.namespace.clone(),
1835 include_archived: request.include_archived,
1836 })
1837 .await
1838 .map_err(map_http_store_error)?;
1839 let exported_records = package.records.len() as u64;
1840 let import_request = ImportRequest {
1841 package,
1842 mode: request.mode,
1843 dry_run: request.dry_run,
1844 };
1845 let (remote_status, remote_report) = post_remote_import(
1846 &request.target_url,
1847 request.bearer_token.as_deref(),
1848 &import_request,
1849 )
1850 .await?;
1851 let report = SnapshotShipReport {
1852 target_url: request.target_url.clone(),
1853 exported_records,
1854 imported_records: remote_report.imported_records,
1855 skipped_records: remote_report.skipped_records,
1856 dry_run: request.dry_run,
1857 compatible_package: remote_report.compatible_package,
1858 remote_status,
1859 remote_snapshot_id: Some(remote_report.snapshot_id),
1860 };
1861 record_trace(
1862 &runtime,
1863 TraceOperationKind::SnapshotShip,
1864 "http",
1865 request.tenant_id,
1866 request.namespace,
1867 http_principal(&headers),
1868 started_at_unix_ms,
1869 TraceStatus::Ok,
1870 None,
1871 OperationTraceSummary {
1872 request_count: Some(exported_records as u32),
1873 dry_run: Some(request.dry_run),
1874 ..Default::default()
1875 },
1876 None,
1877 correlation_id,
1878 );
1879 Ok(Json(report))
1880}
1881
1882async fn import_http<S>(
1883 store: Arc<S>,
1884 request: ImportRequest,
1885 headers: HeaderMap,
1886 runtime: ServerRuntime,
1887) -> Result<Json<ImportReport>, (StatusCode, Json<HttpErrorBody>)>
1888where
1889 S: MemoryStore + 'static,
1890{
1891 runtime
1892 .metrics()
1893 .http_import
1894 .fetch_add(1, Ordering::Relaxed);
1895 let started_at_unix_ms = now_unix_ms();
1896 let correlation_id = runtime.traces().next_id("corr");
1897 let tenant_id = request
1898 .package
1899 .records
1900 .first()
1901 .map(|entry| entry.record.scope.tenant_id.clone());
1902 let namespace = request
1903 .package
1904 .records
1905 .first()
1906 .map(|entry| entry.record.scope.namespace.clone());
1907 let _permit = runtime
1908 .admission()
1909 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
1910 .await
1911 .map_err(|error| map_http_admission_error(runtime.metrics().as_ref(), error))?;
1912 let report = store
1913 .import(request.clone())
1914 .await
1915 .map_err(map_http_store_error)?;
1916 record_trace(
1917 &runtime,
1918 TraceOperationKind::Import,
1919 "http",
1920 tenant_id,
1921 namespace,
1922 http_principal(&headers),
1923 started_at_unix_ms,
1924 if report.failed_records.is_empty() {
1925 TraceStatus::Ok
1926 } else {
1927 TraceStatus::Error
1928 },
1929 (!report.failed_records.is_empty())
1930 .then(|| format!("{} import validation failures", report.failed_records.len())),
1931 OperationTraceSummary {
1932 request_count: Some(request.package.records.len() as u32),
1933 dry_run: Some(request.dry_run),
1934 ..OperationTraceSummary::default()
1935 },
1936 None,
1937 correlation_id,
1938 );
1939 Ok(Json(report))
1940}
1941
1942#[derive(Debug, Clone)]
1943struct HttpTarget {
1944 host: String,
1945 port: u16,
1946 path: String,
1947}
1948
1949fn parse_http_target(raw: &str) -> Result<HttpTarget, String> {
1950 let without_scheme = raw
1951 .strip_prefix("http://")
1952 .ok_or_else(|| "snapshot shipping currently supports http:// targets".to_string())?;
1953 let (authority, path) = without_scheme
1954 .split_once('/')
1955 .map(|(authority, path)| (authority, format!("/{path}")))
1956 .unwrap_or((without_scheme, "/admin/import".to_string()));
1957 let path = if path.ends_with("/admin/import") {
1958 path
1959 } else {
1960 format!("{}/admin/import", path.trim_end_matches('/'))
1961 };
1962 let (host, port) = authority
1963 .rsplit_once(':')
1964 .map(|(host, port)| {
1965 port.parse::<u16>()
1966 .map(|port| (host.to_string(), port))
1967 .map_err(|err| format!("invalid snapshot shipping target port '{port}': {err}"))
1968 })
1969 .unwrap_or_else(|| Ok((authority.to_string(), 80)))?;
1970 if host.trim().is_empty() {
1971 return Err("snapshot shipping target host is required".to_string());
1972 }
1973 Ok(HttpTarget { host, port, path })
1974}
1975
1976async fn post_remote_import(
1977 target_url: &str,
1978 bearer_token: Option<&str>,
1979 request: &ImportRequest,
1980) -> Result<(u16, ImportReport), (StatusCode, Json<HttpErrorBody>)> {
1981 post_remote_import_raw(target_url, bearer_token, request)
1982 .await
1983 .map_err(|error| (StatusCode::BAD_GATEWAY, Json(HttpErrorBody { error })))
1984}
1985
1986async fn post_remote_import_raw(
1987 target_url: &str,
1988 bearer_token: Option<&str>,
1989 request: &ImportRequest,
1990) -> Result<(u16, ImportReport), String> {
1991 let target = parse_http_target(target_url)
1992 .map_err(|error| format!("invalid snapshot shipping target: {error}"))?;
1993 let body = serde_json::to_string(request)
1994 .map_err(|err| format!("failed to encode snapshot shipment: {err}"))?;
1995 let bearer_token = bearer_token.map(str::to_string);
1996 tokio::task::spawn_blocking(move || {
1997 use std::io::{Read, Write};
1998 use std::net::TcpStream;
1999
2000 let mut stream = TcpStream::connect((target.host.as_str(), target.port))
2001 .map_err(|err| format!("failed to connect to snapshot target: {err}"))?;
2002 let mut request_head = format!(
2003 "POST {} HTTP/1.1\r\nHost: {}\r\nContent-Type: application/json\r\nAccept: application/json\r\nContent-Length: {}\r\nConnection: close\r\n",
2004 target.path,
2005 target.host,
2006 body.len()
2007 );
2008 if let Some(token) = bearer_token {
2009 request_head.push_str(&format!("Authorization: Bearer {token}\r\n"));
2010 }
2011 request_head.push_str("\r\n");
2012 stream
2013 .write_all(request_head.as_bytes())
2014 .and_then(|_| stream.write_all(body.as_bytes()))
2015 .map_err(|err| format!("failed to write snapshot shipment: {err}"))?;
2016 let mut response = String::new();
2017 stream
2018 .read_to_string(&mut response)
2019 .map_err(|err| format!("failed to read snapshot shipment response: {err}"))?;
2020 let (head, body) = response
2021 .split_once("\r\n\r\n")
2022 .ok_or_else(|| "snapshot target returned an invalid HTTP response".to_string())?;
2023 let status = head
2024 .lines()
2025 .next()
2026 .and_then(|line| line.split_whitespace().nth(1))
2027 .and_then(|value| value.parse::<u16>().ok())
2028 .ok_or_else(|| "snapshot target returned an invalid HTTP status".to_string())?;
2029 if !(200..300).contains(&status) {
2030 return Err(format!("snapshot target returned HTTP {status}: {body}"));
2031 }
2032 serde_json::from_str::<ImportReport>(body)
2033 .map(|report| (status, report))
2034 .map_err(|err| format!("failed to decode snapshot target import report: {err}"))
2035 })
2036 .await
2037 .map_err(|err| format!("snapshot shipping worker failed: {err}"))?
2038}
2039
2040async fn metrics_http(metrics: Arc<ServerMetrics>) -> impl IntoResponse {
2041 metrics.http_metrics.fetch_add(1, Ordering::Relaxed);
2042 (
2043 [(
2044 axum::http::header::CONTENT_TYPE,
2045 "text/plain; version=0.0.4",
2046 )],
2047 metrics.render(),
2048 )
2049}
2050
2051async fn enforce_http_guardrails(
2052 request: AxumRequest,
2053 next: Next,
2054 limits: Arc<ServerLimits>,
2055 metrics: Arc<ServerMetrics>,
2056 auth: Arc<AuthConfig>,
2057) -> Result<axum::response::Response, StatusCode> {
2058 let path = request.uri().path().to_string();
2059 if let Some(content_length) = request.headers().get(axum::http::header::CONTENT_LENGTH)
2060 && let Ok(content_length) = content_length.to_str()
2061 && let Ok(content_length) = content_length.parse::<usize>()
2062 && content_length > limits.max_http_body_bytes
2063 {
2064 metrics
2065 .http_rejected_body_too_large
2066 .fetch_add(1, Ordering::Relaxed);
2067 return Err(StatusCode::PAYLOAD_TOO_LARGE);
2068 }
2069 validate_http_auth(&request, &path, auth.as_ref())?;
2070 Ok(next.run(request).await)
2071}
2072
2073fn http_principal(headers: &HeaderMap) -> Option<String> {
2074 headers
2075 .get(axum::http::header::AUTHORIZATION)
2076 .and_then(|value| value.to_str().ok())
2077 .map(|_| "bearer".to_string())
2078}
2079
2080fn grpc_principal<T>(request: &Request<T>) -> Option<String> {
2081 request
2082 .metadata()
2083 .get("authorization")
2084 .and_then(|value| value.to_str().ok())
2085 .map(|_| "bearer".to_string())
2086}
2087
2088fn map_http_admission_error(
2089 metrics: &ServerMetrics,
2090 error: AdmissionError,
2091) -> (StatusCode, Json<HttpErrorBody>) {
2092 match error {
2093 AdmissionError::QueueFull | AdmissionError::TenantBusy => {
2094 metrics.admission_rejected.fetch_add(1, Ordering::Relaxed);
2095 (
2096 StatusCode::TOO_MANY_REQUESTS,
2097 Json(HttpErrorBody {
2098 error: "request admission rejected".to_string(),
2099 }),
2100 )
2101 }
2102 AdmissionError::TimedOut => {
2103 metrics.admission_timed_out.fetch_add(1, Ordering::Relaxed);
2104 (
2105 StatusCode::TOO_MANY_REQUESTS,
2106 Json(HttpErrorBody {
2107 error: "request admission timed out".to_string(),
2108 }),
2109 )
2110 }
2111 }
2112}
2113
2114fn map_grpc_admission_error(metrics: &ServerMetrics, error: AdmissionError) -> Status {
2115 match error {
2116 AdmissionError::QueueFull | AdmissionError::TenantBusy => {
2117 metrics.admission_rejected.fetch_add(1, Ordering::Relaxed);
2118 Status::resource_exhausted("request admission rejected")
2119 }
2120 AdmissionError::TimedOut => {
2121 metrics.admission_timed_out.fetch_add(1, Ordering::Relaxed);
2122 Status::resource_exhausted("request admission timed out")
2123 }
2124 }
2125}
2126
2127fn attach_correlation_id(result: &mut RecallResult, correlation_id: &str) {
2128 if let Some(explanation) = result.explanation.as_mut() {
2129 if let Some(existing) = explanation.trace_id.replace(correlation_id.to_string()) {
2130 explanation
2131 .policy_notes
2132 .push(format!("planning_trace_id={existing}"));
2133 }
2134 explanation
2135 .policy_notes
2136 .push(format!("correlation_id={correlation_id}"));
2137 }
2138 for hit in &mut result.hits {
2139 if let Some(explanation) = hit.explanation.as_mut() {
2140 if let Some(existing) = explanation.trace_id.replace(correlation_id.to_string()) {
2141 explanation
2142 .policy_notes
2143 .push(format!("planning_trace_id={existing}"));
2144 }
2145 explanation
2146 .policy_notes
2147 .push(format!("correlation_id={correlation_id}"));
2148 }
2149 }
2150}
2151
2152#[allow(clippy::too_many_arguments)]
2153fn record_trace(
2154 runtime: &ServerRuntime,
2155 operation: TraceOperationKind,
2156 transport: &str,
2157 tenant_id: Option<String>,
2158 namespace: Option<String>,
2159 principal: Option<String>,
2160 started_at_unix_ms: u64,
2161 status: TraceStatus,
2162 status_message: Option<String>,
2163 summary: OperationTraceSummary,
2164 recall_explanation: Option<RecallExplanation>,
2165 correlation_id: String,
2166) {
2167 let completed_at_unix_ms = now_unix_ms();
2168 let admission_class = admission_class_for_operation(operation.clone()).to_string();
2169 let planning_trace_id = recall_explanation.as_ref().and_then(|value| {
2170 value
2171 .planning_trace
2172 .as_ref()
2173 .map(|trace| trace.trace_id.clone())
2174 });
2175 let store_span_id = runtime.traces().next_id("store-span");
2176 let evicted = runtime.traces().record(OperationTrace {
2177 trace_id: runtime.traces().next_id("trace"),
2178 correlation_id,
2179 operation,
2180 transport: transport.to_string(),
2181 backend: Some(runtime.backend().as_ref().clone()),
2182 admission_class: Some(admission_class),
2183 tenant_id,
2184 namespace,
2185 principal,
2186 store_span_id: Some(store_span_id),
2187 planning_trace_id,
2188 started_at_unix_ms,
2189 completed_at_unix_ms,
2190 latency_ms: completed_at_unix_ms.saturating_sub(started_at_unix_ms),
2191 status,
2192 status_message,
2193 summary,
2194 recall_explanation,
2195 });
2196 runtime
2197 .metrics()
2198 .trace_records
2199 .fetch_add(1, Ordering::Relaxed);
2200 if evicted {
2201 runtime
2202 .metrics()
2203 .trace_evictions
2204 .fetch_add(1, Ordering::Relaxed);
2205 }
2206}
2207
2208fn admission_class_for_operation(operation: TraceOperationKind) -> &'static str {
2209 match operation {
2210 TraceOperationKind::Recall | TraceOperationKind::Snapshot | TraceOperationKind::Stats => {
2211 "read"
2212 }
2213 TraceOperationKind::Upsert
2214 | TraceOperationKind::BatchUpsert
2215 | TraceOperationKind::Compact
2216 | TraceOperationKind::Delete
2217 | TraceOperationKind::Archive
2218 | TraceOperationKind::Suppress
2219 | TraceOperationKind::Recover => "write",
2220 TraceOperationKind::IntegrityCheck
2221 | TraceOperationKind::Repair
2222 | TraceOperationKind::Export
2223 | TraceOperationKind::Import
2224 | TraceOperationKind::MaintenanceRun
2225 | TraceOperationKind::SnapshotShip => "admin",
2226 }
2227}
2228
2229fn validate_http_auth(
2230 request: &AxumRequest,
2231 path: &str,
2232 auth: &AuthConfig,
2233) -> Result<(), StatusCode> {
2234 if path == "/healthz" || path == "/readyz" || (path == "/metrics" && !auth.protect_metrics) {
2235 return Ok(());
2236 }
2237 let provided = request
2238 .headers()
2239 .get(axum::http::header::AUTHORIZATION)
2240 .and_then(|value| value.to_str().ok());
2241 let permission = match path {
2242 "/metrics" => AuthPermission::Metrics,
2243 "/memory/recall" => AuthPermission::Read,
2244 "/memory/upsert" | "/memory/batch-upsert" => AuthPermission::Write,
2245 "/admin/stats"
2246 | "/admin/graph"
2247 | "/admin/integrity"
2248 | "/admin/repair"
2249 | "/admin/snapshot"
2250 | "/admin/compact"
2251 | "/admin/delete"
2252 | "/admin/archive"
2253 | "/admin/suppress"
2254 | "/admin/recover"
2255 | "/admin/maintenance/run"
2256 | "/admin/runtime"
2257 | "/admin/export"
2258 | "/admin/replication/ship"
2259 | "/admin/import" => AuthPermission::Admin,
2260 _ => AuthPermission::Admin,
2261 };
2262 if path.starts_with("/admin/traces") {
2263 return auth
2264 .authorize(provided, AuthPermission::Admin)
2265 .then_some(())
2266 .ok_or(StatusCode::UNAUTHORIZED);
2267 }
2268 if auth.authorize(provided, permission) {
2269 Ok(())
2270 } else {
2271 Err(StatusCode::UNAUTHORIZED)
2272 }
2273}
2274
2275fn validate_grpc_auth<T>(
2276 request: &Request<T>,
2277 auth: &AuthConfig,
2278 permission: AuthPermission,
2279) -> Result<(), Status> {
2280 let provided = request
2281 .metadata()
2282 .get("authorization")
2283 .and_then(|value| value.to_str().ok());
2284 if auth.authorize(provided, permission) {
2285 Ok(())
2286 } else {
2287 Err(Status::unauthenticated("missing or invalid bearer token"))
2288 }
2289}
2290
2291fn bearer_token_from_header(header: Option<&str>) -> Option<&str> {
2292 header.and_then(|value| value.strip_prefix("Bearer "))
2293}
2294
2295fn invalid_argument(message: impl Into<String>) -> Status {
2296 Status::invalid_argument(message.into())
2297}
2298
2299fn internal_status(message: impl Into<String>) -> Status {
2300 Status::internal(message.into())
2301}
2302
2303fn trust_level_from_proto(value: &str) -> Result<MemoryTrustLevel, Status> {
2304 match value {
2305 "" | "derived" => Ok(MemoryTrustLevel::Derived),
2306 "untrusted" => Ok(MemoryTrustLevel::Untrusted),
2307 "observed" => Ok(MemoryTrustLevel::Observed),
2308 "verified" => Ok(MemoryTrustLevel::Verified),
2309 "pinned" => Ok(MemoryTrustLevel::Pinned),
2310 other => Err(invalid_argument(format!("unknown trust level: {other}"))),
2311 }
2312}
2313
2314fn trust_level_to_proto(value: MemoryTrustLevel) -> String {
2315 match value {
2316 MemoryTrustLevel::Untrusted => "untrusted",
2317 MemoryTrustLevel::Observed => "observed",
2318 MemoryTrustLevel::Derived => "derived",
2319 MemoryTrustLevel::Verified => "verified",
2320 MemoryTrustLevel::Pinned => "pinned",
2321 }
2322 .to_string()
2323}
2324
2325fn record_kind_from_proto(value: &str) -> Result<MemoryRecordKind, Status> {
2326 match value {
2327 "episodic" => Ok(MemoryRecordKind::Episodic),
2328 "summary" => Ok(MemoryRecordKind::Summary),
2329 "fact" => Ok(MemoryRecordKind::Fact),
2330 "preference" => Ok(MemoryRecordKind::Preference),
2331 "task" => Ok(MemoryRecordKind::Task),
2332 "artifact" => Ok(MemoryRecordKind::Artifact),
2333 "hypothesis" => Ok(MemoryRecordKind::Hypothesis),
2334 other => Err(invalid_argument(format!("unknown record kind: {other}"))),
2335 }
2336}
2337
2338fn record_kind_to_proto(value: MemoryRecordKind) -> String {
2339 match value {
2340 MemoryRecordKind::Episodic => "episodic",
2341 MemoryRecordKind::Summary => "summary",
2342 MemoryRecordKind::Fact => "fact",
2343 MemoryRecordKind::Preference => "preference",
2344 MemoryRecordKind::Task => "task",
2345 MemoryRecordKind::Artifact => "artifact",
2346 MemoryRecordKind::Hypothesis => "hypothesis",
2347 }
2348 .to_string()
2349}
2350
2351fn quality_state_from_proto(value: &str) -> Result<MemoryQualityState, Status> {
2352 match value {
2353 "draft" => Ok(MemoryQualityState::Draft),
2354 "active" => Ok(MemoryQualityState::Active),
2355 "verified" => Ok(MemoryQualityState::Verified),
2356 "archived" => Ok(MemoryQualityState::Archived),
2357 "suppressed" => Ok(MemoryQualityState::Suppressed),
2358 "deleted" => Ok(MemoryQualityState::Deleted),
2359 other => Err(invalid_argument(format!("unknown quality state: {other}"))),
2360 }
2361}
2362
2363fn quality_state_to_proto(value: MemoryQualityState) -> String {
2364 match value {
2365 MemoryQualityState::Draft => "draft",
2366 MemoryQualityState::Active => "active",
2367 MemoryQualityState::Verified => "verified",
2368 MemoryQualityState::Archived => "archived",
2369 MemoryQualityState::Suppressed => "suppressed",
2370 MemoryQualityState::Deleted => "deleted",
2371 }
2372 .to_string()
2373}
2374
2375fn scope_from_proto(scope: ProtoMemoryScope) -> Result<MemoryScope, Status> {
2376 Ok(MemoryScope {
2377 tenant_id: scope.tenant_id,
2378 namespace: scope.namespace,
2379 actor_id: scope.actor_id,
2380 conversation_id: scope.conversation_id,
2381 session_id: scope.session_id,
2382 source: scope.source,
2383 labels: scope.labels,
2384 trust_level: trust_level_from_proto(&scope.trust_level)?,
2385 })
2386}
2387
2388fn scope_to_proto(scope: MemoryScope) -> ProtoMemoryScope {
2389 ProtoMemoryScope {
2390 tenant_id: scope.tenant_id,
2391 namespace: scope.namespace,
2392 actor_id: scope.actor_id,
2393 conversation_id: scope.conversation_id,
2394 session_id: scope.session_id,
2395 source: scope.source,
2396 labels: scope.labels,
2397 trust_level: trust_level_to_proto(scope.trust_level),
2398 }
2399}
2400
2401fn artifact_from_proto(value: ProtoArtifactPointer) -> ArtifactPointer {
2402 ArtifactPointer {
2403 uri: value.uri,
2404 media_type: value.media_type,
2405 checksum: value.checksum,
2406 }
2407}
2408
2409fn artifact_to_proto(value: ArtifactPointer) -> ProtoArtifactPointer {
2410 ProtoArtifactPointer {
2411 uri: value.uri,
2412 media_type: value.media_type,
2413 checksum: value.checksum,
2414 }
2415}
2416
2417fn continuity_state_from_proto(value: &str) -> Result<EpisodeContinuityState, Status> {
2418 match value {
2419 "" | "open" => Ok(EpisodeContinuityState::Open),
2420 "resolved" => Ok(EpisodeContinuityState::Resolved),
2421 "superseded" => Ok(EpisodeContinuityState::Superseded),
2422 "abandoned" => Ok(EpisodeContinuityState::Abandoned),
2423 other => Err(invalid_argument(format!(
2424 "unknown continuity state: {other}"
2425 ))),
2426 }
2427}
2428
2429fn continuity_state_to_proto(value: EpisodeContinuityState) -> String {
2430 match value {
2431 EpisodeContinuityState::Open => "open",
2432 EpisodeContinuityState::Resolved => "resolved",
2433 EpisodeContinuityState::Superseded => "superseded",
2434 EpisodeContinuityState::Abandoned => "abandoned",
2435 }
2436 .to_string()
2437}
2438
2439fn affective_provenance_from_proto(value: &str) -> Result<AffectiveAnnotationProvenance, Status> {
2440 match value {
2441 "" | "authored" => Ok(AffectiveAnnotationProvenance::Authored),
2442 "imported" => Ok(AffectiveAnnotationProvenance::Imported),
2443 "derived" => Ok(AffectiveAnnotationProvenance::Derived),
2444 other => Err(invalid_argument(format!(
2445 "unknown affective provenance: {other}"
2446 ))),
2447 }
2448}
2449
2450fn affective_provenance_to_proto(value: AffectiveAnnotationProvenance) -> String {
2451 match value {
2452 AffectiveAnnotationProvenance::Authored => "authored",
2453 AffectiveAnnotationProvenance::Imported => "imported",
2454 AffectiveAnnotationProvenance::Derived => "derived",
2455 }
2456 .to_string()
2457}
2458
2459fn temporal_order_from_proto(value: &str) -> Result<RecallTemporalOrder, Status> {
2460 match value {
2461 "" | "relevance" => Ok(RecallTemporalOrder::Relevance),
2462 "chronological_asc" => Ok(RecallTemporalOrder::ChronologicalAsc),
2463 "chronological_desc" => Ok(RecallTemporalOrder::ChronologicalDesc),
2464 other => Err(invalid_argument(format!("unknown temporal order: {other}"))),
2465 }
2466}
2467
2468fn planning_profile_from_proto(value: &str) -> Result<RecallPlanningProfile, Status> {
2469 match value {
2470 "" | "fast_path" => Ok(RecallPlanningProfile::FastPath),
2471 "continuity_aware" => Ok(RecallPlanningProfile::ContinuityAware),
2472 other => Err(invalid_argument(format!(
2473 "unknown planning profile: {other}"
2474 ))),
2475 }
2476}
2477
2478fn planning_profile_to_proto(value: RecallPlanningProfile) -> String {
2479 match value {
2480 RecallPlanningProfile::FastPath => "fast_path",
2481 RecallPlanningProfile::ContinuityAware => "continuity_aware",
2482 }
2483 .to_string()
2484}
2485
2486fn planner_stage_to_proto(value: RecallPlannerStage) -> String {
2487 match value {
2488 RecallPlannerStage::CandidateGeneration => "candidate_generation",
2489 RecallPlannerStage::GraphExpansion => "graph_expansion",
2490 RecallPlannerStage::Selection => "selection",
2491 }
2492 .to_string()
2493}
2494
2495fn candidate_source_to_proto(value: RecallCandidateSource) -> String {
2496 match value {
2497 RecallCandidateSource::Lexical => "lexical",
2498 RecallCandidateSource::Semantic => "semantic",
2499 RecallCandidateSource::Metadata => "metadata",
2500 RecallCandidateSource::Episode => "episode",
2501 RecallCandidateSource::Graph => "graph",
2502 RecallCandidateSource::Temporal => "temporal",
2503 RecallCandidateSource::Provenance => "provenance",
2504 }
2505 .to_string()
2506}
2507
2508fn affective_annotation_from_proto(
2509 value: ProtoAffectiveAnnotation,
2510) -> Result<AffectiveAnnotation, Status> {
2511 Ok(AffectiveAnnotation {
2512 tone: value.tone,
2513 sentiment: value.sentiment,
2514 urgency: value.urgency,
2515 confidence: value.confidence,
2516 tension: value.tension,
2517 provenance: affective_provenance_from_proto(&value.provenance)?,
2518 })
2519}
2520
2521fn affective_annotation_to_proto(value: AffectiveAnnotation) -> ProtoAffectiveAnnotation {
2522 ProtoAffectiveAnnotation {
2523 tone: value.tone,
2524 sentiment: value.sentiment,
2525 urgency: value.urgency,
2526 confidence: value.confidence,
2527 tension: value.tension,
2528 provenance: affective_provenance_to_proto(value.provenance),
2529 }
2530}
2531
2532fn historical_state_from_proto(value: &str) -> Result<MemoryHistoricalState, Status> {
2533 match value {
2534 "" | "current" => Ok(MemoryHistoricalState::Current),
2535 "historical" => Ok(MemoryHistoricalState::Historical),
2536 "superseded" => Ok(MemoryHistoricalState::Superseded),
2537 other => Err(invalid_argument(format!(
2538 "unknown historical state: {other}"
2539 ))),
2540 }
2541}
2542
2543fn historical_state_to_proto(value: MemoryHistoricalState) -> String {
2544 match value {
2545 MemoryHistoricalState::Current => "current",
2546 MemoryHistoricalState::Historical => "historical",
2547 MemoryHistoricalState::Superseded => "superseded",
2548 }
2549 .to_string()
2550}
2551
2552fn lineage_relation_from_proto(value: &str) -> Result<LineageRelationKind, Status> {
2553 match value {
2554 "" | "derived_from" => Ok(LineageRelationKind::DerivedFrom),
2555 "consolidated_from" => Ok(LineageRelationKind::ConsolidatedFrom),
2556 "supersedes" => Ok(LineageRelationKind::Supersedes),
2557 "superseded_by" => Ok(LineageRelationKind::SupersededBy),
2558 "conflicts_with" => Ok(LineageRelationKind::ConflictsWith),
2559 other => Err(invalid_argument(format!(
2560 "unknown lineage relation: {other}"
2561 ))),
2562 }
2563}
2564
2565fn lineage_relation_to_proto(value: LineageRelationKind) -> String {
2566 match value {
2567 LineageRelationKind::DerivedFrom => "derived_from",
2568 LineageRelationKind::ConsolidatedFrom => "consolidated_from",
2569 LineageRelationKind::Supersedes => "supersedes",
2570 LineageRelationKind::SupersededBy => "superseded_by",
2571 LineageRelationKind::ConflictsWith => "conflicts_with",
2572 }
2573 .to_string()
2574}
2575
2576fn lineage_link_from_proto(value: ProtoLineageLink) -> Result<LineageLink, Status> {
2577 Ok(LineageLink {
2578 record_id: value.record_id,
2579 relation: lineage_relation_from_proto(&value.relation)?,
2580 confidence: value.confidence,
2581 })
2582}
2583
2584fn lineage_link_to_proto(value: LineageLink) -> ProtoLineageLink {
2585 ProtoLineageLink {
2586 record_id: value.record_id,
2587 relation: lineage_relation_to_proto(value.relation),
2588 confidence: value.confidence,
2589 }
2590}
2591
2592fn conflict_review_state_from_proto(value: &str) -> Result<ConflictReviewState, Status> {
2593 match value {
2594 "" | "none" => Ok(ConflictReviewState::None),
2595 "potential_conflict" => Ok(ConflictReviewState::PotentialConflict),
2596 "under_review" => Ok(ConflictReviewState::UnderReview),
2597 "resolved" => Ok(ConflictReviewState::Resolved),
2598 "dismissed" => Ok(ConflictReviewState::Dismissed),
2599 other => Err(invalid_argument(format!(
2600 "unknown conflict review state: {other}"
2601 ))),
2602 }
2603}
2604
2605fn conflict_review_state_to_proto(value: ConflictReviewState) -> String {
2606 match value {
2607 ConflictReviewState::None => "none",
2608 ConflictReviewState::PotentialConflict => "potential_conflict",
2609 ConflictReviewState::UnderReview => "under_review",
2610 ConflictReviewState::Resolved => "resolved",
2611 ConflictReviewState::Dismissed => "dismissed",
2612 }
2613 .to_string()
2614}
2615
2616fn conflict_resolution_kind_from_proto(value: &str) -> Result<ConflictResolutionKind, Status> {
2617 match value {
2618 "" | "none" => Ok(ConflictResolutionKind::None),
2619 "accepted" => Ok(ConflictResolutionKind::Accepted),
2620 "rejected" => Ok(ConflictResolutionKind::Rejected),
2621 "superseded" => Ok(ConflictResolutionKind::Superseded),
2622 "merged" => Ok(ConflictResolutionKind::Merged),
2623 other => Err(invalid_argument(format!(
2624 "unknown conflict resolution kind: {other}"
2625 ))),
2626 }
2627}
2628
2629fn conflict_resolution_kind_to_proto(value: ConflictResolutionKind) -> String {
2630 match value {
2631 ConflictResolutionKind::None => "none",
2632 ConflictResolutionKind::Accepted => "accepted",
2633 ConflictResolutionKind::Rejected => "rejected",
2634 ConflictResolutionKind::Superseded => "superseded",
2635 ConflictResolutionKind::Merged => "merged",
2636 }
2637 .to_string()
2638}
2639
2640fn conflict_annotation_from_proto(
2641 value: ProtoConflictAnnotation,
2642) -> Result<ConflictAnnotation, Status> {
2643 Ok(ConflictAnnotation {
2644 state: conflict_review_state_from_proto(&value.state)?,
2645 conflicting_record_ids: value.conflicting_record_ids,
2646 drift_score: value.drift_score,
2647 resolution: conflict_resolution_kind_from_proto(&value.resolution)?,
2648 resolved_by: value.resolved_by,
2649 resolved_at_unix_ms: value.resolved_at_unix_ms,
2650 note: value.note,
2651 })
2652}
2653
2654fn conflict_annotation_to_proto(value: ConflictAnnotation) -> ProtoConflictAnnotation {
2655 ProtoConflictAnnotation {
2656 state: conflict_review_state_to_proto(value.state),
2657 conflicting_record_ids: value.conflicting_record_ids,
2658 drift_score: value.drift_score,
2659 resolution: conflict_resolution_kind_to_proto(value.resolution),
2660 resolved_by: value.resolved_by,
2661 resolved_at_unix_ms: value.resolved_at_unix_ms,
2662 note: value.note,
2663 }
2664}
2665
2666fn historical_mode_from_proto(value: &str) -> Result<RecallHistoricalMode, Status> {
2667 match value {
2668 "" | "current_only" => Ok(RecallHistoricalMode::CurrentOnly),
2669 "include_historical" => Ok(RecallHistoricalMode::IncludeHistorical),
2670 "historical_only" => Ok(RecallHistoricalMode::HistoricalOnly),
2671 other => Err(invalid_argument(format!(
2672 "unknown historical mode: {other}"
2673 ))),
2674 }
2675}
2676
2677fn episode_context_from_proto(value: ProtoEpisodeContext) -> Result<EpisodeContext, Status> {
2678 Ok(EpisodeContext {
2679 schema_version: if value.schema_version == 0 {
2680 EPISODE_SCHEMA_VERSION
2681 } else {
2682 value.schema_version
2683 },
2684 episode_id: value.episode_id,
2685 summary: value.summary,
2686 continuity_state: continuity_state_from_proto(&value.continuity_state)?,
2687 actor_ids: value.actor_ids,
2688 goal: value.goal,
2689 outcome: value.outcome,
2690 started_at_unix_ms: value.started_at_unix_ms,
2691 ended_at_unix_ms: value.ended_at_unix_ms,
2692 last_active_unix_ms: value.last_active_unix_ms,
2693 recurrence_key: value.recurrence_key,
2694 recurrence_interval_ms: value.recurrence_interval_ms,
2695 boundary_label: value.boundary_label,
2696 previous_record_id: value.previous_record_id,
2697 next_record_id: value.next_record_id,
2698 causal_record_ids: value.causal_record_ids,
2699 related_record_ids: value.related_record_ids,
2700 linked_artifact_uris: value.linked_artifact_uris,
2701 salience: value
2702 .salience
2703 .map_or_else(EpisodeSalience::default, |salience| EpisodeSalience {
2704 reuse_count: salience.reuse_count,
2705 novelty_score: salience.novelty_score,
2706 goal_relevance: salience.goal_relevance,
2707 unresolved_weight: salience.unresolved_weight,
2708 }),
2709 affective: value
2710 .affective
2711 .map(affective_annotation_from_proto)
2712 .transpose()?,
2713 })
2714}
2715
2716fn episode_context_to_proto(value: EpisodeContext) -> ProtoEpisodeContext {
2717 ProtoEpisodeContext {
2718 schema_version: value.schema_version,
2719 episode_id: value.episode_id,
2720 summary: value.summary,
2721 continuity_state: continuity_state_to_proto(value.continuity_state),
2722 actor_ids: value.actor_ids,
2723 goal: value.goal,
2724 outcome: value.outcome,
2725 started_at_unix_ms: value.started_at_unix_ms,
2726 ended_at_unix_ms: value.ended_at_unix_ms,
2727 last_active_unix_ms: value.last_active_unix_ms,
2728 recurrence_key: value.recurrence_key,
2729 recurrence_interval_ms: value.recurrence_interval_ms,
2730 boundary_label: value.boundary_label,
2731 previous_record_id: value.previous_record_id,
2732 next_record_id: value.next_record_id,
2733 causal_record_ids: value.causal_record_ids,
2734 related_record_ids: value.related_record_ids,
2735 linked_artifact_uris: value.linked_artifact_uris,
2736 salience: Some(ProtoEpisodeSalience {
2737 reuse_count: value.salience.reuse_count,
2738 novelty_score: value.salience.novelty_score,
2739 goal_relevance: value.salience.goal_relevance,
2740 unresolved_weight: value.salience.unresolved_weight,
2741 }),
2742 affective: value.affective.map(affective_annotation_to_proto),
2743 }
2744}
2745
2746fn record_from_proto(record: ProtoMemoryRecord) -> Result<MemoryRecord, Status> {
2747 Ok(MemoryRecord {
2748 id: record.id,
2749 scope: scope_from_proto(
2750 record
2751 .scope
2752 .ok_or_else(|| invalid_argument("memory record scope is required"))?,
2753 )?,
2754 kind: record_kind_from_proto(&record.kind)?,
2755 content: record.content,
2756 summary: record.summary,
2757 source_id: record.source_id,
2758 metadata: record.metadata.into_iter().collect(),
2759 quality_state: quality_state_from_proto(&record.quality_state)?,
2760 created_at_unix_ms: record.created_at_unix_ms,
2761 updated_at_unix_ms: record.updated_at_unix_ms,
2762 expires_at_unix_ms: record.expires_at_unix_ms,
2763 importance_score: record.importance_score,
2764 artifact: record.artifact.map(artifact_from_proto),
2765 episode: record.episode.map(episode_context_from_proto).transpose()?,
2766 historical_state: historical_state_from_proto(
2767 record.historical_state.as_deref().unwrap_or(""),
2768 )?,
2769 lineage: record
2770 .lineage
2771 .into_iter()
2772 .map(lineage_link_from_proto)
2773 .collect::<Result<Vec<_>, _>>()?,
2774 conflict: record
2775 .conflict
2776 .map(conflict_annotation_from_proto)
2777 .transpose()?,
2778 })
2779}
2780
2781fn record_to_proto(record: MemoryRecord) -> ProtoMemoryRecord {
2782 ProtoMemoryRecord {
2783 id: record.id,
2784 scope: Some(scope_to_proto(record.scope)),
2785 kind: record_kind_to_proto(record.kind),
2786 content: record.content,
2787 summary: record.summary,
2788 metadata: record.metadata.into_iter().collect(),
2789 quality_state: quality_state_to_proto(record.quality_state),
2790 created_at_unix_ms: record.created_at_unix_ms,
2791 updated_at_unix_ms: record.updated_at_unix_ms,
2792 expires_at_unix_ms: record.expires_at_unix_ms,
2793 importance_score: record.importance_score,
2794 source_id: record.source_id,
2795 artifact: record.artifact.map(artifact_to_proto),
2796 episode: record.episode.map(episode_context_to_proto),
2797 historical_state: Some(historical_state_to_proto(record.historical_state)),
2798 lineage: record
2799 .lineage
2800 .into_iter()
2801 .map(lineage_link_to_proto)
2802 .collect(),
2803 conflict: record.conflict.map(conflict_annotation_to_proto),
2804 }
2805}
2806
2807fn recall_explanation_to_proto(value: RecallExplanation) -> ProtoRecallExplanation {
2808 ProtoRecallExplanation {
2809 selected_channels: value.selected_channels,
2810 policy_notes: value.policy_notes,
2811 trace_id: value.trace_id,
2812 planning_trace: value.planning_trace.map(recall_planning_trace_to_proto),
2813 scorer_kind: value
2814 .scorer_kind
2815 .map(recall_scorer_kind_to_proto)
2816 .unwrap_or(ProtoRecallScorerKind::Unspecified as i32),
2817 scoring_profile: value
2818 .scoring_profile
2819 .map(recall_scoring_profile_to_proto)
2820 .unwrap_or(ProtoRecallScoringProfile::Unspecified as i32),
2821 planning_profile: value.planning_profile.map(planning_profile_to_proto),
2822 policy_profile: value
2823 .policy_profile
2824 .map(recall_policy_profile_to_proto)
2825 .unwrap_or(ProtoRecallPolicyProfile::Unspecified as i32),
2826 }
2827}
2828
2829fn recall_scorer_kind_to_proto(value: RecallScorerKind) -> i32 {
2830 match value {
2831 RecallScorerKind::Profile => ProtoRecallScorerKind::Profile as i32,
2832 RecallScorerKind::Curated => ProtoRecallScorerKind::Curated as i32,
2833 }
2834}
2835
2836fn recall_scoring_profile_to_proto(value: RecallScoringProfile) -> i32 {
2837 match value {
2838 RecallScoringProfile::Balanced => ProtoRecallScoringProfile::Balanced as i32,
2839 RecallScoringProfile::LexicalFirst => ProtoRecallScoringProfile::LexicalFirst as i32,
2840 RecallScoringProfile::ImportanceFirst => ProtoRecallScoringProfile::ImportanceFirst as i32,
2841 }
2842}
2843
2844fn recall_policy_profile_to_proto(value: RecallPolicyProfile) -> i32 {
2845 match value {
2846 RecallPolicyProfile::General => ProtoRecallPolicyProfile::General as i32,
2847 RecallPolicyProfile::Support => ProtoRecallPolicyProfile::Support as i32,
2848 RecallPolicyProfile::Research => ProtoRecallPolicyProfile::Research as i32,
2849 RecallPolicyProfile::Assistant => ProtoRecallPolicyProfile::Assistant as i32,
2850 RecallPolicyProfile::AutonomousAgent => ProtoRecallPolicyProfile::AutonomousAgent as i32,
2851 }
2852}
2853
2854fn embedding_provider_kind_to_proto(value: EmbeddingProviderKind) -> i32 {
2855 match value {
2856 EmbeddingProviderKind::Disabled => ProtoEmbeddingProviderKind::Disabled as i32,
2857 EmbeddingProviderKind::DeterministicLocal => {
2858 ProtoEmbeddingProviderKind::DeterministicLocal as i32
2859 }
2860 }
2861}
2862
2863fn engine_tuning_info_to_proto(value: EngineTuningInfo) -> ProtoEngineTuningInfo {
2864 ProtoEngineTuningInfo {
2865 recall_scorer_kind: recall_scorer_kind_to_proto(value.recall_scorer_kind),
2866 recall_scoring_profile: recall_scoring_profile_to_proto(value.recall_scoring_profile),
2867 embedding_provider_kind: embedding_provider_kind_to_proto(value.embedding_provider_kind),
2868 embedding_dimensions: value.embedding_dimensions as u64,
2869 graph_expansion_max_hops: u32::from(value.graph_expansion_max_hops),
2870 compaction_summarize_after_record_count: value.compaction_summarize_after_record_count
2871 as u64,
2872 compaction_cold_archive_after_days: value.compaction_cold_archive_after_days,
2873 compaction_cold_archive_importance_threshold_per_mille: u32::from(
2874 value.compaction_cold_archive_importance_threshold_per_mille,
2875 ),
2876 recall_planning_profile: Some(planning_profile_to_proto(value.recall_planning_profile)),
2877 recall_policy_profile: recall_policy_profile_to_proto(value.recall_policy_profile),
2878 }
2879}
2880
2881fn engine_tuning_info_from_proto(value: ProtoEngineTuningInfo) -> Result<EngineTuningInfo, Status> {
2882 Ok(EngineTuningInfo {
2883 recall_scorer_kind: match ProtoRecallScorerKind::try_from(value.recall_scorer_kind)
2884 .unwrap_or(ProtoRecallScorerKind::Unspecified)
2885 {
2886 ProtoRecallScorerKind::Profile => RecallScorerKind::Profile,
2887 ProtoRecallScorerKind::Curated => RecallScorerKind::Curated,
2888 ProtoRecallScorerKind::Unspecified => {
2889 return Err(invalid_argument("engine recall scorer kind is required"));
2890 }
2891 },
2892 recall_scoring_profile: match ProtoRecallScoringProfile::try_from(
2893 value.recall_scoring_profile,
2894 )
2895 .unwrap_or(ProtoRecallScoringProfile::Unspecified)
2896 {
2897 ProtoRecallScoringProfile::Balanced => RecallScoringProfile::Balanced,
2898 ProtoRecallScoringProfile::LexicalFirst => RecallScoringProfile::LexicalFirst,
2899 ProtoRecallScoringProfile::ImportanceFirst => RecallScoringProfile::ImportanceFirst,
2900 ProtoRecallScoringProfile::Unspecified => {
2901 return Err(invalid_argument(
2902 "engine recall scoring profile is required",
2903 ));
2904 }
2905 },
2906 recall_planning_profile: planning_profile_from_proto(
2907 value.recall_planning_profile.as_deref().unwrap_or(""),
2908 )?,
2909 recall_policy_profile: match ProtoRecallPolicyProfile::try_from(value.recall_policy_profile)
2910 .unwrap_or(ProtoRecallPolicyProfile::Unspecified)
2911 {
2912 ProtoRecallPolicyProfile::General => RecallPolicyProfile::General,
2913 ProtoRecallPolicyProfile::Support => RecallPolicyProfile::Support,
2914 ProtoRecallPolicyProfile::Research => RecallPolicyProfile::Research,
2915 ProtoRecallPolicyProfile::Assistant => RecallPolicyProfile::Assistant,
2916 ProtoRecallPolicyProfile::AutonomousAgent => RecallPolicyProfile::AutonomousAgent,
2917 ProtoRecallPolicyProfile::Unspecified => RecallPolicyProfile::General,
2918 },
2919 embedding_provider_kind: match ProtoEmbeddingProviderKind::try_from(
2920 value.embedding_provider_kind,
2921 )
2922 .unwrap_or(ProtoEmbeddingProviderKind::Unspecified)
2923 {
2924 ProtoEmbeddingProviderKind::Disabled => EmbeddingProviderKind::Disabled,
2925 ProtoEmbeddingProviderKind::DeterministicLocal => {
2926 EmbeddingProviderKind::DeterministicLocal
2927 }
2928 ProtoEmbeddingProviderKind::Unspecified => {
2929 return Err(invalid_argument(
2930 "engine embedding provider kind is required",
2931 ));
2932 }
2933 },
2934 embedding_dimensions: value.embedding_dimensions as usize,
2935 graph_expansion_max_hops: value.graph_expansion_max_hops as u8,
2936 compaction_summarize_after_record_count: value.compaction_summarize_after_record_count
2937 as usize,
2938 compaction_cold_archive_after_days: value.compaction_cold_archive_after_days,
2939 compaction_cold_archive_importance_threshold_per_mille: value
2940 .compaction_cold_archive_importance_threshold_per_mille
2941 as u16,
2942 })
2943}
2944
2945fn recall_planning_trace_to_proto(value: RecallPlanningTrace) -> ProtoRecallPlanningTrace {
2946 ProtoRecallPlanningTrace {
2947 trace_id: value.trace_id,
2948 token_budget_applied: value.token_budget_applied,
2949 candidates: value
2950 .candidates
2951 .into_iter()
2952 .map(recall_trace_candidate_to_proto)
2953 .collect(),
2954 }
2955}
2956
2957fn recall_trace_candidate_to_proto(value: RecallTraceCandidate) -> ProtoRecallTraceCandidate {
2958 ProtoRecallTraceCandidate {
2959 record_id: value.record_id,
2960 kind: record_kind_to_proto(value.kind),
2961 selected: value.selected,
2962 matched_terms: value.matched_terms,
2963 decision_reason: value.decision_reason,
2964 breakdown: Some(recall_score_breakdown_to_proto(value.breakdown)),
2965 selection_rank: value.selection_rank,
2966 selected_channels: value.selected_channels,
2967 filter_reasons: value.filter_reasons,
2968 relation_reasons: value.relation_reasons,
2969 candidate_sources: value
2970 .candidate_sources
2971 .into_iter()
2972 .map(candidate_source_to_proto)
2973 .collect(),
2974 planner_stage: Some(planner_stage_to_proto(value.planner_stage)),
2975 }
2976}
2977
2978fn recall_score_breakdown_to_proto(
2979 value: mnemara_core::RecallScoreBreakdown,
2980) -> ProtoRecallScoreBreakdown {
2981 ProtoRecallScoreBreakdown {
2982 lexical: value.lexical,
2983 semantic: value.semantic,
2984 graph: value.graph,
2985 temporal: value.temporal,
2986 policy: value.policy,
2987 total: value.total,
2988 metadata: value.metadata,
2989 curation: value.curation,
2990 episodic: value.episodic,
2991 salience: value.salience,
2992 }
2993}
2994
2995fn namespace_stats_to_proto(value: NamespaceStats) -> ProtoNamespaceStats {
2996 ProtoNamespaceStats {
2997 tenant_id: value.tenant_id,
2998 namespace: value.namespace,
2999 active_records: value.active_records,
3000 archived_records: value.archived_records,
3001 deleted_records: value.deleted_records,
3002 suppressed_records: value.suppressed_records,
3003 pinned_records: value.pinned_records,
3004 }
3005}
3006
3007fn maintenance_stats_to_proto(value: MaintenanceStats) -> ProtoMaintenanceStats {
3008 ProtoMaintenanceStats {
3009 duplicate_candidate_groups: value.duplicate_candidate_groups,
3010 duplicate_candidate_records: value.duplicate_candidate_records,
3011 tombstoned_records: value.tombstoned_records,
3012 expired_records: value.expired_records,
3013 stale_idempotency_keys: value.stale_idempotency_keys,
3014 historical_records: value.historical_records,
3015 superseded_records: value.superseded_records,
3016 lineage_links: value.lineage_links,
3017 }
3018}
3019
3020fn graph_edge_kind_to_wire(value: GraphInspectionEdgeKind) -> String {
3021 match value {
3022 GraphInspectionEdgeKind::EpisodeMembership => "EpisodeMembership",
3023 GraphInspectionEdgeKind::ChronologyPrevious => "ChronologyPrevious",
3024 GraphInspectionEdgeKind::ChronologyNext => "ChronologyNext",
3025 GraphInspectionEdgeKind::Causal => "Causal",
3026 GraphInspectionEdgeKind::Related => "Related",
3027 GraphInspectionEdgeKind::Lineage => "Lineage",
3028 GraphInspectionEdgeKind::Conflict => "Conflict",
3029 }
3030 .to_string()
3031}
3032
3033fn graph_node_to_proto(value: GraphInspectionNode) -> ProtoGraphInspectionNode {
3034 ProtoGraphInspectionNode {
3035 record_id: value.record_id,
3036 tenant_id: value.tenant_id,
3037 namespace: value.namespace,
3038 actor_id: value.actor_id,
3039 kind: record_kind_to_proto(value.kind),
3040 summary: value.summary,
3041 quality_state: quality_state_to_proto(value.quality_state),
3042 historical_state: historical_state_to_proto(value.historical_state),
3043 episode_id: value.episode_id,
3044 continuity_state: value.continuity_state.map(continuity_state_to_proto),
3045 conflict_state: value.conflict_state.map(conflict_review_state_to_proto),
3046 importance_per_mille: u32::from(value.importance_per_mille),
3047 updated_at_unix_ms: value.updated_at_unix_ms,
3048 }
3049}
3050
3051fn graph_edge_to_proto(value: GraphInspectionEdge) -> ProtoGraphInspectionEdge {
3052 ProtoGraphInspectionEdge {
3053 source_id: value.source_id,
3054 target_id: value.target_id,
3055 kind: graph_edge_kind_to_wire(value.kind),
3056 details: value.details,
3057 }
3058}
3059
3060fn graph_report_to_proto(value: GraphInspectionReport) -> GraphInspectionReply {
3061 GraphInspectionReply {
3062 generated_at_unix_ms: value.generated_at_unix_ms,
3063 total_records_scanned: value.total_records_scanned,
3064 nodes: value.nodes.into_iter().map(graph_node_to_proto).collect(),
3065 edges: value.edges.into_iter().map(graph_edge_to_proto).collect(),
3066 truncated: value.truncated,
3067 }
3068}
3069
3070fn portable_record_to_proto(value: PortableRecord) -> ProtoPortableRecord {
3071 ProtoPortableRecord {
3072 record: Some(record_to_proto(value.record)),
3073 idempotency_key: value.idempotency_key,
3074 }
3075}
3076
3077fn snapshot_manifest_to_proto(value: SnapshotManifest) -> SnapshotReply {
3078 SnapshotReply {
3079 snapshot_id: value.snapshot_id,
3080 created_at_unix_ms: value.created_at_unix_ms,
3081 namespaces: value.namespaces,
3082 record_count: value.record_count,
3083 storage_bytes: value.storage_bytes,
3084 engine: Some(engine_tuning_info_to_proto(value.engine)),
3085 }
3086}
3087
3088fn portable_package_to_proto(value: PortableStorePackage) -> ProtoExportReply {
3089 ProtoExportReply {
3090 package_version: value.package_version,
3091 exported_at_unix_ms: value.exported_at_unix_ms,
3092 manifest: Some(snapshot_manifest_to_proto(value.manifest)),
3093 records: value
3094 .records
3095 .into_iter()
3096 .map(portable_record_to_proto)
3097 .collect(),
3098 }
3099}
3100
3101fn import_mode_from_proto(value: i32) -> Result<ImportMode, Status> {
3102 match ProtoImportMode::try_from(value).unwrap_or(ProtoImportMode::Unspecified) {
3103 ProtoImportMode::Validate => Ok(ImportMode::Validate),
3104 ProtoImportMode::Merge => Ok(ImportMode::Merge),
3105 ProtoImportMode::Replace => Ok(ImportMode::Replace),
3106 ProtoImportMode::Unspecified => Err(invalid_argument("import mode is required")),
3107 }
3108}
3109
3110fn integrity_report_to_proto(value: IntegrityCheckReport) -> ProtoIntegrityCheckReply {
3111 ProtoIntegrityCheckReply {
3112 generated_at_unix_ms: value.generated_at_unix_ms,
3113 healthy: value.healthy,
3114 scanned_records: value.scanned_records,
3115 scanned_idempotency_keys: value.scanned_idempotency_keys,
3116 stale_idempotency_keys: value.stale_idempotency_keys,
3117 missing_idempotency_keys: value.missing_idempotency_keys,
3118 duplicate_active_records: value.duplicate_active_records,
3119 }
3120}
3121
3122fn repair_report_to_proto(value: RepairReport) -> ProtoRepairReply {
3123 ProtoRepairReply {
3124 dry_run: value.dry_run,
3125 scanned_records: value.scanned_records,
3126 scanned_idempotency_keys: value.scanned_idempotency_keys,
3127 removed_stale_idempotency_keys: value.removed_stale_idempotency_keys,
3128 rebuilt_missing_idempotency_keys: value.rebuilt_missing_idempotency_keys,
3129 healthy_after: value.healthy_after,
3130 }
3131}
3132
3133fn compaction_report_to_proto(value: CompactionReport) -> CompactReply {
3134 CompactReply {
3135 deduplicated_records: value.deduplicated_records,
3136 archived_records: value.archived_records,
3137 summarized_clusters: value.summarized_clusters,
3138 pruned_graph_edges: value.pruned_graph_edges,
3139 superseded_records: value.superseded_records,
3140 lineage_links_created: value.lineage_links_created,
3141 dry_run: value.dry_run,
3142 }
3143}
3144
3145fn maintenance_report_to_proto(value: MaintenanceRunReport) -> MaintenanceRunReply {
3146 MaintenanceRunReply {
3147 dry_run: value.dry_run,
3148 integrity_before: value.integrity_before.map(integrity_report_to_proto),
3149 repair: value.repair.map(repair_report_to_proto),
3150 compaction: value.compaction.map(compaction_report_to_proto),
3151 integrity_after: value.integrity_after.map(integrity_report_to_proto),
3152 }
3153}
3154
3155fn snapshot_ship_report_to_proto(value: SnapshotShipReport) -> SnapshotShipReply {
3156 SnapshotShipReply {
3157 target_url: value.target_url,
3158 exported_records: value.exported_records,
3159 imported_records: value.imported_records,
3160 skipped_records: value.skipped_records,
3161 dry_run: value.dry_run,
3162 compatible_package: value.compatible_package,
3163 remote_status: u32::from(value.remote_status),
3164 remote_snapshot_id: value.remote_snapshot_id,
3165 }
3166}
3167
3168fn portable_record_from_proto(value: ProtoPortableRecord) -> Result<PortableRecord, Status> {
3169 Ok(PortableRecord {
3170 record: record_from_proto(
3171 value
3172 .record
3173 .ok_or_else(|| invalid_argument("portable record payload is required"))?,
3174 )?,
3175 idempotency_key: value.idempotency_key,
3176 })
3177}
3178
3179fn snapshot_manifest_from_proto(value: SnapshotReply) -> Result<SnapshotManifest, Status> {
3180 Ok(SnapshotManifest {
3181 snapshot_id: value.snapshot_id,
3182 created_at_unix_ms: value.created_at_unix_ms,
3183 namespaces: value.namespaces,
3184 record_count: value.record_count,
3185 storage_bytes: value.storage_bytes,
3186 engine: value
3187 .engine
3188 .map(engine_tuning_info_from_proto)
3189 .transpose()?
3190 .ok_or_else(|| invalid_argument("snapshot engine tuning info is required"))?,
3191 })
3192}
3193
3194fn portable_package_from_proto(value: ProtoExportReply) -> Result<PortableStorePackage, Status> {
3195 Ok(PortableStorePackage {
3196 package_version: value.package_version,
3197 exported_at_unix_ms: value.exported_at_unix_ms,
3198 manifest: snapshot_manifest_from_proto(
3199 value
3200 .manifest
3201 .ok_or_else(|| invalid_argument("portable export manifest is required"))?,
3202 )?,
3203 records: value
3204 .records
3205 .into_iter()
3206 .map(portable_record_from_proto)
3207 .collect::<Result<Vec<_>, _>>()?,
3208 })
3209}
3210
3211fn trace_operation_kind_to_proto(value: TraceOperationKind) -> i32 {
3212 match value {
3213 TraceOperationKind::Upsert => ProtoTraceOperationKind::Upsert as i32,
3214 TraceOperationKind::BatchUpsert => ProtoTraceOperationKind::BatchUpsert as i32,
3215 TraceOperationKind::Recall => ProtoTraceOperationKind::Recall as i32,
3216 TraceOperationKind::Snapshot => ProtoTraceOperationKind::Snapshot as i32,
3217 TraceOperationKind::Stats => ProtoTraceOperationKind::Stats as i32,
3218 TraceOperationKind::IntegrityCheck => ProtoTraceOperationKind::IntegrityCheck as i32,
3219 TraceOperationKind::Repair => ProtoTraceOperationKind::Repair as i32,
3220 TraceOperationKind::Compact => ProtoTraceOperationKind::Compact as i32,
3221 TraceOperationKind::Delete => ProtoTraceOperationKind::Delete as i32,
3222 TraceOperationKind::Archive => ProtoTraceOperationKind::Archive as i32,
3223 TraceOperationKind::Suppress => ProtoTraceOperationKind::Suppress as i32,
3224 TraceOperationKind::Recover => ProtoTraceOperationKind::Recover as i32,
3225 TraceOperationKind::Export => ProtoTraceOperationKind::Export as i32,
3226 TraceOperationKind::Import => ProtoTraceOperationKind::Import as i32,
3227 TraceOperationKind::MaintenanceRun => ProtoTraceOperationKind::MaintenanceRun as i32,
3228 TraceOperationKind::SnapshotShip => ProtoTraceOperationKind::SnapshotShip as i32,
3229 }
3230}
3231
3232fn trace_operation_kind_from_proto(value: ProtoTraceOperationKind) -> Option<TraceOperationKind> {
3233 match value {
3234 ProtoTraceOperationKind::Upsert => Some(TraceOperationKind::Upsert),
3235 ProtoTraceOperationKind::BatchUpsert => Some(TraceOperationKind::BatchUpsert),
3236 ProtoTraceOperationKind::Recall => Some(TraceOperationKind::Recall),
3237 ProtoTraceOperationKind::Snapshot => Some(TraceOperationKind::Snapshot),
3238 ProtoTraceOperationKind::Stats => Some(TraceOperationKind::Stats),
3239 ProtoTraceOperationKind::IntegrityCheck => Some(TraceOperationKind::IntegrityCheck),
3240 ProtoTraceOperationKind::Repair => Some(TraceOperationKind::Repair),
3241 ProtoTraceOperationKind::Compact => Some(TraceOperationKind::Compact),
3242 ProtoTraceOperationKind::Delete => Some(TraceOperationKind::Delete),
3243 ProtoTraceOperationKind::Archive => Some(TraceOperationKind::Archive),
3244 ProtoTraceOperationKind::Suppress => Some(TraceOperationKind::Suppress),
3245 ProtoTraceOperationKind::Recover => Some(TraceOperationKind::Recover),
3246 ProtoTraceOperationKind::Export => Some(TraceOperationKind::Export),
3247 ProtoTraceOperationKind::Import => Some(TraceOperationKind::Import),
3248 ProtoTraceOperationKind::MaintenanceRun => Some(TraceOperationKind::MaintenanceRun),
3249 ProtoTraceOperationKind::SnapshotShip => Some(TraceOperationKind::SnapshotShip),
3250 ProtoTraceOperationKind::Unspecified => None,
3251 }
3252}
3253
3254fn trace_status_to_proto(value: TraceStatus) -> i32 {
3255 match value {
3256 TraceStatus::Ok => ProtoTraceStatus::Ok as i32,
3257 TraceStatus::Rejected => ProtoTraceStatus::Rejected as i32,
3258 TraceStatus::Error => ProtoTraceStatus::Error as i32,
3259 }
3260}
3261
3262fn trace_status_from_proto(value: ProtoTraceStatus) -> Option<TraceStatus> {
3263 match value {
3264 ProtoTraceStatus::Ok => Some(TraceStatus::Ok),
3265 ProtoTraceStatus::Rejected => Some(TraceStatus::Rejected),
3266 ProtoTraceStatus::Error => Some(TraceStatus::Error),
3267 ProtoTraceStatus::Unspecified => None,
3268 }
3269}
3270
3271fn operation_trace_to_proto(value: OperationTrace) -> ProtoOperationTrace {
3272 ProtoOperationTrace {
3273 trace_id: value.trace_id,
3274 correlation_id: value.correlation_id,
3275 operation: trace_operation_kind_to_proto(value.operation),
3276 transport: value.transport,
3277 backend: value.backend,
3278 admission_class: value.admission_class,
3279 tenant_id: value.tenant_id,
3280 namespace: value.namespace,
3281 principal: value.principal,
3282 store_span_id: value.store_span_id,
3283 planning_trace_id: value.planning_trace_id,
3284 started_at_unix_ms: value.started_at_unix_ms,
3285 completed_at_unix_ms: value.completed_at_unix_ms,
3286 latency_ms: value.latency_ms,
3287 status: trace_status_to_proto(value.status),
3288 status_message: value.status_message,
3289 summary: Some(ProtoOperationTraceSummary {
3290 record_id: value.summary.record_id,
3291 request_count: value.summary.request_count,
3292 query_text: value.summary.query_text,
3293 max_items: value.summary.max_items,
3294 token_budget: value.summary.token_budget,
3295 dry_run: value.summary.dry_run,
3296 }),
3297 recall_explanation: value.recall_explanation.map(recall_explanation_to_proto),
3298 }
3299}
3300
3301fn kinds_from_proto(values: Vec<String>) -> Result<Vec<MemoryRecordKind>, Status> {
3302 values
3303 .into_iter()
3304 .map(|value| record_kind_from_proto(&value))
3305 .collect()
3306}
3307
3308fn trust_levels_from_proto(values: Vec<String>) -> Result<Vec<MemoryTrustLevel>, Status> {
3309 values
3310 .into_iter()
3311 .map(|value| trust_level_from_proto(&value))
3312 .collect()
3313}
3314
3315fn quality_states_from_proto(values: Vec<String>) -> Result<Vec<MemoryQualityState>, Status> {
3316 values
3317 .into_iter()
3318 .map(|value| quality_state_from_proto(&value))
3319 .collect()
3320}
3321
3322fn conflict_states_from_proto(values: Vec<String>) -> Result<Vec<ConflictReviewState>, Status> {
3323 values
3324 .into_iter()
3325 .map(|value| conflict_review_state_from_proto(&value))
3326 .collect()
3327}
3328
3329fn resolution_kinds_from_proto(values: Vec<String>) -> Result<Vec<ConflictResolutionKind>, Status> {
3330 values
3331 .into_iter()
3332 .map(|value| conflict_resolution_kind_from_proto(&value))
3333 .collect()
3334}
3335
3336fn recall_filters_from_proto(filters: Option<ProtoRecallFilters>) -> Result<RecallFilters, Status> {
3337 let Some(filters) = filters else {
3338 return Ok(RecallFilters::default());
3339 };
3340
3341 Ok(RecallFilters {
3342 kinds: kinds_from_proto(filters.kinds)?,
3343 required_labels: filters.required_labels,
3344 source: filters.source,
3345 from_unix_ms: filters.from_unix_ms,
3346 to_unix_ms: filters.to_unix_ms,
3347 min_importance_score: filters.min_importance_score,
3348 trust_levels: trust_levels_from_proto(filters.trust_levels)?,
3349 states: quality_states_from_proto(filters.states)?,
3350 include_archived: filters.include_archived,
3351 episode_id: filters.episode_id,
3352 continuity_states: filters
3353 .continuity_states
3354 .into_iter()
3355 .map(|value| continuity_state_from_proto(&value))
3356 .collect::<Result<Vec<_>, _>>()?,
3357 unresolved_only: filters.unresolved_only,
3358 temporal_order: temporal_order_from_proto(filters.temporal_order.as_deref().unwrap_or(""))?,
3359 historical_mode: historical_mode_from_proto(
3360 filters.historical_mode.as_deref().unwrap_or(""),
3361 )?,
3362 lineage_record_id: filters.lineage_record_id,
3363 before_record_id: filters.before_record_id,
3364 after_record_id: filters.after_record_id,
3365 boundary_labels: filters.boundary_labels,
3366 recurrence_key: filters.recurrence_key,
3367 conflict_states: conflict_states_from_proto(filters.conflict_states)?,
3368 resolution_kinds: resolution_kinds_from_proto(filters.resolution_kinds)?,
3369 unresolved_conflicts_only: filters.unresolved_conflicts_only,
3370 })
3371}
3372
3373fn validate_scope_limits(scope: &MemoryScope, limits: &ServerLimits) -> Result<(), Status> {
3374 if scope.labels.len() > limits.max_labels_per_scope {
3375 return Err(invalid_argument(format!(
3376 "scope labels exceed configured max of {}",
3377 limits.max_labels_per_scope
3378 )));
3379 }
3380 Ok(())
3381}
3382
3383fn validate_record_limits(record: &MemoryRecord, limits: &ServerLimits) -> Result<(), Status> {
3384 validate_scope_limits(&record.scope, limits)?;
3385 if record.content.len() > limits.max_record_content_bytes {
3386 return Err(invalid_argument(format!(
3387 "record content exceeds configured max of {} bytes",
3388 limits.max_record_content_bytes
3389 )));
3390 }
3391 if record.summary.as_deref().map(str::len).unwrap_or(0) > limits.max_query_text_bytes {
3392 return Err(invalid_argument(format!(
3393 "record summary exceeds configured max of {} bytes",
3394 limits.max_query_text_bytes
3395 )));
3396 }
3397 Ok(())
3398}
3399
3400fn validate_recall_limits(query: &RecallQuery, limits: &ServerLimits) -> Result<(), Status> {
3401 validate_scope_limits(&query.scope, limits)?;
3402 if query.max_items == 0 {
3403 return Err(invalid_argument("recall max_items must be greater than 0"));
3404 }
3405 if query.max_items > limits.max_recall_items {
3406 return Err(invalid_argument(format!(
3407 "recall max_items exceeds configured max of {}",
3408 limits.max_recall_items
3409 )));
3410 }
3411 if query.query_text.len() > limits.max_query_text_bytes {
3412 return Err(invalid_argument(format!(
3413 "query text exceeds configured max of {} bytes",
3414 limits.max_query_text_bytes
3415 )));
3416 }
3417 Ok(())
3418}
3419
3420fn map_store_error(error: mnemara_core::Error) -> Status {
3421 match error {
3422 mnemara_core::Error::InvalidConfig(message)
3423 | mnemara_core::Error::InvalidRequest(message) => invalid_argument(message),
3424 mnemara_core::Error::Conflict(message) => Status::already_exists(message),
3425 mnemara_core::Error::Unsupported(message) => Status::unimplemented(message),
3426 mnemara_core::Error::Backend(message) => internal_status(message),
3427 }
3428}
3429
3430fn map_http_store_error(error: mnemara_core::Error) -> (StatusCode, Json<HttpErrorBody>) {
3431 let status = match error {
3432 mnemara_core::Error::InvalidConfig(_) | mnemara_core::Error::InvalidRequest(_) => {
3433 StatusCode::BAD_REQUEST
3434 }
3435 mnemara_core::Error::Conflict(_) => StatusCode::CONFLICT,
3436 mnemara_core::Error::Unsupported(_) => StatusCode::NOT_IMPLEMENTED,
3437 mnemara_core::Error::Backend(_) => StatusCode::INTERNAL_SERVER_ERROR,
3438 };
3439 (
3440 status,
3441 Json(HttpErrorBody {
3442 error: error.to_string(),
3443 }),
3444 )
3445}
3446
3447fn map_http_validation_error(status: Status) -> (StatusCode, Json<HttpErrorBody>) {
3448 (
3449 StatusCode::BAD_REQUEST,
3450 Json(HttpErrorBody {
3451 error: status.message().to_string(),
3452 }),
3453 )
3454}
3455
3456fn record_grpc_result<T>(
3457 metrics: &MethodMetrics,
3458 result: Result<Response<T>, Status>,
3459) -> Result<Response<T>, Status> {
3460 match result {
3461 Ok(response) => {
3462 metrics.record_status(&Status::ok(""));
3463 Ok(response)
3464 }
3465 Err(status) => {
3466 metrics.record_status(&status);
3467 Err(status)
3468 }
3469 }
3470}
3471
3472#[tonic::async_trait]
3473impl<S> MemoryService for GrpcMemoryService<S>
3474where
3475 S: MemoryStore + 'static,
3476{
3477 async fn upsert_memory_record(
3478 &self,
3479 request: Request<ProtoUpsertMemoryRecordRequest>,
3480 ) -> Result<Response<UpsertMemoryRecordReply>, Status> {
3481 self.runtime.metrics().grpc_upsert.record_started();
3482 validate_grpc_auth(
3483 &request,
3484 self.runtime.auth().as_ref(),
3485 AuthPermission::Write,
3486 )?;
3487 let principal = grpc_principal(&request);
3488 let started_at_unix_ms = now_unix_ms();
3489 let correlation_id = self.runtime.traces().next_id("corr");
3490 let result = async {
3491 let request = request.into_inner();
3492 let record = record_from_proto(
3493 request
3494 .record
3495 .ok_or_else(|| invalid_argument("memory record is required"))?,
3496 )?;
3497 let tenant_id = record.scope.tenant_id.clone();
3498 let namespace = record.scope.namespace.clone();
3499 validate_record_limits(&record, self.runtime.limits().as_ref())?;
3500 let _permit = self
3501 .runtime
3502 .admission()
3503 .acquire(AdmissionClass::Write, Some(&tenant_id))
3504 .await
3505 .map_err(|error| {
3506 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
3507 })?;
3508 let receipt = self
3509 .store
3510 .upsert(mnemara_core::UpsertRequest {
3511 record,
3512 idempotency_key: request.idempotency_key,
3513 })
3514 .await
3515 .map_err(map_store_error)?;
3516 record_trace(
3517 &self.runtime,
3518 TraceOperationKind::Upsert,
3519 "grpc",
3520 Some(tenant_id),
3521 Some(namespace),
3522 principal,
3523 started_at_unix_ms,
3524 TraceStatus::Ok,
3525 None,
3526 OperationTraceSummary {
3527 record_id: Some(receipt.record_id.clone()),
3528 ..OperationTraceSummary::default()
3529 },
3530 None,
3531 correlation_id,
3532 );
3533
3534 Ok(Response::new(UpsertMemoryRecordReply {
3535 record_id: receipt.record_id,
3536 deduplicated: receipt.deduplicated,
3537 summary_refreshed: receipt.summary_refreshed,
3538 }))
3539 }
3540 .await;
3541 record_grpc_result(&self.runtime.metrics().grpc_upsert, result)
3542 }
3543
3544 async fn batch_upsert_memory_records(
3545 &self,
3546 request: Request<BatchUpsertMemoryRecordsRequest>,
3547 ) -> Result<Response<BatchUpsertMemoryRecordsReply>, Status> {
3548 self.runtime.metrics().grpc_batch_upsert.record_started();
3549 validate_grpc_auth(
3550 &request,
3551 self.runtime.auth().as_ref(),
3552 AuthPermission::Write,
3553 )?;
3554 let principal = grpc_principal(&request);
3555 let started_at_unix_ms = now_unix_ms();
3556 let correlation_id = self.runtime.traces().next_id("corr");
3557 let result = async {
3558 let request = request.into_inner();
3559 if request.requests.len() > self.runtime.limits().max_batch_upsert_requests {
3560 return Err(invalid_argument(format!(
3561 "batch upsert request count exceeds configured max of {}",
3562 self.runtime.limits().max_batch_upsert_requests
3563 )));
3564 }
3565 let requests = request
3566 .requests
3567 .into_iter()
3568 .map(|request| {
3569 let record = record_from_proto(
3570 request
3571 .record
3572 .ok_or_else(|| invalid_argument("memory record is required"))?,
3573 )?;
3574 validate_record_limits(&record, self.runtime.limits().as_ref())?;
3575 Ok(mnemara_core::UpsertRequest {
3576 record,
3577 idempotency_key: request.idempotency_key,
3578 })
3579 })
3580 .collect::<Result<Vec<_>, Status>>()?;
3581 let tenant_id = requests
3582 .first()
3583 .map(|item| item.record.scope.tenant_id.clone());
3584 let namespace = requests
3585 .first()
3586 .map(|item| item.record.scope.namespace.clone());
3587 let _permit = self
3588 .runtime
3589 .admission()
3590 .acquire(AdmissionClass::Write, tenant_id.as_deref())
3591 .await
3592 .map_err(|error| {
3593 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
3594 })?;
3595
3596 let receipts: Vec<UpsertMemoryRecordReply> = self
3597 .store
3598 .batch_upsert(BatchUpsertRequest { requests })
3599 .await
3600 .map_err(map_store_error)?
3601 .into_iter()
3602 .map(|receipt| UpsertMemoryRecordReply {
3603 record_id: receipt.record_id,
3604 deduplicated: receipt.deduplicated,
3605 summary_refreshed: receipt.summary_refreshed,
3606 })
3607 .collect();
3608 record_trace(
3609 &self.runtime,
3610 TraceOperationKind::BatchUpsert,
3611 "grpc",
3612 tenant_id,
3613 namespace,
3614 principal,
3615 started_at_unix_ms,
3616 TraceStatus::Ok,
3617 None,
3618 OperationTraceSummary {
3619 request_count: Some(receipts.len() as u32),
3620 ..OperationTraceSummary::default()
3621 },
3622 None,
3623 correlation_id,
3624 );
3625
3626 Ok(Response::new(BatchUpsertMemoryRecordsReply { receipts }))
3627 }
3628 .await;
3629 record_grpc_result(&self.runtime.metrics().grpc_batch_upsert, result)
3630 }
3631
3632 async fn recall(
3633 &self,
3634 request: Request<ProtoRecallRequest>,
3635 ) -> Result<Response<RecallReply>, Status> {
3636 self.runtime.metrics().grpc_recall.record_started();
3637 validate_grpc_auth(&request, self.runtime.auth().as_ref(), AuthPermission::Read)?;
3638 let principal = grpc_principal(&request);
3639 let started_at_unix_ms = now_unix_ms();
3640 let correlation_id = self.runtime.traces().next_id("corr");
3641 let result = async {
3642 let request = request.into_inner();
3643 let query = RecallQuery {
3644 scope: scope_from_proto(
3645 request
3646 .scope
3647 .ok_or_else(|| invalid_argument("recall scope is required"))?,
3648 )?,
3649 query_text: request.query_text,
3650 max_items: request.max_items as usize,
3651 token_budget: request.token_budget.map(|value| value as usize),
3652 filters: recall_filters_from_proto(request.filters)?,
3653 include_explanation: request.include_explanation,
3654 };
3655 validate_recall_limits(&query, self.runtime.limits().as_ref())?;
3656 let tenant_id = query.scope.tenant_id.clone();
3657 let namespace = query.scope.namespace.clone();
3658 let query_text = query.query_text.clone();
3659 let max_items = query.max_items as u32;
3660 let token_budget = query.token_budget.map(|value| value as u32);
3661 let _permit = self
3662 .runtime
3663 .admission()
3664 .acquire(AdmissionClass::Read, Some(&tenant_id))
3665 .await
3666 .map_err(|error| {
3667 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
3668 })?;
3669 let mut result = self.store.recall(query).await.map_err(map_store_error)?;
3670 attach_correlation_id(&mut result, &correlation_id);
3671 let recall_explanation = result.explanation.clone();
3672
3673 let hits = result
3674 .hits
3675 .into_iter()
3676 .map(|hit| ProtoRecallHit {
3677 record: Some(record_to_proto(hit.record)),
3678 breakdown: Some(recall_score_breakdown_to_proto(hit.breakdown)),
3679 selected_channels: hit
3680 .explanation
3681 .as_ref()
3682 .map(|value| value.selected_channels.clone())
3683 .unwrap_or_default(),
3684 policy_notes: hit
3685 .explanation
3686 .as_ref()
3687 .map(|value| value.policy_notes.clone())
3688 .unwrap_or_default(),
3689 explanation: hit.explanation.map(recall_explanation_to_proto),
3690 })
3691 .collect();
3692 record_trace(
3693 &self.runtime,
3694 TraceOperationKind::Recall,
3695 "grpc",
3696 Some(tenant_id),
3697 Some(namespace),
3698 principal,
3699 started_at_unix_ms,
3700 TraceStatus::Ok,
3701 None,
3702 OperationTraceSummary {
3703 query_text: Some(query_text),
3704 max_items: Some(max_items),
3705 token_budget,
3706 ..OperationTraceSummary::default()
3707 },
3708 recall_explanation,
3709 correlation_id,
3710 );
3711
3712 Ok(Response::new(RecallReply {
3713 hits,
3714 total_candidates_examined: result.total_candidates_examined as u32,
3715 explanation: result.explanation.map(recall_explanation_to_proto),
3716 }))
3717 }
3718 .await;
3719 record_grpc_result(&self.runtime.metrics().grpc_recall, result)
3720 }
3721
3722 async fn compact(
3723 &self,
3724 request: Request<ProtoCompactRequest>,
3725 ) -> Result<Response<CompactReply>, Status> {
3726 self.runtime.metrics().grpc_compact.record_started();
3727 validate_grpc_auth(
3728 &request,
3729 self.runtime.auth().as_ref(),
3730 AuthPermission::Admin,
3731 )?;
3732 let principal = grpc_principal(&request);
3733 let started_at_unix_ms = now_unix_ms();
3734 let correlation_id = self.runtime.traces().next_id("corr");
3735 let result = async {
3736 let request = request.into_inner();
3737 let tenant_id = request.tenant_id.clone();
3738 let namespace = request.namespace.clone();
3739 let _permit = self
3740 .runtime
3741 .admission()
3742 .acquire(AdmissionClass::Admin, Some(&tenant_id))
3743 .await
3744 .map_err(|error| {
3745 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
3746 })?;
3747 let report = self
3748 .store
3749 .compact(CompactionRequest {
3750 tenant_id,
3751 namespace: namespace.clone(),
3752 dry_run: request.dry_run,
3753 reason: request.reason,
3754 })
3755 .await
3756 .map_err(map_store_error)?;
3757 record_trace(
3758 &self.runtime,
3759 TraceOperationKind::Compact,
3760 "grpc",
3761 Some(request.tenant_id),
3762 namespace,
3763 principal,
3764 started_at_unix_ms,
3765 TraceStatus::Ok,
3766 None,
3767 OperationTraceSummary {
3768 dry_run: Some(request.dry_run),
3769 ..OperationTraceSummary::default()
3770 },
3771 None,
3772 correlation_id,
3773 );
3774
3775 Ok(Response::new(CompactReply {
3776 deduplicated_records: report.deduplicated_records,
3777 archived_records: report.archived_records,
3778 summarized_clusters: report.summarized_clusters,
3779 pruned_graph_edges: report.pruned_graph_edges,
3780 superseded_records: report.superseded_records,
3781 lineage_links_created: report.lineage_links_created,
3782 dry_run: report.dry_run,
3783 }))
3784 }
3785 .await;
3786 record_grpc_result(&self.runtime.metrics().grpc_compact, result)
3787 }
3788
3789 async fn snapshot(
3790 &self,
3791 request: Request<SnapshotRequest>,
3792 ) -> Result<Response<SnapshotReply>, Status> {
3793 self.runtime.metrics().grpc_snapshot.record_started();
3794 validate_grpc_auth(
3795 &request,
3796 self.runtime.auth().as_ref(),
3797 AuthPermission::Admin,
3798 )?;
3799 let principal = grpc_principal(&request);
3800 let started_at_unix_ms = now_unix_ms();
3801 let correlation_id = self.runtime.traces().next_id("corr");
3802 let result = async {
3803 let _permit = self
3804 .runtime
3805 .admission()
3806 .acquire(AdmissionClass::Admin, None)
3807 .await
3808 .map_err(|error| {
3809 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
3810 })?;
3811 let manifest = self.store.snapshot().await.map_err(map_store_error)?;
3812 record_trace(
3813 &self.runtime,
3814 TraceOperationKind::Snapshot,
3815 "grpc",
3816 None,
3817 None,
3818 principal,
3819 started_at_unix_ms,
3820 TraceStatus::Ok,
3821 None,
3822 OperationTraceSummary::default(),
3823 None,
3824 correlation_id,
3825 );
3826 Ok(Response::new(SnapshotReply {
3827 snapshot_id: manifest.snapshot_id,
3828 created_at_unix_ms: manifest.created_at_unix_ms,
3829 namespaces: manifest.namespaces,
3830 record_count: manifest.record_count,
3831 storage_bytes: manifest.storage_bytes,
3832 engine: Some(engine_tuning_info_to_proto(manifest.engine)),
3833 }))
3834 }
3835 .await;
3836 record_grpc_result(&self.runtime.metrics().grpc_snapshot, result)
3837 }
3838
3839 async fn delete(
3840 &self,
3841 request: Request<ProtoDeleteRequest>,
3842 ) -> Result<Response<DeleteReply>, Status> {
3843 self.runtime.metrics().grpc_delete.record_started();
3844 validate_grpc_auth(
3845 &request,
3846 self.runtime.auth().as_ref(),
3847 AuthPermission::Admin,
3848 )?;
3849 let principal = grpc_principal(&request);
3850 let started_at_unix_ms = now_unix_ms();
3851 let correlation_id = self.runtime.traces().next_id("corr");
3852 let result = async {
3853 let request = request.into_inner();
3854 let tenant_id = request.tenant_id.clone();
3855 let namespace = request.namespace.clone();
3856 let _permit = self
3857 .runtime
3858 .admission()
3859 .acquire(AdmissionClass::Admin, Some(&tenant_id))
3860 .await
3861 .map_err(|error| {
3862 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
3863 })?;
3864 let receipt = self
3865 .store
3866 .delete(DeleteRequest {
3867 tenant_id,
3868 namespace: namespace.clone(),
3869 record_id: request.record_id,
3870 hard_delete: request.hard_delete,
3871 audit_reason: request.audit_reason,
3872 })
3873 .await
3874 .map_err(map_store_error)?;
3875 record_trace(
3876 &self.runtime,
3877 TraceOperationKind::Delete,
3878 "grpc",
3879 Some(request.tenant_id),
3880 Some(namespace),
3881 principal,
3882 started_at_unix_ms,
3883 TraceStatus::Ok,
3884 None,
3885 OperationTraceSummary {
3886 record_id: Some(receipt.record_id.clone()),
3887 ..OperationTraceSummary::default()
3888 },
3889 None,
3890 correlation_id,
3891 );
3892
3893 Ok(Response::new(DeleteReply {
3894 record_id: receipt.record_id,
3895 tombstoned: receipt.tombstoned,
3896 hard_deleted: receipt.hard_deleted,
3897 }))
3898 }
3899 .await;
3900 record_grpc_result(&self.runtime.metrics().grpc_delete, result)
3901 }
3902
3903 async fn archive(
3904 &self,
3905 request: Request<ProtoArchiveRequest>,
3906 ) -> Result<Response<ArchiveReply>, Status> {
3907 self.runtime.metrics().grpc_archive.record_started();
3908 validate_grpc_auth(
3909 &request,
3910 self.runtime.auth().as_ref(),
3911 AuthPermission::Admin,
3912 )?;
3913 let principal = grpc_principal(&request);
3914 let started_at_unix_ms = now_unix_ms();
3915 let correlation_id = self.runtime.traces().next_id("corr");
3916 let result = async {
3917 let request = request.into_inner();
3918 let tenant_id = request.tenant_id.clone();
3919 let namespace = request.namespace.clone();
3920 let _permit = self
3921 .runtime
3922 .admission()
3923 .acquire(AdmissionClass::Admin, Some(&tenant_id))
3924 .await
3925 .map_err(|error| {
3926 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
3927 })?;
3928 let receipt = self
3929 .store
3930 .archive(ArchiveRequest {
3931 tenant_id,
3932 namespace: namespace.clone(),
3933 record_id: request.record_id,
3934 dry_run: request.dry_run,
3935 audit_reason: request.audit_reason,
3936 })
3937 .await
3938 .map_err(map_store_error)?;
3939 record_trace(
3940 &self.runtime,
3941 TraceOperationKind::Archive,
3942 "grpc",
3943 Some(request.tenant_id),
3944 Some(namespace),
3945 principal,
3946 started_at_unix_ms,
3947 TraceStatus::Ok,
3948 None,
3949 OperationTraceSummary {
3950 record_id: Some(receipt.record_id.clone()),
3951 dry_run: Some(receipt.dry_run),
3952 ..OperationTraceSummary::default()
3953 },
3954 None,
3955 correlation_id,
3956 );
3957
3958 Ok(Response::new(ArchiveReply {
3959 record_id: receipt.record_id,
3960 previous_quality_state: quality_state_to_proto(receipt.previous_quality_state),
3961 previous_historical_state: historical_state_to_proto(
3962 receipt.previous_historical_state,
3963 ),
3964 quality_state: quality_state_to_proto(receipt.quality_state),
3965 historical_state: historical_state_to_proto(receipt.historical_state),
3966 changed: receipt.changed,
3967 dry_run: receipt.dry_run,
3968 }))
3969 }
3970 .await;
3971 record_grpc_result(&self.runtime.metrics().grpc_archive, result)
3972 }
3973
3974 async fn suppress(
3975 &self,
3976 request: Request<ProtoSuppressRequest>,
3977 ) -> Result<Response<SuppressReply>, Status> {
3978 self.runtime.metrics().grpc_suppress.record_started();
3979 validate_grpc_auth(
3980 &request,
3981 self.runtime.auth().as_ref(),
3982 AuthPermission::Admin,
3983 )?;
3984 let principal = grpc_principal(&request);
3985 let started_at_unix_ms = now_unix_ms();
3986 let correlation_id = self.runtime.traces().next_id("corr");
3987 let result = async {
3988 let request = request.into_inner();
3989 let tenant_id = request.tenant_id.clone();
3990 let namespace = request.namespace.clone();
3991 let _permit = self
3992 .runtime
3993 .admission()
3994 .acquire(AdmissionClass::Admin, Some(&tenant_id))
3995 .await
3996 .map_err(|error| {
3997 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
3998 })?;
3999 let receipt = self
4000 .store
4001 .suppress(SuppressRequest {
4002 tenant_id,
4003 namespace: namespace.clone(),
4004 record_id: request.record_id,
4005 dry_run: request.dry_run,
4006 audit_reason: request.audit_reason,
4007 })
4008 .await
4009 .map_err(map_store_error)?;
4010 record_trace(
4011 &self.runtime,
4012 TraceOperationKind::Suppress,
4013 "grpc",
4014 Some(request.tenant_id),
4015 Some(namespace),
4016 principal,
4017 started_at_unix_ms,
4018 TraceStatus::Ok,
4019 None,
4020 OperationTraceSummary {
4021 record_id: Some(receipt.record_id.clone()),
4022 dry_run: Some(receipt.dry_run),
4023 ..OperationTraceSummary::default()
4024 },
4025 None,
4026 correlation_id,
4027 );
4028
4029 Ok(Response::new(SuppressReply {
4030 record_id: receipt.record_id,
4031 previous_quality_state: quality_state_to_proto(receipt.previous_quality_state),
4032 previous_historical_state: historical_state_to_proto(
4033 receipt.previous_historical_state,
4034 ),
4035 quality_state: quality_state_to_proto(receipt.quality_state),
4036 historical_state: historical_state_to_proto(receipt.historical_state),
4037 changed: receipt.changed,
4038 dry_run: receipt.dry_run,
4039 }))
4040 }
4041 .await;
4042 record_grpc_result(&self.runtime.metrics().grpc_suppress, result)
4043 }
4044
4045 async fn recover(
4046 &self,
4047 request: Request<ProtoRecoverRequest>,
4048 ) -> Result<Response<RecoverReply>, Status> {
4049 self.runtime.metrics().grpc_recover.record_started();
4050 validate_grpc_auth(
4051 &request,
4052 self.runtime.auth().as_ref(),
4053 AuthPermission::Admin,
4054 )?;
4055 let principal = grpc_principal(&request);
4056 let started_at_unix_ms = now_unix_ms();
4057 let correlation_id = self.runtime.traces().next_id("corr");
4058 let result = async {
4059 let request = request.into_inner();
4060 let tenant_id = request.tenant_id.clone();
4061 let namespace = request.namespace.clone();
4062 let _permit = self
4063 .runtime
4064 .admission()
4065 .acquire(AdmissionClass::Admin, Some(&tenant_id))
4066 .await
4067 .map_err(|error| {
4068 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4069 })?;
4070 let receipt = self
4071 .store
4072 .recover(RecoverRequest {
4073 tenant_id,
4074 namespace: namespace.clone(),
4075 record_id: request.record_id,
4076 dry_run: request.dry_run,
4077 audit_reason: request.audit_reason,
4078 quality_state: quality_state_from_proto(&request.quality_state)?,
4079 historical_state: request
4080 .historical_state
4081 .as_deref()
4082 .map(historical_state_from_proto)
4083 .transpose()?,
4084 })
4085 .await
4086 .map_err(map_store_error)?;
4087 record_trace(
4088 &self.runtime,
4089 TraceOperationKind::Recover,
4090 "grpc",
4091 Some(request.tenant_id),
4092 Some(namespace),
4093 principal,
4094 started_at_unix_ms,
4095 TraceStatus::Ok,
4096 None,
4097 OperationTraceSummary {
4098 record_id: Some(receipt.record_id.clone()),
4099 dry_run: Some(receipt.dry_run),
4100 ..OperationTraceSummary::default()
4101 },
4102 None,
4103 correlation_id,
4104 );
4105
4106 Ok(Response::new(RecoverReply {
4107 record_id: receipt.record_id,
4108 previous_quality_state: quality_state_to_proto(receipt.previous_quality_state),
4109 previous_historical_state: historical_state_to_proto(
4110 receipt.previous_historical_state,
4111 ),
4112 quality_state: quality_state_to_proto(receipt.quality_state),
4113 historical_state: historical_state_to_proto(receipt.historical_state),
4114 changed: receipt.changed,
4115 dry_run: receipt.dry_run,
4116 }))
4117 }
4118 .await;
4119 record_grpc_result(&self.runtime.metrics().grpc_recover, result)
4120 }
4121
4122 async fn stats(
4123 &self,
4124 request: Request<ProtoStoreStatsRequest>,
4125 ) -> Result<Response<ProtoStoreStatsReply>, Status> {
4126 self.runtime.metrics().grpc_stats.record_started();
4127 validate_grpc_auth(
4128 &request,
4129 self.runtime.auth().as_ref(),
4130 AuthPermission::Admin,
4131 )?;
4132 let principal = grpc_principal(&request);
4133 let started_at_unix_ms = now_unix_ms();
4134 let correlation_id = self.runtime.traces().next_id("corr");
4135 let result = async {
4136 let request = request.into_inner();
4137 let tenant_id = request.tenant_id.clone();
4138 let namespace = request.namespace.clone();
4139 let _permit = self
4140 .runtime
4141 .admission()
4142 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4143 .await
4144 .map_err(|error| {
4145 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4146 })?;
4147 let report = self
4148 .store
4149 .stats(StoreStatsRequest {
4150 tenant_id: tenant_id.clone(),
4151 namespace: namespace.clone(),
4152 })
4153 .await
4154 .map_err(map_store_error)?;
4155 record_trace(
4156 &self.runtime,
4157 TraceOperationKind::Stats,
4158 "grpc",
4159 tenant_id,
4160 namespace,
4161 principal,
4162 started_at_unix_ms,
4163 TraceStatus::Ok,
4164 None,
4165 OperationTraceSummary::default(),
4166 None,
4167 correlation_id,
4168 );
4169
4170 Ok(Response::new(ProtoStoreStatsReply {
4171 generated_at_unix_ms: report.generated_at_unix_ms,
4172 total_records: report.total_records,
4173 storage_bytes: report.storage_bytes,
4174 namespaces: report
4175 .namespaces
4176 .into_iter()
4177 .map(namespace_stats_to_proto)
4178 .collect(),
4179 maintenance: Some(maintenance_stats_to_proto(report.maintenance)),
4180 engine: Some(engine_tuning_info_to_proto(report.engine)),
4181 }))
4182 }
4183 .await;
4184 record_grpc_result(&self.runtime.metrics().grpc_stats, result)
4185 }
4186
4187 async fn inspect_graph(
4188 &self,
4189 request: Request<ProtoGraphInspectionRequest>,
4190 ) -> Result<Response<GraphInspectionReply>, Status> {
4191 self.runtime.metrics().grpc_stats.record_started();
4192 validate_grpc_auth(
4193 &request,
4194 self.runtime.auth().as_ref(),
4195 AuthPermission::Admin,
4196 )?;
4197 let principal = grpc_principal(&request);
4198 let started_at_unix_ms = now_unix_ms();
4199 let correlation_id = self.runtime.traces().next_id("corr");
4200 let result = async {
4201 let request = request.into_inner();
4202 let tenant_id = request.tenant_id.clone();
4203 let namespace = request.namespace.clone();
4204 let _permit = self
4205 .runtime
4206 .admission()
4207 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4208 .await
4209 .map_err(|error| {
4210 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4211 })?;
4212 let report = self
4213 .store
4214 .inspect_graph(GraphInspectionRequest {
4215 tenant_id: tenant_id.clone(),
4216 namespace: namespace.clone(),
4217 actor_id: request.actor_id,
4218 conversation_id: request.conversation_id,
4219 session_id: request.session_id,
4220 include_archived: request.include_archived,
4221 include_suppressed: request.include_suppressed,
4222 include_deleted: request.include_deleted,
4223 max_nodes: request.max_nodes.map(|value| value as usize),
4224 })
4225 .await
4226 .map_err(map_store_error)?;
4227 record_trace(
4228 &self.runtime,
4229 TraceOperationKind::Stats,
4230 "grpc",
4231 tenant_id,
4232 namespace,
4233 principal,
4234 started_at_unix_ms,
4235 TraceStatus::Ok,
4236 None,
4237 OperationTraceSummary {
4238 request_count: Some(report.nodes.len() as u32),
4239 max_items: Some(report.edges.len() as u32),
4240 ..Default::default()
4241 },
4242 None,
4243 correlation_id,
4244 );
4245
4246 Ok(Response::new(graph_report_to_proto(report)))
4247 }
4248 .await;
4249 record_grpc_result(&self.runtime.metrics().grpc_stats, result)
4250 }
4251
4252 async fn integrity_check(
4253 &self,
4254 request: Request<ProtoIntegrityCheckRequest>,
4255 ) -> Result<Response<ProtoIntegrityCheckReply>, Status> {
4256 self.runtime.metrics().grpc_integrity.record_started();
4257 validate_grpc_auth(
4258 &request,
4259 self.runtime.auth().as_ref(),
4260 AuthPermission::Admin,
4261 )?;
4262 let principal = grpc_principal(&request);
4263 let started_at_unix_ms = now_unix_ms();
4264 let correlation_id = self.runtime.traces().next_id("corr");
4265 let result = async {
4266 let request = request.into_inner();
4267 let tenant_id = request.tenant_id.clone();
4268 let namespace = request.namespace.clone();
4269 let _permit = self
4270 .runtime
4271 .admission()
4272 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4273 .await
4274 .map_err(|error| {
4275 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4276 })?;
4277 let report = self
4278 .store
4279 .integrity_check(IntegrityCheckRequest {
4280 tenant_id: tenant_id.clone(),
4281 namespace: namespace.clone(),
4282 })
4283 .await
4284 .map_err(map_store_error)?;
4285 record_trace(
4286 &self.runtime,
4287 TraceOperationKind::IntegrityCheck,
4288 "grpc",
4289 tenant_id,
4290 namespace,
4291 principal,
4292 started_at_unix_ms,
4293 TraceStatus::Ok,
4294 None,
4295 OperationTraceSummary::default(),
4296 None,
4297 correlation_id,
4298 );
4299
4300 Ok(Response::new(ProtoIntegrityCheckReply {
4301 generated_at_unix_ms: report.generated_at_unix_ms,
4302 healthy: report.healthy,
4303 scanned_records: report.scanned_records,
4304 scanned_idempotency_keys: report.scanned_idempotency_keys,
4305 stale_idempotency_keys: report.stale_idempotency_keys,
4306 missing_idempotency_keys: report.missing_idempotency_keys,
4307 duplicate_active_records: report.duplicate_active_records,
4308 }))
4309 }
4310 .await;
4311 record_grpc_result(&self.runtime.metrics().grpc_integrity, result)
4312 }
4313
4314 async fn repair(
4315 &self,
4316 request: Request<ProtoRepairRequest>,
4317 ) -> Result<Response<ProtoRepairReply>, Status> {
4318 self.runtime.metrics().grpc_repair.record_started();
4319 validate_grpc_auth(
4320 &request,
4321 self.runtime.auth().as_ref(),
4322 AuthPermission::Admin,
4323 )?;
4324 let principal = grpc_principal(&request);
4325 let started_at_unix_ms = now_unix_ms();
4326 let correlation_id = self.runtime.traces().next_id("corr");
4327 let result = async {
4328 let request = request.into_inner();
4329 let tenant_id = request.tenant_id.clone();
4330 let namespace = request.namespace.clone();
4331 let _permit = self
4332 .runtime
4333 .admission()
4334 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4335 .await
4336 .map_err(|error| {
4337 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4338 })?;
4339 let report = self
4340 .store
4341 .repair(RepairRequest {
4342 tenant_id: tenant_id.clone(),
4343 namespace: namespace.clone(),
4344 dry_run: request.dry_run,
4345 reason: request.reason,
4346 remove_stale_idempotency_keys: request.remove_stale_idempotency_keys,
4347 rebuild_missing_idempotency_keys: request.rebuild_missing_idempotency_keys,
4348 })
4349 .await
4350 .map_err(map_store_error)?;
4351 record_trace(
4352 &self.runtime,
4353 TraceOperationKind::Repair,
4354 "grpc",
4355 tenant_id,
4356 namespace,
4357 principal,
4358 started_at_unix_ms,
4359 TraceStatus::Ok,
4360 None,
4361 OperationTraceSummary {
4362 dry_run: Some(request.dry_run),
4363 ..OperationTraceSummary::default()
4364 },
4365 None,
4366 correlation_id,
4367 );
4368
4369 Ok(Response::new(ProtoRepairReply {
4370 dry_run: report.dry_run,
4371 scanned_records: report.scanned_records,
4372 scanned_idempotency_keys: report.scanned_idempotency_keys,
4373 removed_stale_idempotency_keys: report.removed_stale_idempotency_keys,
4374 rebuilt_missing_idempotency_keys: report.rebuilt_missing_idempotency_keys,
4375 healthy_after: report.healthy_after,
4376 }))
4377 }
4378 .await;
4379 record_grpc_result(&self.runtime.metrics().grpc_repair, result)
4380 }
4381
4382 async fn run_maintenance(
4383 &self,
4384 request: Request<ProtoMaintenanceRunRequest>,
4385 ) -> Result<Response<MaintenanceRunReply>, Status> {
4386 self.runtime.metrics().grpc_maintenance.record_started();
4387 validate_grpc_auth(
4388 &request,
4389 self.runtime.auth().as_ref(),
4390 AuthPermission::Admin,
4391 )?;
4392 let principal = grpc_principal(&request);
4393 let started_at_unix_ms = now_unix_ms();
4394 let correlation_id = self.runtime.traces().next_id("corr");
4395 let result = async {
4396 let request = request.into_inner();
4397 let tenant_id = request.tenant_id.clone();
4398 let namespace = request.namespace.clone();
4399 let run_defaults =
4400 !request.run_integrity_check && !request.run_repair && !request.run_compaction;
4401 let maintenance_request = MaintenanceRunRequest {
4402 tenant_id: tenant_id.clone(),
4403 namespace: namespace.clone(),
4404 dry_run: request.dry_run,
4405 reason: request.reason,
4406 run_integrity_check: request.run_integrity_check || run_defaults,
4407 run_repair: request.run_repair || run_defaults,
4408 run_compaction: request.run_compaction || run_defaults,
4409 remove_stale_idempotency_keys: request.remove_stale_idempotency_keys,
4410 rebuild_missing_idempotency_keys: request.rebuild_missing_idempotency_keys,
4411 };
4412 let _permit = self
4413 .runtime
4414 .admission()
4415 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4416 .await
4417 .map_err(|error| {
4418 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4419 })?;
4420 let report = self
4421 .store
4422 .run_maintenance(maintenance_request)
4423 .await
4424 .map_err(map_store_error)?;
4425 record_trace(
4426 &self.runtime,
4427 TraceOperationKind::MaintenanceRun,
4428 "grpc",
4429 tenant_id,
4430 namespace,
4431 principal,
4432 started_at_unix_ms,
4433 TraceStatus::Ok,
4434 None,
4435 OperationTraceSummary {
4436 dry_run: Some(report.dry_run),
4437 ..OperationTraceSummary::default()
4438 },
4439 None,
4440 correlation_id,
4441 );
4442 Ok(Response::new(maintenance_report_to_proto(report)))
4443 }
4444 .await;
4445 record_grpc_result(&self.runtime.metrics().grpc_maintenance, result)
4446 }
4447
4448 async fn list_traces(
4449 &self,
4450 request: Request<ProtoListTracesRequest>,
4451 ) -> Result<Response<ProtoListTracesReply>, Status> {
4452 validate_grpc_auth(
4453 &request,
4454 self.runtime.auth().as_ref(),
4455 AuthPermission::Admin,
4456 )?;
4457 let request = request.into_inner();
4458 self.runtime
4459 .metrics()
4460 .trace_reads
4461 .fetch_add(1, Ordering::Relaxed);
4462 let traces = self.runtime.traces().list(&TraceListRequest {
4463 tenant_id: request.tenant_id,
4464 namespace: request.namespace,
4465 operation: ProtoTraceOperationKind::try_from(request.operation)
4466 .ok()
4467 .and_then(trace_operation_kind_from_proto),
4468 status: ProtoTraceStatus::try_from(request.status)
4469 .ok()
4470 .and_then(trace_status_from_proto),
4471 before_started_at_unix_ms: request.before_started_at_unix_ms,
4472 limit: request.limit.map(|value| value as usize),
4473 });
4474 Ok(Response::new(ProtoListTracesReply {
4475 traces: traces.into_iter().map(operation_trace_to_proto).collect(),
4476 }))
4477 }
4478
4479 async fn get_trace(
4480 &self,
4481 request: Request<ProtoGetTraceRequest>,
4482 ) -> Result<Response<ProtoOperationTrace>, Status> {
4483 validate_grpc_auth(
4484 &request,
4485 self.runtime.auth().as_ref(),
4486 AuthPermission::Admin,
4487 )?;
4488 let trace_id = request.into_inner().trace_id;
4489 self.runtime
4490 .metrics()
4491 .trace_reads
4492 .fetch_add(1, Ordering::Relaxed);
4493 let trace = self
4494 .runtime
4495 .traces()
4496 .get(&trace_id)
4497 .ok_or_else(|| Status::not_found(format!("trace {trace_id} not found")))?;
4498 Ok(Response::new(operation_trace_to_proto(trace)))
4499 }
4500
4501 async fn export(
4502 &self,
4503 request: Request<ProtoExportRequest>,
4504 ) -> Result<Response<ProtoExportReply>, Status> {
4505 validate_grpc_auth(
4506 &request,
4507 self.runtime.auth().as_ref(),
4508 AuthPermission::Admin,
4509 )?;
4510 let principal = grpc_principal(&request);
4511 let started_at_unix_ms = now_unix_ms();
4512 let correlation_id = self.runtime.traces().next_id("corr");
4513 let request = request.into_inner();
4514 let export_request = ExportRequest {
4515 tenant_id: request.tenant_id,
4516 namespace: request.namespace,
4517 include_archived: request.include_archived,
4518 };
4519 let _permit = self
4520 .runtime
4521 .admission()
4522 .acquire(AdmissionClass::Admin, export_request.tenant_id.as_deref())
4523 .await
4524 .map_err(|error| map_grpc_admission_error(self.runtime.metrics().as_ref(), error))?;
4525 let package = self
4526 .store
4527 .export(export_request.clone())
4528 .await
4529 .map_err(map_store_error)?;
4530 record_trace(
4531 &self.runtime,
4532 TraceOperationKind::Export,
4533 "grpc",
4534 export_request.tenant_id,
4535 export_request.namespace,
4536 principal,
4537 started_at_unix_ms,
4538 TraceStatus::Ok,
4539 None,
4540 OperationTraceSummary::default(),
4541 None,
4542 correlation_id,
4543 );
4544 Ok(Response::new(portable_package_to_proto(package)))
4545 }
4546
4547 async fn import(
4548 &self,
4549 request: Request<ProtoImportRequest>,
4550 ) -> Result<Response<ProtoImportReply>, Status> {
4551 validate_grpc_auth(
4552 &request,
4553 self.runtime.auth().as_ref(),
4554 AuthPermission::Admin,
4555 )?;
4556 let principal = grpc_principal(&request);
4557 let started_at_unix_ms = now_unix_ms();
4558 let correlation_id = self.runtime.traces().next_id("corr");
4559 let request = request.into_inner();
4560 let import_request = ImportRequest {
4561 package: portable_package_from_proto(
4562 request
4563 .package
4564 .ok_or_else(|| invalid_argument("portable package is required"))?,
4565 )?,
4566 mode: import_mode_from_proto(request.mode)?,
4567 dry_run: request.dry_run,
4568 };
4569 let tenant_id = import_request
4570 .package
4571 .records
4572 .first()
4573 .map(|entry| entry.record.scope.tenant_id.clone());
4574 let namespace = import_request
4575 .package
4576 .records
4577 .first()
4578 .map(|entry| entry.record.scope.namespace.clone());
4579 let _permit = self
4580 .runtime
4581 .admission()
4582 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4583 .await
4584 .map_err(|error| map_grpc_admission_error(self.runtime.metrics().as_ref(), error))?;
4585 let report = self
4586 .store
4587 .import(import_request.clone())
4588 .await
4589 .map_err(map_store_error)?;
4590 record_trace(
4591 &self.runtime,
4592 TraceOperationKind::Import,
4593 "grpc",
4594 tenant_id,
4595 namespace,
4596 principal,
4597 started_at_unix_ms,
4598 if report.failed_records.is_empty() {
4599 TraceStatus::Ok
4600 } else {
4601 TraceStatus::Error
4602 },
4603 (!report.failed_records.is_empty())
4604 .then(|| format!("{} import validation failures", report.failed_records.len())),
4605 OperationTraceSummary {
4606 request_count: Some(import_request.package.records.len() as u32),
4607 dry_run: Some(import_request.dry_run),
4608 ..OperationTraceSummary::default()
4609 },
4610 None,
4611 correlation_id,
4612 );
4613 Ok(Response::new(ProtoImportReply {
4614 mode: request.mode,
4615 dry_run: report.dry_run,
4616 applied: report.applied,
4617 compatible_package: report.compatible_package,
4618 package_version: report.package_version,
4619 validated_records: report.validated_records,
4620 imported_records: report.imported_records,
4621 skipped_records: report.skipped_records,
4622 replaced_existing: report.replaced_existing,
4623 snapshot_id: report.snapshot_id,
4624 failed_records: report
4625 .failed_records
4626 .into_iter()
4627 .map(|failure| mnemara_protocol::v1::ImportFailure {
4628 record_id: failure.record_id,
4629 reason: failure.reason,
4630 })
4631 .collect(),
4632 }))
4633 }
4634
4635 async fn ship_snapshot(
4636 &self,
4637 request: Request<ProtoSnapshotShipRequest>,
4638 ) -> Result<Response<SnapshotShipReply>, Status> {
4639 self.runtime.metrics().grpc_snapshot_ship.record_started();
4640 validate_grpc_auth(
4641 &request,
4642 self.runtime.auth().as_ref(),
4643 AuthPermission::Admin,
4644 )?;
4645 let principal = grpc_principal(&request);
4646 let started_at_unix_ms = now_unix_ms();
4647 let correlation_id = self.runtime.traces().next_id("corr");
4648 let result = async {
4649 let request = request.into_inner();
4650 let mode = import_mode_from_proto(request.mode)?;
4651 let tenant_id = request.tenant_id.clone();
4652 let namespace = request.namespace.clone();
4653 let _permit = self
4654 .runtime
4655 .admission()
4656 .acquire(AdmissionClass::Admin, tenant_id.as_deref())
4657 .await
4658 .map_err(|error| {
4659 map_grpc_admission_error(self.runtime.metrics().as_ref(), error)
4660 })?;
4661 let package = self
4662 .store
4663 .export(ExportRequest {
4664 tenant_id: tenant_id.clone(),
4665 namespace: namespace.clone(),
4666 include_archived: request.include_archived,
4667 })
4668 .await
4669 .map_err(map_store_error)?;
4670 let exported_records = package.records.len() as u64;
4671 let import_request = ImportRequest {
4672 package,
4673 mode,
4674 dry_run: request.dry_run,
4675 };
4676 let (remote_status, remote_report) = post_remote_import_raw(
4677 &request.target_url,
4678 request.bearer_token.as_deref(),
4679 &import_request,
4680 )
4681 .await
4682 .map_err(Status::unavailable)?;
4683 let report = SnapshotShipReport {
4684 target_url: request.target_url,
4685 exported_records,
4686 imported_records: remote_report.imported_records,
4687 skipped_records: remote_report.skipped_records,
4688 dry_run: remote_report.dry_run,
4689 compatible_package: remote_report.compatible_package,
4690 remote_status,
4691 remote_snapshot_id: Some(remote_report.snapshot_id),
4692 };
4693 record_trace(
4694 &self.runtime,
4695 TraceOperationKind::SnapshotShip,
4696 "grpc",
4697 tenant_id,
4698 namespace,
4699 principal,
4700 started_at_unix_ms,
4701 if report.compatible_package {
4702 TraceStatus::Ok
4703 } else {
4704 TraceStatus::Error
4705 },
4706 (!report.compatible_package)
4707 .then(|| "snapshot target rejected the package".to_string()),
4708 OperationTraceSummary {
4709 request_count: Some(report.exported_records as u32),
4710 dry_run: Some(report.dry_run),
4711 ..OperationTraceSummary::default()
4712 },
4713 None,
4714 correlation_id,
4715 );
4716 Ok(Response::new(snapshot_ship_report_to_proto(report)))
4717 }
4718 .await;
4719 record_grpc_result(&self.runtime.metrics().grpc_snapshot_ship, result)
4720 }
4721}