mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 12:13:28 +00:00
Require approval mode for governed Patrol finding handoffs
This commit is contained in:
@@ -324,7 +324,10 @@ the canonical monitored-system blocked payload.
|
||||
remediation-plan handoffs must use the same boundary for frontend-authored
|
||||
prompts: plan status, risk, step labels, and command counts are allowed,
|
||||
while raw command and rollback command payloads remain in governed action
|
||||
surfaces. The operator
|
||||
surfaces. Frontend finding-discussion handoffs that carry any live approval,
|
||||
proposed-fix, fix outcome, or remediation-plan reference must force a
|
||||
request-local approval-required Assistant mode instead of inheriting the
|
||||
user's persistent autonomous control setting. The operator
|
||||
decision and action-posture lines in the briefing must derive from those
|
||||
same structured action references after recovery so the briefing cannot
|
||||
contradict the handoff action payload. Related finding
|
||||
|
||||
@@ -826,7 +826,11 @@ frontend primitive boundary.
|
||||
persistent Assistant control level. Patrol remediation-plan drawer handoffs
|
||||
must use the same primitive boundary: plan title/status/risk, step labels,
|
||||
and command counts may enter Assistant context; raw command and rollback
|
||||
command payloads must stay in the governed remediation/action panel.
|
||||
command payloads must stay in the governed remediation/action panel. Finding
|
||||
discussion handoffs that reference a live approval, proposed fix, fix
|
||||
outcome, or remediation plan must also pass `autonomousMode:false` as a
|
||||
request-local override so the drawer shows approval-required posture without
|
||||
mutating the persistent Assistant control setting.
|
||||
11. Keep shared filter primitives coherent with route-owned option hydration.
|
||||
Feature shells such as `frontend-modern/src/features/infrastructure/`
|
||||
must keep a route-owned canonical option visible in shared selects like
|
||||
|
||||
@@ -163,8 +163,12 @@ Patrol-specific presentation helpers.
|
||||
proposed-fix command text into the chat prompt. Remediation-plan Assistant
|
||||
handoffs follow the same boundary: step labels, plan status, risk, and command
|
||||
counts are allowed, while command and rollback command text stays in the
|
||||
governed remediation or approval surface. The assembled handoff must still
|
||||
pass through the Assistant runtime's
|
||||
governed remediation or approval surface. Generic finding discussion
|
||||
handoffs must also force request-local approval-required mode when the finding
|
||||
already references a live approval, proposed fix, fix outcome, or remediation
|
||||
plan, so default autonomous Assistant settings cannot bypass the Patrol
|
||||
action-governance boundary. The assembled handoff must still pass through the
|
||||
Assistant runtime's
|
||||
resource-policy sanitizer before prompt injection, so Patrol-owned prose
|
||||
cannot leak governed resource names, IDs, aliases, nodes, paths, or
|
||||
addresses outside the canonical policy boundary.
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
buildPatrolAssistantFindingPrompt,
|
||||
buildPatrolRemediationPlanAssistantBriefing,
|
||||
buildPatrolRemediationPlanAssistantPrompt,
|
||||
patrolAssistantFindingHandoffRequiresApprovalMode,
|
||||
} from '@/features/patrol/patrolInvestigationContextModel';
|
||||
import { useResources } from '@/hooks/useResources';
|
||||
import { InvestigationSection, ApprovalSection } from '@/components/patrol';
|
||||
@@ -427,6 +428,16 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
const pendingApproval = aiIntelligenceStore.patrolPendingApprovals.find(
|
||||
(approval) => approval.toolId === 'investigation_fix' && approval.targetId === finding.id,
|
||||
);
|
||||
const pendingApprovalBriefing = pendingApproval
|
||||
? {
|
||||
id: pendingApproval.id,
|
||||
status: pendingApproval.status,
|
||||
riskLevel: pendingApproval.riskLevel,
|
||||
requestedAt: pendingApproval.requestedAt,
|
||||
expiresAt: pendingApproval.expiresAt,
|
||||
targetName: pendingApproval.targetName,
|
||||
}
|
||||
: undefined;
|
||||
const prompt = buildPatrolAssistantFindingPrompt({
|
||||
title,
|
||||
subject,
|
||||
@@ -443,16 +454,13 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
regressionCount: finding.regressionCount,
|
||||
lastRegressionAt: finding.lastRegressionAt,
|
||||
remediationId: finding.remediationPlanId,
|
||||
pendingApproval: pendingApproval
|
||||
? {
|
||||
id: pendingApproval.id,
|
||||
status: pendingApproval.status,
|
||||
riskLevel: pendingApproval.riskLevel,
|
||||
requestedAt: pendingApproval.requestedAt,
|
||||
expiresAt: pendingApproval.expiresAt,
|
||||
targetName: pendingApproval.targetName,
|
||||
}
|
||||
: undefined,
|
||||
pendingApproval: pendingApprovalBriefing,
|
||||
investigationRecord: finding.investigationRecord,
|
||||
});
|
||||
const requiresApprovalMode = patrolAssistantFindingHandoffRequiresApprovalMode({
|
||||
investigationOutcome: finding.investigationOutcome,
|
||||
remediationId: finding.remediationPlanId,
|
||||
pendingApproval: pendingApprovalBriefing,
|
||||
investigationRecord: finding.investigationRecord,
|
||||
});
|
||||
aiChatStore.openWithPrompt(prompt, {
|
||||
@@ -460,6 +468,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
targetId: finding.resourceId,
|
||||
findingId: finding.id,
|
||||
briefing,
|
||||
autonomousMode: requiresApprovalMode ? false : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -49,6 +49,10 @@ describe('FindingsPanel assistant handoff', () => {
|
||||
expect(findingsPanelSource).toContain('investigationRecord: finding.investigationRecord');
|
||||
expect(findingsPanelSource).toContain('pendingApproval: pendingApproval');
|
||||
expect(findingsPanelSource).toContain('await aiIntelligenceStore.loadPendingApprovals()');
|
||||
expect(findingsPanelSource).toContain('patrolAssistantFindingHandoffRequiresApprovalMode');
|
||||
expect(findingsPanelSource).toContain(
|
||||
'autonomousMode: requiresApprovalMode ? false : undefined',
|
||||
);
|
||||
});
|
||||
|
||||
it('routes remediation plan handoffs through the command-free Patrol handoff model', () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
buildPatrolInvestigationRecordPresentation,
|
||||
buildPatrolRemediationPlanAssistantBriefing,
|
||||
buildPatrolRemediationPlanAssistantPrompt,
|
||||
patrolAssistantFindingHandoffRequiresApprovalMode,
|
||||
} from '../patrolInvestigationContextModel';
|
||||
|
||||
describe('patrolInvestigationContextModel', () => {
|
||||
@@ -280,6 +281,50 @@ describe('patrolInvestigationContextModel', () => {
|
||||
expect(JSON.stringify(briefing)).not.toContain('systemctl');
|
||||
});
|
||||
|
||||
it('forces approval-required Assistant mode for governed finding handoffs', () => {
|
||||
expect(
|
||||
patrolAssistantFindingHandoffRequiresApprovalMode({
|
||||
pendingApproval: { id: 'approval-1', status: 'pending' },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
patrolAssistantFindingHandoffRequiresApprovalMode({
|
||||
remediationId: 'plan-1',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
patrolAssistantFindingHandoffRequiresApprovalMode({
|
||||
investigationOutcome: 'fix_queued',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
patrolAssistantFindingHandoffRequiresApprovalMode({
|
||||
investigationRecord: {
|
||||
id: 'record-1',
|
||||
finding_id: 'finding-1',
|
||||
subject: { resource_id: 'agent-1' },
|
||||
trigger: { detected_at: '2026-05-06T12:00:00Z' },
|
||||
status: 'completed',
|
||||
evidence: [],
|
||||
proposed_fix: {
|
||||
id: 'fix-1',
|
||||
description: 'Restart service',
|
||||
commands: ['systemctl restart nginx'],
|
||||
destructive: false,
|
||||
},
|
||||
verification: [],
|
||||
tools_used: [],
|
||||
started_at: '2026-05-06T12:00:00Z',
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
patrolAssistantFindingHandoffRequiresApprovalMode({
|
||||
investigationOutcome: 'needs_attention',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('builds an operator briefing from current finding facts before a Patrol record exists', () => {
|
||||
expect(
|
||||
buildPatrolAssistantFindingBriefing({
|
||||
|
||||
@@ -70,6 +70,13 @@ export interface PatrolAssistantFindingBriefingInput {
|
||||
investigationRecord?: InvestigationRecord | null;
|
||||
}
|
||||
|
||||
export interface PatrolAssistantFindingModeInput {
|
||||
investigationOutcome?: string | null;
|
||||
remediationId?: string | null;
|
||||
pendingApproval?: PatrolAssistantApprovalBriefingInput | null;
|
||||
investigationRecord?: InvestigationRecord | null;
|
||||
}
|
||||
|
||||
export interface PatrolRemediationPlanAssistantInput {
|
||||
title: string;
|
||||
subject: string;
|
||||
@@ -176,6 +183,21 @@ export function buildPatrolAssistantFindingPrompt(
|
||||
return prompt;
|
||||
}
|
||||
|
||||
export function patrolAssistantFindingHandoffRequiresApprovalMode(
|
||||
input: PatrolAssistantFindingModeInput,
|
||||
): boolean {
|
||||
const pendingApproval = normalizeApprovalBriefing(input.pendingApproval);
|
||||
if (pendingApproval.id) return true;
|
||||
if (normalizeText(input.remediationId)) return true;
|
||||
|
||||
const record = input.investigationRecord;
|
||||
if (normalizeText(record?.approval_id)) return true;
|
||||
if (record?.proposed_fix) return true;
|
||||
|
||||
const outcome = normalizeText(input.investigationOutcome || record?.outcome).toLowerCase();
|
||||
return GOVERNED_ACTION_OUTCOMES.has(outcome);
|
||||
}
|
||||
|
||||
export function buildPatrolRemediationPlanAssistantPrompt(
|
||||
input: PatrolRemediationPlanAssistantInput,
|
||||
): string {
|
||||
@@ -629,3 +651,12 @@ const PATROL_TOOL_LABELS: Record<string, string> = {
|
||||
'metrics.history': 'Metrics history',
|
||||
'ssh.exec': 'SSH exec',
|
||||
};
|
||||
|
||||
const GOVERNED_ACTION_OUTCOMES = new Set([
|
||||
'fix_queued',
|
||||
'fix_executed',
|
||||
'fix_failed',
|
||||
'fix_verified',
|
||||
'fix_verification_failed',
|
||||
'fix_verification_unknown',
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user