Rust · 5688 bytes Raw Blame History
1 //! Shared IPC types for garcard daemon and control clients.
2
3 use serde::{Deserialize, Serialize};
4 use serde_json::Value;
5 use std::ffi::OsString;
6 use std::os::unix::net::UnixListener;
7 use std::path::PathBuf;
8 use std::time::{SystemTime, UNIX_EPOCH};
9
10 /// Protocol version for compatibility checks.
11 pub const PROTOCOL_VERSION: u32 = 1;
12
13 /// Runtime socket filename.
14 pub const SOCKET_BASENAME: &str = "garcard.sock";
15
16 /// Commands accepted by garcard daemon.
17 #[derive(Debug, Clone, Serialize, Deserialize)]
18 #[serde(tag = "command", rename_all = "snake_case")]
19 pub enum Command {
20 Ping,
21 Status,
22 Diagnose,
23 Version,
24 AuthSummary,
25 TempList,
26 TempRevoke { authorization_id: String },
27 TempRevokeAll,
28 Quit,
29 }
30
31 /// Standard daemon response envelope.
32 #[derive(Debug, Clone, Serialize, Deserialize)]
33 pub struct Response {
34 pub success: bool,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub data: Option<Value>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub error: Option<String>,
39 }
40
41 impl Response {
42 pub fn ok() -> Self {
43 Self {
44 success: true,
45 data: None,
46 error: None,
47 }
48 }
49
50 pub fn ok_with_data(data: impl Serialize) -> Self {
51 Self {
52 success: true,
53 data: serde_json::to_value(data).ok(),
54 error: None,
55 }
56 }
57
58 pub fn err(message: impl Into<String>) -> Self {
59 Self {
60 success: false,
61 data: None,
62 error: Some(message.into()),
63 }
64 }
65 }
66
67 /// Minimal health and runtime status summary.
68 #[derive(Debug, Clone, Serialize, Deserialize)]
69 pub struct StatusData {
70 pub running: bool,
71 pub pid: u32,
72 pub uptime_secs: u64,
73 pub version: String,
74 pub protocol_version: u32,
75 pub socket_path: String,
76 pub agent_backend: String,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub authority_connected: Option<bool>,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub authority_error: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub subject_kind: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub temporary_authorization_count: Option<usize>,
85 }
86
87 /// Version handshake payload.
88 #[derive(Debug, Clone, Serialize, Deserialize)]
89 pub struct VersionData {
90 pub component: String,
91 pub version: String,
92 pub protocol_version: u32,
93 }
94
95 /// Auth workflow summary with no sensitive data.
96 #[derive(Debug, Clone, Serialize, Deserialize)]
97 pub struct AuthSummary {
98 pub state: String,
99 pub active_requests: usize,
100 pub queued_requests: usize,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub last_action_id: Option<String>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub last_outcome: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub last_retention_policy: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub last_retention_enforced: Option<bool>,
109 }
110
111 /// Resolve the daemon socket path from `XDG_RUNTIME_DIR` with `/tmp` fallback.
112 pub fn socket_path() -> PathBuf {
113 if let Some(explicit) = std::env::var_os("GARCARD_SOCKET") {
114 return PathBuf::from(explicit);
115 }
116
117 let runtime_dir = std::env::var_os("XDG_RUNTIME_DIR");
118 if runtime_dir.as_ref().is_some_and(runtime_dir_is_usable) {
119 return socket_path_from_runtime_dir(runtime_dir);
120 }
121
122 socket_path_from_runtime_dir(None)
123 }
124
125 /// Build socket path from an explicit runtime dir value.
126 pub fn socket_path_from_runtime_dir(runtime_dir: Option<OsString>) -> PathBuf {
127 match runtime_dir {
128 Some(dir) => PathBuf::from(dir).join(SOCKET_BASENAME),
129 None => PathBuf::from("/tmp").join(SOCKET_BASENAME),
130 }
131 }
132
133 /// Check if we can create a socket in runtime dir.
134 ///
135 /// In sandboxed environments, `XDG_RUNTIME_DIR` can exist but still reject
136 /// socket creation. We probe with a unique temporary socket path so caller
137 /// code can safely fall back to `/tmp`.
138 fn runtime_dir_is_usable(runtime_dir: &OsString) -> bool {
139 let base = PathBuf::from(runtime_dir);
140 let timestamp = SystemTime::now()
141 .duration_since(UNIX_EPOCH)
142 .ok()
143 .map(|d| d.as_nanos())
144 .unwrap_or(0);
145 let probe = base.join(format!(
146 ".garcard-probe-{}-{}.sock",
147 std::process::id(),
148 timestamp
149 ));
150
151 match UnixListener::bind(&probe) {
152 Ok(listener) => {
153 drop(listener);
154 let _ = std::fs::remove_file(&probe);
155 true
156 }
157 Err(_) => false,
158 }
159 }
160
161 #[cfg(test)]
162 mod tests {
163 use super::*;
164
165 #[test]
166 fn command_round_trip() {
167 let cmd = Command::AuthSummary;
168 let encoded = serde_json::to_string(&cmd).expect("encode command");
169 let decoded: Command = serde_json::from_str(&encoded).expect("decode command");
170 assert!(matches!(decoded, Command::AuthSummary));
171 }
172
173 #[test]
174 fn command_round_trip_diagnose() {
175 let cmd = Command::Diagnose;
176 let encoded = serde_json::to_string(&cmd).expect("encode command");
177 let decoded: Command = serde_json::from_str(&encoded).expect("decode command");
178 assert!(matches!(decoded, Command::Diagnose));
179 }
180
181 #[test]
182 fn socket_path_falls_back_to_tmp() {
183 let path = socket_path_from_runtime_dir(None);
184 assert_eq!(path, PathBuf::from("/tmp/garcard.sock"));
185 }
186
187 #[test]
188 fn socket_path_uses_runtime_dir() {
189 let path = socket_path_from_runtime_dir(Some(OsString::from("/run/user/1000")));
190 assert_eq!(path, PathBuf::from("/run/user/1000/garcard.sock"));
191 }
192 }
193