mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Persist Patrol approval requester identity
- store requester provenance on approval records - carry requester metadata through approval APIs and Assistant handoffs - document the safe Patrol approval provenance boundary
This commit is contained in:
parent
f65505ba04
commit
ea3e1b216a
16 changed files with 156 additions and 16 deletions
|
|
@ -289,8 +289,9 @@ profile and assignment columns, but embedded table framing must route through
|
|||
agent command grant or lifecycle authorization primitive.
|
||||
Patrol queued-fix approvals may now seed the shared action-audit store with
|
||||
planned and pending lifecycle evidence, but lifecycle surfaces must treat
|
||||
those records as action-governance context only; they do not create install,
|
||||
enrollment, assignment, or fleet-command authority.
|
||||
those records and their persisted requester identity as action-governance
|
||||
context only; they do not create install, enrollment, assignment, or
|
||||
fleet-command authority.
|
||||
Lifecycle flows must not reintroduce anonymous bootstrap identity,
|
||||
tenant-local commercial-owner surrogates, or fake activation records when
|
||||
they traverse those shared handlers. They also must not infer tenant
|
||||
|
|
|
|||
|
|
@ -660,7 +660,10 @@ attach a governed action plan, seed the shared action-audit store as planned
|
|||
and pending with `pulse_patrol` as the requester/actor, and leave later
|
||||
execution or approval decisions to the governed action/approval paths instead
|
||||
of creating Patrol-only execution context or collapsing Patrol proposals into
|
||||
generic Assistant-origin actions.
|
||||
generic Assistant-origin actions. The approval record itself must also persist
|
||||
and expose that requester identity so `/api/ai/approvals` and Assistant
|
||||
handoffs preserve Patrol provenance before later action-audit hydration refreshes
|
||||
the current action state.
|
||||
The same ownership includes the Pulse query tool schema under
|
||||
`internal/ai/tools/`: topology-query input names must stay canonical inside
|
||||
the AI runtime itself, so new tool arguments such as `max_proxmox_nodes`
|
||||
|
|
|
|||
|
|
@ -439,7 +439,10 @@ the canonical monitored-system blocked payload.
|
|||
hydration. Queueing a Patrol investigation fix must stamp the action request
|
||||
and initial lifecycle events as `pulse_patrol`; it must not execute the fix,
|
||||
create a Patrol-local audit record that bypasses `/api/actions`, or present
|
||||
Patrol-origin proposals as generic Assistant-origin actions.
|
||||
Patrol-origin proposals as generic Assistant-origin actions. `/api/ai/approvals`
|
||||
must expose the persisted `requestedBy` identity so frontend Assistant
|
||||
handoffs can carry Patrol provenance before the chat runtime refreshes the
|
||||
current action state from action audit.
|
||||
Action execution is API-owned as the next explicit contract:
|
||||
`POST /api/actions/{id}/execute` may only start execution for an approved
|
||||
action or an approval-free executable plan, must atomically persist the
|
||||
|
|
@ -3157,8 +3160,9 @@ server-refreshed approval, resource, or action authority. Frontend Patrol
|
|||
handoff helpers may consume current pending
|
||||
approval list payloads only as safe metadata for that visible briefing and any
|
||||
structured `handoff_actions`: approval ID, status, risk, request/expiry
|
||||
timestamps, target label, action ID, approval policy, plan expiry, and dry-run
|
||||
summary are allowed. Assessment-level visible briefings may reuse that same
|
||||
timestamps, target label, requester identity, action ID, approval policy,
|
||||
plan expiry, and dry-run summary are allowed. Assessment-level visible
|
||||
briefings may reuse that same
|
||||
safe metadata for action labels, safety notes, and approval-aware suggested
|
||||
prompts, while approval command text remains inside the governed
|
||||
approval/remediation surface.
|
||||
|
|
|
|||
|
|
@ -877,8 +877,9 @@ frontend primitive boundary.
|
|||
assessment handoff may add live pending
|
||||
approval posture only as safe structured metadata: approval ID, pending
|
||||
status, risk, target, requested/expiry timestamps, action plan identity,
|
||||
approval policy, plan expiry, dry-run posture, and command count. Those
|
||||
entries may be passed through shared chat transport as `handoff_actions` for
|
||||
requester identity, approval policy, plan expiry, dry-run posture, and
|
||||
command count. Those entries may be passed through shared chat transport as
|
||||
`handoff_actions` for
|
||||
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
|
||||
|
|
|
|||
|
|
@ -854,7 +854,10 @@ Assistant handoffs and resource timelines can hydrate the same canonical
|
|||
action record before any operator decision or execution occurs. Those queued
|
||||
fix records must carry `pulse_patrol` as the requester and lifecycle actor so
|
||||
resource timelines preserve the product source of the proposal instead of
|
||||
flattening proactive Patrol work into generic Assistant chat activity.
|
||||
flattening proactive Patrol work into generic Assistant chat activity. Pending
|
||||
approval payloads and Patrol Assistant handoff actions must carry the same
|
||||
requester identity as safe metadata, without copying the approval command
|
||||
payload into Assistant.
|
||||
That same store now owns the Patrol dashboard load bundle as well, so the
|
||||
page refresh path stays aligned on a single orchestrated AI bundle instead of
|
||||
repeating the individual summary, findings, approval, and correlation fetches
|
||||
|
|
|
|||
|
|
@ -1124,9 +1124,10 @@ freshness, restore authorization, storage remediation permission, or recovery
|
|||
transport state.
|
||||
Patrol queued-fix approvals that seed shared action-audit records follow the
|
||||
same rule: storage and recovery may display the resulting action history as
|
||||
incident-adjacent context, but they must not treat the pending action state,
|
||||
approval policy, or preflight posture as recovery proof or storage-local
|
||||
execution permission.
|
||||
incident-adjacent context, including the requester identity that distinguishes
|
||||
Patrol-origin proposals from generic Assistant work, but they must not treat the
|
||||
pending action state, approval policy, requester, or preflight posture as
|
||||
recovery proof or storage-local execution permission.
|
||||
That same storage ownership also includes the shared storage-source presentation
|
||||
contract in `frontend-modern/src/utils/storageSources.ts`: storage pages and
|
||||
cross-surface storage links must reuse one canonical ordering, label, tone, and
|
||||
|
|
|
|||
|
|
@ -186,6 +186,24 @@ describe('AIAPI', () => {
|
|||
await expect(AIAPI.getPendingApprovals()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('preserves approval requester identity from pending approvals', async () => {
|
||||
apiFetchJSONMock.mockResolvedValueOnce({
|
||||
approvals: [
|
||||
{
|
||||
id: 'approval-1',
|
||||
requestedBy: 'pulse_patrol',
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
|
||||
await expect(AIAPI.getPendingApprovals()).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'approval-1',
|
||||
requestedBy: 'pulse_patrol',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('normalizes remediation plans from optional or legacy collection payloads', async () => {
|
||||
apiFetchJSONMock.mockResolvedValueOnce({ plans: [{ id: 'plan-1' }] } as any);
|
||||
await expect(AIAPI.getRemediationPlans()).resolves.toEqual({
|
||||
|
|
|
|||
|
|
@ -584,6 +584,7 @@ export interface ApprovalRequest {
|
|||
targetId: string;
|
||||
targetName: string;
|
||||
context: string;
|
||||
requestedBy?: string;
|
||||
riskLevel: RiskLevel;
|
||||
status: ApprovalStatus;
|
||||
requestedAt: string;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { PatrolRunRecord } from '@/api/patrol';
|
|||
|
||||
import {
|
||||
buildPatrolAssessmentAssistantHandoff,
|
||||
buildPatrolAssistantApprovalBriefingInput,
|
||||
buildPatrolAssistantFindingBriefing,
|
||||
buildPatrolAssistantFindingHandoff,
|
||||
buildPatrolAssistantFindingHandoffActions,
|
||||
|
|
@ -375,6 +376,7 @@ describe('patrolInvestigationContextModel', () => {
|
|||
actionPlanMessage: 'Restart after the backup window clears.',
|
||||
actionPreflight: 'Restart workload service',
|
||||
actionDryRunSummary: 'No provider-supported dry run is available for this action.',
|
||||
actionRequestedBy: 'pulse_patrol',
|
||||
},
|
||||
proposedFix: {
|
||||
description: 'Restart the workload service',
|
||||
|
|
@ -396,6 +398,7 @@ describe('patrolInvestigationContextModel', () => {
|
|||
expect(handoff.context.handoffContext).toContain('high risk');
|
||||
expect(handoff.context.handoffContext).toContain('approval target web-server');
|
||||
expect(handoff.context.handoffContext).toContain('expires 2026-05-06T12:10:00Z');
|
||||
expect(handoff.context.handoffContext).toContain('requested by pulse_patrol');
|
||||
expect(handoff.context.handoffContext).toContain('1 command recorded for approval context');
|
||||
expect(handoff.context.handoffContext).toContain('destructive proposed fix');
|
||||
expect(handoff.context.handoffActions).toHaveLength(1);
|
||||
|
|
@ -406,6 +409,7 @@ describe('patrolInvestigationContextModel', () => {
|
|||
approvalRequestedAt: '2026-05-06T12:00:00Z',
|
||||
approvalExpiresAt: '2026-05-06T12:10:00Z',
|
||||
actionId: 'action-1',
|
||||
actionRequestedBy: 'pulse_patrol',
|
||||
actionApprovalPolicy: 'admin',
|
||||
actionRequiresApproval: true,
|
||||
actionPlanExpiresAt: '2026-05-06T12:10:00Z',
|
||||
|
|
@ -641,6 +645,35 @@ describe('patrolInvestigationContextModel', () => {
|
|||
expect(JSON.stringify(briefing)).not.toContain('systemctl restart workload.service');
|
||||
});
|
||||
|
||||
it('carries approval requester identity into safe Patrol handoff metadata', () => {
|
||||
const briefing = buildPatrolAssistantApprovalBriefingInput({
|
||||
id: 'approval-1',
|
||||
toolId: 'investigation_fix',
|
||||
command: 'systemctl restart nginx',
|
||||
targetType: 'investigation',
|
||||
targetId: 'finding-1',
|
||||
targetName: 'node-1',
|
||||
context: 'Restart nginx after Patrol investigation',
|
||||
requestedBy: 'pulse_patrol',
|
||||
riskLevel: 'high',
|
||||
status: 'pending',
|
||||
requestedAt: '2026-05-06T12:00:00Z',
|
||||
expiresAt: '2026-05-06T12:10:00Z',
|
||||
plan: {
|
||||
actionId: 'action-1',
|
||||
approvalPolicy: 'admin',
|
||||
},
|
||||
});
|
||||
|
||||
expect(briefing).toMatchObject({
|
||||
id: 'approval-1',
|
||||
actionId: 'action-1',
|
||||
actionApprovalPolicy: 'admin',
|
||||
actionRequestedBy: 'pulse_patrol',
|
||||
});
|
||||
expect(JSON.stringify(briefing)).not.toContain('systemctl restart nginx');
|
||||
});
|
||||
|
||||
it('frames Assistant handoff around the structured Patrol record when one exists', () => {
|
||||
expect(
|
||||
buildPatrolAssistantFindingPrompt({
|
||||
|
|
@ -737,6 +770,7 @@ describe('patrolInvestigationContextModel', () => {
|
|||
actionPlanMessage: 'Restart nginx after validating load balancer drain.',
|
||||
actionPreflight: 'Would restart nginx on node-1.',
|
||||
actionDryRunSummary: 'One service restart would be attempted.',
|
||||
actionRequestedBy: 'pulse_patrol',
|
||||
},
|
||||
proposedFix: buildPatrolAssistantProposedFixBriefingInput({
|
||||
description: 'Restart nginx',
|
||||
|
|
@ -756,6 +790,7 @@ describe('patrolInvestigationContextModel', () => {
|
|||
approvalRequestedAt: '2026-05-06T12:00:00Z',
|
||||
approvalExpiresAt: '2026-05-06T12:10:00Z',
|
||||
actionId: 'restart-nginx',
|
||||
actionRequestedBy: 'pulse_patrol',
|
||||
actionApprovalPolicy: 'operator',
|
||||
actionRequiresApproval: true,
|
||||
actionPlanExpiresAt: '2026-05-06T12:10:00Z',
|
||||
|
|
@ -818,6 +853,7 @@ describe('patrolInvestigationContextModel', () => {
|
|||
actionPlanMessage: 'Restart nginx after validating load balancer drain.',
|
||||
actionPreflight: 'Would restart nginx on node-1.',
|
||||
actionDryRunSummary: 'One service restart would be attempted.',
|
||||
actionRequestedBy: 'pulse_patrol',
|
||||
},
|
||||
proposedFix: buildPatrolAssistantProposedFixBriefingInput({
|
||||
description: 'Restart nginx',
|
||||
|
|
@ -846,6 +882,7 @@ describe('patrolInvestigationContextModel', () => {
|
|||
});
|
||||
expect(handoff.context.handoffContext).toContain('[Patrol Finding Context]');
|
||||
expect(handoff.context.handoffContext).toContain('Approval: approval-1');
|
||||
expect(handoff.context.handoffContext).toContain('Action Requested By: pulse_patrol');
|
||||
expect(handoff.context.handoffContext).toContain(
|
||||
'Dry-Run Posture: One service restart would be attempted.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export interface PatrolAssistantApprovalBriefingInput {
|
|||
actionPlanMessage?: string | null;
|
||||
actionPreflight?: string | null;
|
||||
actionDryRunSummary?: string | null;
|
||||
actionRequestedBy?: string | null;
|
||||
}
|
||||
|
||||
export interface PatrolAssistantProposedFixBriefingInput {
|
||||
|
|
@ -414,6 +415,7 @@ export function buildPatrolAssistantApprovalBriefingInput(
|
|||
actionPlanMessage: normalizeText(approval.plan?.message || approval.plan?.summary),
|
||||
actionPreflight: normalizeText(approval.preflight?.intendedChange),
|
||||
actionDryRunSummary: normalizeText(approval.preflight?.dryRunSummary),
|
||||
actionRequestedBy: normalizeText(approval.requestedBy),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1448,6 +1450,7 @@ function buildPatrolFindingHandoffAction(
|
|||
approvalRequestedAt: pendingApproval.requestedAt || undefined,
|
||||
approvalExpiresAt: pendingApproval.expiresAt || undefined,
|
||||
actionId: pendingApproval.actionId || undefined,
|
||||
actionRequestedBy: pendingApproval.actionRequestedBy || undefined,
|
||||
actionApprovalPolicy: pendingApproval.actionApprovalPolicy || undefined,
|
||||
actionRequiresApproval: Boolean(approvalId),
|
||||
actionPlanExpiresAt: pendingApproval.actionPlanExpiresAt || undefined,
|
||||
|
|
@ -1937,6 +1940,7 @@ function buildPatrolAssistantFindingModelContext(
|
|||
formatContextLine('Approval Requested At', pendingApproval.requestedAt),
|
||||
formatContextLine('Approval Expires At', pendingApproval.expiresAt),
|
||||
formatContextLine('Approval Policy', pendingApproval.actionApprovalPolicy),
|
||||
formatContextLine('Action Requested By', pendingApproval.actionRequestedBy),
|
||||
formatContextLine('Approval Plan Expires At', pendingApproval.actionPlanExpiresAt),
|
||||
formatContextLine('Action Plan Summary', pendingApproval.actionPlanMessage),
|
||||
formatContextLine('Action Preflight', pendingApproval.actionPreflight),
|
||||
|
|
@ -1978,6 +1982,9 @@ function formatAssessmentPendingApprovalContextParts(
|
|||
if (approval.requestedAt) {
|
||||
parts.push(`requested ${approval.requestedAt}`);
|
||||
}
|
||||
if (approval.actionRequestedBy) {
|
||||
parts.push(`requested by ${approval.actionRequestedBy}`);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
|
|
@ -2371,6 +2378,9 @@ function buildPatrolAssistantOperatorDecision(
|
|||
if (pendingApproval.requestedAt) {
|
||||
parts.push(`requested ${pendingApproval.requestedAt}`);
|
||||
}
|
||||
if (pendingApproval.actionRequestedBy) {
|
||||
parts.push(`requested by ${pendingApproval.actionRequestedBy}`);
|
||||
}
|
||||
if (record.proposed_fix) {
|
||||
const fixId = normalizeText(record.proposed_fix.id);
|
||||
if (fixId) {
|
||||
|
|
@ -2435,6 +2445,9 @@ function buildPatrolAssistantOperatorDecision(
|
|||
if (pendingApproval.requestedAt) {
|
||||
parts.push(`Requested: ${pendingApproval.requestedAt}.`);
|
||||
}
|
||||
if (pendingApproval.actionRequestedBy) {
|
||||
parts.push(`Requested by: ${pendingApproval.actionRequestedBy}.`);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
|
|
@ -2559,6 +2572,7 @@ function normalizeApprovalBriefing(
|
|||
actionPlanMessage: normalizeText(approval?.actionPlanMessage),
|
||||
actionPreflight: normalizeText(approval?.actionPreflight),
|
||||
actionDryRunSummary: normalizeText(approval?.actionDryRunSummary),
|
||||
actionRequestedBy: normalizeText(approval?.actionRequestedBy),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ type ApprovalRequest struct {
|
|||
TargetType string `json:"targetType"` // agent, container, vm, node
|
||||
TargetID string `json:"targetId"`
|
||||
TargetName string `json:"targetName"`
|
||||
Context string `json:"context"` // Why AI wants to run this
|
||||
Context string `json:"context"` // Why AI wants to run this
|
||||
RequestedBy string `json:"requestedBy,omitempty"`
|
||||
RiskLevel RiskLevel `json:"riskLevel"` // low, medium, high
|
||||
Status ApprovalStatus `json:"status"`
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
|
|
@ -131,6 +132,9 @@ func RequesterForRequest(req *ApprovalRequest) string {
|
|||
if req == nil {
|
||||
return RequesterPulseAssistant
|
||||
}
|
||||
if requester := strings.TrimSpace(req.RequestedBy); requester != "" {
|
||||
return requester
|
||||
}
|
||||
if strings.TrimSpace(req.ToolID) == "investigation_fix" ||
|
||||
strings.TrimSpace(req.TargetType) == "investigation" {
|
||||
return RequesterPulsePatrol
|
||||
|
|
@ -258,6 +262,7 @@ func (s *Store) CreateApproval(req *ApprovalRequest) error {
|
|||
if req.ExpiresAt.IsZero() {
|
||||
req.ExpiresAt = req.RequestedAt.Add(s.defaultTimeout)
|
||||
}
|
||||
req.RequestedBy = RequesterForRequest(req)
|
||||
if req.Plan != nil {
|
||||
if strings.TrimSpace(req.Plan.ActionID) == "" {
|
||||
req.Plan.ActionID = uuid.New().String()
|
||||
|
|
|
|||
|
|
@ -287,6 +287,9 @@ func TestCreateApproval_UsesPatrolRequesterForInvestigationFixPreflight(t *testi
|
|||
if got := RequesterForRequest(req); got != RequesterPulsePatrol {
|
||||
t.Fatalf("RequesterForRequest = %q, want %q", got, RequesterPulsePatrol)
|
||||
}
|
||||
if req.RequestedBy != RequesterPulsePatrol {
|
||||
t.Fatalf("requestedBy = %q, want %q", req.RequestedBy, RequesterPulsePatrol)
|
||||
}
|
||||
if req.Plan.Preflight == nil {
|
||||
t.Fatal("expected normalized action preflight")
|
||||
}
|
||||
|
|
@ -298,6 +301,47 @@ func TestCreateApproval_UsesPatrolRequesterForInvestigationFixPreflight(t *testi
|
|||
}
|
||||
}
|
||||
|
||||
func TestCreateApproval_PreservesExplicitRequester(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
store, _ := NewStore(StoreConfig{
|
||||
DataDir: tmpDir,
|
||||
DefaultTimeout: 1 * time.Minute,
|
||||
DisablePersistence: true,
|
||||
})
|
||||
|
||||
req := &ApprovalRequest{
|
||||
ID: "approval-explicit-requester",
|
||||
ToolID: "investigation_fix",
|
||||
Command: "systemctl restart nginx",
|
||||
TargetType: "investigation",
|
||||
TargetID: "finding-123",
|
||||
Context: "Restart nginx after Patrol investigation",
|
||||
RequestedBy: "pulse_custom",
|
||||
Plan: &unifiedresources.ActionPlan{
|
||||
ActionID: "action-explicit-requester",
|
||||
Allowed: true,
|
||||
RequiresApproval: true,
|
||||
ApprovalPolicy: unifiedresources.ApprovalAdmin,
|
||||
PlannedAt: time.Now().UTC(),
|
||||
},
|
||||
}
|
||||
|
||||
if err := store.CreateApproval(req); err != nil {
|
||||
t.Fatalf("CreateApproval() error = %v", err)
|
||||
}
|
||||
if req.RequestedBy != "pulse_custom" {
|
||||
t.Fatalf("requestedBy = %q, want pulse_custom", req.RequestedBy)
|
||||
}
|
||||
if got := RequesterForRequest(req); got != "pulse_custom" {
|
||||
t.Fatalf("RequesterForRequest = %q, want pulse_custom", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateApproval_RejectsUnsupportedHostTargetType(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "approval-test-*")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2636,9 +2636,10 @@ func TestAISettingsHandler_Approvals(t *testing.T) {
|
|||
|
||||
appID := "app-123"
|
||||
_ = approvalStore.CreateApproval(&approval.ApprovalRequest{
|
||||
ID: appID,
|
||||
Command: "ls -la",
|
||||
Status: approval.StatusPending,
|
||||
ID: appID,
|
||||
Command: "ls -la",
|
||||
RequestedBy: approval.RequesterPulsePatrol,
|
||||
Status: approval.StatusPending,
|
||||
})
|
||||
|
||||
t.Run("HandleGetApproval", func(t *testing.T) {
|
||||
|
|
@ -2651,6 +2652,7 @@ func TestAISettingsHandler_Approvals(t *testing.T) {
|
|||
err := json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, appID, resp.ID)
|
||||
assert.Equal(t, approval.RequesterPulsePatrol, resp.RequestedBy)
|
||||
})
|
||||
|
||||
t.Run("HandleListApprovals", func(t *testing.T) {
|
||||
|
|
@ -2667,6 +2669,7 @@ func TestAISettingsHandler_Approvals(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.Len(t, resp.Approvals, 1)
|
||||
assert.Equal(t, appID, resp.Approvals[0].ID)
|
||||
assert.Equal(t, approval.RequesterPulsePatrol, resp.Approvals[0].RequestedBy)
|
||||
assert.Equal(t, 1, resp.Stats["pending"])
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -2951,6 +2951,7 @@ func TestContract_ApprovalJSONSnapshot(t *testing.T) {
|
|||
TargetID: "host-1",
|
||||
TargetName: "alpha",
|
||||
Context: "Cleanup temporary cache",
|
||||
RequestedBy: approval.RequesterPulsePatrol,
|
||||
RiskLevel: approval.RiskHigh,
|
||||
Status: approval.StatusApproved,
|
||||
RequestedAt: now,
|
||||
|
|
@ -2976,6 +2977,7 @@ func TestContract_ApprovalJSONSnapshot(t *testing.T) {
|
|||
"targetId":"host-1",
|
||||
"targetName":"alpha",
|
||||
"context":"Cleanup temporary cache",
|
||||
"requestedBy":"pulse_patrol",
|
||||
"riskLevel":"high",
|
||||
"status":"approved",
|
||||
"requestedAt":"2026-02-08T13:14:15Z",
|
||||
|
|
|
|||
|
|
@ -466,6 +466,7 @@ func (a *approvalStoreAdapter) CreateApproval(info *aicontracts.ApprovalInfo) er
|
|||
TargetID: info.TargetID,
|
||||
TargetName: info.TargetName,
|
||||
Context: info.Context,
|
||||
RequestedBy: info.RequestedBy,
|
||||
RiskLevel: approval.RiskLevel(info.RiskLevel),
|
||||
Plan: approvalPlanInfoToRequest(info.Plan),
|
||||
ContextConfidence: contextConfidenceInfoToRequest(info.ContextConfidence),
|
||||
|
|
@ -519,6 +520,7 @@ func approvalRequestToInfo(req *approval.ApprovalRequest) *aicontracts.ApprovalI
|
|||
TargetID: req.TargetID,
|
||||
TargetName: req.TargetName,
|
||||
Context: req.Context,
|
||||
RequestedBy: req.RequestedBy,
|
||||
RiskLevel: string(req.RiskLevel),
|
||||
Status: string(req.Status),
|
||||
RequestedAt: req.RequestedAt,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type ApprovalInfo struct {
|
|||
TargetID string `json:"targetId"`
|
||||
TargetName string `json:"targetName"`
|
||||
Context string `json:"context"`
|
||||
RequestedBy string `json:"requestedBy,omitempty"`
|
||||
RiskLevel string `json:"riskLevel"`
|
||||
Status string `json:"status"`
|
||||
RequestedAt time.Time `json:"requestedAt"`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue