Commit Graph

8007 Commits

Author SHA1 Message Date
rcourtman 82c7c52727 Expose server-authored action policy provenance 2026-07-12 00:18:43 +01:00
rcourtman 76f1084c9b Enforce server-owned Patrol Autopilot acknowledgement 2026-07-11 23:12:00 +01:00
rcourtman b7b897651b Add canonical action result truth 2026-07-11 21:27:00 +01:00
rcourtman cd7886aa44 Enforce server-owned action approval authority 2026-07-11 19:49:17 +01:00
rcourtman e5dbbb3145 Derive cluster-scoped canonical host IDs from full hostnames
Sibling of #1559: canonicalIDFromIdentity hashed the short hostname in
its cluster arm, so two machine-keyless Docker Swarm members with FQDN
hostnames sharing a first label (cloud.a, cloud.b) derived the same
cluster:<swarm>:cloud canonical ID and fully merged in the registry.
PVE is unaffected (single-label node names hash identically).

Both hostname-derived arms now hash NormalizeFullHostname. Derivation
stays a pure function of identity so store-less registries keep deriving
the same IDs as the durable one. Compatibility for hosts minted under
the short era: journal reads already merge both eras via
ResourceIdentityPin.EraIDs, and new canonical-ID succession re-keys
resource_operator_state and action_audits rows to the successor ID at
pin persist (never across a contradicting machine key, never while the
old ID is still live, never rewriting journal rows) so operator intent
like never-auto-remediate survives the era change. Already-merged
keyless pairs cannot be retroactively split; the merged rows succeed to
whichever member persists first.
2026-07-11 19:05:14 +01:00
rcourtman a23a8fad9a Addresses #1558 2026-07-11 19:02:14 +01:00
rcourtman aefc631374 Addresses #1559
The code fix (preserve full dotted hostnames in identity pins and the
presentation host coalescer, with short/FQDN equivalence kept for
matching) rode commit 1d3b8e194 through a shared-index race. This
commit carries the unified-resources subsystem contract update and
moves the end-to-end regression test into registry_test.go.
2026-07-11 18:31:45 +01:00
rcourtman 0b6b97115b Addresses #1557 2026-07-11 18:30:43 +01:00
rcourtman 76dc690840 Suppress recovery notifications when the firing never left the queue
An alert that resolved while its firing notification was still in the
grouping window or waiting in the persistent queue (alert delay pushes
activation close to resolution; quiet-hours replay defers delivery)
produced a recovery-only notification: CancelAlert dropped the queued
firing, but LastNotified had been set optimistically at dispatch, so
the resolved-notification gate believed the firing had been sent.

CancelAlert now reports whether it cancelled a firing notification that
had not been delivered (grouping window entries and pending queue rows;
mid-send rows are excluded because their delivery may still complete),
and handleAlertResolved suppresses the recovery in that case. This also
covers the quiet-hours replay bypass: a recovery only follows a deferred
firing if the replay was actually delivered.

Addresses #1553
2026-07-11 18:28:28 +01:00
rcourtman 1d3b8e1949 Addresses #1555 2026-07-11 18:25:17 +01:00
rcourtman 175309b8f7 Thread alert emails with their resolved notifications
Every email covering a single alert now carries In-Reply-To and
References headers set to a deterministic incident thread ID derived
from the alert ID and firing start time, so mail clients thread the
firing, re-notification, and resolved emails of one incident together.
Message-ID stays unique per send because re-notified incidents send
multiple emails and some providers de-duplicate on Message-ID. Grouped
emails skip threading since firing and resolved batches rarely contain
the same alert set.

Addresses #1543 (discussion)
2026-07-11 18:22:07 +01:00
rcourtman 5a6871813f Grant CAP_NET_RAW to the installed systemd unit for ICMP probes
The installer's unit hardens with NoNewPrivileges=true, which strips the
ping binary's setuid/file capabilities, so ICMP availability probes could
never work on a systemd install. Grant the capability ambiently instead,
document the systemctl edit override for existing units, and pin the
hardening block with an install test and contract clause.

Addresses #1554 (discussion)
2026-07-11 18:15:22 +01:00
rcourtman a44e5513ca Register template funcs on the enhanced ntfy payload path
The ntfy branch of prepareEnhancedWebhookExecution parsed the payload
template without templateFuncMap(), so the built-in ntfy preset (which
uses {{.Type | title}}) failed to parse and Test sends returned HTTP
400 with "function \"title\" not defined". Register the func map on
that parse, matching the generic webhook path, and add a regression
test that renders the actual built-in ntfy preset template.

Addresses #1549
2026-07-11 18:11:22 +01:00
rcourtman 71ea14e4dd Add durable action dispatch continuity 2026-07-11 16:08:44 +01:00
rcourtman f510b99095 Enforce the canonical mutation plane 2026-07-11 15:19:41 +01:00
rcourtman c74c88fb86 Fix dispatch-time policy revocation 2026-07-11 14:29:26 +01:00
rcourtman e65bef8e84 Fix atomic action lifecycle replay 2026-07-11 14:02:32 +01:00
rcourtman c0bbed1c59 test(frontend): branch-coverage tests for 19 more pure frontend modules
Wave 2 of coverage_backlog_2026-07-11 (items 17-35): new *.branchcov.test.ts
covering top uncovered-branch functions in patrolRunPresentation,
infrastructureSelectors, resourceDetailDiscoveryModel, thresholdsResourceModel,
infrastructureSummaryCache, monitoredSystemPresentation, connectionsTableModel,
activeTurnStatus, resourceStoragePresentation, storagePoolDetailPresentation,
cephSummaryPresentation, settingsNavigationModel, workloadMetricHistoryModel,
resourceCorrelationPresentation, recoveryItemTypePresentation,
setupCompletionModel, storagePageState, discoveryPresentation, searchQuery.
GLM-authored, verify-gated: vitest (888 tests) + full type-check + eslint +
adversarial GLM review (KEEP 19/19). Tests only, no source changes.
2026-07-11 13:05:13 +01:00
rcourtman ffd1ea8127 Enforce Assistant read-only scope integrity 2026-07-11 12:43:55 +01:00
rcourtman c17e3814ee Rename the demo Patrol snapshot parameter so the SRC-04b guard passes
synthesizeDemoPatrolFindings took its patrolRuntimeState parameter as
'state', which trips the textual direct-state-access ban in
TestNoDirectStateAccessForMigratedResources even though the struct is
the sanctioned snapshot wrapper. The rest of the Patrol code names
these parameters 'snap' for exactly this reason, so the demo function
now follows suit. No behavior change.
2026-07-11 12:02:05 +01:00
rcourtman 4edf5f8265 fix: close chat command authority boundary 2026-07-11 11:54:56 +01:00
rcourtman 91b10b19b5 Simulate Patrol end to end in demo/mock mode
Mock mode faked assistant chat but left Patrol on the real provider
path. The public demo therefore ran real patrol cycles that failed with
"no patrol model configured", raised the Provider analysis error
finding, and rendered /patrol as an unconfigured setup surface with no
findings, which also cost the landing page its Patrol screenshot.

Demo mode now simulates the whole surface without ever contacting a
provider:

- runDemoPatrolCycle synthesizes findings from the live mock state
  (offline Docker edge host, degraded PBS and PMG, unhealthy or
  restart-looping containers, the fullest storage pool, the hungriest
  guest), quoting each resource's actual observed values so the patrol
  page stays consistent with the rest of the demo UI. Cycles refresh
  findings as heartbeats, auto-resolve ones whose mock condition
  cleared, and append matching run records plus a one-time backfilled
  history.
- Readiness, the API readiness checks, and the model-check preflight
  report a simulated pass under a "demo" provider identity, and the
  stale provider-error finding is auto-resolved.
- Release demo ordering is handled: mock fixtures only enable after the
  demo_fixtures license sync, so the patrol loop now starts for
  demo-intended runtimes (PULSE_MOCK_MODE requested in env) and every
  entry point re-checks IsDemoMode() live. Dev installs with the
  background-AI guard get a warmup goroutine instead of the scheduler.
- Run-history evidence filtering is now symmetric (demo runtimes hide
  real records, matching the existing inverse), scoped/targeted patrol
  runs are skipped in demo mode, and demo findings are excluded from
  ai_findings.json so fixtures never leak into a real install.
2026-07-11 11:25:52 +01:00
rcourtman 6b18652812 Point the portal operations-guide link at the repository root
The MSP portal support row deep-linked docs/MSP.md at blob/main, which
the repo-docs link-drift guard rightly rejects: a released portal build
must not reference branch-tip docs that can move out from under it.
The row keeps the docs/MSP.md path as text and links the repository
root instead, matching every other runtime surface. Portal dist rebuilt
alongside.
2026-07-11 11:11:58 +01:00
rcourtman 32f0fd4244 Keep Patrol background sessions out of the Assistant quick-resume list
Patrol detection reuses the durable patrol-main session (and investigations
use investigation-* sessions) as forensic logs. Those surfaced in the
Assistant empty-state Recent sessions list as resumable chats, titled with
the raw triage seed ("# Deterministic Triage Results Scanned ..."), which
reads as a broken user session (reported by a 6.0.5 user via support).
Session summaries now carry a server-derived system flag for Pulse-owned
background runs and the quick-resume list filters them out. The drawer
session picker and the Settings sessions panel still list them for
inspection.
2026-07-11 11:09:28 +01:00
rcourtman 704e738913 test(frontend): branch-coverage tests for 16 pure frontend modules
New *.branchcov.test.ts covering the top uncovered-branch functions from
coverage_backlog_2026-07-11 across models/presentation/selectors/mappers
(infrastructureOperationsModel, resourceBadgePresentation, kubernetesPageModel,
agentCapabilities, alertResourceTableModel, alertHistoryModel, storageAdapters,
resourceDetailDrawerServiceModel, proxmoxBackupRecoveryModel, transcriptExport,
resourceDetailMappers, diskPresentation, infrastructureImportPlanModel,
storageAdapterCore, resourceIdentity, workloadSelectors). GLM-authored,
verify-gated: vitest (711 tests) + full type-check + eslint + adversarial GLM
review (KEEP 16/16). Tests only, no source changes.
2026-07-11 11:04:54 +01:00
rcourtman a63c3b1045 Route service-context discovery through the Patrol model by default
Discovery is high-fan-out background work (one model call per container or
service), but GetDiscoveryModel fell back straight to the shared default
model. An operator who picked a cheap Patrol model to keep background AI
inexpensive still had scheduled service-context refreshes burning the
expensive shared model, dozens of calls per run, with no chat activity
(reported by a 6.0.5 user via support). Discovery now follows the same
fallback chain as auto-fix: DiscoveryModel, then PatrolModel, then the
shared default. Settings copy and the ai-runtime contract state the chain.
2026-07-11 10:55:10 +01:00
rcourtman 8980ae1f5f Raise the bundle size baseline to the shipped platform features
The Docker, Kubernetes and TrueNAS chunks grew with the shared
platform-table sorting fabric and the grouped-by-host restoration, and
UpdatesSettingsPanel grew with the digest-pinned Pro Docker update
commands. All deliberate landed work, so the budget baseline moves to
match. The violations were hidden behind the failing frontend unit
tests and only surfaced once those were re-pinned.
2026-07-11 10:51:20 +01:00
rcourtman d5609a2dc3 Re-pin frontend tests to the Patrol typed-action vocabulary
The Patrol lifecycle work renamed the queued-fix copy (Review fix ->
Recover action, proposed fix -> typed action, and the waiting-for-
approval summaries) and reshaped ApprovalSection's badge constant, but
the unit tests still asserted the old strings and identifiers, leaving
build-and-test red on main since last night. Expectations now match the
shipped source. ApprovalSection kept MetadataBadge and shared Button
variants, so the primitive guardrails re-pin to its current shape
(BADGE_PROPS, no warningSolid variant) rather than flagging a real
regression.
2026-07-11 10:40:20 +01:00
rcourtman d4d12f3289 Let SSO sessions carry admin privileges through the settings gate
The privileged-session gate (ensureAdminSession) and the settings
capabilities snapshot only accepted a session whose username equals the
configured local admin identity. SSO users are keyed by their
provider-scoped principal (sso:oidc:...), which can never match, so an
SSO session was locked out of every settings-scoped route no matter what
roles it held. That made group role mappings (group=admin) appear
broken (#1535) and surfaced as 'Unable to load SSO providers' when an
SSO user opened the SSO settings panel (#1533).

A session user now passes the gate when their effective RBAC
permissions include the admin action on all resources, which is the
shape the built-in Administrator role assigns through group role
mappings. On an instance with no local admin identity configured at all
(the v5 OIDC-only pattern), SSO sessions pass as before the rewrite,
since they are the only administrators the instance has. Org-scoped
tenant sessions keep their own management rules.

Refs #1533, #1535
2026-07-11 10:27:26 +01:00
rcourtman f2b732721e Fix physical disks vanishing on wide nodes and standby-misreporting SSDs
Two root causes behind #1516's remaining reports:

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

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

Refs #1516
2026-07-11 10:24:45 +01:00
rcourtman eb9954618a Add governed storage pressure cleanup 2026-07-11 10:11:35 +01:00
rcourtman 500d3be2a7 Align Pro feature catalog names with the new Patrol vocabulary
The public pricing surface and in-app plan-selling copy now say
"Patrol investigates issues and explains the root cause" and
"Patrol applies safe fixes and verifies the result". Rename the
licensing catalog DisplayName/ComparisonName pairs to the title-cased
forms, align the ai_autofix UpgradeReason verbs (apply/verify the
result), regenerate the frontend catalog, and update the Go, vitest,
and Playwright pins plus README and PULSE_PRO docs.
2026-07-11 09:33:47 +01:00
rcourtman 1312da3acb Add governed host update autonomy 2026-07-11 01:25:14 +01:00
rcourtman 0b15855de3 Align in-app Plans & Billing copy with the new public pricing vocabulary
The public pricing surface (pulserelay.pro and the license-server pricing
model, pulse-pro 3d9e365/2da1290) now sells Pro with the real Patrol mode
names and plain-English job framing. The in-app plan cards still used the
old taxonomy (Choose Patrol mode, Patrol investigates issues, Patrol
handles safe fixes) and 'admin controls' phrasing. Pro plan-selling copy
now matches the public wording: mode names spelled out (Ask first, Safe
auto-fix, Autopilot), investigation explains the root cause, safe fixes
are verified, and RBAC/audit/reporting/agent profiles are 'team controls'.
Functional CTAs (the Choose Patrol mode link) and canonical mode labels
are unchanged. Test pins updated.

The Go-side feature catalog names (Patrol Investigates Issues / Patrol
Handles Safe Fixes) still carry the old phrasing and are a separate
follow-up since they regenerate from pkg/licensing.
2026-07-11 01:16:00 +01:00
rcourtman b57fb95e20 Fix click-dead zone on the Assistant launcher tab
The document root kept a forced scrollbar (html { overflow-y: scroll })
from before the app-scroll-shell owned scrolling. The root never
scrolls, so the rule only reserved a dead 11px gutter at the right
viewport edge and pulled every fixed right-anchored element off the
true screen edge - parking the desktop Assistant launcher tab under
the scroll shell's own scrollbar, which hit-tests above descendants.
Net effect: the tab's right quarter plus the whole screen-edge strip
silently swallowed clicks, exactly where an edge-docked control gets
aimed at.

Drop the legacy root-scrollbar rule and portal the launcher out of the
scroll shell so its full face receives clicks, flush to the real edge.

Verified live: elementFromPoint returns the button across its entire
width including the last pixel column, and an extreme-edge click opens
the drawer.
2026-07-11 01:02:01 +01:00
rcourtman 2b028d31bd Refresh v6.0.6 RC1 release packet 2026-07-11 00:42:20 +01:00
rcourtman 69b2028513 Add policy-scoped Patrol autonomy 2026-07-11 00:37:34 +01:00
rcourtman 1ae2c6726d Re-pin vSphere activity date-time guardrail to sortable head
The TrueNAS/vSphere sorting adoption (cf0f027b6) moved the activity
table's When header onto PlatformSortableTableHead, so the guardrail's
literal getPlatformTableHeadClassForKind('numeric-value') pin no longer
matches. The alignment intent survives as the sortable head's
kind="numeric-value" prop; pin that form instead. The metadata-badge
and Button guardrail failures predate the sorting commits and are left
for their owner.
2026-07-11 00:36:28 +01:00
rcourtman 4572f83bc0 Document platform-table sort fabric in subsystem contracts
The three sorting commits (20cccc1a9, f66c4b46b, cf0f027b6) added the
shared sortable-header fabric and adopted it across Docker, Kubernetes,
TrueNAS, vSphere, and Proxmox nodes tables, but landed frontend-only,
which skips the local governance audit; Canonical Governance CI then
flagged frontend-primitives and unified-resources for missing contract
deltas. Record the fabric's interaction contract (shared sort state,
persistence, missing-values-sink, aria-sort, canonical alignment) and
the two-layer ordering rule (status-first default, user sort on top).
2026-07-11 00:24:29 +01:00
rcourtman cf0f027b64 Adopt shared platform-table sorting on TrueNAS, vSphere, and Proxmox nodes tables
v5 parity follow-up to 20cccc1a9, completing the adoption sweep after the
Kubernetes pass (f66c4b46b). Adopts createPlatformTableSortState +
PlatformSortableTableHead across the TrueNAS systems / apps / services /
network shares / protection / storage topology / VMs tables and the vSphere
hosts / datastores / networks / activity tables: per-table storage keys,
descendingFirst on metric, count, and timestamp columns, the built-in order
preserved until the user sorts, and canonical kind-based alignment via the
sortable head. Composite summary columns (inventory, ports, images, access,
clients, devices, flags, hosts, consumers) stay non-sortable.

The TrueNAS storage topology sorts sibling groups and re-emits the tree
depth-first, so pools re-order against pools and every dataset or disk stays
under its parent; a new sort test pins that plus descending-first metrics and
persistence.

ProxmoxNodesTable drops its bespoke non-persisted createSignal sort for the
same fabric, so the nodes sort now survives reloads like every other platform
table. Its existing sort assertions pass unchanged; the suite now clears
localStorage between tests since the sort persists by design.

Also repairs the platformOverviewLayout guardrails at HEAD: f66c4b46b landed
with the TrueNAS/vSphere head assertions already flipped to the kind form and
the Kubernetes assertions still on the legacy form (a staging race between
parallel agents); this commit carries the version with both sets on the kind
form, which is green against the migrated tables.
2026-07-11 00:15:42 +01:00
rcourtman f66c4b46b3 Restore user column sorting on Kubernetes platform tables
v5 parity follow-up to 20cccc1a9 (v5 KubernetesClusters.tsx persisted
k8s-sort-key/k8s-sort-dir sorting; v6 shipped fixed ordering). Adopts the
shared platform-table sort fabric (createPlatformTableSortState +
PlatformSortableTableHead) across the clusters, nodes, pods, deployments,
controllers, services, storage, config, autoscaling, and networking tables:
per-table storage keys, descendingFirst on metric/count columns (pods Ready
stays ascending-first so the least-ready pods surface), the status-first
compare preserved as the base order until the user sorts, and canonical
kind-based alignment via the sortable head's kind prop. Composite summary
columns (ports, selectors, labels, access/policy, bounds, detail) stay
non-sortable. TrueNAS/vSphere adoption is landing separately.
2026-07-11 00:11:04 +01:00
rcourtman 20cccc1a91 Restore user column sorting on Docker platform tables
v5 parity: v6 platform tables shipped with fixed status-first ordering and
no sortable headers. Adds opt-in sortable-header support to the shared
platform table fabric (createPlatformTableSortState + PlatformSortableTableHead
in sharedPlatformPage.tsx): click cycles first-direction -> flipped -> built-in
order, state persists per table via usePersistentSignal, missing values sink
to the bottom, aria-sort and arrow indicator match the workloads table, and
alignment stays on the canonical kind helpers.

Adopted across the Docker tables (containers, hosts, images, services,
volumes, tasks, swarm nodes, secrets, configs). Container sorting applies
within host groups and stays orthogonal to grouping, unlike v5's
sort-by-host-equals-grouping design. Kubernetes and TrueNAS/vSphere adoption
are follow-ups.
2026-07-10 23:43:45 +01:00
rcourtman d5437a9353 Allow RCs while Windows signing is pending 2026-07-10 23:12:12 +01:00
rcourtman d5c4f9adf3 Restore grouped-by-host view and web links on Docker containers
v5 parity regressions reported by a Pro customer after the v6.0.5
upgrade:

- The Docker containers table lost the grouped view: add a
  Grouped/List segmented control (same fabric as the workloads
  table), grouped by host with count badges and host web links,
  persisted locally and defaulting to grouped for multi-host fleets.
- Container web-interface links were buried in the detail drawer's
  access section: the container name now renders through
  WebInterfaceNameLink, linking straight to the resource customUrl
  like VM/LXC guest names do. Applies to all docker native tables
  via the shared name cell.
2026-07-10 22:50:42 +01:00
rcourtman cb310d9932 Hydrate container customUrl in REST resource snapshot
The websocket broadcast path applies docker metadata (container
customUrl) via applyDockerMetadataToUnifiedResources, but the REST
/api/resources registry seed read the raw unified state view, so the
two payloads drifted: a container web-interface URL saved in the
drawer never appeared in REST-hydrated tables. Apply the same
hydration at the UnifiedResourceSnapshot provider boundary.
2026-07-10 22:48:49 +01:00
rcourtman 4883235936 Add public code signing policy 2026-07-10 22:46:35 +01:00
rcourtman 92ea590000 Prepare v6.0.6 RC1 release packet 2026-07-10 21:39:03 +01:00
rcourtman 5bbfce956d Complete Patrol action lifecycle continuity 2026-07-10 21:22:06 +01:00
rcourtman 3bea52b1b5 Harden native Windows agent lifecycle 2026-07-10 19:16:23 +01:00
rcourtman c89918d78e fix(setup): make onboarding actions explicit and truthful 2026-07-10 18:21:56 +01:00