mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 12:13:28 +00:00
Keep Patrol approval handoffs command-free
This commit is contained in:
@@ -2974,6 +2974,9 @@ that same handoff. Frontend Patrol handoff helpers may consume current pending
|
||||
approval list payloads only as safe metadata for that visible briefing: approval
|
||||
ID, status, risk, request/expiry timestamps, and target label are allowed, 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.
|
||||
Patrol run-history serialization and persistence must also preserve full field
|
||||
parity across API responses and restart boundaries, including
|
||||
`pmg_checked`, `rejected_findings`, `triage_flags`, `triage_skipped_llm`, and
|
||||
|
||||
@@ -819,7 +819,11 @@ frontend primitive boundary.
|
||||
When the feature helper adds live approval state to the generic drawer
|
||||
briefing, it may pass only safe approval metadata into
|
||||
`AIChatContextBriefing`; raw approval commands remain owned by the governed
|
||||
approval/remediation panels.
|
||||
approval/remediation panels. 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.
|
||||
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
|
||||
|
||||
@@ -157,8 +157,11 @@ Patrol-specific presentation helpers.
|
||||
expiry, and target label; it must 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. The assembled handoff must still pass
|
||||
through the Assistant runtime's
|
||||
posture. Inline Patrol approval actions that open Assistant must follow the
|
||||
same rule: pass approval ID/status/risk/target as review context, force the
|
||||
request-local approval-required mode, and never paste the approval command or
|
||||
proposed-fix command text into the chat prompt. 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.
|
||||
|
||||
@@ -51,23 +51,24 @@ export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
|
||||
) => {
|
||||
e.stopPropagation();
|
||||
const desc = approval?.context || fix?.description || 'No description available';
|
||||
const command =
|
||||
approval?.command || (fix?.commands && fix.commands.length > 0 ? fix.commands[0] : undefined);
|
||||
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 proposed a fix. Please help me execute it.\n\n**Finding:** ${props.findingTitle || 'Unknown finding'} on ${props.resourceName || 'unknown resource'}\n**Proposed fix:** ${desc}`;
|
||||
if (command) prompt += `\n**Command:** \`${command}\``;
|
||||
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\nPlease execute this fix on the target agent.`;
|
||||
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,
|
||||
autonomousMode: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -79,6 +80,7 @@ export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
|
||||
targetType: props.resourceType,
|
||||
targetId: props.resourceId,
|
||||
findingId: props.findingId,
|
||||
autonomousMode: false,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -128,10 +128,57 @@ describe('ApprovalSection', () => {
|
||||
targetType: 'host',
|
||||
targetId: 'host-1',
|
||||
findingId: 'finding-1',
|
||||
autonomousMode: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('opens Assistant from a pending Patrol approval without carrying raw command text', async () => {
|
||||
state.pendingApprovals = [
|
||||
{
|
||||
id: 'approval-1',
|
||||
toolId: 'investigation_fix',
|
||||
command: 'systemctl restart nginx',
|
||||
targetType: 'investigation',
|
||||
targetId: 'finding-1',
|
||||
targetName: 'node-1',
|
||||
context: 'Restart the workload service',
|
||||
riskLevel: 'high',
|
||||
status: 'pending',
|
||||
requestedAt: new Date().toISOString(),
|
||||
expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
render(() => (
|
||||
<ApprovalSection
|
||||
findingId="finding-1"
|
||||
investigationOutcome="fix_queued"
|
||||
findingTitle="CPU saturation"
|
||||
resourceName="node-1"
|
||||
resourceType="agent"
|
||||
resourceId="agent-1"
|
||||
/>
|
||||
));
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /fix with assistant/i }));
|
||||
|
||||
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).not.toContain('systemctl restart nginx');
|
||||
expect(prompt).not.toContain('Please execute this fix');
|
||||
expect(context).toEqual({
|
||||
targetType: 'agent',
|
||||
targetId: 'agent-1',
|
||||
findingId: 'finding-1',
|
||||
autonomousMode: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('recreates and executes a queued fix when autofix is available', async () => {
|
||||
state.hasAutoFix = true;
|
||||
getInvestigationMock.mockResolvedValue(null);
|
||||
|
||||
Reference in New Issue
Block a user