mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
feat: improve AI config and persistence
- Enhance AI configuration options - Improve persistence layer - Add AI config tests
This commit is contained in:
@@ -64,7 +64,7 @@ type AIConfig struct {
|
||||
|
||||
// AI Infrastructure Control settings
|
||||
// These control whether AI can take actions on infrastructure (start/stop VMs, containers, etc.)
|
||||
ControlLevel string `json:"control_level,omitempty"` // "read_only", "suggest", "controlled", "autonomous"
|
||||
ControlLevel string `json:"control_level,omitempty"` // "read_only", "controlled", "autonomous"
|
||||
ProtectedGuests []string `json:"protected_guests,omitempty"` // VMIDs or names that AI cannot control
|
||||
}
|
||||
|
||||
@@ -81,8 +81,6 @@ const (
|
||||
const (
|
||||
// ControlLevelReadOnly - AI can only query infrastructure, no control tools available
|
||||
ControlLevelReadOnly = "read_only"
|
||||
// ControlLevelSuggest - AI suggests commands, user must copy/paste to execute
|
||||
ControlLevelSuggest = "suggest"
|
||||
// ControlLevelControlled - AI can execute with per-command approval
|
||||
ControlLevelControlled = "controlled"
|
||||
// ControlLevelAutonomous - AI executes without approval (requires Pro license)
|
||||
@@ -506,7 +504,14 @@ func (c *AIConfig) GetControlLevel() string {
|
||||
}
|
||||
return ControlLevelReadOnly
|
||||
}
|
||||
return c.ControlLevel
|
||||
switch c.ControlLevel {
|
||||
case ControlLevelReadOnly, ControlLevelControlled, ControlLevelAutonomous:
|
||||
return c.ControlLevel
|
||||
case "suggest":
|
||||
return ControlLevelControlled
|
||||
default:
|
||||
return ControlLevelReadOnly
|
||||
}
|
||||
}
|
||||
|
||||
// IsControlEnabled returns true if AI has any control capability beyond read-only
|
||||
@@ -523,7 +528,7 @@ func (c *AIConfig) IsAutonomous() bool {
|
||||
// IsValidControlLevel checks if a control level string is valid
|
||||
func IsValidControlLevel(level string) bool {
|
||||
switch level {
|
||||
case ControlLevelReadOnly, ControlLevelSuggest, ControlLevelControlled, ControlLevelAutonomous:
|
||||
case ControlLevelReadOnly, ControlLevelControlled, ControlLevelAutonomous:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -1661,10 +1661,10 @@ func (c *ConfigPersistence) SaveAIConfig(settings AIConfig) error {
|
||||
// LoadAIConfig retrieves the persisted AI settings. It returns default config when no configuration exists yet.
|
||||
func (c *ConfigPersistence) LoadAIConfig() (*AIConfig, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
data, err := c.fs.ReadFile(c.aiFile)
|
||||
if err != nil {
|
||||
c.mu.RUnlock()
|
||||
if os.IsNotExist(err) {
|
||||
// Return default config if file doesn't exist
|
||||
return NewDefaultAIConfig(), nil
|
||||
@@ -1675,6 +1675,7 @@ func (c *ConfigPersistence) LoadAIConfig() (*AIConfig, error) {
|
||||
if c.crypto != nil {
|
||||
decrypted, err := c.crypto.Decrypt(data)
|
||||
if err != nil {
|
||||
c.mu.RUnlock()
|
||||
return nil, err
|
||||
}
|
||||
data = decrypted
|
||||
@@ -1683,6 +1684,7 @@ func (c *ConfigPersistence) LoadAIConfig() (*AIConfig, error) {
|
||||
// Start with defaults so new fields get proper values
|
||||
settings := NewDefaultAIConfig()
|
||||
if err := json.Unmarshal(data, settings); err != nil {
|
||||
c.mu.RUnlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1692,6 +1694,22 @@ func (c *ConfigPersistence) LoadAIConfig() (*AIConfig, error) {
|
||||
settings.PatrolIntervalMinutes = 15
|
||||
}
|
||||
|
||||
migratedControlLevel := false
|
||||
if settings.ControlLevel == "suggest" {
|
||||
settings.ControlLevel = ControlLevelControlled
|
||||
migratedControlLevel = true
|
||||
}
|
||||
|
||||
c.mu.RUnlock()
|
||||
|
||||
if migratedControlLevel {
|
||||
if err := c.SaveAIConfig(*settings); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to save migrated AI control level")
|
||||
} else {
|
||||
log.Info().Str("control_level", settings.ControlLevel).Msg("Migrated AI control level")
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Str("file", c.aiFile).Bool("enabled", settings.Enabled).Bool("patrol_enabled", settings.PatrolEnabled).Bool("alert_triggered_analysis", settings.AlertTriggeredAnalysis).Msg("AI configuration loaded")
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -115,6 +116,31 @@ func TestPersistence_AIConfig(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPersistence_AIConfig_MigratesSuggestControlLevel(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
p := NewConfigPersistence(tempDir)
|
||||
|
||||
cfg := NewDefaultAIConfig()
|
||||
cfg.ControlLevel = "suggest"
|
||||
require.NoError(t, p.SaveAIConfig(*cfg))
|
||||
|
||||
loaded, err := p.LoadAIConfig()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ControlLevelControlled, loaded.ControlLevel)
|
||||
|
||||
updatedRaw, err := os.ReadFile(filepath.Join(tempDir, "ai.enc"))
|
||||
require.NoError(t, err)
|
||||
if p.crypto != nil {
|
||||
decoded, err := p.crypto.Decrypt(updatedRaw)
|
||||
require.NoError(t, err)
|
||||
updatedRaw = decoded
|
||||
}
|
||||
|
||||
var saved AIConfig
|
||||
require.NoError(t, json.Unmarshal(updatedRaw, &saved))
|
||||
assert.Equal(t, ControlLevelControlled, saved.ControlLevel)
|
||||
}
|
||||
|
||||
func TestPersistence_PatrolRunHistory(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
p := NewConfigPersistence(tempDir)
|
||||
|
||||
Reference in New Issue
Block a user