hub: HostCapabilityChecker — operator alert on agent capability-degraded (v0.19.0)
Companion to felhom-agent v0.44.0. New monitor.HostCapabilityChecker (sibling of HostStalenessChecker) reads the capabilities snapshot from the latest host report and emits agent_capability_degraded/recovered (operator-only, 1h cooldown) on ok<->degraded transitions for any Critical capability. store.GetHostCapabilities (MAX(id), no migration). Goldens mirror the new capabilities field. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EPZ4GJ8L5Jqf8UiPwbn1kt
This commit is contained in:
@@ -1624,3 +1624,52 @@ func (s *Store) GetHostStaleness() ([]HostStaleRow, error) {
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// CapabilityStatus mirrors the agent's capability.Status wire shape (felhom-agent v0.44.0): one
|
||||
// privileged `sudo -n` grant the non-root agent depends on, and whether it is currently usable.
|
||||
type CapabilityStatus struct {
|
||||
Name string `json:"name"`
|
||||
Feature string `json:"feature"`
|
||||
Critical bool `json:"critical"`
|
||||
Status string `json:"status"` // "ok" | "degraded"
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// HostCapabilityRow is the per-host capability snapshot the HostCapabilityChecker reads — extracted
|
||||
// from the latest host-report's report_json (no dedicated column; the array rides the report body).
|
||||
type HostCapabilityRow struct {
|
||||
HostID string
|
||||
CustomerID string
|
||||
Capabilities []CapabilityStatus
|
||||
}
|
||||
|
||||
// GetHostCapabilities returns the latest capability snapshot per host (from the most recent
|
||||
// host_reports row). Hosts whose latest report carries no capabilities array (a pre-v0.44.0 agent)
|
||||
// yield an empty slice — the checker treats that as "ok/unknown" and never alerts, so an old agent
|
||||
// can't trip a false degraded.
|
||||
func (s *Store) GetHostCapabilities() ([]HostCapabilityRow, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT hr.host_id, hr.customer_id, hr.report_json
|
||||
FROM host_reports hr
|
||||
JOIN (SELECT host_id, MAX(id) AS mx FROM host_reports GROUP BY host_id) latest
|
||||
ON hr.id = latest.mx`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []HostCapabilityRow
|
||||
for rows.Next() {
|
||||
var r HostCapabilityRow
|
||||
var reportJSON string
|
||||
if err := rows.Scan(&r.HostID, &r.CustomerID, &reportJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var body struct {
|
||||
Capabilities []CapabilityStatus `json:"capabilities"`
|
||||
}
|
||||
_ = json.Unmarshal([]byte(reportJSON), &body) // a malformed/old body → nil caps → no alert
|
||||
r.Capabilities = body.Capabilities
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user