tact_memory/server/protocol.rs
1//! Versioned wire types shared by the remote client and reference server.
2
3use crate::{MemoryCandidate, MemoryKey, MemoryRecord};
4use const_format::concatcp;
5use serde::{Deserialize, Serialize};
6
7/// Header carrying the authenticated namespace on every request.
8pub const NAMESPACE_HEADER: &str = "x-tact-memory-namespace";
9/// Maximum UTF-8 bytes accepted in a namespace.
10pub const MAX_NAMESPACE_BYTES: usize = 128;
11
12/// Session-negotiation route.
13pub const SESSION_PATH: &str = concatcp!("v", crate::VERSION, "/session");
14/// Semantic scan route.
15pub const SCAN_PATH: &str = concatcp!("v", crate::VERSION, "/memories/scan");
16/// Record-read route.
17pub const READ_PATH: &str = concatcp!("v", crate::VERSION, "/memories/read");
18/// Visible-record listing route.
19pub const LIST_PATH: &str = concatcp!("v", crate::VERSION, "/memories/list");
20/// Direct mutation route.
21pub const PUT_PATH: &str = concatcp!("v", crate::VERSION, "/memories/put");
22/// Compare-and-swap deletion route.
23pub const DELETE_PATH: &str = concatcp!("v", crate::VERSION, "/memories/delete");
24/// Authoritative namespace snapshot route.
25pub const SYNC_PATH: &str = concatcp!("v", crate::VERSION, "/memories/sync");
26/// Paginated server snapshot export route.
27pub const EXPORT_PATH: &str = concatcp!("v", crate::VERSION, "/memories/export");
28/// Maximum records returned by one export page.
29pub const MAX_EXPORT_PAGE_RECORDS: usize = 128;
30
31/// Returns whether a namespace is non-empty, bounded, and URL/header safe.
32pub fn is_valid_namespace(namespace: &str) -> bool {
33 !namespace.is_empty()
34 && namespace.len() <= MAX_NAMESPACE_BYTES
35 && namespace
36 .bytes()
37 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
38}
39
40/// Authorization role returned by session negotiation.
41#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
42#[serde(rename_all = "snake_case")]
43pub enum RemoteRole {
44 /// May scan, read, list, and export visible memories.
45 Reader,
46 /// May also mutate the credential's namespace.
47 Writer,
48}
49
50/// Session negotiation result.
51#[derive(Debug, Deserialize, Serialize)]
52pub struct SessionResponse {
53 /// Server wire protocol version.
54 pub protocol_version: u32,
55 /// Namespace bound to the presented credential.
56 pub namespace: String,
57 /// Operations authorized for the credential.
58 pub role: RemoteRole,
59}
60
61/// Request for semantic retrieval.
62#[derive(Debug, Deserialize, Serialize)]
63#[serde(deny_unknown_fields)]
64pub struct ScanRequest {
65 /// Search text, bounded by [`crate::MemoryLimits::query_bytes`].
66 pub query: String,
67 /// Maximum candidates requested from the server.
68 pub limit: usize,
69}
70
71/// Response to [`ScanRequest`].
72#[derive(Debug, Deserialize, Serialize)]
73pub struct ScanResponse {
74 /// Candidates in descending rank.
75 pub candidates: Vec<MemoryCandidate>,
76}
77
78/// Request to read unversioned IDs and versioned keys.
79#[derive(Debug, Deserialize, Serialize)]
80#[serde(deny_unknown_fields)]
81pub struct ReadRequest {
82 /// IDs in the authenticated namespace.
83 #[serde(default)]
84 pub ids: Vec<i64>,
85 /// Versioned keys, which may identify visible foreign namespaces.
86 #[serde(default)]
87 pub keys: Vec<MemoryKey>,
88}
89
90/// Response containing requested current records.
91#[derive(Debug, Deserialize, Serialize)]
92pub struct ReadResponse {
93 /// Existing, current requested records.
94 pub memories: Vec<MemoryRecord>,
95}
96
97/// Response containing a bounded deterministic window of visible records.
98#[derive(Debug, Deserialize, Serialize)]
99pub struct ListResponse {
100 /// At most [`crate::MemoryLimits::records`] records in deterministic store order.
101 pub memories: Vec<MemoryRecord>,
102}
103
104/// Request to insert or compare-and-swap replace a memory.
105#[derive(Debug, Deserialize, Serialize)]
106#[serde(deny_unknown_fields)]
107pub struct PutRequest {
108 /// New record content.
109 pub content: String,
110 /// Current key of the record to replace, or `None` to insert.
111 #[serde(default)]
112 pub replacement: Option<MemoryKey>,
113}
114
115/// Response to [`PutRequest`].
116#[derive(Debug, Deserialize, Serialize)]
117pub struct PutResponse {
118 /// Inserted or replaced current record.
119 pub memory: MemoryRecord,
120}
121
122/// Request to compare-and-swap delete a memory.
123#[derive(Debug, Deserialize, Serialize)]
124#[serde(deny_unknown_fields)]
125pub struct DeleteRequest {
126 /// Current key of the record to delete.
127 pub key: MemoryKey,
128}
129
130/// Authoritative full snapshot for one authenticated namespace.
131#[derive(Debug, Deserialize, Serialize)]
132#[serde(deny_unknown_fields)]
133pub struct SyncRequest {
134 /// Complete local snapshot; absent server records are deleted.
135 pub memories: Vec<MemoryRecord>,
136}
137
138/// Exclusive position in deterministic `(namespace, id)` export order.
139#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
140#[serde(deny_unknown_fields)]
141pub struct ExportCursor {
142 /// Namespace of the last returned record.
143 pub namespace: String,
144 /// ID of the last returned record.
145 pub id: i64,
146}
147
148/// Request for one deterministic snapshot export page.
149#[derive(Debug, Deserialize, Serialize)]
150#[serde(deny_unknown_fields)]
151pub struct ExportRequest {
152 /// `None` exports every namespace; an explicit list exports only those namespaces.
153 pub namespaces: Option<Vec<String>>,
154 /// Exclusive cursor returned by the preceding page.
155 #[serde(default)]
156 pub cursor: Option<ExportCursor>,
157 /// Requested page size, clamped to [`MAX_EXPORT_PAGE_RECORDS`].
158 pub limit: usize,
159}
160
161/// One deterministic snapshot export page.
162#[derive(Debug, Deserialize, Serialize)]
163pub struct ExportResponse {
164 /// Records strictly after the request cursor in `(namespace, id)` order.
165 pub memories: Vec<MemoryRecord>,
166 /// Cursor for another page, or `None` when the snapshot is exhausted.
167 pub next_cursor: Option<ExportCursor>,
168}
169
170/// Counts produced by applying an authoritative namespace snapshot.
171#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
172pub struct SyncReport {
173 /// Previously absent records inserted.
174 pub inserted: usize,
175 /// Existing records replaced by snapshot state.
176 pub replaced: usize,
177 /// Existing records already equal to snapshot state.
178 pub unchanged: usize,
179 /// Existing records absent from the snapshot and deleted.
180 pub deleted: usize,
181}
182
183/// Stable machine-readable remote failure category.
184#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
185#[serde(rename_all = "snake_case")]
186pub enum RemoteErrorCode {
187 /// Request shape or values are invalid.
188 BadRequest,
189 /// Credential is missing or invalid.
190 Unauthorized,
191 /// Credential does not authorize the operation.
192 Forbidden,
193 /// Requested namespace differs from the credential namespace.
194 NamespaceMismatch,
195 /// Client and server protocol versions differ.
196 UnsupportedProtocol,
197 /// Scan query exceeds its byte limit.
198 QueryTooLarge,
199 /// One memory exceeds its content byte limit.
200 ContentTooLarge,
201 /// Namespace record capacity is exhausted.
202 RecordCapacity,
203 /// Namespace aggregate content capacity is exhausted.
204 ContentCapacity,
205 /// Equivalent content already exists.
206 Duplicate,
207 /// Requested record does not exist.
208 NotFound,
209 /// Compare-and-swap version or snapshot state conflicts.
210 Conflict,
211 /// Service cannot currently complete the operation.
212 Unavailable,
213 /// Server failed without a safe client-visible detail.
214 Internal,
215}
216
217/// Error response returned for a failed protocol operation.
218#[derive(Debug, Deserialize, Serialize)]
219pub struct ErrorResponse {
220 /// Stable failure category.
221 pub code: RemoteErrorCode,
222}