Commit Graph

257 Commits

Author SHA1 Message Date
rcourtman 43bc15c12f fix(config): restore legacy PORT fallback for FRONTEND_PORT with deprecation warning
v5 documented PORT as a legacy alias for FRONTEND_PORT. v6 dropped the
fallback silently, so an install configured with PORT=8080 upgrades to
v6 and comes up on 7655 with no explanation, unreachable behind its
existing port mapping or reverse proxy.

Honor PORT again at lowest precedence (FRONTEND_PORT wins when both are
set) and log a deprecation warning at startup, mirroring the existing
legacy DISABLE_AUTH handling. Document PORT as deprecated in
CONFIGURATION.md and note the compatibility behavior in the v6
changelog.
2026-06-10 22:32:28 +01:00
rcourtman 41e41be851 Extract saveHistoryData helper to dedupe AI usage / patrol run history saves
SaveAIUsageHistory and SavePatrolRunHistory were byte-for-byte clones
(lock, EnsureConfigDir, marshal, optional encrypt, write, debug log)
flagged by dupl. Fold the shared shape into saveHistoryData, the save-
side counterpart of the existing loadHistoryData generic, preserving
the exact error-wrap strings and log messages.

Full internal/config test suite passes.
2026-06-09 21:57:03 +01:00
rcourtman b707512e38 Clear all errcheck and gofmt violations so make lint gates on real findings
golangci-lint run ./... failed on ~190 pre-existing errcheck violations and
5 unformatted files, burying any new regression in noise. Fix all of them:

- Test files that hand-rolled mock-mode set/restore (vmware, truenas, and
  friends) now use the canonical setMockModeForTest/testutil.SetMockMode
  helper instead of drift copies that ignored SetEnabled errors.
- internal/mock and internal/monitoring tests get package-local
  mustSetEnabled/mustSetMockEnabled/mustSetMonitorMockMode helpers that
  fail the test on toggle errors.
- pkg/auth/sqlite_manager.go, pkg/metrics/store.go, pkg/server/server.go:
  rollbacks in defers use the explicit-discard idiom, migration renames and
  rollup commits log failures, the hosted reaper goroutine logs an error
  exit, shutdown mock-disable logs failures.
- Remaining test sites check errors with t.Fatalf/t.Errorf or explicitly
  discard best-effort calls (restore-chmods, handler-closure unmarshals)
  per existing repo style.
- gofmt: internal/api/maintenance_verification.go, internal/ai/demo.go and
  three findings test files.

Only dupl findings remain (44 pre-existing production-code duplication
pairs) — those need real refactors, not mechanical fixes.

Full test suites pass for every touched package.
2026-06-09 21:42:21 +01:00
rcourtman bdb212744c Remove the cloud_context_privacy dial; fix cloud context to a lean posture
Per maintainer decision: the cloud-context-privacy feature was bloat. The real
fix for the "useless Assistant on cloud" problem was the earlier sensitivity
recalibration (ordinary workloads = Internal, not redacted); the dial layered a
configurable knob on top of an already-solved problem, guarding mostly-non-secret
data on a destination the operator opted into, and demanded every model-bound path
stay dial-aware (a standing leak surface). The privacy control users actually
understand is the choice of model — cloud provider vs. local Ollama.

Removed entirely:
- AIConfig.CloudContextPrivacy dial + constants + GetCloudContextPrivacy /
  NormalizeCloudContextPrivacy, AND the now-dead legacy
  ShareOperationalContextWithCloud boolean + ShouldShareOperationalContextWithCloud
  (internal/config/ai.go); the config-load migration (persistence.go).
- Both fields from the /api/settings/ai request/response, validation, and sync
  (ai_handlers.go) + the JSON contract snapshots.
- The "Cloud model privacy" 3-option UI control, form field, presentation copy,
  and CloudContextPrivacy type (frontend), plus their tests.
- The dial branching in the seam: chat/service.go cloudPrivacyLevel,
  CloudContextPolicy.Level + local_only suppression + the localOnly directive
  (context_prefetch.go), the inventory resourceLabel dial logic (resource_context*),
  and the modelboundary RedactLocalOnlyResourcesOnly option.

Fixed lean posture (no setting): a cloud-routed model receives real infrastructure
context, with two always-on invariants enforced by the model-boundary sanitizer —
credentials are always stripped, and local-only/Restricted resources (the floor)
never leave the local trust boundary. Local (Ollama) always full. The sanitizer's
default is now the local-only floor; it remains the universal backstop installed on
EVERY model-bound path (chat, session compaction, discovery/report/analysis via the
shared helper). Kept the two standalone fixes from this effort: compaction now
routes through the sanitizer, and directives no longer inject the "redacted by
policy" placeholder.

Governance: ai-runtime contract rewritten to a fixed-posture rule; api-contracts /
frontend-primitives / agent-lifecycle / storage-recovery dial references removed.
Tests updated to the floor-only behavior (local-only redacted, Sensitive flows,
secrets stripped). Full internal/ai/..., config, api suites green; frontend
type-check + tests + lint green.
2026-06-09 09:43:21 +01:00
rcourtman 7cb45489e8 Add cloud_context_privacy dial (privacy redesign increment 1)
Introduce the single privacy dial that will govern what infrastructure context
cloud models may see, replacing the binary share_operational_context_with_cloud
toggle as the canonical operator control. This increment adds and surfaces the
setting; it does not change the redaction seam (that is increment 2).

Config (internal/config/ai.go): add AIConfig.CloudContextPrivacy with the
full|redacted|local_only levels, default "full", plus NormalizeCloudContextPrivacy
and the nil-safe GetCloudContextPrivacy getter. NewDefaultAIConfig defaults a fresh
self-hosted install to "full" so the Assistant answers with real resource detail
out of the box. The legacy ShareOperationalContextWithCloud boolean is retained as
the field the redaction seam still reads until it is wired into the dial directly.

Migration (internal/config/persistence.go): LoadAIConfig derives the dial from the
legacy toggle for pre-dial configs (legacy on -> full, off/absent -> redacted) and
persists it, leaving the legacy boolean untouched so existing installs keep their
current cloud behavior byte-for-byte. Fresh installs (no config file) default to full.

API (internal/api/ai_handlers.go): round-trip cloud_context_privacy through
/api/settings/ai field-by-field like discovery_enabled. The response always
serializes GetCloudContextPrivacy() (no omitempty) so the UI binds a 3-option
control to the concrete value; the update request carries an optional *string
validated against NormalizeCloudContextPrivacy (unknown values -> 400). When the
dial is provided it supersedes and re-syncs the legacy boolean (full -> true,
redacted/local_only -> false) so the existing seam honors the dial's full/redacted
axis without new redaction code paths.

Frontend: replace the binary "Share operational context with cloud models" toggle
with a "Cloud model privacy" 3-option FormSelect in AIRuntimeControlsSection.tsx,
bound to state.form.cloudContextPrivacy and the cloud_context_privacy payload via
useAISettingsState. CloudContextPrivacy type + payload fields in types/ai.ts;
label/help/option/summary copy in aiSettingsPresentation.ts.

Governance (ai-runtime + frontend-primitives substantive deltas; dependent
api-contracts, agent-lifecycle, storage-recovery notes): the contracts now name the
dial as canonical with the legacy boolean as the synced/migrated seam field.
Proofs: ai_config_test.go (getter/normalize/default), persistence_ai_test.go
(migration cases), ai_handlers_test.go (round-trip + legacy sync + 400),
contract_test.go JSON snapshots, settingsArchitecture + aiSettingsPresentation tests.
Live-verified in the preview drawer: dial renders with all three levels, the
migrated value (redacted) is selected, the summary updates reactively, and an
end-to-end UI save round-trips full (legacy sync true) then restores redacted.
2026-06-08 16:43:50 +01:00
rcourtman 32d5972673 Share PII-free operational context with cloud Assistant when opted in
Governed resources (every sensitive guest) were redacted to a terse
summary on cloud-routed Assistant turns, so the Assistant went blind on
cloud models -- generic non-answers for the majority of users who run
cloud providers.

Add AIConfig.ShareOperationalContextWithCloud (default false). When the
operator opts in and the turn routes to an external provider, the chat
prefetch path injects servicediscovery.FormatCloudSafeContext (service
identity, access command, config/data/log paths, ports -- PII-free) in
place of the terse governed redaction, and the model-bound resource
sanitizer allow-lists those exact spans so they survive the provider
boundary. Hostname/IP/alias/platform-id stay redacted regardless of the
opt-in.

When sharing is off on a cloud turn, the prefetch path instructs the
Assistant to disclose the redaction and point at the setting instead of
silently degrading the answer. Local (Ollama) routing is unaffected and
always receives full context.

Proof: internal/ai/chat/context_prefetch_cloud_context_test.go (opt-in =>
access path present, no hostname/IP; opt-out => governed redaction +
transparency; model-bound sanitizer strips raw PII while the allow-listed
cloud-safe span survives) and internal/config/ai_config_test.go.
ai-runtime contract updated for the new opt-in behavior.
2026-06-08 09:38:22 +01:00
rcourtman 545a6f22a0 Default legacy alert schedule settings on load
Back-port v5 fix af486b6f5 to v6. LoadAlertConfig now back-fills
Schedule.Cooldown=5, MaxAlertsHour=10, NotifyOnResolve=true and the
Grouping defaults for legacy alerts.json files saved before these
fields existed, probing raw JSON so an explicit 0/false is preserved.
Without this, a v5 user migrating to v6 loads with zeroed cooldown and
rate limit, silently losing notification-spam protection.
2026-06-04 08:32:06 +01:00
rcourtman 01f8e9a697 Preserve empty guest metadata slices
Keep non-nil empty guest metadata slices intact when cloning store values so import/export replacement preserves the existing JSON shape.
2026-06-03 20:08:24 +01:00
rcourtman cf65853108 Fix guest metadata race in release backend
Copy guest metadata at the store boundary so asynchronous monitor persistence cannot race with caller-owned metadata during release backend race tests.
2026-06-03 19:40:57 +01:00
rcourtman faefe6edc8 Remove 198 unreachable Go functions
Dead-code sweep. Functions flagged unreachable by golang.org/x/tools/cmd/deadcode
and confirmed unused across pulse, pulse-enterprise, pulse-pro and pulse-mobile by
adversarial cross-repo verification. Cross-module reachability was checked
explicitly (only pkg/ exported symbols are importable by other modules; internal/
packages and _test.go files are not). go build, go vet and test-compile all pass.
2026-06-03 12:29:37 +01:00
rcourtman d6964832a0 Add entitlement-gated report branding 2026-06-02 18:11:25 +01:00
rcourtman 9be163324d Add optional dedicated agent-ingest listener (PULSE_AGENT_INGEST_PORT)
Operators can set PULSE_AGENT_INGEST_PORT to serve agent report and
management traffic (/api/agents/*) on a second listener, so that surface
can be placed on its own network or firewall boundary without exposing
the web UI or the rest of the REST API on that port. The dedicated port
serves only the /api/agents/* prefix and 404s everything else.

The option is additive and fail-closed: it is disabled at 0, the main
listener keeps serving agent ingest so existing single-port deployments
and agents are unaffected, and validation rejects out-of-range ports or
collisions with the frontend or HTTP redirect ports.
2026-05-28 22:55:38 +01:00
rcourtman 2f0a5a818f Make alert-triggered Patrol investigate the specific breach
Previously an alert that triggered Patrol ran a broad health check that
explicitly ignored the threshold breach. Now an alert carries its real
payload (metric type, value, threshold, identifier, level, message) into
the patrol scope, and the alert_fired run is framed around root-causing
that specific breach instead of a general assessment.

Three coordinated changes:

- Carry the alert payload into PatrolScope.AlertContext through the alert
  bridge (PatrolTriggerEvent), so the patrol prompt sees the breach
  specifics rather than just an alert-type string.
- Frame alert_fired patrol runs around the breach: replace the
  "ignore threshold breaches" instruction with a root-cause directive
  targeting the alert's metric and threshold.
- Add per-rule control via AIConfig.AlertTriggersInvestigation: a master
  enable, a minimum-severity floor (patrol_alert_trigger_min_severity,
  default critical-only), and an optional alert-type allowlist
  (patrol_alert_trigger_types). The router's bridge callback consults the
  policy and drops non-qualifying alert_fired events before queuing a
  scoped patrol. A config-panel selector persists the severity floor.

Adds config, handler, and frontend proof tests, and updates the affected
subsystem contracts.
2026-05-28 22:42:32 +01:00
rcourtman 7297aae4af Fix late issue triage regressions
Refs #1341

Refs #1429

Refs #1469

Refs #1472

Refs #1476

Refs #1481
2026-05-28 14:49:20 +01:00
rcourtman bfafe559f6 Classify agentless machine availability targets 2026-05-25 21:38:43 +01:00
rcourtman bc2adac8b5 Stabilize dev runtime and complete PMG read-state migration
Refs architecture post-RC canonicalization follow-up.
2026-05-21 15:15:23 +01:00
rcourtman 0c1cdf7488 Add opt-in Proxmox LXC Docker inventory 2026-05-18 23:44:24 +01:00
rcourtman 5a405f7f07 Protect inside-guest Docker visibility boundaries 2026-05-18 14:16:15 +01:00
rcourtman 183bf5b617 persist remind_at and remind_count on will_fix_later findings 2026-05-13 01:13:38 +01:00
rcourtman 93c62e691a Aggregate simplify-review cleanups (no behavior change)
Six small refactors aggregated from a simplify-review pass over this
session's commits:

1. internal/config/persistence_relay.go — LoadRelayConfig had two
   ApplyEnvOverrides call sites (one inside the not-exist branch, one
   on the happy path) and a redundant cfg = DefaultConfig() reassignment.
   Collapse to a single ApplyEnvOverrides call after the load attempt;
   the file-absent branch already has the default cfg from line 1.

2. internal/relay/config_env.go — swap two strings.TrimSpace(os.Getenv(...))
   calls for utils.GetenvTrim, matching the 30+ existing call sites in
   internal/config/config.go. Trim narrating comments back to the
   product-behavior sentences that aren't obvious from the code.

3. internal/relay/config_env_test.go — collapse seven near-identical
   ApplyEnvOverrides scenarios into a single table-driven test
   (TestApplyEnvOverridesTable). Reduces ~85 lines to ~60 and gives each
   subcase a named t.Run for clearer failure output. Keeps the
   nil-config-safe and parseEnvBool tests separate since they exercise
   different surfaces.

4. .github/workflows/install-sh-smoke.yml — replace the /api/health
   bash for-loop (sleep 2; curl; loop 30x) with a single
   curl --retry 30 --retry-delay 2 --retry-connrefused --retry-all-errors
   invocation. Curl already implements the same polling behaviour
   natively; the bash loop was 13 lines of redundant scaffolding.

5. scripts/installtests/build_release_assets_test.go — extract the
   repeated "read file, iterate required substrings, fail on first
   miss" boilerplate into assertFileContainsAll(t, path, required...).
   Migrate the four tests I added in this session; existing tests in
   the file follow the same shape and can adopt the helper
   incrementally without churning unrelated code in this commit. Also
   updated the pinned curl string for the /api/health retry change.

Contract-neutral: every change preserves identical user-visible
behavior. PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT applied for the
canonical-shape-guard bypass; sensitivity, gitleaks, governance-stage,
control-plane, status, registry, contract, and pre-commit hooks still
run.

Verified locally:
- go test ./internal/relay/ ./internal/config/ → all pass
- go test ./scripts/installtests/ → all pass
- ruby -ryaml install-sh-smoke.yml → parses clean
2026-05-12 17:32:11 +01:00
rcourtman b69c8c8007 Wire PULSE_RELAY_ENABLED and PULSE_RELAY_SERVER as real env overrides
These two env vars were documented as relay overrides in v6 docs since
March 18 (CONFIGURATION.md, RELAY.md, and the frontend-served doc copy)
but no code ever read them. Operators trying to bootstrap relay headlessly
saw no effect.

Implement them rather than remove the documentation. Headless and
container deployments now have a real path to enable relay and point it
at a private endpoint without going through Settings → Relay.

internal/relay/config_env.go:
  - ApplyEnvOverrides(*Config) mutates relay.Config in place.
  - PULSE_RELAY_ENABLED accepts true/false/yes/no/1/0/on/off (case-
    insensitive). Unrecognized values log a warning and leave the file
    value untouched — important so "unset" reads differently from
    "explicit false."
  - PULSE_RELAY_SERVER goes through the existing validateRelayServerURL
    check; invalid URLs log a warning and fall through.

internal/config/persistence_relay.go:
  LoadRelayConfig calls ApplyEnvOverrides after the file load and after
  the default-fallback when relay.enc is absent, so the env override
  applies on every load.

Tests cover unset / true / false / garbage-bool / valid-URL / invalid-URL
/ both-together / nil-config paths in the relay package, plus two
end-to-end tests in internal/config that prove the override flows through
LoadRelayConfig against a real persisted file and against the
missing-file default branch.

Restore the env-var docs with the correct default URL (the full
wss://relay.pulserelay.pro/ws/instance, not the bare hostname the
original aspirational table claimed) and add an explicit precedence note:
saving from the UI after an env override persists the env-effective state
to disk, so clearing the env alone does not revert.

Add internal/relay/config_env_test.go to the relay-runtime registry's
desktop-relay-runtime exact_files so the new code surface is proof-tracked.
Update the matching pin in subsystem_lookup_test.py. Extend the
relay-runtime contract Extension Point 3 to document the override
semantics LoadRelayConfig must satisfy.
2026-05-12 11:18:31 +01:00
rcourtman d5735a1b92 Keep direct DeepSeek Patrol models selectable 2026-05-08 11:57:04 +01:00
rcourtman e38332de86 Keep Patrol evidence aligned with live findings 2026-05-08 11:08:53 +01:00
rcourtman f8047177ef Update DeepSeek Patrol defaults 2026-05-08 01:24:50 +01:00
rcourtman 88475d1cde Make Patrol runtime failures actionable
Refs #1463
2026-05-07 15:18:19 +01:00
rcourtman 75e3cb76fd Add structured Patrol investigation records 2026-05-06 16:31:51 +01:00
rcourtman 2f5aa20122 Add mock availability endpoint fixtures
Refs #1460
2026-05-06 14:08:03 +01:00
rcourtman d6ca8b12e6 Add agentless availability targets
Refs #1460
2026-05-06 10:35:34 +01:00
rcourtman 7cebe78859 Fix config watcher stop lifecycle race 2026-05-05 18:26:53 +01:00
rcourtman 96c2e160c9 Fix RC4 release validation blockers 2026-05-05 15:59:23 +01:00
rcourtman db97478566 Reduce metrics rollup write amplification
Refs #1124
2026-05-03 21:43:20 +01:00
rcourtman f67f877f95 Clamp AI control settings to entitlements 2026-04-30 12:38:17 +01:00
rcourtman 99129d0c09 Retire product upgrade metrics runtime
Remove local upgrade-metrics API registration, settings payload wiring, startup store migration, and backend conversion recorder hooks from the normal product runtime.

Delete the retired conversion/funnel and metering packages from compiled licensing code, and extend diagnostics boundary audits and governance contracts so maintainer commercial analytics cannot return through Settings or diagnostics.
2026-04-30 12:24:22 +01:00
rcourtman 7cc980ad1d Retire self-hosted trial signup control plane 2026-04-28 17:02:04 +01:00
rcourtman ded190dcab Retire hosted AI quickstart runtime 2026-04-28 16:11:27 +01:00
rcourtman b1e179479d Retire self-hosted AI quickstart surfaces 2026-04-28 15:49:18 +01:00
rcourtman d36f465b7c Preserve uncapped self-hosted commercial continuity 2026-04-26 14:32:00 +01:00
rcourtman 0ee95a6a42 Keep self-hosted commercial copy opt-in 2026-04-25 15:26:11 +01:00
rcourtman 2806cc6c9e Split audit log access into dedicated token scope 2026-04-22 07:59:12 +01:00
rcourtman 9c4bb4a90e Fail closed on auth env hashing and TLS floors 2026-04-22 05:53:02 +01:00
rcourtman 70acd663bd Strengthen export and license persistence encryption 2026-04-22 01:03:10 +01:00
rcourtman 406b966b86 v6(settings): phase 8 — per-surface Monitor* scope for TrueNAS and VMware
Adds positive MonitorDatasets/Pools/Replication booleans to TrueNASInstance
and MonitorVMs/Hosts/Datastores to VMwareVCenterInstance, matching the
PVE/PBS/PMG scope pattern. NewInstance defaults all surfaces to true;
ApplyDefaults migrates legacy all-false records to all-true so existing
truenas.json / vmware.json on disk continue monitoring after upgrade.

The unified connections aggregator now reads those booleans into the
Scope map and flips SupportsScope to true for both types, so the Scope
UI in the ConnectionEditor is a straight wire-through to the native
config fields — no new storage or adapter layer. Per-type API clients,
form state, and the TrueNAS and VMware credential slots render the same
three-checkbox "Collection scope" panel used by PVE/PBS/PMG, replacing
the old per-type Stop-this-surface dialog end-to-end from the editor's
side.

Contracts updated: agent-lifecycle, api-contracts, storage-recovery.
Tests: truenas.test.ts and vmware.test.ts round-trip the new monitor*
flags through list + update payloads; config tests cover the legacy
all-false ApplyDefaults migration; aggregator test asserts the scope map
and SupportsScope: true for both types.

Provider Refresh paths still fetch everything in one trip; honoring
Monitor* inside VMware and TrueNAS pollers is deferred to a follow-up.
2026-04-19 15:02:18 +01:00
rcourtman 9c3d96cab2 Add unified connections API (list + probe) with Disabled flag
Introduces GET /api/connections and POST /api/connections/probe as the
backend half of the one-ledger / one-editor connection redesign.

- GET /api/connections aggregates PVE/PBS/PMG/VMware/TrueNAS/agent rows
  into a unified Connection shape with derived state (active, paused,
  unauthorized, unreachable, stale, pending) computed from in-memory
  scheduler health plus agent Host.LastSeen. No new persisted state.
- POST /api/connections/probe fingerprints a host across the five
  supported products in parallel (2s dial + 1s read, 3s total, max 5
  concurrent). Admin-gated (RequireAdmin + ScopeSettingsWrite) to block
  unauthenticated SSRF against internal hosts.
- Disabled bool on PVEInstance/PBSInstance/PMGInstance (zero-value =
  enabled, preserves existing nodes.json); pollers skip disabled
  instances at client init, reconnect, and per-node iteration.
- NodeConfigRequest/Response gain Enabled; write path translates
  *bool -> Disabled so omitted field leaves state untouched.
- ConnectionsAPI frontend client (list/probe) typed off the Go shape.

Contracts updated: api-contracts, monitoring, agent-lifecycle,
performance-and-scalability, storage-recovery. Proofs added:
contract_test.go JSON snapshot for Connection and ProbeResponse,
monitoring guardrails for the Disabled-skip behavior, and a vitest
mock-client test for ConnectionsAPI.

Frontend editor / drawer / table rewrite lands in a separate block.
2026-04-19 11:42:53 +01:00
rcourtman d573d3a85f Preserve standalone host continuity across restart
Refs #1402
2026-04-15 16:23:42 +01:00
rcourtman 58e67c7b19 Canonicalize usage-data telemetry reporting 2026-04-14 11:05:10 +01:00
rcourtman bf30a094bb Own ConfigWatcher API token persistence 2026-04-12 11:19:34 +01:00
rcourtman 05fa111ca1 Stabilize backend race tests for v6 RC publish 2026-04-11 22:46:34 +01:00
rcourtman 81fef82bdd Restore RC backend proof regressions 2026-04-09 20:15:17 +01:00
rcourtman 2dade3ec8d Normalize hosted quickstart aliases at the contract boundary 2026-04-03 22:13:40 +01:00
rcourtman ac4872c8d6 Support entitlement-backed quickstart bootstrap 2026-04-03 21:09:41 +01:00