Skip to main content

wire/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Shared protocol/auth transport types.
3//! Live authorization scope rules are owned by weft-server/src/access/scope.rs.
4
5#[cfg(test)]
6mod auth_tests;
7mod auth_token;
8mod key_binding;
9mod message_hosted;
10mod message_objects;
11mod message_pushpull;
12mod message_refs;
13mod message_status;
14mod native_pack;
15mod object_transfer;
16mod provider_pack;
17mod semantic_graph;
18
19pub use auth_token::AuthToken;
20pub use key_binding::{
21    WireKeyBinding, WireKeyBindingLiveness, WireKeyBindingRegistry, decode_key_binding_registry,
22    encode_key_binding_registry,
23};
24pub use message_hosted::{
25    HarnessIdentity, HostedGrantInfo, HostedNamespaceInfo, HostedRepositoryInfo, HostedSpoolInfo,
26    HostedSpoolKind, ProgressCheckpoint, SessionDiffSummary, SessionReportEnvelope,
27    TranscriptAttachmentRef, UsageTotals, WorktreeChangeBaseline,
28};
29pub use message_objects::{ObjectData, ObjectRequest};
30pub use message_pushpull::{PullComplete, PushComplete};
31pub use message_refs::{
32    AdvertisedRef, AdvertisedRefError, HeadInfo, RefEntry, RefFilter, RefKind, RefUpdated,
33};
34pub use message_status::{
35    Error, ErrorCode, RemoteCursorFailure, RemoteCursorReason, RemoteDuration, RemoteFailureCode,
36    RemoteFailureDetail, RemoteTimestamp,
37};
38pub use native_pack::{
39    GitPackChunkState, GrowingPackChunkReader, MAX_RECEIVED_GIT_PACK_SIZE,
40    MAX_RECEIVED_PACK_INDEX_SIZE, MAX_RECEIVED_PACK_SIZE, NativePackBundle, NativePackFileBundle,
41    NativePackStreamingWriter, PackChunkSpool, PackChunkState, PackFileChunkReader,
42    build_native_pack, install_received_pack, is_native_packable_object_type,
43    native_pack_excluded_object_types, next_pack_chunk, receive_pack_chunk,
44    reuse_native_pack_encoded_subset_in,
45};
46pub use object_transfer::{
47    MAX_PULL_FRAME_MESSAGE_SIZE, MAX_RECEIVED_REDACTIONS_BLOB_SIZE,
48    MAX_RECEIVED_STATE_VISIBILITY_BLOB_SIZE, admit_declared_received_len,
49    check_received_transfer_blob_size, chunk_bounds, chunk_count, chunk_offset, load_object_data,
50    load_requested_object, store_received_object,
51};
52pub use objects::transfer::{
53    GitLaneTransferIntent, ObjectAvailabilityPlan, ObjectId, ObjectInfo, ObjectType,
54    ObjectTypeBucket, PlannedObject, RepositoryTransferPlan, StateClosureOptions,
55    TransferPartitions, TransferPlanStats, enumerate_state_closure, enumerate_state_closure_plan,
56    enumerate_state_closure_plan_with_options, enumerate_state_closure_transfer_from_boundaries,
57    enumerate_state_closure_transfer_with_options, enumerate_state_closure_with_options,
58    has_object, is_ancestor, missing_blobs_in_tree, plan_object_availability,
59};
60pub use provider_pack::{
61    CompletedProviderPack, ProviderPackBundle, ProviderPackExtent, ProviderPackIndexEntry,
62    ProviderPackManifest, ProviderPackSpool, ProviderPackWriter, assemble_provider_pack,
63};
64pub use semantic_graph::{
65    SemanticGraphQueryKind, SemanticGraphQueryRequest, SemanticGraphQueryResponse, SemanticGraphRef,
66};
67
68/// Default port for Heddle protocol.
69pub const DEFAULT_PORT: u16 = 8421;
70
71/// Protocol version.
72pub const PROTOCOL_VERSION: u32 = 1;
73
74/// Maximum message size (64 MB).
75pub const MAX_MESSAGE_SIZE: usize = 64 * 1024 * 1024;
76
77/// Error type for protocol operations.
78#[derive(Debug, thiserror::Error)]
79pub enum ProtocolError {
80    #[error("io error: {0}")]
81    Io(#[from] std::io::Error),
82
83    #[error("serialization error: {0}")]
84    Serialization(String),
85
86    #[error("message too large: {size} bytes (max {max})")]
87    MessageTooLarge { size: usize, max: usize },
88
89    #[error("invalid message type: {0}")]
90    InvalidMessageType(u8),
91
92    #[error("protocol version mismatch: server={server}, client={client}")]
93    VersionMismatch { server: u32, client: u32 },
94
95    #[error("capability not supported: {0}")]
96    CapabilityNotSupported(String),
97
98    #[error("authentication failed: {0}")]
99    AuthenticationFailed(String),
100
101    #[error("authorization failed: {0}")]
102    AuthorizationFailed(String),
103
104    #[error("object not found: {0}")]
105    ObjectNotFound(String),
106
107    #[error("already exists: {0}")]
108    AlreadyExists(String),
109
110    #[error("invalid state: {0}")]
111    InvalidState(String),
112
113    #[error("remote error: {0}")]
114    Remote(String),
115
116    #[error("remote failure ({code:?}): {message}")]
117    RemoteFailure {
118        code: RemoteFailureCode,
119        message: String,
120        details: Vec<RemoteFailureDetail>,
121    },
122
123    #[error("lock error: {0}")]
124    LockError(String),
125}
126
127impl From<rmp_serde::encode::Error> for ProtocolError {
128    fn from(e: rmp_serde::encode::Error) -> Self {
129        ProtocolError::Serialization(e.to_string())
130    }
131}
132
133impl From<rmp_serde::decode::Error> for ProtocolError {
134    fn from(e: rmp_serde::decode::Error) -> Self {
135        ProtocolError::Serialization(e.to_string())
136    }
137}
138
139impl From<objects::error::HeddleError> for ProtocolError {
140    fn from(e: objects::error::HeddleError) -> Self {
141        match &e {
142            // Missing objects must surface as NotFound, not degrade to a
143            // generic server error via the blanket Remote arm below.
144            objects::error::HeddleError::NotFound(_)
145            | objects::error::HeddleError::StateNotFound(_)
146            | objects::error::HeddleError::MissingObject { .. } => {
147                ProtocolError::ObjectNotFound(e.to_string())
148            }
149            _ => ProtocolError::Remote(e.to_string()),
150        }
151    }
152}
153
154impl ProtocolError {
155    pub fn client_message(&self) -> String {
156        match self {
157            ProtocolError::Io(_) => "network error".to_string(),
158            ProtocolError::Serialization(_) => "protocol error".to_string(),
159            ProtocolError::MessageTooLarge { .. } => "message too large".to_string(),
160            ProtocolError::InvalidMessageType(_) => "protocol error".to_string(),
161            ProtocolError::VersionMismatch { .. } => "protocol version mismatch".to_string(),
162            ProtocolError::CapabilityNotSupported(_) => "capability not supported".to_string(),
163            ProtocolError::AuthenticationFailed(_) => "permission denied".to_string(),
164            ProtocolError::AuthorizationFailed(_) => "permission denied".to_string(),
165            ProtocolError::ObjectNotFound(_) => "object not found".to_string(),
166            ProtocolError::AlreadyExists(_) => "resource already exists".to_string(),
167            ProtocolError::InvalidState(_) => "invalid request state".to_string(),
168            ProtocolError::Remote(_) => "internal server error".to_string(),
169            ProtocolError::RemoteFailure { message, .. } => message.clone(),
170            ProtocolError::LockError(_) => "internal server error".to_string(),
171        }
172    }
173
174    pub fn error_code(&self) -> ErrorCode {
175        match self {
176            ProtocolError::Io(_) => ErrorCode::Network,
177            ProtocolError::Serialization(_) => ErrorCode::Protocol,
178            ProtocolError::MessageTooLarge { .. } => ErrorCode::Protocol,
179            ProtocolError::InvalidMessageType(_) => ErrorCode::Protocol,
180            ProtocolError::VersionMismatch { .. } => ErrorCode::Protocol,
181            ProtocolError::CapabilityNotSupported(_) => ErrorCode::Protocol,
182            ProtocolError::AuthenticationFailed(_) => ErrorCode::PermissionDenied,
183            ProtocolError::AuthorizationFailed(_) => ErrorCode::PermissionDenied,
184            ProtocolError::ObjectNotFound(_) => ErrorCode::NotFound,
185            ProtocolError::AlreadyExists(_) => ErrorCode::InvalidArgument,
186            ProtocolError::InvalidState(_) => ErrorCode::InvalidArgument,
187            ProtocolError::Remote(_) => ErrorCode::Server,
188            ProtocolError::RemoteFailure { code, .. } => match code {
189                RemoteFailureCode::InvalidArgument
190                | RemoteFailureCode::AlreadyExists
191                | RemoteFailureCode::FailedPrecondition
192                | RemoteFailureCode::OutOfRange => ErrorCode::InvalidArgument,
193                RemoteFailureCode::NotFound => ErrorCode::NotFound,
194                RemoteFailureCode::PermissionDenied | RemoteFailureCode::Unauthenticated => {
195                    ErrorCode::PermissionDenied
196                }
197                RemoteFailureCode::DeadlineExceeded
198                | RemoteFailureCode::ResourceExhausted
199                | RemoteFailureCode::Aborted
200                | RemoteFailureCode::Unavailable
201                | RemoteFailureCode::Cancelled => ErrorCode::Network,
202                RemoteFailureCode::Unspecified
203                | RemoteFailureCode::Unknown
204                | RemoteFailureCode::Unimplemented
205                | RemoteFailureCode::Internal
206                | RemoteFailureCode::DataLoss => ErrorCode::Server,
207            },
208            ProtocolError::LockError(_) => ErrorCode::Server,
209        }
210    }
211
212    pub fn to_wire_error(&self, details: Option<String>) -> Error {
213        Error {
214            code: self.error_code(),
215            message: self.client_message(),
216            details,
217        }
218    }
219}
220
221pub type Result<T> = std::result::Result<T, ProtocolError>;
222
223#[cfg(test)]
224mod tests {
225    use std::io;
226
227    use super::{ErrorCode, ProtocolError, RemoteFailureCode};
228
229    #[test]
230    fn missing_object_errors_surface_as_not_found() {
231        use objects::error::HeddleError;
232
233        let cases = vec![
234            HeddleError::NotFound("blob missing".to_string()),
235            HeddleError::StateNotFound(objects::object::StateId::from_content_hash(
236                objects::object::ContentHash::compute(b"missing"),
237            )),
238            HeddleError::MissingObject {
239                object_type: "tree".to_string(),
240                id: "abc123".to_string(),
241            },
242        ];
243        for err in cases {
244            let protocol_error = ProtocolError::from(err);
245            assert_eq!(
246                protocol_error.error_code(),
247                ErrorCode::NotFound,
248                "missing-object errors must surface as NotFound, got {protocol_error:?}"
249            );
250            assert!(
251                !matches!(protocol_error, ProtocolError::Remote(_)),
252                "missing-object errors must not degrade to Remote/Server"
253            );
254        }
255    }
256
257    #[test]
258    fn protocol_error_public_mapping_is_stable() {
259        let cases = vec![
260            (
261                ProtocolError::Io(io::Error::new(io::ErrorKind::TimedOut, "timeout")),
262                "network error",
263                ErrorCode::Network,
264            ),
265            (
266                ProtocolError::Serialization("bad msgpack".to_string()),
267                "protocol error",
268                ErrorCode::Protocol,
269            ),
270            (
271                ProtocolError::MessageTooLarge { size: 65, max: 64 },
272                "message too large",
273                ErrorCode::Protocol,
274            ),
275            (
276                ProtocolError::InvalidMessageType(42),
277                "protocol error",
278                ErrorCode::Protocol,
279            ),
280            (
281                ProtocolError::VersionMismatch {
282                    server: 2,
283                    client: 1,
284                },
285                "protocol version mismatch",
286                ErrorCode::Protocol,
287            ),
288            (
289                ProtocolError::CapabilityNotSupported("pack-v2".to_string()),
290                "capability not supported",
291                ErrorCode::Protocol,
292            ),
293            (
294                ProtocolError::AuthenticationFailed("bad token".to_string()),
295                "permission denied",
296                ErrorCode::PermissionDenied,
297            ),
298            (
299                ProtocolError::AuthorizationFailed("missing grant".to_string()),
300                "permission denied",
301                ErrorCode::PermissionDenied,
302            ),
303            (
304                ProtocolError::ObjectNotFound("abc123".to_string()),
305                "object not found",
306                ErrorCode::NotFound,
307            ),
308            (
309                ProtocolError::AlreadyExists("__users/luke/repo".to_string()),
310                "resource already exists",
311                ErrorCode::InvalidArgument,
312            ),
313            (
314                ProtocolError::InvalidState("bad resume".to_string()),
315                "invalid request state",
316                ErrorCode::InvalidArgument,
317            ),
318            (
319                ProtocolError::Remote("database unavailable".to_string()),
320                "internal server error",
321                ErrorCode::Server,
322            ),
323            (
324                ProtocolError::RemoteFailure {
325                    code: RemoteFailureCode::InvalidArgument,
326                    message: "server supplied message".to_string(),
327                    details: Vec::new(),
328                },
329                "server supplied message",
330                ErrorCode::InvalidArgument,
331            ),
332            (
333                ProtocolError::LockError("ref locked".to_string()),
334                "internal server error",
335                ErrorCode::Server,
336            ),
337        ];
338
339        for (error, expected_message, expected_code) in cases {
340            assert_eq!(error.client_message(), expected_message);
341            assert_eq!(error.error_code(), expected_code);
342
343            let wire_error = error.to_wire_error(Some("trace id".to_string()));
344            assert_eq!(wire_error.code, expected_code);
345            assert_eq!(wire_error.message, expected_message);
346            assert_eq!(wire_error.details.as_deref(), Some("trace id"));
347        }
348    }
349}