Commit Graph

158 Commits

Author SHA1 Message Date
courtmanr@gmail.com 72599bd1ec Allow one Proxmox bootstrap per canonical type on combined hosts (#1644)
A host running both PVE and PBS is a deployment the docs call officially
supported, and the agent's RunAll registers each product in turn from the
one install token. The bootstrap grant recorded consumption per TOKEN, so
the PVE leg spent it, the PBS leg came back canRegister=false, the agent
wrote a proxmox-pbs-registration-blocked marker, and install.sh printed an
ERROR banner over a PVE source that had registered perfectly well.

- server: consumption is now recorded per canonical type. One PVE create
  and one PBS create per token, each still one-shot — a second create of
  the same type takes the same 403. The bounds that are not per type stay
  singular: the 24h mint-age clock and the first-use bound_hostname are
  shared, so whichever type registers first pins the hostname for both and
  the second type cannot be aimed at another machine. The per-type ledger
  lives in proxmox_registration_consumed_types; a record carrying only
  proxmox_registration_completed=true predates it and still reads as every
  type consumed, so upgrading cannot revive a token already spent in the
  field. The unsuffixed completion block keeps tracking the most recent
  completion, which also leaves an older binary reading the same store
  failing closed.

- rollback: the consume-before-persist undo is scoped to the keys one
  consumption wrote, so a failed PBS source save restores the PBS grant
  without resurrecting the PVE grant that already produced a source.

- agent: RunAll no longer lets one product's failure speak for the host. It
  attempts and returns the remaining products, errors only when every
  detected product failed, and publishes the detected products in a
  proxmox-detected-types state marker.

- installer: report_proxmox_registration_outcome reads that marker, waits
  for an outcome from each detected product, and prints a success or denial
  line per product instead of one verdict. Agents predating the marker keep
  the old first-outcome-wins timing so a single-product host does not wait
  out the window. The blocked-marker path now only fires on genuine refusals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:03:55 +01:00
courtmanr@gmail.com ac43506e6e Fix Proxmox registration for host-token installs
The Settings > Infrastructure installer mints generic host install
tokens, but install.sh auto-detects Proxmox and the agent presents type
pve/pbs at /api/auto-register. The bootstrap grant required an exact
install_type match, so every generic install on a Proxmox node was
denied source creation and the denial was a single buried journal warn.

Four-part fix (#1644):
- server: extend the one-shot bootstrap grant to host-issued install
  tokens presenting a canonical Proxmox type. Typed tokens stay pinned,
  the grant keeps its settings-write mint requirement, first-hostname
  binding, serialized completion, and single consumption across types.
- agent: a canRegister=false denial now logs at error level, returns a
  setup error, and records the operator-facing reason in a
  proxmox-<type>-registration-blocked state marker.
- installer: report the Proxmox registration outcome in install output
  by reading the registered/blocked markers, and poll the server lookup
  for a bounded retry window before warning that registration was not
  confirmed (readyz flips before the first report cycle).
- setup script: the auto-register transport now captures the HTTP
  status alongside the body (no -f), making the invalid-setup-token
  branch reachable via 401/403 instead of a dead server-string grep,
  and operator guidance names Settings -> Infrastructure instead of the
  retired Nodes page (also updated in docs/PBS.md and the pinned
  assertions in contract, setup-script, and repoctl docs tests).

Regression proof: internal/api/issue1644_host_install_token_proxmox_test.go
plus new install.sh proofs for the retry window and blocked-marker
surfacing, and the updated hostagent blocked-registration test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 11:21:14 +01:00
courtmanr@gmail.com a9dad6a29c Run assigned availability checks from the host agent
The unified agent gains an availability module: probe assignments
arrive through the signed remote-config channel (missing key clears
the schedule), each enabled target runs on its own clamped interval
through the shared probe core, and results queue in a bounded
drop-oldest buffer. A result is offered to the primary server until
one delivery succeeds and never again after - buffered offline
reports are stripped of availability results so the disk buffer
cannot replay observations the queue still holds. ApplyHostReport
feeds accepted reports into the probe ingestion path, where the
ownership check and failure accounting live, and the probe agent id
is projected onto unified availability resources for source
attribution in the UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 11:13:04 +01:00
courtmanr@gmail.com c08da19ae7 fix(ceph): parse Quincy+/Squid status schema for MON and MGR counts
Ceph Quincy and later (including Squid on PVE 9) dropped the monmap
mons array and the mgrmap active_name/standbys arrays from ceph status
output, replacing them with monmap.num_mons and mgrmap.num_standbys,
with quorum membership reported at the top level of the payload. Both
the host agent parser and the Proxmox API path only understood the
legacy arrays, so modern clusters showed 0 monitors and undercounted
managers.

- hostagent: read num_mons/num_standbys and top-level quorum data,
  taking the largest available signal, and base the mon/mgr service
  rows on the same counts
- pkg/proxmox: decode mgrmap num_standbys and top-level
  quorum_names/quorum on CephStatus
- monitoring: fall back to the new fields when counting MON/MGR
  daemons, and log Ceph 401/403 failures at warn level with a hint to
  grant Sys.Audit on / instead of hiding them at debug
- models: prefer the larger non-zero MON/MGR counts when merging Ceph
  cluster records from multiple sources

Fixes #1626, Refs discussion #1290

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:31:15 +01:00
rcourtman 9449a56318 Rebuild SMART scan fixtures from real --scan output
6e93cb3b5 switched SMART discovery from `smartctl --scan-open` to `--scan`
but did not recapture its fixtures. It edited the comments and the command
string and left the payloads, so the tests asserted that `--scan` returns
`-d sat # /dev/sda [SAT], ATA device`, and the variable was still named
scanOpenPVESATA.

A capture from Unraid (tower, smartctl 7.5 2025-04-30 r5714) shows the real
shape. The non-opening scan cannot interrogate a device, so every libata
disk is reported as a generic SCSI device:

	/dev/sdb -d scsi # /dev/sdb, SCSI device
	/dev/nvme0 -d nvme # /dev/nvme0, NVMe device

and sysfs on the same host carries no protocol, transport or sas_address for
those disks, only vendor ATA under an /ataN/ path.

The production path is correct: smartctlDeviceTypeMatchesTransport rejects
the scan's scsi type against a sata transport, so probe selection falls back
to untyped plus the inferred -d sat. Only the fixtures were wrong, but they
were wrong in the direction that hid whether this worked at all.

Fixtures now carry the real scan shape with its provenance, the misleading
scanOpen prefix is gone, and the identity-only test stubs the vendor ATA a
real libata host exposes instead of leaving transport unknowable.

The new end-to-end test drives the captured shape and fails if a SATA disk
is ever probed as scsi or never reaches -d sat.

The megaraid claim in the Unraid fixture remains unverified: tower is plain
AHCI, so it cannot show whether `--scan` enumerates /dev/bus/N -d megaraid,M
controller members or whether that needs --scan-open. Left as-is pending a
capture from a host with an HBA.
2026-07-24 23:07:17 +01:00
rcourtman 25b2df07a0 Classify SATA disks behind a SAS HBA as SATA
linuxBlockDeviceTransportEvidence tested sas_address before vendor. A SATA
disk attached to an LSI/mpt3sas HBA, which is the common Unraid and TrueNAS
layout, exposes sas_address on its scsi_device while reporting vendor ATA,
so it classified as SAS. linuxInferredSmartctlDeviceTypes then returned only
"scsi" and smartctlDeviceTypeMatchesTransport discarded the correct "-d sat"
hint, steering a direct-ATA disk back to the "-d scsi" probe that 6e93cb3b5
exists to avoid.

Vendor ATA is the SCSI layer's marker for a device reached through a SAT
translation layer, so it is the more specific signal and is now tested
first. A genuine SAS disk reports its own vendor and never ATA, so the SAS
arm is unaffected.

Regression coverage pins all three shapes (SATA behind an HBA, a genuine SAS
disk, and a direct SATA disk) plus the resulting probe hint, and was
verified against the old ordering: it reports transport "sas" and inferred
types [scsi] for the HBA case.
2026-07-24 23:01:02 +01:00
rcourtman 6e93cb3b5c Fix Unraid SMART probing and disk authority 2026-07-24 09:38:39 +01:00
rcourtman b4877b4b44 Fix Proxmox agent registration authorization flow 2026-07-23 23:44:05 +01:00
rcourtman b8ea840f11 fix(monitoring): make Unraid task state freshness-safe 2026-07-23 22:17:25 +01:00
rcourtman 6a527ba9f2 Fix Proxmox physical disk inventory continuity 2026-07-23 22:05:09 +01:00
rcourtman 8580b48abc Fix Docker container update recreation lifecycle 2026-07-23 21:39:35 +01:00
rcourtman d2c268c0e5 Fix agent update triage and durable action recovery 2026-07-23 11:19:50 +01:00
rcourtman cba394e56b Cover control plane status helpers, patrol assertions and setup classifiers
Five new branch-coverage tests taking previously unreached functions from zero
to covered, with no source or existing test touched.

cmd/pulse-control-plane: the MSP status failure lookup across exact, absent,
case-differing and substring inputs, and the status state ordering proved
deterministic across repeated runs over the same map so a random map iteration
order cannot pass by luck.

internal/ai/eval: the signal coverage assertion driven through both the lazy
quality evaluation path and the supplied-quality path, at the minimum rate
boundary and either side of it, asserting the formatted message rather than
only the pass or fail outcome, plus the approval write command builder.

internal/api: the setup script server name derivation including the fallback
arm, the patrol autonomy level validator, the approval risk assessment across
every level it can return, and the two typed error messages.

internal/hostagent: the token already exists classifier over each phrase it
recognises, a wrapped error carrying the phrase, and near-miss messages that
must not match.

internal/monitoring: the limited temperature buffer including its truncation
boundary and its aliasing behaviour, and the mock series generators asserted on
determinism under a fixed seed and on their value bounds.

Reported for follow-up, not fixed here: the node escalation arm in the approval
risk assessment is unreachable for the current command set, generatePlateauSeries
panics on a negative point count and leaves its tail unclamped, and the limited
temperature buffer returns a slice that aliases its internal storage.

Contract-Neutral: test-only branch coverage, no contract surface touched
2026-07-23 06:34:03 +01:00
rcourtman 743ba7e5a3 Cover the Unraid merge, setup classifier and retry helpers left untested
The Unraid disk mergers, the Proxmox setup output classifiers, the ZFS SMART
annotator, the Docker runtime command builder, the approval decision actor
and the update retry classifier were all at zero coverage. Each decides
something a user feels directly, whether an Unraid disk keeps its name after
a merge, whether a setup run reports an already-registered token as a
failure, which pool a SMART entry is attributed to, and whether a failed
update request is retried or surfaced as an error.

Adds branch coverage for mergeUnraidDiskINI, mergeUnraidDisk,
defaultUnraidDiskName, isAlreadyExistsOutput, the client error Error and
Unwrap methods, annotateSMARTWithZFSPools, dockerRuntimeCommand,
approvalDecisionActor and isRetryableUpdateRequestError, including nil and
whitespace arms, field precedence between base and incoming values, in-place
slice mutation asserted on the caller's value, and wrapped errors that only
resolve through errors.Is and errors.As. Every named target moves from zero
to full statement coverage.

Test-only change.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT=test-only branch coverage, no contract surface touched
2026-07-22 08:06:56 +01:00
rcourtman 59e6f25a65 Detect SAS transport and parse SCSI attributes in SMART collection
smartctl reports SAS drives with device protocol SCSI, so detectDiskType
fell through to its blanket sata default, and that non-empty type also
masked the text-output transport evidence the fallback parser had
already extracted. The wrong sata label then blocked the merge layer
from promoting the smartctl serial over the SAS transport address
Proxmox reports.

Classify SCSI-protocol devices via the scsi_transport_protocol
descriptor, let the text and sysfs refinements upgrade a generic scsi
label, and apply the legacy sata default only after all evidence is
exhausted. Parse the SCSI log-page fields (power-on hours, grown defect
count, endurance used) that SCSI drives report instead of an ATA
attribute table, and let agent-reported sas replace coarse hdd/ssd/sata
types during the disk merge.

Refs #1595

Contract-Neutral: behavioral fix: SAS transport detection and SCSI attribute parsing in host agent SMART collection; no public contract delta (#1595)
2026-07-21 17:36:14 +01:00
rcourtman 4f6179368a Add branch-coverage tests for unifiedresources helpers, smartctl targets, intent-policy manager
Covers previously-uncovered pure/near-pure functions from the recent
alert-intent and disk-inventory source drop:

- unifiedresources registry seeded K8s identity/source-id builders
  (cluster/node/pod/deployment/typed) and VMware/proxmox/predicate
  helpers (seededVMwareSourceID, proxmoxGuestFallbackSourceID,
  isDockerNetworkAttachmentRelationship, physicalDiskTopologyCompatible)
- unifiedresources ActionPolicyAuthorizationDigest (digest-excluded
  invariant) and normalizePair ordering invariant
- hostagent smartctlTargetsFromDevices
- alerts Manager UpdateIntentPolicies (validation / nil / revision
  conflict / success), GetIntentPolicies, SetBackupIntentContextResolver

All 13 target functions move 0% to 100% under the new tests; test-only,
no source changes.
2026-07-21 06:14:26 +01:00
rcourtman b6a74576bc Integrate trust-gate reliability fixes 2026-07-20 16:03:29 +01:00
rcourtman 62993899fc Isolate agent buffering qualification 2026-07-20 04:35:52 +01:00
rcourtman 12eef62f76 Stabilize cross-platform release gates 2026-07-20 02:32:29 +01:00
rcourtman 52ed884640 Prepare v6.1.0-rc.4 release 2026-07-19 15:52:42 +01:00
rcourtman 4b066a0f1e Add report-only Unified Agent observer destinations 2026-07-19 15:30:34 +01:00
rcourtman 2991ed9a95 Teach the host agent ZFS matcher nvme-eui and namespace-suffixed references
The server-side disk-to-pool matcher learned nvme-eui.<hex> zpool member
references in 4dcc18fbd, but the host agent keeps its own copy of the
matching for SMART pool annotation and never derived a key from the
nvme-eui form, nor stripped the eui. prefix smartctl reports on NVMe
WWNs. systemd's nvme by-id links can also carry a trailing _<n>
namespace suffix (nvme-MODEL_SERIAL_1), which made the serial
derivation return the namespace digit instead of the serial. Both gaps
left a pool member unmatched so the disk fell back to the generic
usage string (issue #1540).

Contract-Neutral: bugfix restoring intended disk-to-pool annotation (#1540); no contract surface changes
2026-07-18 11:54:09 +01:00
rcourtman 331758f32b fix(agent): surface the real cause when reviewed actions go unavailable
Two diagnosability gaps found by exercising the docker-update vertical
live with a real agent, each of which turns 'approved action cannot
execute' into a dead end with no visible cause:

- The durable operation-receipt store failing to open (e.g. unwritable
  state dir) silently registered the agent with receipt version 0; the
  only symptom was a server-side claim that the agent was too old. The
  agent now logs the real cause and state dir loudly at startup.
- The server readiness copy asserted 'older agent version' for any
  receipt-version mismatch. It now names both causes (old version or
  unavailable state directory) and points at the agent logs.

(The related docker collect-cycle watchdog landed separately in
a0f75b1bb with a hung-daemon reproducer.)

Contract-Neutral: agent-side diagnosability: receipt-store failure warning + honest readiness copy; no public contract delta
2026-07-17 23:39:57 +01:00
rcourtman 488731aed3 refactor(hostagent): dedupe the host APT operation handlers behind shared begin/complete helpers
Contract-Neutral: dupl lint dedupe refactor; no behavior or contract delta (host APT handler admission prefix and terminal suffix extracted to shared helpers)
2026-07-17 17:34:01 +01:00
rcourtman 3c778e2b26 Restore one-click Docker container updates through the typed action plane
v6.1.0-rc.1 retired the legacy update endpoints before a replacement
existed, so the UI's Update button failed with an internal-jargon 410
(issue #1564). This lands the replacement end to end: update_container
is a typed agentexec operation with its own strict codec, durable
receipts, and a request digest bound to the image digest the plan
observed; the unified agent bridges execution to the Docker module's
existing pull/backup/recreate/verify/rollback implementation (which now
reports rollback attempt and outcome); and the container action
executor plans, dispatches, and reconciles the operation with declared
backup/rollback compensation truth. Containers advertise an
admin-approval update capability while an image update with a stated
current digest is detected. The legacy endpoints stay retired but
return actionable copy.

Proven live against a Colima daemon: single-container update, the
issue-1564 shared-network-namespace update, and the full UI journey
(Update button, governed review, approve, run) all completed with the
namespace preserved and the backup retained.
2026-07-14 12:19:04 +01:00
rcourtman 373b491484 Fix durable APT drift receipts 2026-07-12 21:53:52 +01:00
rcourtman 99aa2bd6eb Add Colima autonomy release proof 2026-07-12 19:47:56 +01:00
rcourtman 0062128414 Add durable Docker restart lifecycle proof 2026-07-12 12:11:16 +01:00
rcourtman d6838d3a25 Complete durable APT workflow continuity 2026-07-12 05:23:02 +01:00
rcourtman 4aac79dc72 Add durable agent operation receipts 2026-07-12 04:16:22 +01:00
rcourtman e77ab9518d Add safe APT workflow foundations 2026-07-12 02:53:01 +01:00
rcourtman 1d3b8e1949 Addresses #1555 2026-07-11 18:25:17 +01:00
rcourtman f2b732721e Fix physical disks vanishing on wide nodes and standby-misreporting SSDs
Two root causes behind #1516's remaining reports:

- A node whose Proxmox disks/list query fails (PVE probes SMART per disk
  inside that call, so dozens of disks can exceed the API window) now
  falls back to the linked host agent's smartctl inventory instead of
  leaving the Physical Disks view empty. Each node also gets its own
  attempt window so one slow node no longer starves the rest of the
  cluster, and a poll that runs out of budget saves partial results.

- The -n standby probe guard is dropped for positively confirmed
  non-rotational devices in both the host agent and the node sensor
  wrapper. The guard exists to avoid spinning up sleeping HDDs; an SSD
  has nothing to spin up, and some SATA SSDs answer CHECK POWER MODE
  with a bogus standby state that permanently hid their temperature,
  attributes and history.

Refs #1516
2026-07-11 10:24:45 +01:00
rcourtman eb9954618a Add governed storage pressure cleanup 2026-07-11 10:11:35 +01:00
rcourtman 1312da3acb Add governed host update autonomy 2026-07-11 01:25:14 +01:00
rcourtman f4c2fd0c38 Fail closed on unknown remediation lock state for autonomous dispatches
The AI action broker treated an unreadable operator lock as unlocked:
isResourceRemediationLocked returned (false, nil) with no audit store
wired, and the caller logged store errors then dispatched anyway. An
operator's NeverAutoRemediate=true could be silently ignored whenever
the policy store was missing or erroring, which is unacceptable while
Patrol and Assistant run at assisted or full autonomy.

Posture change at the dispatch decision point:
- isResourceRemediationLocked now reports unknown state (nil store or
  lookup failure) as an ErrRemediationLockStateUnknown-wrapped error
  instead of silently defaulting to unlocked.
- New checkRemediationLockForDispatch gate: dispatches without an
  approved human decision fail CLOSED on unknown lock state and
  surface "remediation lock state unknown; operator approval
  required". Human-approved dispatches keep the historical fail-open
  behavior with a warning log. A confirmed lock still refuses even
  approved dispatches, as before.
- executeNativeActionWithAudit (TrueNAS app start/stop/restart) now
  enforces the lock too; it previously skipped the check entirely.
- Refusals persist Failed audit records with stable
  remediation_lock_state_unknown: / resource_remediation_locked:
  ErrorMessage prefixes.
- ai-runtime subsystem contract updated to pin the new posture.

Tests cover store-error and nil-store at both autonomy postures on
both dispatch paths; routing/control tests now wire an in-memory
audit store since autonomous dispatch without one is refused.
2026-07-10 00:14:01 +01:00
rcourtman 211b80717d Partition host agent tests by platform 2026-07-09 23:40:34 +01:00
rcourtman 255c7c23d4 Modernize Unified Agent lifecycle and platform support 2026-07-09 23:20:35 +01:00
Richard Courtman 1968dc4171 Close remaining CodeQL allocation and cookie gaps 2026-07-09 20:10:08 +01:00
rcourtman 042e7ef966 Harden remaining CodeQL security boundaries 2026-07-09 19:46:40 +01:00
rcourtman 92524e1c27 Harden CodeQL storage and integer boundaries 2026-07-09 17:37:08 +01:00
rcourtman a54e67cb0f Point agent 401 recovery copy at the Pulse UI, not a wrong breadcrumb
Refs #1515

The install command is generated under the Settings infrastructure
installer, not a "Settings > Agents" tab. Correct the agent log, installer
warning, and Machines tooltip to say "the Pulse UI" so the recovery step is
accurate.
2026-07-08 08:40:29 +01:00
rcourtman c7dcd90b83 Surface agent auth failures and staleness instead of a silent 401 loop
Refs #1515

When an upgraded or restored Pulse server no longer recognises an agent's
API token, the report endpoint returns 401. The agent buffered and retried
that report forever with only a generic warning, and the server kept the
node green at its last known agent version because a Proxmox node stays
online via the PVE API poll even after its agent dies.

- Agent: special-case 401 on /api/agents/agent/report. Drop the report
  instead of buffering it and log a throttled, actionable error pointing the
  operator at the install command to mint a fresh token.
- Installer: verify_agent_server_registration now tells a rejected token
  (401/403) apart from "agent has not reported yet" and prints the recovery
  steps at install time instead of a vague soft warning.
- Status layer: resourceFromHost flags a stale agent (its host marked
  offline by the staleness evaluator) and carries the agent's own last
  report time. Coalescing keeps a node online via the PVE source but the
  dead agent stays flagged, so its version is no longer presented as current.
  The Machines table renders such versions as "(stale)".
2026-07-08 08:35:46 +01:00
rcourtman 022f170dff Prefer route-aware Proxmox host URLs 2026-07-07 10:37:10 +01:00
rcourtman 4b669ad9b5 Fix SAT SMART temperature collection
Refs #1471
2026-07-06 23:42:00 +01:00
rcourtman 04a9e6ad3e Fix Proxmox agent install token isolation
Keep Proxmox setup tokens node-scoped so cluster installs do not rotate one shared Pulse API token.

Suppress command-enable config for tokens that cannot register command channels and keep reusable installer tokens out of agent:exec.
2026-07-05 17:21:34 +01:00
rcourtman 6344d17fd1 Allow local Pulse HTTP agent URLs
Refs #1505
2026-07-03 00:24:40 +01:00
rcourtman d393ccf310 Add typed NVIDIA GPU stats 2026-06-30 09:43:33 +01:00
rcourtman fe3c0f3ee3 Collect NVIDIA GPU temperatures 2026-06-30 09:11:18 +01:00
rcourtman 4fc56162f6 Harden hostagent SMART no-device test 2026-06-28 01:01:20 +01:00