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:
@@ -97,6 +97,12 @@ pub enum IntrospectRequest {
|
||||
RemoveRule { id: Ulid },
|
||||
/// Lista las últimas N entradas del audit log. limit=0 = todas.
|
||||
ListAudit { limit: usize },
|
||||
/// Persiste todas las entries pendientes al CAS y actualiza el head
|
||||
/// pointer si el log lo tiene configurado.
|
||||
FlushAudit,
|
||||
/// Recarga reglas desde el archivo configurado por --rules-out (o el
|
||||
/// path provisto). Vacía el engine antes de cargar.
|
||||
ReloadRules { path: Option<String> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -114,6 +120,10 @@ pub enum IntrospectResponse {
|
||||
Removed(bool),
|
||||
/// Entradas del audit log (más recientes al final).
|
||||
AuditEntries(Vec<crate::audit::AuditEntry>),
|
||||
/// Resultado de FlushAudit: cuántas entries se escribieron y SHA del head.
|
||||
Flushed { written: usize, head_sha: Option<[u8; 32]>, total_flushed: u64 },
|
||||
/// Resultado de ReloadRules: número total de reglas tras el reload.
|
||||
Reloaded { count: usize },
|
||||
Error(String),
|
||||
}
|
||||
|
||||
@@ -296,6 +306,45 @@ impl IntrospectServer {
|
||||
let audit = self.state.audit.read().await;
|
||||
IntrospectResponse::AuditEntries(audit.recent(limit).cloned().collect())
|
||||
}
|
||||
IntrospectRequest::FlushAudit => {
|
||||
let mut audit = self.state.audit.write().await;
|
||||
match audit.flush_to_cas() {
|
||||
Ok(written) => IntrospectResponse::Flushed {
|
||||
written,
|
||||
head_sha: audit.last_flushed_sha(),
|
||||
total_flushed: audit.flushed_count(),
|
||||
},
|
||||
Err(e) => IntrospectResponse::Error(format!("flush_to_cas: {e}")),
|
||||
}
|
||||
}
|
||||
IntrospectRequest::ReloadRules { path } => {
|
||||
// Path explícito gana sobre el rules_out configurado.
|
||||
let resolved = path.map(std::path::PathBuf::from)
|
||||
.or_else(|| self.state.rules_out.as_ref().map(|p| p.as_path().to_path_buf()));
|
||||
let path = match resolved {
|
||||
Some(p) => p,
|
||||
None => return IntrospectResponse::Error(
|
||||
"ReloadRules sin path y sin rules_out configurado".into()
|
||||
),
|
||||
};
|
||||
let rules = match crate::kcl_loader::load_rules_file(&path) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return IntrospectResponse::Error(format!("load: {e}")),
|
||||
};
|
||||
// Vaciamos el engine antes de re-cargar — semántica clean-slate.
|
||||
let mut engine = self.state.engine.write().await;
|
||||
*engine = crate::engine::RuleEngine::empty();
|
||||
let count = rules.len();
|
||||
for r in rules { engine.insert(r); }
|
||||
drop(engine);
|
||||
self.state.audit.write().await.append(
|
||||
crate::audit::AuditAction::LoadRulesFile {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
count,
|
||||
}
|
||||
);
|
||||
IntrospectResponse::Reloaded { count }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user