From d2625c4dfbec62dc40497762a9d1af5aeaf0f032 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 7 May 2026 19:26:00 +0100 Subject: [PATCH] Persist Patrol settings with readiness handoff Refs #1463 --- docs/release-control/v6/internal/status.json | 34 +++- .../v6/internal/subsystems/agent-lifecycle.md | 4 + .../v6/internal/subsystems/ai-runtime.md | 9 +- .../v6/internal/subsystems/api-contracts.md | 14 +- .../subsystems/frontend-primitives.md | 5 +- .../subsystems/patrol-intelligence.md | 23 ++- .../internal/subsystems/storage-recovery.md | 4 + .../src/api/__tests__/patrol.test.ts | 6 + frontend-modern/src/api/patrol.ts | 3 + .../patrol/PatrolIntelligenceHeader.tsx | 30 +++ .../patrolInvestigationContextModel.test.ts | 52 ++++++ .../patrol/patrolInvestigationContextModel.ts | 174 ++++++++++++++++++ .../patrol/usePatrolIntelligenceState.ts | 66 ++++++- frontend-modern/src/types/ai.ts | 23 +++ internal/ai/findings.go | 4 + internal/ai/investigation_records.go | 1 + internal/ai/investigation_records_test.go | 4 + internal/ai/patrol.go | 2 + internal/ai/patrol_findings.go | 6 + internal/ai/patrol_init.go | 7 +- internal/ai/patrol_readiness.go | 57 ++++-- internal/ai/patrol_readiness_test.go | 100 ++++++++++ internal/ai/patrol_run.go | 21 ++- internal/ai/patrol_runtime_failure.go | 11 ++ internal/ai/patrol_runtime_failure_test.go | 15 ++ internal/ai/service.go | 2 + internal/api/ai_handlers.go | 100 +++++++--- internal/api/ai_handlers_test.go | 21 ++- internal/api/contract_test.go | 6 +- pkg/aicontracts/investigation.go | 1 + .../release_control/subsystem_lookup_test.py | 2 +- .../tests/18-patrol-runtime-state.spec.ts | 28 +++ 32 files changed, 753 insertions(+), 82 deletions(-) create mode 100644 internal/ai/patrol_readiness_test.go diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index e19564538..05365edd4 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -2424,7 +2424,7 @@ }, { "id": "RA27", - "summary": "Patrol readiness owns server-authored runtime and configuration safety: provider, model, settings-persistence, and tool-calling prerequisites must travel as structured `/api/ai/patrol/status`, settings-save, and Patrol action errors, and known not-ready states must block readiness-sensitive settings saves plus manual, scheduled, and scoped Patrol runs before they become generic interrupted-analysis failures.", + "summary": "Patrol readiness owns server-authored runtime and configuration safety: provider, model, settings-persistence, and tool-calling prerequisites must travel as structured `/api/ai/patrol/status`, settings-save, and Patrol action errors. Settings saves must persist recoverable provider/model changes while returning structured readiness, and known not-ready states must block manual, scheduled, and scoped Patrol runs before they become generic interrupted-analysis failures.", "kind": "invariant", "blocking_level": "repo-ready", "proof_type": "automated", @@ -2484,7 +2484,7 @@ "test", "./internal/api", "-run", - "UpdateSettingsRejectsNotReadyPatrolModel|UpdateSettingsDoesNotLockUnrelatedSavesBehindExistingPatrolReadiness|HandleForcePatrol_BlocksNotReadyPatrolModel", + "UpdateSettingsPersistsNotReadyPatrolModelWithReadiness|UpdateSettingsDoesNotLockUnrelatedSavesBehindExistingPatrolReadiness|HandleForcePatrol_BlocksNotReadyPatrolModel", "-count=1" ] }, @@ -2531,11 +2531,26 @@ "path": "frontend-modern/src/api/patrol.ts", "kind": "file" }, + { + "repo": "pulse", + "path": "frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts", + "kind": "file" + }, { "repo": "pulse", "path": "frontend-modern/src/features/patrol/PatrolIntelligenceBanners.tsx", "kind": "file" }, + { + "repo": "pulse", + "path": "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts", + "kind": "file" + }, { "repo": "pulse", "path": "frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts", @@ -2561,11 +2576,26 @@ "path": "internal/ai/patrol_readiness.go", "kind": "file" }, + { + "repo": "pulse", + "path": "internal/ai/patrol_readiness_test.go", + "kind": "file" + }, { "repo": "pulse", "path": "internal/ai/patrol_run.go", "kind": "file" }, + { + "repo": "pulse", + "path": "internal/ai/patrol_runtime_failure.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/ai/patrol_runtime_failure_test.go", + "kind": "file" + }, { "repo": "pulse", "path": "internal/ai/service.go", diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index adfba8158..8dfe5315c 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -218,6 +218,10 @@ profile and assignment columns, but embedded table framing must route through `internal/api/` route wiring: monitor-mode configuration and remediation entitlement payloads remain AI runtime/API-contract owned and must not create agent lifecycle authority, install-token scope, or fleet command semantics. + Patrol readiness and settings-save payload changes on those shared handlers + are also adjacent only: structured provider/model/tool causes may be exposed + to Patrol and Assistant, but they do not grant agent install, enrollment, + or fleet command authority. Hosted handoff subjects consumed through the shared API auth boundary must already be stable, non-email principals; lifecycle-adjacent routes must not recover authority from a blank handoff subject by falling back to contact diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 2f7c1105f..662dcb834 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -111,10 +111,11 @@ runtime cost control, and shared AI transport surfaces. model, settings-persistence, and tool-calling prerequisites so the UI can block known-bad manual Patrol runs before they become generic runtime failures. The same `internal/ai` readiness evaluation must gate Patrol - runtime admission directly: readiness-sensitive settings saves, manual run - requests, scheduled ticks, and scoped alert/anomaly runs must fail or skip - before LLM execution when the selected Patrol model/provider is known - not-ready. + runtime admission directly: settings saves that are needed to recover a bad + provider/model selection must persist and return structured readiness cause + metadata, while manual run requests, scheduled ticks, and scoped + alert/anomaly runs must fail or skip before LLM execution when the selected + Patrol model/provider is known not-ready. 4. Keep discovery scheduling authoritative through `internal/config/ai.go`: `discovery_enabled` and `discovery_interval_hours` must govern both lightweight infrastructure discovery and deep service-discovery background loops 5. Preserve auditability for outbound model-bound context exports and keep the export record aligned with the prompt boundary that actually reaches the provider External provider-bound unified-resource context must enforce the same diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 4980fa46e..bfa5bf348 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -114,10 +114,11 @@ product API routes free of maintainer commercial analytics. The Patrol status payload owns Patrol readiness as structured API state: provider/model/settings/tool prerequisites must travel as bounded readiness checks instead of frontend-only heuristics or generic analysis-failed text. - `/api/settings/ai/update` and `/api/ai/patrol/run` must use that same - `patrol_readiness_not_ready` error taxonomy when they reject a known-bad - Patrol runtime configuration, with bounded `status`, `provider`, and - `model` details where available. + `/api/settings/ai/update` must persist recoverable provider/model changes + and return the same structured readiness cause in its settings payload, while + `/api/ai/patrol/run` must use the `patrol_readiness_not_ready` error taxonomy + when it rejects a known-bad Patrol runtime configuration. Bounded `status`, + `cause`, `provider`, and `model` details are the canonical transport shape. 7. `frontend-modern/src/api/rbac.ts` shared with `organization-settings`: the RBAC frontend client is both an organization settings control surface and a canonical API payload contract boundary. 8. `frontend-modern/src/api/security.ts` shared with `security-privacy`: the security frontend client is both a security/privacy control surface and a canonical API payload contract boundary. 9. `frontend-modern/src/api/updates.ts` shared with `deployment-installability`: the updates frontend client is both a deployment-installability control surface and a canonical API payload contract boundary. @@ -838,6 +839,11 @@ the canonical monitored-system blocked payload. may persist only `monitor` autonomy settings through `/api/ai/patrol/autonomy`, while `approval`, `assisted`, and `full` return the canonical license-required response instead of a generic save failure + and the Patrol settings-save readiness contract, so + `/api/settings/ai/update` may save a selected Patrol provider/model even + when that model is not ready for tool-backed Patrol execution, but it must + echo `patrol_readiness` with stable `cause` metadata and execution routes + must continue to fail closed before model calls and the structured investigation-record contract, so unified findings may expose `investigation_record` only through the shared `aicontracts.InvestigationRecord` payload shape, with frontend API types diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index ed0d0b899..1eedd67e2 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -709,7 +709,10 @@ frontend primitive boundary. names remain hidden from operators. The Patrol configuration popover is part of that shared feature-presentation boundary: it must stay viewport-bounded, expose an accessible dialog label, and pass backend save rejection reasons - through to the toast surface instead of replacing them with generic copy. + through as inline dialog state instead of replacing them with generic toast + copy. If that inline state opens Assistant, the Patrol feature must hand off + a source-named, model-only briefing and close the popover so the shared + Assistant drawer is not visually hidden behind feature chrome. 19. Keep the shared `system-ai` settings shell product-first. `frontend-modern/src/components/Settings/AISettings.tsx`, `frontend-modern/src/components/Settings/settingsHeaderMeta.ts`, diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index c3f547477..e233c24a2 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -84,9 +84,10 @@ Patrol-specific presentation helpers. managed credits or account-backed AI access. Server-authored Patrol readiness from the status payload is part of the Patrol product surface: warnings must be visible before a run starts, and - known not-ready states must block readiness-sensitive settings saves plus - manual, scheduled, and scoped Patrol runs instead of letting operators - discover provider/model/tool incompatibility through a failed run. + known not-ready states must keep recoverable provider/model settings saves + visible and actionable while blocking manual, scheduled, and scoped Patrol + runs instead of letting operators discover provider/model/tool + incompatibility through a failed run. 5. Keep customer-facing Patrol naming product-first: page titles, route chrome, summary copy, actions, and empty states should lead with `Patrol` or `Pulse Patrol` rather than generic `AI` branding. Reserve `AI` terminology @@ -350,13 +351,17 @@ That same browser proof now covers the Patrol configuration save contract. The advanced Patrol panel must stay within the desktop viewport, scroll its own contents to the Apply control, and surface the backend's concrete license/validation reason when a save is rejected instead of replacing it with -a generic `Failed to save advanced settings` toast. +a generic `Failed to save advanced settings` toast. That inline failure may +handoff to Assistant only as model-only explanation context: raw command, +script, credential, and provider-detail payloads stay redacted, Assistant opens +with `autonomousMode:false`, and the configuration panel closes so the operator +is not left behind an overlapping popover. The readiness contract now applies before Patrol work is admitted, not only -after a page render: readiness-sensitive settings saves must reject known-bad -Patrol runtime configurations, manual run requests must return the structured -readiness reason if a stale UI still submits, and scheduled or scoped -alert/anomaly runs must skip before calling the model while preserving the -blocked reason in Patrol status. +after a page render: recoverable Patrol provider/model settings saves must +persist and echo structured readiness cause metadata, manual run requests must +return the structured readiness reason if a stale UI still submits, and +scheduled or scoped alert/anomaly runs must skip before calling the model while +preserving the blocked reason and cause in Patrol status. That same Patrol-owned presentation rule also applies to the findings empty state: `frontend-modern/src/components/AI/FindingsPanel.tsx` must not treat `0 active findings` as equivalent to "your infrastructure looks healthy" when diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 7be730ad1..e1bf3e91a 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -221,6 +221,10 @@ bypass the API fail-closed execution gate. those notes. Recovery-adjacent diagnostics consumers must preserve the source-specific Docker / Podman wording and recovery destinations governed by the shared diagnostics API contract. + When shared `internal/api/` handlers expose structured Patrol readiness or + provider/model/tool causes, storage and recovery surfaces may treat them only + as adjacent operator context and must not convert them into storage health, + recovery execution, or backup remediation authority. Shared Patrol autonomy routes may also touch broad `internal/api/` wiring, but monitor-mode AI configuration and remediation entitlement responses stay AI runtime/API-contract owned and must not become recovery-local policy, diff --git a/frontend-modern/src/api/__tests__/patrol.test.ts b/frontend-modern/src/api/__tests__/patrol.test.ts index ebae98c7b..1212c66cd 100644 --- a/frontend-modern/src/api/__tests__/patrol.test.ts +++ b/frontend-modern/src/api/__tests__/patrol.test.ts @@ -63,10 +63,12 @@ describe('patrol api', () => { apiFetchJSONMock.mockResolvedValueOnce({ runtime_state: 'blocked', blocked_reason: 'Connect a provider to power Pulse Assistant and Patrol.', + blocked_cause: 'provider_not_configured', healthy: false, readiness: { status: 'not_ready', ready: false, + cause: 'model_unsupported_tools', summary: 'The selected Patrol model is a reasoning-only model family that commonly does not emit tool calls.', provider: 'ollama', @@ -75,6 +77,7 @@ describe('patrol api', () => { { id: 'tools', status: 'not_ready', + cause: 'model_unsupported_tools', label: 'Patrol tools', message: 'The selected Patrol model is a reasoning-only model family that commonly does not emit tool calls.', @@ -87,16 +90,19 @@ describe('patrol api', () => { await expect(getPatrolStatus()).resolves.toMatchObject({ runtime_state: 'blocked', blocked_reason: 'Connect a provider to power Pulse Assistant and Patrol.', + blocked_cause: 'provider_not_configured', healthy: false, readiness: { status: 'not_ready', ready: false, + cause: 'model_unsupported_tools', provider: 'ollama', model: 'ollama:deepseek-r1:7b-llama-distill-q4_K_M', checks: [ { id: 'tools', status: 'not_ready', + cause: 'model_unsupported_tools', action: 'open_provider_settings', }, ], diff --git a/frontend-modern/src/api/patrol.ts b/frontend-modern/src/api/patrol.ts index 5cb44d1c2..6ed282705 100644 --- a/frontend-modern/src/api/patrol.ts +++ b/frontend-modern/src/api/patrol.ts @@ -146,6 +146,7 @@ export type PatrolReadinessStatus = 'ready' | 'warning' | 'not_ready'; export interface PatrolReadinessCheck { id: string; status: PatrolReadinessStatus; + cause?: string; label: string; message: string; action?: string; @@ -154,6 +155,7 @@ export interface PatrolReadinessCheck { export interface PatrolReadiness { status: PatrolReadinessStatus; ready: boolean; + cause?: string; summary: string; provider?: string; model?: string; @@ -186,6 +188,7 @@ export interface PatrolStatus { interval_ms: number; // Patrol interval in milliseconds fixed_count: number; // Number of issues remediated by Patrol blocked_reason?: string; // Canonical server-authored Patrol block reason. + blocked_cause?: string; blocked_at?: string; license_required?: boolean; license_status?: LicenseStatus; diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx index ee682e80b..c512c2ac2 100644 --- a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx +++ b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx @@ -2,6 +2,7 @@ import { createMemo, For, Show } from 'solid-js'; import RefreshCwIcon from 'lucide-solid/icons/refresh-cw'; import PlayIcon from 'lucide-solid/icons/play'; import CircleHelpIcon from 'lucide-solid/icons/circle-help'; +import MessageSquareIcon from 'lucide-solid/icons/message-square'; import XIcon from 'lucide-solid/icons/x'; import SettingsIcon from 'lucide-solid/icons/settings'; import { PulsePatrolLogo } from '@/components/Brand/PulsePatrolLogo'; @@ -404,6 +405,35 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
+ + {(failure) => ( + + )} + +