mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
331 lines
9.6 KiB
Go
331 lines
9.6 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/approval"
|
|
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
func (e *PulseToolExecutor) executeCommandWithAudit(
|
|
ctx context.Context,
|
|
capabilityName string,
|
|
resourceID string,
|
|
approvalID string,
|
|
requiresApproval bool,
|
|
agentID string,
|
|
payload agentexec.ExecuteCommandPayload,
|
|
requestedBy string,
|
|
reason string,
|
|
) (*agentexec.CommandResultPayload, error) {
|
|
if e.agentServer == nil {
|
|
return nil, fmt.Errorf("no agent server available")
|
|
}
|
|
|
|
actionID := uuid.NewString()
|
|
requestCorrelationID := strings.TrimSpace(approvalID)
|
|
if requestCorrelationID == "" {
|
|
requestCorrelationID = actionID
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
plan := unifiedresources.ActionPlan{
|
|
ActionID: actionID,
|
|
RequestID: requestCorrelationID,
|
|
Allowed: true,
|
|
RequiresApproval: requiresApproval,
|
|
ApprovalPolicy: func() unifiedresources.ActionApprovalLevel {
|
|
if requiresApproval {
|
|
return unifiedresources.ApprovalAdmin
|
|
}
|
|
return unifiedresources.ApprovalNone
|
|
}(),
|
|
PlannedAt: now,
|
|
ExpiresAt: now.Add(5 * time.Minute),
|
|
ResourceVersion: "",
|
|
PolicyVersion: "",
|
|
PlanHash: actionPlanHash(actionID, requestCorrelationID, capabilityName, resourceID, payload, reason),
|
|
Message: reason,
|
|
}
|
|
|
|
record := unifiedresources.ActionAuditRecord{
|
|
ID: actionID,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
State: unifiedresources.ActionStateExecuting,
|
|
Request: unifiedresources.ActionRequest{
|
|
RequestID: requestCorrelationID,
|
|
ResourceID: strings.TrimSpace(resourceID),
|
|
CapabilityName: capabilityName,
|
|
Params: map[string]any{
|
|
"command": payload.Command,
|
|
"targetType": payload.TargetType,
|
|
"targetId": payload.TargetID,
|
|
"agentId": agentID,
|
|
"approvalId": approvalID,
|
|
"requestedBy": requestedBy,
|
|
},
|
|
Reason: reason,
|
|
RequestedBy: requestedBy,
|
|
},
|
|
Plan: plan,
|
|
}
|
|
|
|
if approvalID != "" {
|
|
if store := approval.GetStore(); store != nil {
|
|
if req, ok := store.GetApproval(approvalID); ok && req != nil {
|
|
record.Approvals = append(record.Approvals, unifiedresources.ActionApprovalRecord{
|
|
Actor: strings.TrimSpace(req.DecidedBy),
|
|
Method: unifiedresources.MethodAPI,
|
|
Timestamp: approvalTimestamp(req),
|
|
Outcome: unifiedresources.OutcomeApproved,
|
|
Reason: strings.TrimSpace(req.Context),
|
|
})
|
|
if req.Status == approval.StatusDenied {
|
|
record.Approvals[len(record.Approvals)-1].Outcome = unifiedresources.OutcomeRejected
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
e.recordActionLifecycle(actionID, unifiedresources.ActionStatePlanned, requestedBy, reason)
|
|
e.recordActionLifecycle(actionID, unifiedresources.ActionStateExecuting, requestedBy, fmt.Sprintf("dispatching command to agent %s", agentID))
|
|
e.recordActionAudit(record)
|
|
|
|
result, err := e.agentServer.ExecuteCommand(ctx, agentID, payload)
|
|
finalState := unifiedresources.ActionStateCompleted
|
|
finalMessage := "command completed"
|
|
executionResult := &unifiedresources.ExecutionResult{Success: true}
|
|
|
|
if err != nil {
|
|
finalState = unifiedresources.ActionStateFailed
|
|
finalMessage = err.Error()
|
|
executionResult.Success = false
|
|
executionResult.ErrorMessage = err.Error()
|
|
} else {
|
|
output := result.Stdout
|
|
if strings.TrimSpace(result.Stderr) != "" {
|
|
if output != "" {
|
|
output += "\n"
|
|
}
|
|
output += result.Stderr
|
|
}
|
|
executionResult.Output = output
|
|
if result.ExitCode != 0 {
|
|
finalState = unifiedresources.ActionStateFailed
|
|
finalMessage = fmt.Sprintf("exit code %d", result.ExitCode)
|
|
executionResult.Success = false
|
|
executionResult.ErrorMessage = finalMessage
|
|
}
|
|
}
|
|
|
|
record.State = finalState
|
|
record.UpdatedAt = time.Now().UTC()
|
|
record.Result = executionResult
|
|
e.recordActionAudit(record)
|
|
e.recordActionLifecycle(actionID, finalState, requestedBy, finalMessage)
|
|
|
|
return result, err
|
|
}
|
|
|
|
func (e *PulseToolExecutor) executeNativeActionWithAudit(
|
|
ctx context.Context,
|
|
capabilityName string,
|
|
resourceID string,
|
|
approvalID string,
|
|
requiresApproval bool,
|
|
params map[string]any,
|
|
requestedBy string,
|
|
reason string,
|
|
execute func(context.Context) (*unifiedresources.ExecutionResult, error),
|
|
) (*unifiedresources.ExecutionResult, error) {
|
|
actionID := uuid.NewString()
|
|
requestCorrelationID := strings.TrimSpace(approvalID)
|
|
if requestCorrelationID == "" {
|
|
requestCorrelationID = actionID
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
plan := unifiedresources.ActionPlan{
|
|
ActionID: actionID,
|
|
RequestID: requestCorrelationID,
|
|
Allowed: true,
|
|
RequiresApproval: requiresApproval,
|
|
ApprovalPolicy: func() unifiedresources.ActionApprovalLevel {
|
|
if requiresApproval {
|
|
return unifiedresources.ApprovalAdmin
|
|
}
|
|
return unifiedresources.ApprovalNone
|
|
}(),
|
|
PlannedAt: now,
|
|
ExpiresAt: now.Add(5 * time.Minute),
|
|
ResourceVersion: "",
|
|
PolicyVersion: "",
|
|
PlanHash: actionPlanHashForParams(actionID, requestCorrelationID, capabilityName, resourceID, params, reason),
|
|
Message: reason,
|
|
}
|
|
|
|
record := unifiedresources.ActionAuditRecord{
|
|
ID: actionID,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
State: unifiedresources.ActionStateExecuting,
|
|
Request: unifiedresources.ActionRequest{
|
|
RequestID: requestCorrelationID,
|
|
ResourceID: strings.TrimSpace(resourceID),
|
|
CapabilityName: capabilityName,
|
|
Params: cloneActionParams(params),
|
|
Reason: reason,
|
|
RequestedBy: requestedBy,
|
|
},
|
|
Plan: plan,
|
|
}
|
|
record.Approvals = approvalRecordsForID(approvalID)
|
|
|
|
e.recordActionLifecycle(actionID, unifiedresources.ActionStatePlanned, requestedBy, reason)
|
|
e.recordActionLifecycle(actionID, unifiedresources.ActionStateExecuting, requestedBy, "dispatching native resource action")
|
|
e.recordActionAudit(record)
|
|
|
|
result, err := execute(ctx)
|
|
finalState := unifiedresources.ActionStateCompleted
|
|
finalMessage := "native resource action completed"
|
|
executionResult := &unifiedresources.ExecutionResult{Success: true}
|
|
|
|
if err != nil {
|
|
finalState = unifiedresources.ActionStateFailed
|
|
finalMessage = err.Error()
|
|
executionResult.Success = false
|
|
executionResult.ErrorMessage = err.Error()
|
|
} else if result != nil {
|
|
executionResult = result
|
|
if !result.Success {
|
|
finalState = unifiedresources.ActionStateFailed
|
|
finalMessage = strings.TrimSpace(result.ErrorMessage)
|
|
if finalMessage == "" {
|
|
finalMessage = "native resource action failed"
|
|
}
|
|
}
|
|
}
|
|
|
|
record.State = finalState
|
|
record.UpdatedAt = time.Now().UTC()
|
|
record.Result = executionResult
|
|
e.recordActionAudit(record)
|
|
e.recordActionLifecycle(actionID, finalState, requestedBy, finalMessage)
|
|
|
|
return executionResult, err
|
|
}
|
|
|
|
func (e *PulseToolExecutor) recordActionAudit(record unifiedresources.ActionAuditRecord) {
|
|
if e == nil || e.actionAuditStore == nil {
|
|
return
|
|
}
|
|
if err := e.actionAuditStore.RecordActionAudit(record); err != nil {
|
|
log.Warn().
|
|
Err(err).
|
|
Str("action_id", record.ID).
|
|
Str("resource_id", record.Request.ResourceID).
|
|
Msg("failed to persist action audit")
|
|
}
|
|
}
|
|
|
|
func (e *PulseToolExecutor) recordActionLifecycle(actionID string, state unifiedresources.ActionState, actor, message string) {
|
|
if e == nil || e.actionAuditStore == nil || strings.TrimSpace(actionID) == "" {
|
|
return
|
|
}
|
|
event := unifiedresources.ActionLifecycleEvent{
|
|
ActionID: actionID,
|
|
Timestamp: time.Now().UTC(),
|
|
State: state,
|
|
Actor: actor,
|
|
Message: message,
|
|
}
|
|
if err := e.actionAuditStore.RecordActionLifecycleEvent(event); err != nil {
|
|
log.Warn().
|
|
Err(err).
|
|
Str("action_id", actionID).
|
|
Str("state", string(state)).
|
|
Msg("failed to persist action lifecycle event")
|
|
}
|
|
}
|
|
|
|
func actionPlanHash(actionID, requestID, capabilityName, resourceID string, payload agentexec.ExecuteCommandPayload, reason string) string {
|
|
sum := sha256.Sum256([]byte(strings.Join([]string{
|
|
actionID,
|
|
requestID,
|
|
capabilityName,
|
|
strings.TrimSpace(resourceID),
|
|
payload.Command,
|
|
payload.TargetType,
|
|
payload.TargetID,
|
|
reason,
|
|
}, "|")))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func actionPlanHashForParams(actionID, requestID, capabilityName, resourceID string, params map[string]any, reason string) string {
|
|
encoded, _ := json.Marshal(cloneActionParams(params))
|
|
sum := sha256.Sum256([]byte(strings.Join([]string{
|
|
actionID,
|
|
requestID,
|
|
capabilityName,
|
|
strings.TrimSpace(resourceID),
|
|
string(encoded),
|
|
reason,
|
|
}, "|")))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func cloneActionParams(params map[string]any) map[string]any {
|
|
if len(params) == 0 {
|
|
return nil
|
|
}
|
|
cloned := make(map[string]any, len(params))
|
|
for key, value := range params {
|
|
cloned[key] = value
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func approvalRecordsForID(approvalID string) []unifiedresources.ActionApprovalRecord {
|
|
approvalID = strings.TrimSpace(approvalID)
|
|
if approvalID == "" {
|
|
return nil
|
|
}
|
|
store := approval.GetStore()
|
|
if store == nil {
|
|
return nil
|
|
}
|
|
req, ok := store.GetApproval(approvalID)
|
|
if !ok || req == nil {
|
|
return nil
|
|
}
|
|
|
|
record := unifiedresources.ActionApprovalRecord{
|
|
Actor: strings.TrimSpace(req.DecidedBy),
|
|
Method: unifiedresources.MethodAPI,
|
|
Timestamp: approvalTimestamp(req),
|
|
Outcome: unifiedresources.OutcomeApproved,
|
|
Reason: strings.TrimSpace(req.Context),
|
|
}
|
|
if req.Status == approval.StatusDenied {
|
|
record.Outcome = unifiedresources.OutcomeRejected
|
|
}
|
|
return []unifiedresources.ActionApprovalRecord{record}
|
|
}
|
|
|
|
func approvalTimestamp(req *approval.ApprovalRequest) time.Time {
|
|
if req == nil || req.DecidedAt == nil {
|
|
return time.Now().UTC()
|
|
}
|
|
return req.DecidedAt.UTC()
|
|
}
|