package hub 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 // field-for-field. Encoding is ordinary encoding/json (no canonicalization — // nothing signs this report; canonical JSON is a slice-10 signing concern). // // The report IS the heartbeat: one periodic POST /api/v1/host-report, whose // server-side received_at is the hub's dead-man's-switch liveness signal. There is // no separate heartbeat endpoint. type HostReport struct { HostID string `json:"host_id"` // echoes config.Hub.HostID ReportedAt string `json:"reported_at"` // RFC3339, agent clock AgentVersion string `json:"agent_version"` Host HostMetrics `json:"host"` Guests []Guest `json:"guests"` // Defined now as the stable contract; emitted EMPTY (non-nil) this slice. StorageTargets []StorageTarget `json:"storage_targets"` // slice 5 (storage manifest) Backups []Backup `json:"backups"` // slice 6 RestoreTests []RestoreTest `json:"restore_tests"` // slice 6 PBSSnapshots []PBSSnapshot `json:"pbs_snapshots"` // slice 6 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"` // LeafFingerprint is the SHA-256 of the local-API leaf the agent CURRENTLY serves (v0.48.0). The // hub records the first value per host as the baseline and raises `host_leaf_changed` if it ever // changes — a proactive, fleet-wide agent-re-key alert independent of any controller's channel // check. Empty when the local API is disabled (no leaf) → the hub treats "" as unknown, never an // alert. Not a secret (the fp is public; the token is never reported). LeafFingerprint string `json:"leaf_fingerprint"` // 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. DRRecipe *DRRecipeHostHalf `json:"dr_recipe"` // Wireguard is the offsite-tunnel status stanza (S3, doc 06 §4.6). Present only when the // wg_tunnel feature is enabled. The report is stored opaquely hub-side, so no hub change is // needed; the pubkey here is the operator's revocation-recovery handle (re-add the peer with // it). Carries NO secret — the pubkey is public by definition. Wireguard *WireguardStatus `json:"wireguard,omitempty"` // SelfUpdatePending is true when an operator-signed agent self-update has flipped the binary // but the new binary has not yet committed (TASK D1). SelfUpdatePendingVersion names the // awaited version when pending. A runs-but-never-commits binary reports pending=true every // heartbeat → the operator sees WHY the version isn't advancing; a crash-loop is auto-rolled // back by systemd and this flips back to false when the good binary re-commits/clears. The // report is stored opaquely hub-side, so these additive fields need no hub-schema change. // Both are `omitempty` (the Wireguard precedent): in the steady state (no update in flight) // they are absent — which keeps the cross-repo host-report golden contract byte-stable without // a hub change. They appear only while an update is pending. The hub reads an absent field as // pending=false, the correct default. SelfUpdatePending bool `json:"selfupdate_pending,omitempty"` SelfUpdatePendingVersion string `json:"selfupdate_pending_version,omitempty"` // MgmtPlane is the management-plane health stanza (TASK G1). It reports whether OpenSSH's SHARED // privilege-separation dir (/run/sshd) is present and whether the stock sshd listener answers, plus // whether the agent-independent watchdog auto-healed a missing dir since boot (and when). This is // the visibility half of the break-glass system: the dumb watchdog fixes /run/sshd with no login, // and this stanza surfaces a RECURRING clobber to the operator BEFORE it becomes a lockout — // complementing host_staleness (which only catches a box gone silent). `omitempty` (the // SelfUpdatePending precedent): stored opaquely hub-side, so these additive fields need no // hub-schema change and are absent when the reporter is not wired. MgmtPlane *MgmtPlaneStatus `json:"mgmt_plane,omitempty"` // PBSDR is the PBS-DR-tier bridge status stanza (slice 2). Present only when the pbsdr // consumer is wired. `consumed_failed` is the LOUD persistent state: the one-time token // secret was consumed but the apply failed afterwards — the secret is burned, the bridge // will NOT silently retry, the operator must Re-issue on the hub. Stored opaquely hub-side // (the Wireguard precedent) — additive, no hub-schema change; hub rendering joins in slice 3. // Carries NO secret. PBSDR *PBSDRStatus `json:"pbs_dr,omitempty"` // OOB is the operator-access health stanza (TASK H1). It answers the operator's question — "can I // get into this box right now, and if not, why" — from the hub: felhom-sshd up + on which port, // locally reachable, the tunnel handshake age (the OOB path rides wg-felhom), whether the operator // peer/key is configured, and whether the config is invalid. The heartbeat reaches the hub over // HTTPS even when felhom-sshd or the tunnel is DOWN (channel independence). `omitempty`: absent // when the feature is not wired (pre-H1) — additive, no hub-schema change. OOB *OOBStatus `json:"oob,omitempty"` } // PBSDRStatus is the per-heartbeat PBS-DR-tier bridge state (slice 2). States: // "adopted" (existing entry verified + reconciled, no consume), "applied" (fresh entry created, // K born), "waiting_secret" (verified but no unconsumed secret staged — retrying), // "verify_failed" (fingerprint/reachability pre-consume check failing — retrying, NOTHING // consumed), "consumed_failed" (LOUD: secret burned, apply failed, no auto-retry — operator // re-issue required), "disabled" (descriptor enabled:false). Carries no secret. type PBSDRStatus struct { State string `json:"state"` StorageID string `json:"storage_id,omitempty"` Namespace string `json:"namespace,omitempty"` Message string `json:"message,omitempty"` ConsumedFailed bool `json:"consumed_failed,omitempty"` AppliedAt string `json:"applied_at,omitempty"` // RFC3339; set on adopted/applied } // OOBStatus is the per-heartbeat operator-access health (TASK H1). Carries no secret. type OOBStatus struct { FelhomSshdActive bool `json:"felhom_sshd_active"` // the felhom-sshd unit is active FelhomSshdPort int `json:"felhom_sshd_port"` // the claimed OOB port (0 = unclaimed) Reachable bool `json:"reachable"` // a local TCP dial to the OOB port succeeds ConfigInvalid bool `json:"config_invalid"` // `sshd -t` on the OOB config fails OperatorPeerConfigured bool `json:"operator_peer_configured"` // oob_peer_ip present in desired-state OperatorKeyConfigured bool `json:"operator_key_configured"` // operator authorized_key installed WGHandshakeAgeS *int64 `json:"wg_handshake_age_s,omitempty"` // wg-felhom last handshake age (nil = unknown) HealedAt string `json:"healed_at,omitempty"` // last felhom-sshd auto-heal (RFC3339) } // MgmtPlaneStatus is the per-heartbeat management-plane health (TASK G1). Carries no secret. // HealedRecently is true while the watchdog's heal-marker exists (a privsep-dir heal happened this // boot); PrivsepHealedAt is that marker's RFC3339 timestamp (absent when no heal has occurred). The // hub raises a warning event on a PrivsepHealedAt it has not alerted on — a recurring auto-heal means // a persistent clobber cause worth investigating before the box locks out. type MgmtPlaneStatus struct { PrivsepDirOK bool `json:"privsep_dir_ok"` // /run/sshd exists (the KEXINIT-reset detector) SshdReachable bool `json:"sshd_reachable"` // the stock sshd listener accepts TCP HealedRecently bool `json:"healed_recently"` // the watchdog heal-marker is present (this boot) PrivsepHealedAt string `json:"privsep_healed_at,omitempty"` // marker timestamp; hub warns on a NEW value } // WireguardStatus is the per-heartbeat offsite-tunnel status (S3). LastHandshakeAgeS is nil when // the handshake age is unreadable (service down, capability degraded) — nil ≠ 0. type WireguardStatus struct { Pubkey string `json:"pubkey"` Registered bool `json:"registered"` // the registration marker exists Active bool `json:"active"` // wg-quick@wg-felhom is-active LastHandshakeAgeS *int64 `json:"last_handshake_age_s,omitempty"` AssignedIP string `json:"assigned_ip,omitempty"` // from the marker, e.g. "10.77.0.2/32" } // HostMetrics is the host block, sourced from proxmox NodeStatus. type HostMetrics struct { Node string `json:"node"` CPUPercent float64 `json:"cpu_percent"` // 0–100 MemoryTotalBytes int64 `json:"memory_total_bytes"` MemoryUsedBytes int64 `json:"memory_used_bytes"` MemoryPercent float64 `json:"memory_percent"` DiskTotalBytes int64 `json:"disk_total_bytes"` // host root fs DiskUsedBytes int64 `json:"disk_used_bytes"` DiskPercent float64 `json:"disk_percent"` LoadAvg []string `json:"loadavg"` // array of STRINGS (PVE shape) UptimeSeconds int64 `json:"uptime_seconds"` // CPUTempC is the host CPU/chassis temperature in whole °C, or null when the hardware // exposes no usable sensor (a headless VM, an unsupported board, or any read error all // degrade to null — a missing sensor never fails the report). Same nullable contract as // the per-disk SmartSummary.TemperatureC. Sourced from sysfs (hwmon / thermal zones). // Cross-repo wire field (slice 9) — the hub's HostMetrics copy + golden carry it too. CPUTempC *int `json:"cpu_temp_c"` } // Guest is one LXC. The agent reports vmid; the hub derives the guest PK // "/" (keeping the id scheme hub-side — locked decision 4). type Guest struct { VMID int `json:"vmid"` Name string `json:"name"` Status string `json:"status"` // running | stopped | unknown ControllerVersion string `json:"controller_version"` // "" this slice (slice 8 fills) Spec *GuestSpec `json:"spec,omitempty"` // omitted when status unknown } // GuestSpec is the provisioned guest sizing. type GuestSpec struct { Cores int `json:"cores"` MemoryBytes int64 `json:"memory_bytes"` DiskBytes int64 `json:"disk_bytes"` } // Cloudflared is the tunnel service health (read-only probe this slice). type Cloudflared struct { Status string `json:"status"` // active | inactive | failed | unknown } // The following element types are declared now so the empty collections above are // typed and slices 5/6 only fill them. // StorageTarget is one observed host storage target (doc 03 §7). It is the REPORTED // shape — what the agent observes and tells the hub. The hub holds the AUTHORITATIVE // manifest (desired class/role/policy/creds, slice 10); the agent reports what it sees // and reconciles toward the hub's manifest. So `class_hint` is a rotational HINT, never // authoritative class, and `role` is set only when derivable from an existing Proxmox // storage definition (else empty — the hub owns it). // // This is a cross-repo contract DUPLICATED in felhom.eu/hub (no shared module yet); // testdata/host-report.golden.json must stay byte-identical with the hub's copy and the // bidirectional key-set test (contract_test.go) guards drift. type StorageTarget struct { Name string `json:"name"` // the Proxmox storage id (its name) Type string `json:"type"` // local-dir | lvmthin | usb | nfs | cifs | pbs | local DurableID string `json:"durable_id"` // fs-UUID (usb/local-dir) | server:export (nfs/cifs) | repo+fingerprint (pbs) // State is the observed lifecycle state. attached (present & usable) | disconnected // (a KNOWN target whose backing device/mount/reachability dropped) | decommissioned // (hub-manifest state — built but not served until slice 10). State string `json:"state"` Reachable bool `json:"reachable"` // backing device present + mounted (local) / reachable (network) TotalBytes int64 `json:"total_bytes"` UsedBytes int64 `json:"used_bytes"` AvailBytes int64 `json:"avail_bytes"` UsedFraction float64 `json:"used_fraction"` Content string `json:"content"` // Proxmox content list, e.g. "rootdir,images" / "backup,vztmpl" MountPath string `json:"mount_path"` // host mountpoint (dir/usb); "" for network/lvm BackingDevice string `json:"backing_device"` // resolved block device (e.g. /dev/sdb1); "" for network // ClassHint is a fast|slow HINT derived from the backing disk's rotational flag — a // hint only; the authoritative class is hub-owned (locked decision). "" when not // derivable (network targets have no local rotational flag). ClassHint string `json:"class_hint"` // Role is primary|vzdump-target|pbs-offsite|bulk-data when derivable from the existing // storage definition; else "" (the manifest role is hub-owned, slice 10). Role string `json:"role"` // ThinPool carries the lvmthin DATA fill prominently — a full thin-pool corrupts every // guest on it (the storage analog of single-node OOM). Present ONLY for lvmthin targets; // metadata fill is null until Phase B's privileged `lvs` read. ThinPool *ThinPoolFill `json:"thin_pool,omitempty"` // Smart is the disk-health summary, populated in Phase B via smartctl (sudoers). In // Phase A it is {health:"UNKNOWN", } — the keys are committed now so the // contract is stable across both phases. Smart SmartSummary `json:"smart"` } // ThinPoolFill is the lvmthin pool fill (doc 03 §7). DataUsedFraction is the live data // fill (used/total of the lvmthin store); MetadataUsedFraction needs the privileged // `lvs` read (Phase B) and is null until then. type ThinPoolFill struct { DataUsedFraction float64 `json:"data_used_fraction"` MetadataUsedFraction *float64 `json:"metadata_used_fraction"` } // SmartSummary is a read-only disk-health summary. Health is PASSED|FAILING|UNKNOWN. // Counters are pointers so "unknown / not-applicable for this device type" (e.g. a USB // bridge that exposes no SMART, or NVMe counters on a SATA disk) is null, distinct from a // real zero. The SATA set (reallocated/pending/offline-uncorrectable) and the NVMe set // (critical_warning/media_errors/percentage_used) are both carried; a device populates // only its own set. Filled in Phase B. type SmartSummary struct { Health string `json:"health"` TemperatureC *int `json:"temperature_c"` PowerOnHours *int `json:"power_on_hours"` // SATA attributes. ReallocatedSectors *int `json:"reallocated_sectors"` PendingSectors *int `json:"pending_sectors"` OfflineUncorrectable *int `json:"offline_uncorrectable"` // NVMe attributes. CriticalWarning *int `json:"critical_warning"` MediaErrors *int `json:"media_errors"` PercentageUsed *int `json:"percentage_used"` } // SMART health constants (the reported vocabulary). const ( SmartPassed = "PASSED" SmartFailing = "FAILING" SmartUnknown = "UNKNOWN" ) // Storage target type + state constants (the reported vocabulary; doc 03 §7). const ( StorageTypeLocalDir = "local-dir" StorageTypeLVMThin = "lvmthin" StorageTypeUSB = "usb" StorageTypeNFS = "nfs" StorageTypeCIFS = "cifs" StorageTypePBS = "pbs" StorageTypeLocal = "local" // builtin dir storage (PVE "local") StorageStateAttached = "attached" StorageStateDisconnected = "disconnected" StorageStateDecommissioned = "decommissioned" ) // Backup is the latest guest-vzdump result per target (doc 03 §8, slice 6 Phase A). An // agent-initiated vzdump is CRASH-CONSISTENT only (no fsfreeze; app-consistency needs the // controller quiesce, slice 8) — marked so here. UncoveredVolumes lists the guest's // backup=0 (or backup-unset) mountpoints excluded from the vzdump — the bulk-volume DR gap // (the bulk-backup mechanism is slice 10). Cross-repo contract: keep byte-identical with // felhom.eu/hub and the bidirectional golden key-set test. type Backup struct { TargetID string `json:"target_id"` // backup storage name (e.g. "local") VMID int `json:"vmid"` // source guest Archive string `json:"archive"` // produced vzdump volid Mode string `json:"mode"` // snapshot | stop CrashConsistent bool `json:"crash_consistent"` // always true this slice SizeBytes int64 `json:"size_bytes"` Success bool `json:"success"` Error string `json:"error,omitempty"` StartedAt string `json:"started_at"` // RFC3339 DurationSeconds float64 `json:"duration_seconds"` UncoveredVolumes []string `json:"uncovered_volumes"` // backup=0/unset mountpoints (bulk gap) } // RestoreTest is the latest self-restore-test result (doc 03 §8). This slice verifies // boot+running only (deep app-health is slice 8). SourceTier is "local" here; PBS is Phase B. type RestoreTest struct { SourceArchive string `json:"source_archive"` SourceTier string `json:"source_tier"` // "local" (pbs = Phase B) ScratchVMID int `json:"scratch_vmid"` Pass bool `json:"pass"` Verified string `json:"verified"` // "boot+running" this slice Error string `json:"error,omitempty"` TestedAt string `json:"tested_at"` // RFC3339 DurationSeconds float64 `json:"duration_seconds"` // Warnings are the guest-start task's warning line(s) (e.g. the systemd-nesting advisory). // Present on a PASS that emitted warnings; pass/fail itself is liveness-only, so a passed // restore-test can carry warnings. Omitted when there are none. Warnings []string `json:"warnings,omitempty"` // WarningsRecognized is true iff every Warnings line is the known-benign anchor. Omitted // (⇒ false) when absent — and false is the SAFE default: the hub then treats it as an // unrecognized warning (louder), so a missing flag can only over-notice, never hide. WarningsRecognized bool `json:"warnings_recognized,omitempty"` // MountParity ("ok"|"mismatch"; omitted on pre-v0.76.0 agents) + MountInventory (the verified // mpN set) carry the GL-5b full-fidelity proof: the restored scratch matched the ARCHIVE's // mount layout, not just booted. Additive — a hub that predates them ignores the unknown keys. MountParity string `json:"mount_parity,omitempty"` MountInventory []string `json:"mount_inventory,omitempty"` } // PBSSnapshot is one PBS (offsite) snapshot's inventory + integrity state (doc 03 §8, slice // 6 Phase B). Sourced from the PBS API (internal/pbs). `verify_state` is the load-bearing // field — "none" until a verify runs, then "ok"/"failed" (a failed verify is the loudest // offsite-DR signal). `encrypted` is derived from the snapshot's data crypt-mode // (zero-knowledge: the PBS server can't read it). Cross-repo contract — byte-identical golden // + bidirectional key-set test, the slice-5/6 pattern. type PBSSnapshot struct { Namespace string `json:"namespace"` // "root" = default ns BackupType string `json:"backup_type"` // ct | vm BackupID string `json:"backup_id"` BackupTime string `json:"backup_time"` // RFC3339 SizeBytes int64 `json:"size_bytes"` Owner string `json:"owner"` Protected bool `json:"protected"` Encrypted bool `json:"encrypted"` VerifyState string `json:"verify_state"` // ok | failed | none VerifyUPID string `json:"verify_upid,omitempty"` } type AuditEntry struct{} // audit-log tail entry fields TBD // ControlEnvelope is the hub's 200 response to a host-report — the "Down" channel (slice 10A). // It is a cheap change-notification on every heartbeat: the agent adopts PollIntervalSeconds, // and when DesiredGeneration ADVANCES past its cached one it fetches the full desired-state from // GET /hosts/{id}/desired-state (the heavy state moves only on change). HasSignedOps flags a // non-empty signed-jobs queue (the agent fetches/executes them in 10B). Blocked stays reserved. type ControlEnvelope struct { Status string `json:"status"` // PollIntervalSeconds is a pointer so a missing field (keep current interval) is // distinguishable from an explicit 0. PollIntervalSeconds *int `json:"poll_interval_seconds"` Blocked bool `json:"blocked"` // reserved — ignored DesiredGeneration int64 `json:"desired_generation"` // slice 10A: the cached-vs-current change signal HasSignedOps bool `json:"has_signed_ops"` // slice 10A: signed-jobs queue non-empty (exec 10B) } // DesiredStateResponse is GET /hosts/{host_id}/desired-state (slice 10A — the "Down" channel's // heavy payload, fetched only when the envelope's generation advances). Generation is the // generation this state corresponds to, so the agent caches state+generation atomically. This is // a cross-repo wire contract (DUPLICATED in felhom.eu/hub until a shared module exists); the // desired-state golden stays byte-identical across the two repos. type DesiredStateResponse struct { Generation int64 `json:"generation"` DesiredState WireDesiredState `json:"desired_state"` } // WireDesiredState is the hub's authoritative per-host target (slice 10A). The agent reconciles the // parts it can today (guests: benign deltas reconciled, an explicit decommission gated // pending_signature); the rest are FORWARD-COMPAT — carried + cached, NOT acted on in 10A. The // restore_directive is consumed in 10D (host/guest-loss DR); storage_manifest / backup_policy / // pbs_namespace are placeholders kept opaque so the wire is stable as those land. The wireguard // block (S3) is HUB-OWNED state merged into the served document at read time (hub S2) — consumed // by internal/wgtunnel via the desired.Syncer raw-consumer seam. type WireDesiredState struct { Guests []WireDesiredGuest `json:"guests"` StorageManifest json.RawMessage `json:"storage_manifest,omitempty"` BackupPolicy json.RawMessage `json:"backup_policy,omitempty"` PBSNamespace string `json:"pbs_namespace,omitempty"` RestoreDirective *WireRestoreDirective `json:"restore_directive,omitempty"` // slice 10D (forward-compat) Wireguard *WireWireguard `json:"wireguard,omitempty"` // S3 (doc 06 §3.2; golden-pinned) PBSDR *WirePBSDR `json:"pbs_dr,omitempty"` // PBS DR tier (slice 2 consumer) } // WirePBSDR is the hub's PBS-DR-tier descriptor (PBS DR slice 1, hub/internal/web/pbsdr.go // pbsDRDescriptor — field-exact, cross-repo). NON-SECRET by contract: the token secret NEVER // rides the desired-state; the agent fetches it consume-once via ConsumePBSToken. Absent/nil on // pre-v0.44.0 hubs → the pbsdr consumer no-ops (old-hub compat). type WirePBSDR struct { Enabled bool `json:"enabled"` StorageID string `json:"storage_id,omitempty"` PBSTunnelIP string `json:"pbs_tunnel_ip,omitempty"` Datastore string `json:"datastore,omitempty"` Namespace string `json:"namespace,omitempty"` TokenID string `json:"token_id,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` } // WireWireguard is the hub-owned offsite-tunnel assignment (S3) — field-exact with the S2 golden // (testdata/desired-state-wireguard.golden.json, byte-identical hub copy). Client-side // AllowedIPs, PersistentKeepalive=25 and MTU 1280 are deliberately NOT wire fields — wgtunnel // constants derived from endpoint.pbs_tunnel_ip + doc 06 §4. type WireWireguard struct { Endpoint WireWireguardEndpoint `json:"endpoint"` Pubkey string `json:"pubkey"` // the box's registered pubkey AssignedIP string `json:"assigned_ip"` // e.g. "10.77.0.2/32" // OOBPeerIP is the operator OOB peer's tunnel address (bare IPv4, e.g. "10.77.0.250", TASK H1). // When set, the agent appends it as a second /32 to the box's wg-felhom AllowedIPs so packets the // endpoint forwards FROM the operator are accepted — RENDERED into the conf (not a runtime `wg // set`) so it survives self-heal/restart/reboot ([OF-1]: a runtime widening was wiped by the // agent's own self-heal). `omitempty`: absent = OOB off, byte-identical conf for pre-H1 hosts. OOBPeerIP string `json:"oob_peer_ip,omitempty"` // OOBOperatorSSHKey is the operator's SSH PUBLIC key (an authorized_keys line, TASK H1). The agent // writes it to felhom-sshd's dedicated AuthorizedKeysFile (/etc/felhom-sshd/authorized_keys/{root, // felhom-op}) — OUTSIDE ~/.ssh, so the customer's sshd never honours it [SF-3]. Hub-driven so it // rotates fleet-wide. `omitempty`: absent = no operator login installed. NOT a secret (public key). OOBOperatorSSHKey string `json:"oob_operator_ssh_key,omitempty"` } // WireWireguardEndpoint is the endpoint half of the wireguard block. type WireWireguardEndpoint struct { DNSName string `json:"dns_name"` WGPort int `json:"wg_port"` ServerPubkey string `json:"server_pubkey"` PBSTunnelIP string `json:"pbs_tunnel_ip"` } // WireDesiredGuest is one guest's target (slice 10A). Every field is optional ("unmanaged"); the // agent's planner acts only on the fields that are set. Run is running|stopped|""; Spec reuses // GuestSpec (cores/memory_bytes/disk_bytes); Decommission is the EXPLICIT destructive delta (gated // pending_signature in 10A — executor is 10B). type WireDesiredGuest struct { VMID int `json:"vmid"` Run string `json:"run,omitempty"` Spec *GuestSpec `json:"spec,omitempty"` Description *string `json:"description,omitempty"` Decommission bool `json:"decommission,omitempty"` } // WireRestoreDirective is the forward-compat restore directive (slice 10D — host/guest-loss DR). // Defined now so the wire contract is stable; 10A carries it through to the cache but does NOT // consume it (no restore is initiated from desired-state in 10A). type WireRestoreDirective struct { Mode string `json:"mode,omitempty"` // guest_loss | host_loss (10D vocabulary) Archive string `json:"archive,omitempty"` // source archive/snapshot to restore from VMID int `json:"vmid,omitempty"` }