Admit detached metadata in order, merge identity fields without restoring stale
operator edits, and persist complete snapshots with one owned writer. Seal late
writes and return shutdown failures before organisation data or offboarding is
removed. Preserve existing drain budgets, file format, tenant boundaries and
atomic persistence. Add controlled burst, ordering, write-error, blocked-close
and real deletion controls.
Contract-Neutral: Behavioural repair of existing JSON persistence and organisation deletion; no route, field, permission, identity format, schema or provider authority change. Shutdown failure uses the existing HTTP error envelope. Internal ownership methods and explanatory contract text change, not the public contract shape.
Change-source: pulse-maintainer
Install the default monitor's snapshot callback after both platform pollers exist. Settings already observes their runtime owners; alert checks must not retain startup's nil pointers. Exercise actual saved connections, loopback failures, ledger, monitor confirmation ticks, disable and ordinary scheduled inventory recovery without changing policy or adding a surface.
Contract-Neutral: Restore existing connection-alert wiring; no route, payload, authentication, persistence or detector-policy changes.
Change-source: pulse-maintainer
Saved TrueNAS and VMware connections belong to validated tenant storage keys, not optional organisation metadata. A malformed org.json could prevent provider creation and block authoritative disable or removal in an unrelated tenant. Enumerate storage identities while retaining strict metadata/account readers, tenant isolation and config/credential failure handling.
Synthetic probe, polling and API controls reproduce the interval-only response family in #2382. They do not establish either reporter installation’s cause or native appliance acceptance.
Change-source: pulse-maintainer
Update the legacy contract assertion exposed by the full API suite. A successful system-info-only probe remains successful but must not invent a runtime attempt, success or observed inventory. The real poll and failure-preservation controls remain unchanged.
Change-source: pulse-maintainer
The connected proof passed both probe/poll evidence cases but found one older fixture still expecting a successful stored-key probe to manufacture poll success. Keep its stored-secret, HTTPS and missing-ID controls while requiring untouched runtime timestamps and observations. Retain the adverse proof separately.
Change-source: pulse-maintainer
Preserve reviewed tip 888d918ba1 and startup-fetched upstream 8e14b4a63e unchanged in history. Resolve the allocation-test conflict by retaining the upstream boolean entry point with the reviewed bounded child launcher, strict measurement witness, escaping root sink and background-allocation control. No runtime or threshold changes; no stopped refresh replay.
Contract-Neutral: Test-only additive reconciliation of allocation-isolation repairs; runtime contracts unchanged.
Change-source: pulse-maintainer
Merge exact upstream 99927467f5. Resolve only the agent-lifecycle contract append conflict by retaining both monitor recovery ownership and AI usage export clauses. Frontend auto-merges preserve reviewed final bytes; no runtime conflict correction or browser receipt rebinding.
Change-source: pulse-maintainer
A saved connection test reads system.info on a separate client. Keep both probe results out of runtime timestamps, failures, retry cadence and inventory readiness. Preserve stored-secret and tenant boundaries, and validate the existing API evidence through real local client/poller/handler controls. Update the matching API, monitoring, lifecycle and recovery contracts together. This repairs the diagnostic gap in #2382; its native polling/session cause remains unverified.
Change-source: pulse-maintainer
TestNormalizeRoute_RootFastPathDoesNotAllocate failed in the api-2 shard
of PR 2445 with normalizeRoute("/") reporting 419 allocations per call,
though the fast path only returns a constant. testing.AllocsPerRun diffs
the process-wide malloc counter, so it also counts allocations made by
parallel tests and by goroutines earlier tests leave running whenever the
scheduler lets one in during the measurement. Under CPU contention with a
leaked allocating goroutine, the old checks failed in 30 of 30 runs.
Both zero-allocation tests (normalizeRoute's root fast path and
normalizeSegment) now re-run themselves in a child of the same test
binary, limited to that one test, where nothing else allocates. The
assertion is unchanged: removing the fast path still fails with
normalizeRoute("/") allocated 1 times per call. The same contended
stress passes 30 of 30, and -count=200 passes locally and on pulse-dev,
with and without -race.
AllocsPerRun observes process-wide allocations even with GOMAXPROCS=1. Run both zero-allocation checks in the same instrumented test binary with only the selected measurement enabled, bound execution to its marker and retain strict failure propagation. A synchronised background-allocation control demonstrates the old attribution error while the child checks still demand zero. Root results now escape as real metric labels do. Production normalisation and thresholds are unchanged.
Contract-Neutral: Test-process isolation and connected allocation controls only; no runtime, API or performance contract delta.
Change-source: pulse-maintainer
The required API shard failed because its healthy filesystem fixture omitted VM config. Supply a verified unlocked config instead of bypassing the guard, and cover backup/snapshot locks, absent or malformed config, permission refusals and locks appearing in flight through the real diagnostic client.
Contract-Neutral: Diagnostic fixtures and connected safety assertions only; production guest admission, APIs and polling behavior are unchanged.
Change-source: pulse-maintainer
The containing-source API suite passed its auth assertions but failed temporary-directory cleanup in the tenant token test. It unnecessarily started a complete mock monitoring runtime for a request-context check. Use the existing cached-monitor fixture pattern instead; retain global fallback and add real tenant-token and invalid-token controls. This changes no production authentication or monitor lifecycle behaviour.
Change-source: pulse-maintainer
The Anthropic client logged cache_creation_input_tokens and
cache_read_input_tokens and then returned only input_tokens and
output_tokens, so every cached Patrol run reached the usage ledger, the
cost preview, the budget check and the weekly digest with its cache
buckets missing. Pulse #2350 reproduced the loss on both the Chat and
ChatStream paths, and #2118's reporter was told to trust the provider's
billing view over Pulse's totals until this was fixed.
Both response paths now carry the two buckets as fields disjoint from
InputTokens (streaming assigns them from message_start and the final
cumulative message_delta rather than summing). The agentic loops, Patrol
responses and run records sum them beside the ordinary totals, the usage
ledger and its persisted copies keep them, and cost.EstimateUsageUSD
prices each bucket at Anthropic's published cache rates (5-minute write
1.25x, read 0.1x of the input rate), falling back to the input rate for
providers without a known cache price. Spend summaries, the digest spend,
the cost projection medians and the usage export expose the buckets and
price them the same way. InputTokens keeps its meaning everywhere, so the
telemetry token buckets are unchanged.
Keep at most 4096 remembered org/agent pairs and use an expiry index instead of repeated full-map scans. Audit unremembered overflow deliveries conservatively, preserving daily suppression for established agents, changed-config/token events and every failed fetch. Cover capacity, expiry, concurrency and the real handler contract.
Change-source: pulse-maintainer
Handle nullable retry-row diagnostics while checking the actual attempt audit and DLQ error; verify omitted stored API keys through the established redacted response type and preserve authorised endpoint/target round-trips. Retain the original diagnostic repair and its adverse proof.
Change-source: pulse-maintainer
Suppress raw CLI targets/output, HTTP response bodies and credential-bearing endpoint paths across firing, recovery, tests, queue audits and settings logs. Preserve exact delivery inputs, typed causes and retry classification; retain structured status, counts and safe validation reasons.
Contract-Neutral: Shared agent-lifecycle and storage-recovery references are unchanged; this diagnostic repair alters no agent or storage behaviour, API schema, destination admission or delivery policy.
Change-source: pulse-maintainer
Adapt PR2351 to current main without losing bounded probe deadlines, retries, explicit binds or confidentiality. Inspect IPv6 wildcard socket mode so unrelated servers sharing the port cannot determine Pulse health. Apply the telemetry callback only after a persisted explicit boolean transition, including stale-disk and null-input boundaries.
Retain real HTTP/HTTPS wildcard and same-port isolation regressions, callback persistence-order tests, and the owning subsystem contracts. No dependency manifests, frontend source or telemetry payload schema change. Exact runtime proof remains dependent on the unavailable root graph; it is not represented as passed.
Change-source: pulse-maintainer
Original-source: https://github.com/rcourtman/Pulse/pull/2351
Original-commit: f50c4d6e3b
Co-authored-by: rcourtman <8825017+rcourtman@users.noreply.github.com>
The executable overflow regression and a bounded Bash PTY probe show canonical terminals truncate long input before the size check. Read a bounded character count with echo held off, drain an invalid line before returning to the calling shell, and restore its modes on every exit. Pin maximum-size preservation, overflow history safety and terminal restoration.
Contract-Neutral: Repair bounded private terminal entry, overflow draining and mode restoration; no issuance, scope, install transport or response-shape delta.
Change-source: pulse-maintainer
Reuse the complete-download and preflight private-token entry boundary for both monitoring modes. Keep issued credentials separate and no-store, use the installer-owned token path in the diagnostic service reference, and reject structural unit injection before minting. Add executable root/sudo, history, transport, failure cleanup and systemd grammar regressions alongside the subsystem contracts.
Change-source: pulse-maintainer
Initialise the real persistent auth stores for the guide lifecycle and prove user removal revokes its session and CSRF token. Clarify that an empty role list clears built-in assignments too; no runtime authority changes.
Contract-Neutral: Documentation and test-fixture correction only; session revocation, role assignment and authorization runtime contracts are unchanged.
Change-source: pulse-maintainer
Use the accepted organisation ID alphabet and canonical resource types, and expect the documented role assignments to return 204. Keep the guide lifecycle checks at the existing API contract.
Contract-Neutral: Documentation and test expectations only; API methods, validation, authentication and tenant isolation are unchanged.
Change-source: pulse-maintainer
Keep RBAC, audit and organisation tokens out of process arguments. Use signed-in organisation mutations, actual schemas and acceptance rules, a custom role ID, and private audit exports. Exercise copied commands and handler lifecycles without changing runtime authority.
Contract-Neutral: Documentation and documentation tests only; authentication, RBAC, tenant isolation, licensing and API runtime contracts are unchanged.
Change-source: pulse-maintainer
Require the exact shared artifact command, stdin PVE authorization and correctly delimited normalized URL instead of legacy pipe/header forms or an over-broad path substring. These corrections retain installer, ACL and transport assertions; no runtime change.
Change-source: pulse-maintainer
Retain configured-origin and proxy-spoofing coverage while requiring separate credentials, identical tokenless script URLs and private-input renderers. This aligns remaining routed setup assertions with the credential-safe response; no runtime change.
Change-source: pulse-maintainer
The root parser's legacy no-token alias accepted a new private-file command beside a legacy download URL. Reject every mixed alias, as the existing coherent-transport contract requires. Preserve backup-permission URL bindings. Align older secret-in-command and shared type fixtures, and exercise the actual wrong server installer parser.
Contract-Neutral: Enforce the already-declared coherent bootstrap transports and correct verification fixtures; no API, scope or documented behaviour delta.
Change-source: pulse-maintainer
Separate PVE/PBS setup and telemetry credentials from shell source and download URLs. Use silent root/sudo input, private-file handoff, complete downloads and the agent preflight; preserve scope, TLS defaults, single-line paste and coherent rolling-upgrade metadata. Reveal tokens through the existing dialog and discard late issuance after close. Pin executable shell, history, TLS/registration and browser contracts without using real credentials.
Change-source: pulse-maintainer
Updating to v6.4.5-rc.5 and then v6.4.5 left the progress modal on
"Downloading update... 10%" even though the update finished in the
background. The status stream could go silent behind a proxy (Tailscale
Serve, nginx buffering) and the modal only advanced on stream events.
Backend: the update SSE endpoint now sends the current status on connect,
writes events through one ordered writer with an explicit flush, sends a
15s heartbeat and sets X-Accel-Buffering: no.
Frontend: a silence watchdog falls back to status polling, the restart
phase is entered only on real restart evidence (restarting status, the
server going away, or a version change) rather than a single failed poll,
and the page never auto-reloads on unconfirmed completion without a
pre-update version baseline to compare against.
Relay left public checkout on 2026-09-29: it only ever connected the
Pulse Mobile app, which retires on 31 March 2027, and existing Relay
subscribers now carry Pro entitlements. The app still sold it. Every
Community install saw a "Get alerts on your phone ... Available with
Relay and Pro plans" upgrade panel on Alerts destinations, the plan
screen offered a Relay card with "Remote web access via Relay", and the
settings section was called Remote Access although Relay never reached
the web UI.
The Alerts push panel now renders only on instances that have the relay
feature, with no upsell. The settings section, nav, header and locale
catalogs say Pulse Mobile and carry the retirement date. Community sees
only the Pro comparison card and Relay-tier licenses see none; gated
mobile features name Pro as their minimum plan. The relay feature is
labelled "Pulse Relay (Mobile Connection)" in the catalog, plan copy no
longer claims remote web access, the backend pairing diagnostics point
at Settings > Pulse Mobile, and the user docs, including PRIVACY.md,
state that Relay does not provide remote web UI access.
The unlock screen told Docker users to run `docker exec` from the host,
which does not fit the Unraid, Portainer and TrueNAS app UIs, where the
console already runs inside the container. Those `container_other`
installs are the largest group stalling before first setup: in the week
to 2026-09-28, 158 of 3114 persistent Docker installs never got past
first start, against 94 of 5462 binary and systemd installs.
The startup log also suggested revealing the token "by reading the token
file path". That file holds ciphertext, as the security-privacy contract
requires, so anyone following the hint pasted JSON the unlock screen
rejects. The log now names the command, including the console form, and
the install docs and the contract gain the same guidance.
Contract-Neutral: copy-only: new unlock-screen command row and corrected startup log text; no API payload, lifecycle, storage or primitive change; owning security-privacy clause updated
A failed update check never reaches the update history, because nothing
was applied, so telemetry could not tell an install whose check is broken
from one that was offered an update and ignored it. The 6.4.3-rc.1 installs
looked healthy while their check failed on every call (#2282).
The update manager now records a closed outcome for every check on the
install's effective channel (up_to_date, available, no_release,
rate_limited, network_error, metadata_error, skipped, error), tagging
lookup failures without changing their error text, and ignores previews of
another channel from Settings. Telemetry schema v18 exports update_channel,
update_check_outcome and update_available. No version, URL or error text
leaves the install. The Settings preview type, privacy disclosure, adoption
report and subsystem contracts are updated to match; the Pulse Pro receiver
change lands separately and the parity check passes against it.
The advisory Benchmarks check failed four pull requests on
NormalizeSegment alone in the last 48 paired runs: #2217, #2225, #2252
and #2253. None of them touched the normaliser. On #2252 all five
sub-benchmarks rose 25-39% together. Nine other runs moved the rows
down by more than 10% instead, -20% to -28% on #2228, #2230 and #2242.
Those 13 runs used EPYC 7763, EPYC 9V74 and Intel Xeon runners.
The values are quantized. On EPYC 7763 short_name is 2.50ns or 3.12ns
and uuid is about 29.6ns or 40.5ns, a cycle or two per call, or one
per byte of the UUID scan. Go aligns these functions to 32 bytes, so a
change anywhere earlier in the binary can move them between the halves
of a 64-byte block. A 32-byte shift is enough: on pulse-dev, adding an
unrelated init-time function ahead of the normaliser flipped that
parity and failed the old tree the way #2252 did, all five rows +25% to
+40% at p=0.000 and nothing else flagged. More samples cannot separate
that from a real change, and batching does not help, because every
call repeats the same instructions.
The route-level rows do not behave this way. Over the same 48 runs no
NormalizeRoute row except the root case retired in #2255 moved by more
than 10%. with_uuid came closest at 9.2%.
The guarantees the benchmark carried now live in deterministic tests.
TestNormalizeSegment, TestLooksLikeUUID and TestIsNumeric already pin
classification. TestNormalizeSegment_DoesNotAllocate pins zero
allocations for the five former inputs, and fails with one allocation
per call when the pass-through return copies the segment. It stores
each result in a package-level sink: when normalizeSegment was cut
down to an inlinable copy, a discarded result let the compiler keep
short segments on the stack, and the test missed the allocation.
Timing regressions still show in BenchmarkNormalizeRoute, since every
path runs its segments through normalizeSegment. The proofs below are
paired runs of the CI runner's recipe on pulse-dev, narrowed to
./internal/api/ and raised to 30 samples, because the shared host's
load spread these rows by up to 25%, against about 1% on CI runners.
- looksLikeUUID scanning twice: check-bench-regression.sh exited 1,
flagging only NormalizeRoute/with_uuid, +13.7% (p=0.005).
- normalizeSegment doing its work twice: exited 1, flagging only
NormalizeRoute/with_uuid, +21.9% (p=0.030).
- the candidate against a copy of itself: exited 0.
- the candidate against itself plus the same init-time function:
exited 0. The route rows still moved 4-5%, and with_uuid 9.2%
(p=0.000).
The margin for a UUID-scan slowdown is thin. On EPYC 7763 the scan is
about 30ns of a 280ns with_uuid call, so doubling it reads as roughly
+11% to +14%, while layout alone has moved with_uuid by up to 9.2% both
on CI and in the probe above. Giving UUIDs a larger share of a route
would make that row flip the way this one did; a smaller share would
hide the slowdown.
The advisory Benchmarks check kept failing unrelated pull requests on
NormalizeRoute/root alone (#2228, #2242, #2245, #2250, and PR 1973 on
2026-09-08), and flagged it beside other results on #2230 and #2244.
The "/" path is a length compare and a constant return.
In the last 19 paired PR runs, the 11 on AMD EPYC 7763 measured root
at exactly 2.18ns or 2.50ns on both sides, each within 0-1%. The two
values are about one CPU cycle apart. In five of those runs base and
candidate landed on different values, four times up (+14%) and once
down (-13%), though none touched the normaliser. The earlier
investigation found the benchmark loop identical and the normaliser
differing only in relocated addresses. More samples cannot separate a
stable one-cycle shift from a real change, and a looser threshold
would blunt the gate for everything else.
Batching root calls does not escape it. On an amd64 host a 64-call
batch spread as widely in relative terms as a single call, because
every call repeats the same few instructions.
What the path must guarantee is that "/" and "" return before
splitting. TestNormalizeRoute_RootFastPathDoesNotAllocate pins that
deterministically: with the fast path removed, TestNormalizeRoute
still passes, but the new test fails with one allocation per call.
Real regressions stay visible. A paired run using the CI runner's own
alternating recipe (10 samples, 100ms), with normalizeRoute made to do
its work twice, flagged every remaining NormalizeRoute sub-benchmark
at +89% to +107%, FullMiddlewarePath at +54%, and bytes and allocs at
+100%. check-bench-regression.sh exited 1. The same tree compared with
itself exited 0.
Every accepted agent report refreshed the canonical resource store, and
every refresh covers the whole estate, so refresh work grew with agent
count × estate size (#2199). #2235 made each pass linear and cheaper.
This PR bounds how many passes reports can trigger.
**Behaviour.** Accepted host, Docker and Kubernetes reports, including
rejected-report liveness, refresh through
`refreshUnifiedResourceStoreAfterAgentReport`:
- The first report after a quiet window is published before its handler
returns, the same as today.
- Reports within 2 s of the previous refresh fold into one trailing
refresh at the window's end. It reads the latest state and broadcasts it
to websocket clients.
- 2 s matches `readPathRegistryFreshness`, the staleness the read paths
already accept.
- Agent removal and host-agent evaluation still refresh immediately, so
removed inventory retires at once.
- `Monitor.Stop` cancels a pending trailing refresh and waits for a
running one before closing stores.
- Monitors built without `New` keep a zero window and refresh on every
report.
**Cost bound.** At 2,080 resources one refresh costs about 82 ms (after
#2235). 200 agents reporting every 30 s drive 6.7 refreshes/s, roughly
55% of a core; coalesced, reports trigger at most one refresh per 2 s,
about 4%, however many agents report.
**Contract change.** The agent-lifecycle contract promised that an
accepted report is visible when its handler returns. It is now visible
within the 2 s window. The founder approved that trade on 24 September
2026, and the contract records it.
**Tests**
- `TestAgentReportRefreshFoldsReportsWithinWindow`: the leading report
is published synchronously; a burst inside the window is deferred; one
trailing refresh publishes the burst's latest value; after a quiet
window the next report is synchronous again. It fails when the throttle
is removed.
- `TestStopCancelsPendingAgentReportRefresh`: fails if stop does not
cancel the pending refresh.
- The issue-1753 end-to-end test posts two reports back to back, so it
now waits for the published rows instead of reading them at once.
**Verification on pulse-dev**
- `internal/monitoring` runs in 112 s against 107 s on #2235's head. No
test is slower by more than 1.2 s.
- `internal/unifiedresources`, `internal/websocket` and
`internal/api/...` pass. `TestServerInfoEndpointReportsDevelopment`
fails in that source export with and without this change, because the
export has no git checkout.
Refs #2199
Agents poll /api/agents/agent/{id}/config every minute, and every
successful poll was written as a signed agent_config_fetch audit event.
On a five-agent homelab that was 483,000 of 483,101 audit rows (250 MB)
over 75 days, burying the 64 logins and 25 CSRF failures the log exists
to show; at the default 90-day retention it is roughly 60 MB per agent.
Every failed fetch is still audited. A successful fetch is audited when
it is new audit information: the agent's first delivery after Pulse
starts, a different token or desiredConfig hash, or an unchanged
delivery last recorded 24 hours earlier. Recorded successes now carry
the delivered config hash and the reason. The tracker is per handler,
keyed by org and agent, and bounded to 4,096 entries.
Existing rows are left to age out under retention rather than deleted,
since removing entries from a security log would look like tampering.
AUDIT_LOGGING.md and the API and agent-lifecycle contracts describe the
new rule.
Every accepted agent report refreshed the canonical resource store, and
every refresh covers the whole estate, so refresh work grew with agent
count times estate size (#2199). The previous commit made each pass
linear and cheaper; this bounds how many passes reports can trigger.
Accepted host, Docker and Kubernetes reports, including rejected-report
liveness, now refresh through a throttle. The first report after a
quiet window is published before its handler returns. Reports within 2
seconds of the previous refresh fold into one trailing refresh at the
window's end, which reads the latest state and broadcasts it. Two
seconds matches readPathRegistryFreshness, the staleness read paths
already accept. Agent removal and host-agent evaluation still refresh
immediately. Monitor.Stop cancels a pending trailing refresh and waits
for a running one before closing stores. Monitors built without New
keep a zero window and refresh on every report.
At 2,080 resources a refresh costs about 82 ms, so 200 agents reporting
every 30 seconds went from roughly 55% of a core in report-driven
refreshes to at most about 4%, however many agents report.
This relaxes the agent-lifecycle guarantee that a report is visible
when its handler returns: it is now visible within the window. The
founder approved that trade on 24 September 2026 and the contract
records it. The issue-1753 end-to-end test posts two reports back to
back, so it now waits for the published rows instead of reading them
at once.
Refs #2199
Every provider MSP client workspace runs in hosted mode, where users sign
in through the control plane handoff. The install endpoints decided
whether to mint a token from the workspace's own config, which on a fresh
client has no local credential, API token, proxy secret or SSO provider.
So Settings > Infrastructure > Generate token answered 200 with an empty
token and the Proxmox flow built a command without one: a provider could
not install a single agent from the client workspace. Found on a fresh
v6.4.1 provider bundle.
The router's configTransferAuthenticationConfigured already counts hosted
mode as enforced authentication; the agent-lifecycle copy of the rule
never gained that term. Add it. Self-hosted runtimes with no auth keep
the token-less command.
A provider opening a client workspace from the Pulse Account portal
arrives in that client's organization as its owner. ensureAdminSession
admits an org manager to the org-bound settings routes, but the security
status snapshot reported every settings capability as false for any
org-scoped session. Since the Settings navigation started hiding gated
panels in August, the owner saw only General: no Infrastructure page to
connect the client's Proxmox or install agents, and no Reporting page
for the client reports. The portal's handoff to
/settings/infrastructure fell through to General. Found on a fresh
v6.4.1 provider bundle, where /api/config/nodes answered 200 for the
same session that /api/security/status said could reach nothing.
Route and snapshot now share one settings-admin rule,
sessionUserHasSettingsAdminPrivileges. The snapshot keeps it out of
sessionIsAdmin: it widens only infrastructureRead, availabilityRead and
reportingRead, whose handlers read the organization's own config and
monitor state. Instance administration, detailLevel, authUsername and
billingAdmin stay withheld from org-scoped sessions, as the scoped
local session contract requires.
Cover real installer and local HTTP handlers with an explicit uname shim. Distinguish bundled artifacts from missing binaries or signature sidecars without claiming native pfSense installation acceptance.
Change-source: pulse-maintainer
Record the #2140 node TLS-preference fix in the contracts its runtime commits touch: agent-lifecycle, api-contracts, security-privacy and the dependent storage-recovery contract, with the node API payload and persisted-field proofs in internal/api/contract_test.go and internal/config/config_load_test.go. The canonical completion history registers these contract and proof files as the completion of the auto-register and consolidation runtime commits.
Change-source: pulse-maintainer
ConsolidatePVEInstances promoted VerifySSL false->true whenever a merged duplicate or standalone had it enabled, silently re-enabling a disabled Verify SSL certificate setting on every save, load and monitor reconciliation (#2140). The add and update handlers now record an explicit operator choice in VerifySSLExplicit; a merge never overrides it, while the historical promotion is kept when neither side recorded a choice so an existing secure connection is not downgraded. A merged fingerprint still pins the peer.
Regression tests cover the standalone and duplicate-cluster merges and the reported PUT-save path through normalizePVEConfigState.
Change-source: pulse-maintainer
ConsolidatePVEInstances promoted VerifySSL false->true when folding a duplicate cluster or overlapping standalone into the canonical instance, silently re-enabling a disabled Verify SSL certificate setting on every save, load and monitor reconciliation (#2140). The canonical instance now owns the preference; a merged fingerprint still pins the peer, so verification is not downgraded.
Regression tests cover the standalone and duplicate-cluster merges and the reported PUT-save path through normalizePVEConfigState.
Change-source: pulse-maintainer
Re-registration of an existing node overwrote the stored VerifySSL value with the fingerprint-capture result, silently re-enabling a disabled Verify SSL certificate setting on every agent health-check repair (#2140). Preserve the operator's choice and keep only the legacy heal where strict verification is on with no pin (#1303).
Change-source: pulse-maintainer
The shared release preflight worker exports GITHUB_ACTIONS=true so isolated single-repository checkouts take their private-sibling skip. That flag also made the load/SLO latency guards hard-fail on a contended shared host, where the tests' own documented local-contention skip is the intended behaviour. Distinguish a real GitHub Actions run by GITHUB_RUN_ID, which only a hosted runner sets, so the worker keeps the cross-repo skip while latency overruns skip instead of failing. Hosted CI enforcement is unchanged.
Change-source: pulse-maintainer
Initialize the persistent CSRF store for middleware tests that construct session-cookie requests, and restore the package test store afterward. This keeps exact release preflight runs from panicking before they can report qualification results.