From ecf8fd42995eec905e01c8a561d50acef0acdc30 Mon Sep 17 00:00:00 2001
From: rcourtman
Date: Tue, 28 Apr 2026 11:23:49 +0100
Subject: [PATCH] Keep self-hosted Pro prompts opt-in
---
README.md | 6 +-
cmd/pulse-agent/main.go | 4 +-
cmd/pulse-agent/main_test.go | 15 +++++
docs/AI_AUTONOMY.md | 10 +--
.../v6/internal/subsystems/agent-lifecycle.md | 1 +
.../v6/internal/subsystems/ai-runtime.md | 5 +-
.../v6/internal/subsystems/api-contracts.md | 12 ++--
.../v6/internal/subsystems/cloud-paid.md | 19 +++---
.../subsystems/frontend-primitives.md | 50 ++++++++-------
.../subsystems/patrol-intelligence.md | 28 +++++----
.../internal/subsystems/storage-recovery.md | 6 ++
.../src/components/patrol/ApprovalSection.tsx | 40 ------------
.../patrol/__tests__/ApprovalSection.test.tsx | 16 ++---
.../components/shared/HistoryChartOverlay.tsx | 40 +-----------
.../SharedPrimitives.guardrails.test.ts | 7 ++-
.../shared/__tests__/HistoryChart.test.tsx | 24 +++----
...MonitoredSystemLimitWarningBanner.test.tsx | 30 +++++++++
.../components/shared/useHistoryChartState.ts | 35 -----------
...eMonitoredSystemLimitWarningBannerState.ts | 2 +
.../patrol/PatrolIntelligenceBanners.tsx | 22 +------
.../patrol/PatrolIntelligenceHeader.tsx | 42 ++-----------
.../patrol/usePatrolIntelligenceState.ts | 62 -------------------
.../pages/__tests__/AIIntelligence.test.tsx | 32 +++++-----
internal/ai/service.go | 4 +-
internal/ai/service_extended_test.go | 15 +++++
internal/api/ai_handlers.go | 18 +++---
internal/api/ai_handlers_test.go | 11 ++++
internal/api/contract_test.go | 15 +++++
internal/api/router_routes_ai_relay.go | 4 +-
pkg/server/server.go | 2 +-
30 files changed, 229 insertions(+), 348 deletions(-)
diff --git a/README.md b/README.md
index 3bdf76a90..76ea24b85 100644
--- a/README.md
+++ b/README.md
@@ -161,7 +161,7 @@ Self-hosted tiers:
|---|---:|---|---:|---|
| Community | Free | Unlimited | 7 days | Full self-hosted monitoring |
| Relay | $4.99/mo or $39/yr | Unlimited | 14 days | Remote access, mobile, and push notifications |
-| Pro | $8.99/mo or $79/yr | Unlimited | 90 days | Root-cause analysis, safe auto-fix, and operations tooling |
+| Pro | $8.99/mo or $79/yr | Unlimited | 90 days | Root-cause analysis, safe remediation workflows, and operations tooling |
Pulse still counts top-level monitored systems once no matter how they are
collected. VMs, containers, pods, disks, backups, and other child resources
@@ -181,7 +181,7 @@ Runtime-aligned capability summary:
| Pulse Patrol (Background Health Checks) | ✅ | ✅ | ✅ | ✅ |
| Remote Access / Mobile / Push | — | ✅ | ✅ | ✅ |
| Alert-Triggered Root-Cause Analysis | — | — | ✅ | ✅ |
-| Pulse Patrol Auto-Fix | — | — | ✅ | ✅ |
+| Safe Remediation Workflows | — | — | ✅ | ✅ |
| Centralized Agent Profiles | — | — | ✅ | ✅ |
| Update Alerts (Container/Package Updates) | ✅ | ✅ | ✅ | ✅ |
| Basic SSO (OIDC) | ✅ | ✅ | ✅ | ✅ |
@@ -207,7 +207,7 @@ Community installs continue with BYOK. Chat Assistant remains BYOK.
Technical highlights:
- Cross-system context (nodes, VMs, backups, containers, and metrics history)
- LLM analysis with your provider plus alert-triggered root-cause investigations (Pro / hosted Cloud)
-- Optional auto-fix with command safety policies and audit trail
+- Optional safe remediation execution with command safety policies and audit trail
- Centralized agent profiles for consistent fleet settings
**[Try the live demo →](https://demo.pulserelay.pro)** or **[learn more at pulserelay.pro](https://pulserelay.pro)**
diff --git a/cmd/pulse-agent/main.go b/cmd/pulse-agent/main.go
index cd4893c14..1d5dcb8b1 100644
--- a/cmd/pulse-agent/main.go
+++ b/cmd/pulse-agent/main.go
@@ -602,7 +602,7 @@ type Config struct {
DockerRuntime string // Force container runtime: docker, podman, or auto
// Security
- EnableCommands bool // Enable command execution for AI auto-fix (disabled by default)
+ EnableCommands bool // Enable command execution for Patrol remediation (disabled by default)
// Enrollment
Enroll bool // Exchange bootstrap token for runtime token on startup
@@ -726,7 +726,7 @@ func loadConfig(args []string, getenv func(string) string) (Config, error) {
disableAutoUpdateFlag := fs.Bool("disable-auto-update", utils.ParseBool(envDisableAutoUpdate), "Disable automatic updates")
disableDockerUpdateChecksFlag := fs.Bool("disable-docker-update-checks", utils.ParseBool(envDisableDockerUpdateChecks), "Disable Docker image update detection (avoids Docker Hub rate limits)")
dockerRuntimeFlag := fs.String("docker-runtime", envDockerRuntime, "Container runtime: auto, docker, or podman (default: auto)")
- enableCommandsFlag := fs.Bool("enable-commands", utils.ParseBool(envEnableCommands), "Enable command execution for AI auto-fix (disabled by default)")
+ enableCommandsFlag := fs.Bool("enable-commands", utils.ParseBool(envEnableCommands), "Enable command execution for Patrol remediation (disabled by default)")
disableCommandsFlag := fs.Bool("disable-commands", false, "[DEPRECATED] Commands are now disabled by default; use --enable-commands to enable")
enrollFlag := fs.Bool("enroll", false, "Exchange bootstrap token for runtime token (used by deploy wizard)")
healthAddrFlag := fs.String("health-addr", defaultHealthAddr, "Health/metrics server address (empty to disable)")
diff --git a/cmd/pulse-agent/main_test.go b/cmd/pulse-agent/main_test.go
index 0967997a3..b8a905700 100644
--- a/cmd/pulse-agent/main_test.go
+++ b/cmd/pulse-agent/main_test.go
@@ -21,6 +21,21 @@ import (
"github.com/rs/zerolog"
)
+func TestEnableCommandsHelpUsesPatrolRemediationCopy(t *testing.T) {
+ source, err := os.ReadFile("main.go")
+ if err != nil {
+ t.Fatalf("read pulse-agent main.go: %v", err)
+ }
+ text := string(source)
+
+ if !strings.Contains(text, "Enable command execution for Patrol remediation (disabled by default)") {
+ t.Fatal("expected enable-commands help to describe Patrol remediation")
+ }
+ if strings.Contains(text, "Enable command execution for AI auto-fix") {
+ t.Fatal("enable-commands help must not revive AI auto-fix wording")
+ }
+}
+
func TestGatherTags(t *testing.T) {
tests := []struct {
name string
diff --git a/docs/AI_AUTONOMY.md b/docs/AI_AUTONOMY.md
index af2f701fd..435a57d84 100644
--- a/docs/AI_AUTONOMY.md
+++ b/docs/AI_AUTONOMY.md
@@ -25,13 +25,13 @@ Patrol autonomy controls how aggressively Patrol responds to findings.
|-------|-----|:------:|:-----------:|:-------------:|:--------------:|------|
| **Monitor** | `monitor` | Yes | No | No | No | Community |
| **Approval** | `approval` | Yes | Yes | Approval required | Approval required | Pro / legacy Pro+ / Cloud |
-| **Assisted** | `assisted` | Yes | Yes | Auto-fix | Approval required | Pro / legacy Pro+ / Cloud |
-| **Full** | `full` | Yes | Yes | Auto-fix | Auto-fix | Pro / legacy Pro+ / Cloud |
+| **Assisted** | `assisted` | Yes | Yes | Execute automatically | Approval required | Pro / legacy Pro+ / Cloud |
+| **Full** | `full` | Yes | Yes | Execute automatically | Execute automatically | Pro / legacy Pro+ / Cloud |
- **Monitor** (default): Patrol creates findings but takes no action. This is the Community and Relay baseline. Suitable for learning what Patrol detects before enabling investigation or remediation.
- **Approval** (Pro and above): Patrol investigates findings and proposes fixes. All fixes queue for manual approval before execution.
-- **Assisted** (Pro and above): Warning-level findings are auto-fixed. Critical findings still require approval. This is the recommended starting point for most Pro and legacy Pro+ users who enable fix execution.
-- **Full** (Pro and above): All findings are auto-fixed without approval. Requires an explicit toggle and a Pro, legacy Pro+, or Cloud license. Recommended only for environments with thorough alert coverage.
+- **Assisted** (Pro and above): Warning-level safe remediation plans can execute automatically. Critical findings still require approval. This is the recommended starting point for most Pro and legacy Pro+ users who enable fix execution.
+- **Full** (Pro and above): Safe remediation plans can execute without approval. Requires an explicit toggle and a Pro, legacy Pro+, or Cloud license. Recommended only for environments with thorough alert coverage.
### Configuration
@@ -148,7 +148,7 @@ For new deployments, we recommend gradually increasing autonomy:
1. **Start with Monitor** — Run Patrol for a few cycles to see what it detects. Dismiss false positives.
2. **Move to Approval where available** — Enable investigation. Review proposed fixes to build confidence.
-3. **Use Assisted when fix execution is enabled** — Let Patrol auto-fix warnings while you approve critical fixes.
+3. **Use Assisted when fix execution is enabled** — Let Patrol execute warning-level remediation while you approve critical fixes.
4. **Consider Full** — Only if your environment has comprehensive alerting and you trust the fix patterns.
---
diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
index ebde916ef..ce9b20e54 100644
--- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
+++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
@@ -259,6 +259,7 @@ the manifest's support-floor row.
7. Add or change the unified agent CLI entrypoint, version/help exit semantics, or startup argument/error routing through `cmd/pulse-agent/main.go`.
The CLI entrypoint owns propagation of persistence context into runtime-owned helpers. When installer-selected state roots differ from the default, `cmd/pulse-agent/main.go` must pass that exact `StateDir` through both the host-agent runtime and updater startup paths instead of letting one path silently fall back to `/var/lib/pulse-agent`.
The same runtime-owned boundary also owns Pulse control-plane URL validation for agent startup, remote config, updater continuity, and command transport. Non-loopback control-plane URLs remain HTTPS/WSS by default, but explicitly insecure agent/dev-runtime flows may use plain HTTP/WS for LAN development control planes; installer-persisted dev URLs must not be accepted by one runtime path and rejected by another.
+ The unified agent CLI copy follows the same Patrol remediation vocabulary as the install surface. `cmd/pulse-agent/main.go` may keep the `--enable-commands` flag name for compatibility, but the help text and inline comments must describe command execution as Patrol remediation rather than reviving AI auto-fix language.
8. Add or change installer flags, persisted service arguments, or upgrade-safe re-entry behavior through `scripts/install.sh` and `scripts/install.ps1`.
Persistence-sensitive NAS targets must keep one canonical continuity model here: installer-owned bootstraps may use flash-backed or immutable-root launch hooks only as thin trampolines, while the durable wrapper, state, and reboot-surviving binary copy stay in the governed persistent state directory that updater continuity also refreshes.
Approval-gated command execution must expose stable rejection reasons for
diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md
index 74d011db3..d2ccb8880 100644
--- a/docs/release-control/v6/internal/subsystems/ai-runtime.md
+++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md
@@ -131,8 +131,9 @@ runtime cost control, and shared AI transport surfaces.
11. Keep AI runtime transport compatibility separate from operator-facing
product copy. Existing Patrol payload fields such as `fixed_count`,
`auto_fix_model`, and `patrol_auto_fix` may remain stable wire/API names,
- but frontend comments, status labels, and commercial prompts that describe
- the capability must use safe remediation or remediation wording.
+ but frontend comments, API denial messages, runtime logs, status labels,
+ CLI help, and commercial prompts that describe the capability must use safe
+ remediation or remediation wording.
## Current State
diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index 3c885b519..43f324f7b 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -338,11 +338,13 @@ the canonical monitored-system blocked payload.
25. Keep long-range workload chart transport time-proportional on the shared API surface: `internal/api/router.go`, `internal/api/contract_test.go`, and workload chart consumers must cap mixed-cadence workload history by equal-time buckets rather than raw point index for the per-workload and aggregate workload chart APIs, so 7-day and 30-day workload cards do not bunch recent samples at the right edge just because recent telemetry is stored more densely.
26. Keep chart timestamp precision canonical on that same shared API surface: when `internal/api/router.go` serializes monitoring history into infrastructure or workload chart payloads, it must preserve canonical millisecond timestamps from the shared monitoring timeline instead of rounding through whole-second conversion, so seeded mock history and live appends collapse onto one operator-visible timeline instead of appearing as duplicated tail samples.
27. Keep Patrol remediation payload naming backward-compatible without leaking
- legacy automation-first wording into product copy. `frontend-modern/src/api/patrol.ts`
- may continue to expose stable transport fields such as `fixed_count` and
- legacy `auto_fix` capability names, but comments and presentation labels
- layered on that API contract must describe the operator-visible capability
- as remediation or safe remediation workflows.
+ legacy automation-first wording into product copy. `frontend-modern/src/api/patrol.ts`,
+ `internal/api/ai_handlers.go`, and `internal/api/router_routes_ai_relay.go`
+ may continue to expose stable transport fields such as `fixed_count`,
+ `auto_fix`, `autofix`, and the `ai_autofix` capability name, but comments,
+ API license-required messages, and presentation labels layered on that API
+ contract must describe the operator-visible capability as remediation or
+ safe remediation workflows.
27. Keep storage chart identity canonical on that same shared API surface: the shared storage charts endpoint must key pool and physical-disk series by the resolved unified-resource `MetricsTarget.ResourceID`, not by canonical resource IDs or page-local aliases, so storage rows, focused summary cards, sticky summary shells, and detail charts all address the same history series in live and mock mode.
28. Keep synthetic summary-chart fallback identity canonical on that same shared API surface: when `internal/api/router.go` has to synthesize mock summary history for infrastructure, workloads, or storage cards, it must derive the fallback from canonical `resourceType`, `resourceID`, and `metricType` ownership instead of raw min/max seed-prefix helpers, so range changes and runtime mock updates stay on one governed timeline.
The same compact chart boundary also owns aggregate-only storage summary
diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md
index 446c41cbe..04c87deec 100644
--- a/docs/release-control/v6/internal/subsystems/cloud-paid.md
+++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md
@@ -491,9 +491,10 @@ Community limit enforcement.
may keep checkout, activation, recovery, and support-only trial plumbing
available for explicit handoffs and entitled installs, but default
self-hosted browser surfaces must honor `presentationPolicy.hideUpgrade`
- and suppress Relay/Pro plan comparison, Pro trial CTAs, paid-only settings
- navigation, and feature upsells unless hosted mode, direct intent,
- activation/recovery state, or active entitlement makes them relevant.
+ and suppress Relay/Pro plan comparison, Pro trial CTAs, monitored-system
+ limit pressure, paid-only settings navigation, and feature upsells unless
+ hosted mode, direct intent, activation/recovery state, or active entitlement
+ makes them relevant.
## Current State
@@ -504,9 +505,12 @@ self-hosted billing plan page instead of the Pulse Account purchase-start
handoff. The purchase-start handoff requires a `PublicURL` and fails on local
instances; routing these keys to the in-product billing plan keeps upgrades
accessible from self-hosted environments.
-The monitored-system app-shell warning CTA now follows that same self-hosted
-commercial boundary by reviewing finite-policy usage on the usage ledger rather
-than sending operators to the plan-selection surface with capacity-shaped copy.
+The monitored-system app-shell warning CTA now follows that same commercial
+boundary by rendering only in hosted mode. Ordinary self-hosted installs must
+not see finite monitored-system pressure in the global app shell; when hosted
+capacity policy is active, the banner reviews finite-policy usage on the usage
+ledger rather than sending operators to the plan-selection surface with
+capacity-shaped copy.
Cloud paid readiness is materially behind architecture work. The main concern is
contract coherence between pricing, entitlements, and runtime enforcement.
@@ -1554,7 +1558,8 @@ for that feature's owning subsystem — those are user-initiated discovery
paths, not blanket funnels, and are not required to be removed.
Public AI and entitlement docs must use the same boundary: Community/Relay may
describe Patrol background findings with BYOK, while investigation, proposed
-remediation, auto-fix, and higher autonomy remain paid AI-operations features.
+remediation, safe remediation execution, and higher autonomy remain paid
+AI-operations features.
Those docs should describe moving between available modes, not tell readers to
"upgrade" as part of an ordinary safety progression.
That same counted-unit boundary also owns the disclosure rule for retail copy:
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 9b2ad4f38..3088970e3 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -795,12 +795,13 @@ work extends shared components instead of creating new local variants.
takeover between tabs.
35. Keep self-hosted paid-service prompts opt-in at the shared shell layer.
`settingsNavCatalog.ts`, `settingsNavVisibility.ts`, shared upgrade link
- primitives, trial banners, and history-lock overlays must honor
- `presentationPolicy.hideUpgrade` by hiding paid prompts by default on
- ordinary self-hosted installs. Direct activation/recovery routes may
- render their owned content, but sidebar discovery, trial CTAs, plan upsells,
- and feature upgrade links must require hosted mode, explicit handoff, or
- active entitlement.
+ primitives, trial banners, monitored-system warning banners, history-lock
+ overlays, and Patrol lock helpers must honor `presentationPolicy.hideUpgrade`
+ by hiding paid prompts by default on ordinary self-hosted installs. Direct
+ activation/recovery routes may render their owned content, but sidebar
+ discovery, trial CTAs, plan upsells, monitored-system limit pressure, and
+ feature upgrade links must require hosted mode, explicit handoff, or active
+ entitlement.
## Current State
@@ -812,9 +813,10 @@ infrastructure area uses `InfrastructurePanelStep` in-page state.
The shared monitored-system warning banner now uses a neutral policy-review
CTA and `reviewPolicyDestination` state, keeping the render shell pointed at
the usage-owned policy ledger instead of plan-selection or capacity wording.
-It also follows the resolved session-presentation upgrade policy, so ordinary
-self-hosted sessions with `presentationPolicy.hideUpgrade` do not render the
-banner, its plan-review link, or its upgrade-impression telemetry.
+It also requires hosted mode and follows the resolved session-presentation
+upgrade policy, so ordinary self-hosted sessions do not render the banner, its
+plan-review link, or its upgrade-impression telemetry even when stale finite
+policy data is present.
Shared alert presentation surfaces (`OverviewTab.tsx`, `HistoryTab.tsx`,
`AlertOverviewActiveAlertsSection.tsx`, `AlertHistoryTableSection.tsx`,
`AlertHistoryTableAlertRow.tsx`, `AlertOverviewAlertCard.tsx`) no longer accept
@@ -1386,11 +1388,13 @@ parallel modal-stack bookkeeping.
The shared history chart now follows the same owner shape.
`frontend-modern/src/components/shared/HistoryChart.tsx` stays the render
shell, `frontend-modern/src/components/shared/useHistoryChartState.ts` owns
-license gating, trial actions, history fetch/refresh, canvas draw lifecycle,
-and hover state, and `frontend-modern/src/components/shared/historyChartModel.ts`
-owns tooltip formatting, scale and axis math, and closest-point selection.
-Future history-chart work should extend those owners instead of pushing fetch,
-license, or canvas math back into the shared component shell.
+license gating, history fetch/refresh, canvas draw lifecycle, and hover state,
+and `frontend-modern/src/components/shared/historyChartModel.ts` owns tooltip
+formatting, scale and axis math, and closest-point selection. Lock overlays in
+ordinary self-hosted surfaces must stay informational rather than presenting
+trial-start or upgrade-link actions. Future history-chart work should extend
+those owners instead of pushing fetch, license, commercial trial actions, or
+canvas math back into the shared component shell.
The remaining header, overlay, and tooltip render surfaces now live in
`frontend-modern/src/components/shared/HistoryChartHeader.tsx`,
`frontend-modern/src/components/shared/HistoryChartOverlay.tsx`, and
@@ -2113,20 +2117,22 @@ agent-era banner filename or component name as the primary primitive.
That shared monitored-system warning banner now also follows the shell/runtime/model
owner split. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx`
stays the render shell, `frontend-modern/src/components/shared/useMonitoredSystemLimitWarningBannerState.ts`
-owns entitlement load, warning metric emission, migration click tracking, and
-policy-review plus collector-link runtime, and
+owns hosted-mode eligibility, entitlement load, warning metric emission,
+migration click tracking, and policy-review plus collector-link runtime, and
`frontend-modern/src/components/shared/monitoredSystemLimitWarningBannerModel.ts`
owns monitored-system warning policy, count aggregation, and tone/text-class
policy while sourcing customer-facing monitored-system copy from the canonical
`frontend-modern/src/utils/monitoredSystemPresentation.ts` helper. Future
warning-banner work should extend those owners instead of pushing entitlement
-state or route selection back into the render shell. When the warning points at
-the self-hosted commercial surface, the shared primitive must stay a compact
+state, hosted-mode checks, or route selection back into the render shell. In
+ordinary self-hosted mode the banner must remain absent rather than turning
+stale finite policy data into monitored-system limit pressure. When hosted
+capacity policy is active, the shared primitive must stay a compact
policy-review pointer into the usage-owned ledger rather than a plan-selection
-or "capacity" CTA. The banner may signal the current monitored-system posture
-and link into the owned usage surface, but the longer over-plan or continuity
-explanation belongs in the bounded legacy usage ledger and commercial detail
-sections owned by `cloud-paid`, not in permanent app-shell banner copy.
+or "capacity" CTA. The banner may signal the current hosted monitored-system
+posture and link into the owned usage surface, but the longer over-plan or
+continuity explanation belongs in the bounded usage ledger and commercial
+detail sections owned by `cloud-paid`, not in permanent app-shell banner copy.
That same shared warning boundary now also owns the monitored-system capacity
posture vocabulary. Shared banners, plan summaries, and ledger headers must
describe the canonical admission-freeze model from
diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md
index e9a097a57..7547829a8 100644
--- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md
+++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md
@@ -169,14 +169,16 @@ commercial destinations from the shared license boundary, but they must leave
internal-versus-external navigation semantics to `frontend-primitives` once a
Patrol feature can resolve to either product-owned routes or public pricing.
That same Patrol-owned commercial boundary must also fail closed in public
-demo runtimes. Patrol header and banner upgrade/trial affordances may render
-for real customer workspaces, but the browser-owned trigger for that
-suppression is now the shared resolved `presentationPolicy` from
+demo and ordinary self-hosted runtimes. Patrol header, banner, approval, and
+history-adjacent lock surfaces must remain informational unless the operator
+is already in an entitled, hosted, or explicit commercial handoff context. The
+browser-owned trigger for self-hosted suppression is the shared resolved
+`presentationPolicy` from
`/api/security/status`, seeded by the backend capability fact
-`sessionCapabilities.demoMode`. Patrol surfaces must therefore suppress
-upgrade CTAs, trial nudges, and Pro-only helper copy only from that shared
-policy instead of reviving local demo heuristics or issuing early commercial
-reads before the policy resolves.
+`sessionCapabilities.demoMode` plus the self-hosted upgrade policy. Patrol
+surfaces must therefore suppress upgrade CTAs, trial nudges, checkout links,
+and Pro-only helper copy from that shared policy instead of reviving local demo
+heuristics or issuing early commercial reads before the policy resolves.
That same shared policy now also owns Patrol approval polling posture.
`frontend-modern/src/stores/aiIntelligence.ts` must fail
`loadPendingApprovals()` closed in public demo mode and return the canonical
@@ -199,12 +201,12 @@ That same store-owned demo boundary also covers remediation artifacts.
`/api/ai/remediation/plans` paywall probes after demo posture has resolved.
That same posture split now also centralizes Patrol commercial bootstrap.
`frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` and
-`frontend-modern/src/components/patrol/ApprovalSection.tsx` may consume the
-resolved commercial-posture store when Patrol needs upgrade or trial context,
-but they must not trigger their own mount-time `loadCommercialPosture()`
-reads. Authenticated Patrol shells inherit that bootstrap from
-`frontend-modern/src/useAppRuntimeState.ts`, so Patrol-specific hooks do not
-quietly retake ownership of commercial fetch timing.
+`frontend-modern/src/components/patrol/ApprovalSection.tsx` must not mount
+commercial trial or upgrade state merely because an ordinary self-hosted
+operator opens Patrol. Authenticated Patrol shells inherit any allowed
+commercial bootstrap from `frontend-modern/src/useAppRuntimeState.ts`, so
+Patrol-specific hooks do not quietly retake ownership of commercial fetch
+timing or reintroduce local trial-start actions.
Under ordinary self-hosted v6, Patrol commercial affordances must also honor
the shared `presentationPolicy.hideUpgrade` contract. A free self-hosted
install may show Patrol runtime availability and configuration gaps, but it
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index 5fe29cf5b..50f6a3989 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -710,6 +710,12 @@ fields and nullable mode/kind metadata before presenting canonical item labels,
while storage detail drawers and filter controls must route summary series IDs,
source tones, and disk metrics through the shared storage helpers instead of
reconstructing them from local table state.
+Storage and recovery's adjacent `internal/api/` contract must also preserve
+the product-facing remediation vocabulary used by shared API denials. When
+storage/recovery-adjacent browser sessions encounter AI or Patrol remediation
+license responses while sharing the app shell, those API messages must use
+safe remediation wording and must not revive `Auto-Fix` as customer-facing
+paid copy.
That same adjacent `internal/api/` router boundary now also keeps usage-data
transport descriptive-only for storage and recovery. Shared storage/recovery
surfaces may coexist with `/api/upgrade-metrics/*` config reads and telemetry
diff --git a/frontend-modern/src/components/patrol/ApprovalSection.tsx b/frontend-modern/src/components/patrol/ApprovalSection.tsx
index cc69c9799..56a348312 100644
--- a/frontend-modern/src/components/patrol/ApprovalSection.tsx
+++ b/frontend-modern/src/components/patrol/ApprovalSection.tsx
@@ -9,12 +9,10 @@ import { Component, Show, createSignal, createResource, createMemo } from 'solid
import { aiIntelligenceStore } from '@/stores/aiIntelligence';
import { notificationStore } from '@/stores/notifications';
import { aiChatStore } from '@/stores/aiChat';
-import { canStartCommercialTrial } from '@/stores/licenseCommercial';
import { hasFeature } from '@/stores/license';
import { AIAPI, type ApprovalRequest, type ApprovalExecutionResult } from '@/api/ai';
import { getApprovalRiskPresentation } from '@/utils/approvalRiskPresentation';
import { RemediationStatus } from './RemediationStatus';
-import { runStartProTrialAction } from '@/utils/trialStartAction';
interface ApprovalSectionProps {
findingId: string;
@@ -40,24 +38,6 @@ export const ApprovalSection: Component = (props) => {
const canAutoFix = createMemo(() => hasFeature('ai_autofix'));
- const [startingTrial, setStartingTrial] = createSignal(false);
- const canStartTrial = createMemo(() => canStartCommercialTrial());
-
- const handleStartTrial = async (e: Event) => {
- e.stopPropagation();
- if (startingTrial()) return;
- setStartingTrial(true);
- try {
- await runStartProTrialAction({
- branded: true,
- showSuccess: notificationStore.success,
- showError: notificationStore.error,
- });
- } finally {
- setStartingTrial(false);
- }
- };
-
const handleFixWithAssistant = (
approval: ApprovalRequest | null,
fix: {
@@ -271,16 +251,6 @@ export const ApprovalSection: Component = (props) => {
{assistantLabel}
-
-
-
);
@@ -380,16 +350,6 @@ export const ApprovalSection: Component = (props) => {
Fix with Assistant
-
-
-
>
diff --git a/frontend-modern/src/components/patrol/__tests__/ApprovalSection.test.tsx b/frontend-modern/src/components/patrol/__tests__/ApprovalSection.test.tsx
index 392d58151..27ec87263 100644
--- a/frontend-modern/src/components/patrol/__tests__/ApprovalSection.test.tsx
+++ b/frontend-modern/src/components/patrol/__tests__/ApprovalSection.test.tsx
@@ -22,7 +22,6 @@ const denyInvestigationFixMock = vi.hoisted(() => vi.fn());
const notificationSuccessMock = vi.hoisted(() => vi.fn());
const notificationErrorMock = vi.hoisted(() => vi.fn());
const openWithPromptMock = vi.hoisted(() => vi.fn());
-const startProTrialMock = vi.hoisted(() => vi.fn());
vi.mock('@/api/ai', () => ({
AIAPI: {
@@ -62,13 +61,7 @@ vi.mock('@/stores/license', () => ({
}));
vi.mock('@/stores/licenseCommercial', () => ({
- canStartCommercialTrial: () => {
- const ent = state.entitlements;
- if (!ent) return false;
- if (ent.subscription_state === 'active' || ent.subscription_state === 'trial') return false;
- return ent.trial_eligible !== false;
- },
- startProTrial: (...args: unknown[]) => startProTrialMock(...args),
+ canStartCommercialTrial: () => false,
}));
vi.mock('../RemediationStatus', () => ({
@@ -93,15 +86,16 @@ describe('ApprovalSection', () => {
notificationSuccessMock.mockReset();
notificationErrorMock.mockReset();
openWithPromptMock.mockReset();
- startProTrialMock.mockReset();
});
afterEach(() => {
cleanup();
});
- it('keeps patrol trial gating on shared commercial selector helpers', () => {
- expect(approvalSectionSource).toContain('canStartCommercialTrial');
+ it('keeps fix approvals out of commercial trial prompts', () => {
+ expect(approvalSectionSource).not.toContain('canStartCommercialTrial');
+ expect(approvalSectionSource).not.toContain('runStartProTrialAction');
+ expect(approvalSectionSource).not.toContain('start a free 14-day trial');
expect(approvalSectionSource).not.toContain('commercialPosture');
});
diff --git a/frontend-modern/src/components/shared/HistoryChartOverlay.tsx b/frontend-modern/src/components/shared/HistoryChartOverlay.tsx
index 490bc6c89..f044ac4f0 100644
--- a/frontend-modern/src/components/shared/HistoryChartOverlay.tsx
+++ b/frontend-modern/src/components/shared/HistoryChartOverlay.tsx
@@ -1,6 +1,4 @@
import { Component, Show } from 'solid-js';
-import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
-import { UpgradeLink } from './UpgradeLink';
import type { HistoryChartState } from './useHistoryChartState';
interface HistoryChartOverlayProps {
@@ -74,41 +72,9 @@ export const HistoryChartOverlay: Component = (props)
{props.chart.lockDays()}-Day History
-
- Historical data beyond {props.chart.lockDays()} days is not enabled on this
- instance.
-
- }
- >
-
- Upgrade to {props.chart.lockTierLabel()} to unlock {props.chart.lockDays()} days of
- historical data retention.
-
- trackUpgradeClicked('ai_intelligence_banner', 'ai_autofix')}
- >
- Upgrade to Pro
- {' '}
- to unlock safe remediation workflows and alert-triggered root-cause analysis.
+ Safe remediation workflows and alert-triggered root-cause analysis are not enabled
+ on this plan.
@@ -89,15 +80,6 @@ export function PatrolIntelligenceBanners(props: { state: PatrolIntelligenceStat
Open Patrol provider settings
-
-
-
- Upgrade
-
-
diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx
index 1f1d957c2..40a75a175 100644
--- a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx
+++ b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx
@@ -6,7 +6,6 @@ import XIcon from 'lucide-solid/icons/x';
import SettingsIcon from 'lucide-solid/icons/settings';
import { PulsePatrolLogo } from '@/components/Brand/PulsePatrolLogo';
import { PageHeader } from '@/components/shared/PageHeader';
-import { UpgradeLink } from '@/components/shared/UpgradeLink';
import { Toggle, TogglePrimitive } from '@/components/shared/Toggle';
import { CountdownTimer } from '@/components/patrol';
import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
@@ -274,8 +273,8 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
title={
!presentationPolicyHidesUpgradePrompts() && isProLocked()
? level === 'approval'
- ? 'Upgrade to Pro to investigate findings'
- : 'Upgrade to Pro for safe remediation workflows'
+ ? 'Investigation is not enabled on this plan'
+ : 'Safe remediation workflows are not enabled on this plan'
: undefined
}
class={`flex-1 py-1.5 px-2 text-xs font-semibold rounded-md transition-all duration-200 ${isActive() ? ' text-blue-600 dark:text-blue-400 shadow-[0_1px_3px_rgba(0,0,0,0.1)]' : isDisabled() ? ' ' : 'text-muted hover:text-base-content hover:bg-surface-hover'} ${isDisabled() ? 'opacity-50 cursor-not-allowed' : ''}`}
@@ -292,24 +291,7 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
-
- Upgrade to Pro
- {' '}
- to unlock investigation and safe remediation workflows.
-
- {' '}
-
-
+ Investigation and safe remediation workflows are not enabled on this plan.