tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Definitions

⎇ main crates/takana-agent/src/client.rs Rust · 313 lines · 10.9 KB History Who touched what raw

takana etapa 4: los 10 crates de librería y el CLI pasan a takana-* 24bcf1783 · Sergio ·

1//! `AgentClient`: cliente síncrono del bus de agente. Ver `docs/07-agent-bus.md` §3.
2//!
3//! Modelo: una conexión = un thread del que llama. El reader thread interno demultiplexa
4//! eventos por tipo: respuestas correlativas a un comando se devuelven al caller via
5//! `recv_*_event_blocking`; eventos asíncronos (`Modified`, `Crashed`) se acumulan en una
6//! cola que el caller drena con `drain_async()`.
7//!
8//! No hay correlation-id en el protocolo todavía (el SDD 07 no lo exige), así que la
9//! correlación se hace por **tipo**: tras un `Compile` esperamos `BuildReady|BuildFailed`,
10//! cualquier otra cosa que no sea `Modified`/`Crashed` se reporta como protocol error.
11
12use std::collections::VecDeque;
13use std::io::{BufRead, BufReader, Write};
14use std::os::unix::net::UnixStream;
15use std::path::Path;
16use std::sync::{Arc, Condvar, Mutex};
17use std::thread;
18use std::time::{Duration, Instant};
19
20use takana_core::proto::{Cap, Command, Event, Peer, RecipeInline};
21
22#[derive(Debug, thiserror::Error)]
23pub enum ClientError {
24    #[error("io: {0}")]
25    Io(#[from] std::io::Error),
26    #[error("json: {0}")]
27    Json(#[from] serde_json::Error),
28    #[error("protocolo: {0}")]
29    Protocol(String),
30    #[error("timeout esperando {0}")]
31    Timeout(&'static str),
32    #[error("el bus devolvió error code={code} msg={msg}")]
33    Bus { code: String, msg: String },
34    #[error("la conexión se cerró")]
35    Closed,
36}
37
38pub type Result<T> = std::result::Result<T, ClientError>;
39
40#[derive(Debug, Clone)]
41pub struct Welcome {
42    pub ver: u32,
43    pub caps: Vec<Cap>,
44    pub peer: Peer,
45}
46
47/// Resultado de `compile`: el hash del artefacto sellado más el veredicto de la evidencia (H1c).
48/// `verdict` es `None` cuando la receta no declaró evidencia.
49#[derive(Debug, Clone)]
50pub struct CompileOutcome {
51    pub artifact: String,
52    pub verdict: Option<takana_core::proto::EvidenceVerdict>,
53}
54
55/// Estado compartido entre el reader thread y el caller.
56struct Inbox {
57    /// Eventos asíncronos (Modified/Crashed) en orden de llegada.
58    async_events: VecDeque<Event>,
59    /// Próximo evento "respuesta" al último comando. Sólo cabe uno a la vez en este
60    /// cliente síncrono (el caller envía + espera antes del siguiente).
61    pending: Option<Event>,
62    closed: bool,
63}
64
65#[derive(Clone)]
66struct Shared {
67    inbox: Arc<Mutex<Inbox>>,
68    cv: Arc<Condvar>,
69}
70
71pub struct AgentClient {
72    writer: UnixStream,
73    shared: Shared,
74    pub welcome: Welcome,
75}
76
77impl AgentClient {
78    /// Conecta al socket, hace handshake `Hello` y arranca el reader thread. Devuelve un
79    /// cliente listo para enviar comandos.
80    pub fn connect(sock_path: &Path) -> Result<Self> {
81        Self::connect_with_client_name(sock_path, "hammer-agent")
82    }
83
84    pub fn connect_with_client_name(sock_path: &Path, client: &str) -> Result<Self> {
85        let stream = UnixStream::connect(sock_path)?;
86        let reader_stream = stream.try_clone()?;
87        let writer = stream;
88
89        let shared = Shared {
90            inbox: Arc::new(Mutex::new(Inbox {
91                async_events: VecDeque::new(),
92                pending: None,
93                closed: false,
94            })),
95            cv: Arc::new(Condvar::new()),
96        };
97        // Reader thread: drena el socket línea a línea hasta EOF.
98        let s_for_reader = shared.clone();
99        thread::Builder::new()
100            .name("agent-client-reader".into())
101            .spawn(move || reader_loop(reader_stream, s_for_reader))
102            .ok();
103
104        let mut me = Self {
105            writer,
106            shared,
107            welcome: Welcome {
108                ver: 0,
109                caps: vec![],
110                peer: Peer { uid: 0, gid: 0, pid: 0 },
111            },
112        };
113        me.send(&Command::Hello {
114            ver: takana_core::proto::PROTOCOL_VERSION,
115            client: client.to_string(),
116        })?;
117        let ev = me.recv_pending(Duration::from_secs(5))?;
118        match ev {
119            Event::Welcome { ver, caps, peer } => {
120                me.welcome = Welcome { ver, caps, peer };
121            }
122            Event::Error { code, msg } => return Err(ClientError::Bus { code, msg }),
123            other => {
124                return Err(ClientError::Protocol(format!(
125                    "esperaba Welcome, llegó {other:?}"
126                )));
127            }
128        }
129        Ok(me)
130    }
131
132    /// Bloquea hasta `BuildReady` o `BuildFailed`. `timeout` es el wall-clock máximo —
133    /// el lab puede tardar varios minutos, así que el default debería ser generoso.
134    pub fn compile(&mut self, recipe: RecipeInline, timeout: Duration) -> Result<CompileOutcome> {
135        self.send(&Command::Compile { recipe })?;
136        match self.recv_pending(timeout)? {
137            Event::BuildReady { artifact, verdict, .. } => Ok(CompileOutcome { artifact, verdict }),
138            Event::BuildFailed { reason, .. } => Err(ClientError::Bus {
139                code: "build_failed".into(),
140                msg: reason,
141            }),
142            Event::Error { code, msg } => Err(ClientError::Bus { code, msg }),
143            other => Err(ClientError::Protocol(format!(
144                "esperaba BuildReady|BuildFailed, llegó {other:?}"
145            ))),
146        }
147    }
148
149    pub fn inject(
150        &mut self,
151        artifact: &str,
152        target: &str,
153        overlay: Option<&str>,
154        timeout: Duration,
155    ) -> Result<usize> {
156        self.send(&Command::Inject {
157            artifact: artifact.to_string(),
158            target: target.to_string(),
159            overlay: overlay.map(String::from),
160        })?;
161        match self.recv_pending(timeout)? {
162            Event::Injected { files, .. } => Ok(files),
163            Event::Error { code, msg } => Err(ClientError::Bus { code, msg }),
164            other => Err(ClientError::Protocol(format!("esperaba Injected, llegó {other:?}"))),
165        }
166    }
167
168    pub fn query_file(&mut self, path: &str, timeout: Duration) -> Result<serde_json::Value> {
169        self.send(&Command::Query {
170            what: "file".into(),
171            path: Some(path.to_string()),
172            name: None,
173            expr: None,
174        })?;
175        match self.recv_pending(timeout)? {
176            Event::QueryResult { value, .. } => Ok(value),
177            Event::Error { code, msg } => Err(ClientError::Bus { code, msg }),
178            other => Err(ClientError::Protocol(format!(
179                "esperaba QueryResult, llegó {other:?}"
180            ))),
181        }
182    }
183
184    /// Evalúa una expresión del mini-lenguaje (`bin:grep`, `pin:musl`, …) en el daemon.
185    /// El JSON devuelto sigue el schema de `takana_core::query::eval`.
186    pub fn query_expr(&mut self, expr: &str, timeout: Duration) -> Result<serde_json::Value> {
187        self.send(&Command::Query {
188            what: "expr".into(),
189            path: None,
190            name: None,
191            expr: Some(expr.to_string()),
192        })?;
193        match self.recv_pending(timeout)? {
194            Event::QueryResult { value, .. } => Ok(value),
195            Event::Error { code, msg } => Err(ClientError::Bus { code, msg }),
196            other => Err(ClientError::Protocol(format!(
197                "esperaba QueryResult, llegó {other:?}"
198            ))),
199        }
200    }
201
202    pub fn init(&mut self, cmd: &str, timeout: Duration) -> Result<()> {
203        self.send(&Command::Init { cmd: cmd.to_string() })?;
204        match self.recv_pending(timeout)? {
205            Event::InitAck { .. } => Ok(()),
206            Event::Error { code, msg } => Err(ClientError::Bus { code, msg }),
207            other => Err(ClientError::Protocol(format!("esperaba InitAck, llegó {other:?}"))),
208        }
209    }
210
211    /// Vacía la cola de eventos asíncronos. No bloquea.
212    pub fn drain_async(&self) -> Vec<Event> {
213        let Ok(mut g) = self.shared.inbox.lock() else { return vec![] };
214        std::mem::take(&mut g.async_events).into_iter().collect()
215    }
216
217    /// Espera el próximo evento asíncrono (típicamente para verificar que un `Modified`
218    /// específico haya llegado tras una operación).
219    pub fn next_async(&self, timeout: Duration) -> Result<Event> {
220        let g = self.shared.inbox.lock().unwrap();
221        let (mut g, _) = self
222            .shared
223            .cv
224            .wait_timeout_while(g, timeout, |i| {
225                !i.closed && i.async_events.is_empty()
226            })
227            .unwrap();
228        if let Some(ev) = g.async_events.pop_front() {
229            Ok(ev)
230        } else if g.closed {
231            Err(ClientError::Closed)
232        } else {
233            Err(ClientError::Timeout("async event"))
234        }
235    }
236
237    fn send(&mut self, cmd: &Command) -> Result<()> {
238        let line = serde_json::to_string(cmd)?;
239        self.writer.write_all(line.as_bytes())?;
240        self.writer.write_all(b"\n")?;
241        self.writer.flush()?;
242        Ok(())
243    }
244
245    fn recv_pending(&self, timeout: Duration) -> Result<Event> {
246        let deadline = Instant::now() + timeout;
247        let g = self.shared.inbox.lock().unwrap();
248        let (mut g, _) = self
249            .shared
250            .cv
251            .wait_timeout_while(g, timeout, |i| !i.closed && i.pending.is_none())
252            .unwrap();
253        if let Some(ev) = g.pending.take() {
254            return Ok(ev);
255        }
256        if g.closed {
257            return Err(ClientError::Closed);
258        }
259        // Si el wait expiró exactamente en el momento de un evento, vuelvo a comprobar.
260        if Instant::now() >= deadline {
261            return Err(ClientError::Timeout("response"));
262        }
263        Err(ClientError::Timeout("response"))
264    }
265}
266
267fn reader_loop(stream: UnixStream, shared: Shared) {
268    let reader = BufReader::new(stream);
269    for line in reader.lines() {
270        let line = match line {
271            Ok(l) => l,
272            Err(e) => {
273                tracing::warn!(error = %e, "agent-client: read error, cerrando");
274                break;
275            }
276        };
277        if line.trim().is_empty() {
278            continue;
279        }
280        let ev: Event = match serde_json::from_str(&line) {
281            Ok(e) => e,
282            Err(e) => {
283                tracing::warn!(error = %e, line = %line, "agent-client: línea no parseable");
284                continue;
285            }
286        };
287        let is_async = matches!(
288            &ev,
289            Event::Modified { .. } | Event::Crashed { .. }
290        );
291        let mut g = match shared.inbox.lock() {
292            Ok(g) => g,
293            Err(_) => break,
294        };
295        if is_async {
296            g.async_events.push_back(ev);
297        } else {
298            // Si ya hay un pending sin recoger, eso es un bug del caller (envió dos
299            // comandos antes de leer la primera respuesta). Lo logueamos y reemplazamos.
300            if g.pending.is_some() {
301                tracing::warn!(
302                    "agent-client: pending ya tenía respuesta; el caller no la recogió"
303                );
304            }
305            g.pending = Some(ev);
306        }
307        shared.cv.notify_all();
308    }
309    if let Ok(mut g) = shared.inbox.lock() {
310        g.closed = true;
311        shared.cv.notify_all();
312    }
313}