From f932db501c5617c99c7c878be5e9fa69dc6d80f2 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 7 May 2026 10:57:15 +0100 Subject: [PATCH] Surface governed action posture in Patrol Assistant briefings --- .../v6/internal/subsystems/api-contracts.md | 15 ++-- .../subsystems/frontend-primitives.md | 5 +- .../subsystems/patrol-intelligence.md | 12 ++- .../PatrolIntelligenceSummary.test.tsx | 5 +- .../patrolInvestigationContextModel.test.ts | 11 ++- .../patrol/patrolInvestigationContextModel.ts | 82 ++++++++++++++++++- 6 files changed, 114 insertions(+), 16 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index c8ea56960..51588c269 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -458,7 +458,10 @@ the canonical monitored-system blocked payload. 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 + counts instead of pasting page-local UI text or raw command payloads into chat, + and may derive visible action labels, safety notes, and approval-aware + suggested prompts from that same safe metadata so the drawer does not fall + back to generic assessment copy when governed actions are present and that same Patrol investigation-context owner, so visible Assistant drawer handoffs may include live pending-approval metadata only as safe operator context: approval ID, status, risk, requested/expiry timestamps, @@ -3037,10 +3040,12 @@ visible handoffs from findings, while the downstream chat service hydrates live resource state, timeline, and action audit context around that same handoff. Frontend Patrol handoff helpers may consume current pending approval list payloads only as safe metadata for that visible briefing and any -structured `handoff_actions`: approval ID, status, risk, request/expiry -timestamps, target label, action ID, approval policy, plan expiry, and dry-run -summary are allowed, while approval command text remains inside the governed -approval/remediation surface. + structured `handoff_actions`: approval ID, status, risk, request/expiry + timestamps, target label, action ID, approval policy, plan expiry, and dry-run + summary are allowed. Assessment-level visible briefings may reuse that same + safe metadata for action labels, safety notes, and approval-aware suggested + prompts, while approval command text remains inside the governed + approval/remediation surface. Patrol approval-row Assistant prompts must use the same safe metadata boundary and set `autonomousMode:false` for the request-local chat handoff; they must not paste raw approval or proposed-fix command text into the authored chat prompt. diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 1132a717f..ccd23b683 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -819,7 +819,10 @@ frontend primitive boundary. approval policy, plan expiry, dry-run posture, and command count. Those entries may be passed through shared chat transport as `handoff_actions` for model-only refresh, but the shared drawer stays a generic shell rather than - a Patrol summary prompt builder. The + 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, but 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 the source labels, attention reason, diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 6f5c45d77..d665d19ec 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -223,9 +223,10 @@ Patrol-specific presentation helpers. approval ID, pending status, risk, target, requested/expiry timestamps, action plan identity, approval policy, plan expiry, dry-run posture, and command count. They must force request-local approval-required mode, keep raw - command and approval payloads out of prompt and drawer copy, and frame - Assistant as explanation, prioritization, and safe next-step review rather - than a generic reactive chat + command and approval payloads out of prompt and drawer copy, surface visible + drawer action posture from the same safe references, and frame Assistant as + explanation, prioritization, and safe next-step review rather than a generic + reactive chat box. ## Current State @@ -284,7 +285,10 @@ evidence, bounded active-finding summaries, source-owned suggested prompts, and deduped resource references plus 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. +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, while command payloads stay out of the drawer. That active-runtime label must stay operational rather than verdict-like: the header chip should communicate that Patrol is enabled or available, not imply that infrastructure health is currently good merely because the runtime is on. diff --git a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceSummary.test.tsx b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceSummary.test.tsx index 6d2786315..0112f014f 100644 --- a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceSummary.test.tsx +++ b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceSummary.test.tsx @@ -88,10 +88,13 @@ describe('PatrolIntelligenceSummary', () => { actionDryRunSummary: 'No provider-supported dry run is available for this action.', }); expect(context.briefing).toMatchObject({ + actionLabel: '1 pending governed approval attached', + safetyNote: + 'Review approvals in the governed flow; approval policy is attached; dry-run posture is attached; raw command payloads stay out of Assistant.', suggestedPrompts: [ 'Prioritize findings and safest next step', 'Explain recent changes and correlations', - 'Summarize governed remediation risks', + 'Review pending approvals and safest next step', ], }); expect(JSON.stringify(context)).not.toContain('systemctl restart workload.service'); diff --git a/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts b/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts index 9af062550..723cf02ac 100644 --- a/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts @@ -262,7 +262,9 @@ describe('patrolInvestigationContextModel', () => { sourceLabel: 'Pulse Patrol', title: 'Patrol assessment attached', subject: 'Issues detected', - safetyNote: 'Diagnostics and remediation require governed approval.', + actionLabel: '1 governed action reference attached', + safetyNote: + 'Review action posture in the governed flow; raw command payloads stay out of Assistant.', suggestedPrompts: [ 'Prioritize findings and safest next step', 'Explain recent changes and correlations', @@ -345,8 +347,13 @@ describe('patrolInvestigationContextModel', () => { expect(handoff.context.context).toMatchObject({ pendingApprovalCount: 1, }); + expect(handoff.context.briefing).toMatchObject({ + actionLabel: '1 pending governed approval attached', + safetyNote: + 'Review approvals in the governed flow; approval policy is attached; dry-run posture is attached; destructive actions remain approval-bound; raw command payloads stay out of Assistant.', + }); expect(handoff.context.briefing?.suggestedPrompts).toContain( - 'Summarize governed remediation risks', + 'Review pending approvals and safest next step', ); expect(JSON.stringify(handoff)).not.toContain('systemctl restart workload.service'); }); diff --git a/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts b/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts index d725a2c93..d8a16b71b 100644 --- a/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts +++ b/frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts @@ -410,6 +410,8 @@ function buildPatrolAssessmentAssistantBriefing( const recentChanges = normalizeAssessmentRecentChanges(input.supportingEvidence?.recentChanges); const correlations = normalizeAssessmentCorrelations(input.supportingEvidence?.correlations); const findingEvidence = findings.map(formatAssessmentFindingEvidence).filter(isNonEmptyString); + const handoffActions = buildPatrolAssessmentHandoffActions(input); + const actionPosture = buildPatrolAssessmentActionPosture(input, handoffActions); const supportingEvidence = [ ...recentChanges.map(formatAssessmentRecentChangeEvidence), ...correlations.map(formatAssessmentCorrelationEvidence), @@ -426,8 +428,8 @@ function buildPatrolAssessmentAssistantBriefing( .filter(isNonEmptyString) .slice(0, 4), evidence: [...findingEvidence.slice(0, 3), ...supportingEvidence].slice(0, 5), - actionLabel: 'Discuss Patrol assessment', - safetyNote: 'Diagnostics and remediation require governed approval.', + actionLabel: actionPosture.actionLabel, + safetyNote: actionPosture.safetyNote, suggestedPrompts: buildPatrolAssessmentSuggestedPrompts(input, { findings, recentChanges, @@ -436,6 +438,76 @@ function buildPatrolAssessmentAssistantBriefing( }; } +function buildPatrolAssessmentActionPosture( + input: PatrolAssessmentAssistantHandoffInput, + handoffActions: AIChatHandoffAction[], +): { actionLabel: string; safetyNote: string } { + const pendingApprovalCount = normalizeAssessmentPendingApprovalCount(input.activeFindings); + const actionCount = handoffActions.length; + const hasDryRunPosture = handoffActions.some((action) => + Boolean(normalizeText(action.actionDryRunSummary) || normalizeText(action.actionPreflight)), + ); + const hasDestructiveAction = handoffActions.some((action) => Boolean(action.destructive)); + const hasApprovalPolicy = handoffActions.some((action) => + Boolean(normalizeText(action.actionApprovalPolicy)), + ); + + if (pendingApprovalCount > 0) { + return { + actionLabel: `${formatAssessmentMetricCount( + 'Pending governed approvals', + pendingApprovalCount, + )} attached`, + safetyNote: formatAssessmentActionSafetyNote({ + primary: 'Review approvals in the governed flow', + hasDryRunPosture, + hasDestructiveAction, + hasApprovalPolicy, + }), + }; + } + + if (actionCount > 0) { + return { + actionLabel: `${formatAssessmentMetricCount( + 'Governed action references', + actionCount, + )} attached`, + safetyNote: formatAssessmentActionSafetyNote({ + primary: 'Review action posture in the governed flow', + hasDryRunPosture, + hasDestructiveAction, + hasApprovalPolicy, + }), + }; + } + + return { + actionLabel: 'Discuss Patrol assessment', + safetyNote: 'Diagnostics and remediation require governed approval.', + }; +} + +function formatAssessmentActionSafetyNote(input: { + primary: string; + hasDryRunPosture: boolean; + hasDestructiveAction: boolean; + hasApprovalPolicy: boolean; +}): string { + const parts = [input.primary]; + if (input.hasApprovalPolicy) { + parts.push('approval policy is attached'); + } + if (input.hasDryRunPosture) { + parts.push('dry-run posture is attached'); + } + if (input.hasDestructiveAction) { + parts.push('destructive actions remain approval-bound'); + } + parts.push('raw command payloads stay out of Assistant'); + return `${parts.join('; ')}.`; +} + function buildPatrolAssessmentSuggestedPrompts( input: PatrolAssessmentAssistantHandoffInput, normalized: { @@ -461,7 +533,11 @@ function buildPatrolAssessmentSuggestedPrompts( } if (hasGovernedAction) { - prompts.push('Summarize governed remediation risks'); + prompts.push( + normalizeAssessmentPendingApprovalCount(input.activeFindings) > 0 + ? 'Review pending approvals and safest next step' + : 'Summarize governed remediation risks', + ); } else if (activeFindingCount > 0) { prompts.push('List evidence to verify before action'); } else if (hasSupportingEvidence) {