Skip to main content

winx_code_agent/daemon/
protocol.rs

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::types::BashCommand;
9
10pub const PROTOCOL_MAJOR: u16 = 1;
11pub const PROTOCOL_MINOR: u16 = 3;
12pub const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
13
14#[derive(Debug, Serialize, Deserialize)]
15pub(crate) struct RpcRequest {
16    pub jsonrpc: String,
17    pub id: u64,
18    pub method: String,
19    pub params: serde_json::Value,
20}
21
22#[derive(Debug, Serialize, Deserialize)]
23pub(crate) struct RpcResponse {
24    pub jsonrpc: String,
25    pub id: u64,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub result: Option<serde_json::Value>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub error: Option<RpcError>,
30}
31
32#[derive(Debug, Serialize, Deserialize)]
33pub(crate) struct RpcError {
34    pub code: i32,
35    pub message: String,
36}
37
38#[derive(Clone, Debug, Serialize, Deserialize)]
39pub struct HelloResult {
40    pub protocol_major: u16,
41    pub protocol_minor: u16,
42    pub capabilities: Vec<String>,
43    pub max_frame_bytes: usize,
44    pub daemon_epoch: String,
45    pub daemon_pid: u32,
46}
47
48#[derive(Debug, Serialize, Deserialize)]
49pub(crate) struct RunActionParams {
50    pub snapshot: BashStateSnapshot,
51    pub command: BashCommand,
52    pub request_key: String,
53    #[serde(default)]
54    pub consumer_id: String,
55}
56
57#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub(crate) enum ConfigureSessionTransition {
60    FirstCall,
61    ModeChange,
62    Reset,
63    WorkspaceChange,
64}
65
66#[derive(Debug, Serialize, Deserialize)]
67pub(crate) struct ConfigureSessionParams {
68    pub snapshot: BashStateSnapshot,
69    pub transition: ConfigureSessionTransition,
70}
71
72#[derive(Clone, Debug, Serialize, Deserialize)]
73pub(crate) struct ConfigureSessionResult {
74    pub attach_hint: Option<String>,
75    /// Existing guardians return their authoritative state so a restarted adapter
76    /// can attach without resetting the PTY. Absent for protocol-1.2 guardians.
77    #[serde(default)]
78    pub snapshot: Option<BashStateSnapshot>,
79    #[serde(default)]
80    pub attached_existing: bool,
81}
82
83#[derive(Clone, Debug, Serialize, Deserialize)]
84pub(crate) struct RunActionResult {
85    pub output: Option<String>,
86    pub snapshot: BashStateSnapshot,
87    pub error: Option<WireShellError>,
88}
89
90#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
91pub struct JournalRead {
92    pub output: String,
93    pub next_seq: u64,
94    pub gap: bool,
95}
96
97#[derive(Debug, Serialize, Deserialize)]
98pub(crate) struct JournalReadParams {
99    pub thread_id: String,
100    pub consumer_id: String,
101}
102
103#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
104pub struct SessionInfo {
105    pub thread_id: String,
106    pub cwd: String,
107    pub shell_pid: Option<u32>,
108    pub command_id: Option<String>,
109    pub running: bool,
110    pub background_command_ids: Vec<String>,
111    /// Guardian-owned lifecycle clock. Optional for compatibility with guardians
112    /// created by releases before protocol 1.3.
113    #[serde(default)]
114    pub created_at_unix_ms: Option<u64>,
115    #[serde(default)]
116    pub last_activity_unix_ms: Option<u64>,
117    #[serde(default)]
118    pub last_command_at_unix_ms: Option<u64>,
119    #[serde(default)]
120    pub ever_ran_command: bool,
121}
122
123#[derive(Debug, Serialize, Deserialize)]
124pub(crate) struct SessionParams {
125    pub thread_id: String,
126}
127
128#[derive(Debug, Default, Serialize, Deserialize)]
129pub(crate) struct PruneParams {
130    #[serde(default)]
131    pub idle_seconds: Option<u64>,
132}
133
134#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
135pub struct PruneResult {
136    pub removed_thread_ids: Vec<String>,
137    pub skipped_active_thread_ids: Vec<String>,
138    pub stale_socket_count: usize,
139    pub unreachable_guardian_count: usize,
140}
141
142#[derive(Clone, Debug, Serialize, Deserialize)]
143#[serde(tag = "kind", content = "message", rename_all = "snake_case")]
144pub(crate) enum WireShellError {
145    BashStateNotInitialized,
146    ShellInitialization(String),
147    CommandExecution(String),
148    NoActiveCommand(String),
149    BackgroundSessionNotFound(String),
150    EmptyInteractiveInput(String),
151    InteractiveTargetNotRunning(String),
152    CommandAlreadyRunning { current_command: String, duration_seconds: f64 },
153    CommandNotAllowed(String),
154    ThreadIdMismatch(String),
155    InvalidInput(String),
156    Other(String),
157}
158
159pub(crate) async fn write_json_frame<W, T>(writer: &mut W, value: &T) -> std::io::Result<()>
160where
161    W: AsyncWrite + Unpin,
162    T: Serialize,
163{
164    let payload = serde_json::to_vec(value)
165        .map_err(|error| IoError::new(ErrorKind::InvalidData, error.to_string()))?;
166    if payload.len() > MAX_FRAME_BYTES {
167        return Err(IoError::new(
168            ErrorKind::InvalidData,
169            format!("JSON-RPC frame exceeds {MAX_FRAME_BYTES} bytes"),
170        ));
171    }
172    let length = u32::try_from(payload.len())
173        .map_err(|_| IoError::new(ErrorKind::InvalidData, "JSON-RPC frame is too large"))?;
174    writer.write_all(&length.to_be_bytes()).await?;
175    writer.write_all(&payload).await?;
176    writer.flush().await
177}
178
179pub(crate) async fn read_json_frame<R, T>(reader: &mut R) -> std::io::Result<T>
180where
181    R: AsyncRead + Unpin,
182    T: DeserializeOwned,
183{
184    let mut length = [0_u8; 4];
185    reader.read_exact(&mut length).await?;
186    let length = u32::from_be_bytes(length) as usize;
187    if length == 0 || length > MAX_FRAME_BYTES {
188        return Err(IoError::new(
189            ErrorKind::InvalidData,
190            format!("invalid JSON-RPC frame length: {length}"),
191        ));
192    }
193    let mut payload = vec![0_u8; length];
194    reader.read_exact(&mut payload).await?;
195    serde_json::from_slice(&payload)
196        .map_err(|error| IoError::new(ErrorKind::InvalidData, error.to_string()))
197}