Files
arje/crates/ente-brain/examples/brainctl.rs
T
Sergio badf4257ec audit-verify, autopromote, histogramas y hot-reload caps
- audit::verify_chain_from_cas() recorre prev_sha desde head hasta genesis,
  valida sha256(blob)==sha del CAS para detectar tampering. Endpoint
  VerifyAudit + brainctl audit-verify.
- autopromote loop background: cada N segundos detecta cristales sobre
  threshold y los promueve sin operador. Anti-doble vía HashSet de pares
  (ant, con). Flag --autopromote-secs.
- GapHistogram con buckets exponenciales (1ms..1000s) capturado en
  observer.record(). top_gap_pairs(K) limita cardinalidad. Expuesto en
  Prometheus como ente_brain_pair_gap_seconds histogram per pair.
- BusRequest::UpdateCapabilities con auth obligatorio (sólo el dueño puede
  modificar sus caps). Incarnated.dynamic_provides separa runtime de la
  Card immutable. Graph mediator actualiza providers index + revoca grants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:32:54 +00:00

155 lines
6.4 KiB
Rust

//! brainctl: cliente CLI del introspect API.
//!
//! Uso:
//! cargo run --example brainctl -p ente-brain -- list-rules
//! cargo run --example brainctl -p ente-brain -- entropy
//! cargo run --example brainctl -p ente-brain -- top 10
//! cargo run --example brainctl -p ente-brain -- crystals
//! cargo run --example brainctl -p ente-brain -- crystal-kcl 0
//!
//! Path del socket: $ENTE_BRAIN_SOCK o $XDG_RUNTIME_DIR/ente-brain.sock
use ente_brain::introspect::{call, IntrospectRequest, IntrospectResponse};
use std::path::PathBuf;
fn socket_path() -> PathBuf {
if let Ok(p) = std::env::var("ENTE_BRAIN_SOCK") {
return p.into();
}
let runtime = std::env::var("XDG_RUNTIME_DIR")
.unwrap_or_else(|_| std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into()));
format!("{runtime}/ente-brain.sock").into()
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let args: Vec<String> = std::env::args().collect();
let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("entropy");
let req = match cmd {
"list-rules" | "rules" => IntrospectRequest::ListRules,
"entropy" => IntrospectRequest::EntropySnapshot,
"top" => {
let n: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(10);
IntrospectRequest::TopCorrelations { n }
}
"crystals" => IntrospectRequest::Crystals,
"crystal-kcl" => {
let i: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
IntrospectRequest::CrystalKcl { index: i }
}
"promote" => {
let i: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
IntrospectRequest::PromoteCrystal { index: i }
}
"remove" => {
let id_s = args.get(2).ok_or_else(|| anyhow::anyhow!("se requiere <ulid>"))?;
let id: ulid::Ulid = id_s.parse()?;
IntrospectRequest::RemoveRule { id }
}
"audit" => {
let limit: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(20);
IntrospectRequest::ListAudit { limit }
}
"flush-audit" => IntrospectRequest::FlushAudit,
"audit-verify" | "verify" => IntrospectRequest::VerifyAudit,
"reload" => {
let path = args.get(2).cloned();
IntrospectRequest::ReloadRules { path }
}
other => {
eprintln!("subcomando desconocido: {other}");
eprintln!("válidos: list-rules | entropy | top <n> | crystals | crystal-kcl <i> | promote <i> | remove <ulid> | audit <limit> | flush-audit | reload [path]");
std::process::exit(2);
}
};
let path = socket_path();
let resp = call(&path, req).await?;
print_response(&resp);
Ok(())
}
fn print_response(r: &IntrospectResponse) {
match r {
IntrospectResponse::Rules(rs) => {
println!("{} reglas vivas:", rs.len());
for r in rs {
println!(" {} prio={} kind={} actions={} wildcard={}",
r.id, r.priority, r.event_kind_tag, r.action_count, r.scope_wildcard);
}
}
IntrospectResponse::Rule(rule) => match rule {
Some(r) => println!("{r:#?}"),
None => println!("regla no encontrada"),
},
IntrospectResponse::Entropy { value_bits, sample_size, distinct_kinds, window_full } => {
println!("Shannon entropy : {value_bits:.4} bits");
println!("Sample size : {sample_size}");
println!("Distinct kinds : {distinct_kinds}");
println!("Window full : {window_full}");
}
IntrospectResponse::Correlations(entries) => {
println!("{} pares (top, ordenado por co-ocurrencia):", entries.len());
for e in entries {
println!(" n={:>4} P(b|a)={:.3} PMI={:>6.3}b {}{}",
e.joint_count, e.conditional_prob, e.pmi_bits, e.a, e.b);
}
}
IntrospectResponse::Crystals(cs) => {
println!("{} cristales detectados:", cs.len());
for (i, c) in cs.iter().enumerate() {
println!(" [{i}] {:?}{:?} P={:.3} PMI={:.3}b n={}",
c.antecedent, c.consequent, c.conditional_prob, c.pmi, c.support);
}
}
IntrospectResponse::Kcl(s) => println!("{s}"),
IntrospectResponse::Promoted { rule_id, kcl_snippet } => {
println!("regla creada: {rule_id}");
println!("--- KCL para auditoría / persistencia ---");
println!("{kcl_snippet}");
}
IntrospectResponse::Removed(was_present) => {
if *was_present { println!("regla eliminada"); }
else { println!("regla no encontrada"); }
}
IntrospectResponse::AuditEntries(entries) => {
println!("{} entries de audit log:", entries.len());
for e in entries {
let prev = e.prev_sha.map(hex_short).unwrap_or_else(|| "".into());
let sha = hex_short(e.sha);
println!(" seq={:>4} t={} prev={} sha={} {:?}",
e.seq, e.timestamp_ms, prev, sha, e.action);
}
}
IntrospectResponse::Flushed { written, head_sha, total_flushed } => {
println!("flushed: {written} entries esta pasada, total acumulado: {total_flushed}");
if let Some(sha) = head_sha {
println!("head sha: {}", hex_long(*sha));
}
}
IntrospectResponse::Reloaded { count } => {
println!("reload OK: {count} reglas activas tras reload");
}
IntrospectResponse::AuditVerified(rep) => {
if let Some(seq) = rep.broken_at_seq {
println!("✗ verificación FALLÓ tras seq={seq}");
if let Some(e) = &rep.error { println!(" motivo: {e}"); }
println!(" entries verificadas: {}", rep.verified);
} else {
println!("✓ chain verificada — {} entries íntegras", rep.verified);
if let Some(g) = rep.genesis_sha { println!(" genesis: {}", hex_long(g)); }
}
}
IntrospectResponse::Error(e) => eprintln!("error: {e}"),
}
}
fn hex_short(sha: [u8; 32]) -> String {
sha[..4].iter().map(|b| format!("{:02x}", b)).collect::<String>() + ".."
}
fn hex_long(sha: [u8; 32]) -> String {
sha.iter().map(|b| format!("{:02x}", b)).collect()
}