gioser
⎇ f4eb7dd94404c2fadd8099c61ca11d2860bcff7a crates/ente-brain/examples/brainctl.rs Rust · 234 siq'ikuna · 9.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

Capability quotas, CAS gc, patrones burst/silence, snapshot incremental f4eb7dd94 · Sergio ·

1//! brainctl: cliente CLI del introspect API.
2//!
3//! Uso:
4//!   cargo run --example brainctl -p ente-brain -- list-rules
5//!   cargo run --example brainctl -p ente-brain -- entropy
6//!   cargo run --example brainctl -p ente-brain -- top 10
7//!   cargo run --example brainctl -p ente-brain -- crystals
8//!   cargo run --example brainctl -p ente-brain -- crystal-kcl 0
9//!
10//! Path del socket: $ENTE_BRAIN_SOCK o $XDG_RUNTIME_DIR/ente-brain.sock
11
12use ente_brain::introspect::{call, IntrospectRequest, IntrospectResponse};
13use std::path::{Path, PathBuf};
14use tokio::io::{AsyncReadExt, AsyncWriteExt};
15use tokio::net::UnixStream;
16
17fn socket_path() -> PathBuf {
18    if let Ok(p) = std::env::var("ENTE_BRAIN_SOCK") {
19        return p.into();
20    }
21    let runtime = std::env::var("XDG_RUNTIME_DIR")
22        .unwrap_or_else(|_| std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into()));
23    format!("{runtime}/ente-brain.sock").into()
24}
25
26#[tokio::main(flavor = "current_thread")]
27async fn main() -> anyhow::Result<()> {
28    let args: Vec<String> = std::env::args().collect();
29    let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("entropy");
30
31    // Comando especial: streaming. Mantiene la conn abierta y lee frames
32    // hasta Ctrl-C o EOF del servidor.
33    if cmd == "stream-audit" || cmd == "stream" {
34        return run_stream_audit(socket_path()).await;
35    }
36
37    let req = match cmd {
38        "list-rules" | "rules" => IntrospectRequest::ListRules,
39        "entropy" => IntrospectRequest::EntropySnapshot,
40        "top" => {
41            let n: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(10);
42            IntrospectRequest::TopCorrelations { n }
43        }
44        "crystals" => IntrospectRequest::Crystals,
45        "crystal-kcl" => {
46            let i: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
47            IntrospectRequest::CrystalKcl { index: i }
48        }
49        "promote" => {
50            let i: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
51            IntrospectRequest::PromoteCrystal { index: i }
52        }
53        "remove" => {
54            let id_s = args.get(2).ok_or_else(|| anyhow::anyhow!("se requiere <ulid>"))?;
55            let id: ulid::Ulid = id_s.parse()?;
56            IntrospectRequest::RemoveRule { id }
57        }
58        "audit" => {
59            let limit: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(20);
60            IntrospectRequest::ListAudit { limit }
61        }
62        "flush-audit" => IntrospectRequest::FlushAudit,
63        "audit-verify" | "verify" => IntrospectRequest::VerifyAudit,
64        "replay" => IntrospectRequest::ReplayAudit,
65        "gc-cas" => IntrospectRequest::GcCas { extra_roots: Vec::new() },
66        "patterns" => IntrospectRequest::PatternCrystals,
67        "reload" => {
68            let path = args.get(2).cloned();
69            IntrospectRequest::ReloadRules { path }
70        }
71        other => {
72            eprintln!("subcomando desconocido: {other}");
73            eprintln!("válidos: list-rules | entropy | top <n> | crystals | crystal-kcl <i> | promote <i> | remove <ulid> | audit <limit> | flush-audit | reload [path]");
74            std::process::exit(2);
75        }
76    };
77
78    let path = socket_path();
79    let resp = call(&path, req).await?;
80    print_response(&resp);
81    Ok(())
82}
83
84fn print_response(r: &IntrospectResponse) {
85    match r {
86        IntrospectResponse::Rules(rs) => {
87            println!("{} reglas vivas:", rs.len());
88            for r in rs {
89                println!("  {} prio={} kind={} actions={} wildcard={}",
90                    r.id, r.priority, r.event_kind_tag, r.action_count, r.scope_wildcard);
91            }
92        }
93        IntrospectResponse::Rule(rule) => match rule {
94            Some(r) => println!("{r:#?}"),
95            None => println!("regla no encontrada"),
96        },
97        IntrospectResponse::Entropy { value_bits, sample_size, distinct_kinds, window_full } => {
98            println!("Shannon entropy : {value_bits:.4} bits");
99            println!("Sample size     : {sample_size}");
100            println!("Distinct kinds  : {distinct_kinds}");
101            println!("Window full     : {window_full}");
102        }
103        IntrospectResponse::Correlations(entries) => {
104            println!("{} pares (top, ordenado por co-ocurrencia):", entries.len());
105            for e in entries {
106                println!("  n={:>4}  P(b|a)={:.3}  PMI={:>6.3}b  {} → {}",
107                    e.joint_count, e.conditional_prob, e.pmi_bits, e.a, e.b);
108            }
109        }
110        IntrospectResponse::Crystals(cs) => {
111            println!("{} cristales detectados:", cs.len());
112            for (i, c) in cs.iter().enumerate() {
113                println!("  [{i}] {:?} → {:?}  P={:.3}  PMI={:.3}b  n={}",
114                    c.antecedent, c.consequent, c.conditional_prob, c.pmi, c.support);
115            }
116        }
117        IntrospectResponse::Kcl(s) => println!("{s}"),
118        IntrospectResponse::Promoted { rule_id, kcl_snippet } => {
119            println!("regla creada: {rule_id}");
120            println!("--- KCL para auditoría / persistencia ---");
121            println!("{kcl_snippet}");
122        }
123        IntrospectResponse::Removed(was_present) => {
124            if *was_present { println!("regla eliminada"); }
125            else { println!("regla no encontrada"); }
126        }
127        IntrospectResponse::AuditEntries(entries) => {
128            println!("{} entries de audit log:", entries.len());
129            for e in entries {
130                let prev = e.prev_sha.map(hex_short).unwrap_or_else(|| "—".into());
131                let sha = hex_short(e.sha);
132                println!("  seq={:>4} t={} prev={} sha={}  {:?}",
133                    e.seq, e.timestamp_ms, prev, sha, e.action);
134            }
135        }
136        IntrospectResponse::Flushed { written, head_sha, total_flushed } => {
137            println!("flushed: {written} entries esta pasada, total acumulado: {total_flushed}");
138            if let Some(sha) = head_sha {
139                println!("head sha: {}", hex_long(*sha));
140            }
141        }
142        IntrospectResponse::Reloaded { count } => {
143            println!("reload OK: {count} reglas activas tras reload");
144        }
145        IntrospectResponse::Replayed(rep) => {
146            if let Some(e) = &rep.error {
147                println!("✗ replay falló: {e}");
148            } else {
149                println!("✓ replay completo — {} actions aplicadas, {} reglas finales",
150                    rep.applied, rep.final_rule_count);
151            }
152        }
153        IntrospectResponse::AuditVerified(rep) => {
154            if let Some(seq) = rep.broken_at_seq {
155                println!("✗ verificación FALLÓ tras seq={seq}");
156                if let Some(e) = &rep.error { println!("  motivo: {e}"); }
157                println!("  entries verificadas: {}", rep.verified);
158            } else {
159                println!("✓ chain verificada — {} entries íntegras", rep.verified);
160                if let Some(g) = rep.genesis_sha { println!("  genesis: {}", hex_long(g)); }
161            }
162        }
163        IntrospectResponse::Patterns(ps) => {
164            println!("{} cristales pattern detectados:", ps.len());
165            for p in ps {
166                match p {
167                    ente_brain::crystallize::PatternCrystal::Burst { kind, count, frequency_per_sec } => {
168                        println!("  burst: {kind:?}  count={count}  freq={frequency_per_sec:.2} Hz");
169                    }
170                    ente_brain::crystallize::PatternCrystal::Silence { kind, last_count, since_secs } => {
171                        println!("  silence: {kind:?}  last_count={last_count}  ausente={since_secs:.1}s");
172                    }
173                }
174            }
175        }
176        IntrospectResponse::GcResult { deleted, freed_bytes } => {
177            println!("CAS gc: {deleted} blobs eliminados, {freed_bytes} bytes liberados");
178        }
179        IntrospectResponse::AuditStreamFrame(_) => {
180            // En modo request/response no debería llegar; solo aparece en
181            // run_stream_audit. Si llega aquí es un bug del servidor.
182            eprintln!("frame de stream recibido fuera de stream-audit (bug)");
183        }
184        IntrospectResponse::Error(e) => eprintln!("error: {e}"),
185    }
186}
187
188fn hex_short(sha: [u8; 32]) -> String {
189    sha[..4].iter().map(|b| format!("{:02x}", b)).collect::<String>() + ".."
190}
191
192fn hex_long(sha: [u8; 32]) -> String {
193    sha.iter().map(|b| format!("{:02x}", b)).collect()
194}
195
196async fn run_stream_audit(path: PathBuf) -> anyhow::Result<()> {
197    let mut stream = UnixStream::connect(&path).await?;
198    let req = IntrospectRequest::StreamAudit;
199    let buf = bincode::serialize(&req)?;
200    stream.write_u32(buf.len() as u32).await?;
201    stream.write_all(&buf).await?;
202    eprintln!("audit stream conectado a {} — Ctrl-C para salir", path.display());
203
204    loop {
205        let mut len_buf = [0u8; 4];
206        if stream.read_exact(&mut len_buf).await.is_err() {
207            eprintln!("\nstream cerrado por el servidor");
208            return Ok(());
209        }
210        let len = u32::from_be_bytes(len_buf) as usize;
211        if len > 4 * 1024 * 1024 { anyhow::bail!("frame oversize"); }
212        let mut buf = vec![0u8; len];
213        stream.read_exact(&mut buf).await?;
214        let resp: IntrospectResponse = bincode::deserialize(&buf)?;
215        match resp {
216            IntrospectResponse::AuditStreamFrame(entry) => {
217                let prev = entry.prev_sha
218                    .map(|s| s[..4].iter().map(|b| format!("{:02x}", b)).collect::<String>() + "..")
219                    .unwrap_or_else(|| "—".into());
220                let sha = entry.sha[..4].iter().map(|b| format!("{:02x}", b))
221                    .collect::<String>() + "..";
222                println!("[stream] seq={} prev={} sha={}  {:?}",
223                    entry.seq, prev, sha, entry.action);
224            }
225            other => {
226                eprintln!("frame no esperado en stream: {other:?}");
227                return Ok(());
228            }
229        }
230    }
231}
232
233#[allow(dead_code)]
234fn _suppress(_: &Path) {} // mantener Path import si compilador se queja