Commit Graph

485 Commits

Author SHA1 Message Date
rcourtman 2323c96385 Guard pseudonymous telemetry terminology 2026-07-23 09:38:50 +01:00
rcourtman dbca44937b Add privacy-safe telemetry lifecycle and outcome signals 2026-07-23 01:10:38 +01:00
rcourtman 82083378a2 Cover cloud tenant registry queries, unified views and slowlog wrappers
Three new branch-coverage tests taking eighteen previously unreached functions
from 0.0% to covered.

internal/cloudcp/registry: the workspace limit error message on both the nil
and populated receiver, the active workspace count per account, the tenant
lookup across owning account, foreign account and missing rows, the invitation
listing by email including case and whitespace normalization, and the
invitation delete for both an existing pair and a pair that never existed.

internal/unifiedresources: the Docker container and Kubernetes node typed
views, the presentation listing and the metrics target, each asserted on the
nil receiver, the nil nested payload and the populated case, with returned
slices proven independent of the store.

pkg/db: the tracing wrappers around BeginTx, Query, QueryRow and their context
variants plus the pool setters, with exact histogram deltas proving the
observe wiring rather than that SQLite works.

All three files are new; no source or existing test was touched.

Contract-Neutral: test-only: new Go branch-coverage tests, no source or contract change
2026-07-22 21:41:02 +01:00
rcourtman b63fc0a23d Convert audit.db to incremental auto-vacuum and reclaim after retention
Retention deletes freed pages inside audit.db but the file never
shrank on disk, the same bloat class #1496 fixed for
unified_resources.db. Migrate existing databases to incremental
auto-vacuum at startup and return freed pages to the OS after each
retention pass, capped per cycle so a backlog drains gradually.
2026-07-22 09:09:25 +01:00
rcourtman 963401ac90 Let metrics reads run concurrently with writes instead of queueing
The metrics store capped its SQLite pool at one connection, so every UI
history read queued behind every buffered-write commit, and behind the
WAL checkpoints those commits pick up at the 4000-page threshold. On
write-heavy installs (many Docker agents with churning containers) that
serialization presented as sustained 120-260ms COMMIT warnings and an
unresponsive UI even with idle CPU and fast disks. Writes were never the
risk: flush, rollup, retention, and maintenance already funnel through
the single background worker goroutine, and the WriteBatchSync poller
path serializes on the WAL write lock via busy_timeout.

Raising the pool exposed a second bug: auto_vacuum(INCREMENTAL) in the
per-connection DSN pragmas replays as a database-header write whenever
the pool opens a new connection, which blocks connection creation behind
the active writer for up to the full 30s busy_timeout. auto_vacuum is a
persistent database property that migrateAutoVacuum already establishes
once at startup, so the per-connection copy is dropped.

Refs #1601

Contract-Neutral: behavioral fix: metrics store read concurrency and per-connection auto_vacuum pragma removal, no public contract delta (#1601)
2026-07-21 20:55:41 +01:00
rcourtman ef4f204d25 Make report branding PDF assertion case-insensitive
Treat extracted PDF presentation casing as non-semantic while preserving exact resource identity and cross-client branding exclusion checks.
2026-07-21 10:23:06 +01:00
rcourtman 4f43878ebb Add focused branch coverage and repair infrastructure source contract
Contract-Neutral: test-only coverage and source-contract assertions; no runtime or contract behavior changed
2026-07-20 22:41:19 +01:00
rcourtman b6a74576bc Integrate trust-gate reliability fixes 2026-07-20 16:03:29 +01:00
rcourtman e9dddf86b8 Harden RC qualification coverage
Contract-Neutral: RC qualification and contract-preserving reliability fixes; no canonical product contract changes
2026-07-20 01:13:26 +01:00
rcourtman ff93d99fa1 Add Go branch-coverage tests for pure helpers across six packages
Test-only wave, contract-neutral. New *_branchcov0719pm_test.go files cover
previously-uncovered pure value-in/value-out helpers, each verified to move
its target functions from 0% to full coverage:

- internal/agentcontext: formatKubernetesServicePorts (empty/single/cap/overflow
  arms) and addMetricFact (nil-metric, percent/value/ratio arms) now 100%.
- pkg/reporting: reportLogoTypeFromPath 0->100, reportLogoTypeFromData 28.6->100,
  scaledLogoSize 70->90 (extension and aspect branches).
- internal/cloudcp/email: RenderMagicLinkEmail 0->80 (render success path; the
  compile-time template-error arm is unreachable and left uncovered).
- internal/recovery: recoveryDetailString (nil map, missing key, non-string,
  string arms) and recoveryPointObservedAt 40->100.
- internal/ai/tools: ErrStrictResolution/ErrRoutingMismatch ToToolResponse 100.
- internal/ai/providers: every NormalizeCollections receiver 0->100.

No source or existing test modified.
2026-07-19 15:11:02 +01:00
rcourtman 278673aa0f Add Go branch-coverage tests for twelve pure backend helpers
New *_branchcov0718_test.go files raise coverage of previously-uncovered pure
functions across ten packages. Covered areas include securityutil SSRF and URL
validation, truenas path and telemetry parse helpers, storagehealth SMART and
physical-disk risk assessment, vmware inventory sort keys and error classifiers,
servicediscovery token filtering and readiness, telemetry evidence-from-history,
models ToFrontend converters and frontend NormalizeCollections normalizers,
actionplanner type predicates and canonical resource-id sort, config API-token
accessors, and licensing state accessors.

Test-only, with no runtime or subsystem-contract change. Verified in a clean
worktree at HEAD with go vet and package tests green, gofmt clean, and every
named target function moved from 0 percent to covered.
2026-07-19 15:10:09 +01:00
rcourtman e4e43c3f11 feat(telemetry): attribute approved-action failures to cause buckets
The 30d window said 18 of 26 approved action attempts failed but
carried zero cause information. Every approved attempt that is not a
verified success now lands in exactly one content-free bucket:

- pre_dispatch: terminally refused before dispatch (plan drift, expiry,
  emergency stop, policy authorization)
- execution: dispatched execution failed or ended inconclusive
- unverified: execution succeeded but outcome verification was not
  confirmed (reads as success in the UI, counted as failure here)
- stuck_executing: still executing over an hour after dispatch

plus one sanitized machine reason code for the most recent failure.
Successes, failure buckets, and recent in-flight attempts partition the
attempt count, so the next window explains its own gap.

RefuseActionExecution now persists the specific refusal code
(plan_drift, action_plan_expired, ...) as the canonical execution
reason code instead of the generic pre_dispatch_refused, so audit truth
and telemetry distinguish refusal causes without message parsing.

Privacy disclosures updated in both PRIVACY.md copies; the adoption
report script surfaces the new counters.

Contract-Neutral: additive content-free telemetry counters + specific pre-dispatch refusal reason codes; privacy disclosures updated in-commit per security-privacy extension point
2026-07-17 23:39:34 +01:00
rcourtman 64fb3d198d fix(securityutil): dial every permitted resolved IP instead of pinning the first
The restricted outbound dialer resolved a hostname, validated the results,
then dialed only the first permitted IP. When localhost resolves to ::1
ahead of 127.0.0.1 and the target service binds only 127.0.0.1 (Ollama's
default), the dial failed with 'connect: connection refused' even though
curl and browsers connect fine via address-family fallback. This broke the
AI provider setup quickstart: the blessed http://localhost:11434 Ollama URL
failed Pulse's connection test on IPv6-first hosts (setup-friction class of
#847/#1003).

Try each permitted IP from the validating resolution in order until one
connects. Every candidate is still validated against the outbound policy
from the same resolution, so the DNS-rebinding pinning guarantee is
unchanged. Fixes all consumers of the shared client (Ollama provider, OIDC,
SSO, connection probes, availability poller); api-contracts wording updated
to match. Live-verified against Ollama 0.32.1 bound to 127.0.0.1 only:
TestConnection via http://localhost:11434 failed before, passes after.
2026-07-17 23:35:45 +01:00
rcourtman dadffe8a26 fix(tlsutil): discard the subprocess kill error explicitly 2026-07-17 17:26:55 +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 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 676117bf27 Separate Patrol evidence and model-turn budgets 2026-07-16 17:50:08 +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 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 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 e3382c8bcb Use authoritative Docker OOM evidence 2026-07-15 17:55:26 +01:00
rcourtman cdc109d408 Publish Patrol investigation limits live 2026-07-15 10:49:16 +01:00
rcourtman 93fff4f1d8 Add an explicit operator override for plaintext HTTP to non-local Pulse hosts
Fleets on networks numbered from nominally public IP space (issue
#1522: an AD estate on 192.20.0.0/16) cannot pass the agent's
local-network plaintext heuristic, and the only workaround was pointing
a .internal DNS alias at the server, which bypasses the same control
less visibly than a flag would. --allow-plaintext-http
(PULSE_AGENT_ALLOW_PLAINTEXT_HTTP) records process-wide consent once at
agent startup before any module validates a URL, covers every agent
transport including the websocket command channel, warns at startup
that the API token travels in cleartext, defaults closed, and is never
emitted by generated install commands or settable by the server.
2026-07-14 16:42:02 +01:00
rcourtman ae4162f8f2 Enforce installation-scoped license invalidation 2026-07-14 16:18:21 +01:00
rcourtman e1985528d9 test(coverage): agentexec codecs and licensing service branch coverage
Add purely-additive branch-coverage tests for freshly-landed, previously-
untested backend surface:
- internal/agentexec: docker container update codec (decode/validate/bind/
  digest/result-cross-validation), residual host apt codec validation arms
  (host update + storage cleanup decode/bind/result/operation-query identity),
  and NormalizeDeployMaxParallel clamping.
- pkg/licensing: service helpers (unionFeatures, safeIntFromInt64,
  remainingDaysCeil, ensureGracePeriodEnd, Set/Get Evaluator+StateMachine,
  IsValid, CurrentUnsafeForTesting, dev-mode) and activation GrantEnvelope
  ParseExpiresAt + uncapped-core-monitoring tier classification.
No source changed.
2026-07-14 14:45:55 +01:00
rcourtman 848b4d5038 Preserve customer data across plan downgrades 2026-07-14 11:47:25 +01:00
rcourtman 4aac79dc72 Add durable agent operation receipts 2026-07-12 04:16:22 +01:00
rcourtman 82c7c52727 Expose server-authored action policy provenance 2026-07-12 00:18:43 +01:00
rcourtman b7b897651b Add canonical action result truth 2026-07-11 21:27:00 +01:00
rcourtman cd7886aa44 Enforce server-owned action approval authority 2026-07-11 19:49:17 +01:00
rcourtman eb9954618a Add governed storage pressure cleanup 2026-07-11 10:11:35 +01:00
rcourtman 500d3be2a7 Align Pro feature catalog names with the new Patrol vocabulary
The public pricing surface and in-app plan-selling copy now say
"Patrol investigates issues and explains the root cause" and
"Patrol applies safe fixes and verifies the result". Rename the
licensing catalog DisplayName/ComparisonName pairs to the title-cased
forms, align the ai_autofix UpgradeReason verbs (apply/verify the
result), regenerate the frontend catalog, and update the Go, vitest,
and Playwright pins plus README and PULSE_PRO docs.
2026-07-11 09:33:47 +01:00
rcourtman 1312da3acb Add governed host update autonomy 2026-07-11 01:25:14 +01:00
rcourtman 69b2028513 Add policy-scoped Patrol autonomy 2026-07-11 00:37:34 +01:00
rcourtman 5bbfce956d Complete Patrol action lifecycle continuity 2026-07-10 21:22:06 +01:00
rcourtman a1ba51630d Commit B (pulse): typed lifecycle is the only Patrol route; delete the command side doors
OrchestratorChatService is replaced, not extended: it exposes only
ExecuteInvestigationStream (returning a structured
OrchestratorInvestigationResult) and ListInvestigationTools. The generic
ExecuteStream, SetAutonomousMode, ListAvailableTools, and the
AutonomousMode request field are gone, along with
OrchestratorCommandExecutor, OrchestratorApprovalStore, and the
autonomy/fix-verifier/license dependency interfaces. OrchestratorDeps
now carries a REQUIRED ActionBroker and no command/autonomy deps.

The pulse investigation adapter is rewritten to drive
ExecuteInvestigationStream, injecting proposal/finding/investigation
identity from trusted context and feeding a per-org proposal catalog
resolved from the tenant-bound action lifecycle (so acceptance and
planning validate identically). The retired command-execution,
autonomy, and command-shaped approval adapters are deleted; the router
wires the broker and catalog factories onto the AI settings handler.

The sixth side door is removed: PatrolService.generateRemediation-
PlanFromInvestigation and its call, which copied Fix.Commands into an
executable enterprise remediation plan, are deleted, pinned closed by a
source-audit test.

ActionCapabilityParamInfo gains Pattern (mapped by the broker), so the
canonical planner's pattern validation survives the cross-repo
boundary.

L20 readiness assertion RA35 asserts every Patrol-initiated
infrastructure mutation originates as a typed proposal and reaches
execution only through the canonical action lifecycle, with unsupported
proposals failing closed. Contract prose updated across ai-runtime,
api-contracts, agent-lifecycle, performance-and-scalability,
security-privacy, and storage-recovery.

Lands together with the pulse-enterprise orchestrator migration (the
replace directive means both heads move as one window).
2026-07-10 18:05:25 +01:00
rcourtman 26718e70b6 Add the typed Patrol action-proposal seam over the shared lifecycle
pkg/aicontracts gains the plan-only OrchestratorActionBroker contract:
ActionProposal (typed capability reference, no command, host, risk, or
approval fields), a read-only ActionCapabilityCatalog with parameter
sensitivity, ActionDisposition over the existing safe ActionPlanInfo
projection, and an additive Action *ActionReference on
InvestigationSession and InvestigationRecord. ProposedFix/ApprovalID are
documented as migration-only; OrchestratorDeps gains the ActionBroker
seam while CmdExecutor/ApprovalStore are marked legacy pending removal.

internal/api/patrol_action_broker.go implements the seam tenant-bound
over ResourceHandlers.ActionLifecycle(): fixed pulse_patrol actor,
broker-owned ActionOrigin stamped through the service's internal
PlanWithOptions (the public plan endpoint cannot claim an origin),
plan-only submission even for ApprovalNone capabilities, and refusal of
proposals that populate IsSensitive parameters before any persistence.

The lifecycle service adds Capabilities (same registry resolution and
typed errors as planning) and an OnActionTransition persisted-state
callback covering plan, decision, and terminal execution transitions,
published only after the store write succeeds, so Patrol can reconcile
decisions and outcomes deterministically. ActionAuditRecord carries the
new broker-owned Origin, persisted in action_audits.origin_json with a
schema migration and round-trip normalization.

Contracts updated across api-contracts, ai-runtime, unified-resources,
agent-lifecycle, and storage-recovery; proofs added in
pkg/aicontracts/contracts_test.go (propose-only method set, command-free
wire shape, additive reference), internal/api/contract_test.go
(plan-only broker pins), broker behavior tests, lifecycle origin and
transition tests, and a SQLite origin round-trip test.

Slice 2a of the typed-lifecycle enforcement ratchet: additive core
fabric only; enterprise migration and side-door deletion follow.
2026-07-10 11:56:54 +01:00
rcourtman 4c37a8c332 reporting: fit and wrap operator-controlled cover title and brand lines
Long custom titles on generate-multi covers overflowed the page edges;
the cover now shrinks to fit, wraps at the minimum size, and flows the
subtitle and scope blocks below a wrapped title. Also drop the stale
frontend-primitives contract and registry references to the
Settings/reportingResourceTypes.ts re-export removed in c862fb0ca; that
frontend-only commit skipped the governance audit, leaving the stale
references blocking every Go commit.
2026-07-10 00:42:06 +01:00
rcourtman f7aa9ee922 Remove unreachable exported functions from pkg/
Dead-code audit batch 1: five functions with zero references from main,
tests, or pulse-enterprise (deadcode -test, grep-verified per symbol,
enterprise compile-checked). Also renames the tlsutil test that
exercised the deleted compat alias's target to name the Unverified
function it actually calls.

Skipped from the audit list after re-verification: IsNilAlertPayload
(live pulse-enterprise consumer in internal/aialertanalysis).
2026-07-10 00:27:35 +01:00
rcourtman 255c7c23d4 Modernize Unified Agent lifecycle and platform support 2026-07-09 23:20:35 +01:00
rcourtman 042e7ef966 Harden remaining CodeQL security boundaries 2026-07-09 19:46:40 +01:00
rcourtman e8ad8b2fd3 Use checked int parsing for Proxmox conversions 2026-07-09 17:47:23 +01:00
rcourtman 92524e1c27 Harden CodeQL storage and integer boundaries 2026-07-09 17:37:08 +01:00
rcourtman 24b2e40e92 Harden scanned request and storage boundaries
Harden CodeQL-scanned request, command, path, and frontend sinks across relay proxying, availability probes, connection probing, notification CLI execution, report storage, licensing persistence, preview bootstrapping, tooltip rendering, logging, and test identity generation.
2026-07-09 17:22:29 +01:00
rcourtman 847cf98544 Fix activation fingerprint and license period status 2026-07-09 11:30:26 +01:00
rcourtman e23505206d Carry the max_users grant claim into instance license limits
GrantClaims and Claims gain a MaxUsers seat limit mirroring MaxGuests:
the grant JWT claim is copied at the activation bridge and surfaced
through EffectiveLimits()["max_users"], which the already-shipped
user-limit enforcement (MaxUsersLimitFromLicense) reads unchanged.
Inert by default: no plan sets max_users yet, so absent claim = 0 =
unlimited for every existing license and grant. The self-hosted
commercial volume scrub strips only max_monitored_systems and
max_guests; tests now pin that max_users survives it from both the
named field and explicit limits, plus the end-to-end proof that a grant
carrying max_users=3 enforces 3 and a grant without it stays unlimited.
Contract shape recorded in cloud-paid Extension Point 26; the grant
wire contract list gains max_users against the relay-server reference
(pulse-pro 95a18bd).
2026-07-08 16:56:07 +01:00
rcourtman fca5975c38 Add the dormant Business tier to the licensing core
TierBusiness: Pro's feature set, 365-day history retention, uncapped
core monitoring like every self-hosted tier, Business display name, and
a slot in the min-tier ordering. Differentiation is by max_users limit,
retention, and support rather than features (multi-user is already a Pro
capability via RBAC; see specs/pricing-segmentation.md binder findings).
Dormant until the license server issues business plan versions; no
checkout, pricing, or UI surface references it yet. Contract shape
recorded as cloud-paid Extension Point 26 and pinned by
TestBusinessTierContractShape.
2026-07-08 16:02:56 +01:00
rcourtman 983a89326f Block in-app self-update on the Pro binary to prevent silent downgrade
The in-app updater and install.sh both fetch the public community build from
github.com/rcourtman/Pulse. On non-Docker Pro installs (systemd, proxmoxve,
source) the "Apply Update" button was live, so applying an update replaced the
separately compiled Pro binary with community and silently stripped Audit,
RBAC, Reporting, and SSO from a paying customer. Docker was already blocked;
nothing else was.

Add a dependency-free pkg/edition marker (defaults to community) that the Pro
binary flips via enterpriseruntime.Initialize, mirroring the existing
coreaudit.SetLogger / server.SetBusinessHooks registration seam. ApplyUpdate
now refuses when the edition is Pro, pointing at the Private Release Access
portal (https://pulserelay.pro/download.html) and the install.sh --archive
path. The gate keys off the compiled binary, not license state: a community
binary with an active license still self-updates as before.

The update banner hides the in-app apply affordance and shows portal
instructions for the Pro runtime, keyed off the existing runtime-identity
signal (runtime.build) rather than a new payload field, so nothing extra is
plumbed through the version API and the frontend reuses the canonical
"which binary am I" contract. The backend gate is the hard guarantee; the
banner is the UX layer.

Guard 2 of the Pro download/update experience spec.
2026-07-08 10:02:04 +01:00
rcourtman c6fdd9058c Allow clearing SSO provider restriction fields from Settings
Empty allowedGroups, allowedDomains, allowedEmails, groupsClaim and
groupRoleMappings in a provider PUT were silently restored from the
existing config, so an admin could never clear them. The guards
shielded against lossy round-trips that no longer exist: the detail
GET and the flat list response both carry these fields, so an empty
value is an intentional clear. Nested OIDC/SAML config and the client
secret stay preserved since toggle payloads omit them and secrets are
never echoed in reads.

Also expose groupsClaim and groupRoleMappings in the shared extensions
list response so the enterprise list matches the core one, and make
the Settings enable/disable toggle send only writable fields; the old
list-item spread included computed response fields that the enterprise
strict PUT decoder rejects.
2026-07-07 22:04:46 +01:00
rcourtman 8355335e08 Clarify v5 migration recovery states 2026-07-07 21:46:07 +01:00