WitNote / internal /config /config_load.go
AUXteam's picture
Upload folder using huggingface_hub
6a7089a verified
package config
import (
"encoding/json"
"log/slog"
"os"
"path/filepath"
"strconv"
"time"
)
// Load returns the RuntimeConfig with precedence: env vars > config file > defaults.
func Load() *RuntimeConfig {
cfg := &RuntimeConfig{
// Server defaults
Bind: "127.0.0.1",
Port: "9867",
InstancePortStart: 9868,
InstancePortEnd: 9968,
Token: os.Getenv("PINCHTAB_TOKEN"),
StateDir: userConfigDir(),
// Security defaults
AllowEvaluate: false,
AllowMacro: false,
AllowScreencast: false,
AllowDownload: false,
AllowUpload: false,
MaxRedirects: -1, // Unlimited by default; set to N to limit redirect hops
// Browser / instance defaults
Headless: true,
NoRestore: false,
ProfileDir: "",
ProfilesBaseDir: "",
DefaultProfile: "default",
ChromeVersion: "144.0.7559.133",
Timezone: "",
BlockImages: false,
BlockMedia: false,
BlockAds: false,
MaxTabs: 20,
MaxParallelTabs: 0,
ChromeBinary: "", // Set via config.json only
ChromeExtraFlags: "",
ExtensionPaths: nil,
UserAgent: "",
NoAnimations: false,
StealthLevel: "light",
TabEvictionPolicy: "close_lru",
// Timeout defaults
ActionTimeout: 30 * time.Second,
NavigateTimeout: 60 * time.Second,
ShutdownTimeout: 10 * time.Second,
WaitNavDelay: 1 * time.Second,
// Orchestrator defaults
Strategy: "always-on",
AllocationPolicy: "fcfs",
RestartMaxRestarts: 20,
RestartInitBackoff: 2 * time.Second,
RestartMaxBackoff: 60 * time.Second,
RestartStableAfter: 5 * time.Minute,
// Attach defaults
AttachEnabled: true,
AttachAllowHosts: []string{"127.0.0.1", "localhost", "::1"},
AttachAllowSchemes: []string{"ws", "wss"},
// IDPI defaults
IDPI: IDPIConfig{
Enabled: true,
AllowedDomains: append([]string(nil), defaultLocalAllowedDomains...),
StrictMode: true,
ScanContent: true,
WrapContent: true,
ScanTimeoutSec: 5,
},
// Engine default (set via config.json only)
Engine: "chrome",
}
finalizeProfileConfig(cfg)
// Load config file (supports both legacy flat and new nested format)
configPath := envOr("PINCHTAB_CONFIG", filepath.Join(userConfigDir(), "config.json"))
data, err := os.ReadFile(configPath)
if err != nil {
if !os.IsNotExist(err) {
slog.Warn("failed to read config file", "path", configPath, "error", err)
}
return cfg
}
slog.Debug("loading config file", "path", configPath)
var fc *FileConfig
if isLegacyConfig(data) {
var lc legacyFileConfig
if err := json.Unmarshal(data, &lc); err != nil {
slog.Warn("failed to parse legacy config", "path", configPath, "error", err)
return cfg
}
fc = convertLegacyConfig(&lc)
slog.Info("loaded legacy flat config, consider migrating to nested format", "path", configPath)
} else {
fc = &FileConfig{}
if err := json.Unmarshal(data, fc); err != nil {
slog.Warn("failed to parse config", "path", configPath, "error", err)
return cfg
}
}
// Validate file config and log warnings
if errs := ValidateFileConfig(fc); len(errs) > 0 {
for _, e := range errs {
slog.Warn("config validation error", "path", configPath, "error", e)
}
}
// Apply file config (only if env var NOT set)
applyFileConfig(cfg, fc)
finalizeProfileConfig(cfg)
return cfg
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func envIntOr(key string, fallback int) int {
v := os.Getenv(key)
if v == "" {
return fallback
}
n, err := strconv.Atoi(v)
if err != nil || n < 0 {
return fallback
}
return n
}
func finalizeProfileConfig(cfg *RuntimeConfig) {
if cfg.DefaultProfile == "" {
cfg.DefaultProfile = "default"
}
if cfg.ProfilesBaseDir == "" {
cfg.ProfilesBaseDir = filepath.Join(cfg.StateDir, "profiles")
}
if cfg.ProfileDir == "" {
cfg.ProfileDir = filepath.Join(cfg.ProfilesBaseDir, cfg.DefaultProfile)
}
}
func applyFileConfig(cfg *RuntimeConfig, fc *FileConfig) {
// Server
if fc.Server.Port != "" {
cfg.Port = fc.Server.Port
}
if fc.Server.Bind != "" {
cfg.Bind = fc.Server.Bind
}
if fc.Server.Token != "" && os.Getenv("PINCHTAB_TOKEN") == "" {
cfg.Token = fc.Server.Token
}
if fc.Server.StateDir != "" {
cfg.StateDir = fc.Server.StateDir
}
if fc.Server.Engine != "" {
cfg.Engine = fc.Server.Engine
}
// Security
if fc.Security.AllowEvaluate != nil {
cfg.AllowEvaluate = *fc.Security.AllowEvaluate
}
if fc.Security.AllowMacro != nil {
cfg.AllowMacro = *fc.Security.AllowMacro
}
if fc.Security.AllowScreencast != nil {
cfg.AllowScreencast = *fc.Security.AllowScreencast
}
if fc.Security.AllowDownload != nil {
cfg.AllowDownload = *fc.Security.AllowDownload
}
if fc.Security.AllowUpload != nil {
cfg.AllowUpload = *fc.Security.AllowUpload
}
if fc.Security.MaxRedirects != nil {
cfg.MaxRedirects = *fc.Security.MaxRedirects
}
// IDPI – copy the whole struct; individual fields have safe zero-value defaults.
cfg.IDPI = fc.Security.IDPI
// Browser
if fc.Browser.ChromeVersion != "" {
cfg.ChromeVersion = fc.Browser.ChromeVersion
}
if fc.Browser.ChromeBinary != "" {
cfg.ChromeBinary = fc.Browser.ChromeBinary
}
if fc.Browser.ChromeExtraFlags != "" {
cfg.ChromeExtraFlags = fc.Browser.ChromeExtraFlags
}
if len(fc.Browser.ExtensionPaths) > 0 {
cfg.ExtensionPaths = fc.Browser.ExtensionPaths
}
// Instance defaults
if fc.InstanceDefaults.Mode != "" {
cfg.Headless = modeToHeadless(fc.InstanceDefaults.Mode, cfg.Headless)
}
if fc.InstanceDefaults.NoRestore != nil {
cfg.NoRestore = *fc.InstanceDefaults.NoRestore
}
if fc.InstanceDefaults.Timezone != "" {
cfg.Timezone = fc.InstanceDefaults.Timezone
}
if fc.InstanceDefaults.BlockImages != nil {
cfg.BlockImages = *fc.InstanceDefaults.BlockImages
}
if fc.InstanceDefaults.BlockMedia != nil {
cfg.BlockMedia = *fc.InstanceDefaults.BlockMedia
}
if fc.InstanceDefaults.BlockAds != nil {
cfg.BlockAds = *fc.InstanceDefaults.BlockAds
}
if fc.InstanceDefaults.MaxTabs != nil {
cfg.MaxTabs = *fc.InstanceDefaults.MaxTabs
}
if fc.InstanceDefaults.MaxParallelTabs != nil {
cfg.MaxParallelTabs = *fc.InstanceDefaults.MaxParallelTabs
}
if fc.InstanceDefaults.UserAgent != "" {
cfg.UserAgent = fc.InstanceDefaults.UserAgent
}
if fc.InstanceDefaults.NoAnimations != nil {
cfg.NoAnimations = *fc.InstanceDefaults.NoAnimations
}
if fc.InstanceDefaults.StealthLevel != "" {
cfg.StealthLevel = fc.InstanceDefaults.StealthLevel
}
if fc.InstanceDefaults.TabEvictionPolicy != "" {
cfg.TabEvictionPolicy = fc.InstanceDefaults.TabEvictionPolicy
}
// Profiles
if fc.Profiles.BaseDir != "" {
cfg.ProfilesBaseDir = fc.Profiles.BaseDir
}
if fc.Profiles.DefaultProfile != "" {
cfg.DefaultProfile = fc.Profiles.DefaultProfile
}
cfg.ProfileDir = ""
// Multi-instance
if fc.MultiInstance.Strategy != "" {
cfg.Strategy = fc.MultiInstance.Strategy
}
if fc.MultiInstance.AllocationPolicy != "" {
cfg.AllocationPolicy = fc.MultiInstance.AllocationPolicy
}
if fc.MultiInstance.InstancePortStart != nil {
cfg.InstancePortStart = *fc.MultiInstance.InstancePortStart
}
if fc.MultiInstance.InstancePortEnd != nil {
cfg.InstancePortEnd = *fc.MultiInstance.InstancePortEnd
}
// Restart
if fc.MultiInstance.Restart.MaxRestarts != nil {
cfg.RestartMaxRestarts = *fc.MultiInstance.Restart.MaxRestarts
}
if fc.MultiInstance.Restart.InitBackoffSec != nil {
cfg.RestartInitBackoff = time.Duration(*fc.MultiInstance.Restart.InitBackoffSec) * time.Second
}
if fc.MultiInstance.Restart.MaxBackoffSec != nil {
cfg.RestartMaxBackoff = time.Duration(*fc.MultiInstance.Restart.MaxBackoffSec) * time.Second
}
if fc.MultiInstance.Restart.StableAfterSec != nil {
cfg.RestartStableAfter = time.Duration(*fc.MultiInstance.Restart.StableAfterSec) * time.Second
}
// Attach
if fc.Security.Attach.Enabled != nil {
cfg.AttachEnabled = *fc.Security.Attach.Enabled
}
if len(fc.Security.Attach.AllowHosts) > 0 {
cfg.AttachAllowHosts = append([]string(nil), fc.Security.Attach.AllowHosts...)
}
if len(fc.Security.Attach.AllowSchemes) > 0 {
cfg.AttachAllowSchemes = append([]string(nil), fc.Security.Attach.AllowSchemes...)
}
// Timeouts
if fc.Timeouts.ActionSec > 0 {
cfg.ActionTimeout = time.Duration(fc.Timeouts.ActionSec) * time.Second
}
if fc.Timeouts.NavigateSec > 0 {
cfg.NavigateTimeout = time.Duration(fc.Timeouts.NavigateSec) * time.Second
}
if fc.Timeouts.ShutdownSec > 0 {
cfg.ShutdownTimeout = time.Duration(fc.Timeouts.ShutdownSec) * time.Second
}
if fc.Timeouts.WaitNavMs > 0 {
cfg.WaitNavDelay = time.Duration(fc.Timeouts.WaitNavMs) * time.Millisecond
}
// Scheduler
if fc.Scheduler.Enabled != nil {
cfg.Scheduler.Enabled = *fc.Scheduler.Enabled
}
if fc.Scheduler.Strategy != "" {
cfg.Scheduler.Strategy = fc.Scheduler.Strategy
}
if fc.Scheduler.MaxQueueSize != nil {
cfg.Scheduler.MaxQueueSize = *fc.Scheduler.MaxQueueSize
}
if fc.Scheduler.MaxPerAgent != nil {
cfg.Scheduler.MaxPerAgent = *fc.Scheduler.MaxPerAgent
}
if fc.Scheduler.MaxInflight != nil {
cfg.Scheduler.MaxInflight = *fc.Scheduler.MaxInflight
}
if fc.Scheduler.MaxPerAgentFlight != nil {
cfg.Scheduler.MaxPerAgentFlight = *fc.Scheduler.MaxPerAgentFlight
}
if fc.Scheduler.ResultTTLSec != nil {
cfg.Scheduler.ResultTTLSec = *fc.Scheduler.ResultTTLSec
}
if fc.Scheduler.WorkerCount != nil {
cfg.Scheduler.WorkerCount = *fc.Scheduler.WorkerCount
}
}
// ApplyFileConfigToRuntime merges file configuration into an existing runtime
// config and refreshes derived profile paths for long-running processes.
func ApplyFileConfigToRuntime(cfg *RuntimeConfig, fc *FileConfig) {
if cfg == nil || fc == nil {
return
}
applyFileConfig(cfg, fc)
finalizeProfileConfig(cfg)
}