mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-21 10:23:36 +00:00
Route Patrol approval handoffs through structured context
This commit is contained in:
parent
92ee9c3b1e
commit
dd2ee25867
5 changed files with 211 additions and 132 deletions
|
|
@ -309,7 +309,13 @@ runtime cost control, and shared AI transport surfaces.
|
|||
while preserving the safe visible briefing and request-local
|
||||
approval-required posture; later turns must rely on backend-owned session
|
||||
model-context hydration and current canonical stores instead of resending
|
||||
stale browser handoff payloads.
|
||||
stale browser handoff payloads. Patrol approval-row Assistant entries are
|
||||
still Patrol finding handoffs, not local prompt-only shortcuts: live
|
||||
approval rows, expired proposed-fix rows, and missing-detail queued-fix
|
||||
recovery rows must route through the shared Patrol finding handoff builder
|
||||
so the backend receives the same bounded model-only finding context,
|
||||
resource reference, and safe action reference posture that the main finding
|
||||
handoff uses.
|
||||
Proposed-fix command text must stay out of both the persisted chat message
|
||||
and the model-only handoff context, and command payloads remain
|
||||
approval-context data, not conversational copy.
|
||||
|
|
|
|||
|
|
@ -877,18 +877,20 @@ frontend primitive boundary.
|
|||
structured finding context, so
|
||||
Patrol handoffs render as Patrol handoffs or Patrol findings, and alert
|
||||
handoffs render as alert investigations, rather than generic dashboard
|
||||
briefs. Patrol approval-row Assistant prompts must
|
||||
follow that same drawer primitive contract: safe approval metadata may enter
|
||||
the prompt and context, but raw command text stays out and the scoped
|
||||
request must pass `autonomousMode:false` instead of changing the user's
|
||||
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. 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.
|
||||
briefs. Patrol approval-row Assistant prompts must route through the same
|
||||
feature-owned finding handoff helper rather than hand-written prompt-only
|
||||
drawer opens: safe approval metadata, proposed-fix summaries, resource
|
||||
references, and bounded `handoff_actions` may enter the prompt and context,
|
||||
but raw command text stays out and the scoped request must pass
|
||||
`autonomousMode:false` instead of changing the user's 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. 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
|
||||
|
|
|
|||
|
|
@ -171,13 +171,16 @@ Patrol-specific presentation helpers.
|
|||
sync. The model-only context may include current finding status, recurrence,
|
||||
investigation record facts, evidence, verification, approval posture,
|
||||
dry-run posture, proposed-fix summary, and target resource references without
|
||||
raw command payloads. 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
|
||||
never paste the approval command or proposed-fix command text into the chat
|
||||
prompt. Remediation-plan Assistant
|
||||
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
|
||||
summary/count metadata as review context, attach the target resource
|
||||
reference, include bounded `handoff_actions` for live approvals or structured
|
||||
proposed fixes when present, force the request-local approval-required mode,
|
||||
attach the Patrol-owned visible drawer briefing for the pending approval or
|
||||
queued-fix recovery state, and never paste the approval command or
|
||||
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, safe suggested prompts may ask about plan risk,
|
||||
prerequisites, rollback, and verification, while command and rollback command
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { hasFeature } from '@/stores/license';
|
|||
import { AIAPI, type ApprovalRequest, type ApprovalExecutionResult } from '@/api/ai';
|
||||
import { getApprovalRiskPresentation } from '@/utils/approvalRiskPresentation';
|
||||
import {
|
||||
buildPatrolAssistantFindingBriefing,
|
||||
buildPatrolAssistantFindingHandoff,
|
||||
buildPatrolAssistantApprovalBriefingInput,
|
||||
buildPatrolAssistantProposedFixBriefingInput,
|
||||
type PatrolAssistantProposedFixBriefingSource,
|
||||
|
|
@ -44,28 +44,44 @@ export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
|
|||
|
||||
const canAutoFix = createMemo(() => hasFeature('ai_autofix'));
|
||||
|
||||
const approvalBriefing = (
|
||||
const proposedFixBriefing = (
|
||||
approval: ApprovalRequest | null,
|
||||
fix?: PatrolAssistantProposedFixBriefingSource | null,
|
||||
) =>
|
||||
buildPatrolAssistantFindingBriefing({
|
||||
buildPatrolAssistantProposedFixBriefingInput(
|
||||
fix ||
|
||||
(approval
|
||||
? {
|
||||
description: approval.context,
|
||||
riskLevel: approval.riskLevel,
|
||||
targetHost: approval.targetName,
|
||||
commandCount: approval.command ? 1 : 0,
|
||||
}
|
||||
: null),
|
||||
);
|
||||
|
||||
const assistantHandoff = (
|
||||
approval: ApprovalRequest | null,
|
||||
fix?: PatrolAssistantProposedFixBriefingSource | null,
|
||||
) =>
|
||||
buildPatrolAssistantFindingHandoff({
|
||||
id: props.findingId,
|
||||
title: props.findingTitle || 'Patrol finding',
|
||||
subject: props.resourceName || 'affected resource',
|
||||
description:
|
||||
approval?.context ||
|
||||
fix?.description ||
|
||||
(!approval && props.investigationOutcome === 'fix_queued'
|
||||
? 'The original approval details are no longer available. Recover or regenerate the governed approval before execution.'
|
||||
: undefined),
|
||||
findingStatus: 'active',
|
||||
investigationOutcome: props.investigationOutcome,
|
||||
loopState: props.investigationOutcome || 'awaiting_approval',
|
||||
resourceId: props.resourceId,
|
||||
resourceName: props.resourceName,
|
||||
resourceType: props.resourceType,
|
||||
pendingApproval: buildPatrolAssistantApprovalBriefingInput(approval),
|
||||
proposedFix: buildPatrolAssistantProposedFixBriefingInput(
|
||||
fix ||
|
||||
(approval
|
||||
? {
|
||||
description: approval.context,
|
||||
riskLevel: approval.riskLevel,
|
||||
targetHost: approval.targetName,
|
||||
commandCount: approval.command ? 1 : 0,
|
||||
}
|
||||
: null),
|
||||
),
|
||||
proposedFix: proposedFixBriefing(approval, fix),
|
||||
});
|
||||
|
||||
const handleFixWithAssistant = (
|
||||
|
|
@ -74,47 +90,14 @@ export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
|
|||
e: Event,
|
||||
) => {
|
||||
e.stopPropagation();
|
||||
const desc = approval?.context || fix?.description || 'No description available';
|
||||
const targetHost = approval?.targetName || fix?.target_host;
|
||||
const riskLevel = approval?.riskLevel || fix?.risk_level || 'unknown';
|
||||
const rationale = fix?.rationale;
|
||||
|
||||
let prompt = `Patrol investigated a finding and queued a governed fix for review.\n\n**Finding:** ${props.findingTitle || 'Unknown finding'} on ${props.resourceName || 'unknown resource'}\n**Proposed fix:** ${desc}`;
|
||||
if (approval?.id) prompt += `\n**Approval:** ${approval.id}`;
|
||||
if (approval?.status) prompt += `\n**Approval status:** ${approval.status}`;
|
||||
if (targetHost) prompt += `\n**Target:** ${targetHost}`;
|
||||
prompt += `\n**Risk level:** ${riskLevel}`;
|
||||
if (rationale) prompt += `\n**Rationale:** ${rationale}`;
|
||||
prompt +=
|
||||
'\n\nUse the attached finding context and governed approval flow. Do not infer, repeat, or execute raw command text from this chat handoff.';
|
||||
|
||||
aiChatStore.openWithPrompt(prompt, {
|
||||
targetType: props.resourceType,
|
||||
targetId: props.resourceId,
|
||||
findingId: props.findingId,
|
||||
briefing: approvalBriefing(approval, fix),
|
||||
autonomousMode: false,
|
||||
});
|
||||
const handoff = assistantHandoff(approval, fix);
|
||||
aiChatStore.openWithPrompt(handoff.prompt, handoff.context);
|
||||
};
|
||||
|
||||
const handleDiscussQueuedFix = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
const title = props.findingTitle || 'Patrol finding';
|
||||
const subject = props.resourceName || 'affected resource';
|
||||
aiChatStore.openWithPrompt(
|
||||
[
|
||||
`Patrol queued a governed fix for ${title} on ${subject}, but the live approval payload is no longer available.`,
|
||||
'Use the attached Patrol briefing to explain the current action state, approval recovery path, and safest next step.',
|
||||
'Do not infer, repeat, or execute raw command text from this chat handoff.',
|
||||
].join('\n\n'),
|
||||
{
|
||||
targetType: props.resourceType,
|
||||
targetId: props.resourceId,
|
||||
findingId: props.findingId,
|
||||
briefing: approvalBriefing(null),
|
||||
autonomousMode: false,
|
||||
},
|
||||
);
|
||||
const handoff = assistantHandoff(null);
|
||||
aiChatStore.openWithPrompt(handoff.prompt, handoff.context);
|
||||
};
|
||||
|
||||
// Load investigation details when outcome indicates a fix was proposed/executed
|
||||
|
|
|
|||
|
|
@ -122,9 +122,15 @@ describe('ApprovalSection', () => {
|
|||
|
||||
fireEvent.click(screen.getByRole('button', { name: /discuss with assistant/i }));
|
||||
|
||||
expect(openWithPromptMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Patrol queued a governed fix for CPU saturation on node-1'),
|
||||
{
|
||||
expect(openWithPromptMock).toHaveBeenCalledTimes(1);
|
||||
const [prompt, context] = openWithPromptMock.mock.calls[0];
|
||||
expect(prompt).toContain(
|
||||
"I'd like to discuss this Patrol finding: \"CPU saturation\" on node-1",
|
||||
);
|
||||
expect(prompt).toContain('Start by reviewing the governed proposed fix or action posture');
|
||||
expect(prompt).toContain('Recover or regenerate the governed approval before execution');
|
||||
expect(context).toEqual(
|
||||
expect.objectContaining({
|
||||
targetType: 'host',
|
||||
targetId: 'host-1',
|
||||
findingId: 'finding-1',
|
||||
|
|
@ -144,8 +150,24 @@ describe('ApprovalSection', () => {
|
|||
],
|
||||
}),
|
||||
autonomousMode: false,
|
||||
},
|
||||
handoffResources: [{ id: 'host-1', name: 'node-1', node: undefined, type: 'host' }],
|
||||
handoffActions: undefined,
|
||||
context: expect.objectContaining({
|
||||
source: 'pulse-patrol-finding',
|
||||
findingId: 'finding-1',
|
||||
resourceId: 'host-1',
|
||||
resourceName: 'node-1',
|
||||
resourceType: 'host',
|
||||
actionReferenceCount: 0,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(context.handoffContext).toContain('[Patrol Finding Context]');
|
||||
expect(context.handoffContext).toContain('Finding ID: finding-1');
|
||||
expect(context.handoffContext).toContain(
|
||||
'Recover or regenerate the governed approval before execution',
|
||||
);
|
||||
expect(context.handoffContext).toContain('Command Boundary:');
|
||||
});
|
||||
|
||||
it('opens Assistant from a pending Patrol approval without carrying raw command text', async () => {
|
||||
|
|
@ -180,39 +202,70 @@ describe('ApprovalSection', () => {
|
|||
|
||||
expect(openWithPromptMock).toHaveBeenCalledTimes(1);
|
||||
const [prompt, context] = openWithPromptMock.mock.calls[0];
|
||||
expect(prompt).toContain('queued a governed fix for review');
|
||||
expect(prompt).toContain('**Approval:** approval-1');
|
||||
expect(prompt).toContain('**Approval status:** pending');
|
||||
expect(prompt).toContain('**Risk level:** high');
|
||||
expect(prompt).toContain('Start by reviewing governed approval approval-1');
|
||||
expect(prompt).toContain('approval status pending');
|
||||
expect(prompt).toContain('high risk');
|
||||
expect(prompt).not.toContain('systemctl restart nginx');
|
||||
expect(prompt).not.toContain('Please execute this fix');
|
||||
expect(context).toEqual({
|
||||
targetType: 'agent',
|
||||
targetId: 'agent-1',
|
||||
findingId: 'finding-1',
|
||||
briefing: expect.objectContaining({
|
||||
sourceLabel: 'Pulse Patrol',
|
||||
title: 'Operator briefing attached',
|
||||
subject: 'CPU saturation on node-1',
|
||||
statusLabel: 'Pending approval · High risk',
|
||||
detailLines: expect.arrayContaining([
|
||||
expect.stringContaining('live approval pending'),
|
||||
expect.stringContaining('Proposed fix: Restart the workload service'),
|
||||
expect.stringContaining('1 command recorded for approval context'),
|
||||
expect.stringContaining('Review live governed approval approval-1 before execution'),
|
||||
]),
|
||||
actionLabel: 'Restart the workload service',
|
||||
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',
|
||||
expect(context).toEqual(
|
||||
expect.objectContaining({
|
||||
targetType: 'agent',
|
||||
targetId: 'agent-1',
|
||||
findingId: 'finding-1',
|
||||
briefing: expect.objectContaining({
|
||||
sourceLabel: 'Pulse Patrol',
|
||||
title: 'Operator briefing attached',
|
||||
subject: 'CPU saturation on node-1',
|
||||
statusLabel: 'Pending approval · High risk',
|
||||
detailLines: expect.arrayContaining([
|
||||
expect.stringContaining('live approval pending'),
|
||||
expect.stringContaining('Proposed fix: Restart the workload service'),
|
||||
expect.stringContaining('1 command recorded for approval context'),
|
||||
expect.stringContaining('Review live governed approval approval-1 before execution'),
|
||||
]),
|
||||
actionLabel: 'Restart the workload service',
|
||||
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' }],
|
||||
handoffActions: [
|
||||
expect.objectContaining({
|
||||
findingId: 'finding-1',
|
||||
approvalId: 'approval-1',
|
||||
approvalStatus: 'pending',
|
||||
actionRequiresApproval: true,
|
||||
description: 'Restart the workload service',
|
||||
riskLevel: 'high',
|
||||
destructive: false,
|
||||
targetHost: 'node-1',
|
||||
targetResourceId: 'agent-1',
|
||||
targetResourceName: 'node-1',
|
||||
targetResourceType: 'agent',
|
||||
}),
|
||||
],
|
||||
context: expect.objectContaining({
|
||||
source: 'pulse-patrol-finding',
|
||||
findingId: 'finding-1',
|
||||
resourceId: 'agent-1',
|
||||
resourceName: 'node-1',
|
||||
resourceType: 'agent',
|
||||
pendingApprovalId: 'approval-1',
|
||||
actionReferenceCount: 1,
|
||||
}),
|
||||
}),
|
||||
autonomousMode: false,
|
||||
});
|
||||
);
|
||||
expect(context.handoffContext).toContain('[Patrol Finding Context]');
|
||||
expect(context.handoffContext).toContain('Approval: approval-1');
|
||||
expect(context.handoffContext).toContain('Approval Status: pending');
|
||||
expect(context.handoffContext).toContain('Command Boundary:');
|
||||
expect(JSON.stringify(context.handoffActions)).not.toContain('systemctl restart nginx');
|
||||
expect(JSON.stringify(context.briefing)).not.toContain('systemctl restart nginx');
|
||||
});
|
||||
|
||||
|
|
@ -252,37 +305,69 @@ describe('ApprovalSection', () => {
|
|||
|
||||
expect(openWithPromptMock).toHaveBeenCalledTimes(1);
|
||||
const [prompt, context] = openWithPromptMock.mock.calls[0];
|
||||
expect(prompt).toContain('queued a governed fix for review');
|
||||
expect(prompt).toContain('**Proposed fix:** Restart the workload service');
|
||||
expect(prompt).toContain('**Target:** node-1');
|
||||
expect(prompt).toContain('**Risk level:** high');
|
||||
expect(prompt).toContain('Start by reviewing the governed proposed fix or action posture');
|
||||
expect(prompt).toContain('proposed fix Restart the workload service');
|
||||
expect(prompt).toContain('target node-1');
|
||||
expect(prompt).toContain('high risk');
|
||||
expect(prompt).not.toContain('systemctl restart nginx');
|
||||
expect(context).toEqual({
|
||||
targetType: 'agent',
|
||||
targetId: 'agent-1',
|
||||
findingId: 'finding-1',
|
||||
briefing: expect.objectContaining({
|
||||
sourceLabel: 'Pulse Patrol',
|
||||
title: 'Operator briefing attached',
|
||||
subject: 'CPU saturation on node-1',
|
||||
statusLabel: 'Fix Queued',
|
||||
detailLines: expect.arrayContaining([
|
||||
expect.stringContaining('fix queued for governed review'),
|
||||
expect.stringContaining('Proposed fix: Restart the workload service'),
|
||||
expect.stringContaining('Recover or regenerate the governed approval before execution'),
|
||||
]),
|
||||
actionLabel: 'Restart the workload service',
|
||||
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',
|
||||
expect(context).toEqual(
|
||||
expect.objectContaining({
|
||||
targetType: 'agent',
|
||||
targetId: 'agent-1',
|
||||
findingId: 'finding-1',
|
||||
briefing: expect.objectContaining({
|
||||
sourceLabel: 'Pulse Patrol',
|
||||
title: 'Operator briefing attached',
|
||||
subject: 'CPU saturation on node-1',
|
||||
statusLabel: 'Fix Queued',
|
||||
detailLines: expect.arrayContaining([
|
||||
expect.stringContaining('fix queued for governed review'),
|
||||
expect.stringContaining('Proposed fix: Restart the workload service'),
|
||||
expect.stringContaining(
|
||||
'Recover or regenerate the governed approval before execution',
|
||||
),
|
||||
]),
|
||||
actionLabel: 'Restart the workload service',
|
||||
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' }],
|
||||
handoffActions: [
|
||||
expect.objectContaining({
|
||||
findingId: 'finding-1',
|
||||
approvalId: undefined,
|
||||
actionRequiresApproval: false,
|
||||
description: 'Restart the workload service',
|
||||
riskLevel: 'high',
|
||||
destructive: true,
|
||||
targetHost: 'node-1',
|
||||
targetResourceId: 'agent-1',
|
||||
targetResourceName: 'node-1',
|
||||
targetResourceType: 'agent',
|
||||
}),
|
||||
],
|
||||
context: expect.objectContaining({
|
||||
source: 'pulse-patrol-finding',
|
||||
findingId: 'finding-1',
|
||||
resourceId: 'agent-1',
|
||||
resourceName: 'node-1',
|
||||
resourceType: 'agent',
|
||||
pendingApprovalId: undefined,
|
||||
actionReferenceCount: 1,
|
||||
}),
|
||||
}),
|
||||
autonomousMode: false,
|
||||
});
|
||||
);
|
||||
expect(context.handoffContext).toContain('[Patrol Finding Context]');
|
||||
expect(context.handoffContext).toContain('Proposed Fix:');
|
||||
expect(context.handoffContext).toContain('Command Boundary:');
|
||||
expect(JSON.stringify(context.handoffActions)).not.toContain('systemctl restart nginx');
|
||||
expect(JSON.stringify(context.briefing)).not.toContain('systemctl restart nginx');
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue