Skip to main content

pty_mcp/ssh/
model.rs

1use std::{fmt, str::FromStr};
2
3use chrono::{DateTime, Utc};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value};
7use uuid::Uuid;
8
9#[derive(
10    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
11)]
12#[serde(transparent)]
13pub struct SshConnectionId(String);
14
15impl SshConnectionId {
16    pub fn new() -> Self {
17        Self(format!("sshconn_{}", Uuid::new_v4().simple()))
18    }
19
20    pub fn as_str(&self) -> &str {
21        &self.0
22    }
23}
24
25impl Default for SshConnectionId {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl fmt::Display for SshConnectionId {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        f.write_str(&self.0)
34    }
35}
36
37impl FromStr for SshConnectionId {
38    type Err = &'static str;
39
40    fn from_str(value: &str) -> Result<Self, Self::Err> {
41        if value.trim().is_empty() {
42            return Err("ssh connection id cannot be empty");
43        }
44
45        Ok(Self(value.to_string()))
46    }
47}
48
49impl From<String> for SshConnectionId {
50    fn from(value: String) -> Self {
51        Self(value)
52    }
53}
54
55#[derive(
56    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
57)]
58#[serde(transparent)]
59pub struct SshMountId(String);
60
61impl SshMountId {
62    pub fn new() -> Self {
63        Self(format!("sshmnt_{}", Uuid::new_v4().simple()))
64    }
65
66    pub fn as_str(&self) -> &str {
67        &self.0
68    }
69}
70
71impl Default for SshMountId {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl fmt::Display for SshMountId {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        f.write_str(&self.0)
80    }
81}
82
83impl FromStr for SshMountId {
84    type Err = &'static str;
85
86    fn from_str(value: &str) -> Result<Self, Self::Err> {
87        if value.trim().is_empty() {
88            return Err("ssh mount id cannot be empty");
89        }
90
91        Ok(Self(value.to_string()))
92    }
93}
94
95impl From<String> for SshMountId {
96    fn from(value: String) -> Self {
97        Self(value)
98    }
99}
100
101#[derive(
102    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
103)]
104#[serde(transparent)]
105pub struct SshTunnelId(String);
106
107impl SshTunnelId {
108    pub fn new() -> Self {
109        Self(format!("sshtun_{}", Uuid::new_v4().simple()))
110    }
111
112    pub fn as_str(&self) -> &str {
113        &self.0
114    }
115}
116
117impl Default for SshTunnelId {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123impl fmt::Display for SshTunnelId {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        f.write_str(&self.0)
126    }
127}
128
129impl FromStr for SshTunnelId {
130    type Err = &'static str;
131
132    fn from_str(value: &str) -> Result<Self, Self::Err> {
133        if value.trim().is_empty() {
134            return Err("ssh tunnel id cannot be empty");
135        }
136
137        Ok(Self(value.to_string()))
138    }
139}
140
141impl From<String> for SshTunnelId {
142    fn from(value: String) -> Self {
143        Self(value)
144    }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
148pub struct SshTarget {
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub host_alias: Option<String>,
151    pub host: String,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub user: Option<String>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub port: Option<u16>,
156}
157
158impl SshTarget {
159    pub fn summary(&self) -> String {
160        let host = self.host_alias.as_deref().unwrap_or(&self.host);
161        let authority = match &self.user {
162            Some(user) if !user.is_empty() => format!("{user}@{host}"),
163            _ => host.to_string(),
164        };
165
166        match self.port {
167            Some(port) => format!("{authority}:{port}"),
168            None => authority,
169        }
170    }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
174#[schemars(inline)]
175#[serde(rename_all = "snake_case")]
176pub enum SshAuthKind {
177    SshAgent,
178    IdentityFile,
179    ConfigAlias,
180}
181
182impl SshAuthKind {
183    pub const fn as_str(&self) -> &'static str {
184        match self {
185            Self::SshAgent => "ssh_agent",
186            Self::IdentityFile => "identity_file",
187            Self::ConfigAlias => "config_alias",
188        }
189    }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
193#[serde(rename_all = "snake_case")]
194pub enum SshConnectionStatus {
195    Connecting,
196    Ready,
197    Degraded,
198    Disconnecting,
199    Disconnected,
200    Failed,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
204#[serde(rename_all = "snake_case")]
205pub enum SshMountStatus {
206    Mounting,
207    Mounted,
208    Unmounting,
209    Unmounted,
210    Failed,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
214#[serde(rename_all = "snake_case")]
215pub enum SshTunnelStatus {
216    Opening,
217    Active,
218    Closing,
219    Closed,
220    Failed,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
224#[schemars(inline)]
225#[serde(rename_all = "snake_case")]
226pub enum SshMountBackend {
227    Sshfs,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
231#[schemars(inline)]
232#[serde(rename_all = "snake_case")]
233pub enum SshTunnelKind {
234    LocalForward,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
238pub struct SshBinaryCapability {
239    pub available: bool,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub path: Option<String>,
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub version: Option<String>,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
247pub struct MacFuseCapability {
248    pub available: bool,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub provider: Option<String>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub version: Option<String>,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
256pub struct SshCapabilityView {
257    pub platform: String,
258    pub ssh: SshBinaryCapability,
259    pub sshfs: SshBinaryCapability,
260    pub unmount: SshBinaryCapability,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub diskutil: Option<SshBinaryCapability>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub macfuse: Option<MacFuseCapability>,
265}
266
267impl Default for SshCapabilityView {
268    fn default() -> Self {
269        Self {
270            platform: std::env::consts::OS.to_string(),
271            ssh: SshBinaryCapability::default(),
272            sshfs: SshBinaryCapability::default(),
273            unmount: SshBinaryCapability::default(),
274            diskutil: None,
275            macfuse: None,
276        }
277    }
278}
279
280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
281pub struct SshConnectionSummary {
282    pub connection_id: SshConnectionId,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub title: Option<String>,
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub description: Option<String>,
287    pub status: SshConnectionStatus,
288    pub target: SshTarget,
289    pub target_summary: String,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub auth_kind: Option<SshAuthKind>,
292    pub started_at: DateTime<Utc>,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub last_used_at: Option<DateTime<Utc>>,
295    pub active_session_count: usize,
296    pub active_mount_count: usize,
297    pub active_tunnel_count: usize,
298    #[serde(default, skip_serializing_if = "Map::is_empty")]
299    pub metadata: Map<String, Value>,
300}
301
302#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
303pub struct SshMountSummary {
304    pub mount_id: SshMountId,
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub title: Option<String>,
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub description: Option<String>,
309    pub connection_id: SshConnectionId,
310    pub target_summary: String,
311    pub status: SshMountStatus,
312    pub backend: SshMountBackend,
313    pub local_path: String,
314    pub remote_path: String,
315    pub read_only: bool,
316    pub mounted_at: DateTime<Utc>,
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub last_error: Option<String>,
319}
320
321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
322pub struct SshTunnelSummary {
323    pub tunnel_id: SshTunnelId,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub title: Option<String>,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub description: Option<String>,
328    pub connection_id: SshConnectionId,
329    pub target_summary: String,
330    pub kind: SshTunnelKind,
331    pub status: SshTunnelStatus,
332    pub bind_host: String,
333    pub local_port: u16,
334    pub remote_host: String,
335    pub remote_port: u16,
336    pub started_at: DateTime<Utc>,
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub last_error: Option<String>,
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub pid: Option<u32>,
341}