Pulse/internal/ai/resource_export.go
2026-03-19 04:07:47 +00:00

114 lines
4 KiB
Go

package ai
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/google/uuid"
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rs/zerolog/log"
)
type unifiedResourceExportDigest struct {
OrgID string `json:"orgId"`
DestinationModel string `json:"destinationModel"`
Summary string `json:"summary"`
ResourceCount int `json:"resourceCount"`
InfrastructureCount int `json:"infrastructureCount"`
WorkloadCount int `json:"workloadCount"`
SensitivityFloor unifiedresources.DataSensitivity `json:"sensitivityFloor"`
RouteDecision unifiedresources.ModelRouteDecision `json:"routeDecision"`
}
func (s *Service) recordUnifiedResourceExport(destinationModel, summary string, stats unifiedresources.ResourceStats, sensitivityCounts map[unifiedresources.ResourceSensitivity]int, localOnlyCount int, redactionHints []unifiedresources.ResourceRedactionHint) {
destinationModel = strings.TrimSpace(destinationModel)
summary = strings.TrimSpace(summary)
if s == nil || destinationModel == "" || summary == "" {
return
}
s.mu.RLock()
store := s.resourceExportStore
orgID := strings.TrimSpace(s.orgID)
storeOrgID := strings.TrimSpace(s.resourceExportStoreOrgID)
s.mu.RUnlock()
if store == nil || (storeOrgID != "" && storeOrgID != orgID) {
return
}
redactions := unifiedresources.ResourceRedactionLabelsFromHints(redactionHints)
sensitivityFloor := unifiedresources.ExportSensitivityFloor(sensitivityCounts)
decision, reason := unifiedresources.ExportDecisionForContext(sensitivityFloor, localOnlyCount, len(redactions))
infraCount := stats.ByType[unifiedresources.ResourceTypeAgent] +
stats.ByType[unifiedresources.ResourceTypeK8sCluster] +
stats.ByType[unifiedresources.ResourceTypeK8sNode]
workloadCount := stats.ByType[unifiedresources.ResourceTypeVM] +
stats.ByType[unifiedresources.ResourceTypeSystemContainer] +
stats.ByType[unifiedresources.ResourceTypeAppContainer] +
stats.ByType[unifiedresources.ResourceTypePod] +
stats.ByType[unifiedresources.ResourceTypeK8sDeployment]
envelope := unifiedresources.ExportEnvelope{
DestinationModel: destinationModel,
DataPayload: map[string]any{
"summary": summary,
"resourceCount": stats.Total,
"infrastructureCount": infraCount,
"workloadCount": workloadCount,
"localOnlyCount": localOnlyCount,
},
RouteDecision: unifiedresources.ModelRouteDecision{
ResourceID: "unified-resource-context",
OriginalExport: unifiedresources.ExportAllowed,
FinalDecision: decision,
AppliedRedactions: redactions,
RoutingReason: reason,
},
SensitivityFloor: sensitivityFloor,
}
hashInput := unifiedResourceExportDigest{
OrgID: orgID,
DestinationModel: envelope.DestinationModel,
Summary: summary,
ResourceCount: stats.Total,
InfrastructureCount: infraCount,
WorkloadCount: workloadCount,
SensitivityFloor: envelope.SensitivityFloor,
RouteDecision: envelope.RouteDecision,
}
payload, err := json.Marshal(hashInput)
if err != nil {
log.Warn().
Err(err).
Str("orgID", orgID).
Str("destinationModel", destinationModel).
Msg("failed to hash unified resource export envelope")
return
}
sum := sha256.Sum256(payload)
record := unifiedresources.ExportAuditRecord{
ID: uuid.NewString(),
Timestamp: time.Now().UTC(),
Actor: fmt.Sprintf("ai-service:%s", orgID),
EnvelopeHash: hex.EncodeToString(sum[:]),
Decision: decision,
Destination: destinationModel,
Redactions: redactions,
}
if err := store.RecordExportAudit(record); err != nil {
log.Warn().
Err(err).
Str("orgID", orgID).
Str("destinationModel", destinationModel).
Msg("failed to persist unified resource export audit")
}
}