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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user