Commit graph

1436 commits

Author SHA1 Message Date
rcourtman
51155a42c9 Remove uncalled test helpers
Dead-code audit batch 3: hasPrefix in internal/ai/service_test.go and
the staticExternalAgentActivityProvider test double in
internal/api/agent_resource_context_test.go have no callers in any
test or production code.

Skipped from the audit list after re-verification:
createTestEncryptionKey in cmd/pulse/test_helpers_test.go (called nine
times by commands_integration_test.go under the integration build tag).
2026-07-10 00:29:10 +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
255c7c23d4 Modernize Unified Agent lifecycle and platform support 2026-07-09 23:20:35 +01:00
Richard Courtman
1968dc4171 Close remaining CodeQL allocation and cookie gaps 2026-07-09 20:10:08 +01:00
rcourtman
042e7ef966 Harden remaining CodeQL security boundaries 2026-07-09 19:46:40 +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
f850099134 Adopt the agent's preferred host when re-registering a matched node
The resolved-identity match in canonical auto-register always preserved
the stored host, so the route-aware IP preference added to the agent in
022f170df could never take effect for an already-registered node: a
reinstall re-matched the node and kept the stale short-DNS host forever.
Preserve the stored host only when it is absent from the agent's ordered
candidate list (an admin-managed endpoint the agent cannot see); when the
agent itself lists the stored host as a lower-priority candidate, adopt
the newly selected host. Identity continuity (same node record, same
token) is unchanged.

Note: commit c21693489 carries this same message by mistake; it actually
contains concurrent OIDC/SAML mapping work from a parallel session that
was staged when the shared index raced. This commit is the real change.
2026-07-09 09:56:04 +01:00
rcourtman
c21693489a Adopt the agent's preferred host when re-registering a matched node
The resolved-identity match in canonical auto-register always preserved
the stored host, so the route-aware IP preference added to the agent in
022f170df could never take effect for an already-registered node: a
reinstall re-matched the node and kept the stale short-DNS host forever.
Preserve the stored host only when it is absent from the agent's ordered
candidate list (an admin-managed endpoint the agent cannot see); when the
agent itself lists the stored host as a lower-priority candidate, adopt
the newly selected host. Identity continuity (same node record, same
token) is unchanged.
2026-07-09 09:52:59 +01:00
rcourtman
93bccaff18 Update the Pro binary in-app from the license server download broker
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
Canonical Governance / governance (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 1/4) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 2/4) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 3/4) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 4/4) (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions
The in-app updater and the unattended timer both target the public
rcourtman/Pulse community assets, so a Pro install that used them was
silently downgraded to community. Guard 2 (983a89326) blocked in-app
apply on the Pro edition, which stopped the downgrade but left Pro
installs with no update path except manual portal downloads. That
friction is a plausible driver of the runtime split: as of 2026-07-08
only 10 of 66 active paid licenses have any Pro-runtime install.

Root fix: the compiled Pro binary now checks and applies updates
through the license server download broker (GET /v1/downloads/pulse-pro
with the installation token and instance fingerprint). The check
compares against the broker's pinned private release instead of GitHub,
respecting the stable/rc channel guard. Apply re-resolves fresh signed
R2 URLs at apply time, verifies the archive against the same pinned
pulse-installer SSHSIG key plus the broker manifest sha256, and refuses
GitHub-shaped download URLs outright. An unactivated Pro binary still
refuses with the portal fallback. The community edition path is
unchanged.

The update banner restores in-app apply for auto-updatable Pro
deployments and keeps the portal instructions for deployments the
updater cannot drive (Docker). scripts/pulse-auto-update.sh now skips
when the installed binary reports Pulse Pro so the unattended timer can
never reinstall community over Pro.

Note: internal/updates/pro_update.go and manager_pro_update_test.go for
this change landed one commit early inside 313552deb via a parallel
session committing a shared staged index; this commit completes the
wiring they belong to.
2026-07-08 23:39:37 +01:00
rcourtman
07fb7dbbac Serve shipped markdown docs as text instead of a forced download
The embedded frontend file server had no content type for .md, so every
in-app "Full details" / "Terms of Service" / security-guide link to
/docs/*.md answered application/octet-stream and the browser downloaded
the file instead of showing it. Shipped docs now serve as
text/plain; charset=utf-8 and open readable in the tab the app targets.
2026-07-08 14:36:55 +01:00
rcourtman
590f8c34db Serve legacy 3-segment OIDC SSO login and callback paths
An OIDC provider upgraded from v5 keeps its v5 redirect URL
(/api/oidc/callback, the DefaultOIDCCallbackPath), so the IdP redirects
the browser back to the 3-segment legacy path carrying only code and
state, with no session cookie and no API token yet. The global auth
middleware only marked the 4-segment per-provider path public, so in
API-token-only mode (AuthUser=="" && AuthPass=="" && HasAPITokens())
the callback was rejected with "API token required via Authorization
header or X-API-Token header", and the route dispatcher 404ed the same
path.

The handler layer (extractOIDCProviderID) already maps the legacy
/api/oidc/login and /api/oidc/callback paths to the migrated legacy
provider, so only the public-path allowlist and the route dispatcher
needed to recognise the 3-segment form. Adds a regression test covering
both legacy paths in API-token-only mode, the exact configuration that
emitted the error.

Refs #1533
2026-07-08 08:15:03 +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
rcourtman
4382919447 Add MSP report scheduling and alert rollup 2026-07-07 20:37:18 +01:00
rcourtman
eb99d7a6b3 Fix SSO session display labels
Refs #1535
2026-07-07 16:35:07 +01:00
rcourtman
57139c65c5 Fix mobile onboarding URL handoff sanitization
Some checks are pending
Canonical Governance / governance (push) Waiting to run
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
Helm CI / Lint and Render Chart (push) Waiting to run
Core E2E Tests / Playwright Core E2E (push) Waiting to run
2026-07-07 15:12:59 +01:00
rcourtman
3dd2ecd198 Add Proxmox token smoke diagnostics
Refs #1476
2026-07-07 10:10:16 +01:00
rcourtman
1c8a9346ef Fix legacy OIDC SSO discovery and CSP nonce
Refs #1533
2026-07-07 09:54:14 +01:00
rcourtman
d52231855d Fix Proxmox token preservation regressions
Refs #1470, #1476, #1511
2026-07-07 09:49:26 +01:00
rcourtman
4b669ad9b5 Fix SAT SMART temperature collection
Refs #1471
2026-07-06 23:42:00 +01:00
rcourtman
6f582671b0 Fix Proxmox live chart CPU fallback
Refs #1525
2026-07-06 15:05:18 +01:00
rcourtman
caa9b41834 Fix OIDC provider detail persistence
Refs #1521
2026-07-06 11:18:18 +01:00
rcourtman
c9c5f34eb4 Fix host metric percent normalization
Apply token-gated command policy when deriving connection fleet state.
2026-07-06 10:08:36 +01:00
rcourtman
04a9e6ad3e Fix Proxmox agent install token isolation
Keep Proxmox setup tokens node-scoped so cluster installs do not rotate one shared Pulse API token.

Suppress command-enable config for tokens that cannot register command channels and keep reusable installer tokens out of agent:exec.
2026-07-05 17:21:34 +01:00
rcourtman
c384d5e0c8 Prepare v6.0.3 patch release
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
2026-07-05 11:07:27 +01:00
rcourtman
d029e4dc42 Fix demo fixture entitlement recovery
Seed the hidden demo fixture entitlement during stable demo updates so release builds can enable governed mock resources after runtime configuration is restored.

Keep the deployment contract and release policy checks aligned with the release-build entitlement gate.
2026-07-04 22:44:43 +01:00
rcourtman
313648b2f1 feat(mcp): add additive fleet-context filtering
get_fleet_context returned the whole registry with no way to narrow it,
so a large fleet produced a payload that exceeded the agent harness's
50KB cap and forced agents to receive or page through healthy resources
that are pure noise from a triage standpoint.

Add optional additive query-param filters — hasFindings, severity,
technology, resourceType — that compose by intersection. All optional
(omitting every filter returns the full fleet, backward compatible);
unknown/unmatched values return 200 with resources: [] (a valid triage
answer, not an error). The hasFindings=true filter is the headline
triage case: show me only what needs attention.

The MCP adapter layer had no GET query-param transport — ProjectCapabilityCall
short-circuited GET after path substitution and dropped all other args.
Close that gap generically: ProjectedCall gains a Query url.Values field
populated from leftover non-path public args for GET/DELETE, and
BuildCapabilityHTTPRequest encodes it onto the request URL. Benefits any
future GET capability with filter args, not just fleet-context. The
resources/list adapter calls fleet-context with empty args and continues
to receive the unfiltered fleet.

The manifest now declares the filters via inputSchema so they are
discoverable through the capability surface the same way add_node's
params are.

- ProjectedCall.Query + GET query-param forwarding (projection.go, http.go)
- fleetContextFilter parsing/matching in HandleFleetContext
- fleetContextInputSchema + InputSchema on the capability
- Regenerated cmd/pulse-mcp/README.md via generate-pulse-intelligence-docs
- Contract updates: api-contracts, ai-runtime, agent-lifecycle, storage-recovery
- Tests: adapter query-forwarding unit tests, 7 filter handler tests
  (hasFindings/severity/technology/resourceType/no-filter/unknown/compose),
  contract pinning inputSchema + end-to-end query forwarding
2026-07-03 18:01:05 +01:00
rcourtman
20c2f27057 feat(mcp): add list_resource_capabilities tool
Agents had no way to discover which governed capabilities a resource
advertises before calling plan_action. The data existed (Resource.Capabilities
with full param schemas) but was only surfaced as count-limited prose
facts inside get_resource_context, which deliberately omits parameter
schemas. This forced agents to guess capabilityName and params.

Add a dedicated structured surface: GET /api/agent/resource-capabilities/{id}
returns the canonical ResourceCapability list (name, type, approval level,
platform, full parameter schemas) for a single resource. A resource with
no advertised capabilities returns 200 with an empty array, the signal
to skip plan_action.

Internal plumbing already existed (registry.Get + Resource.Capabilities,
the same path plan_action validates against); this is the wiring work at
the canonical agent-surface layer. The tools/call dispatch is fully
generic so no mcp.go changes were needed.

Companion to get_resource_context: that tool remains the human-readable
prose summary; this is the structured schema surface for action planning.

- Manifest capability, name/path constants, output schema helper
- Handler mirroring HandleResourceContext error/scope patterns
- Route registration behind monitoring:read
- Activity telemetry mapping to resource_context class
- Regenerated cmd/pulse-mcp/README.md via generate-pulse-intelligence-docs
- Contract updates: api-contracts, ai-runtime, agent-lifecycle, storage-recovery
- Tests: manifest ownership/addressing/table-test, handler happy/empty/404/405,
  contract pinning capability presence/scope/wire-shape
2026-07-03 17:07:41 +01:00
rcourtman
b1fdefdd50 Normalize Docker container CPU capacity
Refs #1293
2026-07-03 11:51:08 +01:00
rcourtman
875e414b4b Align resource staleness with poll cadence
Refs #1468
2026-07-03 09:05:12 +01:00
rcourtman
dcf541784d Bound server state broadcast memory
Refs #1442
2026-07-02 23:27:49 +01:00
rcourtman
dd357c1ef3 Fix hosted relay onboarding test setup
Some checks are pending
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Frontend & Backend (push) Waiting to run
- seed connected relay status before building mobile onboarding payloads
2026-07-02 22:34:37 +01:00
rcourtman
97b9369333 Guard infrastructure import plan approvals
- add candidate import approval and preview gate for node onboarding
- document metrics, availability, and proxy role behavior
- lock config auth reads and preserve storage metrics fallback
2026-07-02 21:59:26 +01:00
rcourtman
6c9b4f8e8d Harden GA telemetry disclosure 2026-07-02 11:50:03 +01:00
rcourtman
df67c01a64 Fix mobile relay runtime startup 2026-07-01 10:38:57 +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
c759c9a77a Gate mobile pairing codes on relay readiness 2026-06-30 17:12:55 +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
597c369a63 Add alert delivery diagnosis endpoint 2026-06-29 22:27:01 +01:00
rcourtman
6b7d0b45f9 Add agent fleet diagnostics endpoint 2026-06-29 18:03:46 +01:00
rcourtman
73208a2863 Document state summary integration contract 2026-06-29 17:39:11 +01:00
rcourtman
d6a21eba98 Add update funnel telemetry
Record anonymous 30-day update attempts, successes, failures, and coarse failure categories from local update history.
2026-06-28 15:19:39 +01:00
rcourtman
125959e4e0 fix(dev): login endpoint respects admin bypass mode
handleLogin rejected all credentials when ALLOW_ADMIN_BYPASS=1 because
it validated against config AuthUser/AuthPass directly without checking
the bypass flag. This made dev-mode browser testing impossible after a
backend restart with bypass env vars — the API middleware accepted all
requests but the login page could never obtain a session cookie.

When bypass is enabled, accept any credentials and create a session as
'admin'.
2026-06-27 16:09:23 +01:00
rcourtman
63d7640a06 Fix missing impact text and incomplete alert type coverage in unified findings
1. Add Impact field to findingView struct — was completely missing from
   the unified findings API response serialization.
2. Expand generateImpact() to cover all ~25 alert types the system
   produces. Previously only 7 had curated impact text.
3. Fix naming mismatches: poweredOff/nodeOffline never matched actual
   alert types powered-off/host-offline.
4. Expand TypeCategoryMap with all missing alert types.
2026-06-27 14:43:09 +01:00
rcourtman
ad3b90e605 fix: ensure availability probes report at least 1ms latency on success
TCP probes to fast local-network services can complete in under 1ms,
causing latency.Milliseconds() to return 0. Combined with omitempty on
the poller status struct, this made some probes show 'Online' with no
latency in the settings panel. Now successful probes always report at
least 1ms.
2026-06-26 22:52:28 +01:00
rcourtman
06ea25e5ce Fix webhook private CIDR allowlist lost after monitor reload (#1507)
When a monitor reload was triggered by node auto-registration, the
reloadFunc in server.go recreated the monitor (and its notification
manager) but never re-applied system settings. The new notification
manager started with an empty webhook private CIDR allowlist, causing
webhook notifications to private IPs to fail until the allowlist was
manually re-saved in Settings.

Fix: call router.ReloadSystemSettings() at the end of reloadFunc,
after the new monitor references are set. This re-applies all
persisted system settings — including the webhook CIDR allowlist —
to the freshly created notification manager.

Refs #1507
2026-06-26 22:15:48 +01:00
rcourtman
f5adf39c99 fix: stop reporting scheduler unhealthy when individual tasks are dead-lettered
The /api/health endpoint gated scheduler health on
DeadLetterCount()==0, causing a 503 whenever any single monitored
node became unreachable. Dead-lettered tasks are the scheduler
working as designed — after repeated failures the task is moved to
the dead-letter queue to prevent infinite retries.

In the live lab this manifested as a persistent 503 on /api/health
because the Pi node (192.168.0.2) times out on every poll cycle,
dead-lettering its task while the other 20+ tasks continue normally.

The scheduler is healthy when the monitor is running. Dead-letter
counts remain available as diagnostic info via the
/api/monitoring/scheduler/health endpoint.
2026-06-26 14:03:47 +01:00
rcourtman
9bcff2983d Clarify missing license public key activation error 2026-06-26 13:00:42 +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