mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Per maintainer decision: the cloud-context-privacy feature was bloat. The real fix for the "useless Assistant on cloud" problem was the earlier sensitivity recalibration (ordinary workloads = Internal, not redacted); the dial layered a configurable knob on top of an already-solved problem, guarding mostly-non-secret data on a destination the operator opted into, and demanded every model-bound path stay dial-aware (a standing leak surface). The privacy control users actually understand is the choice of model — cloud provider vs. local Ollama. Removed entirely: - AIConfig.CloudContextPrivacy dial + constants + GetCloudContextPrivacy / NormalizeCloudContextPrivacy, AND the now-dead legacy ShareOperationalContextWithCloud boolean + ShouldShareOperationalContextWithCloud (internal/config/ai.go); the config-load migration (persistence.go). - Both fields from the /api/settings/ai request/response, validation, and sync (ai_handlers.go) + the JSON contract snapshots. - The "Cloud model privacy" 3-option UI control, form field, presentation copy, and CloudContextPrivacy type (frontend), plus their tests. - The dial branching in the seam: chat/service.go cloudPrivacyLevel, CloudContextPolicy.Level + local_only suppression + the localOnly directive (context_prefetch.go), the inventory resourceLabel dial logic (resource_context*), and the modelboundary RedactLocalOnlyResourcesOnly option. Fixed lean posture (no setting): a cloud-routed model receives real infrastructure context, with two always-on invariants enforced by the model-boundary sanitizer — credentials are always stripped, and local-only/Restricted resources (the floor) never leave the local trust boundary. Local (Ollama) always full. The sanitizer's default is now the local-only floor; it remains the universal backstop installed on EVERY model-bound path (chat, session compaction, discovery/report/analysis via the shared helper). Kept the two standalone fixes from this effort: compaction now routes through the sanitizer, and directives no longer inject the "redacted by policy" placeholder. Governance: ai-runtime contract rewritten to a fixed-posture rule; api-contracts / frontend-primitives / agent-lifecycle / storage-recovery dial references removed. Tests updated to the floor-only behavior (local-only redacted, Sensitive flows, secrets stripped). Full internal/ai/..., config, api suites green; frontend type-check + tests + lint green.
142 lines
4.7 KiB
Go
142 lines
4.7 KiB
Go
package ai
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
type unifiedResourcePolicyContext struct {
|
|
posture *unifiedresources.PolicyPostureSummary
|
|
externalModel bool
|
|
sensitivityCounts map[unifiedresources.ResourceSensitivity]int
|
|
routingCounts map[unifiedresources.ResourceRoutingScope]int
|
|
localOnlyCount int
|
|
redactionHints []unifiedresources.ResourceRedactionHint
|
|
redactionLabels []string
|
|
}
|
|
|
|
func buildUnifiedResourcePolicyContext(posture *unifiedresources.PolicyPostureSummary, destinationModel string) unifiedResourcePolicyContext {
|
|
context := unifiedResourcePolicyContext{
|
|
externalModel: unifiedResourceContextUsesExternalModel(destinationModel),
|
|
posture: posture,
|
|
sensitivityCounts: map[unifiedresources.ResourceSensitivity]int{},
|
|
routingCounts: map[unifiedresources.ResourceRoutingScope]int{},
|
|
}
|
|
if posture == nil {
|
|
return context
|
|
}
|
|
|
|
if posture.SensitivityCounts != nil {
|
|
context.sensitivityCounts = posture.SensitivityCounts
|
|
}
|
|
if posture.RoutingCounts != nil {
|
|
context.routingCounts = posture.RoutingCounts
|
|
context.localOnlyCount = posture.RoutingCounts[unifiedresources.ResourceRoutingScopeLocalOnly]
|
|
}
|
|
|
|
context.redactionHints = resourcePolicyRedactionHintsFromCounts(posture.RedactionCounts)
|
|
context.redactionLabels = unifiedresources.ResourceRedactionLabelsFromHints(context.redactionHints)
|
|
|
|
return context
|
|
}
|
|
|
|
func unifiedResourceContextUsesExternalModel(destinationModel string) bool {
|
|
destinationModel = strings.TrimSpace(destinationModel)
|
|
if destinationModel == "" {
|
|
return false
|
|
}
|
|
|
|
provider, _ := config.ParseModelString(destinationModel)
|
|
return provider != config.AIProviderOllama
|
|
}
|
|
|
|
func (context unifiedResourcePolicyContext) hasGovernedResources() bool {
|
|
return context.posture != nil && context.posture.TotalResources > 0
|
|
}
|
|
|
|
func (context unifiedResourcePolicyContext) includeResourceDetails(resource unifiedresources.Resource) bool {
|
|
if !context.externalModel || resource.Policy == nil {
|
|
return true
|
|
}
|
|
return resource.Policy.Routing.Scope != unifiedresources.ResourceRoutingScopeLocalOnly
|
|
}
|
|
|
|
func (context unifiedResourcePolicyContext) filterDetailedResources(resources []unifiedresources.Resource) []unifiedresources.Resource {
|
|
if !context.externalModel || len(resources) == 0 {
|
|
return resources
|
|
}
|
|
|
|
filtered := make([]unifiedresources.Resource, 0, len(resources))
|
|
for _, resource := range resources {
|
|
if context.includeResourceDetails(resource) {
|
|
filtered = append(filtered, resource)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func (context unifiedResourcePolicyContext) appendSummarySections(sections []string) []string {
|
|
if !context.hasGovernedResources() {
|
|
return sections
|
|
}
|
|
|
|
sections = append(sections, "\n### Data Governance")
|
|
|
|
sensitivityParts := unifiedresources.ResourcePolicySensitivitySummaryFromCounts(context.sensitivityCounts)
|
|
sections = append(sections, fmt.Sprintf("- Sensitivity: %s", strings.Join(sensitivityParts, ", ")))
|
|
|
|
routingParts := unifiedresources.ResourcePolicyRoutingSummaryFromCounts(context.routingCounts)
|
|
sections = append(sections, fmt.Sprintf("- Routing: %s", strings.Join(routingParts, ", ")))
|
|
sections = append(sections, fmt.Sprintf("- Local-only resources: %d", context.localOnlyCount))
|
|
if context.externalModel && context.localOnlyCount > 0 {
|
|
sections = append(sections, fmt.Sprintf("- External model handling: %d local-only resources are represented only in aggregate and omitted from detailed context.", context.localOnlyCount))
|
|
}
|
|
|
|
if len(context.redactionLabels) > 0 {
|
|
sections = append(sections, "\n### Policy Redaction Hints")
|
|
sections = append(sections, fmt.Sprintf("- Redactions in use: %s", strings.Join(context.redactionLabels, ", ")))
|
|
}
|
|
|
|
return sections
|
|
}
|
|
|
|
func resourcePolicyRedactionHintsFromCounts(counts map[unifiedresources.ResourceRedactionHint]int) []unifiedresources.ResourceRedactionHint {
|
|
if len(counts) == 0 {
|
|
return nil
|
|
}
|
|
|
|
hints := make([]unifiedresources.ResourceRedactionHint, 0, len(counts))
|
|
seen := make(map[unifiedresources.ResourceRedactionHint]struct{}, len(counts))
|
|
|
|
for _, hint := range unifiedresources.ResourceRedactionHintOrder {
|
|
if counts[hint] <= 0 {
|
|
continue
|
|
}
|
|
hints = append(hints, hint)
|
|
seen[hint] = struct{}{}
|
|
}
|
|
|
|
remaining := make([]string, 0, len(counts))
|
|
remainingHints := make(map[string]unifiedresources.ResourceRedactionHint, len(counts))
|
|
for hint, count := range counts {
|
|
if count <= 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[hint]; ok {
|
|
continue
|
|
}
|
|
key := string(hint)
|
|
remaining = append(remaining, key)
|
|
remainingHints[key] = hint
|
|
}
|
|
sort.Strings(remaining)
|
|
for _, key := range remaining {
|
|
hints = append(hints, remainingHints[key])
|
|
}
|
|
|
|
return hints
|
|
}
|