Commit Graph

276 Commits

Author SHA1 Message Date
rcourtman 917a9e5421 Add local subscription model routes 2026-07-14 22:57:38 +01:00
rcourtman 76f1084c9b Enforce server-owned Patrol Autopilot acknowledgement 2026-07-11 23:12:00 +01:00
rcourtman cd7886aa44 Enforce server-owned action approval authority 2026-07-11 19:49:17 +01:00
rcourtman c74c88fb86 Fix dispatch-time policy revocation 2026-07-11 14:29:26 +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 99dad2b511 Add a guided Ollama quickstart blessing qwen3:8b for Patrol
Ollama is the zero-cost AI path but the setup row offered only a Server
URL, and Patrol then failed on models that cannot emit tool_calls
(#1463, #847, #1425, #1152, #880). Bless qwen3:8b, the model family
Ollama's own tool-calling docs are written against, verified locally
against Patrol's real preflight: qwen3:8b emitted the tool call on
every run; qwen3:4b never did (0/4), so no low-RAM tag is suggested.

- Registry: SuggestedModel/Note/Equivalents on AIProviderDefinition,
  projected on /api/settings/ai providers; Ollama default model goes
  llama3.2 -> qwen3:8b.
- Provider row: copyable 'ollama pull qwen3:8b' block with hardware
  note, and a next-step hint when a successful test resolves a model
  outside the blessing set.
- Model resolution: exact-ID blessed preference, so pulling qwen3:8b
  makes it the auto-resolved Patrol model with no manual selection.
- Readiness copy names the blessed model (its contract pins landed
  with 94ccc7a0c's staging; this commit restores green).
- manual_ollama_preflight_test.go is the env-gated re-blessing
  harness; contracts updated for ai-runtime, api-contracts,
  frontend-primitives, and the agent-lifecycle/storage-recovery
  dependent boundaries; subsystem_lookup_test line pins follow the
  api-contracts.md insertion.
2026-07-10 13:02:26 +01:00
rcourtman f4c2fd0c38 Fail closed on unknown remediation lock state for autonomous dispatches
The AI action broker treated an unreadable operator lock as unlocked:
isResourceRemediationLocked returned (false, nil) with no audit store
wired, and the caller logged store errors then dispatched anyway. An
operator's NeverAutoRemediate=true could be silently ignored whenever
the policy store was missing or erroring, which is unacceptable while
Patrol and Assistant run at assisted or full autonomy.

Posture change at the dispatch decision point:
- isResourceRemediationLocked now reports unknown state (nil store or
  lookup failure) as an ErrRemediationLockStateUnknown-wrapped error
  instead of silently defaulting to unlocked.
- New checkRemediationLockForDispatch gate: dispatches without an
  approved human decision fail CLOSED on unknown lock state and
  surface "remediation lock state unknown; operator approval
  required". Human-approved dispatches keep the historical fail-open
  behavior with a warning log. A confirmed lock still refuses even
  approved dispatches, as before.
- executeNativeActionWithAudit (TrueNAS app start/stop/restart) now
  enforces the lock too; it previously skipped the check entirely.
- Refusals persist Failed audit records with stable
  remediation_lock_state_unknown: / resource_remediation_locked:
  ErrorMessage prefixes.
- ai-runtime subsystem contract updated to pin the new posture.

Tests cover store-error and nil-store at both autonomy postures on
both dispatch paths; routing/control tests now wire an in-memory
audit store since autonomous dispatch without one is refused.
2026-07-10 00:14:01 +01:00
rcourtman 92524e1c27 Harden CodeQL storage and integer boundaries 2026-07-09 17:37:08 +01:00
rcourtman 4382919447 Add MSP report scheduling and alert rollup 2026-07-07 20:37:18 +01:00
rcourtman 1c8a9346ef Fix legacy OIDC SSO discovery and CSP nonce
Refs #1533
2026-07-07 09:54:14 +01:00
rcourtman 6c9b4f8e8d Harden GA telemetry disclosure 2026-07-02 11:50:03 +01:00
rcourtman c0ac0762da Fix security scan findings
Harden proxy-auth admin role checks, metrics listener exposure, Teams webhook escaping, and dependency lockfiles.
2026-07-01 09:55:35 +01:00
rcourtman 229867636d Add resource priority to Patrol queue 2026-06-30 10:37:54 +01:00
rcourtman 4eac3b535c Accept ping alias for availability targets 2026-06-29 22:43:13 +01:00
rcourtman 25c93bf77f fix: availability probes fail with exec.Command fallback for local network
All TCP and HTTP availability probes were failing with 'no route to host'
because the Pulse process's Go networking stack cannot reach local subnet
IPs when a Tailscale system extension is active. The ICMP probe already
worked because it uses exec.Command('ping'). Apply the same pattern:

- probeTCP: try Go net.Dialer first, fall back to exec.Command('nc')
- probeHTTP: try Go http.Client first, fall back to exec.Command('curl')
- Both fallbacks use OS-appropriate timeout flags (macOS: -G, Linux: -w)
- HTTP probes now skip TLS verification (availability check, not identity)
- Add AvailabilityProbeHTTPS protocol constant and wire it through
  Validate, HTTPURL, Normalize, and the probe switch
- Replace bulky TooltipPortal tooltip on AvailabilityProbeCell with a
  clean native title attribute ('14 ms - checked 3s ago')

Before: 0/20 probes green (all 'no route to host')
After:  14/20 probes green (6 legitimately down hosts)
2026-06-26 17:57:40 +01:00
rcourtman 62c2b765d0 feat: attach availability checks as facet on known resource row
Agentless availability checks (ICMP/TCP/HTTP) were always minting
standalone network-endpoint resources, leaving them disconnected from
the known Proxmox/Docker guest they actually monitor. This made
availability evidence invisible on the platform resource row where the
user expects it, per the performance-and-scalability bounded-row
contract.

Backend (unified-resources ingest):
- Add LinkedResourceID field to AvailabilityData and AvailabilityTarget
- resolveAvailabilityLink: explicit link first, then exact-IP unambiguous
  correlation; skip hostname-only (lossy); guard against overwriting a
  different target's facet
- Unlinked/unmatched probes still mint network-endpoint (fallback)

Frontend:
- Relax getAvailabilityProbePresentation for any resource with availability
- Add compact protocol badge to UnifiedResourceHostTableCard name cell
- Add optional 'Link to resource' field to availability target form
- Add linkedResourceId to frontend types

Contracts: api-contracts, unified-resources, monitoring,
performance-and-scalability, storage-recovery.

Governance: coverage_gap + candidate_lane in status.json.
2026-06-26 12:16:11 +01:00
rcourtman 1f6fe16e49 Make Z.ai provider base URL user-overridable
Z.ai keys on the coding subscription get 429 'Insufficient balance' on the standard /api/paas/v4 endpoint. Add a per-provider BaseURLField to the Z.ai registry entry, a ZaiBaseURL override on AIConfig (returned by GetBaseURLForProvider when set, else the standard default), the handler request/response/apply plumbing, and a 'Custom Base URL' field on the Z.ai provider card so operators can point at /api/coding/paas/v4. Mirrors the existing OpenAI custom-base-URL override; the standard endpoint remains the default for pay-as-you-go users. Adds config/handler/frontend proofs and updates the ai-runtime, api-contracts, frontend-primitives, agent-lifecycle, and storage-recovery contracts.
2026-06-23 22:49:55 +01:00
rcourtman 09fc029bd6 Canonicalize assistant provider registry
Add a registry-backed provider model for runtime metadata and chat-compatible transports.

Support Z.ai, Groq, Mistral, Cerebras, Together, and Fireworks through the shared chat-compatible client path while keeping native providers on their existing adapters. Expose provider metadata through settings and render the expanded provider set in the frontend.
2026-06-23 19:38:22 +01:00
rcourtman ee8a24e14a backend and governance: MCP contract, agent capabilities, API, and release-control
Manifest-backed MCP tools, prompts, and resources with surface affordance contracts; agent capability manifest and governance projection; API contract tests and capability route projection; operations-loop and intelligence-funnel telemetry; release-control subsystem documentation, registry, and tooling; licensing and configuration.
2026-06-23 17:26:15 +01:00
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