191 lines
6.8 KiB
Go
191 lines
6.8 KiB
Go
// 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 is an optional static admin token (sees all sessions). Users log in via /api/auth.
|
|
AuthToken string `yaml:"auth_token" json:"auth_token"`
|
|
// AllowRegistration enables self-service sign-up (default true).
|
|
AllowRegistration *bool `yaml:"allow_registration" json:"allow_registration"`
|
|
// TokenTTLHours is the lifetime of login tokens (default 720 = 30 days).
|
|
TokenTTLHours int `yaml:"token_ttl_hours" json:"token_ttl_hours"`
|
|
// 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_ALLOW_REGISTRATION"); v != "" {
|
|
b := v == "1" || strings.EqualFold(v, "true") || strings.EqualFold(v, "yes")
|
|
c.AllowRegistration = &b
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// RegistrationAllowed reports allow_registration (default true).
|
|
func (c Config) RegistrationAllowed() bool { return c.AllowRegistration == nil || *c.AllowRegistration }
|
|
|
|
// Validate enforces required fields and secure settings.
|
|
func (c Config) Validate() error {
|
|
var errs []error
|
|
if c.AuthToken != "" && len(c.AuthToken) < 16 {
|
|
errs = append(errs, errors.New("auth_token must be at least 16 characters when set (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...)
|
|
}
|