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