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:
parent
9bd51a7440
commit
629f08a113
4 changed files with 122 additions and 19 deletions
|
|
@ -13,6 +13,17 @@ use crate::rebuild_queue::QueueEntry;
|
|||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "snake_case", tag = "kind")]
|
||||
pub enum DashboardEvent {
|
||||
/// A new agent-initiated privileged action was recorded in the audit
|
||||
/// log. The audit view (`/audit.html`) prepends `entry` live off
|
||||
/// `/dashboard/stream` instead of polling. The `AuditEntry` fields
|
||||
/// are flattened alongside the `kind` tag + `seq`, so the wire shape
|
||||
/// matches one row of the `/api/audit-log` `entries` array exactly
|
||||
/// (`{kind, seq, id, ts_unix, agent, action, target, outcome, detail}`).
|
||||
AuditEntryAdded {
|
||||
seq: u64,
|
||||
#[serde(flatten)]
|
||||
entry: crate::audit_log::AuditEntry,
|
||||
},
|
||||
/// Broker `Sent` event mirrored onto the dashboard channel.
|
||||
/// `file_refs` carries every path-shaped token in `body` that
|
||||
/// hive-c0re verified is a regular file under the allow-listed
|
||||
|
|
@ -270,6 +281,7 @@ impl DashboardEvent {
|
|||
DashboardEvent::RemindersChanged { .. } => "reminders_changed",
|
||||
DashboardEvent::CapabilitiesChanged { .. } => "capabilities_changed",
|
||||
DashboardEvent::ToolGroupsChanged { .. } => "tool_groups_changed",
|
||||
DashboardEvent::AuditEntryAdded { .. } => "audit_entry_added",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -408,6 +420,18 @@ mod tests {
|
|||
descriptions: std::collections::BTreeMap::new(),
|
||||
assignments: std::collections::BTreeMap::new(),
|
||||
},
|
||||
DashboardEvent::AuditEntryAdded {
|
||||
seq: 1,
|
||||
entry: crate::audit_log::AuditEntry {
|
||||
id: 1,
|
||||
ts_unix: 0,
|
||||
agent: "atlas".into(),
|
||||
action: "restart_infra".into(),
|
||||
target: "hive-ci".into(),
|
||||
outcome: "ok".into(),
|
||||
detail: None,
|
||||
},
|
||||
},
|
||||
];
|
||||
for ev in samples {
|
||||
let v: serde_json::Value = serde_json::to_value(&ev).expect("serialise");
|
||||
|
|
@ -418,4 +442,34 @@ mod tests {
|
|||
assert_eq!(ev.kind_tag(), serde_kind, "kind_tag() drift on {ev:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The flattened `AuditEntry` fields must sit alongside `kind`/`seq`
|
||||
/// at the top level (not nested under `entry`) so the wire shape
|
||||
/// matches one `/api/audit-log` row — the audit view prepends it
|
||||
/// directly.
|
||||
#[test]
|
||||
fn audit_entry_added_flattens_to_top_level() {
|
||||
let ev = DashboardEvent::AuditEntryAdded {
|
||||
seq: 7,
|
||||
entry: crate::audit_log::AuditEntry {
|
||||
id: 42,
|
||||
ts_unix: 1_700_000_000,
|
||||
agent: "atlas".into(),
|
||||
action: "restart_infra".into(),
|
||||
target: "hive-gateway".into(),
|
||||
outcome: "err".into(),
|
||||
detail: Some("denied: missing infra_admin capability".into()),
|
||||
},
|
||||
};
|
||||
let v: serde_json::Value = serde_json::to_value(&ev).expect("serialise");
|
||||
assert_eq!(v["kind"], "audit_entry_added");
|
||||
assert_eq!(v["seq"], 7);
|
||||
assert_eq!(v["id"], 42);
|
||||
assert_eq!(v["agent"], "atlas");
|
||||
assert_eq!(v["target"], "hive-gateway");
|
||||
assert_eq!(v["outcome"], "err");
|
||||
assert_eq!(v["detail"], "denied: missing infra_admin capability");
|
||||
// Not nested — there must be no `entry` sub-object.
|
||||
assert!(v.get("entry").is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue