Commit Graph

3607 Commits

Author SHA1 Message Date
rcourtman 2f967406df perf(ai): gate and batch Patrol's per-alert auto-resolve model calls
Production telemetry (30d) shows Patrol averaging ~9.4 billed AI calls
per run while only ~1-2 come from the analysis passes. The remainder is
the alert auto-resolve review: one QuickAnalysis call per active alert
(>=10min old) on every run, re-asking the model about the same standing
alert every interval even when the trigger condition demonstrably still
holds.

Two structural changes, both resolution-neutral:

- Still-firing gate: skip the model review for alerts whose current
  snapshot still shows the trigger condition (metric at/above threshold,
  offline resource still offline). A correct review could only answer
  KEEP, so the question is not worth a billed call. The gate never
  resolves anything locally - uncertain cases (unknown types, missing
  resources, unmapped metrics) still go to the model, which retains
  sole authority over resolution.
- Batched review: the remaining candidates are reviewed in one model
  call per 20 alerts (numbered verdict lines, unparseable -> KEEP,
  bare-RESOLVE fallback for single-alert batches) instead of one call
  per alert.

Steady-state runs with standing alerts drop from N review calls to 0;
runs where conditions may have cleared pay ceil(K/20) instead of K.
QuickAnalysisRequest gains a TargetType tag (recorded on the usage
event) so cost telemetry can decompose alert-review spend from the
main patrol pass going forward. Contract updated in-commit
(ai-runtime: model-owned resolve direction, cost-gated keep direction,
batched review, fail-safe parsing, alert_autoresolve usage tagging).
2026-07-17 23:23:38 +01:00
rcourtman 6351fd6526 fix(updates): select releases by highest version, not GitHub list order
Production telemetry (7d, 2026-07-17) shows a persistent tail of live
installs on 6.0.0-rc.x and 6.0.3/6.0.4 while 6.0.5 has been stable since
2026-07-09. Root cause for the reachable stranding class: release
selection trusted GitHub's created_at ordering, and this repo interleaves
v5-line maintenance releases with v6 releases (v5.1.36 was created the
day before v6.0.5). Whenever a v5.1.x release is the most recently
created stable:

- stable channel (getLatestReleaseForChannel) returned the first
  non-prerelease in list order -> "no update" for every v6 install
  until the next v6 release ships
- the RSS rate-limit fallback returned the first feed entry matching the
  channel, same failure, both channels
- the unattended updater trusted /releases/latest, which GitHub defines
  as most-recently-created stable -> timer no-ops in the same window

All three now select the highest eligible version: the API path tracks
newest stable + newest prerelease in one pass (prerelease = GitHub flag
OR tag shape), the feed fallback picks the max matching the channel, and
pulse-auto-update.sh scans /releases pairing each tag with its own
draft/prerelease flags before the fail-closed shape filter, keeping
/releases/latest as fallback. helm-chart-* tags fail semver parsing and
are dropped everywhere.

Verified not broken (pinned by new tests): rc-channel installs are
offered the newer stable (6.0.0-rc.6 -> 6.0.5) and move onto the next rc
line when one opens (-> 6.1.0-rc.2); the auto-update prerelease filter
(fail-closed is_prerelease_tag, metadata-flag refusal) still refuses
prerelease targets on stable. Go tests fail on the pre-fix code
(returned 5.1.37 / "no update").

Contract: deployment-installability now pins version-max release
selection with proofs in manager_stranded_upgrade_test.go and
pulse_auto_update_test.go.
2026-07-17 23:21:49 +01:00
rcourtman 817aaeabc1 feat(ai): surface the Assistant at first AI setup and fix blessed-path readiness
Telemetry shows thousands of installs configure an AI provider but almost
none ever use the interactive Assistant. A live first-session exercise
(fresh install, Ollama qwen3:8b quickstart) found why: after enabling,
nothing changes on screen — the launcher and handoff buttons are gated on
sessionCapabilities.assistantEnabled, which was only read at page load;
the empty transcript was blank; and the blessed Ollama+qwen3:8b path
reported Patrol degraded while telling the user to pull the model they
had just selected.

- Setup-modal success now opens the Assistant drawer, and the AI settings
  save paths refresh the assistantEnabled capability in place
  (aiChatStore.refreshEnabledFromServer) so entry points appear without a
  reload; toasts point at the Assistant instead of back at settings.
- The empty transcript owns a plain-language welcome and three suggested
  prompts that dispatch as real turns (ASSISTANT_SUGGESTED_PROMPTS).
- Patrol static readiness: the blessed Ollama Patrol model is Ready;
  other Ollama models keep the warning, now naming the selected model.
- pulse_summarize fleet argument errors instruct the model to enumerate
  resources itself instead of interrogating the operator (observed live:
  'how is my machine doing?' ended in a resource-ID elicitation).

Contracts: ai-runtime and frontend-primitives Current State updated.
Tests: full internal/ai + internal/api suites green; vitest ChatMessages,
AISettings, aiChat store, and settingsArchitecture suites green; flow
verified live end-to-end.
2026-07-17 23:20:44 +01:00
rcourtman 0f15de6a4a fix(telemetry): suppress outbound pings while mock mode is enabled
Every mock-mode boot (e2e/CI managed backends, qual runs, demo containers)
was sending startup/heartbeat pings describing the synthetic fixture fleet,
registering as real installs in the license-server telemetry DB and
dominating the VMware/Kubernetes/TrueNAS adoption aggregates (346 of 6,225
weekly-active installs matched the mock signature, incl. 120 of 227 on
6.1.0-rc.1).

- internal/telemetry: route startup and heartbeat sends through sendEvent,
  which drops the ping while mock.IsMockEnabled() is true; checked per event
  so runtime mock toggles take effect immediately. Disclosed in the package
  doc and both PRIVACY.md copies.
- scripts/telemetry_adoption_report.py: exclude historical mock-fleet rows
  (kubernetes_pods = 120×N with vmware_hosts = 7×N, the internal/mock
  fixture template) by default, report the excluded row/install counts, and
  add --include-mock-fleet to audit them. Versions through 6.1.0-rc.2 keep
  pinging the mock fleet until upgraded, so report-side filtering stays
  necessary.
- security-privacy contract: record the mock-mode suppression boundary and
  the report-side mock-fleet exclusion as subsystem invariants.
- Tests: Go sendEvent suppression/sending pair against an httptest endpoint;
  Python signature-matcher and summarize_rows exclusion coverage.
2026-07-17 18:19:18 +01:00
rcourtman 5abb2d8f48 refactor(agentexec): dedupe host operation dispatch and receipt-identity validation
Contract-Neutral: dupl lint dedupe refactor; no behavior or contract delta (typed host-operation dispatch and receipt-identity validation extracted to shared generic helpers)
2026-07-17 17:36:17 +01:00
rcourtman 488731aed3 refactor(hostagent): dedupe the host APT operation handlers behind shared begin/complete helpers
Contract-Neutral: dupl lint dedupe refactor; no behavior or contract delta (host APT handler admission prefix and terminal suffix extracted to shared helpers)
2026-07-17 17:34:01 +01:00
rcourtman a2bcf2e821 refactor(config): dedupe the activity-history record cycle behind a generic helper
Contract-Neutral: dupl lint dedupe refactor; no behavior or contract delta (activity-history record cycle extracted to generic helper)
2026-07-17 17:31:35 +01:00
rcourtman b862723b93 refactor(ai): dedupe guest lifecycle snapshot loops and check the tool-loop helper return
Contract-Neutral: dupl/errcheck lint dedupe refactor; no behavior or contract delta (findings_proxmox_lifecycle snapshot loops extracted to shared helper)
2026-07-17 17:29:22 +01:00
rcourtman 52eaf49444 style(operationreceipt): goimports the sqlite blank import 2026-07-17 17:24:57 +01:00
rcourtman efe1526aa8 refactor(api): dedupe the dupl lint pairs behind shared helpers
make lint (dupl) flagged two clone pairs: shouldRestartAIChat vs
aiSettingsUpdateTouchesPatrolReadiness shared the forty-field provider
config block, and the approved/rejected action decision-ID collectors in
Pulse Intelligence telemetry differed only by lifecycle state and audit
predicate.

Extract aiSettingsUpdateTouchesProviderConfig for the shared field
check and parameterize pulseIntelligenceActionDecisionIDs by state,
label, and audit predicate, keeping the two named wrappers. The
decision-telemetry contract tests now pin the wrapper call sites, which
still fix the lifecycle state and audit predicate per decision kind.

Contract-Neutral: behavior-preserving dedup of internal helpers to clear dupl lint findings; no public-contract delta
2026-07-17 17:02:24 +01:00
rcourtman 18cb497de1 test(unifiedresources): drop the retired run-command validator pin
The upstream removal of HandleRunCommand's dead legacy body (ec5e44da0)
deleted TestNormalizeRunCommandApprovalTarget with it, but the v6
host-alias standards test still required that test's snippet in
ai_handlers_test.go, so TestV6DirectHostAliasValidatorCoverage failed.
Drop only the retired validator's snippet; the surviving execute and
investigate-alert validator pins keep host-alias rejection covered.
2026-07-17 17:00:12 +01:00
rcourtman e60ae5fc48 style(unifiedresources): goimports the action dispatch store test
Move the blank modernc.org/sqlite driver import into its own group so
the file passes the goimports formatting check in make lint.
2026-07-17 16:58:56 +01:00
rcourtman ec5e44da0c refactor(api): drop dead body of retired HandleRunCommand
The raw-command endpoint answers 410 Gone unconditionally; the old
implementation below the retirement return was unreachable (govet) and
kept normalizeRunCommandApprovalTarget, normalizeAIExecuteTargetType,
and AIRunCommandRequest alive with no live callers. Remove them and the
tests that only exercised the removed helpers.

Contract-Neutral: remove unreachable dead code below the 410 retirement return in HandleRunCommand; endpoint behavior unchanged
2026-07-17 16:39:40 +01:00
rcourtman a6f0f9a2ae fix(mock): evaluate docker host alerts in mock mode
checkMockAlerts checked guests, nodes, storage, PBS, and PMG but never
looped over the mock estate's DockerHosts, so no docker container state,
health, metric, update, or swarm service alert could ever fire against
mock data. The generator deliberately keeps one container unhealthy per
estate, and the docker alert lifecycle (disable-all sweeps, update-alert
delay, config reconciliation) was unexercisable in the mock QA surface.

Verified live on a scratch mock instance: canonical docker metric alerts
fire, the Containers global-defaults toggle clears all of them via the
DisableAllDockerContainers sweep (the #1545 fix path), and re-enabling
re-fires them on the next cycle.

Contract-Neutral: mock-mode docker alert evaluation wiring fix; no public contract delta
2026-07-17 16:09:06 +01:00
rcourtman 7675f57349 test(monitoring): pin the recovery-point reconcile wiring and availability fixed-interval provider
Release-level regression pass over the alert lifecycle found two seams
with store/scheduler-level coverage but untested wiring:

- ingestRecoveryPointsBestEffort (#1580): a complete enumeration deletes
  in-scope points the source stopped reporting, an empty enumeration
  clears the scope, and a failed upsert skips reconciliation so a broken
  batch cannot delete points the source still reports.
- availabilityPollProvider.FixedInstanceInterval (#1582): the configured
  poll interval reaches the scheduler descriptor exactly (with the
  10s..1h clamp), 0 for disabled or unknown targets, so the adaptive
  scheduler cannot collapse the availability detection window again.
2026-07-17 16:09:06 +01:00
rcourtman 562417f847 fix(actions): make the agent-too-old refusal reason actionable
'This agent does not support durable typed-operation receipts' told users
nothing they could act on. Say the agent is on an older version and how to
resolve it. Applies to docker container, host update and host storage
cleanup executors.
2026-07-17 12:05:07 +01:00
rcourtman b75c181888 fix(alerts): clear canonical docker alerts when disable-all toggles are set
The DisableAllDockerContainers and DisableAllDockerServices config sweeps
matched only legacy docker-container-/docker-service- alert ID prefixes.
Canonical stateful alerts (image-update, state, health, replica-gap) are
stored under <resourceID>::<specID> state IDs, so existing alerts survived
the toggle and lingered indefinitely, re-notifying past cooldown.

Match on the docker: resource ID scheme as well as the legacy prefixes.
2026-07-17 12:03:00 +01:00
rcourtman fe09420bcb Add coverage tests for agentexec lifecycle codec and aicontracts pure helpers
The docker container lifecycle codec in agentexec had a contract test that never
exercised the codec functions at runtime, leaving decode, bind, validate and
identity helpers at zero coverage. This adds a table-driven test taking each to
near or full coverage including every validation error arm. In aicontracts,
CloneActionReference, IsNilAlertPayload and DefaultEngineConfig were uncovered,
so this covers the nil and populated arms and asserts CloneActionReference
produces an independent deep copy.

Both are new test-only files. No source changed. Verified with go test, gofmt
and go vet, with each target function confirmed to move off zero coverage.
2026-07-17 06:53:16 +01:00
rcourtman 2d38cfb9ef Stabilize legacy license exchange test 2026-07-17 00:27:56 +01:00
rcourtman 76201e89ce Add deep-copy independence and qualification render coverage tests
Two more newly added *_w0716_coverage_test.go files from the same
branch-coverage wave, touching no source and no existing test.

- internal/models deep-copy clone independence for Docker, Kubernetes,
  ZFS and PBS clones, each mutating the original and asserting the clone
  stays unchanged
- internal/ai/qualification lab shell-quoting and resource-name
  rendering plus report replay predicates and markdown renderers
2026-07-16 22:56:24 +01:00
rcourtman e23448df7b Allow Patrol to record sequential findings 2026-07-16 22:50:11 +01:00
rcourtman c2b8d6b9a0 Add pure branch-coverage tests for the fresh Patrol and Docker drop
Cover previously-unexecuted branches in modules that gained behavior
since the last wave. All six are newly added *_w0716_coverage_test.go
files that modify no source and no existing test.

- internal/ai/chat investigation-budget injection plus the agentic
  tool-event and string helpers
- internal/ai/qualification manifest Validate error arms and the Patrol
  docker-predicate and finding-prerequisite validators
- internal/unifiedresources adapters transforms and ContractResourceType
- internal/config AIConfig nil-receiver default branches
- pkg/aicontracts orchestrator error methods and investigation config
2026-07-16 22:27:29 +01:00
rcourtman 72c409743d Warn when two machines share one Docker agent identity
Cloned VMs that keep the same /etc/machine-id collapse into a single
Docker host in Pulse, with each clone's report silently overwriting the
other (#1584). The server now tracks per-identity hostname and machine
ID observations and flags a conflict when a value flaps back to one
already seen inside a 15-minute window, a signature a one-time rename
never produces. The conflict rides the DockerHost model through unified
resources, and the Docker page shows a warning naming the flapping
hostnames with the machine-id remedy. The warning self-clears once one
clone stops reporting for the window.
2026-07-16 20:55:09 +01:00
rcourtman 4d80e53588 Align restart-loop qualification with Patrol threshold 2026-07-16 20:53:02 +01:00
rcourtman cae8477128 Preserve Patrol replay safety outcomes 2026-07-16 20:30:37 +01:00
rcourtman afbcd3023c Bound automatic Patrol inference allowances 2026-07-16 19:55:44 +01:00
rcourtman 2f6bb94ed3 Classify legacy-CIS-only vCenters as unsupported version
A vSphere 6.x target fails the connect test at the Automation session
step with a generic "HTTP 500" message, because /api on those releases
routes to the JSON-RPC servlet. When /api/session fails with a non-auth
status, probe the legacy /rest CIS session API with the same
credentials; if that login works the target predates the JSON APIs
Pulse uses, so the test now reports the unsupported-version warning
naming the vCenter 8.0U1 floor instead of pointing at credentials. The
probe deletes the session it creates. Also reword the frontend guidance
for that category in plain terms.

Related to #1585.
2026-07-16 19:52:14 +01:00
rcourtman fda64e0318 Negotiate registry pull tokens from WWW-Authenticate challenges
Update checks against registries without a hardcoded token endpoint
(lscr.io and other spec-compliant registries) failed with a blanket
"Check failed" because the manifest HEAD went out anonymously and the
401 was terminal. Parse the Bearer challenge on the 401, fetch a pull
token from the advertised realm, and retry once. Token endpoints that
answer with access_token instead of token are now accepted too.

Fixes #1583.
2026-07-16 19:50:04 +01:00
rcourtman 676117bf27 Separate Patrol evidence and model-turn budgets 2026-07-16 17:50:08 +01:00
rcourtman b6e5f4d512 Align remediation qualification with Patrol policy 2026-07-16 17:01:37 +01:00
rcourtman 1397f72c1f Propagate Basic auth identity into action authorization 2026-07-16 16:34:26 +01:00
rcourtman 524aa16233 Add Go branch-coverage tests for monitoring cluster helpers and ai-tools formatting
Adds branch-coverage unit tests for previously uncovered pure functions in
internal/monitoring and internal/ai/tools. New test files only, with no source
changes.

Covers existing-cluster IP-override and fingerprint lookup, storage-summary
capacity-trend building, nouveau GPU temperature parsing, resolved-resource
control identity, and available-agent-host formatting. 32 TestBranchCov
functions in 2 files, all vet and gofmt clean.
2026-07-16 16:25:10 +01:00
rcourtman b91ba759c9 Route Claude native tool intent through Pulse 2026-07-16 16:09:48 +01:00
rcourtman d162110233 Make remediation fixtures model-neutral 2026-07-16 15:44:19 +01:00
rcourtman df59f210e5 Prevent mixed-auth configuration deadlock 2026-07-16 15:25:47 +01:00
rcourtman fb9584aae6 Add Go branch-coverage tests for alerts monitoring model-resolution ai and licensing helpers
Adds table-driven branch-coverage unit tests for previously uncovered pure
functions across internal/alerts/config, internal/ai/modelresolution,
internal/monitoring, internal/ai, internal/ai/qualification and pkg/licensing.
New test files only, with no source changes.

Covers per-subsystem alert-default normalization, configured model and provider
resolution, fleet-doctor identity and cluster-endpoint helpers, docker-state and
infrastructure-key mapping, investigation and SMART issue helpers, JSON and
autonomy normalization, and licensing feature-tier resolution. 65 TestBranchCov
functions in 7 files, all vet and gofmt clean.
2026-07-16 15:17:45 +01:00
rcourtman 43e70411a6 Normalize bounded Docker action clock skew 2026-07-16 14:36:01 +01:00
rcourtman df4e02ef89 Add Go branch-coverage tests for models config securityutil and aicontracts helpers
Adds table-driven branch-coverage unit tests for previously uncovered pure
functions across internal/models, internal/config, pkg/securityutil and
pkg/aicontracts. New test files only, with no source changes.

Covers model collection normalizers, reflect-based metric deep-copy,
removed-host agent bookkeeping, report-schedule and entitlement-billing
normalization, effective AI control and Patrol autonomy resolution,
websocket-origin host normalization, and command-safety classifiers. 61
TestBranchCov functions in 8 files, all vet and gofmt clean.
2026-07-16 14:20:13 +01:00
rcourtman cde850c686 Isolate Patrol investigation dispatch from Watch runs 2026-07-16 14:06:01 +01:00
rcourtman d8557c9256 Shorten fake Google API key fixtures below secret-scanner shape 2026-07-16 13:40:38 +01:00
rcourtman e1e1048e08 Preserve durable Patrol outcomes across Claude wrapper retries 2026-07-16 12:50:15 +01:00
rcourtman 6c8ecd2284 Constrain plain Claude completion fallback 2026-07-16 12:45:24 +01:00
rcourtman 1439495e70 Accept safe plain Claude completion turns 2026-07-16 12:42:12 +01:00
rcourtman 043ecf4c66 Stabilize Claude subscription completion transport 2026-07-16 12:38:19 +01:00
rcourtman 0719e34255 Bind subscription tool calls to native schemas 2026-07-16 12:29:14 +01:00
rcourtman fddcc3fefe Honor Patrol lifecycle tool ordering 2026-07-16 11:39:23 +01:00
rcourtman 07cbdd792b Separate subscription adapter trust boundaries 2026-07-16 10:44:53 +01:00
rcourtman 47f2bdaed4 Add Go branch-coverage tests for pure config and helper packages
Adds table-driven branch-coverage unit tests for previously untested pure
helper functions across internal/alerts/config, internal/config,
internal/ai/safety, internal/ai/modelresolution, internal/operationreceipt,
internal/models, internal/securityutil and pkg/securityutil. New test files
only, with no source changes.

Covers alert-config normalization and validation, sensitive-path and
redaction classifiers, URL normalizers, provider model resolution,
operation-receipt decoding, credential masking, and account-to-org role
mapping. 57 TestBranchCov functions in 12 files, all vet and gofmt clean.
2026-07-16 10:19:51 +01:00
rcourtman 4dcba3a1d8 Name the actual OIDC token verification failure on the login page
Every ID token verification failure rendered the issuer-mismatch
advice, sending users with audience or clock problems down the wrong
path and telling users with a genuine issuer mismatch nothing they had
not already checked (#1533). Map the distinct verification failures
(issuer, audience, expiry) to their own error codes and give each
accurate login-page copy pointing at the server log's got/want detail.
2026-07-16 10:15:42 +01:00
rcourtman 506d2e5b7e Serve agent config from continuity state during reload windows
A full monitor reload tears the state down and rebuilds it empty, so
agent-reported host rows vanish until each agent's next report lands.
Config fetches in that window 404ed with a perfectly valid token, which
showed up as rare correlated agent_config_fetch failures across
unrelated agents (#1570).

When the live snapshot has no match, resolve the host from the
persisted continuity store with the same semantics as the live path. A
report-scoped token resolves only its bound host and manage-scoped
tokens resolve by host ID. Deliberately removed hosts stay 404 because
removal deletes their continuity entry.
2026-07-16 10:13:14 +01:00