Cristales temporales, replay, lease/renew y audit streaming

- GapHistogram añade sum_squares_secs → stddev en O(1). GapStats serializable
  con count/mean/stddev/max.
- Crystal incluye gap_stats?: GapStats. crystal_to_rule emite Sequence con
  within_ms = (mean+2σ)*1000 cuando gap_stats.count >= 4; fallback a Single.
- audit::collect_chain_from_cas() recoge la cadena en orden cronológico.
  replay_chain() reconstruye RuleEngine aplicando PromoteCrystal/RemoveRule.
  Endpoint ReplayAudit + brainctl replay.
- GrantedCapability con expires_at: Instant. DEFAULT_GRANT_TTL = 60s.
  EnteGraph::renew_grant + purge_expired_grants. Tick cada 10s en el bucle
  primordial.
- AuditLog::subscribe() entrega un mpsc::UnboundedReceiver. append() empuja
  a todos los subscribers, purgando los muertos. IntrospectRequest::StreamAudit
  toma posesión de la conn y envía AuditStreamFrame en bucle. brainctl
  stream-audit imprime entries en directo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-03 23:51:36 +00:00
parent badf4257ec
commit ca75ba185f
8 changed files with 340 additions and 21 deletions
+63 -1
View File
@@ -10,7 +10,9 @@
//! Path del socket: $ENTE_BRAIN_SOCK o $XDG_RUNTIME_DIR/ente-brain.sock
use ente_brain::introspect::{call, IntrospectRequest, IntrospectResponse};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
fn socket_path() -> PathBuf {
if let Ok(p) = std::env::var("ENTE_BRAIN_SOCK") {
@@ -26,6 +28,12 @@ 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");
// Comando especial: streaming. Mantiene la conn abierta y lee frames
// hasta Ctrl-C o EOF del servidor.
if cmd == "stream-audit" || cmd == "stream" {
return run_stream_audit(socket_path()).await;
}
let req = match cmd {
"list-rules" | "rules" => IntrospectRequest::ListRules,
"entropy" => IntrospectRequest::EntropySnapshot,
@@ -53,6 +61,7 @@ async fn main() -> anyhow::Result<()> {
}
"flush-audit" => IntrospectRequest::FlushAudit,
"audit-verify" | "verify" => IntrospectRequest::VerifyAudit,
"replay" => IntrospectRequest::ReplayAudit,
"reload" => {
let path = args.get(2).cloned();
IntrospectRequest::ReloadRules { path }
@@ -131,6 +140,14 @@ fn print_response(r: &IntrospectResponse) {
IntrospectResponse::Reloaded { count } => {
println!("reload OK: {count} reglas activas tras reload");
}
IntrospectResponse::Replayed(rep) => {
if let Some(e) = &rep.error {
println!("✗ replay falló: {e}");
} else {
println!("✓ replay completo — {} actions aplicadas, {} reglas finales",
rep.applied, rep.final_rule_count);
}
}
IntrospectResponse::AuditVerified(rep) => {
if let Some(seq) = rep.broken_at_seq {
println!("✗ verificación FALLÓ tras seq={seq}");
@@ -141,6 +158,11 @@ fn print_response(r: &IntrospectResponse) {
if let Some(g) = rep.genesis_sha { println!(" genesis: {}", hex_long(g)); }
}
}
IntrospectResponse::AuditStreamFrame(_) => {
// En modo request/response no debería llegar; solo aparece en
// run_stream_audit. Si llega aquí es un bug del servidor.
eprintln!("frame de stream recibido fuera de stream-audit (bug)");
}
IntrospectResponse::Error(e) => eprintln!("error: {e}"),
}
}
@@ -152,3 +174,43 @@ fn hex_short(sha: [u8; 32]) -> String {
fn hex_long(sha: [u8; 32]) -> String {
sha.iter().map(|b| format!("{:02x}", b)).collect()
}
async fn run_stream_audit(path: PathBuf) -> anyhow::Result<()> {
let mut stream = UnixStream::connect(&path).await?;
let req = IntrospectRequest::StreamAudit;
let buf = bincode::serialize(&req)?;
stream.write_u32(buf.len() as u32).await?;
stream.write_all(&buf).await?;
eprintln!("audit stream conectado a {} — Ctrl-C para salir", path.display());
loop {
let mut len_buf = [0u8; 4];
if stream.read_exact(&mut len_buf).await.is_err() {
eprintln!("\nstream cerrado por el servidor");
return Ok(());
}
let len = u32::from_be_bytes(len_buf) as usize;
if len > 4 * 1024 * 1024 { anyhow::bail!("frame oversize"); }
let mut buf = vec![0u8; len];
stream.read_exact(&mut buf).await?;
let resp: IntrospectResponse = bincode::deserialize(&buf)?;
match resp {
IntrospectResponse::AuditStreamFrame(entry) => {
let prev = entry.prev_sha
.map(|s| s[..4].iter().map(|b| format!("{:02x}", b)).collect::<String>() + "..")
.unwrap_or_else(|| "".into());
let sha = entry.sha[..4].iter().map(|b| format!("{:02x}", b))
.collect::<String>() + "..";
println!("[stream] seq={} prev={} sha={} {:?}",
entry.seq, prev, sha, entry.action);
}
other => {
eprintln!("frame no esperado en stream: {other:?}");
return Ok(());
}
}
}
}
#[allow(dead_code)]
fn _suppress(_: &Path) {} // mantener Path import si compilador se queja