1use std::io::{Error as IoError, ErrorKind};
2
3use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
6
7use crate::state::persistence::BashStateSnapshot;
8use crate::tools::bash_command::BashCommandState;
9use crate::types::BashCommand;
10
11pub const PROTOCOL_MAJOR: u16 = 1;
12pub const PROTOCOL_MINOR: u16 = 5;
13pub const TYPED_ACTION_RESULT_CAPABILITY: &str = "typed_action_result";
14pub const COMPACT_ACTION_OUTPUT_CAPABILITY: &str = "compact_action_output";
15pub const GENERATION_BOUND_ACTIONS_CAPABILITY: &str = "generation_bound_actions";
16pub const CANCELLABLE_ACTION_RESERVATIONS_CAPABILITY: &str = "cancellable_action_reservations";
17pub const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
18
19#[derive(Clone, Debug, Serialize, Deserialize)]
20pub(crate) struct RpcRequest {
21 pub jsonrpc: String,
22 pub id: u64,
23 pub method: String,
24 pub params: serde_json::Value,
25}
26
27#[derive(Debug, Serialize, Deserialize)]
28pub(crate) struct RpcResponse {
29 pub jsonrpc: String,
30 pub id: u64,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub result: Option<serde_json::Value>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub error: Option<RpcError>,
35}
36
37#[derive(Debug, Serialize, Deserialize)]
38pub(crate) struct RpcError {
39 pub code: i32,
40 pub message: String,
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize)]
44pub struct HelloResult {
45 pub protocol_major: u16,
46 pub protocol_minor: u16,
47 pub capabilities: Vec<String>,
48 pub max_frame_bytes: usize,
49 pub daemon_epoch: String,
50 pub daemon_pid: u32,
51}
52
53#[derive(Clone, Debug, Serialize, Deserialize)]
54pub(crate) struct RunActionParams {
55 pub snapshot: BashStateSnapshot,
56 pub command: BashCommand,
57 pub request_key: String,
58 #[serde(default)]
59 pub consumer_id: String,
60 #[serde(default, skip_serializing_if = "crate::runtime::ShellActionOptions::is_default")]
61 pub options: crate::runtime::ShellActionOptions,
62}
63
64#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub(crate) enum ConfigureSessionTransition {
67 FirstCall,
68 ModeChange,
69 Reset,
70 WorkspaceChange,
71}
72
73#[derive(Debug, Serialize, Deserialize)]
74pub(crate) struct ConfigureSessionParams {
75 pub snapshot: BashStateSnapshot,
76 pub transition: ConfigureSessionTransition,
77}
78
79#[derive(Clone, Debug, Serialize, Deserialize)]
80pub(crate) struct ConfigureSessionResult {
81 pub attach_hint: Option<String>,
82 #[serde(default)]
85 pub snapshot: Option<BashStateSnapshot>,
86 #[serde(default)]
87 pub attached_existing: bool,
88}
89
90#[derive(Clone, Debug, Serialize, Deserialize)]
91pub(crate) struct RunActionResult {
92 pub output: Option<String>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub compact_output: Option<String>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub command_generation: Option<u64>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub execution_token: Option<crate::runtime::ShellExecutionToken>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub dropped_output_file: Option<std::path::PathBuf>,
103 #[serde(default)]
104 pub output_truncated: bool,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub state: Option<BashCommandState>,
110 pub snapshot: BashStateSnapshot,
111 pub error: Option<WireShellError>,
112}
113
114#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
115pub struct JournalRead {
116 pub output: String,
117 pub next_seq: u64,
118 pub gap: bool,
119}
120
121#[derive(Debug, Serialize, Deserialize)]
122pub(crate) struct JournalReadParams {
123 pub thread_id: String,
124 pub consumer_id: String,
125}
126
127#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
128pub struct SessionInfo {
129 pub thread_id: String,
130 pub cwd: String,
131 pub shell_pid: Option<u32>,
132 pub command_id: Option<String>,
133 pub running: bool,
134 pub background_command_ids: Vec<String>,
135 #[serde(default)]
138 pub created_at_unix_ms: Option<u64>,
139 #[serde(default)]
140 pub last_activity_unix_ms: Option<u64>,
141 #[serde(default)]
142 pub last_command_at_unix_ms: Option<u64>,
143 #[serde(default)]
144 pub ever_ran_command: bool,
145}
146
147#[derive(Debug, Serialize, Deserialize)]
148pub(crate) struct SessionParams {
149 pub thread_id: String,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub expected_generation: Option<u64>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub expected_execution: Option<crate::runtime::ShellExecutionToken>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub expected_guardian_epoch: Option<String>,
156}
157
158#[derive(Debug, Serialize, Deserialize)]
159pub(crate) struct CancelActionParams {
160 pub thread_id: String,
161 pub cancellation_key: String,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub expected_guardian_epoch: Option<String>,
164}
165
166#[derive(Debug, Default, Serialize, Deserialize)]
167pub(crate) struct PruneParams {
168 #[serde(default)]
169 pub idle_seconds: Option<u64>,
170}
171
172#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
173pub struct PruneResult {
174 pub removed_thread_ids: Vec<String>,
175 pub skipped_active_thread_ids: Vec<String>,
176 pub stale_socket_count: usize,
177 pub unreachable_guardian_count: usize,
178}
179
180#[derive(Clone, Debug, Serialize, Deserialize)]
181#[serde(tag = "kind", content = "message", rename_all = "snake_case")]
182pub(crate) enum WireShellError {
183 BashStateNotInitialized,
184 ShellInitialization(String),
185 CommandExecution(String),
186 NoActiveCommand(String),
187 BackgroundSessionNotFound(String),
188 EmptyInteractiveInput(String),
189 InteractiveTargetNotRunning(String),
190 CommandAlreadyRunning { current_command: String, duration_seconds: f64 },
191 CommandNotAllowed(String),
192 ThreadIdMismatch(String),
193 InvalidInput(String),
194 Other(String),
195}
196
197pub(crate) async fn write_json_frame<W, T>(writer: &mut W, value: &T) -> std::io::Result<()>
198where
199 W: AsyncWrite + Unpin,
200 T: Serialize,
201{
202 let payload = serde_json::to_vec(value)
203 .map_err(|error| IoError::new(ErrorKind::InvalidData, error.to_string()))?;
204 if payload.len() > MAX_FRAME_BYTES {
205 return Err(IoError::new(
206 ErrorKind::InvalidData,
207 format!("JSON-RPC frame exceeds {MAX_FRAME_BYTES} bytes"),
208 ));
209 }
210 let length = u32::try_from(payload.len())
211 .map_err(|_| IoError::new(ErrorKind::InvalidData, "JSON-RPC frame is too large"))?;
212 writer.write_all(&length.to_be_bytes()).await?;
213 writer.write_all(&payload).await?;
214 writer.flush().await
215}
216
217pub(crate) async fn read_json_frame<R, T>(reader: &mut R) -> std::io::Result<T>
218where
219 R: AsyncRead + Unpin,
220 T: DeserializeOwned,
221{
222 let mut length = [0_u8; 4];
223 reader.read_exact(&mut length).await?;
224 let length = u32::from_be_bytes(length) as usize;
225 if length == 0 || length > MAX_FRAME_BYTES {
226 return Err(IoError::new(
227 ErrorKind::InvalidData,
228 format!("invalid JSON-RPC frame length: {length}"),
229 ));
230 }
231 let mut payload = vec![0_u8; length];
232 reader.read_exact(&mut payload).await?;
233 serde_json::from_slice(&payload)
234 .map_err(|error| IoError::new(ErrorKind::InvalidData, error.to_string()))
235}
236
237#[cfg(test)]
238mod tests {
239 #![allow(clippy::expect_used)]
240
241 use super::*;
242
243 #[test]
244 fn guardian_1_4_params_default_new_options_and_do_not_forward_wait_policy() {
245 let command: BashCommand = serde_json::from_value(serde_json::json!({
246 "command": "printf compatible",
247 "wait_policy": "until_complete",
248 "thread_id": "guardian14"
249 }))
250 .expect("adapter command");
251 let mut wire = serde_json::to_value(RunActionParams {
252 snapshot: crate::state::bash_state::BashState::new().snapshot(),
253 command,
254 request_key: "request14".to_string(),
255 consumer_id: "consumer14".to_string(),
256 options: crate::runtime::ShellActionOptions::default(),
257 })
258 .expect("current params");
259 let object = wire.as_object_mut().expect("params object");
260 assert!(object.get("options").is_none());
261 assert!(object["command"].get("wait_policy").is_none());
262
263 let decoded: RunActionParams = serde_json::from_value(wire).expect("protocol 1.4 params");
264 assert_eq!(decoded.options, crate::runtime::ShellActionOptions::default());
265 }
266
267 #[test]
268 fn guardian_1_4_result_defaults_compact_and_generation_extensions() {
269 let mut wire = serde_json::to_value(RunActionResult {
270 output: Some("legacy output".to_string()),
271 compact_output: Some("compact output".to_string()),
272 command_generation: Some(7),
273 execution_token: None,
274 dropped_output_file: Some("/tmp/winx-output".into()),
275 output_truncated: false,
276 state: None,
277 snapshot: crate::state::bash_state::BashState::new().snapshot(),
278 error: None,
279 })
280 .expect("current result");
281 let object = wire.as_object_mut().expect("result object");
282 object.remove("compact_output");
283 object.remove("command_generation");
284 object.remove("execution_token");
285 object.remove("dropped_output_file");
286 object.remove("output_truncated");
287
288 let decoded: RunActionResult = serde_json::from_value(wire).expect("protocol 1.4 result");
289 assert_eq!(decoded.output.as_deref(), Some("legacy output"));
290 assert_eq!(decoded.compact_output, None);
291 assert_eq!(decoded.command_generation, None);
292 assert_eq!(decoded.execution_token, None);
293 assert_eq!(decoded.dropped_output_file, None);
294 assert!(!decoded.output_truncated);
295 }
296
297 #[test]
298 fn compact_wire_result_contains_one_output_payload() {
299 let compact = "x".repeat(1024);
300 let wire = serde_json::to_value(RunActionResult {
301 output: None,
302 compact_output: Some(compact.clone()),
303 command_generation: Some(9),
304 execution_token: None,
305 dropped_output_file: None,
306 output_truncated: false,
307 state: None,
308 snapshot: crate::state::bash_state::BashState::new().snapshot(),
309 error: None,
310 })
311 .expect("compact result");
312 assert!(wire["output"].is_null());
313 assert_eq!(wire["compact_output"], compact);
314 let encoded = serde_json::to_string(&wire).expect("wire bytes");
315 assert_eq!(encoded.matches(&compact).count(), 1, "compact payload must not be duplicated");
316 }
317}