slice 8C Phase A: agent disk endpoints + data-bearing classifier gate + mkfs (v0.12.0)

internal/storage: mkfs executor (Format, device-pinned, narrow FELHOM_FORMAT
sudoers) + data-bearing device inspection (InspectDevice/DeviceProbe via
blkid+lsblk; conservative — ambiguous=data-bearing). internal/localapi: /disks
(+ data-bearing flag), /disks/assign (EnsureMount), /disks/eject (Unmount +
dependent guests), /disks/format. SECURITY CENTERPIECE: the agent inspects the
device itself; data-bearing format -> ClassStorageWipe gate -> pending_signature
refused; the caller's claim is never trusted. Additive (no controller change yet).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 12:52:22 +02:00
parent 4d9e76e66a
commit c17cfde236
10 changed files with 1074 additions and 10 deletions
+23 -5
View File
@@ -65,7 +65,13 @@ type Options struct {
// is "due" when no successful backup is recorded OR the newest one is older than this. 0 → a
// safe default (24h). The hub-served policy is slice 10; this is the agent-local cadence.
BackupCadence time.Duration
Logger *slog.Logger
// Disk management (slice 8C) — OPTIONAL. When Disks + DiskGate are set, the /disks endpoints
// are served; otherwise they report "not configured". DiskGate authorizes the destructive
// (data-bearing) format path; Guests lists guests for the eject dependent-warning.
Disks DiskOps
DiskGate StorageGate
Guests2 GuestLister
Logger *slog.Logger
}
// defaultBackupCadence is the fallback /backup/due window when none is configured.
@@ -104,6 +110,10 @@ type Server struct {
logger *slog.Logger
now func() time.Time
disks DiskOps // slice 8C (optional)
diskGate StorageGate // slice 8C (optional)
guestList GuestLister // slice 8C (optional)
jobsMu sync.Mutex
jobs map[int]*backupJob // per-guest backup job state (slice 8B)
@@ -133,10 +143,13 @@ func NewServer(o Options) (*Server, error) {
store: o.Store,
storage: o.Storage,
tokens: o.Tokens,
cadence: cadence,
logger: o.Logger,
now: func() time.Time { return time.Now().UTC() },
jobs: map[int]*backupJob{},
cadence: cadence,
logger: o.Logger,
now: func() time.Time { return time.Now().UTC() },
disks: o.Disks,
diskGate: o.DiskGate,
guestList: o.Guests2,
jobs: map[int]*backupJob{},
}, nil
}
@@ -150,6 +163,11 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("GET /backup/due", s.withGuest(s.handleBackupDue))
mux.HandleFunc("GET /backup/status", s.withGuest(s.handleBackupStatus))
mux.HandleFunc("GET /restore-test/status", s.withGuest(s.handleRestoreTestStatus))
// Disk management (slice 8C) — self-scoped; format routes through the data-bearing classifier+gate.
mux.HandleFunc("GET /disks", s.withGuest(s.handleDisks))
mux.HandleFunc("POST /disks/assign", s.withGuest(s.handleDiskAssign))
mux.HandleFunc("POST /disks/eject", s.withGuest(s.handleDiskEject))
mux.HandleFunc("POST /disks/format", s.withGuest(s.handleDiskFormat))
return mux
}