v0.44.0: role-aware drive management — protected lockout + customer type-to-confirm wipe + drive-list restyle
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -184,8 +184,15 @@ type DiskInfo struct {
|
||||
BackingDevice string `json:"backing_device"`
|
||||
MountPath string `json:"mount_path"`
|
||||
Class string `json:"class"`
|
||||
DataBearing bool `json:"data_bearing"`
|
||||
DataReason string `json:"data_reason"`
|
||||
// Role is the agent's AUTHORITATIVE protection tier: "system" | "backup" | "user-data". The UI
|
||||
// is driven from it — system/backup get a lock badge and NO destructive controls; user-data is
|
||||
// customer-manageable (eject/wipe with type-to-confirm).
|
||||
Role string `json:"role"`
|
||||
DataBearing bool `json:"data_bearing"`
|
||||
DataReason string `json:"data_reason"`
|
||||
TotalBytes int64 `json:"total_bytes"`
|
||||
UsedBytes int64 `json:"used_bytes"`
|
||||
UsedFraction float64 `json:"used_fraction"`
|
||||
// DurableID is the target's stable identity (e.g. "uuid:<fs-uuid>" for usb/local-dir). The
|
||||
// fs UUID (strip the "uuid:" prefix) is the key the controller passes to AssignDisk — it's the
|
||||
// only way the de-privileged controller learns a mount key it cannot read off the device itself.
|
||||
@@ -209,12 +216,19 @@ type DisksResponse struct {
|
||||
|
||||
// FormatResult mirrors POST /disks/format (the success/refusal payload).
|
||||
type FormatResult struct {
|
||||
VMID int `json:"vmid"`
|
||||
Device string `json:"device"`
|
||||
Formatted bool `json:"formatted"`
|
||||
DataBearing bool `json:"data_bearing"`
|
||||
Reason string `json:"reason"`
|
||||
PendingOp *PendingOp `json:"pending_op,omitempty"`
|
||||
VMID int `json:"vmid"`
|
||||
Device string `json:"device"`
|
||||
Formatted bool `json:"formatted"`
|
||||
DataBearing bool `json:"data_bearing"`
|
||||
Reason string `json:"reason"`
|
||||
// Role is the agent's tier for this device (system | backup | user-data).
|
||||
Role string `json:"role,omitempty"`
|
||||
// NeedsConfirmation is set on a USER-DATA data-bearing refusal: re-submit with confirmed=true +
|
||||
// DurableID after the type-to-confirm UI (NOT an operator signature).
|
||||
NeedsConfirmation bool `json:"needs_confirmation,omitempty"`
|
||||
DurableID string `json:"durable_id,omitempty"`
|
||||
// PendingOp is set on a SYSTEM/BACKUP data-bearing refusal — the operator-signature op.
|
||||
PendingOp *PendingOp `json:"pending_op,omitempty"`
|
||||
}
|
||||
|
||||
// PendingOp mirrors the agent's bound destructive intent on a data-bearing refusal. The controller
|
||||
@@ -231,9 +245,14 @@ func (p PendingOp) OpsignCommand() string {
|
||||
return fmt.Sprintf("felhom-opsign -op %s -host %s -durable-id %s", p.Op, p.HostScope, p.DurableID)
|
||||
}
|
||||
|
||||
// ErrFormatRefused is returned by FormatDisk when the agent refuses a data-bearing format
|
||||
// (pending operator authorization — the 8C invariant). The UI surfaces this distinctly.
|
||||
var ErrFormatRefused = fmt.Errorf("agentapi: format refused — device is data-bearing (operator authorization required)")
|
||||
// ErrFormatRefused is returned by FormatDisk when the agent refuses a data-bearing format on a
|
||||
// SYSTEM/BACKUP device (operator signature required). The UI surfaces the pending opsign command.
|
||||
var ErrFormatRefused = fmt.Errorf("agentapi: format refused — system/backup device (operator authorization required)")
|
||||
|
||||
// ErrNeedsConfirmation is returned by FormatDisk when the agent refuses a data-bearing format on a
|
||||
// USER-DATA device pending the CUSTOMER's informed-confirmation (bound to FormatResult.DurableID).
|
||||
// The UI surfaces the type-to-confirm flow, then re-submits with confirmed=true + that durable id.
|
||||
var ErrNeedsConfirmation = fmt.Errorf("agentapi: format needs customer confirmation — user-data device")
|
||||
|
||||
// Disks lists the host drives the agent manages, with a data-bearing flag per drive.
|
||||
func (c *Client) Disks(ctx context.Context) (DisksResponse, error) {
|
||||
@@ -276,14 +295,22 @@ func (c *Client) EjectDisk(ctx context.Context, where string) (EjectResult, erro
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// FormatDisk asks the agent to format a device. The AGENT inspects the device and decides
|
||||
// data-bearing-ness — a data-bearing device is refused (ErrFormatRefused), the controller's claim
|
||||
// is irrelevant. Only a device the agent reads as blank is formatted.
|
||||
func (c *Client) FormatDisk(ctx context.Context, device, fstype string) (FormatResult, error) {
|
||||
// FormatDisk asks the agent to format a device. The AGENT inspects the device and tiers it by ROLE
|
||||
// (its own classification, never the controller's claim):
|
||||
// - blank device → formatted.
|
||||
// - user-data, data-bearing, NOT confirmed → ErrNeedsConfirmation (out.DurableID = the id to confirm).
|
||||
// - user-data, data-bearing, confirmed + matching durable id → formatted.
|
||||
// - system/backup, data-bearing → ErrFormatRefused (out.PendingOp = the operator opsign command).
|
||||
//
|
||||
// confirmed + durableID authorize a user-data wipe (the durable id the agent gave on the prior
|
||||
// ErrNeedsConfirmation); they are inert for system/backup.
|
||||
func (c *Client) FormatDisk(ctx context.Context, device, fstype string, confirmed bool, durableID string) (FormatResult, error) {
|
||||
var out FormatResult
|
||||
// Status-aware POST: the agent returns the FULL FormatResponse (incl. pending_op) even on the
|
||||
// 403 refusal, so we must read the body on non-2xx rather than discarding it.
|
||||
data, status, err := c.postWithStatus(ctx, "/disks/format", map[string]string{"device": device, "fstype": fstype})
|
||||
// Status-aware POST: the agent returns the FULL FormatResponse (incl. pending_op / durable_id)
|
||||
// even on the 403 refusal, so we must read the body on non-2xx rather than discarding it.
|
||||
data, status, err := c.postWithStatus(ctx, "/disks/format", map[string]any{
|
||||
"device": device, "fstype": fstype, "confirmed": confirmed, "durable_id": durableID,
|
||||
})
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
@@ -291,10 +318,16 @@ func (c *Client) FormatDisk(ctx context.Context, device, fstype string) (FormatR
|
||||
if len(data) > 0 {
|
||||
_ = json.Unmarshal(data, &out) // best-effort; fields default on a missing/partial body
|
||||
}
|
||||
if out.Formatted {
|
||||
return out, nil
|
||||
}
|
||||
if out.NeedsConfirmation {
|
||||
out.DataBearing = true
|
||||
return out, ErrNeedsConfirmation // user-data: surface the type-to-confirm flow
|
||||
}
|
||||
if status == http.StatusForbidden || (out.DataBearing && !out.Formatted) {
|
||||
out.DataBearing = true
|
||||
out.Formatted = false
|
||||
return out, ErrFormatRefused // carries PendingOp for the caller to surface the opsign command
|
||||
return out, ErrFormatRefused // system/backup: surface the opsign command
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user