feat: improve AI config and persistence

- Enhance AI configuration options
- Improve persistence layer
- Add AI config tests
This commit is contained in:
rcourtman
2026-01-22 22:31:42 +00:00
parent 8bf31214f5
commit d909f319a5
3 changed files with 55 additions and 6 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)