Capability quotas, CAS gc, patrones burst/silence, snapshot incremental

- quota_for_capability(cap) tabla por variante: Spawn 2, FilesystemRoot 2,
  Endpoint 16, Journal 32. mediate_capability rechaza con QuotaExceeded
  si holder ya tiene N tokens activos.
- ente_cas::list_all_shas() + gc(reachable). audit::reachable_from_head()
  walks la cadena. Endpoint GcCas con extra_roots para Wasm SHAs.
  brainctl gc-cas.
- PatternCrystal::Burst (kind, count, freq_hz) y Silence (kind, since_secs).
  detect_pattern_crystals + endpoint PatternCrystals + brainctl patterns.
- Observer.dirty_marginal/dirty_cooccur tracking. snapshot() marca
  consumo (clears dirty); snapshot_delta() emite sólo lo cambiado.
  apply_delta() merges incremental sobre estado existente. Útil para
  checkpoints frecuentes con poco overhead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-04 00:43:28 +00:00
parent 6aee9254d4
commit f4eb7dd944
10 changed files with 344 additions and 21 deletions
+3
View File
@@ -52,4 +52,7 @@ pub enum CapabilityGrant {
Granted { token: u64 },
NoProvider,
Denied { reason: &'static str },
/// El holder ya tiene el máximo de tokens activos para esta cap.
/// Debe esperar a que alguno expire o renovar uno existente.
QuotaExceeded { active: u32, limit: u32 },
}
+28 -14
View File
@@ -4,7 +4,7 @@
//! periódicamente con `renew_grant(token)`; en caso contrario, el background
//! task `purge_expired_grants` los revoca al vencimiento.
use super::{ttl_for_capability, EnteGraph, GrantedCapability};
use super::{quota_for_capability, ttl_for_capability, EnteGraph, GrantedCapability};
use crate::events::CapabilityGrant;
use ente_card::Capability;
use std::time::Instant;
@@ -22,24 +22,38 @@ impl EnteGraph {
let grant = match self.providers.get(&cap).and_then(|s| s.iter().next().copied()) {
None => CapabilityGrant::NoProvider,
Some(provider) => {
let token = self.next_token;
self.next_token += 1;
// TTL específico por variante de capability — caps escaladas
// (Spawn, FilesystemRoot) viven menos.
let ttl = ttl_for_capability(&cap);
let expires_at = Instant::now() + ttl;
self.grants.insert(token, GrantedCapability {
cap: cap.clone(),
provider,
holder: from,
expires_at,
});
CapabilityGrant::Granted { token }
// Quota: contar tokens vivos para (from, cap). Si excede,
// rechazar antes de emitir uno nuevo.
let limit = quota_for_capability(&cap);
let active = self.active_tokens_for(from, &cap);
if active >= limit {
CapabilityGrant::QuotaExceeded { active, limit }
} else {
let token = self.next_token;
self.next_token += 1;
let ttl = ttl_for_capability(&cap);
let expires_at = Instant::now() + ttl;
self.grants.insert(token, GrantedCapability {
cap: cap.clone(),
provider,
holder: from,
expires_at,
});
CapabilityGrant::Granted { token }
}
}
};
let _ = reply.send(grant);
}
/// Cuenta tokens vivos (no expirados) emitidos a un holder para una cap.
pub fn active_tokens_for(&self, holder: Ulid, cap: &Capability) -> u32 {
let now = Instant::now();
self.grants.values()
.filter(|g| g.holder == holder && &g.cap == cap && g.expires_at > now)
.count() as u32
}
/// Extiende un grant existente. Devuelve `true` si renovó. Si el token
/// no existe o ya expiró, `false` (el cliente debe re-acquire).
/// Usa el TTL específico de la cap del grant.
+18
View File
@@ -76,6 +76,24 @@ pub(in crate::graph) struct GrantedCapability {
/// corto enough para que credenciales filtradas expiren rápidamente.
pub const DEFAULT_GRANT_TTL: std::time::Duration = std::time::Duration::from_secs(60);
/// Quota máxima de tokens activos por (holder, cap). Caps escaladas tienen
/// quota baja para limitar fugas de credenciales; caps de uso frecuente
/// (Endpoint, Journal) son más laxas.
pub fn quota_for_capability(cap: &Capability) -> u32 {
match cap {
// Caps escaladas: pocos tokens, fuerza patrón request-act-release.
Capability::Spawn => 2,
Capability::FilesystemRoot => 2,
Capability::Device { .. } => 4,
// Caps de propósito general.
Capability::Endpoint { .. } => 16,
Capability::KernelNetlink(_) => 4,
Capability::LegacyLogind => 8,
// Logging: hasta 32 streams.
Capability::Journal => 32,
}
}
/// TTL específico por variante de Capability. Caps de mayor riesgo / costo
/// (Spawn, FilesystemRoot) tienen TTL más corto; caps "logging" como
/// Journal pueden vivir más.
+1 -1
View File
@@ -342,7 +342,7 @@ async fn dispatch_graph_event(
}
// Snapshot del cerebro (observer state) en archivo adjunto
let brain_path = path.with_extension("brain.json");
let obs_snap = brain.observer.read().await.snapshot();
let obs_snap = brain.observer.write().await.snapshot();
match write_brain_snapshot(&brain_path, &obs_snap) {
Ok(()) => info!(
path = %brain_path.display(),