#[cfg(test)]
mod auth_tests;
mod auth_token;
mod key_binding;
mod message_hosted;
mod message_objects;
mod message_pushpull;
mod message_refs;
mod message_status;
mod native_pack;
mod object_transfer;
mod provider_pack;
mod semantic_graph;
pub use auth_token::AuthToken;
pub use key_binding::{
WireKeyBinding, WireKeyBindingLiveness, WireKeyBindingRegistry, decode_key_binding_registry,
encode_key_binding_registry,
};
pub use message_hosted::{
HarnessIdentity, HostedGrantInfo, HostedNamespaceInfo, HostedRepositoryInfo, HostedSpoolInfo,
HostedSpoolKind, ProgressCheckpoint, SessionDiffSummary, SessionReportEnvelope,
TranscriptAttachmentRef, UsageTotals, WorktreeChangeBaseline,
};
pub use message_objects::{ObjectData, ObjectRequest};
pub use message_pushpull::{PullComplete, PushComplete};
pub use message_refs::{
AdvertisedRef, AdvertisedRefError, HeadInfo, RefEntry, RefFilter, RefKind, RefUpdated,
};
pub use message_status::{
Error, ErrorCode, RemoteCursorFailure, RemoteCursorReason, RemoteDuration, RemoteFailureCode,
RemoteFailureDetail, RemoteTimestamp,
};
pub use native_pack::{
GitPackChunkState, GrowingPackChunkReader, MAX_RECEIVED_GIT_PACK_SIZE,
MAX_RECEIVED_PACK_INDEX_SIZE, MAX_RECEIVED_PACK_SIZE, NativePackBundle, NativePackFileBundle,
NativePackStreamingWriter, PackChunkSpool, PackChunkState, PackFileChunkReader,
build_native_pack, install_received_pack, is_native_packable_object_type,
native_pack_excluded_object_types, next_pack_chunk, receive_pack_chunk,
reuse_native_pack_encoded_subset_in,
};
pub use object_transfer::{
MAX_PULL_FRAME_MESSAGE_SIZE, MAX_RECEIVED_REDACTIONS_BLOB_SIZE,
MAX_RECEIVED_STATE_VISIBILITY_BLOB_SIZE, admit_declared_received_len,
check_received_transfer_blob_size, chunk_bounds, chunk_count, chunk_offset, load_object_data,
load_requested_object, store_received_object,
};
pub use objects::transfer::{
GitLaneTransferIntent, ObjectAvailabilityPlan, ObjectId, ObjectInfo, ObjectType,
ObjectTypeBucket, PlannedObject, RepositoryTransferPlan, StateClosureOptions,
TransferPartitions, TransferPlanStats, enumerate_state_closure, enumerate_state_closure_plan,
enumerate_state_closure_plan_with_options, enumerate_state_closure_transfer_from_boundaries,
enumerate_state_closure_transfer_with_options, enumerate_state_closure_with_options,
has_object, is_ancestor, missing_blobs_in_tree, plan_object_availability,
};
pub use provider_pack::{
CompletedProviderPack, ProviderPackBundle, ProviderPackExtent, ProviderPackIndexEntry,
ProviderPackManifest, ProviderPackSpool, ProviderPackWriter, assemble_provider_pack,
};
pub use semantic_graph::{
SemanticGraphQueryKind, SemanticGraphQueryRequest, SemanticGraphQueryResponse, SemanticGraphRef,
};
pub const DEFAULT_PORT: u16 = 8421;
pub const PROTOCOL_VERSION: u32 = 1;
pub const MAX_MESSAGE_SIZE: usize = 64 * 1024 * 1024;
#[derive(Debug, thiserror::Error)]
pub enum ProtocolError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("serialization error: {0}")]
Serialization(String),
#[error("message too large: {size} bytes (max {max})")]
MessageTooLarge { size: usize, max: usize },
#[error("invalid message type: {0}")]
InvalidMessageType(u8),
#[error("protocol version mismatch: server={server}, client={client}")]
VersionMismatch { server: u32, client: u32 },
#[error("capability not supported: {0}")]
CapabilityNotSupported(String),
#[error("authentication failed: {0}")]
AuthenticationFailed(String),
#[error("authorization failed: {0}")]
AuthorizationFailed(String),
#[error("object not found: {0}")]
ObjectNotFound(String),
#[error("already exists: {0}")]
AlreadyExists(String),
#[error("invalid state: {0}")]
InvalidState(String),
#[error("remote error: {0}")]
Remote(String),
#[error("remote failure ({code:?}): {message}")]
RemoteFailure {
code: RemoteFailureCode,
message: String,
details: Vec<RemoteFailureDetail>,
},
#[error("lock error: {0}")]
LockError(String),
}
impl From<rmp_serde::encode::Error> for ProtocolError {
fn from(e: rmp_serde::encode::Error) -> Self {
ProtocolError::Serialization(e.to_string())
}
}
impl From<rmp_serde::decode::Error> for ProtocolError {
fn from(e: rmp_serde::decode::Error) -> Self {
ProtocolError::Serialization(e.to_string())
}
}
impl From<objects::error::HeddleError> for ProtocolError {
fn from(e: objects::error::HeddleError) -> Self {
match &e {
objects::error::HeddleError::NotFound(_)
| objects::error::HeddleError::StateNotFound(_)
| objects::error::HeddleError::MissingObject { .. } => {
ProtocolError::ObjectNotFound(e.to_string())
}
_ => ProtocolError::Remote(e.to_string()),
}
}
}
impl ProtocolError {
pub fn client_message(&self) -> String {
match self {
ProtocolError::Io(_) => "network error".to_string(),
ProtocolError::Serialization(_) => "protocol error".to_string(),
ProtocolError::MessageTooLarge { .. } => "message too large".to_string(),
ProtocolError::InvalidMessageType(_) => "protocol error".to_string(),
ProtocolError::VersionMismatch { .. } => "protocol version mismatch".to_string(),
ProtocolError::CapabilityNotSupported(_) => "capability not supported".to_string(),
ProtocolError::AuthenticationFailed(_) => "permission denied".to_string(),
ProtocolError::AuthorizationFailed(_) => "permission denied".to_string(),
ProtocolError::ObjectNotFound(_) => "object not found".to_string(),
ProtocolError::AlreadyExists(_) => "resource already exists".to_string(),
ProtocolError::InvalidState(_) => "invalid request state".to_string(),
ProtocolError::Remote(_) => "internal server error".to_string(),
ProtocolError::RemoteFailure { message, .. } => message.clone(),
ProtocolError::LockError(_) => "internal server error".to_string(),
}
}
pub fn error_code(&self) -> ErrorCode {
match self {
ProtocolError::Io(_) => ErrorCode::Network,
ProtocolError::Serialization(_) => ErrorCode::Protocol,
ProtocolError::MessageTooLarge { .. } => ErrorCode::Protocol,
ProtocolError::InvalidMessageType(_) => ErrorCode::Protocol,
ProtocolError::VersionMismatch { .. } => ErrorCode::Protocol,
ProtocolError::CapabilityNotSupported(_) => ErrorCode::Protocol,
ProtocolError::AuthenticationFailed(_) => ErrorCode::PermissionDenied,
ProtocolError::AuthorizationFailed(_) => ErrorCode::PermissionDenied,
ProtocolError::ObjectNotFound(_) => ErrorCode::NotFound,
ProtocolError::AlreadyExists(_) => ErrorCode::InvalidArgument,
ProtocolError::InvalidState(_) => ErrorCode::InvalidArgument,
ProtocolError::Remote(_) => ErrorCode::Server,
ProtocolError::RemoteFailure { code, .. } => match code {
RemoteFailureCode::InvalidArgument
| RemoteFailureCode::AlreadyExists
| RemoteFailureCode::FailedPrecondition
| RemoteFailureCode::OutOfRange => ErrorCode::InvalidArgument,
RemoteFailureCode::NotFound => ErrorCode::NotFound,
RemoteFailureCode::PermissionDenied | RemoteFailureCode::Unauthenticated => {
ErrorCode::PermissionDenied
}
RemoteFailureCode::DeadlineExceeded
| RemoteFailureCode::ResourceExhausted
| RemoteFailureCode::Aborted
| RemoteFailureCode::Unavailable
| RemoteFailureCode::Cancelled => ErrorCode::Network,
RemoteFailureCode::Unspecified
| RemoteFailureCode::Unknown
| RemoteFailureCode::Unimplemented
| RemoteFailureCode::Internal
| RemoteFailureCode::DataLoss => ErrorCode::Server,
},
ProtocolError::LockError(_) => ErrorCode::Server,
}
}
pub fn to_wire_error(&self, details: Option<String>) -> Error {
Error {
code: self.error_code(),
message: self.client_message(),
details,
}
}
}
pub type Result<T> = std::result::Result<T, ProtocolError>;
#[cfg(test)]
mod tests {
use std::io;
use super::{ErrorCode, ProtocolError, RemoteFailureCode};
#[test]
fn missing_object_errors_surface_as_not_found() {
use objects::error::HeddleError;
let cases = vec![
HeddleError::NotFound("blob missing".to_string()),
HeddleError::StateNotFound(objects::object::StateId::from_content_hash(
objects::object::ContentHash::compute(b"missing"),
)),
HeddleError::MissingObject {
object_type: "tree".to_string(),
id: "abc123".to_string(),
},
];
for err in cases {
let protocol_error = ProtocolError::from(err);
assert_eq!(
protocol_error.error_code(),
ErrorCode::NotFound,
"missing-object errors must surface as NotFound, got {protocol_error:?}"
);
assert!(
!matches!(protocol_error, ProtocolError::Remote(_)),
"missing-object errors must not degrade to Remote/Server"
);
}
}
#[test]
fn protocol_error_public_mapping_is_stable() {
let cases = vec![
(
ProtocolError::Io(io::Error::new(io::ErrorKind::TimedOut, "timeout")),
"network error",
ErrorCode::Network,
),
(
ProtocolError::Serialization("bad msgpack".to_string()),
"protocol error",
ErrorCode::Protocol,
),
(
ProtocolError::MessageTooLarge { size: 65, max: 64 },
"message too large",
ErrorCode::Protocol,
),
(
ProtocolError::InvalidMessageType(42),
"protocol error",
ErrorCode::Protocol,
),
(
ProtocolError::VersionMismatch {
server: 2,
client: 1,
},
"protocol version mismatch",
ErrorCode::Protocol,
),
(
ProtocolError::CapabilityNotSupported("pack-v2".to_string()),
"capability not supported",
ErrorCode::Protocol,
),
(
ProtocolError::AuthenticationFailed("bad token".to_string()),
"permission denied",
ErrorCode::PermissionDenied,
),
(
ProtocolError::AuthorizationFailed("missing grant".to_string()),
"permission denied",
ErrorCode::PermissionDenied,
),
(
ProtocolError::ObjectNotFound("abc123".to_string()),
"object not found",
ErrorCode::NotFound,
),
(
ProtocolError::AlreadyExists("__users/luke/repo".to_string()),
"resource already exists",
ErrorCode::InvalidArgument,
),
(
ProtocolError::InvalidState("bad resume".to_string()),
"invalid request state",
ErrorCode::InvalidArgument,
),
(
ProtocolError::Remote("database unavailable".to_string()),
"internal server error",
ErrorCode::Server,
),
(
ProtocolError::RemoteFailure {
code: RemoteFailureCode::InvalidArgument,
message: "server supplied message".to_string(),
details: Vec::new(),
},
"server supplied message",
ErrorCode::InvalidArgument,
),
(
ProtocolError::LockError("ref locked".to_string()),
"internal server error",
ErrorCode::Server,
),
];
for (error, expected_message, expected_code) in cases {
assert_eq!(error.client_message(), expected_message);
assert_eq!(error.error_code(), expected_code);
let wire_error = error.to_wire_error(Some("trace id".to_string()));
assert_eq!(wire_error.code, expected_code);
assert_eq!(wire_error.message, expected_message);
assert_eq!(wire_error.details.as_deref(), Some("trace id"));
}
}
}