package config import ( "fmt" ) // Config is the full harness.toml surface. Defaults live in Default(); // apply() overlays the parsed document; Validate() enforces invariants. type Config struct { Vertical string WorkRoot string ReportDir string Loop LoopConfig Redmine RedmineConfig LiteLLM LiteLLMConfig Models ModelsConfig Bash BashConfig Demo DemoConfig } type LoopConfig struct { MaxRounds int } type RedmineConfig struct { URL string KeyRef string ScopeQuery string ScopeQueryID int ClassField string DefaultClass string Limit int } type LiteLLMConfig struct { BaseURL string KeyRef string TimeoutSecs int MaxRetries int } type ModelsConfig struct { // Tiers maps tier aliases (mopac-study, ...) to concrete proxy models. Tiers map[string]string // Classes maps task classes (study, code, ...) to tier aliases. Classes map[string]string // DefaultTier is used when a task carries no class. DefaultTier string } type BashConfig struct { Enabled bool Allow []string Deny []string DefaultAllow bool TimeoutSecs int MaxOutputBytes int } type DemoConfig struct { ID string Subject string Prompt string Class string } // Default returns the built-in defaults for every field. func Default() *Config { return &Config{ WorkRoot: ".", ReportDir: "reports", Loop: LoopConfig{MaxRounds: 8}, Redmine: RedmineConfig{ ClassField: "Class", DefaultClass: "primary", Limit: 50, }, LiteLLM: LiteLLMConfig{TimeoutSecs: 120, MaxRetries: 2}, Models: ModelsConfig{ Tiers: map[string]string{}, Classes: map[string]string{}, DefaultTier: "mopac-primary", }, Bash: BashConfig{Enabled: true, TimeoutSecs: 60, MaxOutputBytes: 100_000}, Demo: DemoConfig{ ID: "demo-1", Subject: "MVP demo: GLM self-description", Prompt: "tell me about yourself", Class: "primary", }, } } func (c *Config) apply(doc TOMLDoc) error { if v, ok := doc.String("vertical"); ok { c.Vertical = v } if v, ok := doc.String("work_root"); ok { c.WorkRoot = v } if v, ok := doc.String("report_dir"); ok { c.ReportDir = v } if v, ok := doc.Table("loop").Int("max_rounds"); ok { c.Loop.MaxRounds = int(v) } rm := doc.Table("redmine") if v, ok := rm.String("url"); ok { c.Redmine.URL = v } if v, ok := rm.String("key_ref"); ok { c.Redmine.KeyRef = v } if v, ok := rm.String("scope_query"); ok { c.Redmine.ScopeQuery = v } if v, ok := rm.Int("scope_query_id"); ok { c.Redmine.ScopeQueryID = int(v) } if v, ok := rm.String("class_field"); ok { c.Redmine.ClassField = v } if v, ok := rm.String("default_class"); ok { c.Redmine.DefaultClass = v } if v, ok := rm.Int("limit"); ok { c.Redmine.Limit = int(v) } lt := doc.Table("litellm") if v, ok := lt.String("base_url"); ok { c.LiteLLM.BaseURL = v } if v, ok := lt.String("key_ref"); ok { c.LiteLLM.KeyRef = v } if v, ok := lt.Int("timeout_secs"); ok { c.LiteLLM.TimeoutSecs = int(v) } if v, ok := lt.Int("max_retries"); ok { c.LiteLLM.MaxRetries = int(v) } md := doc.Table("models") for _, k := range md.Keys() { if k == "default_tier" { continue } if v, ok := md.String(k); ok { c.Models.Tiers[k] = v } } if v, ok := md.String("default_tier"); ok { c.Models.DefaultTier = v } cls := doc.Table("models", "classes") for _, k := range cls.Keys() { if v, ok := cls.String(k); ok { c.Models.Classes[k] = v } } bt := doc.Table("tools", "bash") if v, ok := bt.Bool("enabled"); ok { c.Bash.Enabled = v } if v, ok := bt.StringList("allow"); ok { c.Bash.Allow = v } if v, ok := bt.StringList("deny"); ok { c.Bash.Deny = v } if v, ok := bt.String("default"); ok { switch v { case "allow": c.Bash.DefaultAllow = true case "deny": c.Bash.DefaultAllow = false default: return fmt.Errorf("[tools.bash]: default must be \"allow\" or \"deny\", got %q", v) } } if v, ok := bt.Int("timeout_secs"); ok { c.Bash.TimeoutSecs = int(v) } if v, ok := bt.Int("max_output_bytes"); ok { c.Bash.MaxOutputBytes = int(v) } dm := doc.Table("demo") if v, ok := dm.String("id"); ok { c.Demo.ID = v } if v, ok := dm.String("subject"); ok { c.Demo.Subject = v } if v, ok := dm.String("prompt"); ok { c.Demo.Prompt = v } if v, ok := dm.String("class"); ok { c.Demo.Class = v } return nil } // Validate enforces the invariants the conductor relies on. func (c *Config) Validate() error { if c.Vertical == "" { return fmt.Errorf("[core]: vertical is required") } if c.Loop.MaxRounds < 1 { return fmt.Errorf("[loop]: max_rounds must be >= 1") } if c.LiteLLM.BaseURL == "" { return fmt.Errorf("[litellm]: base_url is required") } if c.LiteLLM.KeyRef == "" { return fmt.Errorf("[litellm]: key_ref is required (env:NAME, file:PATH, or literal:VALUE)") } if err := CheckKeyRef(c.LiteLLM.KeyRef); err != nil { return fmt.Errorf("[litellm]: %w", err) } if c.LiteLLM.TimeoutSecs < 1 || c.LiteLLM.MaxRetries < 0 { return fmt.Errorf("[litellm]: bad timeout_secs/max_retries") } // Redmine is optional (demo-only configs); if any piece is set, the rest // of the minimum set must be too. rc := c.Redmine if rc.URL != "" || rc.KeyRef != "" || rc.ScopeQuery != "" || rc.ScopeQueryID != 0 { if rc.URL == "" { return fmt.Errorf("[redmine]: url is required when redmine is configured") } if rc.KeyRef == "" { return fmt.Errorf("[redmine]: key_ref is required when redmine is configured") } if err := CheckKeyRef(rc.KeyRef); err != nil { return fmt.Errorf("[redmine]: %w", err) } if rc.ScopeQuery == "" && rc.ScopeQueryID == 0 { return fmt.Errorf("[redmine]: scope_query or scope_query_id is required") } } if len(c.Models.Tiers) == 0 { return fmt.Errorf("[models]: at least one tier alias is required") } if _, ok := c.Models.Tiers[c.Models.DefaultTier]; !ok { return fmt.Errorf("[models]: default_tier %q has no entry in [models]", c.Models.DefaultTier) } for class, tier := range c.Models.Classes { if _, ok := c.Models.Tiers[tier]; !ok { return fmt.Errorf("[models.classes]: class %q points at unknown tier %q", class, tier) } } if c.Bash.TimeoutSecs < 1 || c.Bash.MaxOutputBytes < 1 { return fmt.Errorf("[tools.bash]: bad timeout_secs/max_output_bytes") } if c.Demo.Prompt == "" { return fmt.Errorf("[demo]: prompt is required") } return nil }