From 7da385cf28e019f53a7299c37f72df3d307adf18 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 21 Aug 2026 00:00:37 +0100 Subject: [PATCH] Prepare v6.3.0-rc.4 release --- VERSION | 2 +- deploy/helm/pulse/Chart.yaml | 8 +-- deploy/helm/pulse/README.md | 2 +- docker-compose.yml | 2 +- docs/RELEASE_NOTES.md | 8 +-- docs/UPGRADE_v6.md | 4 +- .../v6/internal/subsystems/alerts.md | 9 +++ .../v6/internal/subsystems/api-contracts.md | 5 +- .../subsystems/deployment-installability.md | 17 +++--- .../subsystems/frontend-primitives.md | 7 +++ .../v6/internal/subsystems/notifications.md | 4 ++ ...0-rc.3.md => RELEASE_NOTES_v6.3.0-rc.4.md} | 40 +++++++++--- ....0-rc.3.md => V6_CHANGELOG_v6.3.0-rc.4.md} | 25 ++++++-- frontend-modern/browser-verification.json | 31 +++++----- frontend-modern/public/docs/AGENT_SECURITY.md | 61 ++++++++++++++++--- .../public/docs/PRODUCTION_SECURITY.md | 11 ++-- frontend-modern/public/docs/UPGRADE_v6.md | 4 +- .../src/api/__tests__/notifications.test.ts | 11 ++++ frontend-modern/src/api/notifications.ts | 2 +- .../__tests__/workloadSelectors.test.ts | 4 +- internal/api/contract_test.go | 46 ++++++++------ .../runtime_surface_audit_test.go | 1 + scripts/install-docker.sh | 2 +- .../agent_state_dir_lifecycle_test.go | 4 ++ .../installtests/build_release_assets_test.go | 7 +++ .../installtests/install_docker_sh_test.go | 1 + 26 files changed, 227 insertions(+), 91 deletions(-) rename docs/releases/{RELEASE_NOTES_v6.3.0-rc.3.md => RELEASE_NOTES_v6.3.0-rc.4.md} (76%) rename docs/releases/{V6_CHANGELOG_v6.3.0-rc.3.md => V6_CHANGELOG_v6.3.0-rc.4.md} (83%) diff --git a/VERSION b/VERSION index 990b5af4e..1f9f413fd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.3.0-rc.3 +6.3.0-rc.4 diff --git a/deploy/helm/pulse/Chart.yaml b/deploy/helm/pulse/Chart.yaml index a777b5537..55f274419 100644 --- a/deploy/helm/pulse/Chart.yaml +++ b/deploy/helm/pulse/Chart.yaml @@ -2,9 +2,9 @@ apiVersion: v2 name: pulse description: Helm chart for deploying the Pulse hub and optional Docker, Kubernetes, or OpenShift monitoring agent. type: application -version: 6.3.0-rc.3 -appVersion: "6.3.0-rc.3" -icon: https://raw.githubusercontent.com/rcourtman/Pulse/v6.3.0-rc.3/docs/images/pulse-logo.svg +version: 6.3.0-rc.4 +appVersion: "6.3.0-rc.4" +icon: https://raw.githubusercontent.com/rcourtman/Pulse/v6.3.0-rc.4/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.3.0-rc.3/docs/KUBERNETES.md + url: https://github.com/rcourtman/Pulse/blob/v6.3.0-rc.4/docs/KUBERNETES.md - name: Support url: https://github.com/rcourtman/Pulse/discussions artifacthub.io/maintainers: | diff --git a/deploy/helm/pulse/README.md b/deploy/helm/pulse/README.md index 074da3c6c..b2480dd4f 100644 --- a/deploy/helm/pulse/README.md +++ b/deploy/helm/pulse/README.md @@ -1,6 +1,6 @@ # pulse -![Version: 6.3.0-rc.3](https://img.shields.io/badge/Version-6.3.0--rc.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 6.3.0-rc.3](https://img.shields.io/badge/AppVersion-6.3.0--rc.3-informational?style=flat-square) +![Version: 6.3.0-rc.4](https://img.shields.io/badge/Version-6.3.0--rc.4-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 6.3.0-rc.4](https://img.shields.io/badge/AppVersion-6.3.0--rc.4-informational?style=flat-square) Helm chart for deploying the Pulse hub and optional Docker, Kubernetes, or OpenShift monitoring agent. diff --git a/docker-compose.yml b/docker-compose.yml index e08551803..057b45fa6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: pulse: - image: ${PULSE_IMAGE:-rcourtman/pulse:6.3.0-rc.3} + image: ${PULSE_IMAGE:-rcourtman/pulse:6.3.0-rc.4} container_name: pulse restart: unless-stopped logging: diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index ecd3995de..44abc4597 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -6,10 +6,10 @@ Pulse release notes live on GitHub: For historical v4 notes that previously lived in this repo, see: `docs/releases/RELEASE_NOTES_v4.md` -For the current v6 release candidate packet (the current v6 support release -candidate packet), see: -- `docs/releases/RELEASE_NOTES_v6.3.0-rc.3.md` -- `docs/releases/V6_CHANGELOG_v6.3.0-rc.3.md` +For the current v6 release candidate packet +(the current v6 support release candidate packet), see: +- `docs/releases/RELEASE_NOTES_v6.3.0-rc.4.md` +- `docs/releases/V6_CHANGELOG_v6.3.0-rc.4.md` For the current stable v6 packet, see: - `docs/releases/RELEASE_NOTES_v6.2.1.md` diff --git a/docs/UPGRADE_v6.md b/docs/UPGRADE_v6.md index adf909aa4..ede524558 100644 --- a/docs/UPGRADE_v6.md +++ b/docs/UPGRADE_v6.md @@ -4,8 +4,8 @@ This guide covers practical upgrade steps for existing Pulse installs moving to For the current v6 release candidate packet, see: -- `docs/releases/RELEASE_NOTES_v6.3.0-rc.3.md` -- `docs/releases/V6_CHANGELOG_v6.3.0-rc.3.md` +- `docs/releases/RELEASE_NOTES_v6.3.0-rc.4.md` +- `docs/releases/V6_CHANGELOG_v6.3.0-rc.4.md` For the current stable v6 packet, see: diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index f382b1968..51a834b27 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -437,6 +437,15 @@ removed because a saved view was only ever the page's URL query string, which the browser's own bookmarks already capture and share. History narrowing stays URL-owned so a filtered history page remains a shareable, bookmarkable link. +The alert history severity facet derives each option count through +`useAlertHistoryState.countForSeverity`, using the same +`filterAlertHistoryItems` predicate that supplies the rendered list for the +current fetched period and search term. It must not count an unfiltered or +separately reduced collection that can disagree with the selected chip's +result. These counts follow the shared Inventory totals visibility preference; +the Period facet remains uncounted because it selects the fetched time scope +rather than filtering the already-fetched rows. + Alert history row timestamps render clock time in the viewer's own locale and must carry the absolute date and time as a title. The date otherwise lives only in the day group header, which scrolls out of sight, and a hardcoded diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index d749575c6..7dbf1fa69 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -4331,7 +4331,10 @@ first — `entries` carrying `notificationId`, `type`, `destinationId`, counts are retention-bounded and must not read as lifetime history; webhook secrets are redacted from error text before the payload leaves the API. The frontend transport in `frontend-modern/src/api/notifications.ts` validates -entry shape and drops malformed rows rather than rendering them. Successful +entry shape and drops malformed rows rather than rendering them. It must route +`entries` through the shared `arrayOrEmpty` normalization helper instead of +creating a notifications-local array fallback that can drift from other API +clients. Successful test-send responses from `POST /api/notifications/test` and `POST /api/notifications/webhooks/test` must include `deliveryPaused: true` whenever real alert delivery is gated off, so a passing test cannot be diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index ac792be7a..3bd18b73a 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -1461,10 +1461,10 @@ diagnostics. The same release workflow also executes the generated self-signed and custom-CA Windows installer commands through Windows PowerShell 5.1 before release assembly, so the first HTTPS fetch is release proof rather than a string-shape assertion. -The active prerelease `v6.3.0-rc.3` cut sets the repo-root `VERSION`, +The active prerelease `v6.3.0-rc.4` 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.3.0-rc.3` release -version. It follows stable `v6.2.1`, opens the published 6.3 candidate line, uses +fallback, and Helm chart release metadata to the same `6.3.0-rc.4` release +version. It follows `v6.3.0-rc.3` on the published 6.3 candidate line, uses `rollback_version=v6.2.1`, and keeps stable/latest pointers on `v6.2.1`. This candidate establishes the new minor-release packet with durable scoped Patrol objectives, validated read-only observers, verified work receipts, agent action @@ -1473,8 +1473,11 @@ monitoring correctness fixes. The advanced branch also carries the decision-firs Patrol inbox, first-class Actions workspace, canonical platform-admission projection, and bounded concurrent unified-resource hydration. Subscription-backed Patrol turns bound command cleanup after an idle deadline so descendant-held -output pipes cannot extend the caller-owned stall budget. The changes since -`v6.3.0-rc.2` do not require a Pulse Mobile client change and preserve the +output pipes cannot extend the caller-owned stall budget. This candidate also +adds canonical estate summaries and search, a real-delivery activity log, +least-privilege agent installation, and bounded Docker-in-LXC discovery while +preserving unreadable settings and AI state. The changes since +`v6.3.0-rc.3` do not require a Pulse Mobile client change and preserve the existing mobile, Relay, onboarding, and mobile-facing API contracts, so the server cut is classified `no-mobile-impact`; no companion upload or public mobile-store rollout is part of this candidate. @@ -1760,8 +1763,8 @@ 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 prerelease `v6.3.0-rc.3` cut, the repo-root compose default and -`scripts/install-docker.sh` fallback must both pin `6.3.0-rc.3` +For the active prerelease `v6.3.0-rc.4` cut, the repo-root compose default and +`scripts/install-docker.sh` fallback must both pin `6.3.0-rc.4` until the next governed stable cut moves them forward. Each new release moves these two pins together with the repo-root `VERSION` and the Helm chart metadata in the same commit; a cut that leaves any of the four on a superseded value is a diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index f277e8101..2ef639435 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -4301,6 +4301,13 @@ search-aware active-state accessor to `FilterBar`; the shared shell owns where the contextual Clear filters action appears, while the alerts feature must not fall back to sequential per-control URL writes or hide that action for a search-only result set. +Alert History severity option counts also follow the shared estate-orientation +contract: the alerts feature supplies counts from the exact predicate used by +its list, while `FilterBar` renders the values and the shared Inventory totals +visibility preference decides whether they are shown. A feature must not build +counts from page-wide alert totals after search or another active facet has +narrowed the rendered rows, and a time-scope option must not claim a count from +an unfetched period. Because that popover combines view application, default selection, removal, and an inline naming form, it is a labelled non-modal dialog rather than an ARIA menu. Its trigger exposes the dialog relationship, Escape returns focus diff --git a/docs/release-control/v6/internal/subsystems/notifications.md b/docs/release-control/v6/internal/subsystems/notifications.md index e3763edbd..cb7a8ddd1 100644 --- a/docs/release-control/v6/internal/subsystems/notifications.md +++ b/docs/release-control/v6/internal/subsystems/notifications.md @@ -344,6 +344,10 @@ unreadable queue is an error, never an empty log. This per-attempt surface is deliberately distinct from the content-free telemetry aggregate above, which remains identity-free. Test sends bypass the queue and must not appear in the delivery log, and the destinations UI says so where the log renders. +The frontend client normalizes the `entries` collection through the shared API +collection helper before validating each record, so a malformed collection or +row cannot bypass the canonical client boundary or create a module-local +fallback shape. Because test sends also bypass the alert activation gate, a bare success result is exactly how installs come to believe delivery works while every diff --git a/docs/releases/RELEASE_NOTES_v6.3.0-rc.3.md b/docs/releases/RELEASE_NOTES_v6.3.0-rc.4.md similarity index 76% rename from docs/releases/RELEASE_NOTES_v6.3.0-rc.3.md rename to docs/releases/RELEASE_NOTES_v6.3.0-rc.4.md index c3678258d..d12cc864e 100644 --- a/docs/releases/RELEASE_NOTES_v6.3.0-rc.3.md +++ b/docs/releases/RELEASE_NOTES_v6.3.0-rc.4.md @@ -1,15 +1,15 @@ -# Pulse v6.3.0-rc.3 Release Notes +# Pulse v6.3.0-rc.4 Release Notes -`v6.3.0-rc.3` is a release candidate for the next Pulse v6 minor release, -following `v6.3.0-rc.2` in the 6.3 line and stable `v6.2.1`. It carries the +`v6.3.0-rc.4` is a release candidate for the next Pulse v6 minor release, +following `v6.3.0-rc.3` in the 6.3 line and stable `v6.2.1`. It carries the Patrol v2 operating model, a first-class Actions workspace, guarded agent action preflight, large-estate response improvements, and monitoring correctness fixes. -This candidate adds a group of alert-delivery repairs. Production telemetry -showed installs with a configured notification destination that had never -delivered anything and had no way to find out, because the channel that would -carry such a warning is the one that had failed. +This candidate adds estate-first platform summaries and search, an operator- +visible notification delivery log, a supported least-privilege agent profile, +and safer Docker-in-LXC discovery. It also prevents failed settings or AI-state +reads from silently overwriting preserved data. ## Highlights @@ -19,6 +19,14 @@ carry such a warning is the one that had failed. without granting mutation authority. - Approved actions gain agent preflight and stable refusal telemetry; large installations gain compressed APIs and indexed lookups. +- Platform pages now lead with estate totals, status facets, and search that + share the same predicates as their underlying tables. +- Notification settings show the outcome of real delivery attempts instead of + relying on test sends as a proxy for live delivery health. +- Docker-in-LXC discovery is explicitly controlled and backs off against slow + or failing Proxmox hosts instead of creating a probe storm. +- Unified Agent installs can opt into a supported least-privilege profile with + narrowly scoped elevation for the capabilities that require it. ## Added @@ -35,6 +43,12 @@ carry such a warning is the one that had failed. - Unified Agent preflight contracts for package updates, package-cache cleanup, and Docker lifecycle or update operations. - Production security deployment guidance and a focused security-review packet. +- Estate overviews and infrastructure search across Proxmox, Docker, + Kubernetes, TrueNAS, VMware, and standalone agent surfaces. +- A seven-day notification delivery activity log with destination and outcome + details for queued live alerts. +- An administrator setting for Docker-in-LXC discovery and a supported + least-privilege Unified Agent installation profile. ## Improved @@ -65,6 +79,10 @@ carry such a warning is the one that had failed. limits, readiness checks, deadlines, and continuation latency more reliably. - Subscription-backed turns now complete their idle timeout promptly even when a canceled CLI descendant still holds an inherited output pipe open. +- Platform and alert-history facet counts are derived from each table's own + filter predicate, keeping summary totals aligned with the visible rows. +- Failed settings and AI-state reads preserve the last known data and surface + the failure instead of treating unreadable storage as an empty value. ## Fixed @@ -85,6 +103,10 @@ carry such a warning is the one that had failed. - vSphere backup status, agent thermal history, explicit cluster-member address overrides, and discovery-analysis request timeouts now reflect their actual runtime state. +- Docker-in-LXC discovery no longer retries overlapping probes against slow + Proxmox hosts, and command enablement after install is reflected without a + reinstall. +- In-progress Proxmox backups are no longer presented as completed backups. - The notifications surface now states when alert delivery is paused, and that a passing test send does not prove live alerts are getting through. Test sends bypass the delivery pause, so a configured destination could look @@ -110,7 +132,7 @@ carry such a warning is the one that had failed. ## Upgrade Notes -Use the normal v6 install or update flow for `v6.3.0-rc.3` only when you are +Use the normal v6 install or update flow for `v6.3.0-rc.4` only when you are comfortable testing a release candidate. The rollback target is `v6.2.1`. The exact rollback reinstall command is: @@ -119,7 +141,7 @@ The exact rollback reinstall command is: ./scripts/install.sh --version v6.2.1 ``` -The changes since `v6.3.0-rc.2` do not require a Pulse Mobile client change +The changes since `v6.3.0-rc.3` do not require a Pulse Mobile client change and preserve the existing mobile, Relay, onboarding, and mobile-facing API contracts. No companion mobile build upload or public mobile-store rollout is part of this candidate. diff --git a/docs/releases/V6_CHANGELOG_v6.3.0-rc.3.md b/docs/releases/V6_CHANGELOG_v6.3.0-rc.4.md similarity index 83% rename from docs/releases/V6_CHANGELOG_v6.3.0-rc.3.md rename to docs/releases/V6_CHANGELOG_v6.3.0-rc.4.md index be7f9abf6..f3359fd16 100644 --- a/docs/releases/V6_CHANGELOG_v6.3.0-rc.3.md +++ b/docs/releases/V6_CHANGELOG_v6.3.0-rc.4.md @@ -1,7 +1,7 @@ -# Pulse v6.3.0-rc.3 +# Pulse v6.3.0-rc.4 -_This changelog describes the changes since `v6.3.0-rc.2` and carries forward -the complete cumulative 6.3 packet since stable `v6.2.1`. `v6.3.0-rc.3` is a +_This changelog describes the changes since `v6.3.0-rc.3` and carries forward +the complete cumulative 6.3 packet since stable `v6.2.1`. `v6.3.0-rc.4` is a prerelease and rolls back to stable `v6.2.1`._ ## Added @@ -19,6 +19,11 @@ prerelease and rolls back to stable `v6.2.1`._ - System-scoped alerts, so Pulse can report a fault in itself rather than only in a monitored resource. Broken notification delivery is the first, raised as an ordinary alert so it reaches the alert list and navigation badge. +- Estate summaries, status facets, and canonical search on the primary + infrastructure platform pages. +- A seven-day delivery log for real alert notification attempts. +- A supported least-privilege Unified Agent installation profile and an + administrator-controlled Docker-in-LXC discovery mode. ## Changed @@ -39,6 +44,10 @@ prerelease and rolls back to stable `v6.2.1`._ source-tree value. - Buffered subscription CLI turns cannot extend a caller-owned idle deadline while a canceled descendant keeps an inherited output pipe open. +- Platform and alert-history counts now use the same canonical filter + predicates as their visible result tables. +- Failed settings and AI-state reads retain preserved data and expose the read + failure instead of silently replacing state with defaults. ## Fixed @@ -67,6 +76,10 @@ prerelease and rolls back to stable `v6.2.1`._ drained and a resource oscillating just under the threshold was never damped. - Mobile tables keep narrow values readable, preserve compact replication values, and use a consistent density across platform surfaces. +- Docker-in-LXC discovery serializes and backs off slow probes, preserves + post-install command enablement, and surfaces failures without host churn. +- In-progress Proxmox backups remain in progress rather than being shown as + completed. ## Security @@ -79,8 +92,8 @@ prerelease and rolls back to stable `v6.2.1`._ ## Release Metadata -- Version: `v6.3.0-rc.3` -- Previous release: `v6.2.1` +- Version: `v6.3.0-rc.4` +- Previous release: `v6.3.0-rc.3` - Previous stable: `v6.2.1` - Rollback target: `v6.2.1` - Rollback command: `./scripts/install.sh --version v6.2.1` @@ -89,6 +102,6 @@ prerelease and rolls back to stable `v6.2.1`._ - Windows signing decision: the standing prerelease path publishes exact-SHA, checksum, and detached-signature verified Windows agents without Authenticode; stable `v6.3.0` restores mandatory SignPath signing -- Mobile decision: `no-mobile-impact`; changes since `v6.3.0-rc.2` preserve the +- Mobile decision: `no-mobile-impact`; changes since `v6.3.0-rc.3` preserve the existing mobile, Relay, onboarding, and mobile-facing API contracts, and no companion upload or public store rollout is required diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index df748f40b..e6a1bb44f 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,36 +1,35 @@ { "version": 1, - "base_sha": "ee4cd560b6352fac50b988104e3ee370db8669c1", - "verified_at": "2026-08-20T21:08:35Z", + "base_sha": "4c7b1a2434385f13c5c89dd3a113cd894636ac4d", + "verified_at": "2026-08-20T22:47:56Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx", - "frontend-modern/src/features/alerts/useAlertHistoryState.ts" + "frontend-modern/src/api/notifications.ts" ], "content_sha256": { - "frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx": "07836c5b85ea484cfe984b2d0de5c302202ef6b844eafdc776b3f979f0938acb", - "frontend-modern/src/features/alerts/useAlertHistoryState.ts": "7528bd735ff84961e8bd880d89f0253255c698ab5910c1eb2fb10cf0255e39e8" + "frontend-modern/src/api/notifications.ts": "ac714c8f87c1e26f1abf523b4c4c7b83c0ce75250c92741b69dc6a36b35557ff" }, "routes": [ - "/alerts/history" + "/alerts/notifications" ], "viewports": [ { - "width": 1280, - "height": 720 + "width": 1440, + "height": 1000 }, { - "width": 375, - "height": 812 + "width": 390, + "height": 844 } ], "states": [ - "Alert history severity facet now counted from the live history data: All 47 / Critical 8 / Warning 13, with Period chips left uncounted as time scopes", - "Critical selected: the page's own summary reads '8 alerts', matching the chip count exactly (counts share filterAlertHistoryItems with the list)", - "Chips verified through the mobile Filters sheet (the hidden preview pane renders the responsive mobile layout); the same options render inline at desktop width" + "Notifications page loaded with delivery paused and all destination configuration sections available", + "Recent delivery activity rendered its seven-day empty state with the refresh control enabled", + "Desktop and narrow layouts kept the delivery card, explanatory copy, refresh control, and empty state visible without clipping" ], "interactions": [ - "Opened the Filters sheet on /alerts/history and read the counted severity chips", - "Clicked the Critical chip, confirmed the '8 alerts' summary, then reset the facet back to All" + "Refreshed delivery status at desktop width and confirmed the empty state remained available", + "Scrolled through the narrow notification layout and refreshed delivery status again", + "Checked the browser console after both refresh interactions and found no errors" ] } diff --git a/frontend-modern/public/docs/AGENT_SECURITY.md b/frontend-modern/public/docs/AGENT_SECURITY.md index ef0137f4b..593ee829a 100644 --- a/frontend-modern/public/docs/AGENT_SECURITY.md +++ b/frontend-modern/public/docs/AGENT_SECURITY.md @@ -2,15 +2,26 @@ Pulse agents incorporate several security mechanisms to ensure that the code running on your infrastructure is authentic and untampered with. +**Start with the least privilege that answers your monitoring question.** For +Proxmox VE, PBS, and PMG, that is usually no agent at all: API-only monitoring +with a read-only token covers inventory, status, and metrics, and the +generated setup script creates a privilege-separated monitoring user for it +(see [Proxmox Deployment Choices](#proxmox-deployment-choices)). Install a +host agent only where you want data the platform API cannot provide, and on +Linux consider the supported +[least-privilege profile](#least-privilege-agent-profile) before the root +default. + ## Agent Privilege Model Pulse's Linux/systemd installer runs the unified agent as `root` by default. That is intentional for full host telemetry: disk SMART data, mdadm/RAID state, temperature sensors, Docker or Podman socket reads, Proxmox host-local details that are not available through the API, and some NAS/platform integrations -commonly require root or equivalent local privileges. Running the service as a -lower-privilege user may work for a narrow subset of metrics, but it is not a -supported full-telemetry profile today. +commonly require root or equivalent local privileges. On Linux/systemd hosts, +the supported alternative is the least-privilege profile documented below; it +trades the root-only collectors it has not been granted for a dedicated +non-root service user. Treat a host agent like other infrastructure monitoring software with local root read access: @@ -123,12 +134,44 @@ mirrors the generated read/monitoring ACLs onto both the service user and the token. For PBS, the generated script grants the `Audit` ACL to both the service user and token. -Running `pulse-agent` as a custom non-root systemd user is possible by editing -the service unit, but it is not a supported full-telemetry mode today. Expect -gaps in SMART, temperature, Docker socket, ZFS/Ceph/mdadm, mount, and platform -integration data unless you deliberately grant equivalent capabilities or group -access. If you choose that route, treat it as a local hardening profile and -verify the exact metrics you care about after the change. +## Least-Privilege Agent Profile + +On standard Linux systemd hosts, `install.sh --least-privilege` is a supported +alternative to the root profile. It runs the service as a dedicated +`pulse-agent` system user (nologin shell, owning only its state directory and +binary), joins the `docker` group when Docker monitoring is enabled so socket +reads keep working, and keeps every hardening directive of the root unit while +dropping the LXC-attach ambient capability grant entirely. + +Two optional flags restore the collectors that genuinely need elevation, each +through an exact-command sudoers grant validated with `visudo` and a +root-owned wrapper the agent is pointed at via an absolute-path-only +environment override. Because `NoNewPrivileges` blocks `sudo` entirely, a +unit with an active grant sets `NoNewPrivileges=false` while keeping the +remaining hardening; a grantless least-privilege install keeps +`NoNewPrivileges=true`. Choose grants deliberately: each one is a scoped, +auditable widening of the profile. + +- `--grant-smart` allows exactly `smartctl`, restoring SMART disk health. +- `--grant-pct` allows exactly `pct list` and `pct df`, restoring Proxmox LXC + filesystem capacity. The grant deliberately excludes `pct exec`, `start`, + `stop`, and `enter`, so guest Docker inventory stays a root-profile feature. + +What the profile gives up: command execution (`--enable-commands` is refused +and a later server-side enable requires reinstalling the root profile), +`pct exec` guest Docker inventory, and any platform integration that needs +device or socket access you have not granted. Core metrics, mounts, `/proc` +RAID state, hwmon temperatures, and Docker socket reads work without root. +Ungranted collectors fail soft, and the agent reports its privilege profile so +**Settings → Infrastructure → Agent Doctor** shows the service user and active +helpers instead of presenting missing collectors as a fault. Appliance +platforms (TrueNAS, Synology, QNAP, Unraid) and non-systemd init systems keep +the root profile; the installer refuses `--least-privilege` there rather than +silently falling back to root. + +`--update` preserves an existing least-privilege profile and its grants +without the flags being repeated. Uninstall removes the sudoers file and +helpers; the inert system user is left behind deliberately. ## Supply-Chain Boundary diff --git a/frontend-modern/public/docs/PRODUCTION_SECURITY.md b/frontend-modern/public/docs/PRODUCTION_SECURITY.md index 8520974de..939b9fe98 100644 --- a/frontend-modern/public/docs/PRODUCTION_SECURITY.md +++ b/frontend-modern/public/docs/PRODUCTION_SECURITY.md @@ -48,10 +48,13 @@ The default posture limits that boundary: - Proxmox guest Docker inventory through `pct exec` is disabled by default and requires an explicit server setting. -A custom non-root systemd user is possible, but it is not currently a supported -full-telemetry profile. Expect gaps unless you deliberately grant equivalent -device, filesystem, or socket access. If API data is sufficient, API-only -monitoring is the cleaner least-privilege choice. +On standard Linux systemd hosts the installer also offers a supported +least-privilege profile: `--least-privilege` runs the service as a dedicated +`pulse-agent` system user, with optional `--grant-smart` and `--grant-pct` +flags that restore SMART and Proxmox LXC filesystem collection through +exact-command sudoers grants. Command execution and `pct exec` guest inventory +stay root-profile features. If API data is sufficient, API-only monitoring +remains the cleanest least-privilege choice of all — it needs no agent. See [Agent Security](AGENT_SECURITY.md) for the precise command, guest-access, update, and service-hardening boundaries. diff --git a/frontend-modern/public/docs/UPGRADE_v6.md b/frontend-modern/public/docs/UPGRADE_v6.md index adf909aa4..ede524558 100644 --- a/frontend-modern/public/docs/UPGRADE_v6.md +++ b/frontend-modern/public/docs/UPGRADE_v6.md @@ -4,8 +4,8 @@ This guide covers practical upgrade steps for existing Pulse installs moving to For the current v6 release candidate packet, see: -- `docs/releases/RELEASE_NOTES_v6.3.0-rc.3.md` -- `docs/releases/V6_CHANGELOG_v6.3.0-rc.3.md` +- `docs/releases/RELEASE_NOTES_v6.3.0-rc.4.md` +- `docs/releases/V6_CHANGELOG_v6.3.0-rc.4.md` For the current stable v6 packet, see: diff --git a/frontend-modern/src/api/__tests__/notifications.test.ts b/frontend-modern/src/api/__tests__/notifications.test.ts index e669be2da..4f45cbdc6 100644 --- a/frontend-modern/src/api/__tests__/notifications.test.ts +++ b/frontend-modern/src/api/__tests__/notifications.test.ts @@ -362,6 +362,17 @@ describe('NotificationsAPI', () => { expect(log.windowDays).toBe(7); }); + it('normalizes a malformed delivery-log collection through the shared API boundary', async () => { + apiFetchJSONMock.mockResolvedValueOnce({ + entries: { notificationId: 'not-a-collection' }, + window_days: 7, + } as any); + + const log = await NotificationsAPI.getDeliveryLog(); + + expect(log).toEqual({ entries: [], windowDays: 7 }); + }); + it('passes the deliveryPaused flag through from test-send responses', async () => { apiFetchJSONMock.mockResolvedValueOnce({ status: 'success', diff --git a/frontend-modern/src/api/notifications.ts b/frontend-modern/src/api/notifications.ts index 056959209..2a8b168b2 100644 --- a/frontend-modern/src/api/notifications.ts +++ b/frontend-modern/src/api/notifications.ts @@ -339,7 +339,7 @@ export class NotificationsAPI { const payload = await apiFetchJSON>( `${this.baseUrl}/delivery-log${query}`, ); - const rawEntries = Array.isArray(payload.entries) ? payload.entries : []; + const rawEntries = arrayOrEmpty(payload.entries); const entries: NotificationDeliveryLogEntry[] = []; for (const rawEntry of rawEntries) { const entry = normalizeDeliveryLogEntry(rawEntry); diff --git a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts index 876ab5fac..38715052b 100644 --- a/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts +++ b/frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts @@ -713,9 +713,7 @@ describe('workloadSelectors', () => { it('returns the input list when no scope or exclusions apply', () => { const guests = [makeGuest(1), makeGuest(2)]; - expect( - selectVisibleWorkloadInventory({ guests, excludedTypes: noExclusions }), - ).toBe(guests); + expect(selectVisibleWorkloadInventory({ guests, excludedTypes: noExclusions })).toBe(guests); expect( selectVisibleWorkloadInventory({ guests, diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index cd53933bb..76a1e0999 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -20684,13 +20684,8 @@ func TestContract_AgentFleetDiagnosticsEndpointSurfacesStableShape(t *testing.T) if err != nil { t.Fatalf("read router_routes_registration.go: %v", err) } - monitoringSource, err := os.ReadFile("../monitoring/agent_fleet_doctor.go") - if err != nil { - t.Fatalf("read monitoring agent_fleet_doctor.go: %v", err) - } handlerSrc := string(handler) routerSrc := string(router) - monitoringSrc := string(monitoringSource) if !strings.Contains(routerSrc, `"/api/agents/diagnostics"`) || !strings.Contains(routerSrc, `RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, r.handleAgentFleetDiagnostics))`) { @@ -20699,21 +20694,34 @@ func TestContract_AgentFleetDiagnosticsEndpointSurfacesStableShape(t *testing.T) if !strings.Contains(handlerSrc, "GetAgentFleetDiagnosticsForTarget(serverVersion, agentUpdateTargetVersion, time.Now().UTC())") { t.Error("agent fleet diagnostics handler must delegate to the monitoring-owned read-only producer") } - for _, required := range []string{ - "SchemaVersion int `json:\"schemaVersion\"`", - "GeneratedAt int64 `json:\"generatedAt\"`", - "ServerVersion string `json:\"serverVersion,omitempty\"`", - "AgentUpdateTargetVersion string `json:\"agentUpdateTargetVersion,omitempty\"`", - "Summary AgentFleetDiagnosticSummary `json:\"summary\"`", - "Agents []AgentFleetAgentDiagnostic `json:\"agents\"`", - "ConnectionID string `json:\"connectionId,omitempty\"`", - "AgentUpdate *AgentFleetDiagnosticUpdate `json:\"agentUpdate,omitempty\"`", - "AgentModules []AgentFleetDiagnosticModule `json:\"agentModules,omitempty\"`", - "Reasons []AgentFleetDiagnosticReason `json:\"reasons\"`", - "RepairActions []AgentFleetDiagnosticRepair `json:\"repairActions,omitempty\"`", + for _, required := range []struct { + typeOf reflect.Type + name string + goType reflect.Type + jsonTag string + }{ + {reflect.TypeOf(monitoring.AgentFleetDiagnostics{}), "SchemaVersion", reflect.TypeOf(int(0)), "schemaVersion"}, + {reflect.TypeOf(monitoring.AgentFleetDiagnostics{}), "GeneratedAt", reflect.TypeOf(int64(0)), "generatedAt"}, + {reflect.TypeOf(monitoring.AgentFleetDiagnostics{}), "ServerVersion", reflect.TypeOf(""), "serverVersion,omitempty"}, + {reflect.TypeOf(monitoring.AgentFleetDiagnostics{}), "AgentUpdateTargetVersion", reflect.TypeOf(""), "agentUpdateTargetVersion,omitempty"}, + {reflect.TypeOf(monitoring.AgentFleetDiagnostics{}), "Summary", reflect.TypeOf(monitoring.AgentFleetDiagnosticSummary{}), "summary"}, + {reflect.TypeOf(monitoring.AgentFleetDiagnostics{}), "Agents", reflect.TypeOf([]monitoring.AgentFleetAgentDiagnostic(nil)), "agents"}, + {reflect.TypeOf(monitoring.AgentFleetAgentDiagnostic{}), "ConnectionID", reflect.TypeOf(""), "connectionId,omitempty"}, + {reflect.TypeOf(monitoring.AgentFleetAgentDiagnostic{}), "AgentUpdate", reflect.TypeOf((*monitoring.AgentFleetDiagnosticUpdate)(nil)), "agentUpdate,omitempty"}, + {reflect.TypeOf(monitoring.AgentFleetAgentDiagnostic{}), "AgentModules", reflect.TypeOf([]monitoring.AgentFleetDiagnosticModule(nil)), "agentModules,omitempty"}, + {reflect.TypeOf(monitoring.AgentFleetAgentDiagnostic{}), "Reasons", reflect.TypeOf([]monitoring.AgentFleetDiagnosticReason(nil)), "reasons"}, + {reflect.TypeOf(monitoring.AgentFleetAgentDiagnostic{}), "RepairActions", reflect.TypeOf([]monitoring.AgentFleetDiagnosticRepair(nil)), "repairActions,omitempty"}, } { - if !strings.Contains(monitoringSrc, required) { - t.Errorf("agent fleet diagnostics payload missing stable field %q", required) + field, ok := required.typeOf.FieldByName(required.name) + if !ok { + t.Errorf("%s payload missing stable field %s", required.typeOf.Name(), required.name) + continue + } + if field.Type != required.goType { + t.Errorf("%s.%s type = %s, want %s", required.typeOf.Name(), required.name, field.Type, required.goType) + } + if got := field.Tag.Get("json"); got != required.jsonTag { + t.Errorf("%s.%s json tag = %q, want %q", required.typeOf.Name(), required.name, got, required.jsonTag) } } } diff --git a/internal/mutationregistry/runtime_surface_audit_test.go b/internal/mutationregistry/runtime_surface_audit_test.go index 36686520c..f36904aff 100644 --- a/internal/mutationregistry/runtime_surface_audit_test.go +++ b/internal/mutationregistry/runtime_surface_audit_test.go @@ -169,6 +169,7 @@ func TestTransportCommandCatalogsResolveToRegistry(t *testing.T) { "MsgTypeExecuteCmd": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.raw-command", DurableAuthorityID: "assistant.resource-action"}, "MsgTypeHostStorageCleanup": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.host-package-cache-cleanup", DurableAuthorityID: "resource.host.package-cache-cleanup"}, "MsgTypeReadFile": {Role: TransportRoleProtocol}, + "MsgTypeCancelCmd": {Role: TransportRoleProtocol}, "MsgTypeHostUpdate": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.host-package-update", DurableAuthorityID: "resource.host.package-update"}, "MsgTypeDockerContainerLifecycle": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.docker-container-lifecycle", DurableAuthorityID: "resource.docker.container-lifecycle"}, "MsgTypeDockerContainerUpdate": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.docker-container-update", DurableAuthorityID: "resource.docker.container-update"}, diff --git a/scripts/install-docker.sh b/scripts/install-docker.sh index 2d077aa2f..00b41a9f6 100755 --- a/scripts/install-docker.sh +++ b/scripts/install-docker.sh @@ -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.3.0-rc.3" +CANONICAL_DEFAULT_PULSE_VERSION="6.3.0-rc.4" resolve_default_pulse_version() { if [ -n "${PULSE_IMAGE_VERSION:-}" ]; then diff --git a/scripts/installtests/agent_state_dir_lifecycle_test.go b/scripts/installtests/agent_state_dir_lifecycle_test.go index 20da72153..d4cecc56d 100644 --- a/scripts/installtests/agent_state_dir_lifecycle_test.go +++ b/scripts/installtests/agent_state_dir_lifecycle_test.go @@ -205,6 +205,9 @@ func renderLifecycleService(t *testing.T, stateDir, stateSource, unitPath, pulse SERVER_FINGERPRINT="" OBSERVERS_FILE="" ENABLE_COMMANDS="` + commandFlag + `" + LEAST_PRIVILEGE="false" + GRANT_SMART="false" + GRANT_PCT="false" HEALTH_ADDR_SET="true" HEALTH_ADDR="" ENROLL="true" @@ -234,6 +237,7 @@ func renderLifecycleService(t *testing.T, stateDir, stateSource, unitPath, pulse ` + extractInstallShellFunction(t, "join_exec_arg_items") + ` ` + extractInstallShellFunction(t, "build_exec_args") + ` ` + extractInstallShellFunction(t, "systemd_agent_requires_lxc_attach") + ` +` + extractInstallShellFunction(t, "systemd_agent_may_attach_lxc") + ` ` + extractInstallShellFunction(t, "render_systemd_agent_unit") + ` ` + extractInstallShellFunction(t, "save_connection_info") + recovery + ` ensure_runtime_token_file "$STATE_DIR" diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index c536a7765..e18bff201 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -658,6 +658,10 @@ func TestCurrentPrereleasePacketTracksInstallMetadata(t *testing.T) { "Actions and Patrol identify whether a decision originated from a finding, alert, objective, or explicit operator request", "Action refusal telemetry now classifies target changes, prerequisites, contract failures", "Subscription-backed turns now complete their idle timeout promptly", + "Platform pages now lead with estate totals, status facets, and search", + "Notification settings show the outcome of real delivery attempts", + "Docker-in-LXC discovery is explicitly controlled and backs off against slow or failing Proxmox hosts", + "Unified Agent installs can opt into a supported least-privilege profile", "The rollback target is `v"+previous+"`", "The changes since `v"+comparisonVersion+"` do not require a Pulse Mobile client change", "preserve the existing mobile, Relay, onboarding, and mobile-facing API contracts", @@ -675,6 +679,9 @@ func TestCurrentPrereleasePacketTracksInstallMetadata(t *testing.T) { "A primary Actions workspace for approvals, governed plans, and action records", "Typed Unified Agent action preflight for supported host and Docker operations", "Stable pre-mutation refusal codes and fleet telemetry buckets", + "Estate summaries, status facets, and canonical search on the primary infrastructure platform pages", + "A seven-day delivery log for real alert notification attempts", + "A supported least-privilege Unified Agent installation profile", "Windows signing decision: the standing prerelease path publishes exact-SHA, checksum, and detached-signature verified Windows agents without Authenticode; stable `v"+stableTarget+"` restores mandatory SignPath signing", "Mobile decision: `no-mobile-impact`", "changes since `v"+comparisonVersion+"` preserve the existing mobile, Relay, onboarding, and mobile-facing API contracts", diff --git a/scripts/installtests/install_docker_sh_test.go b/scripts/installtests/install_docker_sh_test.go index 9cbdfb4ee..0f741c2be 100644 --- a/scripts/installtests/install_docker_sh_test.go +++ b/scripts/installtests/install_docker_sh_test.go @@ -358,6 +358,7 @@ func TestInstallDockerProofTracksPrereleaseContract(t *testing.T) { assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"), "The active 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.", + "It follows `v"+comparisonVersion+"` on the published 6.3 candidate line", "This 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.", "The changes since `v"+comparisonVersion+"` do not require a Pulse Mobile client change and preserve the existing mobile, Relay, onboarding, and mobile-facing API contracts, so the server cut is classified `no-mobile-impact`; no companion upload or public mobile-store rollout is part of this candidate.", "The prerelease Windows path retains exact-SHA, checksum, and detached-signature verification without Authenticode; stable `v"+stableTarget+"` restores mandatory SignPath signing unless a new version-bound decision is recorded.",