From 600753b47b75898c311ecfb625bd1887031b3475 Mon Sep 17 00:00:00 2001 From: akbash Date: Wed, 15 Jul 2026 23:43:21 +0300 Subject: fix: guard cli requests against protocol mismatches (#1453) refs #1435 Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com> Co-authored-by: Can Celik --- src/cli/plugin.rs | 3 ++ src/cli/protocol_guard.rs | 110 ++++++++++++++++++++++++++++++++++++++++++++++ src/cli/server.rs | 4 +- 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/cli/protocol_guard.rs (limited to 'src/cli') diff --git a/src/cli/plugin.rs b/src/cli/plugin.rs index 5cc528f4..68cf0282 100644 --- a/src/cli/plugin.rs +++ b/src/cli/plugin.rs @@ -932,6 +932,9 @@ fn register_installed_plugin( ), ))); } + Err(unlink_err) if super::protocol_mismatch_was_reported(&unlink_err) => { + return Err(InstallFailure::KeepCheckout(unlink_err)); + } Err(unlink_err) => { return Err(InstallFailure::KeepCheckout(std::io::Error::other( format!( diff --git a/src/cli/protocol_guard.rs b/src/cli/protocol_guard.rs new file mode 100644 index 00000000..21302bbf --- /dev/null +++ b/src/cli/protocol_guard.rs @@ -0,0 +1,110 @@ +use std::fmt; + +use crate::api::schema::{ErrorBody, ErrorResponse}; + +#[derive(Debug)] +pub(super) struct ProtocolMismatchReported; + +impl fmt::Display for ProtocolMismatchReported { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("protocol mismatch was already reported") + } +} + +impl std::error::Error for ProtocolMismatchReported {} + +pub(super) fn mismatch_response( + request_id: &str, + server_protocol: u32, + restart_guidance: &str, +) -> Option { + let client_protocol = crate::protocol::PROTOCOL_VERSION; + if client_protocol == server_protocol { + return None; + } + + let message = if client_protocol > server_protocol { + format!( + "client protocol {client_protocol} is newer than server protocol {server_protocol}; restart the Herdr server before using this command. {restart_guidance}" + ) + } else { + format!( + "client protocol {client_protocol} is older than server protocol {server_protocol}; upgrade the Herdr client before using this command" + ) + }; + + Some(ErrorResponse { + id: request_id.to_string(), + error: ErrorBody { + code: "protocol_mismatch".into(), + message, + }, + }) +} + +pub(super) fn reported_error() -> std::io::Error { + std::io::Error::other(ProtocolMismatchReported) +} + +pub(super) fn was_reported(err: &std::io::Error) -> bool { + err.get_ref() + .and_then(|source| source.downcast_ref::()) + .is_some() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn matching_protocol_has_no_error() { + assert!(mismatch_response("req", crate::protocol::PROTOCOL_VERSION, "restart").is_none()); + } + + #[test] + fn older_server_error_preserves_request_id_and_guidance() { + let response = mismatch_response( + "cli:wait:agent-status", + crate::protocol::PROTOCOL_VERSION - 1, + "Run the session stop command, then restart.", + ) + .unwrap(); + + assert_eq!(response.id, "cli:wait:agent-status"); + assert_eq!(response.error.code, "protocol_mismatch"); + assert!(response.error.message.contains(&format!( + "client protocol {}", + crate::protocol::PROTOCOL_VERSION + ))); + assert!(response.error.message.contains(&format!( + "server protocol {}", + crate::protocol::PROTOCOL_VERSION - 1 + ))); + assert!(response.error.message.contains("restart")); + } + + #[test] + fn newer_server_error_tells_user_to_upgrade_client() { + let response = mismatch_response( + "cli:pane:list", + crate::protocol::PROTOCOL_VERSION + 1, + "unused restart guidance", + ) + .unwrap(); + + assert!(response + .error + .message + .contains("older than server protocol")); + assert!(response.error.message.contains("upgrade the Herdr client")); + assert!(!response.error.message.contains("unused restart guidance")); + } + + #[test] + fn reported_error_is_recognizable_without_string_matching() { + assert!(was_reported(&reported_error())); + assert!(!was_reported(&std::io::Error::other( + "protocol mismatch was already reported" + ))); + } +} diff --git a/src/cli/server.rs b/src/cli/server.rs index 29775999..ff87cab1 100644 --- a/src/cli/server.rs +++ b/src/cli/server.rs @@ -201,7 +201,9 @@ fn server_live_handoff(args: &[String]) -> std::io::Result { return Ok(2); }; - let response = super::send_request(&Request { + // Live handoff is itself a protocol-mismatch recovery path, so it must + // reach the running server without the normal CLI compatibility guard. + let response = super::send_request_unchecked(&Request { id: "cli:server:live-handoff".into(), method: Method::ServerLiveHandoff(params), })?; -- cgit v1.2.3