mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 11:46:28 +00:00
Attach Patrol finding context to Assistant handoffs
This commit is contained in:
@@ -472,9 +472,10 @@ the canonical monitored-system blocked payload.
|
||||
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; finding-level handoffs may also send one
|
||||
bounded `handoff_actions` entry for that governed approval or proposed fix so
|
||||
the Assistant runtime can refresh action posture from IDs and safe summaries
|
||||
instead of relying on pasted chat text
|
||||
bounded model-only `handoff_context`, one `handoff_resources` target
|
||||
reference, and one `handoff_actions` entry for that governed approval or
|
||||
proposed fix so the Assistant runtime can refresh finding and action posture
|
||||
from IDs and safe summaries instead of relying on pasted chat text
|
||||
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
|
||||
|
||||
@@ -823,6 +823,7 @@ frontend primitive boundary.
|
||||
references into visible action labels, safety notes, and approval-aware
|
||||
suggested prompts or first-turn prompt emphasis for assessment and
|
||||
finding-level handoffs; finding-level drawer opens may also pass one bounded
|
||||
model-only finding context, one target resource reference, and one
|
||||
`handoff_actions` reference for a live approval or proposed fix. It must not
|
||||
expose raw command or execution payloads.
|
||||
The
|
||||
|
||||
@@ -165,10 +165,13 @@ Patrol-specific presentation helpers.
|
||||
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. Finding
|
||||
handoffs may also attach a bounded model-only action reference for that same
|
||||
live approval or proposed fix, carrying approval ID/status/risk, action plan
|
||||
identity/policy/expiry, dry-run posture, proposed-fix summary, and target
|
||||
resource references without raw command payloads. Inline
|
||||
handoffs must be assembled through the Patrol-owned handoff model so the
|
||||
prompt, visible briefing, model-only finding context, resource reference,
|
||||
bounded action reference, and request-local approval-required posture stay in
|
||||
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,
|
||||
|
||||
@@ -18,10 +18,8 @@ import { aiIntelligenceStore, type UnifiedFinding } from '@/stores/aiIntelligenc
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import { aiChatStore } from '@/stores/aiChat';
|
||||
import {
|
||||
buildPatrolAssistantFindingBriefing,
|
||||
buildPatrolAssistantFindingHandoffActions,
|
||||
buildPatrolAssistantFindingPrompt,
|
||||
buildPatrolAssistantApprovalBriefingInput,
|
||||
buildPatrolAssistantFindingHandoff,
|
||||
buildPatrolAssistantProposedFixBriefingInput,
|
||||
buildPatrolRemediationPlanAssistantBriefing,
|
||||
buildPatrolRemediationPlanAssistantPrompt,
|
||||
@@ -489,61 +487,30 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
);
|
||||
const proposedFix =
|
||||
latestInvestigationProposedFix || buildLiveApprovalProposedFixBriefing(pendingApproval);
|
||||
const prompt = buildPatrolAssistantFindingPrompt({
|
||||
const handoff = buildPatrolAssistantFindingHandoff({
|
||||
id: finding.id,
|
||||
title,
|
||||
subject,
|
||||
description: finding.description,
|
||||
investigationOutcome: finding.investigationOutcome,
|
||||
remediationId: finding.remediationPlanId,
|
||||
pendingApproval: pendingApprovalBriefing,
|
||||
proposedFix,
|
||||
investigationRecord: finding.investigationRecord,
|
||||
});
|
||||
const briefing = buildPatrolAssistantFindingBriefing({
|
||||
title,
|
||||
subject,
|
||||
severity: finding.severity,
|
||||
findingStatus: finding.status,
|
||||
investigationStatus: finding.investigationStatus,
|
||||
investigationOutcome: finding.investigationOutcome,
|
||||
loopState: finding.loopState,
|
||||
timesRaised: finding.timesRaised,
|
||||
regressionCount: finding.regressionCount,
|
||||
lastRegressionAt: finding.lastRegressionAt,
|
||||
remediationId: finding.remediationPlanId,
|
||||
pendingApproval: pendingApprovalBriefing,
|
||||
proposedFix,
|
||||
investigationRecord: finding.investigationRecord,
|
||||
});
|
||||
const handoffActions = buildPatrolAssistantFindingHandoffActions({
|
||||
id: finding.id,
|
||||
title,
|
||||
description: finding.description,
|
||||
severity: finding.severity,
|
||||
status: finding.status,
|
||||
resourceId: finding.resourceId,
|
||||
resourceName: finding.resourceName,
|
||||
resourceType: finding.resourceType,
|
||||
investigationStatus: finding.investigationStatus,
|
||||
investigationOutcome: finding.investigationOutcome,
|
||||
loopState: finding.loopState,
|
||||
detectedAt: finding.detectedAt,
|
||||
lastSeenAt: finding.lastSeenAt,
|
||||
pendingApproval: pendingApprovalBriefing,
|
||||
proposedFix,
|
||||
investigationRecord: finding.investigationRecord,
|
||||
});
|
||||
const requiresApprovalMode = patrolAssistantFindingHandoffRequiresApprovalMode({
|
||||
investigationOutcome: finding.investigationOutcome,
|
||||
remediationId: finding.remediationPlanId,
|
||||
pendingApproval: pendingApprovalBriefing,
|
||||
investigationRecord: finding.investigationRecord,
|
||||
});
|
||||
aiChatStore.openWithPrompt(prompt, {
|
||||
targetType: finding.resourceType,
|
||||
targetId: finding.resourceId,
|
||||
findingId: finding.id,
|
||||
briefing,
|
||||
handoffActions: handoffActions.length > 0 ? handoffActions : undefined,
|
||||
autonomousMode: requiresApprovalMode ? false : undefined,
|
||||
});
|
||||
aiChatStore.openWithPrompt(handoff.prompt, handoff.context);
|
||||
};
|
||||
|
||||
const handleOpenPlanInAssistant = (finding: UnifiedFinding, plan: RemediationPlan, e: Event) => {
|
||||
|
||||
@@ -44,25 +44,23 @@ const patrolWorkspaceSource = readFileSync(
|
||||
|
||||
describe('FindingsPanel assistant handoff', () => {
|
||||
it('routes Patrol investigation records into the Assistant briefing context', () => {
|
||||
expect(findingsPanelSource).toContain('buildPatrolAssistantFindingBriefing');
|
||||
expect(findingsPanelSource).toContain('buildPatrolAssistantFindingHandoffActions');
|
||||
expect(findingsPanelSource).toContain('buildPatrolAssistantFindingHandoff');
|
||||
expect(findingsPanelSource).toContain('buildPatrolAssistantApprovalBriefingInput');
|
||||
expect(findingsPanelSource).toContain('buildPatrolAssistantProposedFixBriefingInput');
|
||||
expect(findingsPanelSource).toContain('briefing,');
|
||||
expect(findingsPanelSource).toContain(
|
||||
'aiChatStore.openWithPrompt(handoff.prompt, handoff.context)',
|
||||
);
|
||||
expect(findingsPanelSource).toContain('investigationOutcome: finding.investigationOutcome');
|
||||
expect(findingsPanelSource).toContain('investigationStatus: finding.investigationStatus');
|
||||
expect(findingsPanelSource).toContain('remediationId: finding.remediationPlanId');
|
||||
expect(findingsPanelSource).toContain('resourceId: finding.resourceId');
|
||||
expect(findingsPanelSource).toContain('detectedAt: finding.detectedAt');
|
||||
expect(findingsPanelSource).toContain('lastSeenAt: finding.lastSeenAt');
|
||||
expect(findingsPanelSource).toContain('investigationRecord: finding.investigationRecord');
|
||||
expect(findingsPanelSource).toContain('pendingApproval: pendingApprovalBriefing');
|
||||
expect(findingsPanelSource).toContain('proposedFix,');
|
||||
expect(findingsPanelSource).toContain(
|
||||
'handoffActions: handoffActions.length > 0 ? handoffActions : undefined',
|
||||
);
|
||||
expect(findingsPanelSource).toContain('AIAPI.getInvestigation(finding.id)');
|
||||
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', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { RemediationPlan } from '@/api/ai';
|
||||
import {
|
||||
buildPatrolAssessmentAssistantHandoff,
|
||||
buildPatrolAssistantFindingBriefing,
|
||||
buildPatrolAssistantFindingHandoff,
|
||||
buildPatrolAssistantFindingHandoffActions,
|
||||
buildPatrolAssistantFindingPrompt,
|
||||
buildPatrolAssistantProposedFixBriefingInput,
|
||||
@@ -580,6 +581,76 @@ describe('patrolInvestigationContextModel', () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('builds finding-level Assistant handoff context, resources, and actions together', () => {
|
||||
const handoff = buildPatrolAssistantFindingHandoff({
|
||||
id: 'finding-1',
|
||||
title: 'Nginx down',
|
||||
subject: 'node-1',
|
||||
description: 'The service stopped responding.',
|
||||
severity: 'warning',
|
||||
findingStatus: 'active',
|
||||
investigationStatus: 'completed',
|
||||
investigationOutcome: 'fix_queued',
|
||||
loopState: 'awaiting_approval',
|
||||
timesRaised: 3,
|
||||
regressionCount: 1,
|
||||
lastRegressionAt: '2026-05-06T11:59:00Z',
|
||||
remediationId: 'remediation-1',
|
||||
resourceId: 'agent-1',
|
||||
resourceName: 'node-1',
|
||||
resourceType: 'agent',
|
||||
detectedAt: '2026-05-06T11:50:00Z',
|
||||
lastSeenAt: '2026-05-06T12:00:00Z',
|
||||
pendingApproval: {
|
||||
id: 'approval-1',
|
||||
status: 'pending',
|
||||
riskLevel: 'high',
|
||||
requestedAt: '2026-05-06T12:00:00Z',
|
||||
expiresAt: '2026-05-06T12:10:00Z',
|
||||
targetName: 'node-1',
|
||||
actionId: 'restart-nginx',
|
||||
actionApprovalPolicy: 'operator',
|
||||
actionPlanMessage: 'Restart nginx after validating load balancer drain.',
|
||||
actionPreflight: 'Would restart nginx on node-1.',
|
||||
actionDryRunSummary: 'One service restart would be attempted.',
|
||||
},
|
||||
proposedFix: buildPatrolAssistantProposedFixBriefingInput({
|
||||
description: 'Restart nginx',
|
||||
commands: ['systemctl restart nginx'],
|
||||
riskLevel: 'high',
|
||||
targetHost: 'node-1',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(handoff.prompt).toContain('Start by reviewing governed approval approval-1');
|
||||
expect(handoff.context).toMatchObject({
|
||||
targetType: 'agent',
|
||||
targetId: 'agent-1',
|
||||
findingId: 'finding-1',
|
||||
autonomousMode: false,
|
||||
handoffResources: [{ id: 'agent-1', name: 'node-1', type: 'agent' }],
|
||||
context: {
|
||||
source: 'pulse-patrol-finding',
|
||||
findingId: 'finding-1',
|
||||
resourceId: 'agent-1',
|
||||
resourceName: 'node-1',
|
||||
resourceType: 'agent',
|
||||
pendingApprovalId: 'approval-1',
|
||||
actionReferenceCount: 1,
|
||||
},
|
||||
});
|
||||
expect(handoff.context.handoffContext).toContain('[Patrol Finding Context]');
|
||||
expect(handoff.context.handoffContext).toContain('Approval: approval-1');
|
||||
expect(handoff.context.handoffContext).toContain(
|
||||
'Dry-Run Posture: One service restart would be attempted.',
|
||||
);
|
||||
expect(handoff.context.handoffContext).toContain(
|
||||
'Command Boundary: Command details stay in governed approval or remediation context',
|
||||
);
|
||||
expect(handoff.context.handoffActions).toHaveLength(1);
|
||||
expect(JSON.stringify(handoff)).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();
|
||||
|
||||
@@ -120,6 +120,30 @@ export interface PatrolAssistantFindingBriefingInput {
|
||||
investigationRecord?: InvestigationRecord | null;
|
||||
}
|
||||
|
||||
export interface PatrolAssistantFindingHandoffInput {
|
||||
id?: string | null;
|
||||
title: string;
|
||||
subject: string;
|
||||
description?: string | null;
|
||||
severity?: string | null;
|
||||
findingStatus?: string | null;
|
||||
investigationStatus?: string | null;
|
||||
investigationOutcome?: string | null;
|
||||
loopState?: string | null;
|
||||
timesRaised?: number | null;
|
||||
regressionCount?: number | null;
|
||||
lastRegressionAt?: string | null;
|
||||
remediationId?: string | null;
|
||||
resourceId?: string | null;
|
||||
resourceName?: string | null;
|
||||
resourceType?: string | null;
|
||||
detectedAt?: string | null;
|
||||
lastSeenAt?: string | null;
|
||||
pendingApproval?: PatrolAssistantApprovalBriefingInput | null;
|
||||
proposedFix?: PatrolAssistantProposedFixBriefingInput | null;
|
||||
investigationRecord?: InvestigationRecord | null;
|
||||
}
|
||||
|
||||
export interface PatrolAssistantFindingModeInput {
|
||||
investigationOutcome?: string | null;
|
||||
remediationId?: string | null;
|
||||
@@ -206,6 +230,11 @@ export interface PatrolAssessmentAssistantHandoff {
|
||||
context: Omit<AIChatContext, 'initialPrompt'>;
|
||||
}
|
||||
|
||||
export interface PatrolAssistantFindingHandoff {
|
||||
prompt: string;
|
||||
context: Omit<AIChatContext, 'initialPrompt'>;
|
||||
}
|
||||
|
||||
const MAX_ASSESSMENT_FINDINGS = 5;
|
||||
const MAX_ASSESSMENT_RECENT_CHANGES = 3;
|
||||
const MAX_ASSESSMENT_CORRELATIONS = 3;
|
||||
@@ -454,6 +483,68 @@ export function buildPatrolAssessmentAssistantHandoff(
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPatrolAssistantFindingHandoff(
|
||||
input: PatrolAssistantFindingHandoffInput,
|
||||
): PatrolAssistantFindingHandoff {
|
||||
const findingId = normalizeText(input.id) || normalizeText(input.investigationRecord?.finding_id);
|
||||
const resource = buildPatrolFindingHandoffResource(input);
|
||||
const handoffResources = resource ? [resource] : [];
|
||||
const handoffActions = buildPatrolAssistantFindingHandoffActions(input);
|
||||
const requiresApprovalMode = patrolAssistantFindingHandoffRequiresApprovalMode({
|
||||
investigationOutcome: input.investigationOutcome,
|
||||
remediationId: input.remediationId,
|
||||
pendingApproval: input.pendingApproval,
|
||||
investigationRecord: input.investigationRecord,
|
||||
});
|
||||
|
||||
return {
|
||||
prompt: buildPatrolAssistantFindingPrompt({
|
||||
title: input.title,
|
||||
subject: input.subject,
|
||||
description: normalizeText(input.description),
|
||||
investigationOutcome: input.investigationOutcome,
|
||||
remediationId: input.remediationId,
|
||||
pendingApproval: input.pendingApproval,
|
||||
proposedFix: input.proposedFix,
|
||||
investigationRecord: input.investigationRecord,
|
||||
}),
|
||||
context: {
|
||||
targetType: resource?.type,
|
||||
targetId: resource?.id,
|
||||
findingId: findingId || undefined,
|
||||
autonomousMode: requiresApprovalMode ? false : undefined,
|
||||
handoffContext: buildPatrolAssistantFindingModelContext(input),
|
||||
handoffResources: handoffResources.length > 0 ? handoffResources : undefined,
|
||||
handoffActions: handoffActions.length > 0 ? handoffActions : undefined,
|
||||
briefing: buildPatrolAssistantFindingBriefing({
|
||||
title: input.title,
|
||||
subject: input.subject,
|
||||
severity: input.severity,
|
||||
findingStatus: input.findingStatus,
|
||||
investigationOutcome: input.investigationOutcome,
|
||||
loopState: input.loopState,
|
||||
timesRaised: input.timesRaised,
|
||||
regressionCount: input.regressionCount,
|
||||
lastRegressionAt: input.lastRegressionAt,
|
||||
remediationId: input.remediationId,
|
||||
pendingApproval: input.pendingApproval,
|
||||
proposedFix: input.proposedFix,
|
||||
investigationRecord: input.investigationRecord,
|
||||
}),
|
||||
context: {
|
||||
source: 'pulse-patrol-finding',
|
||||
findingId: findingId || undefined,
|
||||
investigationRecordId: normalizeText(input.investigationRecord?.id) || undefined,
|
||||
resourceId: resource?.id,
|
||||
resourceName: resource?.name,
|
||||
resourceType: resource?.type,
|
||||
pendingApprovalId: normalizeApprovalBriefing(input.pendingApproval).id || undefined,
|
||||
actionReferenceCount: handoffActions.length,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPatrolAssistantFindingHandoffActions(
|
||||
finding: PatrolAssessmentAssistantFindingInput,
|
||||
): AIChatHandoffAction[] {
|
||||
@@ -762,6 +853,25 @@ function buildPatrolAssessmentHandoffResources(
|
||||
return Array.from(resources.values());
|
||||
}
|
||||
|
||||
function buildPatrolFindingHandoffResource(
|
||||
input: PatrolAssistantFindingHandoffInput,
|
||||
): AIChatHandoffResource | undefined {
|
||||
const subject = input.investigationRecord?.subject;
|
||||
const id = normalizeText(input.resourceId) || normalizeText(subject?.resource_id);
|
||||
if (!id) return undefined;
|
||||
|
||||
return {
|
||||
id,
|
||||
name:
|
||||
normalizeText(input.resourceName) ||
|
||||
normalizeText(subject?.resource_name) ||
|
||||
normalizeText(input.subject) ||
|
||||
undefined,
|
||||
type: normalizeText(input.resourceType) || normalizeText(subject?.resource_type) || undefined,
|
||||
node: normalizeText(subject?.node) || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function addAssessmentHandoffResource(
|
||||
resources: Map<string, AIChatHandoffResource>,
|
||||
resource: AIChatHandoffResource,
|
||||
@@ -1243,6 +1353,99 @@ function formatAssessmentFindingContextLine(
|
||||
return `Finding ${index}: ${parts.join('; ')}`;
|
||||
}
|
||||
|
||||
function buildPatrolAssistantFindingModelContext(
|
||||
input: PatrolAssistantFindingHandoffInput,
|
||||
): string {
|
||||
const title = normalizeText(input.title) || 'Patrol finding';
|
||||
const subject = normalizeText(input.subject) || 'affected resource';
|
||||
const record = buildPatrolInvestigationRecordPresentation(input.investigationRecord);
|
||||
const pendingApproval = normalizeApprovalBriefing(input.pendingApproval);
|
||||
const proposedFix = record.proposedFix || normalizeProposedFixBriefing(input.proposedFix);
|
||||
const resource = buildPatrolFindingHandoffResource(input);
|
||||
const findingId = normalizeText(input.id) || normalizeText(input.investigationRecord?.finding_id);
|
||||
const statusParts = [
|
||||
formatIdentifierLabel(input.severity),
|
||||
formatIdentifierLabel(input.findingStatus),
|
||||
formatIdentifierLabel(input.investigationStatus),
|
||||
formatIdentifierLabel(input.investigationOutcome || input.investigationRecord?.outcome),
|
||||
formatIdentifierLabel(input.loopState),
|
||||
].filter(isNonEmptyString);
|
||||
const raisedParts = [
|
||||
normalizeNonNegativeCount(input.timesRaised) > 1
|
||||
? `raised ${normalizeNonNegativeCount(input.timesRaised)} times`
|
||||
: undefined,
|
||||
normalizeNonNegativeCount(input.regressionCount) > 0
|
||||
? `regressed ${normalizeNonNegativeCount(input.regressionCount)} time${
|
||||
normalizeNonNegativeCount(input.regressionCount) === 1 ? '' : 's'
|
||||
}`
|
||||
: undefined,
|
||||
normalizeText(input.lastRegressionAt)
|
||||
? `last regression ${normalizeText(input.lastRegressionAt)}`
|
||||
: undefined,
|
||||
].filter(isNonEmptyString);
|
||||
const attentionReason = buildPatrolAssistantAttentionReason(input, record);
|
||||
const operatorDecision = buildPatrolAssistantOperatorDecision(input);
|
||||
const proposedFixFacts = proposedFix
|
||||
? formatBriefingStringList(
|
||||
[
|
||||
proposedFix.description,
|
||||
proposedFix.targetHost ? `target ${proposedFix.targetHost}` : undefined,
|
||||
proposedFix.riskLabel ? `${proposedFix.riskLabel.toLowerCase()} risk` : undefined,
|
||||
proposedFix.commandSummary,
|
||||
proposedFix.destructive ? 'destructive proposed fix' : undefined,
|
||||
proposedFix.rationale ? `rationale ${proposedFix.rationale}` : undefined,
|
||||
],
|
||||
6,
|
||||
'proposed-fix facts',
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return [
|
||||
'[Patrol Finding Context]',
|
||||
'Source: Pulse Patrol finding handoff',
|
||||
formatContextLine('Finding', title),
|
||||
formatContextLine('Finding ID', findingId),
|
||||
formatContextLine('Subject', subject),
|
||||
formatContextLine('Resource', resource ? formatAssessmentResourceLabel(resource) : undefined),
|
||||
formatContextLine('Status', statusParts.join(' · ')),
|
||||
formatContextLine('Detected At', input.detectedAt),
|
||||
formatContextLine('Last Seen At', input.lastSeenAt),
|
||||
formatContextLine('Recurrence', raisedParts.join('; ')),
|
||||
formatContextLine('Description', input.description),
|
||||
formatContextLine('Attention', attentionReason),
|
||||
formatContextLine('Investigation Record', input.investigationRecord?.id),
|
||||
formatContextLine('Investigation Status', record.statusLabel),
|
||||
formatContextLine('Investigation Outcome', record.outcomeLabel),
|
||||
formatContextLine('Investigation Confidence', record.confidenceLabel),
|
||||
formatContextLine('Conclusion', record.conclusion),
|
||||
formatContextLine('Recommended Action', record.recommendedAction),
|
||||
...record.evidenceSummaries.map((summary, index) =>
|
||||
formatContextLine(`Evidence ${index + 1}`, summary),
|
||||
),
|
||||
...record.verificationSummaries.map((summary, index) =>
|
||||
formatContextLine(`Verification ${index + 1}`, summary),
|
||||
),
|
||||
formatContextLine('Tools Used', record.toolsUsed.join(', ')),
|
||||
formatContextLine('Approval', pendingApproval.id),
|
||||
formatContextLine('Approval Status', pendingApproval.status),
|
||||
formatContextLine('Approval Risk', pendingApproval.riskLevel),
|
||||
formatContextLine('Approval Target', pendingApproval.targetName),
|
||||
formatContextLine('Approval Requested At', pendingApproval.requestedAt),
|
||||
formatContextLine('Approval Expires At', pendingApproval.expiresAt),
|
||||
formatContextLine('Approval Policy', pendingApproval.actionApprovalPolicy),
|
||||
formatContextLine('Approval Plan Expires At', pendingApproval.actionPlanExpiresAt),
|
||||
formatContextLine('Action Plan Summary', pendingApproval.actionPlanMessage),
|
||||
formatContextLine('Action Preflight', pendingApproval.actionPreflight),
|
||||
formatContextLine('Dry-Run Posture', pendingApproval.actionDryRunSummary),
|
||||
formatContextLine('Proposed Fix', proposedFixFacts),
|
||||
formatContextLine('Operator Decision', operatorDecision),
|
||||
'Command Boundary: Command details stay in governed approval or remediation context; this model-only handoff may include command counts but not raw command text.',
|
||||
'Operator Boundary: This Patrol finding handoff is model-only context for explanation and review. Diagnostics, remediation, and command execution require explicit governed approval.',
|
||||
]
|
||||
.filter(isNonEmptyString)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function formatAssessmentPendingApprovalContextParts(
|
||||
approval: Required<PatrolAssistantApprovalBriefingInput>,
|
||||
): string[] {
|
||||
|
||||
Reference in New Issue
Block a user