audit-verify, autopromote, histogramas y hot-reload caps
- audit::verify_chain_from_cas() recorre prev_sha desde head hasta genesis, valida sha256(blob)==sha del CAS para detectar tampering. Endpoint VerifyAudit + brainctl audit-verify. - autopromote loop background: cada N segundos detecta cristales sobre threshold y los promueve sin operador. Anti-doble vía HashSet de pares (ant, con). Flag --autopromote-secs. - GapHistogram con buckets exponenciales (1ms..1000s) capturado en observer.record(). top_gap_pairs(K) limita cardinalidad. Expuesto en Prometheus como ente_brain_pair_gap_seconds histogram per pair. - BusRequest::UpdateCapabilities con auth obligatorio (sólo el dueño puede modificar sus caps). Incarnated.dynamic_provides separa runtime de la Card immutable. Graph mediator actualiza providers index + revoca grants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,16 @@ use ulid::Ulid;
|
||||
|
||||
/// Operaciones que requieren identidad verificada en el bus.
|
||||
///
|
||||
/// Sólo `Announce`: establece la entrada en `bus_connections` para forwarding
|
||||
/// y debe ser no-falsificable. Invoke, ListEntes y power-mgmt se aceptan
|
||||
/// anonymous — políticas por capacidad se aplican aguas abajo, no aquí.
|
||||
/// - `Announce`: establece bus_connections para forwarding.
|
||||
/// - `UpdateCapabilities`: muta dynamic_provides del Ente — sólo el dueño.
|
||||
///
|
||||
/// Invoke, ListEntes y power-mgmt se aceptan anonymous — políticas por
|
||||
/// capacidad se aplican aguas abajo, no aquí.
|
||||
fn requires_auth(req: &BusRequest) -> bool {
|
||||
matches!(req, BusRequest::Announce { .. })
|
||||
matches!(
|
||||
req,
|
||||
BusRequest::Announce { .. } | BusRequest::UpdateCapabilities { .. }
|
||||
)
|
||||
}
|
||||
|
||||
impl EnteGraph {
|
||||
@@ -101,9 +106,65 @@ impl EnteGraph {
|
||||
BusRequest::Invoke { cap, blob } => {
|
||||
self.forward_invoke(from_authenticated, cap, blob, reply).await;
|
||||
}
|
||||
BusRequest::UpdateCapabilities { adds, removes } => {
|
||||
let id = from_authenticated.expect("auth-required guarantees Some");
|
||||
self.apply_capability_update(id, adds, removes);
|
||||
let _ = reply.send(BusResponse::Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Muta `dynamic_provides` del Ente y actualiza el índice global de
|
||||
/// providers. La Card original (immutable) no se toca.
|
||||
fn apply_capability_update(
|
||||
&mut self,
|
||||
ente_id: Ulid,
|
||||
adds: Vec<Capability>,
|
||||
removes: Vec<Capability>,
|
||||
) {
|
||||
// Adiciones: dedupe contra Card.provides + dynamic_provides existentes.
|
||||
let mut added = Vec::new();
|
||||
let mut removed = Vec::new();
|
||||
if let Some(inc) = self.incarnated.get_mut(&ente_id) {
|
||||
for cap in adds {
|
||||
if inc.card.provides.contains(&cap) || inc.dynamic_provides.contains(&cap) {
|
||||
continue; // ya provista, no-op
|
||||
}
|
||||
inc.dynamic_provides.insert(cap.clone());
|
||||
added.push(cap);
|
||||
}
|
||||
for cap in removes {
|
||||
if inc.dynamic_provides.remove(&cap) {
|
||||
removed.push(cap);
|
||||
}
|
||||
// Caps de la Card original no se pueden quitar — silenciosamente
|
||||
// ignoradas. Una Card es contrato; sólo el dynamic es mutable.
|
||||
}
|
||||
}
|
||||
// Actualizar índice global. Hacemos esto fuera del scope `inc` para
|
||||
// evitar el doble-borrow de self.
|
||||
for cap in &added {
|
||||
self.register_dynamic_cap(ente_id, cap.clone());
|
||||
}
|
||||
for cap in &removed {
|
||||
self.unregister_dynamic_cap(ente_id, cap);
|
||||
// Revocar grants emitidos contra esta cap por este Ente.
|
||||
let revoked: Vec<u64> = self.grants.iter()
|
||||
.filter(|(_, g)| g.provider == ente_id && &g.cap == cap)
|
||||
.map(|(t, _)| *t)
|
||||
.collect();
|
||||
for t in revoked {
|
||||
self.grants.remove(&t);
|
||||
}
|
||||
}
|
||||
info!(
|
||||
%ente_id,
|
||||
added_count = added.len(),
|
||||
removed_count = removed.len(),
|
||||
"capabilities actualizadas en runtime"
|
||||
);
|
||||
}
|
||||
|
||||
/// Enruta un Invoke al proveedor real de la capacidad. Aloca un seq
|
||||
/// server-side, registra el reply oneshot en `pending_invokes`, y empuja
|
||||
/// el request por la conexión del proveedor.
|
||||
|
||||
@@ -84,7 +84,10 @@ impl EnteGraph {
|
||||
self.children.entry(parent).or_default().push(card.id);
|
||||
}
|
||||
info!(label = %card.label, ?pid, lineage = ?card.lineage, "Ente encarnado");
|
||||
self.incarnated.insert(card.id, Incarnated { card, pid });
|
||||
self.incarnated.insert(card.id, Incarnated {
|
||||
card, pid,
|
||||
dynamic_provides: std::collections::BTreeSet::new(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,10 @@ pub struct EnteGraph {
|
||||
pub(in crate::graph) struct Incarnated {
|
||||
pub card: EntityCard,
|
||||
pub pid: Option<Pid>,
|
||||
/// Capacidades añadidas en runtime vía BusRequest::UpdateCapabilities.
|
||||
/// La Card original es immutable; la "vista efectiva" del Ente es
|
||||
/// `card.provides ∪ dynamic_provides`.
|
||||
pub dynamic_provides: BTreeSet<Capability>,
|
||||
}
|
||||
|
||||
pub(in crate::graph) struct GrantedCapability {
|
||||
@@ -86,7 +90,10 @@ impl EnteGraph {
|
||||
// El Ente #0 se inscribe a sí mismo como proveedor de las capacidades
|
||||
// que su Card declara — sólo así los hijos pueden requerirlas.
|
||||
g.register_provider(&seed);
|
||||
g.incarnated.insert(seed.id, Incarnated { card: seed, pid: None });
|
||||
g.incarnated.insert(seed.id, Incarnated {
|
||||
card: seed, pid: None,
|
||||
dynamic_provides: BTreeSet::new(),
|
||||
});
|
||||
g
|
||||
}
|
||||
|
||||
@@ -144,6 +151,19 @@ impl EnteGraph {
|
||||
}
|
||||
}
|
||||
|
||||
/// Quita una capacidad dinámica del índice de providers para un Ente
|
||||
/// específico. Usado al recibir UpdateCapabilities con `removes`.
|
||||
pub(in crate::graph) fn unregister_dynamic_cap(&mut self, ente_id: Ulid, cap: &Capability) {
|
||||
if let Some(set) = self.providers.get_mut(cap) {
|
||||
set.remove(&ente_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserta una capacidad dinámica al índice de providers para un Ente.
|
||||
pub(in crate::graph) fn register_dynamic_cap(&mut self, ente_id: Ulid, cap: Capability) {
|
||||
self.providers.entry(cap).or_default().insert(ente_id);
|
||||
}
|
||||
|
||||
/// El Ente #0 (semilla) tiene todas sus capacidades declaradas. Otros
|
||||
/// las tienen si su Card las declara o si poseen un grant vivo.
|
||||
pub(in crate::graph) fn holder_has(&self, holder: Ulid, cap: &Capability) -> bool {
|
||||
|
||||
@@ -42,6 +42,7 @@ struct CliArgs {
|
||||
audit_head: Option<PathBuf>,
|
||||
metrics_addr: Option<String>,
|
||||
brain_half_life: Option<f64>,
|
||||
autopromote_secs: Option<u64>,
|
||||
}
|
||||
|
||||
fn parse_args() -> CliArgs {
|
||||
@@ -53,6 +54,7 @@ fn parse_args() -> CliArgs {
|
||||
let mut audit_head = None;
|
||||
let mut metrics_addr = None;
|
||||
let mut brain_half_life = None;
|
||||
let mut autopromote_secs = None;
|
||||
while let Some(a) = args.next() {
|
||||
match a.as_str() {
|
||||
"--checkpoint" => checkpoint = args.next().map(PathBuf::from),
|
||||
@@ -62,10 +64,14 @@ fn parse_args() -> CliArgs {
|
||||
"--audit-head" => audit_head = args.next().map(PathBuf::from),
|
||||
"--metrics-addr" => metrics_addr = args.next(),
|
||||
"--brain-half-life" => brain_half_life = args.next().and_then(|s| s.parse().ok()),
|
||||
"--autopromote-secs" => autopromote_secs = args.next().and_then(|s| s.parse().ok()),
|
||||
other => warn!(arg = %other, "argumento desconocido, ignorado"),
|
||||
}
|
||||
}
|
||||
CliArgs { checkpoint, restore, rules, rules_out, audit_head, metrics_addr, brain_half_life }
|
||||
CliArgs {
|
||||
checkpoint, restore, rules, rules_out, audit_head,
|
||||
metrics_addr, brain_half_life, autopromote_secs,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
@@ -94,6 +100,7 @@ fn main() -> anyhow::Result<()> {
|
||||
card, dev_mode,
|
||||
cli.checkpoint, cli.rules, cli.rules_out,
|
||||
cli.audit_head, cli.metrics_addr, cli.brain_half_life,
|
||||
cli.autopromote_secs,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -106,6 +113,7 @@ async fn primordial_loop(
|
||||
audit_head: Option<PathBuf>,
|
||||
metrics_addr: Option<String>,
|
||||
brain_half_life: Option<f64>,
|
||||
autopromote_secs: Option<u64>,
|
||||
) -> anyhow::Result<()> {
|
||||
info!(seed_id = %seed_card.id, label = %seed_card.label, "Ente #0 entra al bucle primordial");
|
||||
|
||||
@@ -158,6 +166,15 @@ async fn primordial_loop(
|
||||
*obs = ente_brain::Observer::new(1024).with_half_life(hl);
|
||||
info!(hl_secs = hl, "observer con time-decay activo");
|
||||
}
|
||||
if let Some(secs) = autopromote_secs {
|
||||
ente_brain::spawn_autopromote_loop(
|
||||
brain.clone(),
|
||||
ente_brain::AutopromoteParams {
|
||||
interval_secs: secs,
|
||||
threshold: brain.params, // mismo threshold que crystals manuales
|
||||
},
|
||||
);
|
||||
}
|
||||
// Si --audit-head, configuramos el head pointer y arrancamos auto-flush.
|
||||
if let Some(head_path) = audit_head {
|
||||
// Re-creamos el AuditLog con head pointer.
|
||||
|
||||
Reference in New Issue
Block a user