harness: config layer + model routing v0 (tier map, no heuristics)
harness.toml loading via a stdlib-only TOML subset parser (tables, bare
keys, strings/ints/bools, multi-line arrays; anything richer fails loudly).
Secrets are refs only (env:/file:/literal:, bw: reserved) and are redacted
from every error path. Model routing v0: static class -> tier alias ->
concrete model map per the DESIGN model-selection layer; requests carry the
resolved concrete model and unknown classes fail hard so routing stays
auditable. Table-driven tests cover the parser, validation, key refs, and
routing decisions.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user