v0.317.0: a second copy counts only on a proven different disk; the no-drive install speaks in words (felhom.eu architecture/16, build A)
gates / gates (push) Successful in 46s

Tier-2 and the one-copy notice ask the host agent (POST /disks/same-disk, agent 0.157.0): different +
proven -> made and counted; same disk -> not made (D1); unknown (VM, hidden USB serial) -> made, not
counted, the row says why (D2); a NAS never counts and no longer hides the notice (R-1075). An older
agent keeps the st_dev rule, logged once. D4: a data-folder or media app on a box with no drive is
refused in words with a link to the drives page (R-1068's raw-error half). MinAgent 0.157.0.

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-15 21:21:54 +02:00
parent c6a567122e
commit 0626b9fc8a
34 changed files with 665 additions and 200 deletions
+29
View File
@@ -355,6 +355,12 @@ type DiskInfo struct {
// agent predates the field — the disk-health card + 6h degradation check feature-detect on this and
// render "Nincs adat" (never alarm) when nil. See DiskVerdictFor.
Smart *SmartSummary `json:"smart,omitempty"`
// Physical-disk identity (agent v0.157.0, `16` §3): kind, the keys of the physical disks under the place,
// proven|unknown + reason. Empty from an older agent or for an absent drive.
Kind string `json:"kind,omitempty"`
PhysicalDisks []string `json:"physical_disks,omitempty"`
Identity string `json:"identity,omitempty"`
IdentityReason string `json:"identity_reason,omitempty"`
}
// FSUUID returns the raw filesystem UUID from a "uuid:<…>" DurableID, or "" if this disk's identity
@@ -419,6 +425,29 @@ var ErrFormatRefused = fmt.Errorf("agentapi: format refused — system/backup de
// 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")
// SameDiskResult is the agent's answer to POST /disks/same-disk (v0.157.0). Act on Proven first:
// Proven+Different = a counted second copy; Proven+!Different = the same disk; !Proven = unknown
// (Reason vm|network|usb_bridge|weak_wwn|no_serial|unmapped).
type SameDiskResult struct {
Different bool `json:"different"`
Proven bool `json:"proven"`
Reason string `json:"reason,omitempty"`
}
// SameDisk asks the agent whether two guest paths sit on the same physical disk. Gate it with
// Supports(FeatureSameDisk) — an older agent answers 404.
func (c *Client) SameDisk(ctx context.Context, a, b string) (SameDiskResult, error) {
var out SameDiskResult
raw, err := c.post(ctx, "/disks/same-disk", map[string]string{"a": a, "b": b})
if err != nil {
return out, err
}
if err := json.Unmarshal(raw, &out); err != nil {
return out, fmt.Errorf("agentapi: decode /disks/same-disk: %w", err)
}
return out, nil
}
// Disks lists the host drives the agent manages, with a data-bearing flag per drive.
func (c *Client) Disks(ctx context.Context) (DisksResponse, error) {
var out DisksResponse
+10
View File
@@ -88,6 +88,11 @@ const FeatureRetainedRecoveryClass Feature = "retained_recovery_class"
// Unknown it stays, and an old agent's 404 is answered with the update sentence.
const FeatureHostPowerOff Feature = "host_poweroff"
// FeatureSameDisk — POST /disks/same-disk (agent v0.157.0, felhom.eu architecture/16-storage-physical-identity.md
// §4): the agent's physical-disk verdict for two guest paths. Below it the Tier-2 runner keeps the old st_dev
// rule (backup.Manager.diskRelation), logged once.
const FeatureSameDisk Feature = "same_disk"
// SupportState is a probe verdict. The zero value is SupportUnknown (fail-open: unknown never
// refuses — the existing agent-error paths speak honestly when the agent is down).
type SupportState int
@@ -168,6 +173,9 @@ var featureProbes = map[Feature]func(ctx context.Context, p SupportProber) error
FeatureHostPowerOff: func(ctx context.Context, p SupportProber) error {
return errNoRecoveryProbe
},
FeatureSameDisk: func(ctx context.Context, p SupportProber) error {
return errNoRecoveryProbe // not probeable: decided by the version header alone
},
}
// errNoMemoryProbe classifies to SupportUnknown (not a *StatusError 404), so a prober that cannot be
@@ -204,6 +212,8 @@ var featureMinAgent = map[Feature]string{
// R-998 — POST /host/poweroff, the household's „A gép kikapcsolása".
FeatureHostPowerOff: "0.155.0",
FeatureSameDisk: "0.157.0",
}
// MinAgentFor returns the declared minimum agent version for a feature ("" when the feature has no