Audit→CAS, reload rules, time-decay y forma canónica del hash chain

- AuditLog::flush_to_cas() persiste entries pendientes con bytes canónicos
  (sha=[0;32]) para que CAS-sha == entry.sha. AuditHeadPointer en disco
  tras cada flush — verificación posterior sin escanear el log entero.
- IntrospectRequest::FlushAudit / ReloadRules. brainctl flush-audit / reload.
- Auto-flush task cada 10s cuando --audit-head está configurado.
- ReloadRules { path? } vacía engine + carga (.k vía kcl CLI o .json).
- Observer con time-decay opcional: count * 0.5^(age/half_life).
  conditional_prob y pmi consumen valores decayed transparentemente.
- --brain-half-life flag CLI.
- KCL Rust SDK descartado: kcl-* en crates.io son del proyecto KittyCAD,
  no KusionStack. Subprocess al CLI sigue siendo la vía canónica.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-03 23:16:41 +00:00
parent d6b8f18b43
commit a4fa42c781
6 changed files with 282 additions and 40 deletions
+48 -2
View File
@@ -39,7 +39,9 @@ struct CliArgs {
restore: Option<PathBuf>,
rules: Option<PathBuf>,
rules_out: Option<PathBuf>,
audit_head: Option<PathBuf>,
metrics_addr: Option<String>,
brain_half_life: Option<f64>,
}
fn parse_args() -> CliArgs {
@@ -48,18 +50,22 @@ fn parse_args() -> CliArgs {
let mut restore = None;
let mut rules = None;
let mut rules_out = None;
let mut audit_head = None;
let mut metrics_addr = None;
let mut brain_half_life = None;
while let Some(a) = args.next() {
match a.as_str() {
"--checkpoint" => checkpoint = args.next().map(PathBuf::from),
"--restore" => restore = args.next().map(PathBuf::from),
"--rules" => rules = args.next().map(PathBuf::from),
"--rules-out" => rules_out = args.next().map(PathBuf::from),
"--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()),
other => warn!(arg = %other, "argumento desconocido, ignorado"),
}
}
CliArgs { checkpoint, restore, rules, rules_out, metrics_addr }
CliArgs { checkpoint, restore, rules, rules_out, audit_head, metrics_addr, brain_half_life }
}
fn main() -> anyhow::Result<()> {
@@ -84,7 +90,11 @@ fn main() -> anyhow::Result<()> {
.enable_time()
.build()?;
rt.block_on(primordial_loop(card, dev_mode, cli.checkpoint, cli.rules, cli.rules_out, cli.metrics_addr))
rt.block_on(primordial_loop(
card, dev_mode,
cli.checkpoint, cli.rules, cli.rules_out,
cli.audit_head, cli.metrics_addr, cli.brain_half_life,
))
}
async fn primordial_loop(
@@ -93,7 +103,9 @@ async fn primordial_loop(
checkpoint_path: Option<PathBuf>,
rules_path: Option<PathBuf>,
rules_out: Option<PathBuf>,
audit_head: Option<PathBuf>,
metrics_addr: Option<String>,
brain_half_life: Option<f64>,
) -> anyhow::Result<()> {
info!(seed_id = %seed_card.id, label = %seed_card.label, "Ente #0 entra al bucle primordial");
@@ -139,6 +151,21 @@ async fn primordial_loop(
if let Some(out_path) = rules_out {
brain = brain.with_rules_out(out_path);
}
if let Some(hl) = brain_half_life {
let mut obs = brain.observer.write().await;
// Reemplazar con un observer nuevo que tenga half-life. Estado
// anterior (vacío en este punto) descartado.
*obs = ente_brain::Observer::new(1024).with_half_life(hl);
info!(hl_secs = hl, "observer con time-decay activo");
}
// 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.
let new_audit = ente_brain::audit::AuditLog::new()
.with_head_pointer(head_path);
*brain.audit.write().await = new_audit;
spawn_audit_auto_flush(brain.clone());
}
// Carga inicial de reglas vía KCL o JSON, si --rules path proporcionado.
if let Some(path) = &rules_path {
@@ -343,6 +370,25 @@ fn brain_introspect_path() -> PathBuf {
format!("{runtime}/ente-brain.sock").into()
}
/// Auto-flush del audit log a CAS cada 10 segundos. Ejecuta best-effort:
/// si el flush falla lo logeamos pero no abortamos. La integridad del log
/// queda garantizada por su hash chain — re-flushar es idempotente.
fn spawn_audit_auto_flush(state: BrainState) {
tokio::spawn(async move {
let mut tick = tokio::time::interval(std::time::Duration::from_secs(10));
tick.tick().await; // descartar primer tick inmediato
loop {
tick.tick().await;
let mut audit = state.audit.write().await;
match audit.flush_to_cas() {
Ok(0) => {} // nada nuevo
Ok(n) => info!(written = n, total = audit.flushed_count(), "audit auto-flush"),
Err(e) => warn!(?e, "audit auto-flush falló"),
}
}
});
}
fn spawn_brain_introspect(state: BrainState) {
let path = brain_introspect_path();
tokio::spawn(async move {