diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 68d00ccd..0e037388 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -522,7 +522,7 @@ async fn handle_restart_child(coord: &Arc, 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(coord, agent, name).await; + return handle_restart_infra(agent, name).await; } if let Some(err) = require_child(agent, name, "restart") { return err; @@ -544,24 +544,13 @@ async fn handle_restart_child(coord: &Arc, 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( - coord: &Arc, - 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. +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. let audit = |outcome: crate::audit_log::AuditOutcome, detail: Option<&str>| { - if let Some(entry) = - coord - .audit_log - .record(agent, "restart_infra", container, outcome, detail) - { - coord.emit_audit_entry(entry); + if let Some(log) = crate::audit_log::global() { + log.record(agent, "restart_infra", container, outcome, detail); } }; if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::InfraAdmin) { diff --git a/hive-c0re/src/audit_log.rs b/hive-c0re/src/audit_log.rs index 9ec08284..5f7760dd 100644 --- a/hive-c0re/src/audit_log.rs +++ b/hive-c0re/src/audit_log.rs @@ -133,14 +133,6 @@ impl AuditLog { /// but never returned, so a transient blip never fails the underlying /// privileged action (the action already happened — losing its audit /// row is strictly less bad than failing the action retroactively). - /// - /// Returns the inserted [`AuditEntry`] (with its assigned id + - /// timestamp) on success, or `None` if the insert failed. The - /// returned row is the canonical record — callers that also push a - /// live event (e.g. the dashboard stream) emit *this* rather than - /// re-deriving the fields, so the stored row and the streamed event - /// can't drift. - #[must_use] pub fn record( &self, agent: &str, @@ -148,31 +140,19 @@ impl AuditLog { target: &str, outcome: AuditOutcome, detail: Option<&str>, - ) -> Option { + ) { let now = now_secs(); let conn = self.conn.lock().unwrap(); - match conn.execute( + if let Err(e) = conn.execute( "INSERT INTO audit_log (ts_unix, agent, action, target, outcome, detail) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![now, agent, action, target, outcome.as_str(), detail], ) { - Ok(_) => Some(AuditEntry { - id: conn.last_insert_rowid(), - ts_unix: now, - agent: agent.to_owned(), - action: action.to_owned(), - target: target.to_owned(), - outcome: outcome.as_str().to_owned(), - detail: detail.map(str::to_owned), - }), - Err(e) => { - tracing::warn!( - %agent, %action, %target, - error = ?e, - "audit_log: record failed (dropping entry)" - ); - None - } + tracing::warn!( + %agent, %action, %target, + error = ?e, + "audit_log: record failed (dropping entry)" + ); } } @@ -281,15 +261,8 @@ mod tests { #[test] fn record_and_list_newest_first() { let (_d, db) = tmpdb(); - // record() returns the canonical inserted row (id + ts assigned). - let entry = db - .record("atlas", "restart_infra", "hive-ci", AuditOutcome::Ok, None) - .expect("record returns the inserted entry"); - assert!(entry.id > 0); - assert_eq!(entry.target, "hive-ci"); - assert_eq!(entry.outcome, "ok"); - assert!(entry.detail.is_none()); - let _ = db.record( + db.record("atlas", "restart_infra", "hive-ci", AuditOutcome::Ok, None); + db.record( "atlas", "restart_infra", "hive-gateway", @@ -313,7 +286,7 @@ mod tests { #[test] fn list_clamps_to_500() { let (_d, db) = tmpdb(); - let _ = db.record("a", "x", "t", AuditOutcome::Ok, None); + db.record("a", "x", "t", AuditOutcome::Ok, None); let rows = db.list_recent(999_999).expect("list"); assert!(rows.len() <= 500); } @@ -321,7 +294,7 @@ mod tests { #[test] fn vacuum_drops_only_old_rows() { let (_d, db) = tmpdb(); - let _ = db.record("a", "restart_infra", "hive-ci", AuditOutcome::Ok, None); + db.record("a", "restart_infra", "hive-ci", AuditOutcome::Ok, None); // Backdate it past the retention window. { let conn = db.conn.lock().unwrap(); @@ -331,7 +304,7 @@ mod tests { ) .unwrap(); } - let _ = db.record("a", "restart_infra", "hive-forge", AuditOutcome::Ok, None); + db.record("a", "restart_infra", "hive-forge", AuditOutcome::Ok, None); let removed = db.vacuum().expect("vacuum"); assert_eq!(removed, 1, "only the backdated row should be reaped"); let rows = db.list_recent(10).expect("list"); diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 8e632edf..8a893f79 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -692,18 +692,6 @@ impl Coordinator { self.meta_updates_active.load(Ordering::SeqCst) > 0 } - /// Emit `AuditEntryAdded` immediately after a privileged-action row - /// is recorded, so the dashboard audit view live-appends it off - /// `/dashboard/stream`. Pass the [`AuditEntry`](crate::audit_log::AuditEntry) - /// returned by `audit_log::record` so the streamed event is the same - /// canonical row that was stored. - pub fn emit_audit_entry(&self, entry: crate::audit_log::AuditEntry) { - self.emit_dashboard_event(DashboardEvent::AuditEntryAdded { - seq: self.next_seq(), - entry, - }); - } - /// Emit `ApprovalAdded` immediately after the row is inserted in /// sqlite. Caller passes the diff text it already computed (or /// `None` for spawn approvals which carry no diff). diff --git a/hive-c0re/src/dashboard_events.rs b/hive-c0re/src/dashboard_events.rs index d13f5a77..aaaefbbc 100644 --- a/hive-c0re/src/dashboard_events.rs +++ b/hive-c0re/src/dashboard_events.rs @@ -13,17 +13,6 @@ 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 @@ -281,7 +270,6 @@ impl DashboardEvent { DashboardEvent::RemindersChanged { .. } => "reminders_changed", DashboardEvent::CapabilitiesChanged { .. } => "capabilities_changed", DashboardEvent::ToolGroupsChanged { .. } => "tool_groups_changed", - DashboardEvent::AuditEntryAdded { .. } => "audit_entry_added", } } } @@ -420,18 +408,6 @@ 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"); @@ -442,34 +418,4 @@ 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()); - } }