mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 19:57:09 +00:00
Make Patrol finding prompts action-aware
This commit is contained in:
@@ -467,7 +467,11 @@ the canonical monitored-system blocked payload.
|
||||
drawer handoffs may include live pending-approval metadata only as safe
|
||||
operator context: approval ID, status, risk, requested/expiry timestamps,
|
||||
target label, generated approval summary, and command count are allowed, while
|
||||
approval command payloads stay inside governed approval/remediation surfaces
|
||||
approval command payloads stay inside governed approval/remediation surfaces,
|
||||
and finding-level first-turn prompt emphasis may be derived from the same
|
||||
bounded metadata only so Assistant starts by reviewing approval status, risk,
|
||||
dry-run posture, proposed-fix posture, and safest next step without receiving
|
||||
raw command or execution payloads
|
||||
and the dedicated `frontend-modern/src/stores/aiIntelligenceSummaryModel.ts` owner, so recent-change counts and governed policy-posture fallbacks normalize once at the shared store boundary instead of as Patrol-hook-local payload repair
|
||||
and the shared `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` card, so learned correlations and correlation context stay rendered through one governed frontend card instead of separate page-local list loops
|
||||
and the same shared correlation card's ordering and truncation rule, so callers pass raw correlations instead of encoding their own top-N sort behavior
|
||||
|
||||
@@ -821,8 +821,9 @@ frontend primitive boundary.
|
||||
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, but it must not expose raw
|
||||
command or execution payloads.
|
||||
suggested prompts or first-turn prompt emphasis for assessment and
|
||||
finding-level handoffs, 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
|
||||
|
||||
@@ -161,7 +161,11 @@ Patrol-specific presentation helpers.
|
||||
not copy the approval command payload into Assistant drawer prose. The
|
||||
model-only runtime briefing must apply that same
|
||||
recovered approval reference when framing the operator decision and action
|
||||
posture. Inline Patrol approval actions that open Assistant must follow the
|
||||
posture. The initial visible prompt for a Patrol finding must lead with that
|
||||
governed approval or proposed-fix review instruction when safe metadata is
|
||||
attached, so the operator starts from approval status, risk, dry-run posture,
|
||||
and safest-next-step review instead of generic incident discussion. Inline
|
||||
Patrol approval actions that open Assistant must follow the
|
||||
same rule: pass approval ID/status/risk/target plus safe summary/count
|
||||
metadata as review context, force the request-local approval-required mode,
|
||||
attach the Patrol-owned visible drawer briefing for the pending approval, and
|
||||
|
||||
@@ -492,6 +492,10 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
title,
|
||||
subject,
|
||||
description: finding.description,
|
||||
investigationOutcome: finding.investigationOutcome,
|
||||
remediationId: finding.remediationPlanId,
|
||||
pendingApproval: pendingApprovalBriefing,
|
||||
proposedFix,
|
||||
investigationRecord: finding.investigationRecord,
|
||||
});
|
||||
const briefing = buildPatrolAssistantFindingBriefing({
|
||||
|
||||
@@ -48,6 +48,8 @@ describe('FindingsPanel assistant handoff', () => {
|
||||
expect(findingsPanelSource).toContain('buildPatrolAssistantApprovalBriefingInput');
|
||||
expect(findingsPanelSource).toContain('buildPatrolAssistantProposedFixBriefingInput');
|
||||
expect(findingsPanelSource).toContain('briefing,');
|
||||
expect(findingsPanelSource).toContain('investigationOutcome: finding.investigationOutcome');
|
||||
expect(findingsPanelSource).toContain('remediationId: finding.remediationPlanId');
|
||||
expect(findingsPanelSource).toContain('investigationRecord: finding.investigationRecord');
|
||||
expect(findingsPanelSource).toContain('pendingApproval: pendingApprovalBriefing');
|
||||
expect(findingsPanelSource).toContain('proposedFix,');
|
||||
|
||||
@@ -455,6 +455,61 @@ describe('patrolInvestigationContextModel', () => {
|
||||
).toContain('Use that record as the main context before suggesting next actions.');
|
||||
});
|
||||
|
||||
it('leads finding prompts with live governed approval context without command text', () => {
|
||||
const prompt = buildPatrolAssistantFindingPrompt({
|
||||
title: 'High CPU usage',
|
||||
subject: 'web-server',
|
||||
description: 'CPU stayed above 95%.',
|
||||
pendingApproval: {
|
||||
id: 'approval-1',
|
||||
status: 'pending',
|
||||
riskLevel: 'high',
|
||||
targetName: 'web-server',
|
||||
actionApprovalPolicy: 'operator',
|
||||
actionPreflight: 'Service restart would be attempted after health checks.',
|
||||
actionDryRunSummary: 'Dry run completed with one restart action.',
|
||||
},
|
||||
proposedFix: buildPatrolAssistantProposedFixBriefingInput({
|
||||
description: 'Restart the workload service',
|
||||
commands: ['systemctl restart workload.service'],
|
||||
riskLevel: 'high',
|
||||
targetHost: 'web-server',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(prompt).toContain('Start by reviewing governed approval approval-1');
|
||||
expect(prompt).toContain('approval status pending');
|
||||
expect(prompt).toContain('high risk');
|
||||
expect(prompt).toContain('approval policy attached');
|
||||
expect(prompt).toContain('dry-run posture attached');
|
||||
expect(prompt).toContain('safest next step');
|
||||
expect(prompt).not.toContain('systemctl restart workload.service');
|
||||
});
|
||||
|
||||
it('leads finding prompts with proposed-fix posture without command text', () => {
|
||||
const prompt = buildPatrolAssistantFindingPrompt({
|
||||
title: 'Nginx down',
|
||||
subject: 'node-1',
|
||||
description: 'The service stopped responding.',
|
||||
investigationOutcome: 'fix_queued',
|
||||
remediationId: 'remediation-1',
|
||||
proposedFix: buildPatrolAssistantProposedFixBriefingInput({
|
||||
description: 'Restart nginx',
|
||||
commands: ['systemctl restart nginx'],
|
||||
riskLevel: 'medium',
|
||||
targetHost: 'node-1',
|
||||
destructive: true,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(prompt).toContain('Start by reviewing the governed proposed fix');
|
||||
expect(prompt).toContain('medium risk');
|
||||
expect(prompt).toContain('1 command recorded for approval context');
|
||||
expect(prompt).toContain('destructive action');
|
||||
expect(prompt).toContain('safest next step');
|
||||
expect(prompt).not.toContain('systemctl restart nginx');
|
||||
});
|
||||
|
||||
it('builds a drawer briefing for Assistant handoff without exposing raw commands', () => {
|
||||
const approvalRequestedAt = new Date(Date.now() - 60_000).toISOString();
|
||||
const approvalExpiresAt = new Date(Date.now() + 10 * 60_000).toISOString();
|
||||
|
||||
@@ -61,6 +61,10 @@ export interface PatrolAssistantFindingPromptInput {
|
||||
title: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
investigationOutcome?: string | null;
|
||||
remediationId?: string | null;
|
||||
pendingApproval?: PatrolAssistantApprovalBriefingInput | null;
|
||||
proposedFix?: PatrolAssistantProposedFixBriefingInput | null;
|
||||
investigationRecord?: InvestigationRecord | null;
|
||||
}
|
||||
|
||||
@@ -343,18 +347,79 @@ export function buildPatrolAssistantFindingPrompt(
|
||||
const subject = normalizeText(input.subject) || 'the affected resource';
|
||||
const description = normalizeText(input.description);
|
||||
const hasRecord = Boolean(input.investigationRecord?.id);
|
||||
const actionInstruction = buildPatrolAssistantFindingActionPromptInstruction(input);
|
||||
|
||||
let prompt = `I'd like to discuss this Patrol finding: "${title}" on ${subject}.`;
|
||||
if (hasRecord) {
|
||||
prompt +=
|
||||
'\n\nPulse Patrol has a structured investigation record for this finding. Use that record as the main context before suggesting next actions.';
|
||||
}
|
||||
if (actionInstruction) {
|
||||
prompt += `\n\n${actionInstruction}`;
|
||||
}
|
||||
if (description) {
|
||||
prompt += `\n\n${description}`;
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
function buildPatrolAssistantFindingActionPromptInstruction(
|
||||
input: PatrolAssistantFindingPromptInput,
|
||||
): string | undefined {
|
||||
const pendingApproval = normalizeApprovalBriefing(input.pendingApproval);
|
||||
const record = buildPatrolInvestigationRecordPresentation(input.investigationRecord);
|
||||
const proposedFix = record.proposedFix || normalizeProposedFixBriefing(input.proposedFix);
|
||||
const remediationId = normalizeText(input.remediationId);
|
||||
const outcome = normalizeText(input.investigationOutcome || input.investigationRecord?.outcome);
|
||||
const normalizedOutcome = outcome.toLowerCase();
|
||||
|
||||
if (pendingApproval.id) {
|
||||
const contextParts = [
|
||||
pendingApproval.status ? `approval status ${pendingApproval.status}` : undefined,
|
||||
pendingApproval.riskLevel ? `${pendingApproval.riskLevel} risk` : undefined,
|
||||
pendingApproval.targetName
|
||||
? `target ${truncateContextText(pendingApproval.targetName, 120)}`
|
||||
: undefined,
|
||||
pendingApproval.actionApprovalPolicy ? 'approval policy attached' : undefined,
|
||||
pendingApproval.actionPreflight || pendingApproval.actionDryRunSummary
|
||||
? 'dry-run posture attached'
|
||||
: undefined,
|
||||
].filter(isNonEmptyString);
|
||||
|
||||
return `Start by reviewing governed approval ${pendingApproval.id}${
|
||||
contextParts.length > 0 ? ` (${contextParts.join('; ')})` : ''
|
||||
}. Use the attached Patrol context to explain prerequisites, risk, and the safest next step before any execution.`;
|
||||
}
|
||||
|
||||
const hasGovernedActionPosture = Boolean(
|
||||
proposedFix ||
|
||||
remediationId ||
|
||||
normalizeText(input.investigationRecord?.approval_id) ||
|
||||
GOVERNED_ACTION_OUTCOMES.has(normalizedOutcome),
|
||||
);
|
||||
if (!hasGovernedActionPosture) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const contextParts = [
|
||||
proposedFix?.description
|
||||
? `proposed fix ${truncateContextText(proposedFix.description, 120)}`
|
||||
: undefined,
|
||||
proposedFix?.riskLabel ? `${proposedFix.riskLabel.toLowerCase()} risk` : undefined,
|
||||
proposedFix?.targetHost
|
||||
? `target ${truncateContextText(proposedFix.targetHost, 120)}`
|
||||
: undefined,
|
||||
proposedFix?.commandSummary,
|
||||
proposedFix?.destructive ? 'destructive action' : undefined,
|
||||
remediationId ? `remediation ${truncateContextText(remediationId, 120)}` : undefined,
|
||||
outcome ? `outcome ${formatIdentifierLabel(outcome)?.toLowerCase() || outcome}` : undefined,
|
||||
].filter(isNonEmptyString);
|
||||
|
||||
return `Start by reviewing the governed proposed fix or action posture${
|
||||
contextParts.length > 0 ? ` (${contextParts.join('; ')})` : ''
|
||||
}. Use the attached Patrol context to explain risk, prerequisites, and the safest next step without repeating command text.`;
|
||||
}
|
||||
|
||||
export function buildPatrolAssessmentAssistantHandoff(
|
||||
input: PatrolAssessmentAssistantHandoffInput,
|
||||
): PatrolAssessmentAssistantHandoff {
|
||||
|
||||
Reference in New Issue
Block a user