Commit Graph

4400 Commits

Author SHA1 Message Date
pulse-triage[bot] 7e1543b2d6 Merge current Pulse upstream for publication
Incorporate the landed Patrol digest email work that reached origin/main after publication preflight while preserving reviewed tip 3016bc72a0 unchanged in local history. The upstream and reviewed file sets do not overlap.

Change-source: pulse-maintainer
2026-09-02 12:15:41 +01:00
rcourtman eecf45fafd Email the Patrol weekly summary as a report schedule kind
The customers most likely to lapse are the ones who stopped opening Pulse,
so the in-app "This week" card cannot reach them. Report schedules gain a
kind: the default resources kind is the existing PDF or CSV performance
report, and patrol_digest emails the same weekly "what Patrol did for you"
rollup the Patrol page shows, in plain language, for the whole workspace.
It reuses the existing scheduler, cadence, recipients, tenant email config,
and Pro advanced-reporting entitlement rather than adding a second
scheduler. Digest schedules are weekly and email-only; the server fixes
scope, format, and attachments, and a run with no AI service, no Patrol, or
no email destination records a last_error that names the missing piece
instead of sending an empty week. Nothing is written to disk.

The digest assembly moves from the HTTP handler into
AISettingsHandler.BuildPatrolDigest so the schedule runner and the
endpoint share one code path. docs/MSP.md documents the kind for
providers; the Settings > Reporting "Report type" selector follows in a
separate change once it has a Pro-licensed browser pass.
2026-09-02 11:15:22 +01:00
pulse-triage[bot] 2ef4492972 Qualify TrueNAS 26 JSON-RPC snapshot transport
TrueNAS 26 removes the REST API, making Pulse's authenticated JSON-RPC path the compatibility boundary for snapshot collection. Pin the full transport contract to the published 26-BETA.3 version so any fallback to removed REST endpoints is caught before release.

Change-source: pulse-maintainer
2026-09-02 10:52:41 +01:00
pulse-triage[bot] facee87bb4 Merge pull request #1852 from rcourtman/maintainer/20260902T060320Z
Fix empty-slot alerts and make keyboard navigation accessible
2026-09-02 09:52:36 +01:00
pulse-triage[bot] 9d08bd1c50 Merge captured Pulse upstream for publication
Combine upstream Patrol digest work that was already present when this coordination batch began with the locally reviewed integration line.

Change-source: pulse-maintainer
2026-09-02 09:44:33 +01:00
pulse-triage[bot] 6ab16f9f35 Merge current Pulse upstream for publication
Preserve the reviewed command-palette browser receipt as the current singleton receipt while retaining the upstream Patrol setup proof in its unchanged parent history.

Change-source: pulse-maintainer
2026-09-02 08:56:06 +01:00
rcourtman 8ea94d792b Add the Patrol weekly digest endpoint
Nothing in Pulse showed a paying customer what Patrol had done for them:
about 164 runs a month per install, findings raised, investigations and
fixes, and none of it summarised. GET /api/ai/patrol/digest rolls the last
N days (default 7, max 30) up from records Pulse already keeps: run
history, the findings store, Patrol-origin action audits, and the usage
cost store. It adds no telemetry and persists nothing. The payload
reports when the bounded run history no longer covers the window and when
model pricing is unknown, rather than quietly under-counting.

This is the first slice of the "Patrol weekly digest" named bet in the
pulse-pro demand ledger; the in-app "This week" card follows once its
browser pass is recorded. docs/PATROL_WEEKLY_DIGEST.md holds the design
note and the honest limits of each line.

status.json registers the patrol-value-visibility coverage gap, the
candidate lane, and its work claim. It also drops the second, identical
copy of the ai-provider-guided-setup coverage gap that landed with #1853;
the duplicate id fails the status audit on main for every pull request.
2026-09-02 08:53:50 +01:00
rcourtman ac328e1eee Guide the Patrol model choice with cost preview and budget pause
Forty-two percent of paying installs never get Patrol running, and the
support and issue evidence says the wall is model choice and cost: a
prospect asked which models Pulse recommends before trying, a Pro customer
picked Flash-Lite to save money and Patrol then could not file verdicts,
and a mispriced Opus row tripped the 20 USD budget so the circuit breaker
disabled Patrol with nothing but a log line (#1789).

Answer both questions at the point of choice. The Patrol and shared
default model pickers pin guided models in a "Suggested for Patrol"
section with recommended / suggested / caution markers and a one-line
reason: the Ollama preflight blessing is the only recommended entry,
Gemini Flash-Lite the only caution, and cloud starting points are labelled
price-driven and unqualified until this install's own readiness pass
upgrades them to "Verified on this install". A server-computed cost
preview sits under the model choice: monthly estimate from Pulse's price
table, the schedule, and the install's own median full run once three
priced runs exist (a measured 104,528-in / 4,491-out run otherwise), with
the assumption stated and a token explained once, plus 30-day spend
against budget. Each schedule preset is priced in the Schedule select.

When a per-token model is picked while the schedule is still the 6-hour
default, the cost model proposes the slowest preset that keeps scheduled
runs under half the budget (20 USD reference when none is set) and says
what the slower check costs in detection delay; a schedule the install
already chose is never changed.

A used-up budget is now a spending decision, not a provider fault: the
budget refusal is a typed sentinel with the figures, classified as the
budget_exhausted cause, excluded from circuit-breaker accounting, and
promoted into the Patrol block state, so the Patrol page shows "Patrol
paused" with the spend and limit and a "Raise the cost budget" action
instead of "Check Patrol model".
2026-09-02 07:55:42 +01:00
pulse-triage[bot] 1bdc3b0e37 Merge captured Pulse upstream
Capture the origin/main state that predated this coordination batch while preserving the locally reviewed integration history. Keep the first-parent receipt at the merge tip; each exact non-merge UI commit retains its own parent-bound browser proof.

Change-source: pulse-maintainer
2026-09-02 07:46:39 +01:00
rcourtman a10c175f31 Fold alert-mirroring findings, collapse flapping, surface lasting decisions
Two users (discussions #1623, #1699) could not find Patrol's durable
outcomes: the Needs attention detail offered only Acknowledge and
Suppress, and Remember as expected, Dismiss: Not an issue, Dismiss:
Later, and Create rule sat two levels below it. The same screenshot
showed an alert with eleven open/resolved transitions in a day listed as
eleven timeline rows, and Patrol findings restating an active alert
appearing beside it. Telemetry shows most findings are seen and ignored.

Backend: findings_alert_mirror.go is the single matcher for a finding
that restates an active alert (explicit alert identifier, or same
canonical resource plus the same condition class). Patrol stamps
mirrors_alert_id/mirrors_alert_type after every real and demo cycle and
clears them when the alert resolves; the finding is kept, not dropped.
The storm throttler now also owns flap detection: four or more
open/resolved transitions in 24h mark a finding flapping, and the store
maintains one collapsed lifecycle row with the count instead of a row per
transition, hydrating the window from persisted lifecycle after restart.
The attention projection applies the same threshold to alert timelines
and exposes a flapping summary while keeping the full timeline.

Frontend: the attention detail gains a Lasting decisions section that
offers the four Patrol decisions on the finding mirroring the alert, each
with one line saying what it does and how long it lasts, an inline
confirmation, and a remembered-decision state with Reopen. An alert with
no mirrored finding says Patrol has nothing to remember and points at
alert thresholds. Flapping items carry one label and a summarised
timeline with every transition under an expansion. FindingsPanel demotes
active alert-mirroring findings into a collapsed group. The Patrol
surface loads dismissed findings so remembered decisions are visible.
2026-09-02 07:02:51 +01:00
pulse-triage[bot] 803d217e18 Apply Unraid empty-slot semantics in storage health
Use the reviewed placeholder-filesystem rule when storage health assesses structured Unraid state, while retaining explicit DISK_NP_MISSING members as critical evidence.

Change-source: pulse-maintainer

Contract-Neutral: Unraid fsType=auto placeholder normalization is applied consistently at storage-health assessment without changing contracts
2026-09-02 06:29:41 +01:00
pulse-triage[bot] ace72f8442 Ignore Unraid auto filesystem on empty slots
Treat Unraid's fsType=auto value as a placeholder rather than disk assignment evidence at both agent collection and server ingestion boundaries. Preserve real assigned and explicit missing members.

Change-source: pulse-maintainer
Contract-Neutral: Unraid fsType=auto placeholder normalization fixes false missing-slot alerts without changing wire or subsystem contracts
(cherry picked from commit fd843da7dfc51bba29bb5ddcbcdb4480d0e67079)
(cherry picked from commit 7a6456969d)
2026-09-02 05:40:46 +01:00
rcourtman 5f0a487404 Merge pull request #1823 from rcourtman/pulse/secure-runtime-rootful-qualification
Add rootful runtime qualification packet
2026-09-02 01:48:16 +01:00
rcourtman 7996848b64 Submit advertised lifecycle actions through pulse_control, not prose
A paying operator asked the Assistant to reboot five Proxmox VMs matching
a name pattern (GitHub #1782, support mail 2026-08-26 and 2026-08-29). The
model resolved the VMs and then ended with a report that invented a
prerequisite: a QEMU guest agent on 6.3.2, a "discovery binding" on stable
6.4.0. It never planned the action. Three defects made the governed path
fail whenever the model did try it, and nothing refused the prose ending
when it did not:

- pulse_control handed the session-scoped id (vm:<node>:<vmid>) to the
  action lifecycle, whose registry keys on canonical unified ids, so a
  Proxmox guest plan could never resolve.
- pulse_control gated the action on the legacy per-executor action list,
  which never carried the canonical "reboot" capability Proxmox guests
  advertise, so "reboot" was refused as not permitted before planning.
- A reference absent from the session context was refused with "resource
  discovery is required" even when the unified inventory resolved it.

pulse_control now binds its target to the canonical unified resource
(session alias first, then a unique inventory match, refusing ambiguity
with candidate ids and naming the pulse_query recovery on a miss), passes
the canonical id to the planner, and answers "not available" only from
the resource's current advertised capabilities. The FSM ordering block
and the shared operating instructions state that a recoverable block is
not a limitation to report, and the instructions require the governed
action tool for advertised capabilities. The agentic loop adds a bounded
advertised-action gate: when the operator asked for a lifecycle action,
pulse_control was offered but never submitted, and a session-resolved
resource advertises the action, a tool-free final answer is refused once
with the exact per-target calls.

Covered by tools and loop unit tests (the #1782 transcript against a
scripted provider fails on the previous code with the two exact errors
above), a prompt-contract test, and the live eval scenario
ProxmoxBulkLifecycleActionScenario.
2026-09-02 00:19:08 +01:00
rcourtman b1044cd8a4 Let replayed request ids wait for the in-flight handler instead of dropping
Since 60d0651a88 every typed request registers a per-connection cancellable
slot that its handler goroutine releases in a deferred cleanup after sending
its result. The server replays a request id when it wants the durable receipt
again, and that replay can reach the reader before the previous handler's
deferred release runs. launchCancellableRequest treated that as a duplicate
and dropped it, so the server waited out the operation's full timeout for a
result the agent already held. The Linux x64 native-verification leg failed
this way on 12 of the last 25 main runs, always on a "replay 1" dispatch of
host update, storage cleanup, or Docker lifecycle.

Give each slot a done channel that closes on release. A replay whose id is
still registered on the same connection now waits for that release and then
runs, answering from the durable receipt. Invalid ids and over-capacity
requests are still dropped. A unit test pins the wait-then-run behaviour and
the agent-lifecycle contract records the replay rule.
2026-09-01 23:22:55 +01:00
rcourtman e1e4c3e700 Wait for the server to observe runner disconnect before replay reconnect
The cancellation replay test stopped the first action runner and started a
second one as soon as the client goroutine exited. The server observes the
socket close on its own reader, so under GOMAXPROCS=1 with the race detector
the second startRunner saw the stale session as still connected and the
replay was dispatched to the dead socket, timing out after 30s on the
sharded release preflight while passing on multi-core hosts. Wait until the
server reports the agent disconnected before reconnecting, as the agentexec
server tests already do.
2026-09-01 22:51:56 +01:00
pulse-triage[bot] ab556fc212 Skip numbered block-device partitions in disk I/O
Change-source: pulse-maintainer
2026-09-01 19:36:13 +01:00
rcourtman d99e3cc715 Log rootless admission fallback reasons 2026-09-01 18:39:23 +01:00
pulse-triage[bot] 0e341583cb Align shared-token uninstall API regression
Change-source: pulse-maintainer
2026-09-01 17:41:43 +01:00
pulse-triage[bot] c9e6a8346b Preserve shared unified agent credentials
Change-source: pulse-maintainer
2026-09-01 17:34:30 +01:00
pulse-triage[bot] 78023e0e42 Serve signed Windows agents from canonical assets
Change-source: pulse-maintainer
2026-09-01 16:16:56 +01:00
pulse-triage[bot] 3fd171498f Merge remote-tracking branch 'origin/main'
Change-source: pulse-maintainer
2026-09-01 16:16:33 +01:00
rcourtman 1b51a9c948 Use example.com for the dev-mock portal login email
The portal dev harness mocked the signed-in user as a plausibly real
gmail address in this public repo; the other mock members already use
example.com addresses. Align the login email with them.
2026-09-01 16:11:09 +01:00
pulse-triage[bot] 9a5af6ff79 Keep Windows agent update signatures addressable
Change-source: pulse-maintainer
2026-09-01 16:11:04 +01:00
pulse-triage[bot] 91d120ea5d Merge remote-tracking branch 'origin/main'
Change-source: pulse-maintainer
2026-09-01 16:03:53 +01:00
pulse-triage[bot] e37595748e Merge remote-tracking branch 'origin/main'
Change-source: pulse-maintainer
2026-09-01 15:53:17 +01:00
rcourtman 53267e149d Harden least-privilege installer lifecycle state 2026-09-01 15:53:02 +01:00
rcourtman b1240c6ca3 Point OpenRouter attribution at pulserelay.pro and fix Fireworks key link
External-reference audit after the fabricated security@pulseapp.io find:
the OpenRouter HTTP-Referer claimed pulse.app, a third-party domain Pulse
has never owned, and the in-app Fireworks 'Get API key' link 404s
(fireworks.ai/account/api-keys moved to
app.fireworks.ai/settings/users/api-keys).
2026-09-01 15:49:05 +01:00
pulse-triage[bot] 79b2aab067 Merge remote-tracking branch 'origin/main'
Change-source: pulse-maintainer
2026-09-01 15:37:17 +01:00
rcourtman 3c31aa4805 Stop Docker records flip-flopping under shared same-hostname tokens
The Docker analog of the #1753 estate was still broken: two live
machines reusing one short hostname and one pasted unified install
token collapsed into a single flip-flopping DockerHost record, because
the hostname+token identity fallback adopts a record whose machine ID
disagrees with the report's. That fold is deliberate for recreated
containers (whose /etc/machine-id regenerates), so it cannot simply be
guarded by machine-ID inequality - the discriminator is a revisit: a
recreated container transitions to its new machine ID exactly once,
while two live machines alternate. Removing the collapsed record then
revoked the shared token unconditionally, rejecting every surviving
module - host reports included, since a unified install shares one
credential - with 401 "Unauthorized access attempt".

Consult the identity flap tracker before the hostname fallbacks adopt a
machine-ID-disagreeing record: a report whose machine ID returns to a
value already seen behind that identity is a second live machine and is
not folded. The machine whose identifiers minted the record reclaims it,
so the first site keeps its record and history, and the other site falls
through to the token binding check, converging on the documented "Each
Docker / Podman module must use a unique API token" rejection instead of
silently overwriting the record every cycle. RemoveDockerHost now skips
token revocation while any host or Docker record still authenticates
with the credential, mirroring the host-agent removal guard.

Regression coverage: an end-to-end router test walking the two-site
shared-token estate (host + Docker reports, alternating cycles, removal)
asserting the first site's identity stays stable, the second site gets
the unique-token guidance, and the shared token survives removal; a
router test proving removal of a machine's Docker record keeps the
unified token its host record still uses; and a state-layer test pinning
the reclaim/no-flip-flop convergence. Recreated-container adoption and
the existing token-uniqueness rejections keep their tests unchanged.
2026-09-01 15:26:48 +01:00
pulse-triage[bot] ba4c157aa2 Bound control-plane request reads
Change-source: pulse-maintainer
2026-09-01 15:15:27 +01:00
rcourtman a49a4e65d1 Stop shared-token same-hostname agents collapsing identities
Two standalone Proxmox sites that reuse one short node name and one
pasted install token still folded into a single host record: the
token+hostname binding and the continuity store's hostname+token
fallback both resolve identity without consulting the machine ID, so
the second site's reports adopted the first site's host ID and the
record flip-flopped between machines. Removing that collapsed record
(the agent uninstaller does this on reinstall) then revoked the shared
token because the still-used guard could only see one record, leaving
every surviving agent rejected with 401 "Unauthorized access attempt"
on /api/agents/agent/report - the new symptom reported on #1753 after
the v6.4.2 retest.

Qualify host token binding slots with the reporting machine's identity
so two machines behind one token+hostname hold separate bindings,
refuse to adopt a bound or continuity identity whose recorded machine
ID disagrees with the report's, and rebuild bindings machine-qualified
after token reloads. Reports without a machine ID keep the legacy
binding shape and same-machine reinstalls keep their stable IDs.

Regression coverage: a state-layer test proving two shared-token pve01
machines keep distinct stable identities, continuity-match tests for
the machine guard, and an end-to-end router test walking the reporter's
estate - both install patterns, alternating reports, uninstall and
reinstall - asserting report auth never breaks and no token is revoked
while its sibling site still uses it.
2026-09-01 14:36:49 +01:00
pulse-triage[bot] d083f50fbc Classify oversized agent reports correctly
Change-source: pulse-maintainer
2026-09-01 13:59:36 +01:00
pulse-triage[bot] 12b90e2e5e Keep Proxmox agent links within provider scope
Change-source: pulse-maintainer
2026-09-01 13:17:41 +01:00
pulse-triage[bot] 351386bce7 Bound PDM and updater metadata responses
Change-source: pulse-maintainer
2026-09-01 12:20:52 +01:00
pulse-triage[bot] b1feb10b3f Preserve standalone Proxmox node labels after agent merge
Change-source: pulse-maintainer
2026-09-01 11:26:52 +01:00
rcourtman 60d0651a88 Cancel abandoned typed agent operations 2026-09-01 11:21:26 +01:00
pulse-triage[bot] ba833e53c0 Merge remote-tracking branch 'origin/main'
Change-source: pulse-maintainer
2026-09-01 11:00:03 +01:00
rcourtman 4e22a49f02 Add native PVE action qualification harness 2026-09-01 10:43:35 +01:00
pulse-triage[bot] b8158dce29 Keep PBS write evidence poll-scoped
Cached incomplete snapshots could retain an earlier no-writer observation when both the snapshot refresh and current task query failed. Reset task evidence before each live read so degraded visibility remains unknown and conservative.

Change-source: pulse-maintainer
Contract-Neutral: poll-scoped PBS task evidence cache correction; behavioral bug fix, no wire contract change
2026-09-01 10:16:41 +01:00
rcourtman 01ff388c7e Preserve native Proxmox guest handoff 2026-09-01 09:31:40 +01:00
pulse-triage[bot] 696cb39c08 Bound container stats response bodies
Change-source: pulse-maintainer
2026-09-01 08:47:51 +01:00
pulse-triage[bot] b49b3d89f4 Merge remote-tracking branch 'origin/main'
Change-source: pulse-maintainer
2026-09-01 08:18:37 +01:00
rcourtman 3f4ea6f640 Stop same-hostname standalone sites collapsing at presentation
Two hand-added standalone Proxmox connections whose machines are both
natively named pve survived the state-layer identity fixes but still
collapsed in the presentation host coalesce: a provider node row carries
no machine identity of its own, so once the site agents connected, one
site's node row absorbed an agent row on the shared short hostname and
the other site's node row folded into it. Input ordering varies per
snapshot, so the surviving row alternated between sites on every
refresh, which is exactly the reported NODES 1 view with the VM count
flipping 11/18 and the Agent badge hopping rows on v6.4.1.

Add a provider-scope veto mirroring the state rule: node facets from
different connection instances only merge with same-machine proof (same
node identity, same non-empty cluster, or same endpoint host). In a
bucket holding two distinct provider scopes, an agent row now attaches
only to the node whose state-layer agent link names it, instead of the
first compatible row. The merged row also keeps the Proxmox node row's
display-name-aware name instead of the agent's bare reported hostname,
which is the original 'Node Name field not observed' complaint.

Validated: internal/unifiedresources, internal/monitoring, and
internal/api green on linux/amd64 with the change.

Refs #1753
2026-09-01 06:38:34 +01:00
rcourtman 2ad094c927 Deflake typed action containment tests
The group-writable executable check wrote its fixture through os.WriteFile,
which filters the mode through the process umask, so under the runner's 022
umask the group-write bit never reached disk and validateTrustedExecutable
correctly trusted the file. Chmod the fixture to the asserted mode.

The Proxmox guest executor tests stamped their after observations one second
past the test-start clock, but actionStartedAt is stamped inside
ExecuteAction after handler setup, so a loaded runner overran the margin and
the independent observation was discarded as pre-action. Widen the
observation offset to a minute.

Both failures broke build-and-test on main (run 33454838531). Reproduced the
hostagent failure on Linux under umask 022 and verified both packages green
after the fix.
2026-09-01 06:15:46 +01:00
pulse-triage[bot] 3acc255559 Merge remote-tracking branch 'origin/main'
Change-source: pulse-maintainer
2026-09-01 01:41:09 +01:00
rcourtman f9ae289bd5 Contain typed action subprocesses with systemd 2026-09-01 01:26:39 +01:00
pulse-triage[bot] 8cd65a3f24 Keep missing health telemetry non-green
Change-source: pulse-maintainer
2026-09-01 01:03:53 +01:00
pulse-triage[bot] c253495045 feat: add fleet health home
Change-source: pulse-maintainer
2026-09-01 01:02:17 +01:00
pulse-triage[bot] 3fc25e5934 Bound AI provider HTTP responses 2026-09-01 00:56:50 +01:00