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