Fix multi-organization Proxmox connection identity

This commit is contained in:
courtmanr@gmail.com 2026-07-28 17:15:54 +01:00
parent 8e5ef365d4
commit d1ee6e928b
17 changed files with 251 additions and 86 deletions

View file

@ -1 +1 @@
6.2.0-rc.2
6.2.0-rc.3

View file

@ -2,9 +2,9 @@ apiVersion: v2
name: pulse
description: Helm chart for deploying the Pulse hub and optional Docker monitoring agent.
type: application
version: 6.2.0-rc.2
appVersion: "6.2.0-rc.2"
icon: https://raw.githubusercontent.com/rcourtman/Pulse/v6.2.0-rc.2/docs/images/pulse-logo.svg
version: 6.2.0-rc.3
appVersion: "6.2.0-rc.3"
icon: https://raw.githubusercontent.com/rcourtman/Pulse/v6.2.0-rc.3/docs/images/pulse-logo.svg
keywords:
- monitoring
- proxmox
@ -32,7 +32,7 @@ annotations:
description: Smoke tests with kind cluster deployment
artifacthub.io/links: |
- name: Documentation
url: https://github.com/rcourtman/Pulse/blob/v6.2.0-rc.2/docs/KUBERNETES.md
url: https://github.com/rcourtman/Pulse/blob/v6.2.0-rc.3/docs/KUBERNETES.md
- name: Support
url: https://github.com/rcourtman/Pulse/discussions
artifacthub.io/maintainers: |

View file

@ -1,6 +1,6 @@
# pulse
![Version: 6.2.0-rc.2](https://img.shields.io/badge/Version-6.2.0--rc.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 6.2.0-rc.2](https://img.shields.io/badge/AppVersion-6.2.0--rc.2-informational?style=flat-square)
![Version: 6.2.0-rc.3](https://img.shields.io/badge/Version-6.2.0--rc.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 6.2.0-rc.3](https://img.shields.io/badge/AppVersion-6.2.0--rc.3-informational?style=flat-square)
Helm chart for deploying the Pulse hub and optional Docker monitoring agent.

View file

@ -2,7 +2,7 @@ version: '3.8'
services:
pulse:
image: ${PULSE_IMAGE:-rcourtman/pulse:6.2.0-rc.2}
image: ${PULSE_IMAGE:-rcourtman/pulse:6.2.0-rc.3}
container_name: pulse
restart: unless-stopped
logging:

View file

@ -7,10 +7,12 @@ For historical v4 notes that previously lived in this repo, see:
`docs/releases/RELEASE_NOTES_v4.md`
For the current v6 support release candidate packet, see:
- `docs/releases/RELEASE_NOTES_v6.2.0-rc.2.md`
- `docs/releases/V6_CHANGELOG_v6.2.0-rc.2.md`
- `docs/releases/RELEASE_NOTES_v6.2.0-rc.3.md`
- `docs/releases/V6_CHANGELOG_v6.2.0-rc.3.md`
For historical v6.2 support release candidate packets, see:
- `docs/releases/RELEASE_NOTES_v6.2.0-rc.2.md`
- `docs/releases/V6_CHANGELOG_v6.2.0-rc.2.md`
- `docs/releases/RELEASE_NOTES_v6.2.0-rc.1.md`
- `docs/releases/V6_CHANGELOG_v6.2.0-rc.1.md`

View file

@ -4,11 +4,13 @@ This guide covers practical upgrade steps for existing Pulse installs moving to
For the current v6 support release candidate packet, see:
- `docs/releases/RELEASE_NOTES_v6.2.0-rc.2.md`
- `docs/releases/V6_CHANGELOG_v6.2.0-rc.2.md`
- `docs/releases/RELEASE_NOTES_v6.2.0-rc.3.md`
- `docs/releases/V6_CHANGELOG_v6.2.0-rc.3.md`
For historical v6.2 support release candidate packets, see:
- `docs/releases/RELEASE_NOTES_v6.2.0-rc.2.md`
- `docs/releases/V6_CHANGELOG_v6.2.0-rc.2.md`
- `docs/releases/RELEASE_NOTES_v6.2.0-rc.1.md`
- `docs/releases/V6_CHANGELOG_v6.2.0-rc.1.md`

View file

@ -9229,13 +9229,13 @@
],
"work_claims": [
{
"id": "release-operator-v620rc2-release-gate-single-build-release-promotion-path",
"agent_id": "release-operator-v620rc2",
"summary": "Prepare and publish v6.2.0-rc.2 through the governed exact-SHA promotion workflow",
"id": "release-operator-v620rc3-release-gate-single-build-release-promotion-path",
"agent_id": "release-operator-v620rc3",
"summary": "Prepare and publish v6.2.0-rc.3 through the governed exact-SHA promotion workflow",
"target_id": "v6-product-lane-expansion",
"claimed_at": "2026-07-28T14:59:51Z",
"heartbeat_at": "2026-07-28T14:59:51Z",
"expires_at": "2026-07-28T22:59:51Z",
"claimed_at": "2026-07-28T16:07:39Z",
"heartbeat_at": "2026-07-28T16:07:39Z",
"expires_at": "2026-07-29T00:07:39Z",
"work_item": {
"kind": "release-gate",
"id": "single-build-release-promotion-path"

View file

@ -1136,29 +1136,30 @@ host-local redirect contract as runtime token minting and exchange. Proof input
must reject absolute, scheme-relative, backslash-authority, encoded-separator,
and control-character targets before constructing the handoff request.
The active support prerelease `v6.2.0-rc.2` cut sets the repo-root `VERSION`,
The active support prerelease `v6.2.0-rc.3` cut sets the repo-root `VERSION`,
repo-root `docker-compose.yml` image default, `scripts/install-docker.sh`
fallback, and Helm chart release metadata to the same `6.2.0-rc.2` release
fallback, and Helm chart release metadata to the same `6.2.0-rc.3` release
version. This support prerelease keeps `rollback_version=v6.1.2`, publishes a
versioned public GitHub prerelease plus versioned Docker and Helm artifacts, and
does not move stable/latest install pointers or stable semver aliases. Stable
install pointers stay on `v6.1.2`, whose active stable cut is recorded below and
continues to govern the stable line until this candidate is promoted. The second
`v6.2.0` candidate is a hardening and bugfix cut that supersedes `v6.2.0-rc.1`
without dropping anything it carried: it restores unattended-update reliability
including in-place repair of already-deployed update units, corrects PBS backup
attribution across multiple Proxmox clusters, repairs Proxmox installer
registration including per-canonical-type bootstrap grants on combined hosts,
hardens the Patrol readiness streaming transport and gates readiness on a
completed verdict, ends discovery-policy DNS and SSH retry churn, adds explicit
per-metric threshold off toggles, derives SSO callback URLs from the request
that reached Pulse, and documents Entra ID SSO integration. The exact `main` SHA
must pass the integrated release checks and immutable-candidate build before the
single-build workflow crosses its public mutation boundary.
The `v6.2.0-rc.2` server cut is classified `no-mobile-impact`; no companion
continues to govern the stable line until this candidate is promoted. The third
`v6.2.0` candidate is a focused support correction that supersedes
`v6.2.0-rc.2`: adding a Proxmox connection now applies the same endpoint and
TLS identity doctrine as later configuration consolidation, so clusters in
different organizations are not merged merely because their internal names,
node names, and private addresses overlap. The exact `main` SHA must pass the
integrated release checks and immutable-candidate build before the single-build
workflow crosses its public mutation boundary.
The `v6.2.0-rc.3` server cut is classified `no-mobile-impact`; no companion
build upload is part of this cut. The existing mobile candidate programme
remains separate, and the release packet must not describe a public store
rollout.
The preceding `v6.2.0-rc.2` candidate used the same support-prerelease path
with `rollback_version=v6.1.2` and pinned the same four install surfaces to
`6.2.0-rc.2`. It is superseded by this cut and no longer governs the install
pins; its packet stays in `docs/releases/` as the historical candidate record
for the `v6.2.0` line.
The preceding `v6.2.0-rc.1` candidate used the same support-prerelease path
with `rollback_version=v6.1.2` and pinned the same four install surfaces to
`6.2.0-rc.1`. It is superseded by this cut and no longer governs the install
@ -1313,14 +1314,14 @@ For the active stable `v6.1.2` cut, the repo-root compose default and
`scripts/install-docker.sh` fallback must both pin `6.1.2` whenever the
governed `VERSION` is that stable cut. The stable promotion guard remains in
force and rejects leftover `-rc.` defaults.
For the active support prerelease `v6.2.0-rc.2` cut, the repo-root compose
default and `scripts/install-docker.sh` fallback must both pin `6.2.0-rc.2`
For the active support prerelease `v6.2.0-rc.3` cut, the repo-root compose
default and `scripts/install-docker.sh` fallback must both pin `6.2.0-rc.3`
until the next governed stable cut moves them forward. The stable promotion
guard remains in force and must reject leftover `-rc.` defaults when the
governed `VERSION` returns to a stable release. Each new candidate on the
`v6.2.0` line moves these two pins together with the repo-root `VERSION` and
the Helm chart metadata in the same commit; a candidate that leaves any of the
four on the superseded `6.2.0-rc.1` value is a release-packet blocker.
four on a superseded `6.2.0-rc.*` value is a release-packet blocker.
The RC7 packet refresh records `fc10de9b5477613316473267b72b05b6b2b7aaff`
as the current validation-risk commit. That head includes the earlier
Docker-default correction plus the follow-on capacity-forecast and Patrol

View file

@ -0,0 +1,50 @@
# Pulse v6.2.0-rc.3 Release Notes
`v6.2.0-rc.3` is a release candidate for the next Pulse v6 minor line. It
follows stable `v6.1.2` and supersedes `v6.2.0-rc.2`. This focused support
candidate corrects the remaining multi-organization Proxmox connection
collision reported against the first two candidates.
## Highlights
- Proxmox clusters in different organizations stay separate even when they
reuse the same internal Proxmox cluster name, node names, and private IP
addresses.
- Captured TLS certificate fingerprints now govern the add-connection path,
using the same identity decision as configuration consolidation.
## Fixed
- The Proxmox add-connection handler no longer merges a newly discovered
cluster into an existing connection solely because their internal cluster
names match.
- A contradictory TLS fingerprint vetoes the merge before either connection
can be removed from saved configuration.
- Legitimate duplicate connections to the same physical cluster still merge
when their cluster name, endpoint evidence, and TLS identity agree.
## Upgrade Notes
Use the normal v6 install or update flow for `v6.2.0-rc.3` only when you are
comfortable testing an RC. The rollback target is `v6.1.2`.
The exact rollback reinstall command is:
```bash
./scripts/install.sh --version v6.1.2
```
After updating, a cluster that returned to Network Discovery can be connected
again. Existing healthy connections do not need to be removed first.
This server candidate has no mobile compatibility change and does not require a
companion build upload. No public mobile-store rollout is part of this RC.
Windows Unified Agent binaries in this candidate keep checksum and
detached-signature verification, but they are not yet Authenticode-signed and
Windows may show an unknown-publisher warning. No unsigned-Windows exception
applies to any `v6.2.0` release. Stable `v6.2.0` must publish Windows agents
through the mandatory SignPath Authenticode path.
Paid Pulse Pro, Relay, and eligible legacy customers should continue to use the
private download page and private runtime image for paid runtime features.

View file

@ -0,0 +1,30 @@
# Pulse v6.2.0-rc.3
_This changelog describes the changes since `v6.2.0-rc.2`.
`v6.2.0-rc.3` remains a prerelease and rolls back to stable `v6.1.2`._
## Fixed
- Same-named Proxmox clusters from different organizations remain distinct
when overlapping node names and private addresses carry contradicting TLS
certificate fingerprints.
- The connection add handler now uses the shared identity predicate instead of
treating a matching internal cluster name as sufficient proof of identity.
- A regression test reproduces the reported sequence where adding `rewo`
removed `enacon`, then adding `enacon` removed `rewo`.
## Release Metadata
- Version: `v6.2.0-rc.3`
- Previous candidate: `v6.2.0-rc.2`
- Previous stable: `v6.1.2`
- Rollback target: `v6.1.2`
- Rollback command: `./scripts/install.sh --version v6.1.2`
- Promotion path: exact-SHA single-build release candidate from `main`,
published as a support prerelease that does not move stable or latest
install pointers
- Windows signing decision: Authenticode through SignPath is the mandatory
signing backend and no unsigned-Windows exception applies to any `v6.2.0`
release
- Mobile decision: `no-mobile-impact`; no companion build upload or public
store rollout is part of this candidate

View file

@ -11,6 +11,7 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
)
func TestHandleAddNode(t *testing.T) {
@ -491,3 +492,64 @@ func TestHandleAddNodeConsolidatesStandaloneOverlapIntoClusterInMemory(t *testin
t.Fatalf("expected VerifySSL to be promoted onto the surviving cluster")
}
}
func TestHandleAddNodeKeepsDistinctSameNameClustersWithContradictingFingerprints(t *testing.T) {
cfg := &config.Config{
DataPath: t.TempDir(),
PVEInstances: []config.PVEInstance{
{
Name: "enacon",
Host: "https://192.168.1.253:8006",
ClusterName: "production",
IsCluster: true,
ClusterEndpoints: []config.ClusterEndpoint{
{
NodeName: "pve01",
Host: "https://192.168.1.246:8006",
IP: "192.168.1.246",
Fingerprint: "AA:AA",
},
},
},
},
}
handler := newTestConfigHandlers(t, cfg)
originalDetect := detectPVECluster
t.Cleanup(func() { detectPVECluster = originalDetect })
detectPVECluster = func(clientConfig proxmox.ClientConfig, nodeName string, existing []config.ClusterEndpoint) (bool, string, []config.ClusterEndpoint) {
return true, "production", []config.ClusterEndpoint{
{
NodeName: "pve01",
Host: "https://192.168.1.246:8006",
IP: "192.168.1.246",
Fingerprint: "BB:BB",
},
}
}
body, err := json.Marshal(map[string]any{
"name": "rewo",
"type": "pve",
"host": "192.168.1.254",
"tokenName": "pulse@pve!token",
"tokenValue": "secret",
})
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPost, "/api/config/nodes", bytes.NewBuffer(body))
rec := httptest.NewRecorder()
handler.HandleAddNode(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("status = %d, want 201: %s", rec.Code, rec.Body.String())
}
if len(cfg.PVEInstances) != 2 {
t.Fatalf("PVE instances = %#v, want both organizations to remain configured", cfg.PVEInstances)
}
if cfg.PVEInstances[0].Name != "enacon" || cfg.PVEInstances[1].Name != "rewo" {
t.Fatalf("PVE instance names = %q, %q, want enacon and rewo", cfg.PVEInstances[0].Name, cfg.PVEInstances[1].Name)
}
}

View file

@ -405,15 +405,23 @@ func (h *ConfigHandlers) handleAddNode(w http.ResponseWriter, r *http.Request) {
isCluster, clusterName, clusterEndpoints = detectPVECluster(clientConfig, req.Name, nil)
}
// CLUSTER DEDUPLICATION: If this node is part of a cluster, check if we already
// have that cluster configured. If so, this is a duplicate - we should merge
// the node as an endpoint to the existing cluster instead of creating a new instance.
// This prevents duplicate VMs/containers when users install agents on multiple cluster nodes.
// CLUSTER DEDUPLICATION: If this node is part of a cluster, merge it
// only when endpoint and TLS evidence identify an existing cluster.
// Cluster names and private addresses can repeat across organizations.
if isCluster && clusterName != "" {
candidateCluster := config.PVEInstance{
Name: req.Name,
Host: host,
Fingerprint: req.Fingerprint,
IsCluster: true,
ClusterName: clusterName,
ClusterEndpoints: clusterEndpoints,
}
for i := range h.getConfig(r.Context()).PVEInstances {
existingInstance := &h.getConfig(r.Context()).PVEInstances[i]
if existingInstance.IsCluster && existingInstance.ClusterName == clusterName {
// Found existing cluster with same name - merge endpoints!
if config.PVEClusterInstancesShareIdentity(*existingInstance, candidateCluster) {
// Strong identity evidence confirms this is another view
// of the existing cluster, so merge its endpoints.
log.Info().
Str("cluster", clusterName).
Str("existingInstance", existingInstance.Name).

View file

@ -125,6 +125,18 @@ func pveClusterInstancesHaveFingerprintConflict(a, b PVEInstance) bool {
return false
}
// PVEClusterInstancesShareIdentity reports whether two configured cluster
// views have enough evidence to be consolidated. Cluster names are not unique
// across organizations, so a matching name alone is never sufficient.
func PVEClusterInstancesShareIdentity(a, b PVEInstance) bool {
return a.IsCluster &&
b.IsCluster &&
strings.TrimSpace(a.ClusterName) != "" &&
strings.EqualFold(strings.TrimSpace(a.ClusterName), strings.TrimSpace(b.ClusterName)) &&
!pveClusterInstancesHaveFingerprintConflict(a, b) &&
pveClusterInstancesHaveStrongOverlap(a, b)
}
func mergeDuplicateClusterInstances(instances []PVEInstance) ([]PVEInstance, bool) {
removed := make(map[int]struct{})
mergedAny := false
@ -146,10 +158,7 @@ func mergeDuplicateClusterInstances(instances []PVEInstance) ([]PVEInstance, boo
continue
}
duplicate := instances[dupIdx]
if !duplicate.IsCluster ||
!strings.EqualFold(strings.TrimSpace(primary.ClusterName), strings.TrimSpace(duplicate.ClusterName)) ||
pveClusterInstancesHaveFingerprintConflict(*primary, duplicate) ||
!pveClusterInstancesHaveStrongOverlap(*primary, duplicate) {
if !PVEClusterInstancesShareIdentity(*primary, duplicate) {
continue
}

View file

@ -237,6 +237,29 @@ func TestConsolidatePVEInstancesKeepsSameNameClustersWithCollidingPrivateIPs(t *
}
}
func TestPVEClusterInstancesShareIdentityRejectsSameNameWithContradictingFingerprints(t *testing.T) {
siteA := PVEInstance{
Name: "enacon",
ClusterName: "production",
IsCluster: true,
ClusterEndpoints: []ClusterEndpoint{
{NodeName: "pve01", Host: "https://192.168.1.246:8006", IP: "192.168.1.246", Fingerprint: "AA:AA"},
},
}
siteB := PVEInstance{
Name: "rewo",
ClusterName: "production",
IsCluster: true,
ClusterEndpoints: []ClusterEndpoint{
{NodeName: "pve01", Host: "https://192.168.1.246:8006", IP: "192.168.1.246", Fingerprint: "BB:BB"},
},
}
if PVEClusterInstancesShareIdentity(siteA, siteB) {
t.Fatal("same-name clusters with contradicting TLS fingerprints must not share identity")
}
}
func TestConsolidatePVEInstancesKeepsStandaloneWithContradictingFingerprint(t *testing.T) {
// A standalone whose address collides with a cluster endpoint at another
// site must not fold into that cluster when its TLS fingerprint disagrees.

View file

@ -6,7 +6,7 @@ set -euo pipefail
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
DOCKER_IMAGE_REPO="${DOCKER_IMAGE_REPO:-rcourtman/pulse}"
CANONICAL_DEFAULT_PULSE_VERSION="6.2.0-rc.2"
CANONICAL_DEFAULT_PULSE_VERSION="6.2.0-rc.3"
resolve_default_pulse_version() {
if [ -n "${PULSE_IMAGE_VERSION:-}" ]; then

View file

@ -406,27 +406,13 @@ func TestCurrentSupportPrereleasePacketTracksInstallMetadata(t *testing.T) {
"`v"+version+"` is a release candidate",
"## Highlights",
"stable `v"+previous+"`",
"supersedes `v6.2.0-rc.1`",
"Unattended updates no longer stop Pulse in the night",
"the timer now runs once, failures always restart the service",
"already-deployed installations repair their own stale update units",
"PBS backups are attributed to the right guest on estates with more than one Proxmox cluster",
"combined PVE+PBS hosts",
"survives reverse proxies with short read timeouts",
"never renders a proxy's HTML error page into the results",
"now uses the shared DNS cache",
"`pulse-update.timer`",
"submission-source learner that only trusts evidence when every candidate cluster is visible",
"one registration per product type, bound to the first presenting hostname",
"streams keepalive bytes from the first moment of the run",
"A connection cut mid-run is classified as interrupted",
"an interrupted check never blocks Patrol from running",
"resolves hostnames through the shared cached resolver",
"back off with decay",
"explicit On/Off toggle",
"never a `localhost` guess that fails at the identity provider",
"Microsoft Entra ID (Azure AD) SSO integration guide",
"`CAP_NET_RAW`",
"supersedes `v6.2.0-rc.2`",
"clusters in different organizations stay separate",
"internal Proxmox cluster name",
"node names, and private IP addresses",
"Captured TLS certificate fingerprints",
"add-connection path",
"same identity decision as configuration consolidation",
"no mobile compatibility change",
"No public mobile-store rollout",
"not yet Authenticode-signed",
@ -435,26 +421,17 @@ func TestCurrentSupportPrereleasePacketTracksInstallMetadata(t *testing.T) {
)
assertFileContainsAllNormalized(t, changelogPath,
"Version: `v"+version+"`",
"Previous candidate: `v6.2.0-rc.1`",
"Previous candidate: `v6.2.0-rc.2`",
"Previous stable: `v"+previous+"`",
"Rollback target: `v"+previous+"`",
"Promotion path: exact-SHA single-build release candidate from `main`",
"This changelog describes the changes since `v6.2.0-rc.1`",
"Explicit per-metric off toggles in the per-resource override editor",
"OIDC provider cards show the provider's callback / redirect URL with a copy button",
"carried two `OnCalendar` lines",
"Auto-update asset staging is guarded",
"migrate through a transient `systemd-run` unit on the next update cycle",
"PBS backup attribution for VMIDs shared across clusters",
"one registration per canonical product type",
"The Patrol readiness endpoint commits its response header and streams keepalive bytes",
"Patrol readiness is reported only from a completed overall verdict",
"resolves hostnames through the shared cached resolver",
"back off with decay instead of retrying every poll cycle",
"LXC memory metrics fall back to the cluster-resources listing",
"Agent command execution honors the configuration gate at channel admission",
"derived from the configured public URL or, when none is set, from the request that reached Pulse",
"Microsoft Entra ID (Azure AD) SSO integration guide",
"This changelog describes the changes since `v6.2.0-rc.2`",
"Same-named Proxmox clusters from different organizations remain distinct",
"overlapping node names and private addresses",
"contradicting TLS certificate fingerprints",
"connection add handler",
"shared identity predicate",
"regression test reproduces the reported sequence",
"Windows signing decision: Authenticode through SignPath is the mandatory signing backend",
"Mobile decision: `no-mobile-impact`",
"no companion build upload or public store rollout",

View file

@ -128,7 +128,7 @@ func TestPreviousStableForPrereleaseVersionCrossesMinorBoundaries(t *testing.T)
want string
}{
{version: "6.0.5-rc.4", want: "6.0.4"},
{version: "6.2.0-rc.2", want: "6.1.2"},
{version: "6.2.0-rc.3", want: "6.1.2"},
}
for _, test := range tests {
@ -336,7 +336,8 @@ func TestInstallDockerProofTracksSupportPrereleaseContract(t *testing.T) {
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
"The active support prerelease `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
"This support prerelease keeps `rollback_version=v"+previous+"`, publishes a versioned public GitHub prerelease plus versioned Docker and Helm artifacts, and does not move stable/latest install pointers or stable semver aliases.",
"is a hardening and bugfix cut that supersedes `v6.2.0-rc.1` without dropping anything it carried: it restores unattended-update reliability including in-place repair of already-deployed update units, corrects PBS backup attribution across multiple Proxmox clusters, repairs Proxmox installer registration including per-canonical-type bootstrap grants on combined hosts, hardens the Patrol readiness streaming transport and gates readiness on a completed verdict, ends discovery-policy DNS and SSH retry churn, adds explicit per-metric threshold off toggles, derives SSO callback URLs from the request that reached Pulse, and documents Entra ID SSO integration",
"is a focused support correction that supersedes `v6.2.0-rc.2`: adding a Proxmox connection now applies the same endpoint and TLS identity doctrine as later configuration consolidation",
"The preceding `v6.2.0-rc.2` candidate used the same support-prerelease path with `rollback_version=v6.1.2` and pinned the same four install surfaces to `6.2.0-rc.2`.",
"The preceding `v6.2.0-rc.1` candidate used the same support-prerelease path with `rollback_version=v6.1.2` and pinned the same four install surfaces to `6.2.0-rc.1`.",
"The `v"+version+"` server cut is classified `no-mobile-impact`; no companion build upload is part of this cut. The existing mobile candidate programme remains separate, and the release packet must not describe a public store rollout.",
"Authenticode signing through SignPath is the canonical Windows signing backend for the `v6.2.0` line.",