tawasuyu

tawasuyu/brahman

Rust 212TOML 64JSON 13HTML 12666 Definiciones

⎇ 36dac00c8dd8d39d0306a4804c5432b9ef0c5935 crates/modules/shipote/shipote-protocol/src/lib.rs Rust · 366 líneas · 9.7 KB Historia Quién tocó qué crudo

feat(shipote): data plane + DAG fan-in/out + stats + lifecycle (fases F-I) 36dac00c8 · sergio ·

1//! `shipote-protocol` — wire daemon ↔ cliente (cli/gui).
2//!
3//! Framing: u32 BE length-prefix + payload postcard. Mismo patrón que
4//! `ente-bus`/`brahman-handshake` para que clientes existentes compartan
5//! reader/writer helpers si quieren.
6
7#![forbid(unsafe_code)]
8
9use serde::{Deserialize, Serialize};
10use shipote_card::{PipelineSpec, WorkspaceId, WorkspaceSpec};
11use std::path::PathBuf;
12use thiserror::Error;
13use tokio::io::{AsyncReadExt, AsyncWriteExt};
14use tokio::net::UnixStream;
15use ulid::Ulid;
16
17pub const DEFAULT_SOCK_NAME: &str = "shipote.sock";
18pub const MAX_FRAME: usize = 1 << 20;
19
20fn default_grace_ms() -> u64 {
21    1000
22}
23
24// =====================================================================
25// Mensajes
26// =====================================================================
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub enum Request {
30    /// Health-check.
31    Ping,
32
33    /// Crear un workspace nuevo.
34    WorkspaceCreate { spec: WorkspaceSpec },
35
36    /// Listar todos los workspaces vivos.
37    WorkspaceList,
38
39    /// Detener un workspace y reapear sus comandos. `grace_ms`: tiempo
40    /// que se espera tras SIGTERM antes de SIGKILL. 0 = SIGKILL inmediato.
41    WorkspaceStop {
42        id: WorkspaceId,
43        #[serde(default = "default_grace_ms")]
44        grace_ms: u64,
45    },
46
47    /// Ejecutar un comando one-shot dentro de un workspace existente.
48    Run {
49        workspace: WorkspaceId,
50        exec: String,
51        argv: Vec<String>,
52        envp: Vec<(String, String)>,
53    },
54
55    /// Lanzar un Pipeline completo dentro de un workspace.
56    PipelineRun {
57        spec: PipelineSpec,
58        /// Si `true`, el daemon interpone un tap entre productor y
59        /// consumidor de cada FlowEdge, sampleando los primeros bytes
60        /// y discerniendo el TypeRef.
61        tap: bool,
62        /// Variables para sustitución `${KEY}` en strings del spec
63        /// antes de spawn (templating).
64        #[serde(default)]
65        vars: std::collections::BTreeMap<String, String>,
66    },
67
68    /// Discernir un buffer ad-hoc (sin workspace). Útil para `shipote discern <file>`.
69    Discern { sample: Vec<u8>, hint_path: Option<PathBuf> },
70
71    /// Capacidades runtime del kernel/proceso del daemon.
72    Capabilities,
73
74    /// Listar comandos vivos+pasados de un workspace.
75    CommandList { workspace: shipote_card::WorkspaceId },
76
77    /// Tail del log capturado para un comando.
78    CommandLogs {
79        workspace: shipote_card::WorkspaceId,
80        command: Ulid,
81        tail_bytes: usize,
82        /// "stdout" | "stderr" | "both" (default "both" si vacío).
83        stream: String,
84    },
85
86    /// Guardar (o reemplazar) un PipelineSpec bajo un nombre.
87    PipelineSave { name: String, spec: PipelineSpec },
88
89    /// Listar nombres de pipelines guardados.
90    PipelineSavedList,
91
92    /// Eliminar un pipeline guardado.
93    PipelineDrop { name: String },
94
95    /// Ejecutar un pipeline guardado.
96    PipelineRunSaved {
97        name: String,
98        tap: bool,
99        #[serde(default)]
100        vars: std::collections::BTreeMap<String, String>,
101    },
102
103    /// Resource accounting de un workspace.
104    WorkspaceStats { workspace: shipote_card::WorkspaceId },
105
106    /// Detener selectivamente los comandos de un pipeline (no el workspace
107    /// entero). `grace_ms`: SIGTERM → wait → SIGKILL.
108    PipelineStop {
109        pipeline: Ulid,
110        #[serde(default = "default_grace_ms")]
111        grace_ms: u64,
112    },
113
114    /// Listar pipelines activos con sus flow channels (data plane).
115    FlowList,
116
117    /// Cerrar el data plane de un pipeline (drop sockets + canales).
118    FlowDrop { pipeline: Ulid },
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub enum Response {
123    Pong,
124
125    WorkspaceCreated {
126        id: WorkspaceId,
127        warnings: Vec<String>,
128    },
129
130    WorkspaceList {
131        items: Vec<WorkspaceSummary>,
132    },
133
134    WorkspaceStopped {
135        id: WorkspaceId,
136        reaped: u32,
137    },
138
139    RunStarted {
140        workspace: WorkspaceId,
141        command_id: Ulid,
142        pid: i32,
143    },
144
145    PipelineStarted {
146        pipeline: Ulid,
147        command_pids: Vec<(String, i32)>,
148        /// Discernments por edge cuando tap=true. Vacío sin tap.
149        edges: Vec<EdgeDiscernmentInfo>,
150    },
151
152    Discernment {
153        ty: String,
154        confidence: f32,
155        mime: Option<String>,
156        lens: Option<String>,
157    },
158
159    Capabilities {
160        kernel_version: (u32, u32, u32),
161        user_ns: String,
162        cgroup_v2: String,
163        cgroup_delegated: bool,
164        has_cap_sys_admin: bool,
165    },
166
167    CommandList {
168        items: Vec<CommandInfo>,
169    },
170
171    CommandLogs {
172        bytes: Vec<u8>,
173    },
174
175    PipelineSaved {
176        name: String,
177    },
178
179    PipelineSavedList {
180        names: Vec<String>,
181    },
182
183    PipelineDropped {
184        name: String,
185        existed: bool,
186    },
187
188    PipelineStopped {
189        pipeline: Ulid,
190        reaped: u32,
191    },
192
193    WorkspaceStats {
194        info: WorkspaceStatsInfo,
195    },
196
197    FlowList {
198        items: Vec<FlowInfo>,
199    },
200
201    FlowDropped {
202        pipeline: Ulid,
203        existed: bool,
204    },
205
206    Error {
207        message: String,
208    },
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct WorkspaceStatsInfo {
213    pub commands_alive: u32,
214    pub commands_total: u32,
215    pub rss_bytes: Option<u64>,
216    #[serde(default)]
217    pub rss_peak_bytes: Option<u64>,
218    pub cpu_usec: Option<u64>,
219    pub source: String,
220    pub uptime_ms: u64,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct FlowInfo {
225    pub pipeline: Ulid,
226    pub sockets: Vec<PathBuf>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct CommandInfo {
231    pub id: Ulid,
232    pub label: String,
233    pub pid: i32,
234    pub alive: bool,
235    pub exit_status: Option<i32>,
236    pub log_bytes: u64,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct EdgeDiscernmentInfo {
241    pub from_label: String,
242    pub from_output: String,
243    pub to_label: String,
244    pub to_input: String,
245    /// `Some(ty)` si el discerner detectó algo. `None` si no hubo data
246    /// suficiente o no matcheó ningún discerner.
247    pub ty: Option<String>,
248    pub mime: Option<String>,
249    pub lens: Option<String>,
250    pub confidence: f32,
251    /// Path del Unix socket donde otros módulos pueden suscribirse a los
252    /// bytes replicados de este edge (data plane). `None` si tap=false.
253    pub flow_socket: Option<PathBuf>,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct WorkspaceSummary {
258    pub id: WorkspaceId,
259    pub label: String,
260    pub commands: u32,
261    pub uptime_ms: u64,
262}
263
264// =====================================================================
265// Errores
266// =====================================================================
267
268#[derive(Debug, Error)]
269pub enum ProtocolError {
270    #[error("frame oversize: {0} bytes (max {MAX_FRAME})")]
271    FrameOversize(usize),
272    #[error("io: {0}")]
273    Io(#[from] std::io::Error),
274    #[error("postcard: {0}")]
275    Postcard(#[from] postcard::Error),
276    #[error("connection closed")]
277    Closed,
278}
279
280// =====================================================================
281// Framing helpers
282// =====================================================================
283
284pub async fn write_frame<T: Serialize>(stream: &mut UnixStream, msg: &T) -> Result<(), ProtocolError> {
285    let bytes = postcard::to_allocvec(msg)?;
286    if bytes.len() > MAX_FRAME {
287        return Err(ProtocolError::FrameOversize(bytes.len()));
288    }
289    let len = (bytes.len() as u32).to_be_bytes();
290    stream.write_all(&len).await?;
291    stream.write_all(&bytes).await?;
292    stream.flush().await?;
293    Ok(())
294}
295
296pub async fn read_frame<T: for<'de> Deserialize<'de>>(
297    stream: &mut UnixStream,
298) -> Result<T, ProtocolError> {
299    let mut len_buf = [0u8; 4];
300    stream.read_exact(&mut len_buf).await.map_err(|e| {
301        if e.kind() == std::io::ErrorKind::UnexpectedEof {
302            ProtocolError::Closed
303        } else {
304            ProtocolError::Io(e)
305        }
306    })?;
307    let len = u32::from_be_bytes(len_buf) as usize;
308    if len > MAX_FRAME {
309        return Err(ProtocolError::FrameOversize(len));
310    }
311    let mut buf = vec![0u8; len];
312    stream.read_exact(&mut buf).await?;
313    Ok(postcard::from_bytes(&buf)?)
314}
315
316/// Path canónico del socket del daemon: `$XDG_RUNTIME_DIR/shipote.sock`,
317/// fallback `/run/user/$UID/shipote.sock`, fallback `/tmp/shipote-$UID.sock`.
318pub fn default_socket_path() -> PathBuf {
319    if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
320        return PathBuf::from(xdg).join(DEFAULT_SOCK_NAME);
321    }
322    let uid = nix::unistd::getuid().as_raw();
323    let p = PathBuf::from(format!("/run/user/{uid}"));
324    if p.exists() {
325        return p.join(DEFAULT_SOCK_NAME);
326    }
327    PathBuf::from(format!("/tmp/shipote-{uid}.sock"))
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn ping_roundtrip() {
336        let bytes = postcard::to_allocvec(&Request::Ping).unwrap();
337        let back: Request = postcard::from_bytes(&bytes).unwrap();
338        assert!(matches!(back, Request::Ping));
339    }
340
341    #[test]
342    fn workspace_create_roundtrip() {
343        let req = Request::WorkspaceCreate {
344            spec: WorkspaceSpec {
345                label: "demo".into(),
346                soma: Default::default(),
347                permissions: Default::default(),
348                ttl: None,
349                flow_dirs: vec![],
350                on_exit: shipote_card::ExitPolicy::Reap,
351            },
352        };
353        let bytes = postcard::to_allocvec(&req).unwrap();
354        let back: Request = postcard::from_bytes(&bytes).unwrap();
355        match back {
356            Request::WorkspaceCreate { spec } => assert_eq!(spec.label, "demo"),
357            _ => panic!("wrong variant"),
358        }
359    }
360
361    #[test]
362    fn default_socket_path_uses_runtime_dir() {
363        let p = default_socket_path();
364        assert!(p.to_string_lossy().ends_with("shipote.sock"));
365    }
366}