From eac7dfe9ef18e531db6a950a96077ccd2ed95628 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 15 May 2026 12:22:52 +0100 Subject: [PATCH] Make Patrol reasoning model-owned --- docs/release-control/v6/internal/status.json | 77 ++- .../v6/internal/subsystems/ai-runtime.md | 42 +- .../v6/internal/subsystems/api-contracts.md | 57 +- .../subsystems/frontend-primitives.md | 79 ++- .../subsystems/patrol-intelligence.md | 106 ++-- .../internal/subsystems/security-privacy.md | 5 +- .../internal/subsystems/storage-recovery.md | 6 +- frontend-modern/src/api/__tests__/ai.test.ts | 4 +- .../src/api/__tests__/aiChat.test.ts | 12 - frontend-modern/src/api/ai.ts | 18 +- frontend-modern/src/api/aiChat.ts | 15 - .../AI/Chat/__tests__/AIChat.test.tsx | 31 +- .../src/components/AI/Chat/index.tsx | 110 +--- .../src/components/AI/FindingsPanel.tsx | 228 +------- .../FindingsPanel.capacityForecast.test.tsx | 312 ----------- .../AI/__tests__/FindingsPanel.test.ts | 48 +- .../patrol/__tests__/ApprovalSection.test.tsx | 21 +- .../patrol/PatrolIntelligenceSummary.tsx | 239 -------- .../PatrolIntelligenceSummary.test.tsx | 56 +- .../patrolInvestigationContextModel.test.ts | 304 ++-------- .../patrol/patrolInvestigationContextModel.ts | 530 +----------------- .../pages/__tests__/AIIntelligence.test.tsx | 15 +- .../src/stores/__tests__/aiChat.test.ts | 6 - frontend-modern/src/stores/aiChat.ts | 5 - frontend-modern/src/types/ai.ts | 5 - .../__tests__/aiChatPresentation.test.ts | 16 +- .../patrolSummaryPresentation.test.ts | 135 ----- .../src/utils/aiChatPresentation.ts | 19 +- .../src/utils/patrolSummaryPresentation.ts | 148 ----- .../chat/service_execute_additional_test.go | 4 +- internal/ai/chat/session.go | 260 +-------- internal/ai/chat/session_additional_test.go | 156 +----- internal/ai/chat/types.go | 50 +- .../ai/forecast/capacity_action_templates.go | 293 ---------- .../capacity_action_templates_test.go | 217 ------- internal/ai/patrol.go | 4 +- internal/ai/patrol_alerts_review_test.go | 35 +- internal/ai/patrol_findings.go | 281 +--------- .../ai/patrol_findings_additional_test.go | 58 +- .../ai/patrol_findings_capacity_proposal.go | 191 ------- .../patrol_findings_capacity_proposal_test.go | 203 ------- internal/ai/patrol_run.go | 40 +- internal/ai/patrol_run_test.go | 26 +- internal/ai/patrol_runtime_failure.go | 2 +- internal/ai/unified/store_additional_test.go | 2 +- pkg/aicontracts/remediation.go | 25 +- 46 files changed, 445 insertions(+), 4051 deletions(-) delete mode 100644 frontend-modern/src/components/AI/__tests__/FindingsPanel.capacityForecast.test.tsx delete mode 100644 internal/ai/forecast/capacity_action_templates.go delete mode 100644 internal/ai/forecast/capacity_action_templates_test.go delete mode 100644 internal/ai/patrol_findings_capacity_proposal.go delete mode 100644 internal/ai/patrol_findings_capacity_proposal_test.go diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index 12c174ed4..0062d39dd 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -2739,7 +2739,7 @@ }, { "id": "RA28", - "summary": "Pulse Patrol remains a simple scheduled LLM probe and governance surface: Pulse supplies observed infrastructure context, evidence, policy posture, approval state, and governed execution boundaries, while the configured LLM owns diagnosis and remediation reasoning. Patrol UI and Assistant handoffs must not present Patrol-authored remediation plans, internal step lists, command summaries, or suggested prompt chips as the answer.", + "summary": "Pulse Patrol remains a simple scheduled LLM probe and governance surface: Pulse supplies observed infrastructure context, evidence, policy posture, approval state, and governed execution boundaries, while the configured LLM owns diagnosis, alert-resolution judgment, remediation reasoning, tool choice, and next-step reasoning. Patrol UI, Assistant session summaries, and Assistant handoffs must not present or persist Patrol-authored remediation plans, internal step lists, command summaries, next-step recommendations, route-owned recommendation metadata, or suggested prompt chips as the answer.", "kind": "invariant", "blocking_level": "repo-ready", "proof_type": "automated", @@ -2767,11 +2767,37 @@ "--", "--run", "src/components/AI/Chat/__tests__/AIChat.test.tsx", + "src/components/AI/Chat/__tests__/ChatMessages.test.tsx", "src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts", "src/components/patrol/__tests__/ApprovalSection.test.tsx", "src/components/AI/__tests__/FindingsPanel.test.ts", - "src/components/AI/__tests__/FindingsPanel.capacityForecast.test.tsx", - "src/utils/__tests__/patrolPagePresentation.test.ts" + "src/features/patrol/__tests__/PatrolIntelligenceSummary.test.tsx", + "src/pages/__tests__/AIIntelligence.test.tsx", + "src/utils/__tests__/patrolPagePresentation.test.ts", + "src/utils/__tests__/patrolSummaryPresentation.test.ts", + "src/utils/__tests__/aiChatPresentation.test.ts", + "src/api/__tests__/aiChat.test.ts", + "src/stores/__tests__/aiChat.test.ts" + ] + }, + { + "id": "patrol-assistant-session-summary-tests", + "run": [ + "go", + "test", + "./internal/ai/chat", + "-run", + "TestSessionStore_List|TestService_ExecuteStream_AttachesModelOnlyHandoffContext" + ] + }, + { + "id": "patrol-model-owned-remediation-runtime-tests", + "run": [ + "go", + "test", + "./internal/ai", + "-run", + "TestPatrolService_RecordFindingDoesNotGenerateTemplateRemediationPlan|TestShouldResolveAlert|TestReviewAndResolveAlerts" ] } ], @@ -2801,11 +2827,6 @@ "path": "docs/release-control/v6/internal/subsystems/patrol-intelligence.md", "kind": "file" }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/__tests__/FindingsPanel.capacityForecast.test.tsx", - "kind": "file" - }, { "repo": "pulse", "path": "frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts", @@ -2816,6 +2837,11 @@ "path": "frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx", "kind": "file" }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/AI/Chat/__tests__/ChatMessages.test.tsx", + "kind": "file" + }, { "repo": "pulse", "path": "frontend-modern/src/components/AI/Chat/index.tsx", @@ -2831,6 +2857,11 @@ "path": "frontend-modern/src/components/patrol/__tests__/ApprovalSection.test.tsx", "kind": "file" }, + { + "repo": "pulse", + "path": "frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceSummary.test.tsx", + "kind": "file" + }, { "repo": "pulse", "path": "frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts", @@ -2841,15 +2872,45 @@ "path": "frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts", "kind": "file" }, + { + "repo": "pulse", + "path": "frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx", + "kind": "file" + }, { "repo": "pulse", "path": "frontend-modern/src/utils/__tests__/patrolPagePresentation.test.ts", "kind": "file" }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/aiChatPresentation.ts", + "kind": "file" + }, { "repo": "pulse", "path": "frontend-modern/src/utils/patrolPagePresentation.ts", "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/ai/chat/session.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/ai/chat/types.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/ai/patrol_findings.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/ai/patrol_run.go", + "kind": "file" } ] }, diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index cdaf668f0..0a56cfe07 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -512,12 +512,11 @@ runtime cost control, and shared AI transport surfaces. handoffs. That summary may include the handoff kind, finding ID, resource and Patrol run ID, safe run type/status/runtime-failure flags, resource and action counts, a primary resource label, last-known approval/action status, - risk level, timestamp, and Patrol recommended next-step title/detail/action - labels plus the safe recommendation action kind or whitelisted app-route href only when - they can be safely extracted from the stored Patrol handoff, but it must not - expose model-only handoff text, - runtime failure detail, action preflight/result bodies, remediation - descriptions, raw commands, or approval command payloads. Its + risk level, and timestamp, but it must not expose Patrol-authored + recommended next-step titles, recommendation details, route-owned action + labels, model-only handoff text, runtime failure detail, action + preflight/result bodies, remediation descriptions, raw commands, or + approval command payloads. Its `requires_approval` field is a current operator-decision flag only: pending approval states may set it, but approved, denied, rejected, executing, completed, failed, expired, or otherwise historical action references must @@ -527,8 +526,8 @@ runtime cost control, and shared AI transport surfaces. safe visible briefing: the next chat turn must carry `autonomous_mode:false` even when the summary is context-only and has no queued action, while the visible badge/action copy must still reflect the - actual last-known action state or Patrol assessment recommendation instead - of inventing a pending approval. That + actual last-known action state or Patrol assessment context instead of + inventing a pending approval or restoring a Patrol recommendation. That restoration is success-bound: if the underlying session message load fails, the drawer must leave the current context untouched instead of applying summary-derived Patrol or approval state for a session the operator is not @@ -542,20 +541,17 @@ runtime cost control, and shared AI transport surfaces. The Assistant drawer must also fetch that current session list before opening the session picker instead of presenting mount-time cached summaries as the operator's decision surface. For restored Patrol - assessment or finding sessions, that picker must present the safe - recommended next-step title/detail/action label from `handoff_summary` when - one is available and restore the safe recommendation detail, action kind, - or route-owned href as context metadata instead of reducing the saved - session to generic context. - Live Patrol assessment handoffs that include a currently unavailable - Patrol-owned recommendation action must carry the bounded disabled reason in - the model-only handoff and visible briefing so Assistant explains the - current availability state instead of treating the action as executable. + assessment or finding sessions, that picker must present only the safe + handoff kind, source, resource/action counts, and approval/action status + from `handoff_summary`; it must not restore Patrol recommended + next-step title/detail/action labels, route-owned hrefs, or disabled-action + reasons as visible or hidden context. Browser-originated `handoff_context`, `handoff_resources`, and `handoff_actions` plus safe `handoff_metadata` are one-shot request seeds - for the first successful chat turn. Safe Patrol next-step titles, details, - labels, and route-owned hrefs belong in `handoff_metadata` first, with model-context - text parsing only as a legacy fallback. After that send succeeds, the drawer + for the first successful chat turn. Patrol next-step titles, details, + labels, and route-owned hrefs do not belong in `handoff_metadata`, and + model-context text parsing must not resurrect them as a legacy fallback. + After that send succeeds, the drawer must clear those request payloads while preserving the safe visible briefing and request-local approval-required posture; later turns must rely on backend-owned session @@ -1528,9 +1524,9 @@ derived from the current unified finding and structured Patrol investigation record before the lower-level finding context. That briefing must summarize the finding, resource, priority, current attention reason, current recency facts, bounded evidence and verification summaries, investigation confidence, -recommended next step, operator decision framing, latest lifecycle event, and -governed action posture as operator guidance, while leaving detailed lifecycle -history, current resource-state, timeline, related-finding, and action-audit +operator decision framing, latest lifecycle event, and governed action posture +as model context, while leaving detailed lifecycle history, current +resource-state, timeline, related-finding, and action-audit hydration in the existing canonical AI runtime handoff builders. Related root-cause and correlated finding records may be summarized from current unified finding state, diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 9f0a6d3a2..aabb4c870 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -380,10 +380,9 @@ the canonical monitored-system blocked payload. `internal/api/ai_handler.go`, `internal/api/ai_handler_test.go`, and `internal/api/contract_test.go` together. Patrol-originated handoffs must keep `[Operator Briefing]`, `[Finding Context]`, `[Finding Lifecycle - Context]`, structured handoff resources, safe route-owned next-step action - labels/hrefs, related root-cause/correlation finding context, and structured - handoff actions model-only, with the briefing summarizing operator next - steps, latest lifecycle event, and governed action posture without raw + Context]`, structured handoff resources, related root-cause/correlation + finding context, and structured handoff actions model-only, with the + briefing summarizing latest lifecycle event and governed action posture without raw command text. Structured handoff action references may use the current live Patrol investigation-fix approval for the finding when that approval is newer than the approval ID on the durable @@ -399,13 +398,12 @@ the canonical monitored-system blocked payload. user's persistent autonomous control setting; live approval, proposed-fix, fix-outcome, and remediation-plan references only add structured action posture, they are not the trigger for the boundary. Frontend-visible Patrol - briefing payloads may include short suggested prompts, but those prompts - must be derived from the same safe action posture, evidence, recurrence, and - remediation-plan metadata as the briefing and must never carry raw approval, - command, or rollback command text. Frontend queued-fix recovery handoffs + briefing payloads must stay compact and must not include suggested prompt + chips, Patrol-authored next-step recommendations, or route-owned + recommendation metadata. Frontend queued-fix recovery handoffs where the live approval or proposed-fix payload is unavailable must still carry that Patrol-owned operator briefing, current `fix_queued` posture, - request-local approval-required mode, and safe suggested prompts; they must + request-local approval-required mode, and model-only evidence context; they must not degrade into generic Assistant investigation chat or imply that execution can proceed from missing command payloads. Expired-approval recovery handoffs may use a still-available structured proposed-fix payload @@ -562,32 +560,29 @@ the canonical monitored-system blocked payload. and that same Patrol investigation-context owner, so the current Patrol assessment summary may open Assistant with bounded model-only assessment, verification, latest-run, supporting-context evidence, active-finding, and - resource reference context plus safe source-owned suggested prompts and the - Patrol-owned recommended next step as safe metadata; active + resource reference context while leaving prioritization and next-step + reasoning to the configured LLM; active finding entries may carry live pending Patrol approval posture only as safe structured handoff actions with approval ID/status/risk/target/request/expiry metadata, action plan identity/policy/expiry, dry-run posture, and command counts instead of pasting page-local UI text or raw command payloads into chat, the drawer target must stay `patrol-assessment` rather than a retired dashboard target, - and may derive visible action labels, safety notes, and approval-aware - suggested prompts plus first-turn prompt emphasis from that same safe metadata - plus the current disabled reason for a Patrol-owned recommended action when - the visible summary action is unavailable, - so the drawer does not fall back to generic assessment copy when governed - actions are present or when the summary already exposes a concrete - recommended next step + and may derive model-only approval/action posture from that same safe + metadata, while visible drawer copy stays compact and does not expose + Patrol-authored recommendations, prompt chips, or action labels as the + answer, and that same Patrol investigation-context owner, so coverage signals from the canonical AI summary remain secondary caveats when the same assessment carries active findings, pending approvals, or governed action references; Assistant prompt emphasis, briefing action labels, and safety notes must - lead with finding priority, affected resources, evidence, and the governed - next step instead of recasting the whole handoff as a coverage gap + carry finding priority, affected resources, evidence, and governed approval + posture as context instead of recasting the whole handoff as a coverage gap and that same Patrol investigation-context owner, so coverage-incomplete assessments with no active infrastructure findings are serialized as a verification-gap handoff: prompt and visible briefing copy must explain what scoped activity did and did not prove, keep latest-run and supporting-context - facts model-only, carry the existing summary recommendation as safe + facts model-only, and leave next-step selection to the configured LLM handoff-only metadata, and avoid introducing backend fields beyond the existing Patrol status plus run-history contracts and that same Patrol investigation-context owner, so visible Assistant @@ -2609,14 +2604,10 @@ references used to seed canonical approval and action-audit refresh. `handoff_metadata` is the browser-safe identity envelope for restoring saved product handoffs, currently including Patrol run kind, run ID, safe run type/status, a runtime-failure boolean rather than runtime failure detail, and -safe Patrol recommended next-step title/detail/action labels plus whitelisted app-route -hrefs when the browser handoff already owns that route. Frontend-visible -Patrol assessment briefings may render those same safe recommendation fields as -separate title, reason, and route-action facts, but they must not introduce new -browser-owned authority beyond the structured handoff metadata and bounded -chat context. Browser-visible suggested prompts may follow the structured -recommendation action, such as provider-settings recovery for runtime -visibility failures, while remaining review prompts rather than API authority. +bounded resource/action counts when available. Frontend-visible Patrol +assessment briefings must not render recommendation fields as separate title, +reason, route-action facts, or prompt chips; the configured model owns those +decisions from the structured handoff metadata and bounded chat context. Frontend handoff builders may send these fields for owned alert, incident, Patrol assessment, Patrol finding, or Patrol run-history context, but the backend must not @@ -2631,13 +2622,11 @@ The `/api/ai/sessions` response may expose `handoff_summary` for sessions that carry private Assistant model-context metadata, but that payload is a safe reload marker only. It may carry the handoff kind, finding ID, Patrol run ID, safe run type/status/runtime-failure flags, counts, primary-resource label, -last-known approval/action status, risk level, Patrol recommended -next-step title/detail/action labels plus the safe recommendation action kind or whitelisted -app-route href when safely extractable from the stored Patrol handoff, and -summary timestamp; it must +last-known approval/action status, risk level, and summary timestamp; it must not serialize the model-only `handoff_context`, runtime failure detail, action preflight/result bodies, remediation descriptions, raw commands, or approval -command payloads. +command payloads, and it must not preserve Patrol-authored next-step +recommendation fields from legacy handoffs. Patrol finding handoffs are stricter than ordinary chat requests: when a request carries a non-empty `finding_id` or resolves to model-only Patrol briefing, resource, or action context, `internal/api/ai_handler.go` must clamp the diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 174f6acf6..e88f709b2 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -186,11 +186,9 @@ work extends shared components instead of creating new local variants. `handoffContext`, `handoffResources`, `handoffActions`, and safe `handoffMetadata` while preserving the safe visible briefing and scoped approval-required posture, so later turns rely on backend session hydration - instead of resending stale browser context. When a Patrol handoff includes a - safe next-step label, action kind, or whitelisted app-route href, those - reload-safe fields must travel through `handoffMetadata` rather than relying - on the browser to reconstruct them from model-only handoff text. The drawer - must treat + instead of resending stale browser context. Patrol handoffs must not include + safe next-step labels, action kinds, or whitelisted app-route hrefs in + `handoffMetadata`; the drawer must treat `handoff_summary.requires_approval` as a current pending-decision flag, not a historical action marker, so completed or rejected handoff actions render as action context rather than pending approval. A restored Patrol run summary @@ -204,16 +202,16 @@ work extends shared components instead of creating new local variants. can rebuild model-bound context from the stored Patrol run. Restored Patrol assessment, Patrol finding, and Patrol configuration-failure sessions follow the same safe-summary rule: the drawer may restore source label, - title, target type, status badge, action label, and suggested prompts from - `handoff_summary`, but + title, target type, status badge, bounded resource facts, and approval/action + status from `handoff_summary`, but it must not infer a finding target from bounded action references or reconstruct hidden model context, provider details, retry payloads, commands, preflight output, or action results in the browser. If the safe summary - includes a Patrol recommended next step, recommendation detail, action - label, safe action kind, or whitelisted app-route href, the session picker - plus restored drawer briefing and action copy must use that recommendation - and may carry the safe detail, action kind, or href as context metadata - rather than falling back to generic handoff copy. + was created by a legacy build that stored Patrol next-step metadata, + recommendation detail, action labels, safe action kind, or whitelisted + app-route href, the session picker plus restored drawer must ignore those + fields rather than carrying them forward as hidden context or visible + recommendation copy. Session-load and new-conversation transitions must be success-bound: if the underlying session operation fails, the shared drawer store must not clear or replace the current scoped handoff context. @@ -237,17 +235,14 @@ surface, while paid-plan copy and compatibility feature probes stay out of the frontend primitive boundary. Feature surfaces under `frontend-modern/src/features/` may own product-specific -assessment and recommendation semantics, but they must keep those semantics in -their governed presentation helpers and render them inside the shared neutral -Pulse surface language rather than introducing page-local verdict bands or -nested cards. Patrol's primary recommended next step follows this boundary: -the Patrol helper owns the decision text, while the section component only -renders the compact default next-step title and one bounded action inside the -existing assessment shell, with detail copy deferred to the section's details -expansion. Feature-owned recommendation actions may use the shared -button/link shape, but they must not create a second full-width action band or -nest another card inside the primary Pulse surface. If the same assessment -opens Assistant, the Patrol-to-Assistant handoff must carry that exact +assessment semantics, but they must keep those semantics in their governed +presentation helpers and render them inside the shared neutral Pulse surface +language rather than introducing page-local verdict bands or nested cards. +Patrol's primary assessment strip is descriptive only; it must not render a +Patrol-authored recommended next step, suggested prompt chips, or a secondary +action band inside the assessment shell. If the same assessment opens +Assistant, the Patrol-to-Assistant handoff must carry only bounded evidence, +resource references, and governed approval/action posture as model-only context. recommendation as safe bounded metadata so the drawer briefing and first-turn prompt explain the same operator-facing priority. Feature-owned Assistant handoff prompts may provide source context and safe metadata, but the shared @@ -1055,33 +1050,24 @@ reasoning after the request reaches the AI runtime. `handoff_actions` for model-only refresh, but the shared drawer stays a generic shell rather than a Patrol summary prompt builder. The Patrol helper may turn those same safe - references into visible action labels, safety notes, and approval-aware - suggested prompts or first-turn prompt emphasis for assessment and - finding-level handoffs; if a source-owned Patrol recommendation action is - currently disabled, the helper must pass the bounded disabled reason in - visible briefing and model-only context instead of presenting that action as - available. Assessment recommendation briefings must keep the safe - recommendation title, reason, and route-owned action as separate visible - drawer facts so the shared Assistant shell presents operator guidance - without parsing a compressed context sentence. The same feature helper owns - recommendation-aware suggested prompts; shared drawer primitives must render - those prompts without replacing a provider-settings recovery path with - generic coverage wording. Assessment-level Patrol prompts, action labels, - and safety notes must also prioritize active findings, pending approvals, - and governed action references over secondary coverage caveats, reserving - coverage-gap-first wording for assessments with no active findings. + references into visible action labels and safety notes for assessment and + finding-level handoffs, but it must not produce Patrol-authored suggested + prompt chips, recommendation titles, recommendation reasons, or route-owned + next-step actions. Assessment-level Patrol prompts, action labels, and + safety notes must describe active findings, pending approvals, governed + action references, and coverage caveats as evidence for the configured + model, not as a frontend-authored decision tree. Finding-level drawer opens may also pass one bounded - model-only finding context, one target resource reference, one safe - route-owned next-step action label/href, and one `handoff_actions` reference - for a live approval or proposed fix. It must not expose raw command or - execution payloads. + model-only finding context, one target resource reference, and one + `handoff_actions` reference for a live approval or proposed fix. It must not + expose raw command or execution payloads. The drawer may render a generic context-briefing band from `frontend-modern/src/stores/aiChat.ts`, but feature-owned helpers must provide compact source labels, primary subject, - status, and any safe route action while keeping attention reason, evidence - summaries, operator-decision copy, safety notes, and prompt suggestions in - model-only or governed action context unless the operator asks for them. + status, and governed approval/action posture while keeping attention reason, + evidence summaries, operator-decision copy, and safety notes compact and + source-owned. Prompt suggestions must stay out of Patrol drawer chrome. Patrol finding and action-artifact handoffs must not render suggested prompt chips in the drawer and must not become another primitive path for raw approval, command, or rollback command payload text. Missing-detail @@ -1686,8 +1672,7 @@ repeat assessment or verification labels as a second compact verdict row. The collapsed Patrol assessment strip itself must remain a compact readout rather than a headline-plus-paragraph block; explanatory assessment and recommendation copy belongs in the owning Findings, Runs, Supporting context, -or recommended-action surfaces rather than a normal-path summary details -expansion. +or Assistant chat surfaces rather than a normal-path summary details expansion. That readout should lead with current operator state and score rather than mixing a reassuring grade label with issue-state copy in the same line. That same summary shell should also keep the shared Pulse surface neutral: diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 79c212f7a..e1255ea65 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -94,8 +94,8 @@ Patrol-specific presentation helpers. Patrol assessment copy must not present an all-clear health prediction while active Patrol findings or Patrol runtime issues are still present. The canonical summary helper owns that conflict resolution so the visible - assessment title, description, metrics, and recommended next step all speak - from the same current findings state. Patrol-owned runtime issues must stay + assessment title, description, and compact metrics all speak from the same + current findings state. Patrol-owned runtime issues must stay distinct from infrastructure findings in assessment copy rather than being described as infrastructure warning findings about Patrol itself. Assessment coverage caveats must also reconcile against current run-history @@ -175,11 +175,11 @@ Patrol-specific presentation helpers. than approval, lifecycle, disclosure, or execution authority. Assistant handoffs from Patrol findings must also include a concise operator briefing derived from the unified finding and structured investigation record - before the detailed finding context, so Assistant leads with the current risk, + before the detailed finding context, so Assistant receives current risk, attention reason, recency, evidence snapshot, verification summary, - conclusion, latest lifecycle event, recommended next step, explicit operator - decision framing, and governed approval/action posture instead of behaving - like a generic chat over a pasted incident dump. Patrol is the scheduled + conclusion, latest lifecycle event, explicit operator decision framing, and + governed approval/action posture instead of behaving like a generic chat over + a pasted incident dump. Patrol is the scheduled probe, context assembler, and execution-governance owner; the configured LLM is the diagnostic and remediation-reasoning owner. Patrol handoff prompts may provide system context, resource posture, action posture, and governed tools, @@ -189,8 +189,9 @@ Patrol-specific presentation helpers. produced the correct fix. The visible Assistant drawer briefing opened from a Patrol finding must be compact and source-named: current status/risk, one primary subject, and any approval-required boundary, with richer evidence, action - artifacts, command counts, and prompt suggestions staying in model-only or - governed action context rather than drawer chrome. When a + artifacts, and command counts staying in model-only or governed action + context rather than drawer chrome; prompt suggestions must not be generated + for Patrol handoffs. When a structured investigation record is not available yet, the same Patrol-owned helper must still brief the operator from current finding facts such as active status, severity, recurrence, and loop state instead of opening a @@ -207,12 +208,12 @@ Patrol-specific presentation helpers. answer. Finding handoffs must be assembled through the Patrol-owned handoff model so the prompt, visible briefing, model-only finding context, resource reference, - safe next-step action label/href, bounded action reference, and request-local - approval-required posture stay in sync. The model-only context may include + bounded action reference, and request-local approval-required posture stay in + sync. The model-only context may include current finding status, recurrence, investigation record facts, evidence, verification, approval posture, dry-run posture, proposed-fix summary, target - resource references, and safe route-owned next-step labels/hrefs such as - provider settings without raw command payloads. Inline Patrol approval actions in + resource references, and governed action references without raw command + payloads. Inline Patrol approval actions in `frontend-modern/src/components/patrol/ApprovalSection.tsx` that open Assistant must follow that same Patrol-owned handoff model rather than a prompt-only local shortcut: pass approval ID/status/risk/target plus safe @@ -315,7 +316,7 @@ Patrol-specific presentation helpers. handoff, and a parallel "Explain" button opens the same handoff with a `PatrolAssistantFindingIntent='explain'` seed that asks the LLM to walk through what we know, why it matters, how confident the - analysis is, and whether the recommended action is appropriate. Both + analysis is, what remains uncertain, and what the model would do next. Both buttons must route through `buildPatrolAssistantFindingHandoff` so the structured context (investigation record, operational memory, pending approval, proposed fix, next-step action) is attached @@ -354,13 +355,7 @@ Patrol-specific presentation helpers. verification recency, latest run, secondary investigation context, bounded recent-change and learned-correlation evidence, active-finding summaries, structured resource references, structured approval/action references, and - safe source-owned suggested prompts as model-only context. The same handoff - must also carry the Patrol-owned recommended next step as safe bounded - metadata, including its title, detail copy, action label, and known action - kind when present, plus the current action-disabled reason when the visible - Patrol-owned action is unavailable, so Assistant explains the same - operator-facing priority and current availability shown in the summary card - instead of inventing a separate next step. Assessment-level handoffs must + model-only context. Assessment-level handoffs must identify the drawer target as `patrol-assessment` with `targetId=pulse-patrol-assessment`, matching saved session restore semantics rather than the retired dashboard target. @@ -372,30 +367,26 @@ Patrol-specific presentation helpers. command and approval payloads out of prompt and drawer copy, surface visible drawer action posture from the same safe references, make the initial prompt lead with approval/action review when governed references are attached, and - frame Assistant as explanation, prioritization, and safe next-step review + frame Assistant as explanation, prioritization, and model-owned next-step reasoning rather than a generic reactive chat box. That whole-surface assessment handoff must send safe `handoff_metadata.kind=patrol_assessment` so saved Assistant sessions restore as current-assessment context instead of becoming generic scoped context or an accidental single-finding session because one bounded action reference - names a finding. Saved assessment and finding sessions may expose the - Patrol-owned recommended next step title/detail/action and whitelisted - app-route href through the safe `handoff_summary` only after command-like - and secret-like text is withheld; live handoffs must send those safe fields - through structured `handoff_metadata` where available, and the browser must - use them for restored drawer copy without receiving the private model-only - handoff context. Assessment Assistant drawer briefings must present the safe - recommended step title, reason, and route-owned action as separate operator - facts instead of compressing them into an opaque context sentence, and the - suggested prompts must follow the structured recommendation action so - provider-setting/runtime-visibility failures lead with provider checks and - post-restore verification rather than generic coverage questions. + names a finding. Saved assessment and finding sessions must not expose or + restore Patrol-authored next-step titles, recommendation detail, action + labels, or app-route hrefs through `handoff_summary`; legacy stored + recommendation fields are ignored rather than converted into hidden context. + Assessment Assistant drawer briefings must stay compact and source-named + instead of presenting a recommended step title, reason, route action, or + suggested prompt chips as operator-facing answers, so + provider-setting/runtime-visibility failures remain plain context for the + configured model rather than Pulse-authored recovery guidance. When the current Patrol assessment is coverage-incomplete with no active - infrastructure finding, the same handoff model must frame the briefing as a - verification gap: the prompt leads with what scoped activity did and did not - prove, visible drawer copy names the coverage gap, suggested prompts focus - on full-run verification and early warning signals, and execution or retry - remains operator-controlled. + infrastructure finding, the same handoff model may describe incomplete + coverage as evidence, but it must let the configured model decide whether + more verification, operator action, or governed tool use is needed. Execution + or retry remains operator-controlled. Patrol run-history entries may also open Assistant for a selected run, but that handoff must flow through the same Patrol-owned investigation-context model rather than a row-local prompt. The browser-visible prompt and drawer @@ -416,13 +407,11 @@ Patrol-specific presentation helpers. than a hero-style or decorative card surface. The default collapsed state is a compact readout, not a headline block: show the Patrol assessment label, current operator state, concise high-signal trust posture such as - regressions when present, score, one recommended next-step title, and one - route-owned action. Do not combine reassuring grade labels such as + regressions when present, and score. Do not combine reassuring grade labels such as `Health A` with issue-state copy such as `Issues detected` in the collapsed line. Do not add a normal-path assessment details expansion: assessment - explanation, recommendation detail, verification detail, activity mix, - supporting metrics, and whole-assessment Assistant discussion belong in the - owning Findings, Runs, Supporting context, or recommended-action surfaces + explanation, verification detail, activity mix, and supporting metrics belong in the + owning Findings, Runs, or Supporting context surfaces instead of reopening the compact strip into a sparse status panel. ## Current State @@ -476,29 +465,16 @@ render contract: the header chip, primary summary card, and status bar must all route through the shared `frontend-modern/src/utils/patrolRuntimePresentation.ts` helper plus the backend `runtime_state` payload instead of inferring operator state from the last healthy summary snapshot or run history alone. -The primary summary card now also has a Patrol-owned Assistant assessment -handoff. `frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx` -opens Assistant through -`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`, which -packages the current Patrol assessment, verification posture, latest run, +The primary summary strip is descriptive only: it surfaces the current Patrol +assessment label and compact counts, while action choices remain in the +Findings/Runs workspace, header controls, and the LLM-driven Assistant chat. +`frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` may +package the current Patrol assessment, verification posture, latest run, secondary investigation context, bounded recent-change and learned-correlation -evidence, bounded active-finding summaries, source-owned suggested prompts, and -the Patrol-owned recommended next step, plus deduped resource references and -safe structured approval/action references as -model-only context while forcing `autonomousMode:false` and summarizing -proposed-fix command-bearing records and command-bearing change events without -raw command text. Its visible Assistant briefing must also use those safe -references to distinguish pending governed approvals or attached action -references from a generic assessment discussion, including approval-policy and -dry-run posture when available. When no approval or governed action outranks -the summary recommendation, the briefing action label and initial prompt may -lead with that recommendation, but Assistant remains explanatory and may not -start Patrol runs, settings changes, diagnostics, remediation, or approvals -from the handoff; if the recommended action is currently disabled, the prompt -and briefing must say why instead of describing it as an available action. Its -initial prompt must prioritize approvals or action -references before broader assessment discussion while command payloads stay -out of the drawer. +evidence, bounded active-finding summaries, deduped resource references, and +safe structured approval/action references as model-only context, but visible +drawer copy must not promote Pulse-authored next-step metadata, action chips, or +suggested prompts as the answer. Command payloads stay out of drawer chrome. Run-history rows now follow that same Assistant handoff model. A selected `frontend-modern/src/components/patrol/RunHistoryEntry.tsx` row may open Assistant through diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 7866fa3e1..bc3a4b72f 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -474,8 +474,9 @@ IDs, action IDs, policy, expiry, dry-run posture, and proposed-fix labels; they must not expose raw command or execution payloads or become an approval bypass. Assistant operator briefings generated from Patrol findings follow the same boundary: they may summarize approval IDs, proposed-fix IDs, risk, destructive -posture, and recommended next steps for review, but they must not expose raw -command payloads or convert chat into approval or execution authority. +posture, and bounded evidence for model review, but they must not expose raw +command payloads, present Patrol-authored remediation guidance, or convert chat +into approval or execution authority. Action planning and action decision mutations remain privileged runtime control surfaces even though the decision endpoint does not execute the capability. `POST /api/actions/plan` and diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 41fe8551c..5cacbd2e4 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -594,9 +594,9 @@ bypass the API fail-closed execution gate. recovery freshness, restore eligibility, or storage-local execution state. Operator briefings generated for those same Assistant finding handoffs are also adjacent AI/runtime context only: storage and recovery surfaces may read - their Patrol conclusion or recommended next step as investigation guidance, - but must not reinterpret the briefing as backup recency, restore support, or - storage-local action authority. + their Patrol conclusion and bounded evidence as investigation context, but + must not reinterpret the briefing as backup recency, restore support, + Patrol-authored remediation guidance, or storage-local action authority. That same adjacent `internal/api/` boundary still carries Patrol-run execution identity. Storage and recovery may observe shared Patrol transport through `internal/api/chat_service_adapter.go`, but they must not diff --git a/frontend-modern/src/api/__tests__/ai.test.ts b/frontend-modern/src/api/__tests__/ai.test.ts index 583c4f74d..80943d603 100644 --- a/frontend-modern/src/api/__tests__/ai.test.ts +++ b/frontend-modern/src/api/__tests__/ai.test.ts @@ -188,7 +188,7 @@ describe('AIAPI', () => { description: 'Pulse Patrol could not maintain a healthy connection to the configured provider during analysis.', impact: - 'While Patrol cannot analyze, alerts continue to fire without evidence or recommended actions, and AI Intelligence summaries cannot refresh.', + 'While Patrol cannot analyze, alerts continue to fire without fresh Patrol evidence, and AI Intelligence summaries cannot refresh.', recommendation: 'Check provider reachability, base URL, firewall or proxy rules, and provider availability, then rerun Patrol.', detected_at: '2026-05-08T12:00:00Z', @@ -200,7 +200,7 @@ describe('AIAPI', () => { const result = await AIAPI.getUnifiedFindings(); expect(result.findings[0].impact).toBe( - 'While Patrol cannot analyze, alerts continue to fire without evidence or recommended actions, and AI Intelligence summaries cannot refresh.', + 'While Patrol cannot analyze, alerts continue to fire without fresh Patrol evidence, and AI Intelligence summaries cannot refresh.', ); }); diff --git a/frontend-modern/src/api/__tests__/aiChat.test.ts b/frontend-modern/src/api/__tests__/aiChat.test.ts index fd3d22ee7..7f70313ff 100644 --- a/frontend-modern/src/api/__tests__/aiChat.test.ts +++ b/frontend-modern/src/api/__tests__/aiChat.test.ts @@ -50,10 +50,6 @@ describe('AIChatAPI', () => { last_known_approval_status: 'pending', last_known_action_state: 'awaiting_approval', last_known_action_risk: 'high', - recommended_next_step: 'Review pending Patrol approval', - recommended_next_step_action: 'Review approvals', - recommended_next_step_action_kind: 'review_approvals', - recommended_next_step_action_href: '/patrol', updated_at: '2026-05-06T12:08:00Z', }, }; @@ -225,10 +221,6 @@ describe('AIChatAPI', () => { undefined, { kind: 'patrol_finding', - recommendedNextStep: 'Open Patrol provider settings', - recommendedNextStepDetail: 'Fix the Patrol runtime issue before continuing.', - recommendedNextStepAction: 'Open Patrol provider settings', - recommendedNextStepActionHref: '/settings/system-ai', }, ); @@ -243,10 +235,6 @@ describe('AIChatAPI', () => { autonomous_mode: false, handoff_metadata: { kind: 'patrol_finding', - recommended_next_step: 'Open Patrol provider settings', - recommended_next_step_detail: 'Fix the Patrol runtime issue before continuing.', - recommended_next_step_action: 'Open Patrol provider settings', - recommended_next_step_action_href: '/settings/system-ai', }, }), }), diff --git a/frontend-modern/src/api/ai.ts b/frontend-modern/src/api/ai.ts index ebdeac3c7..ad895727c 100644 --- a/frontend-modern/src/api/ai.ts +++ b/frontend-modern/src/api/ai.ts @@ -494,17 +494,15 @@ export interface RemediationPlan { risk_level: 'low' | 'medium' | 'high'; status: 'pending' | 'approved' | 'executing' | 'completed' | 'failed' | 'rolled_back'; created_at: string; - // Optional deterministic action proposal attached by the patrol pipeline - // (currently only forecast-driven capacity proposals - see internal/ai/forecast). - // RequiresApproval is invariant on every proposal; Allowed=false signals - // a preflight-only proposal (no write capability wired yet). + // Optional governed action proposal projection. Patrol finding creation + // must not populate this from category/title heuristics. + // RequiresApproval is invariant on every proposal; Allowed=false signals a + // preflight-only proposal (no write capability wired yet). proposed_action_plan?: ProposedActionPlan; } // ProposedActionPlan mirrors pkg/aicontracts.ProposedActionPlan on the -// wire. The "capacity_forecast" Source value tells FindingsPanel to render -// the distinguishable forecast approval card; anything else falls back to -// the generic remediation plan card. +// wire. export interface ProposedActionPlan { actionId: string; capabilityName?: string; @@ -512,15 +510,15 @@ export interface ProposedActionPlan { requiresApproval: boolean; approvalPolicy?: string; message?: string; - source?: 'capacity_forecast' | string; + source?: string; projectedMetric?: ProposedMetricSummary; preflight?: ProposedActionPreflight; plannedAt?: string; expiresAt?: string; } -// ProposedMetricSummary is the operator-facing snapshot rendered at the -// top of the capacity-forecast approval card. +// ProposedMetricSummary is the operator-facing metric snapshot for a +// governed action proposal. export interface ProposedMetricSummary { metric: string; currentValue: number; diff --git a/frontend-modern/src/api/aiChat.ts b/frontend-modern/src/api/aiChat.ts index 7c740c5e6..9cfdc5887 100644 --- a/frontend-modern/src/api/aiChat.ts +++ b/frontend-modern/src/api/aiChat.ts @@ -53,11 +53,6 @@ export interface ChatSessionHandoffSummary { last_known_approval_status?: string; last_known_action_state?: string; last_known_action_risk?: string; - recommended_next_step?: string; - recommended_next_step_detail?: string; - recommended_next_step_action?: string; - recommended_next_step_action_kind?: string; - recommended_next_step_action_href?: string; updated_at?: string; } @@ -67,11 +62,6 @@ export interface ChatHandoffMetadata { runType?: string; runStatus?: string; runtimeFailure?: boolean; - recommendedNextStep?: string; - recommendedNextStepDetail?: string; - recommendedNextStepAction?: string; - recommendedNextStepActionKind?: string; - recommendedNextStepActionHref?: string; } export interface ChatHandoffAction { @@ -320,11 +310,6 @@ export class AIChatAPI { run_type: handoffMetadata.runType, run_status: handoffMetadata.runStatus, runtime_failure: handoffMetadata.runtimeFailure, - recommended_next_step: handoffMetadata.recommendedNextStep, - recommended_next_step_detail: handoffMetadata.recommendedNextStepDetail, - recommended_next_step_action: handoffMetadata.recommendedNextStepAction, - recommended_next_step_action_kind: handoffMetadata.recommendedNextStepActionKind, - recommended_next_step_action_href: handoffMetadata.recommendedNextStepActionHref, }; } diff --git a/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx index b808ffd63..2e61823d4 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx +++ b/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx @@ -985,11 +985,6 @@ describe('AIChat', () => { resource_count: 2, action_count: 0, requires_approval: false, - recommended_next_step: 'Verify full coverage', - recommended_next_step_detail: - 'Run a full Patrol sweep before treating the assessment as current.', - recommended_next_step_action: 'Run Patrol', - recommended_next_step_action_kind: 'run_patrol', }, }, ]); @@ -1003,7 +998,7 @@ describe('AIChat', () => { await waitFor(() => { expect(screen.getByText('Pulse Patrol')).toBeInTheDocument(); expect(screen.getByText('Assessment context')).toBeInTheDocument(); - expect(screen.getByText('Recommended: Run Patrol')).toBeInTheDocument(); + expect(screen.queryByText('Recommended: Run Patrol')).not.toBeInTheDocument(); }); fireEvent.click(screen.getByText('Assessment follow-up')); @@ -1018,22 +1013,13 @@ describe('AIChat', () => { context: expect.objectContaining({ kind: 'patrol_assessment', findingId: undefined, - recommendedNextStep: 'Verify full coverage', - recommendedNextStepDetail: - 'Run a full Patrol sweep before treating the assessment as current.', - recommendedNextStepAction: 'Run Patrol', - recommendedNextStepActionKind: 'run_patrol', }), briefing: expect.objectContaining({ sourceLabel: 'Pulse Patrol', title: 'Patrol assessment handoff', subject: 'Current Patrol assessment', - actionLabel: 'Recommended: Run Patrol', - detailLines: expect.arrayContaining([ - 'Recommended next step: Verify full coverage', - 'Reason: Run a full Patrol sweep before treating the assessment as current.', - 'Available action: Run Patrol', - ]), + actionLabel: 'Review Patrol assessment', + detailLines: ['2 linked resources'], }), }), ); @@ -1183,9 +1169,6 @@ describe('AIChat', () => { }, action_count: 0, requires_approval: false, - recommended_next_step: 'Open Patrol provider settings', - recommended_next_step_action: 'Open Patrol provider settings', - recommended_next_step_action_href: '/settings/system-ai', }, }, ]); @@ -1199,7 +1182,7 @@ describe('AIChat', () => { await waitFor(() => { expect(screen.getByText('Pulse Patrol')).toBeInTheDocument(); expect(screen.getByText('Context attached')).toBeInTheDocument(); - expect(screen.getByText('Open Patrol provider settings')).toBeInTheDocument(); + expect(screen.queryByText('Open Patrol provider settings')).not.toBeInTheDocument(); }); fireEvent.click(screen.getByText('Context-only Patrol follow-up')); @@ -1213,15 +1196,11 @@ describe('AIChat', () => { context: expect.objectContaining({ actionCount: 0, requiresApproval: false, - recommendedNextStep: 'Open Patrol provider settings', - recommendedNextStepAction: 'Open Patrol provider settings', - recommendedNextStepActionHref: '/settings/system-ai', }), briefing: expect.objectContaining({ sourceLabel: 'Pulse Patrol', title: 'Patrol finding on web-server', - actionLabel: 'Open Patrol provider settings', - actionHref: '/settings/system-ai', + actionLabel: undefined, }), }), ); diff --git a/frontend-modern/src/components/AI/Chat/index.tsx b/frontend-modern/src/components/AI/Chat/index.tsx index e7c3ad282..1121a3a39 100644 --- a/frontend-modern/src/components/AI/Chat/index.tsx +++ b/frontend-modern/src/components/AI/Chat/index.tsx @@ -145,33 +145,6 @@ const getSessionHandoffBadgeLabel = (summary: ChatSessionHandoffSummary) => { return summary.has_model_context ? 'Context attached' : 'Scoped handoff'; }; -const formatSessionHandoffRecommendationLabel = (summary: ChatSessionHandoffSummary) => { - if (!isPatrolSessionHandoff(summary)) return ''; - const action = summary.recommended_next_step_action?.trim(); - if (action) return isPatrolFindingSessionHandoff(summary) ? action : `Recommended: ${action}`; - const nextStep = summary.recommended_next_step?.trim(); - return nextStep ? `Recommended: ${nextStep}` : ''; -}; - -const getSessionHandoffRecommendedActionHref = ( - summary: ChatSessionHandoffSummary, -): string | undefined => { - const summaryHref = summary.recommended_next_step_action_href?.trim(); - if (summaryHref === '/settings/system-ai' || summaryHref === '/patrol') { - return summaryHref; - } - - switch (summary.recommended_next_step_action_kind?.trim()) { - case 'open_provider_settings': - return '/settings/system-ai'; - case 'review_approvals': - case 'review_findings': - return '/patrol'; - default: - return undefined; - } -}; - const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | undefined => { const summary = session?.handoff_summary; if (!summary) return undefined; @@ -180,11 +153,6 @@ const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | unde const resourceLabel = formatSessionHandoffResourceLabel(summary); const resourceDetail = formatSessionHandoffResourceDetail(summary); const statusLabel = formatSessionHandoffStatus(summary); - const recommendedNextStep = summary.recommended_next_step?.trim() || ''; - const recommendedNextStepDetail = summary.recommended_next_step_detail?.trim() || ''; - const recommendedNextStepAction = summary.recommended_next_step_action?.trim() || ''; - const recommendedNextStepActionKind = summary.recommended_next_step_action_kind?.trim() || ''; - const recommendedNextStepActionHref = getSessionHandoffRecommendedActionHref(summary); const actionCount = summary.action_count ?? 0; const resourceCount = summary.resource_count ?? 0; const findingId = summary.finding_id?.trim() || undefined; @@ -243,11 +211,6 @@ const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | unde lastKnownApprovalStatus: summary.last_known_approval_status, lastKnownActionState: summary.last_known_action_state, lastKnownActionRisk: summary.last_known_action_risk, - recommendedNextStep, - recommendedNextStepDetail, - recommendedNextStepAction, - recommendedNextStepActionKind, - recommendedNextStepActionHref, updatedAt: summary.updated_at, }, findingId, @@ -275,15 +238,6 @@ const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | unde resourceCount > 1 ? pluralizeCount(resourceCount, 'linked resource', 'linked resources') : undefined, - isPatrolAssessment && recommendedNextStep - ? `Recommended next step: ${recommendedNextStep}` - : undefined, - isPatrolAssessment && recommendedNextStepDetail - ? `Reason: ${recommendedNextStepDetail}` - : undefined, - isPatrolAssessment && recommendedNextStepAction - ? `Available action: ${recommendedNextStepAction}` - : undefined, actionCount > 0 ? pluralizeCount(actionCount, 'governed action', 'governed actions') : undefined, @@ -294,27 +248,14 @@ const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | unde : isPatrolConfigurationFailure ? 'Review Patrol configuration issue' : isPatrolAssessment - ? recommendedNextStepAction - ? `Recommended: ${recommendedNextStepAction}` - : recommendedNextStep - ? `Recommended: ${recommendedNextStep}` - : 'Review Patrol assessment' + ? 'Review Patrol assessment' : isPatrolRun && summary.runtime_failure ? 'Review Patrol runtime issue' : isPatrolRun ? 'Review Patrol run' - : isPatrolFinding && recommendedNextStepAction - ? recommendedNextStepAction - : actionCount > 0 - ? 'Governed action context' - : undefined, - actionHref: - !summary.requires_approval && - recommendedNextStepActionHref && - (isPatrolFinding || - (isPatrolAssessment && Boolean(recommendedNextStepAction || recommendedNextStep))) - ? recommendedNextStepActionHref - : undefined, + : actionCount > 0 + ? 'Governed action context' + : undefined, commandSummary: statusLabel ? isPatrolRun ? `Run state: ${statusLabel}` @@ -325,37 +266,9 @@ const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | unde : actionCount > 0 ? 'Detailed command payloads stay in governed approval context.' : undefined, - suggestedPrompts: isPatrolRun - ? summary.runtime_failure - ? [ - 'Explain this Patrol runtime issue', - 'What should I check before retrying?', - 'What should I verify next?', - ] - : [ - 'Explain this Patrol run', - 'What changed during this run?', - 'What should I verify next?', - ] - : isPatrolConfigurationFailure - ? [ - 'Explain this Patrol configuration issue', - 'What should I check before retrying?', - 'What should I change before the next run?', - ] - : isPatrolAssessment - ? [ - 'Prioritize findings and safest next step', - 'Explain recent changes and correlations', - 'What should I verify next?', - ] - : isPatrolFinding - ? [ - 'What changed since this finding was opened?', - 'What evidence supports this finding?', - 'What should I verify next?', - ] - : ['Summarize this handoff', 'What needs attention?', 'What should I verify next?'], + suggestedPrompts: isPatrolSessionHandoff(summary) + ? [] + : ['Summarize this handoff', 'What needs attention?', 'What should I verify next?'], }, }; }; @@ -1456,15 +1369,6 @@ export const AIChat: Component = (props) => { )} - - {(recommendation) => ( - - {recommendation()} - - )} - )} diff --git a/frontend-modern/src/components/AI/FindingsPanel.tsx b/frontend-modern/src/components/AI/FindingsPanel.tsx index cd25397d0..e13b093c0 100644 --- a/frontend-modern/src/components/AI/FindingsPanel.tsx +++ b/frontend-modern/src/components/AI/FindingsPanel.tsx @@ -34,7 +34,6 @@ import { AIAPI, type ApprovalRequest, type RemediationPlan } from '@/api/ai'; import { createSuppressionRuleFromFinding } from '@/api/patrol'; import type { PatrolRunRecord, PatrolRuntimeState } from '@/api/patrol'; import { buildResolvedResourceSurfaceLinks } from '@/routing/resourceLinks'; -import { getApprovalRiskPresentation } from '@/utils/approvalRiskPresentation'; import { formatRelativeTime } from '@/utils/format'; import { getFindingAlertIdentifier, hasTriggeringAlert } from '@/utils/findingAlertIdentity'; import { segmentedButtonClass } from '@/utils/segmentedButton'; @@ -195,45 +194,6 @@ export const FindingsPanel: Component = (props) => { notificationStore.success('Remediation plan dismissed'); }; - // Reuses AIAPI.approveRemediationPlan, the existing approval contract for - // remediation plans. The capacity-forecast card surfaces this as - // "Approve proposal" so the operator can record intent on a preflight-only - // proposal (Allowed=false) - the action broker will still refuse execution - // until a Pulse write capability is wired for the resource type. See - // internal/ai/forecast.BuildActionPlanForFinding for the contract. - const handleApproveProposedPlan = async (plan: RemediationPlan, e: Event) => { - e.stopPropagation(); - try { - await AIAPI.approveRemediationPlan(plan.id); - setDismissedPlanIds((prev) => [...prev, plan.id]); - notificationStore.success('Capacity-forecast proposal approved'); - } catch (err) { - notificationStore.error( - err instanceof Error - ? `Failed to approve proposal: ${err.message}` - : 'Failed to approve proposal', - ); - } - }; - - // formatTimeToThreshold collapses a "seconds until threshold breach" - // value into operator-readable copy. Mirrors forecast service phrasing - // ("3 days", "12 hours") so the proposal card and the existing forecast - // overview don't drift in tone. - const formatTimeToThreshold = (seconds?: number | null): string => { - if (!seconds || seconds <= 0) { - return ''; - } - const hours = seconds / 3600; - if (hours < 1) { - return `${Math.max(1, Math.round(seconds / 60))} min`; - } - if (hours < 48) { - return `${Math.round(hours)} hr`; - } - return `${Math.round(hours / 24)} days`; - }; - // Map of finding_id -> latest remediation plan artifact const plansByFindingId = createMemo(() => { const dismissedPlanIdsSet = new Set(dismissedPlanIds()); @@ -680,7 +640,6 @@ export const FindingsPanel: Component = (props) => { ); const proposedFix = latestInvestigationProposedFix || buildLiveApprovalProposedFixBriefing(pendingApproval); - const nextStepAction = getFindingPrimaryActionPresentation(finding); const handoff = buildPatrolAssistantFindingHandoff({ id: finding.id, title, @@ -703,7 +662,6 @@ export const FindingsPanel: Component = (props) => { pendingApproval: pendingApprovalBriefing, proposedFix, investigationRecord: finding.investigationRecord, - nextStepAction, intent, }); // Explain, Investigate, and Why-did-this-happen are action-style @@ -1759,173 +1717,33 @@ export const FindingsPanel: Component = (props) => { /> - {/* Existing action artifact: compact review entry, not a Patrol-authored fix answer. */} + {/* Existing model-owned action artifact: compact Assistant review entry. */} {(plan) => { - // Capacity-forecast proposal variant - rendered when patrol - // attached a deterministic forecast-driven ActionPlan via the - // template registry in internal/ai/forecast. Other plan artifacts - // collapse to a compact Assistant review entry. - const proposal = () => plan().proposed_action_plan; - const isCapacityForecastProposal = () => - finding.category === 'capacity' && proposal()?.source === 'capacity_forecast'; return (
- { - const planRisk = getApprovalRiskPresentation(plan().risk_level); - return ( - <> -
-
- - Action review - - {planRisk.label} risk - -
-
- - -
-
- - ); - })()} - > - {(() => { - const p = proposal()!; - const metric = p.projectedMetric; - const preflight = p.preflight; - const ttbLabel = formatTimeToThreshold(metric?.timeToThresholdSeconds); - return ( -
-
- - - Capacity-forecast proposal - - - requires approval - - - - preflight only - - -
- - -
- - {metric!.currentValue.toFixed(1)}% - {' '} - now - - {' '} - {' '} - - {metric!.predictedValue!.toFixed(1)}% - {' '} - projected - - 0 - } - > - {' '} - · threshold {metric!.thresholdValue!.toFixed(0)}% - - · breach in {ttbLabel} -
-
- - -

{p.message}

-
- - - -
- Proposed change:{' '} - {preflight!.intendedChange} -
-
- 0} - > -
    - - {(check) =>
  • {check}
  • } -
    -
-
-
- -
- - - -
-
- ); - })()} -
+
+
+ + Assistant context +
+
+ + +
+
); }} diff --git a/frontend-modern/src/components/AI/__tests__/FindingsPanel.capacityForecast.test.tsx b/frontend-modern/src/components/AI/__tests__/FindingsPanel.capacityForecast.test.tsx deleted file mode 100644 index faaac48f2..000000000 --- a/frontend-modern/src/components/AI/__tests__/FindingsPanel.capacityForecast.test.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import { fireEvent, render, screen, waitFor, within } from '@solidjs/testing-library'; -import type { JSX } from 'solid-js'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -import { FindingsPanel } from '../FindingsPanel'; - -// Pin the registry constant locally so the tests fail loudly if Go renames -// the source on the wire (see internal/ai/forecast.CapacityActionPlanSource). -const CAPACITY_FORECAST_SOURCE = 'capacity_forecast'; - -type RemediationPlanFixture = { - id: string; - finding_id: string; - resource_id: string; - title: string; - description: string; - steps: Array<{ order: number; action: string; risk_level: 'low' | 'medium' | 'high' }>; - risk_level: 'low' | 'medium' | 'high'; - status: 'pending' | 'approved'; - created_at: string; - proposed_action_plan?: { - actionId: string; - allowed: boolean; - requiresApproval: boolean; - source?: string; - message?: string; - projectedMetric?: { - metric: string; - currentValue: number; - predictedValue?: number; - thresholdValue?: number; - timeToThresholdSeconds?: number | null; - }; - preflight?: { - intendedChange?: string; - dryRunAvailable: boolean; - safetyChecks?: string[]; - }; - }; -}; - -const baseStep = { order: 1, action: 'Investigate', risk_level: 'low' as const }; - -function makeFinding(overrides: Record) { - return { - id: 'finding-cap', - source: 'ai-patrol', - resourceId: 'node-a/storage/tank', - resourceName: 'tank', - resourceType: 'storage', - category: 'capacity', - severity: 'warning', - title: 'Storage pool tank at 87.3% usage', - description: 'Tank pool exceeded warning threshold.', - detectedAt: '2026-04-01T10:00:00Z', - lastSeenAt: '2026-04-01T10:05:00Z', - status: 'active', - ...overrides, - }; -} - -function makePlan(findingId: string, withProposal: boolean): RemediationPlanFixture { - const plan: RemediationPlanFixture = { - id: `plan-${findingId}`, - finding_id: findingId, - resource_id: 'node-a/storage/tank', - title: 'Fix: Storage pool tank at 87.3% usage', - description: 'Tank pool exceeded warning threshold.', - steps: [baseStep], - risk_level: 'medium', - status: 'pending', - created_at: '2026-04-01T10:05:00Z', - }; - if (withProposal) { - plan.proposed_action_plan = { - actionId: 'capacity-forecast-abc', - allowed: false, - requiresApproval: true, - source: CAPACITY_FORECAST_SOURCE, - message: 'Storage pool "tank" is at 87.3% usage. Propose: prune oldest auto-snapshots.', - projectedMetric: { - metric: 'usage_percent', - currentValue: 87.3, - predictedValue: 93.5, - thresholdValue: 75, - timeToThresholdSeconds: 36 * 3600, - }, - preflight: { - intendedChange: 'Prune oldest auto-snapshots, then list largest reclaimable datasets.', - dryRunAvailable: false, - safetyChecks: [ - 'Operator must explicitly approve before any execution path is wired.', - 'This proposal ships with Allowed=false; the action broker will refuse execution.', - ], - }, - }; - } - return plan; -} - -const mockState = vi.hoisted(() => { - const loadFindings = vi.fn(); - const loadPatrolFindings = vi.fn(); - const loadRemediationPlans = vi.fn(); - const approveRemediationPlan = vi.fn().mockResolvedValue({ success: true }); - return { - findings: [] as unknown[], - remediationPlans: [] as unknown[], - loadFindings, - loadPatrolFindings, - loadRemediationPlans, - approveRemediationPlan, - }; -}); - -vi.mock('@solidjs/router', () => ({ - A: (props: { href: string; children?: JSX.Element; [key: string]: unknown }) => ( - - {props.children} - - ), - useLocation: () => ({ hash: '' }), -})); - -vi.mock('@/components/shared/Card', () => ({ - Card: (props: { children?: JSX.Element }) =>
{props.children}
, -})); - -vi.mock('@/components/patrol', () => ({ - InvestigationSection: () => null, - ApprovalSection: () => null, -})); - -vi.mock('@/stores/notifications', () => ({ - notificationStore: { - success: vi.fn(), - error: vi.fn(), - }, -})); - -vi.mock('@/stores/aiChat', () => ({ - aiChatStore: { - openWithPrompt: vi.fn(), - }, -})); - -vi.mock('@/api/ai', () => ({ - AIAPI: { - approveRemediationPlan: (...args: unknown[]) => mockState.approveRemediationPlan(...args), - }, -})); - -vi.mock('@/hooks/useResources', () => ({ - useResources: () => ({ - get: () => undefined, - }), -})); - -vi.mock('@/stores/aiIntelligence', () => ({ - aiIntelligenceStore: { - get findings() { - return mockState.findings; - }, - get findingsLoading() { - return false; - }, - get findingsError() { - return null; - }, - get patrolFindings() { - return mockState.findings; - }, - get patrolFindingsLoading() { - return false; - }, - get patrolFindingsError() { - return null; - }, - get findingsNeedingAttention() { - return []; - }, - get patrolFindingsNeedingAttention() { - return []; - }, - get findingsWithPendingApprovals() { - return []; - }, - get patrolFindingsWithPendingApprovals() { - return []; - }, - get remediationPlans() { - return mockState.remediationPlans; - }, - findingsSignal: () => mockState.findings, - patrolFindingsSignal: () => mockState.findings, - loadFindings: mockState.loadFindings, - loadPatrolFindings: mockState.loadPatrolFindings, - loadRemediationPlans: mockState.loadRemediationPlans, - }, -})); - -beforeEach(() => { - mockState.findings = []; - mockState.remediationPlans = []; - mockState.loadFindings.mockClear(); - mockState.loadPatrolFindings.mockClear(); - mockState.loadRemediationPlans.mockClear(); - mockState.approveRemediationPlan.mockClear(); - if (typeof window.requestAnimationFrame !== 'function') { - window.requestAnimationFrame = ((callback: FrameRequestCallback) => - window.setTimeout( - () => callback(performance.now()), - 0, - )) as typeof window.requestAnimationFrame; - } -}); - -afterEach(() => { - vi.clearAllMocks(); -}); - -describe('FindingsPanel capacity-forecast approval card', () => { - it('renders the capacity-forecast card when a capacity finding has a forecast-driven proposal', async () => { - const finding = makeFinding({ id: 'finding-cap-1' }); - mockState.findings = [finding]; - mockState.remediationPlans = [makePlan(finding.id, true)]; - - render(() => ); - - await waitFor(() => expect(mockState.loadRemediationPlans).toHaveBeenCalled()); - - fireEvent.click(screen.getByText('Storage pool tank at 87.3% usage')); - - const card = screen.getByTestId('capacity-forecast-approval-card'); - expect(card).toBeInTheDocument(); - expect(within(card).getByText(/Capacity-forecast proposal/i)).toBeInTheDocument(); - expect(within(card).getByText(/requires approval/i)).toBeInTheDocument(); - expect(within(card).getByText(/preflight only/i)).toBeInTheDocument(); - // Operator-facing snapshot must surface current/predicted/threshold and TTB. - // Current value appears in two places inside the card: the projected - // metric snapshot and the proposal message. Both are intentional - the - // first lets the operator see the projection at a glance, the second - // restates it inside the rationale paragraph. Use *AllBy* to match - // both without locking the exact placement. - expect(within(card).getAllByText(/87\.3%/).length).toBeGreaterThanOrEqual(1); - expect(within(card).getAllByText(/93\.5%/).length).toBeGreaterThanOrEqual(1); - expect(within(card).getByText(/threshold 75%/)).toBeInTheDocument(); - expect(within(card).getByText(/breach in/i)).toBeInTheDocument(); - // The generic "Ask Assistant" affordance must NOT appear when the - // forecast variant is active - the generic card is gated by fallback. - expect(screen.queryByText('Ask Assistant')).not.toBeInTheDocument(); - }); - - it('renders compact action review when a capacity finding has no proposal attached', async () => { - const finding = makeFinding({ id: 'finding-cap-2' }); - mockState.findings = [finding]; - mockState.remediationPlans = [makePlan(finding.id, false)]; - - render(() => ); - - await waitFor(() => expect(mockState.loadRemediationPlans).toHaveBeenCalled()); - - fireEvent.click(screen.getByText('Storage pool tank at 87.3% usage')); - - expect(screen.queryByTestId('capacity-forecast-approval-card')).not.toBeInTheDocument(); - expect(screen.getByText('Action review')).toBeInTheDocument(); - expect(screen.getByText('Ask Assistant')).toBeInTheDocument(); - expect(screen.queryByText('Remediation Plan')).not.toBeInTheDocument(); - }); - - it('never renders the capacity-forecast card for a non-capacity finding even if a proposal somehow attaches', async () => { - // This case should not occur in production - the wire-in only attaches - // proposals to capacity findings - but the frontend gate must stay - // closed regardless. A drifted backend that mis-categorises a finding - // must not leak the capacity-forecast card onto the wrong finding type. - const finding = makeFinding({ - id: 'finding-perf-1', - category: 'performance', - title: 'High CPU on appserver: 92.0%', - }); - mockState.findings = [finding]; - mockState.remediationPlans = [makePlan(finding.id, true)]; - - render(() => ); - - await waitFor(() => expect(mockState.loadRemediationPlans).toHaveBeenCalled()); - - fireEvent.click(screen.getByText('High CPU on appserver: 92.0%')); - - expect(screen.queryByTestId('capacity-forecast-approval-card')).not.toBeInTheDocument(); - expect(screen.getByText('Action review')).toBeInTheDocument(); - expect(screen.getByText('Ask Assistant')).toBeInTheDocument(); - }); - - it('routes Approve through AIAPI.approveRemediationPlan and Reject through dismiss without bypassing handlers', async () => { - const finding = makeFinding({ id: 'finding-cap-3' }); - mockState.findings = [finding]; - mockState.remediationPlans = [makePlan(finding.id, true)]; - - render(() => ); - - await waitFor(() => expect(mockState.loadRemediationPlans).toHaveBeenCalled()); - - fireEvent.click(screen.getByText('Storage pool tank at 87.3% usage')); - - fireEvent.click(screen.getByTestId('capacity-forecast-approve')); - - await waitFor(() => - expect(mockState.approveRemediationPlan).toHaveBeenCalledWith('plan-finding-cap-3'), - ); - }); -}); diff --git a/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts b/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts index 3ff571763..b0c8966c0 100644 --- a/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts +++ b/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts @@ -66,10 +66,7 @@ describe('FindingsPanel assistant handoff', () => { expect(findingsPanelSource).toContain('investigationRecord: finding.investigationRecord'); expect(findingsPanelSource).toContain('pendingApproval: pendingApprovalBriefing'); expect(findingsPanelSource).toContain('proposedFix,'); - expect(findingsPanelSource).toContain( - 'const nextStepAction = getFindingPrimaryActionPresentation(finding);', - ); - expect(findingsPanelSource).toContain('nextStepAction,'); + expect(findingsPanelSource).not.toContain('nextStepAction,'); expect(findingsPanelSource).toContain('AIAPI.getInvestigation(finding.id)'); expect(findingsPanelSource).toContain('await aiIntelligenceStore.loadPendingApprovals()'); }); @@ -238,38 +235,19 @@ describe('FindingsPanel assistant handoff', () => { expect(findingsPanelSource).toContain('text-amber-600 dark:text-amber-400'); }); - it('renders a distinct capacity-forecast approval card when a forecast-driven proposal is attached', () => { - // The capacity-forecast card variant is only rendered when the - // RemediationPlan carries a proposed_action_plan with - // source === 'capacity_forecast' AND the finding category is - // 'capacity'. Pin the wiring so the variant doesn't accidentally take - // over the generic remediation plan card. - expect(findingsPanelSource).toContain("finding.category === 'capacity'"); - expect(findingsPanelSource).toContain("proposal()?.source === 'capacity_forecast'"); - expect(findingsPanelSource).toContain('isCapacityForecastProposal()'); - expect(findingsPanelSource).toContain('data-testid="capacity-forecast-approval-card"'); - // Card must surface current/projected/threshold so the operator can - // decide without digging into Patrol metrics. - expect(findingsPanelSource).toContain('metric!.currentValue.toFixed(1)'); - expect(findingsPanelSource).toContain('metric!.predictedValue!.toFixed(1)'); - expect(findingsPanelSource).toContain('metric!.thresholdValue!.toFixed(0)'); - // Approval-gated by contract; the badge must communicate that even when - // the action is preflight-only (Allowed=false). - expect(findingsPanelSource).toContain('requires approval'); - expect(findingsPanelSource).toContain('p.allowed === false'); - expect(findingsPanelSource).toContain('preflight only'); - // Must reuse the existing approval handlers - approve flows through - // AIAPI.approveRemediationPlan, reject flows through handleDismissPlan. - // No bypass / parallel state machine. - expect(findingsPanelSource).toContain('handleApproveProposedPlan'); - expect(findingsPanelSource).toContain('AIAPI.approveRemediationPlan(plan.id)'); - expect(findingsPanelSource).toContain('data-testid="capacity-forecast-approve"'); - expect(findingsPanelSource).toContain('data-testid="capacity-forecast-reject"'); - // The fallback path keeps non-capacity action artifacts compact for - // Assistant review rather than rendering Patrol-authored steps. - expect(findingsPanelSource).toContain('fallback={(()'); - expect(findingsPanelSource).toContain('Action review'); + it('keeps remediation artifacts as compact Assistant context only', () => { + // Patrol findings should not grow a frontend-authored proposal surface. + // Any existing artifact is a pointer for Assistant review, not a visible + // fix plan, capacity proposal, or approval bypass. + expect(findingsPanelSource).toContain('Assistant context'); expect(findingsPanelSource).toContain('Ask Assistant'); + expect(findingsPanelSource).toContain('handleOpenPlanInAssistant'); + expect(findingsPanelSource).toContain('handleDismissPlan'); + expect(findingsPanelSource).not.toContain('capacity_forecast'); + expect(findingsPanelSource).not.toContain('Capacity-forecast proposal'); + expect(findingsPanelSource).not.toContain('Approve proposal'); + expect(findingsPanelSource).not.toContain('handleApproveProposedPlan'); + expect(findingsPanelSource).not.toContain('AIAPI.approveRemediationPlan(plan.id)'); expect(findingsPanelSource).not.toContain('Remediation Plan'); expect(findingsPanelSource).not.toContain(''); }); diff --git a/frontend-modern/src/components/patrol/__tests__/ApprovalSection.test.tsx b/frontend-modern/src/components/patrol/__tests__/ApprovalSection.test.tsx index 78adef963..c7e851ace 100644 --- a/frontend-modern/src/components/patrol/__tests__/ApprovalSection.test.tsx +++ b/frontend-modern/src/components/patrol/__tests__/ApprovalSection.test.tsx @@ -127,7 +127,7 @@ describe('ApprovalSection', () => { expect(prompt).toContain( 'I\'d like to discuss this Patrol finding: "CPU saturation" on node-1', ); - expect(prompt).toContain('Start by reviewing the governed action posture'); + expect(prompt).toContain('Governed action posture is attached'); expect(prompt).toContain('Recover or regenerate the governed approval before execution'); expect(context).toEqual( expect.objectContaining({ @@ -143,11 +143,6 @@ describe('ApprovalSection', () => { expect.stringContaining('fix queued for governed review'), expect.stringContaining('Recover or regenerate the governed approval before execution'), ]), - suggestedPrompts: [ - 'Review approval risk and next step', - 'Explain current finding status', - 'List approval prerequisites before action', - ], }), autonomousMode: false, handoffResources: [{ id: 'host-1', name: 'node-1', node: undefined, type: 'host' }], @@ -202,7 +197,7 @@ describe('ApprovalSection', () => { expect(openWithPromptMock).toHaveBeenCalledTimes(1); const [prompt, context] = openWithPromptMock.mock.calls[0]; - expect(prompt).toContain('Start by reviewing governed approval approval-1'); + expect(prompt).toContain('Governed approval approval-1 is attached'); expect(prompt).toContain('approval status pending'); expect(prompt).toContain('high risk'); expect(prompt).not.toContain('systemctl restart nginx'); @@ -227,11 +222,6 @@ describe('ApprovalSection', () => { commandSummary: '1 command recorded for approval context', safetyNote: 'Command details stay in approval context; execution requires the governed approval flow.', - suggestedPrompts: [ - 'Review approval risk and next step', - 'Explain current finding status', - 'Summarize remediation without command text', - ], }), autonomousMode: false, handoffResources: [{ id: 'agent-1', name: 'node-1', node: undefined, type: 'agent' }], @@ -305,7 +295,7 @@ describe('ApprovalSection', () => { expect(openWithPromptMock).toHaveBeenCalledTimes(1); const [prompt, context] = openWithPromptMock.mock.calls[0]; - expect(prompt).toContain('Start by reviewing the governed action posture'); + expect(prompt).toContain('Governed action posture is attached'); expect(prompt).toContain('recorded action artifact Restart the workload service'); expect(prompt).toContain('target node-1'); expect(prompt).toContain('high risk'); @@ -329,11 +319,6 @@ describe('ApprovalSection', () => { commandSummary: '1 command recorded for approval context', safetyNote: 'Command details stay in approval context; destructive actions require governed approval.', - suggestedPrompts: [ - 'Review approval risk and next step', - 'Explain current finding status', - 'Summarize remediation without command text', - ], }), autonomousMode: false, handoffResources: [{ id: 'agent-1', name: 'node-1', node: undefined, type: 'agent' }], diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx index 0b682db18..f2fa38be0 100644 --- a/frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx +++ b/frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx @@ -1,32 +1,13 @@ import { createMemo, Show } from 'solid-js'; -import ActivityIcon from 'lucide-solid/icons/activity'; -import CheckCircleIcon from 'lucide-solid/icons/check-circle'; import AlertCircleIcon from 'lucide-solid/icons/alert-circle'; -import MessageSquareIcon from 'lucide-solid/icons/message-square'; -import PlayIcon from 'lucide-solid/icons/play'; -import SettingsIcon from 'lucide-solid/icons/settings'; import { getPatrolAssessmentShellPresentation, getPatrolAssessmentPresentation, - getPatrolRecommendedNextStepPresentation, getPatrolRecencyPresentation, - getPatrolScoreChipLabel, - getPatrolVerificationPresentation, getPatrolSummaryMetricState, - type PatrolRecommendedNextStepAction, } from '@/utils/patrolSummaryPresentation'; -import { getPatrolLatestRunPresentation } from '@/utils/patrolRunPresentation'; import { getPatrolRuntimePresentation } from '@/utils/patrolRuntimePresentation'; import { formatRelativeTime } from '@/utils/format'; -import { aiChatStore } from '@/stores/aiChat'; -import { aiIntelligenceStore } from '@/stores/aiIntelligence'; -import type { ApprovalRequest } from '@/api/ai'; -import { - buildPatrolAssessmentAssistantHandoff, - buildPatrolAssistantApprovalBriefingInput, - buildPatrolAssistantProposedFixBriefingInput, - type PatrolAssessmentAssistantFindingInput, -} from './patrolInvestigationContextModel'; import type { PatrolIntelligenceState } from './usePatrolIntelligenceState'; function PatrolAssessmentLoadingShell() { @@ -106,13 +87,6 @@ export function PatrolIntelligenceSummary(props: { state: PatrolIntelligenceStat runs: state.patrolRunHistory.value() ?? [], }), ); - const verification = createMemo(() => - getPatrolVerificationPresentation({ - runs: state.patrolRunHistory.value() ?? [], - runtimeState: state.runtimeState(), - blockedReason: state.blockedReason(), - }), - ); const recency = createMemo(() => getPatrolRecencyPresentation({ runs: state.patrolRunHistory.value() ?? [], @@ -120,23 +94,6 @@ export function PatrolIntelligenceSummary(props: { state: PatrolIntelligenceStat lastActivityAt: state.patrolStatus()?.last_activity_at, }), ); - const latestRun = createMemo(() => - getPatrolLatestRunPresentation(state.patrolRunHistory.value() ?? []), - ); - const scoreChipLabel = createMemo(() => - getPatrolScoreChipLabel({ - overallHealth: state.intelligenceSummary()?.overall_health, - activeFindings: state.activePatrolFindings(), - }), - ); - const recommendedNextStep = createMemo(() => - getPatrolRecommendedNextStepPresentation({ - assessment: assessment(), - verification: verification(), - activeFindings: state.activePatrolFindings(), - pendingApprovalCount: aiIntelligenceStore.patrolPendingApprovals.length, - }), - ); const compactRiskSummary = createMemo(() => { const stats = summaryStats(); const parts: string[] = []; @@ -180,118 +137,6 @@ export function PatrolIntelligenceSummary(props: { state: PatrolIntelligenceStat return parts.join(' · '); }); - const recommendedNextStepAction = createMemo(() => recommendedNextStep().action); - const recommendedNextStepActionDisabled = createMemo(() => { - const action = recommendedNextStepAction(); - return ( - action?.kind === 'run_patrol' && - (state.isTriggeringPatrol() || - !state.canTriggerPatrol() || - state.manualRunRequested() || - state.patrolStream.isStreaming()) - ); - }); - const recommendedNextStepActionDisabledReason = createMemo(() => - recommendedNextStepActionDisabled() ? state.triggerPatrolDisabledReason() : '', - ); - const recommendedNextStepActionLabel = createMemo(() => { - const action = recommendedNextStepAction(); - if (action?.kind !== 'run_patrol') { - return action?.label; - } - - if (state.isTriggeringPatrol()) { - return 'Starting...'; - } - - if (state.manualRunRequested() || state.patrolStream.isStreaming()) { - return 'Running...'; - } - - return action.label; - }); - const activeFindingsWithApprovalContext = createMemo( - () => { - const approvalsByFindingId = new Map( - aiIntelligenceStore.patrolPendingApprovals.map((approval) => [approval.targetId, approval]), - ); - return state.activePatrolFindings().map((finding) => { - const approval = approvalsByFindingId.get(finding.id); - if (!approval) { - return finding; - } - - return { - ...finding, - pendingApproval: buildPatrolAssessmentApprovalBriefing(approval), - proposedFix: finding.investigationRecord?.proposed_fix - ? undefined - : buildPatrolAssessmentApprovalProposedFixBriefing(approval), - }; - }); - }, - ); - const assessmentAssistantHandoff = createMemo(() => { - const recommendation = recommendedNextStep(); - return buildPatrolAssessmentAssistantHandoff({ - assessment: assessment(), - overallHealth: state.intelligenceSummary()?.overall_health, - scoreChipLabel: scoreChipLabel(), - metricState: metricState(), - verification: verification(), - recency: recency(), - latestRun: latestRun(), - investigationContext: { - recentChangeCount: state.recentChangeCount(), - correlationCount: state.correlationTotal(), - governedResourceCount: state.policyPosture()?.total_resources ?? 0, - hasContext: state.hasInvestigationContext(), - summaryText: state.investigationContextSummary(), - }, - supportingEvidence: { - recentChanges: state.supportingRecentChanges(), - correlations: state.correlations(), - }, - recommendedNextStep: { - title: recommendation.title, - description: recommendation.description, - actionLabel: recommendation.action?.label, - actionKind: recommendation.action?.kind, - actionDisabledReason: recommendedNextStepActionDisabledReason(), - }, - activeFindings: activeFindingsWithApprovalContext(), - }); - }); - - const handleDiscussAssessment = async () => { - await aiIntelligenceStore.loadPendingApprovals(); - const handoff = assessmentAssistantHandoff(); - aiChatStore.openWithPrompt(handoff.prompt, handoff.context); - }; - - const handleRecommendedNextStepAction = (action: PatrolRecommendedNextStepAction) => { - switch (action.kind) { - case 'discuss_assessment': - void handleDiscussAssessment(); - return; - case 'review_approvals': - state.setSelectedRun(null); - state.setActiveTab('findings'); - state.setFindingsFilterOverride('approvals'); - return; - case 'review_findings': - state.setSelectedRun(null); - state.setActiveTab('findings'); - state.setFindingsFilterOverride('active'); - return; - case 'run_patrol': - void state.handleRunPatrol(); - return; - case 'open_provider_settings': - return; - } - }; - return ( <> @@ -350,57 +195,6 @@ export function PatrolIntelligenceSummary(props: { state: PatrolIntelligenceStat {compactAssessmentSummary()} - -
- Next: - {recommendedNextStep().title} -
- - -
- - {(action) => ( - handleRecommendedNextStepAction(action())} - > - {renderRecommendedNextStepActionIcon( - action(), - state.isTriggeringPatrol() || - state.manualRunRequested() || - state.patrolStream.isStreaming(), - )} - {recommendedNextStepActionLabel()} - - } - > - {(href) => ( - - {renderRecommendedNextStepActionIcon(action(), false)} - {action().label} - - )} - - )} -
@@ -409,36 +203,3 @@ export function PatrolIntelligenceSummary(props: { state: PatrolIntelligenceStat ); } - -function buildPatrolAssessmentApprovalBriefing(approval: ApprovalRequest) { - return buildPatrolAssistantApprovalBriefingInput(approval); -} - -function buildPatrolAssessmentApprovalProposedFixBriefing(approval: ApprovalRequest) { - return buildPatrolAssistantProposedFixBriefingInput({ - description: approval.context, - riskLevel: approval.riskLevel, - targetHost: approval.targetName, - commandCount: approval.command ? 1 : 0, - }); -} - -function renderRecommendedNextStepActionIcon( - action: PatrolRecommendedNextStepAction, - running: boolean, -) { - const iconClass = `h-4 w-4 ${running && action.kind === 'run_patrol' ? 'animate-pulse' : ''}`; - - switch (action.kind) { - case 'open_provider_settings': - return