// Package config loads ByteClaw configuration from YAML/JSON files and env vars. package config import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) type LLM struct { // Provider selects a preset: openai, openrouter, ollama, dahl, gonkarouter or custom. Provider string `yaml:"provider" json:"provider"` BaseURL string `yaml:"base_url" json:"base_url"` APIKey string `yaml:"api_key" json:"api_key"` Model string `yaml:"model" json:"model"` TimeoutSec int `yaml:"timeout_sec" json:"timeout_sec"` Temperature float64 `yaml:"temperature" json:"temperature"` // Providers holds optional per-preset settings (keys stay server-side) so the // active provider can be switched at runtime, e.g. providers: {dahl: {api_key: ...}}. Providers map[string]ProviderOverride `yaml:"providers" json:"providers"` } type ProviderOverride struct { BaseURL string `yaml:"base_url" json:"base_url"` APIKey string `yaml:"api_key" json:"api_key"` Model string `yaml:"model" json:"model"` } type Agent struct { SystemPrompt string `yaml:"system_prompt" json:"system_prompt"` MaxIterations int `yaml:"max_iterations" json:"max_iterations"` HistoryLimit int `yaml:"history_limit" json:"history_limit"` Summarize Summarize `yaml:"summarize" json:"summarize"` } // Summarize controls automatic summarisation of long sessions. type Summarize struct { Enabled bool `yaml:"enabled" json:"enabled"` // Summarise when unsummarised messages exceed MaxMessages or ~MaxTokens (chars/4). MaxMessages int `yaml:"max_messages" json:"max_messages"` MaxTokens int `yaml:"max_tokens" json:"max_tokens"` // KeepRecent newest messages always stay verbatim. KeepRecent int `yaml:"keep_recent" json:"keep_recent"` } type WebFetch struct { Enabled bool `yaml:"enabled" json:"enabled"` // MaxBytes caps the response body read from remote servers. MaxBytes int64 `yaml:"max_bytes" json:"max_bytes"` TimeoutSec int `yaml:"timeout_sec" json:"timeout_sec"` // AllowPrivate permits fetching loopback/private/link-local addresses (SSRF risk). Default false. AllowPrivate bool `yaml:"allow_private" json:"allow_private"` } // Shell configures the opt-in shell tool (default off). type Shell struct { Enabled bool `yaml:"enabled" json:"enabled"` AllowedCommands []string `yaml:"allowed_commands" json:"allowed_commands"` TimeoutSec int `yaml:"timeout_sec" json:"timeout_sec"` MaxOutputBytes int `yaml:"max_output_bytes" json:"max_output_bytes"` } type Tools struct { Shell Shell `yaml:"shell" json:"shell"` WebFetch WebFetch `yaml:"web_fetch" json:"web_fetch"` // FilesEnabled enables read/write/list tools confined to the workspace directory. FilesEnabled bool `yaml:"files_enabled" json:"files_enabled"` // FileMaxBytes caps file reads/writes. FileMaxBytes int64 `yaml:"file_max_bytes" json:"file_max_bytes"` } type Config struct { Listen string `yaml:"listen" json:"listen"` AuthToken string `yaml:"auth_token" json:"auth_token"` // AllowedOrigins for browser WebSocket/CORS. Empty = same host only. "*" must be set explicitly. AllowedOrigins []string `yaml:"allowed_origins" json:"allowed_origins"` DataDir string `yaml:"data_dir" json:"data_dir"` Workspace string `yaml:"workspace" json:"workspace"` LLM LLM `yaml:"llm" json:"llm"` Agent Agent `yaml:"agent" json:"agent"` Tools Tools `yaml:"tools" json:"tools"` } // Default returns a config with secure defaults. func Default() Config { return Config{ Listen: "127.0.0.1:8787", DataDir: "./data", LLM: LLM{ Provider: "openai", TimeoutSec: 120, Temperature: 0.7, }, Agent: Agent{ SystemPrompt: "You are ByteClaw, a concise and helpful self-hosted assistant. Use tools when they help.", MaxIterations: 8, HistoryLimit: 50, Summarize: Summarize{Enabled: true, MaxMessages: 40, MaxTokens: 12000, KeepRecent: 10}, }, Tools: Tools{ WebFetch: WebFetch{Enabled: true, MaxBytes: 512 * 1024, TimeoutSec: 15}, FilesEnabled: true, Shell: Shell{Enabled: false, TimeoutSec: 30, MaxOutputBytes: 64 * 1024}, FileMaxBytes: 1 << 20, }, } } // Load reads path (optional; .json or .yaml/.yml) on top of defaults, then applies env overrides. func Load(path string) (Config, error) { cfg := Default() if path != "" { b, err := os.ReadFile(path) if err != nil { return cfg, fmt.Errorf("read config: %w", err) } switch strings.ToLower(filepath.Ext(path)) { case ".json": err = json.Unmarshal(b, &cfg) default: err = yaml.Unmarshal(b, &cfg) } if err != nil { return cfg, fmt.Errorf("parse config: %w", err) } } applyEnv(&cfg, os.Getenv) if err := applyPreset(&cfg.LLM, os.Getenv); err != nil { return cfg, err } if cfg.Workspace == "" { cfg.Workspace = filepath.Join(cfg.DataDir, "workspace") } return cfg, cfg.Validate() } func applyEnv(c *Config, get func(string) string) { set := func(key string, dst *string) { if v := get(key); v != "" { *dst = v } } set("BYTECLAW_LISTEN", &c.Listen) set("BYTECLAW_AUTH_TOKEN", &c.AuthToken) set("BYTECLAW_DATA_DIR", &c.DataDir) set("BYTECLAW_WORKSPACE", &c.Workspace) set("BYTECLAW_LLM_PROVIDER", &c.LLM.Provider) set("BYTECLAW_LLM_BASE_URL", &c.LLM.BaseURL) set("BYTECLAW_LLM_API_KEY", &c.LLM.APIKey) set("BYTECLAW_LLM_MODEL", &c.LLM.Model) if v := get("BYTECLAW_ALLOWED_ORIGINS"); v != "" { c.AllowedOrigins = nil for _, o := range strings.Split(v, ",") { if o = strings.TrimSpace(o); o != "" { c.AllowedOrigins = append(c.AllowedOrigins, o) } } } } // Validate enforces required fields and secure settings. func (c Config) Validate() error { var errs []error if len(c.AuthToken) < 16 { errs = append(errs, errors.New("auth_token must be set and at least 16 characters (BYTECLAW_AUTH_TOKEN)")) } if c.LLM.BaseURL == "" || c.LLM.Model == "" { errs = append(errs, errors.New("llm.base_url and llm.model are required")) } if c.Agent.MaxIterations <= 0 { errs = append(errs, errors.New("agent.max_iterations must be > 0")) } return errors.Join(errs...) }