Surface governed action posture in Patrol Assistant briefings

This commit is contained in:
rcourtman
2026-05-07 10:57:15 +01:00
parent a66b80ceb9
commit f932db501c
6 changed files with 114 additions and 16 deletions
@@ -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.
@@ -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,
@@ -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.
@@ -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');
@@ -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');
});
@@ -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) {