Commit Graph

725 Commits

Author SHA1 Message Date
rcourtman 3c103f1263 Fix pinned notification connectivity and cancelled DNS lookups
Serial pinned-address dialing could spend the request deadline on a stalled
first address while a validated fallback was healthy. Share one bounded
two-worker dialer with restricted HTTP clients and retain cancellation in
webhook DNS admission and dialing.

Add real stalled-TCP and DNS cancellation controls, persistent queue verdict
readback, shared transport ownership and explicit proof routing.
2026-10-18 08:49:56 +01:00
rcourtman a224a1a195 Measure aggregate reporting outcomes and repeat use
Record successful response writes, scheduled generation, file saves and SMTP
outcomes in a durable content-free tally. Distinct active days include manual
exports, while saved-schedule periods deduplicate retries and monthly reuse.

Publish bounded scalars through the existing opt-out-controlled heartbeat and
report missing coverage explicitly. Preserve pricing and customer entitlements.
2026-10-17 21:10:17 +01:00
rcourtman 22306e0a93 Add scheduled client backup health reports
Retain backup execution outcomes and collection coverage in each
organization's recovery store so provider inventory changes and restarts
cannot erase report evidence. Keep missing and incomplete history distinct
from an observed period with no attempts.

Reuse existing report scopes, calendar schedules, branding and delivery
recovery for client PDF and CSV reports. Register the paid renderer through
the existing private runtime boundary. Pricing and grants are unchanged.
2026-10-17 17:36:24 +01:00
rcourtman 3f511a5270 Scope report metrics to the requesting organization
The report engine read every report's metrics from the default
organization's store, whatever organization the request came from. A
tenant request for a resource ID outside its own inventory fell through
to that store with the caller's ID, so one organization could read the
default organization's history, and even a tenant's own resources were
reported from the wrong store. Reporting was closed to org owners until
this was fixed.

The engine now resolves the metrics store of each request's OrgID
through the multi-tenant monitor and refuses a request that names none,
so no report path can fall back to the default store. Report handlers
reject resources outside the requesting organization's inventory,
scheduled runs resolve their scope against their owning organization's
inventory and skip resources it no longer holds, and Pulse Assistant's
pulse_summarize reads its executor's organization.

With every report read scoped, the reporting routes move to
ensureOrgAdminSession and reportingRead is offered to org managers
again, so providers can build reports inside client workspaces.

The report engine wiring lives in pkg/server/server.go, which the
completion guard only let telemetry tests prove. server.go now has its
own security-privacy path policy that keeps those proofs and also
accepts pkg/server/reporting_engine_test.go; the other telemetry files
keep their existing policy unchanged.
2026-10-17 13:59:44 +01:00
rcourtman af3b578c06 Keep shared Proxmox login alive when its starter cancels
Session renewal became single-flight in 97e4c8eac7, but the caller that
started the login ran it on its own context. When that caller was a
short-lived read (a 10s guest-agent call, a guest-metadata fetch cancelled
on invalidation, an API request whose browser disconnected), cancelling it
aborted the shared login and every concurrent waiter failed with
"re-authentication failed: context canceled" despite live contexts. The
cluster client treats that text as an authentication error and skips
failover, and a failed GetNodes marks the instance's nodes unreachable for
the cycle.

Run the shared login detached from any single caller's cancellation,
bounded by the client's per-request timeout, and have every caller,
starter included, stop waiting only when its own context ends. A genuine
login failure is still shared with all waiters, and a login whose starter
has left still completes and is reused instead of being repeated.

Contract-Neutral: Proxmox password-session renewal keeps its API fields, TLS policy and guest-command rules; only which context bounds the shared login changes.
2026-10-16 23:55:16 +01:00
rcourtman 84226ad7bc Count successful history reads beyond seven days
Measure delivered extended history before changing free and paid features.
Count only successful responses containing an older metric point and retain
only bounded daily totals. Expose the rolling sum through schema v19 and
Preview with matching privacy disclosure and explicit report coverage.
2026-10-16 19:21:03 +01:00
rcourtman 36c63bbf8b Accept PMG 9 cluster ids and stop guessing backup nodes
Proxmox Mail Gateway 9 returns the cluster status cid as a string, so
decoding it into an int failed the whole cluster read and the poller
lost the node list (issue 2960). Pulse never reads cid, so the field is
no longer decoded and any format works.

With no nodes, the poller then requested backups from a node named
after the connection's display name ("PMG-01"), which PMG rejected with
a hostname-lookup error that pointed users at DNS. That fallback now
runs only when PMG answered with no cluster entries; after a failed
cluster read the request could never return backups, and the poll
already records the cluster failure.
2026-10-16 14:15:58 +01:00
pulse-triage[bot] 97e4c8eac7 Share Proxmox session renewal across concurrent polling reads
Publish ticket, CSRF and expiry as one locked snapshot, and bind expiry/401 renewal to the rejected session generation. Concurrent readers share one authentication attempt while retaining their own cancellation and ordinary single-retry behaviour. Keep guest commands single-attempt and token/TLS policy unchanged.

Contract-Neutral: Synchronise existing Proxmox password-session renewal without changing API fields, TLS policy or guest-command admission and replay rules.
Change-source: pulse-maintainer
2026-10-12 18:14:32 +01:00
rcourtman 8cff7de026 Restore Patrol's Safe auto-fix and Autopilot modes
Reverts #2863 before any release ships it. The live Pro pricing and
Patrol pages, in English, German and Spanish, sell both modes ("applies
safe fixes in the mode you choose: Ask first, Safe auto-fix, or
Autopilot"), and paying installs chose them. Shipping the retirement
would have silently downgraded those installs while the sales page still
promised the modes.

If the modes are retired, that needs a dated notice: stop selling them
first, warn installs on those modes in-app, and then re-ship the change.
It should not arrive through an engineering cleanup.

The receipt #2863 landed stays in place; this commit adds the receipt
for the restored content.
2026-10-12 14:13:47 +01:00
rcourtman 8051ab9f2d Use a current Patrol mode in the telemetry snapshot fixture
The pulse-intelligence telemetry snapshot test fed the retired assisted level through the snapshot and expected it back. Patrol now reports only monitor or approval, so the fixture uses approval; the pass-through it checks is unchanged.
2026-10-12 12:27:41 +01:00
rcourtman afc4c9275d Retire Patrol's Safe auto-fix and Autopilot modes
Patrol could run some fixes on its own in two paid modes: assisted (Safe
auto-fix) and full (Autopilot, behind a server-owned acknowledgement).
Schema-v17 telemetry for the week to 2026-10-11 counts 1 assisted and 2
full installs out of 612 reporting Patrol, 4 executed Patrol actions in
30 days, all on one install, and no GitHub request for more autonomy,
while the automatic path carried its own policy leases, acknowledgement
store, per-resource allowlist, release gate and Colima lab. Investigation
is what paid installs use, so it stays.

Patrol now has two modes. Watch only reports findings. Ask first
investigates and proposes fixes, and every fix waits in Actions for an
operator to approve and run it.

- A saved assisted, full or autonomous level reads as approval, and the
  update routes refuse those levels with 400 invalid_autonomy_level.
- The core Patrol action broker only plans: Plan and Submit persist the
  plan and return. ExecuteUnderPolicy, the policy authorization lease,
  the policy admission store methods, the Autopilot acknowledgement
  persistence, routes and dialog, and the per-resource automatic-action
  allowlist (store column now written as NULL) are gone.
  GetPatrolFullModeUnlocked stays on the extension contract and reports
  false.
- Historical action records keep their policy lease and tenant/resource
  reason codes decodable, labelled as retired modes.
- The Patrol inbox no longer splits issues into "continuing without a
  decision"; every current issue is an operator decision.
- Release gate RG-06 now proves Patrol never executes without an operator
  decision (integration tier) instead of limited unattended autonomy,
  and RG-02/RG-04 point at the surviving human-admission proofs.
- Pro plan copy, AI docs, privacy telemetry wording and the subsystem
  contracts describe the two modes.
2026-10-12 11:52:11 +01:00
pulse-triage[bot] 123652e29b Preserve reviewed monitoring repairs with upstream Discovery removal
Compose current main e2f4bdff03 additively. Retain both subsystem records and the held change-age control while withdrawing only the obsolete Discovery identification test. Reviewed identities and ancestry, canonical guest History ownership and upstream feature removal remain intact.

Change-source: pulse-maintainer
2026-10-12 10:04:58 +01:00
pulse-triage[bot] b16ef31f7b Check the exact persistence destination before bootstrap
Normalise the selected data directory with the storage path routine before inspection. Raw symlink/.. traversal and trailing separators must not probe an empty peer or hide a linked root while the encrypted importer writes elsewhere. Retain and validate the existing canonical agent credential.

Change-source: pulse-maintainer
2026-10-12 05:27:06 +01:00
pulse-triage[bot] 70c1ad1471 Expose bootstrap and persistence path disagreement
Check the actual saved-token destination when a configured path contains symlink/.. traversal, plus live and dangling directory links with trailing separators. The original guard can inspect an empty peer while persistence writes existing canonical state.

Change-source: pulse-maintainer
2026-10-12 05:25:01 +01:00
pulse-triage[bot] 2cefdb0a3a Complete startup import admission compilation
The first exact-source check found the old nodes.enc-only gate was the last user of path/filepath. Remove that unused import; runtime admission and served guide bytes remain unchanged.

Contract-Neutral: Remove an unused standard-library import only; the preceding startup admission contract and regression obligations are unchanged.
Change-source: pulse-maintainer
2026-10-12 05:15:10 +01:00
pulse-triage[bot] acc86c7aca Retain existing state when startup import is configured
Require a blank destination instead of using nodes.enc as a first-run sentinel. Recheck before writes, retain unknown or unreadable state, and leave deliberate authenticated transfer unchanged. Keep deployment .env provisioning and encrypted file precedence; explain the separate archive path in canonical and served help.

Change-source: pulse-maintainer
2026-10-12 05:03:18 +01:00
pulse-triage[bot] 9fde1f802e Expose startup import over existing non-Proxmox state
Exercise ordinary configuration loading, saved access and unknown destination state through the production startup helpers. Retain blank provisioning, file precedence, input failure and deliberate configuration-transfer controls.

Change-source: pulse-maintainer
2026-10-12 04:57:07 +01:00
rcourtman 39f988b220 Remove AI Discovery
AI Discovery ran a separate model-backed service scanner beside Patrol
and the Assistant: its own store, scheduler, model route, API, tool and
drawer UI. No telemetry counter ever measured its use (the telemetry
discovery_enabled flag OR'd it with network subnet discovery), the
maintainer judged it a flop, and the context it produced is already
available to the models through pulse_query, pulse_read and the
unified-resource context sections.

This deletes internal/servicediscovery, internal/ai/infradiscovery, the
pulse_discovery tool, the /api/discovery routes, the mock Discovery
fixtures, the Discovery tabs in the resource, guest, node and Docker host
drawers, the identified-service cards, readiness badges, provenance
markers, suggested web-interface URLs, the Workloads AI Context column,
the chat "Discovery is off" hint and the AI Service Context settings page.
The old settings route redirects to Assistant settings. The model-bound
sanitizer allow-list existed only to pass Discovery context through, so it
goes too, along with the CopyableCodeRow primitive that only the
Discovery tab used.

Network subnet discovery (/api/discover, the Infrastructure discovery
settings) and the discoveryTarget agent coordinates are separate features
and stay. Stored discovery_enabled, discovery_interval_hours and
discovery_model AI settings are ignored on load; the settings API decoder
is lenient, so older frontends saving those fields still succeed.
2026-10-11 22:20:25 +01:00
pulse-triage[bot] a31fdc77da Measure endpoint ownership independently of startup probe count
The first single-endpoint ordinary read performs a connectivity check before the requested API read. Keep the fixture assertion on successful declared-target monitoring and zero injected-target authentication; guest single-attempt and admission assertions remain exact. Retain the failed final-source proof rather than treating it as a runtime failure.

Change-source: pulse-maintainer
2026-10-11 21:17:00 +01:00
pulse-triage[bot] 496b37ec71 Own cluster endpoint membership before routing and trust checks
Snapshot constructor endpoints before guest-channel registration and initial network checks. Caller configuration reuse cannot introduce an undeclared first-use trust target or bypass in-flight guest admission. Preserve endpoint order, TLS policies, backup locks, uncertainty fences and non-replay; prove ordinary monitoring and concurrent input reuse.

Change-source: pulse-maintainer
2026-10-11 20:48:07 +01:00
pulse-triage[bot] cc72ea76e3 Expose caller-owned cluster endpoint edits bypassing admission
Discriminate live routing, first-use TLS trust and shared guest-command coordination after the constructor input is reused. Use only synthetic loopback services; no native guest workload or reporter recovery is claimed.

Change-source: pulse-maintainer
2026-10-11 20:43:26 +01:00
pulse-triage[bot] f0d27be7ba Coordinate mapped IPv4 guest reads without sharing TLS pins
IPv4-mapped IPv6 and literal IPv4 reach the same numeric guest channel. Share only per-VM admission so another spelling cannot bypass an in-flight or uncertain command. Preserve the previous certificate origin identity, URL, proxy and first-use trust behaviour.

Change-source: pulse-maintainer
2026-10-11 12:09:32 +01:00
pulse-triage[bot] f6bd20136f Align ordinary config redirect controls with origin confinement
The broad suite retained the old cross-endpoint positive oracle. Keep same-origin proxy reads, require cross-origin refusal before a second request, and retain every stricter guest-lock refusal and same-target recovery assertion.

Contract-Neutral: Update a verification oracle for the already implemented infrastructure origin contract; no runtime change or relaxation of guest-command safety.
Change-source: pulse-maintainer
2026-10-11 06:43:55 +01:00
pulse-triage[bot] 971d403d01 Keep infrastructure credentials inside their configured HTTP origin
Refuse redirect scheme, host, port and userinfo changes before any redirected request. Preserve same-origin proxy paths, canonical default ports and IPv6 literals, zone identity, the ten-redirect budget and existing CA/pin/explicit-opt-out policy. PVE and PBS password/token/ticket controls reproduce the previous exposure on actual synthetic sockets.

Change-source: pulse-maintainer
2026-10-11 06:37:38 +01:00
pulse-triage[bot] 5abea19419 Expose infrastructure credentials following redirects to another origin
Exercise actual TLS sockets and the PVE/PBS token, ticket/CSRF and password JSON/form flows using synthetic credentials. Retain same-origin proxy paths and the existing ten-redirect budget.

Contract-Neutral: Add discriminating source controls for the existing infrastructure connection trust boundary; no runtime or authority change.
Change-source: pulse-maintainer
2026-10-11 06:34:28 +01:00
pulse-triage[bot] 1d7a0023c1 Capture certificates from the actual connection authority
Keep explicit URL defaults aligned with the API client and join IPv6 socket addresses correctly. Preserve the legacy bare-host port and credential-free capture without weakening any established pin or verification choice. Cover endpoint parsing, actual IPv4/IPv6 leaves and invalid-authority rejection.

Change-source: pulse-maintainer
2026-10-11 06:09:15 +01:00
pulse-triage[bot] 4ed767c539 Expose fingerprint capture selecting the wrong endpoint
Check the public helper selects the HTTPS default port and formats a bare IPv6 authority before dialing. Loopback failures are deliberate discriminators, not native connection acceptance.

Contract-Neutral: This adds endpoint-selection regression controls only; TLS trust, credentials and provider behaviour are unchanged.
Change-source: pulse-maintainer
2026-10-11 06:05:20 +01:00
pulse-triage[bot] e7d8092c63 Reject case-variant guest filesystem identity fields
Apply the existing canonical-key admission rule to filesystem identity and filter metadata as well as counters. A case-variant mount or type can otherwise overwrite a real volume, filter it out and falsely publish a complete healthy inventory. Preserve valid peers with the existing completeness error and hold only the original qualified disk evidence. Cover metadata ordering, array and object replies, both collectors, History, alert continuity and genuine ordinary recovery without changing guest command admission.

Contract-Neutral: Repairs existing filesystem identity and incomplete-reading invariants; no public wire schema, guest command, retry, lock, permission, threshold or backup contract delta.
Change-source: pulse-maintainer
2026-10-10 09:49:01 +01:00
pulse-triage[bot] 615fa29918 Keep incomplete guest filesystem readings out of recovery
Retain valid peer rows with a completeness error instead of treating rejected or unrecognized volumes as removed. Both collectors hold only the last complete tuple within its original age bound, preserving History and real filesystem breaches until a complete ordinary replacement can recover. Keep guest command admission and backup policy unchanged.

Contract-Neutral: Repairs existing truthful guest filesystem completeness and recovery invariants; no public wire schema, guest command, retry, lock, permission, identity or backup contract delta.
Change-source: pulse-maintainer
2026-10-10 05:28:48 +01:00
pulse-triage[bot] b9409a0c74 Preserve measured zero guest memory and reliable guidance proofs
Keep explicit status/current zero availability through decoding and JSON copies so genuine exhaustion cannot become a healthy cache fallback. Connected direct/cluster collector controls cover canonical memory, History and breach/recovery. Complete race validation also exposed a shared-logger guidance fixture race: keep guidance on its caller logger with ordinary fallback, and stop the named scheduler-health fixture monitor. Message, backup precautions, command fences, thresholds and dependencies are unchanged; prior failed proof receipts remain retained.

Change-source: pulse-maintainer
2026-10-10 04:08:26 +01:00
pulse-triage[bot] 7100938fb3 Keep established Proxmox certificate pins during recovery
Reject automatic replacement of primary and known member fingerprints. Admit first-use capture only for a declared new member and a typed local verifier failure; preserve concurrent trust, caller-owned configuration and operator-visible mismatch evidence. Validate real changed/restored TLS leaves through startup, recovery and new-member acceptance while retaining guest-command safeguards.

Change-source: pulse-maintainer
2026-10-09 20:06:58 +01:00
pulse-triage[bot] d16885bc35 Defer guest commands after unrecognised HTTP client errors
An unfamiliar 4xx from an intermediary does not establish QGA command completion. Keep supported terminal refusals and command-bound HTTP 500 rejections as errors; retain the shared uncertainty fence for other statuses. Cover all six readers, configured aliases, independent guests, resumption and connected last-known History without changing ordinary API reads or backup locks.

Change-source: pulse-maintainer
2026-10-09 18:36:47 +01:00
pulse-triage[bot] 9ccfb98ee1 Avoid per-directory allocations while filtering disk mounts
Inspect the byte after each matching system-directory prefix instead of constructing slash-suffixed strings on every disk poll. Keep exact roots and children excluded and local sibling volumes eligible, with unchanged reasons and explicit operator patterns. Add a deterministic zero-allocation guard for eligible local mount paths.

Contract-Neutral: Preserve existing filesystem eligibility, skip reasons, user patterns, identities, wire schemas and guest-command safety policy; change only the allocation cost of the directory-boundary predicate.
Change-source: pulse-maintainer
2026-10-09 17:50:19 +01:00
pulse-triage[bot] 5c3d18e248 Keep guest primary addresses associated with their interfaces
Prefer named guest interfaces over local container networks instead of globally sorting flattened IP text. Preserve all secondary and unknown addresses, management interfaces, warm metadata caches, explicit agent report IPs and machine identity. Cover LXC status/config/runtime fallback, QEMU metadata and canonical list/broadcast projection for issue #2757.

Change-source: pulse-maintainer
2026-10-09 13:07:08 +01:00
pulse-triage[bot] 95fca3477a Merge batch-start upstream summary chart route removal
Preserve the exact published history while reconciling this bounded candidate batch. Change-source: pulse-maintainer

Change-source: pulse-maintainer
2026-10-08 08:42:36 +01:00
pulse-triage[bot] b03965a0d2 Merge branch 'main' into refactor/drop-summary-chart-routes 2026-10-08 07:45:44 +01:00
pulse-triage[bot] 64364d97c8 Honour saved PMG collection scope without inventing recovery
Gate mail, queue and quarantine reads at the owning poller. Preserve legacy mail defaults while persisting explicit all-off scope, keep missing readings out of alert recovery and reject absent PMG object data. Partial queue observations may prove the highest configured severity but cannot resolve or downgrade alerts. Retain version connectivity separately from downstream poll failure and cover scope, persistence, pause, in-flight and adverse-read boundaries.

Change-source: pulse-maintainer
2026-10-08 06:54:46 +01:00
pulse-triage[bot] 1052f21419 Preserve reviewed maintenance with current upstream report and identity ownership
Additively integrate exact upstream 1a094646d7 while retaining starting reviewed 19a8358a49. Keep reviewed UUID incident/event IDs and upstream non-wrapping atomic sequences for other memory IDs. Compose upstream shared SMART/Unraid temperatures with reviewed pending context and observation-gap interruption; pin Unraid-only continuity and both grace paths. Retain all browser, completion and security validation limits until evidenced.

Change-source: pulse-maintainer
2026-10-08 00:57:22 +01:00
rcourtman 4a0a05858d Remove the summary chart routes no client calls
GET /api/charts/workloads-summary has had no first-party caller since
the v6 import: ChartsAPI.getWorkloadsSummaryCharts arrived with it and
nothing ever called it. GET /api/charts/storage-summary lost its last
reader when storageSummaryTrendCache.ts was deleted on 2026-05-26.
Both still cost something. The workloads route carried a payload cache,
an SLO test in the release latency gate and a benchmark, and mock mode
rebuilt the 24-hour storage aggregate on every sampler tick for a cache
nothing read.

No stable release ever requested either route from the browser: v6.0.0
and later carry no caller, and the storage trend reader shipped only in
v6.0.0-rc.1 to rc.5. Neither route is in docs/API.md (a list of common
endpoints, not a complete one), and pulse-pro, pulse-mobile and
pulse-dev-infra never call them. A monitoring:read token could reach
them, so a private script may notice, but nothing known depends on
them. pulse-enterprise names the SLO test only as fixture data, and its
latency gate passes a test that is absent from the candidate as
removed. So the routes go with their clients:
handlers, response types, the monitor trend helpers, the mock aggregate
and its prewarm, the SLO budget, the benchmark, and the ChartsAPI
methods and types. Requests to either path now get 404.
buildMockWorkloadMetricHistorySeries goes too, because the
workloads-summary handler was its only production caller.

A TypeScript reference graph from src/index.tsx finds no production
reference to the removed methods or types. The production build loses
only the two static methods from the ChartsAPI chunk (326 B raw, 49 B
gzip); every other file differs only by chunk hashes and SRI values.
deadcode ./cmd/pulse and golangci unused report nothing newly
unreachable. On private mock stacks the Proxmox overview in Trends, a
guest History drawer and the storage page make the same chart requests
before and after at 1440 and 390 px, and neither build requested the
removed routes.

Contract prose that still described either route as live now records
the retirement, and the workload chart guard pins the per-type metric
sets the surviving /api/charts/workloads route requests.
2026-10-07 23:14:49 +01:00
rcourtman 9bd1b6e582 Let only POST narrate single-resource reports with Pulse Assistant
GET /api/admin/reports/generate resolved the tenant's AI narrator, so a
PDF requested with GET made a paid provider call and appended a usage
event to the AI cost ledger. GET passes the demo-mode guard and the
CSRF check, and a SameSite=Lax session cookie rides a cross-site
top-level GET navigation, so a link could spend a signed-in admin's AI
budget.

POST with a JSON body, decoded with the generate-multi rules, is now
the canonical single-resource transport and the only one that may use
the AI narrator. The settings UI sends it, following a new
performanceReport.singleResourceMethod catalog field; a catalog without
the field, and the legacy fallback catalog, keep the GET transport that
older backends accept. GET stays available with the same query
parameters but narrates deterministically, printing a note in place of
the "Configure Pulse Assistant" tip, and other methods get 405 with
Allow: GET, POST. GET also stops resolving the AI service at all: it
reads Patrol findings through a new ExistingAIService lookup, because
GetAIService constructs a tenant service on first use and that can list
provider models or start background discovery. The router's reporting
wiring moves into wireReportingAIResolvers so a test can drive it.

Keeping GET rather than answering 405 avoids breaking documented API
scripts (MSP.md pointed providers at it) for no security gain: CSV is
never narrated and GET no longer reaches the provider.
2026-10-07 21:00:34 +01:00
pulse-triage[bot] 96653f81c4 Reject malformed UTF-8 backup-lock evidence before guest reads
Do not let encoding/json repair malformed configuration strings into evidence of an absent operation lock. Reject the whole malformed response before a guest command and before publishing its payload, while retaining valid Unicode and ordinary configuration decoding.

Contract-Neutral: Restore the existing malformed-config deferral contract without changing guest commands, lock policy, payloads or public interfaces.
Change-source: pulse-maintainer
2026-10-07 19:05:53 +01:00
pulse-triage[bot] ef222a7c7b Reclaim expired guest-command admission before capacity deferral
Count only new endpoint/VMID slots and reclaim expired idle entries when a whole alias group otherwise exceeds the unchanged 4096-entry bound. Keep busy work, uncertain completion, backup locks and single-attempt commands protected. Cover all six HTTP readers, alias reuse and fail-closed full capacity; no native recovery or reported-cause claim.

Change-source: pulse-maintainer
2026-10-07 17:41:02 +01:00
pulse-triage[bot] 0ef2bfa7f1 Keep local disk mounts beside system directories visible
Match automatic system-mount exclusions at directory boundaries rather than textual prefixes. Local /snapshots, /runtime and /var/lib/docker-data volumes retain capacity and usage through agent collection, both report paths and QGA aggregation; genuine system and remote/container mounts remain filtered. Regression controls include host usage alert onset and recovery.

Contract-Neutral: Restore existing filesystem eligibility without changing schemas, identities, explicit operator patterns or guest-command safety policy.
Change-source: pulse-maintainer
2026-10-07 15:36:53 +01:00
pulse-triage[bot] 5fccde258a Integrate reviewed truthful guest filesystem counters
Retain exact reviewed Core commit 6dcff07343. Its dedicated regression tests, both parent-production controls, full Proxmox race result and focused monitoring race/vet receipts support the existing counter invariant repair. The broad monitoring timeout remains adverse evidence, not whole-suite acceptance; native QEMU-only recovery and containing publication remain separate.

Change-source: pulse-maintainer
2026-10-07 14:26:04 +01:00
pulse-triage[bot] 0840845673 Compose reviewed monitoring repairs with current upstream without losing either fence
Preserve exact reviewed tip b5b1fa9777 and upstream a85bdcdc9a additively. Retain both alert threshold test sets. Combine upstream mock-mode epoch fencing with reviewed structural-revision checks and Docker departure ownership; add a regression that a current revision cannot revive an ended epoch. Resolve the monitoring contract to describe both boundaries, without rewriting accepted history.

Publication remains deferred for genuine final-content browser receipts and independent CodeQL disposition. Composition checks do not establish native recovery, deployment, exact-head hosted acceptance or release readiness.

Change-source: pulse-maintainer
2026-10-07 14:13:28 +01:00
rcourtman a85bdcdc9a Stop a stale agent SMART row lending a replaced disk its identity (#2658)
## Problem

PR #2648 stopped the poller's previous-evidence lookup from handing a
replaced disk the old disk's serial. The linked host agent's SMART merge
runs before that lookup and had the same flaw. Its last-resort match is
the device path, and a path names a slot, not a disk. When a disk is
swapped before the agent reports again, or after the agent went silent
and kept its last rows, the agent's row for the old disk matched the
replacement. It filled the old serial or, for SAS, promoted it over the
Proxmox SAS address. The registry then keyed the replacement to the old
disk's canonical resource and SMART history, and it also took the old
disk's temperature and FAILED health. For SAS, the registry's same-path
join (`resolveLinkedPhysicalDisk`) did the same at the canonical level
even when monitoring kept the rows apart.

Proxmox's disk inventory (`PVE::Diskmanage`, checked on a PVE 9 node)
writes the literal `unknown` for a serial or WWN udev cannot read. The
poller recorded it as a collected serial, so the collection state hid
the gap, and the merge's `Serial == ""` fill never let the agent's real
serial replace it.

## Change

- `mergeHostAgentSMARTIntoDisks` refuses a device-path row whose
identity contradicts the Proxmox disk's. The new
`diskinventory.HardwareIdentityConflict` defines a contradiction:
nothing agrees across serial and WWN, and a field both producers report
like for like disagrees.
  - WWNs count for every disk.
- Serials count only where Proxmox's serial is the drive's own, as
smartctl's is: a disk typed `nvme`, or one presented with SCSI vendor
`ATA` (libata, or a SAS HBA's SCSI-ATA translation) and not typed
`usb`/`sas`. Elsewhere the Proxmox serial may be a SAS address or SCSI
designator (#1595) or a USB bridge's own serial, so it only counts
toward agreement.
- Spelling differences are not disagreement: udev's underscores for
whitespace, a T10 designator for an ATA drive (`ATA`, the model, then
the serial), the agent's NAA field spelling, and udev's 64-bit prefix of
an NAA 6 WWN.
- The refusal runs after the unique-path check, so a second row on the
path keeps the match ambiguous.
- The merge's WWN step compares normalized identity
(`HardwareIdentityMatch`), so the agent's `5-c50-a1b2c3d4` finds the
Proxmox `0x5000c500a1b2c3d4` disk.
- `resolveLinkedPhysicalDisk` refuses the SAS same-path fallback when
the two WWNs name different disks.
- The poller records Proxmox's `unknown` serial/WWN as empty, so the
serial reads `missing` and the agent can fill it. Retained evidence
carries a previous serial only when it is usable, so a pre-upgrade
record cannot restore the placeholder. Identity keys already ignored the
placeholder. When the agent now fills a disk's serial, that disk's
Proxmox-poll SMART samples move from its WWN or path key to the serial
key, where the agent's own samples already go.

## Tests

- `TestHostAgentSMARTRowForSwappedOutDiskDoesNotLendItsIdentity` (sata
and sas) drives the real poller and registry. Proxmox reports `unknown`,
the agent fills the serial, the agent goes silent, and the disk is
swapped. On main, the SATA replacement takes the old serial and
resource; the SAS replacement keeps its own serial in state, but the
registry joins it to the old resource with its temperature and FAILED
health.
- `TestHostAgentSMARTMergeRefusesPathMatchAcrossContradictingIdentity`
has one row per producer rule: SAS/SATA/NVMe replacements refused;
same-disk SAS without an agent WWN, a renamed SAS disk in the agent's
WWN spelling, udev's serial spelling, a USB enclosure (typed `usb` and
typed `ssd`), a T10 designator and an NAA 6 volume merged; and an
ambiguous path.
- `TestRegistrySASPathJoinRefusesContradictingWWN`,
`TestProxmoxUnknownDiskIdentityIsRecordedAsUnreported` and
`TestHardwareIdentityConflict` cover the rest.
- Negative controls via `go test -overlay`: removing each rule fails
only its own case. These cover the guard, the WWN normalization, the
transport/vendor rule, compaction, the designator rule, the NAA 6
prefix, the registry guard, the ingest normalization and the
usable-serial carry. Against main's `monitor.go` and `registry.go`, the
end-to-end, merge and registry tests fail.
- `TestMergeHostAgentSMARTIntoDisks_PreservesExistingProxmoxIdentity`
merged a row with a different WWN by path alone, which the guard now
rightly refuses. Its row is now the same disk in the agent's WWN
spelling, with all assertions unchanged.
- `internal/monitoring`, `internal/unifiedresources` and
`pkg/diskinventory` pass in full.

## Not covered

- A replacement with no comparable identity still matches its path: no
usable serial or WWN at all, or a SAS/SCSI/USB disk with a WWN on one
side only.
- The registry's seeded source mapping, reused after a restart by the
slot-shaped Proxmox source ID, can still hand a swapped disk the old
canonical resource. That's a separate follow-up.
2026-10-07 14:01:19 +01:00
pulse-triage[bot] 28bd7a3eae Incorporate batch-start disk identity repair and selector cleanup
Preserve exact published upstream commits at the batch-start frontier. Resolve the TrueNAS history conflict by passing scoped disk identity through the existing ambiguous-alias and in-flight inventory guards, keeping both reviewed protections. No frontend resolution or receipt changes.

Change-source: pulse-maintainer
2026-10-07 13:28:04 +01:00
rcourtman bc15c3500b Stop QEMU default disk serials merging nested Proxmox disks (#2653)
## Problem

Nested Proxmox clusters (each PVE node is itself a QEMU VM) lose
physical disks from inventory. QEMU gives a virtual disk configured
without a serial a default one built from its drive ID or a per-VM
counter, so every VM built the same way reports the same value:

- Proxmox's `/nodes/{node}/disks/list` serial is udev's
`ID_SERIAL_SHORT` (`PVE/Diskmanage.pm`). A virtio-scsi disk on a Proxmox
VM reports `drive-scsi0` with no `ID_WWN`; on a libvirt VM it is
`drive-scsi0-0-0-0`. A `-drive` given no ID falls back to QEMU's
automatic ID, `scsi0-hd0` for `if=scsi` and `none0` for `if=none`.
- An emulated ATA disk's serial is `QM` plus a five-digit counter
(`QM00001`).

`diskinventory.IsUsableHardwareID` accepted all of these, so
`resourceFromPhysicalDisk` set `MachineID = "drive-scsi0"` on every node
and the registry minted one `machine:drive-scsi0` canonical disk and one
SMART series for the whole cluster. Three nodes with two such disks each
produced 2 canonical disks instead of 6.

## Change

- `IsUsableHardwareID` rejects exactly QEMU's default shapes:
`drive-scsi` plus digits with at most three `-digits` groups,
`scsi<n>-hd<n>`, `none<n>`, and `QM` plus five digits. Real serials that
only share a prefix (`DRIVE-ASSET123`, `drive-sata0`, `QM0001`) stay
identity. It also strips `naa.`/`eui.`/`wwn-`/`0x` framing before its
all-zero and all-F test, so a `0x0000000000000000` WWN is a placeholder
too.
- TrueNAS provider: a disk took its canonical identity from any
non-empty serial, so a placeholder merged disks across appliances. It
now uses only a usable serial, and lists superseded IDs for every disk
without one.
- TrueNAS history keys: for a serial that was already rejected, such as
`UNKNOWN`, native TrueNAS history used TrueNAS's own disk identifier,
and the unified SMART writer (`syncUnifiedPhysicalDiskMetrics`)
substituted the metrics target only for an empty serial, so the samples
went to the canonical resource ID. Neither matched the chart reader's
key, and rejecting QEMU serials would have put TrueNAS VMs on that path.
Native history now falls back to the disk's source ID, and the writer
always writes under the resolved metrics target, so writer, native
history and reader agree. History lookup keys also drop placeholder
serials.

## Upgrade effect

A disk reporting one of these values (nested PVE nodes, TrueNAS or agent
hosts running as QEMU VMs with virtual disks, TrueNAS disks reporting
`UNKNOWN`) changes canonical ID and SMART metric key once. Samples
recorded under the old key are not carried over, because that key mixed
every disk that shared the value. Path-shaped alert references (Proxmox
disk health and wearout, host-agent SMART) are unaffected; anything
keyed on the old canonical ID or metric key starts again under the new
one. This is recorded in the monitoring and agent-lifecycle contracts.

## Tests

- `TestIsUsableHardwareIDRejectsQEMUDefaultDiskSerials`: each default
shape is rejected, falls back to the scoped topology ID and never
matches itself; prefix-sharing real serials stay usable.
`TestPreferredIDRejectsPlaceholderHardwareIdentity` gains the framed
all-zero/all-F WWNs.
- `TestNestedProxmoxDefaultQEMUSerialsStayPerNode`: 3 nodes ×
(`drive-scsi0`, `QM00005`, `drive-scsi0-0-0-1`) give 9 canonical disks,
9 slots and 9 metric keys, every SMART writer key is a key readers
resolve, and agent SMART source IDs stay per host.
-
`TestTrueNASPlaceholderDiskSerialsStayPerApplianceAndShareOneHistoryKey`:
two appliances with `UNKNOWN` and `drive-scsiN` serials keep one
canonical disk each, and the SMART writer's samples are readable under
each disk's metrics target.
- `TestProviderPlaceholderDiskSerialsKeyNeitherIdentityNorHistory`:
placeholder serials key no identity, list superseded canonical IDs, drop
out of native history lookup keys, and native history lands under the
registry's metrics target.
- Negative controls via `go test -overlay`, one runtime file reverted at
a time: base `identity.go` fails all three round trips (3 canonical
disks instead of 9, 9 TrueNAS disks instead of 16); base `provider.go`
fails the identity and history assertions; base `monitor.go` fails the
SMART-history-under-reader-key assertion; reverting only the native
history key, the superseded-ID gate or the lookup-key filter fails its
own provider assertion.
- `pkg/diskinventory`, `internal/truenas/...`,
`internal/unifiedresources/...` and `internal/monitoring` pass in full;
`internal/api/...` disk, hardware, serial and TrueNAS tests pass.

Stacked on #2648 (`fix/pve-disk-serial-carry`), whose commit this branch
carries until it merges. Not in this change: the registry's merge of
disks across parents on a canonical-ID collision (separate change), and
a pre-existing writer/reader key mismatch for controller-member disks
without a usable serial or WWN (`PhysicalDiskMetaMetricID` appends
topology to a source ID that already carries it), which is tracked
separately.
2026-10-07 13:22:34 +01:00
pulse-triage[bot] 6dcff07343 Keep untrusted guest filesystem counters out of disk usage
Preserve exact byte observations and explicit zero readings; reject missing, negative, fractional, ambiguous and out-of-range usage instead of publishing a healthy default or wrapping signed disk values. Keep valid peer rows and existing guest-agent request and backup safeguards.

Contract-Neutral: Repairs existing truthful guest disk reading invariants; no wire fields, routes, command admission, retry, lock, backup or identity contract changes.
Change-source: pulse-maintainer
2026-10-07 13:21:19 +01:00
rcourtman 8d00d54740 Stop a stale agent SMART row lending a replaced disk its identity
The linked host agent's SMART merge falls back to matching a Proxmox
disk by device path, and a path names a slot, not a disk. When a disk
is swapped before the agent reports again, or after the agent went
silent and kept its last rows, the row for the old disk matched the
replacement. It filled or, for SAS, promoted the old serial onto it,
which keyed the replacement to the old disk's canonical resource and
metric history, and lent it the old disk's temperature and failed
health. For SAS the registry's same-path join did the same at the
canonical level even when monitoring kept the rows apart.

Both now refuse a row whose identity contradicts the disk's, judged
only on what both producers report like for like. WWNs count for every
disk. Serials count only where Proxmox's serial is the drive's own, as
smartctl's is: NVMe namespaces, and disks presented with SCSI vendor
"ATA", whose serial udev's ata_id reads from IDENTIFY. Elsewhere
Proxmox's serial may be a SAS address, a SCSI designator or a USB
bridge's own serial (#1595). Reporter spellings are not disagreement:
udev writes whitespace in serials as underscores, its T10 designator
for an ATA drive puts "ATA" and the model before the drive serial, and
its ID_WWN keeps only the first 64 bits of an NAA 6 identifier. The WWN match step compares normalized identity too,
so the agent's 5-c50-a1b2c3d4 spelling finds the Proxmox
0x5000c500a1b2c3d4 disk.

Proxmox's disk inventory reports a serial or WWN udev cannot read as
the literal "unknown". The poller recorded it as a collected serial, so
the collection state hid the gap and the agent's serial never filled
it. It is now recorded as empty, and retained evidence carries only a
usable previous serial so an older record cannot restore it. Identity
keys already ignored the placeholder.
2026-10-07 12:16:36 +01:00