capability: agent privileged-capability self-probe (manifest + build-test + runtime snapshot) v0.44.0
New internal/capability: Manifest of required sudo -n grants + Prober that LISTS each via 'sudo -n -l' (never executes) + binary-exists check → ok/degraded snapshot on the hub report. Build-time test asserts manifest⊆sudoers (red-proof: dropping lxc-info FAILs the gate). Startup logs N/N ok + ERROR per degraded. Serve-degraded; no allowlist change. 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:
+25
-3
@@ -6,6 +6,7 @@ import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/capability"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||
)
|
||||
|
||||
@@ -57,7 +58,8 @@ type Collector struct {
|
||||
backups BackupReporter
|
||||
restoreTests RestoreTestReporter
|
||||
pbs PBSReporter
|
||||
temp TempReader // slice 9: host CPU/chassis temp (nil-safe → nil temp)
|
||||
temp TempReader // slice 9: host CPU/chassis temp (nil-safe → nil temp)
|
||||
capProbe func(ctx context.Context) []capability.Status // v0.44.0: privileged-capability self-check (nil → empty)
|
||||
hostID string
|
||||
agentVersion string
|
||||
logger *slog.Logger
|
||||
@@ -92,6 +94,13 @@ func (c *Collector) SetTempReader(t TempReader) *Collector {
|
||||
return c
|
||||
}
|
||||
|
||||
// SetCapabilityProber wires the privileged-capability self-check (v0.44.0): each collect runs it
|
||||
// and attaches the snapshot. nil → the report carries an empty []. Returns the collector for chaining.
|
||||
func (c *Collector) SetCapabilityProber(probe func(ctx context.Context) []capability.Status) *Collector {
|
||||
c.capProbe = probe
|
||||
return c
|
||||
}
|
||||
|
||||
// Collect builds the report. Best-effort liveness: a failed NodeStatus is a hard
|
||||
// error (no useful report — the cycle skips the POST); a failed per-guest
|
||||
// GuestConfig degrades that guest to status="unknown" without spec but still sends;
|
||||
@@ -117,8 +126,9 @@ func (c *Collector) Collect(ctx context.Context) (*HostReport, error) {
|
||||
RestoreTests: c.collectRestoreTests(ctx),
|
||||
PBSSnapshots: c.collectPBSSnapshots(ctx),
|
||||
|
||||
AuditTail: []AuditEntry{},
|
||||
Cloudflared: Cloudflared{Status: c.cloudflaredStatus(ctx)},
|
||||
AuditTail: []AuditEntry{},
|
||||
Cloudflared: Cloudflared{Status: c.cloudflaredStatus(ctx)},
|
||||
Capabilities: c.capabilities(ctx),
|
||||
}
|
||||
// DR recipe host-half — derived from the just-collected guest/storage/PBS facts (no new reads).
|
||||
// Secret-free by construction (identifiers/intents/sizes/coordinates only).
|
||||
@@ -126,6 +136,18 @@ func (c *Collector) Collect(ctx context.Context) (*HostReport, error) {
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// capabilities runs the privileged-capability self-check for this report (v0.44.0), or returns an
|
||||
// empty (non-nil) slice when no prober is wired (dev/test). Never fatal — serve-degraded.
|
||||
func (c *Collector) capabilities(ctx context.Context) []capability.Status {
|
||||
if c.capProbe == nil {
|
||||
return []capability.Status{}
|
||||
}
|
||||
if s := c.capProbe(ctx); s != nil {
|
||||
return s
|
||||
}
|
||||
return []capability.Status{}
|
||||
}
|
||||
|
||||
// HostMetricsNow does a FRESH NodeStatus + CPU-temp read and returns just the host block (no
|
||||
// guests/storage). It is the source for the local API's GET /host/metrics (slice 9) — current
|
||||
// cpu%/temp, not the 15-min hub-report snapshot. Storage targets come from the observer
|
||||
|
||||
+10
-1
@@ -1,6 +1,10 @@
|
||||
package hub
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/capability"
|
||||
)
|
||||
|
||||
// HostReport is the wire contract shared with the hub's ingest
|
||||
// (felhom.eu TASK-slice3-hub-ingest). Field NAMES must match the hub
|
||||
@@ -27,6 +31,11 @@ type HostReport struct {
|
||||
Cloudflared Cloudflared `json:"cloudflared"`
|
||||
AuditTail []AuditEntry `json:"audit_tail"` // populated by a later slice
|
||||
|
||||
// Capabilities is the agent's privileged-capability self-check snapshot (v0.44.0): per required
|
||||
// `sudo -n` grant, whether it is permitted + the binary exists. The hub keys its operator alert
|
||||
// on a Critical capability flipping to "degraded". Non-nil so it marshals as [].
|
||||
Capabilities []capability.Status `json:"capabilities"`
|
||||
|
||||
// DR recipe — the agent (storage/guest/PBS) half of the secret-free reconstruction recipe
|
||||
// (SPIKE-dr-recipe-2026-06-16). Derived from the facts above; carries ONLY identifiers/intents/
|
||||
// sizes/coordinates, never a secret. The hub assembles it with the controller's app half.
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/capability"
|
||||
)
|
||||
|
||||
func TestHostReport_FieldNamesAndEmptyCollections(t *testing.T) {
|
||||
@@ -28,6 +30,7 @@ func TestHostReport_FieldNamesAndEmptyCollections(t *testing.T) {
|
||||
PBSSnapshots: []PBSSnapshot{},
|
||||
AuditTail: []AuditEntry{},
|
||||
Cloudflared: Cloudflared{Status: "active"},
|
||||
Capabilities: []capability.Status{},
|
||||
}
|
||||
// dr_recipe is always set on the real path (Collect); set it here too so the "no null" invariant
|
||||
// covers it (empty pbs is omitempty → omitted, never null).
|
||||
@@ -46,6 +49,7 @@ func TestHostReport_FieldNamesAndEmptyCollections(t *testing.T) {
|
||||
`"cloudflared":{"status":"active"}`,
|
||||
// empty collections must be [] not null
|
||||
`"storage_targets":[]`, `"backups":[]`, `"restore_tests":[]`, `"pbs_snapshots":[]`, `"audit_tail":[]`,
|
||||
`"capabilities":[]`,
|
||||
} {
|
||||
if !strings.Contains(got, field) {
|
||||
t.Errorf("report JSON missing %s\n got: %s", field, got)
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
],
|
||||
"cloudflared": { "status": "active" },
|
||||
"audit_tail": [],
|
||||
"capabilities": [],
|
||||
"dr_recipe": {
|
||||
"recipe_version": 1,
|
||||
"guests": [
|
||||
|
||||
Reference in New Issue
Block a user