feat(audit): live-append event for the dashboard audit view

Follow-up to the audit-log backend + surface. Emits a DashboardEvent on
each agent-initiated privileged action so the audit view live-appends off
/dashboard/stream instead of polling.

- new DashboardEvent::AuditEntryAdded { seq, <flattened AuditEntry> } —
  serde tag `audit_entry_added`; the AuditEntry fields flatten to the top
  level so the wire shape matches one /api/audit-log `entries` row exactly.
- Coordinator::emit_audit_entry helper (stamps seq like the others).
- audit_log::record now returns the canonical inserted AuditEntry (id + ts
  assigned) so the streamed event is the same row that was stored — no
  drift. Best-effort unchanged (None on a sqlite blip).
- handle_restart_infra records + emits for every attempt (ok/err/denied),
  threading the coordinator through.

Tests: kind_tag round-trip now covers the new variant; added a flatten
test pinning the top-level wire shape (kind/seq/id/…/detail, no nesting).

Pairs with iris's audit view (the /dashboard/stream listener half).
This commit is contained in:
atlas 2026-06-13 16:03:04 +02:00 committed by mara
commit 629f08a113
4 changed files with 122 additions and 19 deletions

View file

@ -522,7 +522,7 @@ async fn handle_restart_child(coord: &Arc<Coordinator>, agent: &str, name: &str)
// tool. These names are never agent children, so this branch is
// disjoint from the child-restart path below.
if hive_sh4re::priv_proto::RESTARTABLE_INFRA_CONTAINERS.contains(&name) {
return handle_restart_infra(agent, name).await;
return handle_restart_infra(coord, agent, name).await;
}
if let Some(err) = require_child(agent, name, "restart") {
return err;
@ -544,13 +544,24 @@ async fn handle_restart_child(coord: &Arc<Coordinator>, agent: &str, name: &str)
/// known to be in `RESTARTABLE_INFRA_CONTAINERS`; this gates on the
/// capability and routes the systemctl restart through hive-priv (which
/// re-validates the name root-side). Direct, not approval-gated.
async fn handle_restart_infra(agent: &str, container: &str) -> AgentResponse {
// Record the attempt in the operator-visible privileged-action audit trail.
// Best-effort: no-op when the global handle isn't installed (early
// startup / tests). `action` is stable so the dashboard can group.
async fn handle_restart_infra(
coord: &Arc<Coordinator>,
agent: &str,
container: &str,
) -> AgentResponse {
// Record the attempt in the operator-visible privileged-action audit
// trail, then emit a live `AuditEntryAdded` so the dashboard audit view
// appends it off `/dashboard/stream`. Best-effort: `record` returns the
// canonical row (or `None` on a sqlite blip), and we stream exactly that
// row so the stored + streamed views can't drift. `action` is stable so
// the dashboard can group/filter.
let audit = |outcome: crate::audit_log::AuditOutcome, detail: Option<&str>| {
if let Some(log) = crate::audit_log::global() {
log.record(agent, "restart_infra", container, outcome, detail);
if let Some(entry) =
coord
.audit_log
.record(agent, "restart_infra", container, outcome, detail)
{
coord.emit_audit_entry(entry);
}
};
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::InfraAdmin) {