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
+50 -6
View File
@@ -1,13 +1,15 @@
//! Mediación de capabilities: emisión y revocación de tokens.
//! Mediación de capabilities: emisión, renovación, revocación de tokens.
//!
//! El Init no fuerza políticas — sólo verifica que el proveedor existe y
//! emite tokens. Las políticas reales (quién puede pedir qué, rate limits,
//! audit) se aplican en capas superiores.
//! Los grants tienen TTL (`DEFAULT_GRANT_TTL`). El cliente debe renovarlos
//! periódicamente con `renew_grant(token)`; en caso contrario, el background
//! task `purge_expired_grants` los revoca al vencimiento.
use super::{EnteGraph, GrantedCapability};
use super::{EnteGraph, GrantedCapability, DEFAULT_GRANT_TTL};
use crate::events::CapabilityGrant;
use ente_card::Capability;
use std::time::Instant;
use tokio::sync::oneshot;
use tracing::debug;
use ulid::Ulid;
impl EnteGraph {
@@ -22,12 +24,54 @@ impl EnteGraph {
Some(provider) => {
let token = self.next_token;
self.next_token += 1;
let expires_at = Instant::now() + DEFAULT_GRANT_TTL;
self.grants.insert(token, GrantedCapability {
cap: cap.clone(), provider, holder: from,
cap: cap.clone(),
provider,
holder: from,
expires_at,
});
CapabilityGrant::Granted { token }
}
};
let _ = reply.send(grant);
}
/// Extiende un grant existente. Devuelve `true` si renovó. Si el token
/// no existe o ya expiró, `false` (el cliente debe re-acquire).
pub fn renew_grant(&mut self, token: u64) -> bool {
let now = Instant::now();
if let Some(g) = self.grants.get_mut(&token) {
if g.expires_at > now {
g.expires_at = now + DEFAULT_GRANT_TTL;
return true;
}
// Expired — purgamos aquí mismo.
self.grants.remove(&token);
}
false
}
/// GC: elimina grants vencidos. Devuelve cuántos fueron purgados.
pub fn purge_expired_grants(&mut self) -> usize {
let now = Instant::now();
let expired: Vec<u64> = self.grants.iter()
.filter(|(_, g)| g.expires_at <= now)
.map(|(t, _)| *t)
.collect();
for t in &expired {
self.grants.remove(t);
}
if !expired.is_empty() {
debug!(count = expired.len(), "grants expirados purgados");
}
expired.len()
}
/// Cuenta de grants vivos (no expirados). Usado por métricas.
pub fn active_grants_count(&self) -> usize {
let now = Instant::now();
self.grants.values().filter(|g| g.expires_at > now).count()
}
}
+6
View File
@@ -66,8 +66,14 @@ pub(in crate::graph) struct GrantedCapability {
pub cap: Capability,
pub provider: Ulid,
pub holder: Ulid,
/// Instante en el que el grant deja de ser válido. El garbage collector
/// del cerebro purga grants con `Instant::now() > expires_at`.
pub expires_at: std::time::Instant,
}
/// TTL default para nuevos grants. Configurable por bus en el futuro.
pub const DEFAULT_GRANT_TTL: std::time::Duration = std::time::Duration::from_secs(60);
impl EnteGraph {
pub fn new(mut seed: EntityCard) -> Self {
// Extraemos genesis antes de almacenar la Semilla — evita duplicación
+11
View File
@@ -237,6 +237,10 @@ async fn primordial_loop(
};
tokio::pin!(dev_exit);
// GC de capability grants expirados — corre cada 10 segundos.
let mut grant_purge = tokio::time::interval(Duration::from_secs(10));
grant_purge.tick().await; // descartar primer tick inmediato
loop {
tokio::select! {
biased;
@@ -258,6 +262,13 @@ async fn primordial_loop(
}
}
_ = grant_purge.tick() => {
let n = graph.purge_expired_grants();
if n > 0 {
info!(purged = n, active = graph.active_grants_count(), "GC capability grants");
}
}
_ = async { dev_exit.as_mut().as_pin_mut().unwrap().await }, if dev_mode => {
info!("dev mode: timer expirado, cerrando bucle primordial");
let _ = graph_tx.send(GraphEvent::Shutdown {