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