v0.23.0: device-ROLE classification + tiered storage-wipe gate (user-data customer-confirmable; system/backup operator-only)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 21:29:18 +02:00
parent 9e3513557f
commit 15f7529a1c
10 changed files with 759 additions and 106 deletions
+23 -11
View File
@@ -43,7 +43,7 @@ import (
// version is the agent version. Overridable at build time with
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
var version = "0.22.0"
var version = "0.23.0"
func main() {
var (
@@ -580,21 +580,33 @@ func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.St
return srv
}
// storageGateAdapter bridges the local-API disk-format path to the slice-4 reversibility gate
// (8C): a data-bearing format is a ClassStorageWipe destructive op, so with no operator signature
// the gate returns pending_signature and the agent refuses. The signed completion is slice 10.
// storageGateAdapter bridges the local-API disk-format path to the reversibility gate, TIERED by the
// agent's authoritative device-role verdict: a USER-DATA data-bearing wipe is customer-confirmable
// (durable-id-bound, no signature); a SYSTEM/BACKUP wipe stays operator-signature (unsigned →
// pending_signature; the signed completion runs via the signed-jobs runner).
type storageGateAdapter struct {
gate *reconcile.Gate
hostID string
}
func (a storageGateAdapter) AuthorizeWipe(device string) (bool, string) {
params, _ := json.Marshal(map[string]string{"device": device, "op": "format"})
dec := a.gate.Authorize(
reconcile.IntentForStorageDestructive(reconcile.ClassStorageWipe, a.hostID, device, params, reconcile.SourceOneShotJob),
nil, // no operator signature in 8C → pending_signature
)
return dec.Allowed, string(dec.Reason)
func (a storageGateAdapter) AuthorizeWipe(req localapi.WipeRequest) localapi.WipeDecision {
dec := a.gate.AuthorizeStorageWipe(reconcile.StorageWipeAuthz{
HostID: a.hostID,
Role: req.Role,
DeviceDurableID: req.DeviceDurableID,
Confirmed: req.Confirmed,
ConfirmDurableID: req.ConfirmDurableID,
}, nil) // no operator signature on the inline path → system/backup is pending_signature
tier := "destructive"
if dec.Disposition == reconcile.CustomerConfirmable {
tier = "customer_confirmable"
}
return localapi.WipeDecision{
Allowed: dec.Allowed,
Tier: tier,
Reason: string(dec.Reason),
NeedsConfirmation: dec.Reason == reconcile.ReasonPendingConfirmation,
}
}
// reconcileJournalPath chooses the op-journal path: a `journal.log` sibling of the