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:
2026-08-28 19:20:51 -05:00
parent a1623e141e
commit 591d345371
8 changed files with 1257 additions and 0 deletions
+265
View File
@@ -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
}
+202
View File
@@ -0,0 +1,202 @@
package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
const testCfg = `
vertical = "teststack"
work_root = "/tmp/harness-test"
report_dir = "out"
[redmine]
url = "https://rm.test"
key_ref = "env:HARNESS_TEST_RM_KEY"
scope_query = "project=x&status_id=open"
[litellm]
base_url = "http://litellm.test:4000"
key_ref = "env:HARNESS_TEST_LLM_KEY"
[models]
mopac-study = "glm-4.7-flash"
mopac-code = "glm-5.2"
mopac-review = "glm-5-turbo"
mopac-primary = "glm-5.3"
default_tier = "mopac-primary"
[models.classes]
study = "mopac-study"
code = "mopac-code"
primary = "mopac-primary"
[tools.bash]
allow = ["pwd", "ls *"]
deny = ["sudo *"]
`
func writeTemp(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "harness.toml")
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return path
}
func TestLoadAppliesDocAndDefaults(t *testing.T) {
cfg, err := Load(writeTemp(t, testCfg))
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Vertical != "teststack" || cfg.WorkRoot != "/tmp/harness-test" || cfg.ReportDir != "out" {
t.Errorf("core fields wrong: %+v", cfg)
}
if cfg.Loop.MaxRounds != 8 {
t.Errorf("default max_rounds = %d, want 8", cfg.Loop.MaxRounds)
}
if cfg.LiteLLM.BaseURL != "http://litellm.test:4000" || cfg.LiteLLM.MaxRetries != 2 {
t.Errorf("litellm wrong: %+v", cfg.LiteLLM)
}
wantTiers := map[string]string{
"mopac-study": "glm-4.7-flash",
"mopac-code": "glm-5.2",
"mopac-review": "glm-5-turbo",
"mopac-primary": "glm-5.3",
}
for tier, model := range wantTiers {
if got, ok := cfg.Models.Tiers[tier]; !ok || got != model {
t.Errorf("tier %s = %q ok=%v, want %q", tier, got, ok, model)
}
}
if cfg.Models.Classes["code"] != "mopac-code" {
t.Errorf("class map wrong: %+v", cfg.Models.Classes)
}
if len(cfg.Bash.Allow) != 2 || len(cfg.Bash.Deny) != 1 {
t.Errorf("bash lists wrong: %v / %v", cfg.Bash.Allow, cfg.Bash.Deny)
}
if cfg.Bash.TimeoutSecs != 60 || cfg.Bash.MaxOutputBytes != 100_000 {
t.Errorf("bash defaults wrong: %+v", cfg.Bash)
}
if cfg.Demo.Prompt != "tell me about yourself" {
t.Errorf("demo default prompt = %q", cfg.Demo.Prompt)
}
}
func TestLoadTrackedExampleFile(t *testing.T) {
// The example ships to users; it must always parse and validate as-is.
path := filepath.Join("..", "..", "harness.toml.example")
if _, err := os.Stat(path); err != nil {
t.Skipf("example not found: %v", err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load(harness.toml.example): %v", err)
}
if cfg.Vertical != "demo" || cfg.Models.Tiers["mopac-primary"] != "glm-5.3" {
t.Errorf("example parsed wrong: %+v", cfg.Models.Tiers)
}
if len(cfg.Models.Classes) != 9 {
t.Errorf("example classes = %d, want 9", len(cfg.Models.Classes))
}
}
func TestLoadValidationErrors(t *testing.T) {
cases := []struct {
name string
mut func(string) string
want string
}{
{
name: "missing vertical",
mut: func(s string) string { return strings.Replace(s, `vertical = "teststack"`, "", 1) },
want: "vertical is required",
},
{
name: "missing litellm url",
mut: func(s string) string { return strings.Replace(s, `base_url = "http://litellm.test:4000"`, "", 1) },
want: "base_url is required",
},
{
name: "bare key value",
mut: func(s string) string { return strings.Replace(s, `key_ref = "env:HARNESS_TEST_LLM_KEY"`, `key_ref = "sk-or-whatever"`, 1) },
want: "env:, file:, literal:, or bw:",
},
{
name: "bw ref not implemented",
mut: func(s string) string { return strings.Replace(s, `key_ref = "env:HARNESS_TEST_LLM_KEY"`, `key_ref = "bw:item"`, 1) },
want: "not implemented",
},
{
name: "redmine half configured",
mut: func(s string) string { return strings.Replace(s, `scope_query = "project=x&status_id=open"`, "", 1) },
want: "scope_query or scope_query_id is required",
},
{
name: "class to unknown tier",
mut: func(s string) string { return strings.Replace(s, `code = "mopac-code"`, `code = "mopac-nope"`, 1) },
want: `unknown tier "mopac-nope"`,
},
{
name: "default tier missing",
mut: func(s string) string { return strings.Replace(s, `mopac-primary = "glm-5.3"`, "", 1) },
want: `default_tier "mopac-primary" has no entry`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := Load(writeTemp(t, tc.mut(testCfg)))
if err == nil {
t.Fatalf("expected error containing %q, got nil", tc.want)
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error %q does not contain %q", err, tc.want)
}
})
}
}
func TestDemoOnlyConfigIsValid(t *testing.T) {
// No [redmine] at all: --demo still works.
cfg, err := Load(writeTemp(t, `
vertical = "demo"
[litellm]
base_url = "http://x:4001"
key_ref = "literal:test-key"
[models]
mopac-primary = "glm-5.3"
`))
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Redmine.URL != "" {
t.Errorf("redmine should be unset")
}
}
func TestResolveKeyRef(t *testing.T) {
t.Setenv("HARNESS_TEST_KEY", "sekrit")
if v, err := ResolveKeyRef("env:HARNESS_TEST_KEY"); err != nil || v != "sekrit" {
t.Errorf("env ref = %q err=%v", v, err)
}
if _, err := ResolveKeyRef("env:HARNESS_TEST_UNSET"); err == nil {
t.Errorf("unset env should error")
}
f := filepath.Join(t.TempDir(), "key")
os.WriteFile(f, []byte(" filekey\n"), 0o600)
if v, err := ResolveKeyRef("file:" + f); err != nil || v != "filekey" {
t.Errorf("file ref = %q err=%v", v, err)
}
if v, err := ResolveKeyRef("literal:abc"); err != nil || v != "abc" {
t.Errorf("literal ref = %q err=%v", v, err)
}
// Secrets must never leak through the redaction path used in errors.
_, err := ResolveKeyRef("super-secret-value")
if err == nil || strings.Contains(err.Error(), "super-secret-value") {
t.Errorf("bare ref error leaks value: %v", err)
}
}
+78
View File
@@ -0,0 +1,78 @@
package config
import (
"fmt"
"os"
"strings"
)
// Secret key references. Values are never logged and never stored by the
// harness; only their resolved bytes reach the outgoing HTTP headers.
//
// Formats:
//
// env:NAME environment variable (must be set and non-empty)
// file:PATH file whose trimmed contents are the key
// literal:VALUE inline key (last resort; still never logged)
// bw:REF bitwarden item (reserved; lands with the bw wrapper, phase 3)
func CheckKeyRef(ref string) error {
switch {
case ref == "":
return fmt.Errorf("empty key reference")
case strings.HasPrefix(ref, "env:"):
if len(ref) <= 4 {
return fmt.Errorf("key ref %q: env: needs a variable name", redact(ref))
}
case strings.HasPrefix(ref, "file:"):
if len(ref) <= 5 {
return fmt.Errorf("key ref %q: file: needs a path", redact(ref))
}
case strings.HasPrefix(ref, "literal:"):
if len(ref) <= 8 {
return fmt.Errorf("key ref: literal: value is empty")
}
case strings.HasPrefix(ref, "bw:"):
return fmt.Errorf("key ref bw: not implemented yet (bitwarden wrapper lands in build phase 3)")
default:
return fmt.Errorf("key ref must use env:, file:, literal:, or bw: prefixes (unrecognized ref redacted)")
}
return nil
}
// ResolveKeyRef resolves a key reference to its value. Never log the result.
func ResolveKeyRef(ref string) (string, error) {
if err := CheckKeyRef(ref); err != nil {
return "", err
}
switch {
case strings.HasPrefix(ref, "env:"):
v := os.Getenv(ref[4:])
if v == "" {
return "", fmt.Errorf("environment variable %s is not set", ref[4:])
}
return v, nil
case strings.HasPrefix(ref, "file:"):
data, err := os.ReadFile(ref[5:])
if err != nil {
return "", fmt.Errorf("read key file: %w", err)
}
v := strings.TrimSpace(string(data))
if v == "" {
return "", fmt.Errorf("key file %s is empty", ref[5:])
}
return v, nil
case strings.HasPrefix(ref, "literal:"):
return ref[8:], nil
}
return "", fmt.Errorf("unreachable key ref branch")
}
// redact masks everything after a recognized prefix so malformed refs can
// be reported without echoing a possibly-pasted secret. Refs without a
// recognizable prefix are never echoed at all.
func redact(ref string) string {
if i := strings.IndexByte(ref, ':'); i >= 0 && len(ref) > i+1 {
return ref[:i+1] + "****"
}
return "<redacted>"
}
+425
View File
@@ -0,0 +1,425 @@
// Package config implements the harness.toml surface for the MOPAC harness.
//
// The TOML parser in toml.go is a deliberate stdlib-only subset: tables,
// bare keys, basic strings, integers, booleans, and (single- or multi-line)
// string/int/bool arrays. Anything richer is a parse error so misconfigured
// files fail loudly instead of silently.
package config
import (
"fmt"
"os"
"sort"
"strings"
)
// TOMLDoc is the parsed representation of harness.toml: nested tables are
// TOMLDoc values; leaves are string, int64, bool, or []any of those.
type TOMLDoc map[string]any
// Table returns the nested table at the given key path, or an empty doc.
func (d TOMLDoc) Table(keys ...string) TOMLDoc {
cur := any(d)
for _, k := range keys {
m, ok := cur.(TOMLDoc)
if !ok {
return TOMLDoc{}
}
cur = m[k]
}
if m, ok := cur.(TOMLDoc); ok {
return m
}
return TOMLDoc{}
}
func (d TOMLDoc) String(key string) (string, bool) {
v, ok := d[key].(string)
return v, ok
}
func (d TOMLDoc) Int(key string) (int64, bool) {
v, ok := d[key].(int64)
return v, ok
}
func (d TOMLDoc) Bool(key string) (bool, bool) {
v, ok := d[key].(bool)
return v, ok
}
func (d TOMLDoc) StringList(key string) ([]string, bool) {
raw, ok := d[key].([]any)
if !ok {
return nil, false
}
out := make([]string, 0, len(raw))
for _, e := range raw {
s, ok := e.(string)
if !ok {
return nil, false
}
out = append(out, s)
}
return out, true
}
// Keys returns the table's key names in sorted order.
func (d TOMLDoc) Keys() []string {
out := make([]string, 0, len(d))
for k := range d {
out = append(out, k)
}
sort.Strings(out)
return out
}
// ParseTOML parses the harness.toml subset.
func ParseTOML(src string) (TOMLDoc, error) {
lines, err := logicalLines(src)
if err != nil {
return nil, err
}
root := TOMLDoc{}
cur := root
for _, ln := range lines {
if strings.HasPrefix(ln.text, "[") {
path, err := parseTableName(ln.text, ln.no)
if err != nil {
return nil, err
}
cur, err = descend(root, path, ln.no)
if err != nil {
return nil, err
}
continue
}
if err := parseKeyValue(cur, ln.text, ln.no); err != nil {
return nil, err
}
}
return root, nil
}
type logicalLine struct {
no int
text string
}
// logicalLines joins physical lines into logical ones: comments are stripped
// (quote-aware), blank lines dropped, and unbalanced array brackets pull the
// next line in so multi-line arrays work. Strings may not span lines.
func logicalLines(src string) ([]logicalLine, error) {
var out []logicalLine
var buf strings.Builder
curNo := 0
depth := 0
for i, raw := range strings.Split(src, "\n") {
no := i + 1
line := stripComment(strings.TrimSuffix(raw, "\r"))
if buf.Len() == 0 {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
curNo = no
buf.WriteString(trimmed)
} else {
buf.WriteString(" ")
buf.WriteString(strings.TrimSpace(line))
}
inStr, esc := false, false
d := 0
for _, r := range line {
switch {
case esc:
esc = false
case inStr && r == '\\':
esc = true
case r == '"':
inStr = !inStr
case !inStr && r == '[':
d++
case !inStr && r == ']':
d--
}
}
if inStr {
return nil, fmt.Errorf("harness.toml:%d: unterminated string", no)
}
if depth+d < 0 {
return nil, fmt.Errorf("harness.toml:%d: unbalanced ']'", no)
}
depth += d
if depth == 0 {
out = append(out, logicalLine{no: curNo, text: buf.String()})
buf.Reset()
}
}
if depth != 0 {
return nil, fmt.Errorf("harness.toml:%d: unterminated array (missing ']')", curNo)
}
return out, nil
}
// stripComment removes a trailing # comment, ignoring # inside strings.
func stripComment(line string) string {
inStr, esc := false, false
for i, r := range line {
switch {
case esc:
esc = false
case inStr && r == '\\':
esc = true
case r == '"':
inStr = !inStr
case r == '#' && !inStr:
return line[:i]
}
}
return line
}
func parseTableName(text string, no int) ([]string, error) {
if !strings.HasSuffix(text, "]") {
return nil, fmt.Errorf("harness.toml:%d: malformed table header %q", no, text)
}
inner := strings.TrimSuffix(strings.TrimPrefix(text, "["), "]")
if inner == "" {
return nil, fmt.Errorf("harness.toml:%d: empty table name", no)
}
parts := strings.Split(inner, ".")
for _, p := range parts {
if !isBareKey(p) {
return nil, fmt.Errorf("harness.toml:%d: invalid table name segment %q", no, p)
}
}
return parts, nil
}
func descend(root TOMLDoc, path []string, no int) (TOMLDoc, error) {
cur := root
for _, k := range path {
if existing, ok := cur[k]; ok {
sub, ok := existing.(TOMLDoc)
if !ok {
return nil, fmt.Errorf("harness.toml:%d: key %q already has a value, cannot become table", no, k)
}
cur = sub
continue
}
sub := TOMLDoc{}
cur[k] = sub
cur = sub
}
return cur, nil
}
func parseKeyValue(m TOMLDoc, text string, no int) error {
eq := strings.IndexByte(text, '=')
if eq < 0 {
return fmt.Errorf("harness.toml:%d: expected key = value, got %q", no, text)
}
key := strings.TrimSpace(text[:eq])
if !isBareKey(key) {
return fmt.Errorf("harness.toml:%d: invalid key %q", no, key)
}
valStr := strings.TrimSpace(text[eq+1:])
if valStr == "" {
return fmt.Errorf("harness.toml:%d: missing value for key %q", no, key)
}
val, err := parseValue(valStr, no)
if err != nil {
return err
}
if _, exists := m[key]; exists {
return fmt.Errorf("harness.toml:%d: duplicate key %q", no, key)
}
m[key] = val
return nil
}
func isBareKey(s string) bool {
if s == "" {
return false
}
for _, r := range s {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '-':
default:
return false
}
}
return true
}
func parseValue(v string, no int) (any, error) {
switch {
case strings.HasPrefix(v, `"`):
return parseStringValue(v, no)
case strings.HasPrefix(v, "["):
return parseArrayValue(v, no)
default:
return parseScalar(v, no)
}
}
func parseScalar(v string, no int) (any, error) {
switch v {
case "true":
return true, nil
case "false":
return false, nil
}
n, err := parseTOMLInt(v)
if err != nil {
return nil, fmt.Errorf("harness.toml:%d: unsupported value %q (want string, int, bool, or array)", no, v)
}
return n, nil
}
func parseTOMLInt(v string) (int64, error) {
s := strings.ReplaceAll(v, "_", "")
return parseSignedInt(s)
}
func parseSignedInt(s string) (int64, error) {
var n int64
i := 0
neg := false
if i < len(s) && (s[i] == '+' || s[i] == '-') {
neg = s[i] == '-'
i++
}
if i >= len(s) {
return 0, fmt.Errorf("not an integer: %q", s)
}
for ; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return 0, fmt.Errorf("not an integer: %q", s)
}
n = n*10 + int64(s[i]-'0')
if n < 0 {
return 0, fmt.Errorf("integer overflow: %q", s)
}
}
if neg {
n = -n
}
return n, nil
}
func parseStringValue(v string, no int) (string, error) {
if len(v) < 2 {
return "", fmt.Errorf("harness.toml:%d: unterminated string %q", no, v)
}
var b strings.Builder
i := 1
for i < len(v) {
c := v[i]
if c == '"' {
if rest := strings.TrimSpace(v[i+1:]); rest != "" {
return "", fmt.Errorf("harness.toml:%d: unexpected trailing content after string: %q", no, rest)
}
return b.String(), nil
}
if c != '\\' {
b.WriteByte(c)
i++
continue
}
if i+1 >= len(v) {
return "", fmt.Errorf("harness.toml:%d: dangling escape in %q", no, v)
}
switch v[i+1] {
case '"':
b.WriteByte('"')
case '\\':
b.WriteByte('\\')
case 'n':
b.WriteByte('\n')
case 't':
b.WriteByte('\t')
case 'r':
b.WriteByte('\r')
default:
return "", fmt.Errorf("harness.toml:%d: unsupported escape \\%c", no, v[i+1])
}
i += 2
}
return "", fmt.Errorf("harness.toml:%d: unterminated string", no)
}
func parseArrayValue(v string, no int) ([]any, error) {
if !strings.HasSuffix(v, "]") {
return nil, fmt.Errorf("harness.toml:%d: unterminated array %q", no, v)
}
inner := strings.TrimSpace(v[1 : len(v)-1])
if inner == "" {
return []any{}, nil
}
var out []any
elems := splitTopLevel(inner, ',')
if len(elems) > 0 && strings.TrimSpace(elems[len(elems)-1]) == "" {
elems = elems[:len(elems)-1] // tolerate a trailing comma
}
for _, elem := range elems {
elem = strings.TrimSpace(elem)
if elem == "" {
return nil, fmt.Errorf("harness.toml:%d: empty array element in %q", no, v)
}
val, err := parseValue(elem, no)
if err != nil {
return nil, err
}
out = append(out, val)
}
return out, nil
}
// splitTopLevel splits on sep outside of double-quoted strings.
func splitTopLevel(s string, sep rune) []string {
var out []string
var cur strings.Builder
inStr, esc := false, false
for _, r := range s {
switch {
case esc:
esc = false
cur.WriteRune(r)
case inStr && r == '\\':
esc = true
cur.WriteRune(r)
case r == '"':
inStr = !inStr
cur.WriteRune(r)
case r == sep && !inStr:
out = append(out, cur.String())
cur.Reset()
default:
cur.WriteRune(r)
}
}
out = append(out, cur.String())
return out
}
// Load reads, parses, and validates harness.toml at path.
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("config: read %s: %w", path, err)
}
doc, err := ParseTOML(string(data))
if err != nil {
return nil, fmt.Errorf("config: %w", err)
}
cfg := Default()
if err := cfg.apply(doc); err != nil {
return nil, fmt.Errorf("config: %w", err)
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("config: %w", err)
}
return cfg, nil
}
+139
View File
@@ -0,0 +1,139 @@
package config
import (
"strings"
"testing"
)
func TestParseTOMLValid(t *testing.T) {
src := `
# top comment
vertical = "demo" # trailing comment
work_root = "."
[loop]
max_rounds = 4
[redmine]
url = "https://rm.example.org"
scope_query_id = 42
class_field = "Class"
[litellm]
base_url = "http://h:4001"
timeout_secs = 30
max_retries = 3
[models]
mopac-study = "glm-4.7-flash" # hyphenated bare key
default_tier = "mopac-study"
[models.classes]
study = "mopac-study"
code = "mopac-code"
[tools.bash]
enabled = false
default = "deny"
allow = ["pwd", "ls *", "cat *"]
deny = [
"sudo *", # comment inside array
"ssh *",
]
[deep.nested.table]
flag = true
note = "quote \" and # inside string"
empty = []
`
doc, err := ParseTOML(src)
if err != nil {
t.Fatalf("ParseTOML: %v", err)
}
tests := []struct {
name string
got any
want any
}{
{"vertical", str(doc.String("vertical")), "demo"},
{"work_root", str(doc.String("work_root")), "."},
{"max_rounds", intg(doc.Table("loop").Int("max_rounds")), int64(4)},
{"scope_query_id", intg(doc.Table("redmine").Int("scope_query_id")), int64(42)},
{"class_field", str(doc.Table("redmine").String("class_field")), "Class"},
{"timeout_secs", intg(doc.Table("litellm").Int("timeout_secs")), int64(30)},
{"max_retries", intg(doc.Table("litellm").Int("max_retries")), int64(3)},
{"tier", str(doc.Table("models").String("mopac-study")), "glm-4.7-flash"},
{"default_tier", str(doc.Table("models").String("default_tier")), "mopac-study"},
{"class map", str(doc.Table("models", "classes").String("study")), "mopac-study"},
{"bash enabled", boolean(doc.Table("tools", "bash").Bool("enabled")), false},
{"deep bool", boolean(doc.Table("deep", "nested", "table").Bool("flag")), true},
{"escaped string", str(doc.Table("deep", "nested", "table").String("note")), `quote " and # inside string`},
}
for _, tc := range tests {
if tc.got != tc.want {
t.Errorf("%s = %v, want %v", tc.name, tc.got, tc.want)
}
}
allow, ok := doc.Table("tools", "bash").StringList("allow")
if !ok || len(allow) != 3 || allow[1] != "ls *" {
t.Errorf("allow list = %v ok=%v", allow, ok)
}
deny, ok := doc.Table("tools", "bash").StringList("deny")
if !ok || len(deny) != 2 || deny[0] != "sudo *" {
t.Errorf("deny list = %v ok=%v", deny, ok)
}
if empty, ok := doc.Table("deep", "nested", "table").StringList("empty"); !ok || len(empty) != 0 {
t.Errorf("empty array = %v ok=%v", empty, ok)
}
}
func str(v string, ok bool) string {
if !ok {
panic("string key missing")
}
return v
}
func intg(v int64, ok bool) int64 {
if !ok {
panic("int key missing")
}
return v
}
func boolean(v bool, ok bool) bool {
if !ok {
panic("bool key missing")
}
return v
}
func TestParseTOMLErrors(t *testing.T) {
cases := []struct {
name string
src string
want string
}{
{"unterminated string", `a = "oops`, "unterminated string"},
{"bare value", `a = sk-123`, "unsupported value"},
{"missing value", `a =`, "missing value"},
{"bad key", `a b = "c"`, "invalid key"},
{"duplicate key", "a = \"1\"\na = \"2\"", "duplicate key"},
{"scalar to table", "a = \"1\"\n[a.b]", "cannot become table"},
{"unterminated array", "a = [\n\"x\",", "unterminated array"},
{"bad table name", `[a.]`, "invalid table name"},
{"no equals", `[t]\njustakey`, "expected key = value"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := ParseTOML(strings.ReplaceAll(tc.src, `\n`, "\n"))
if err == nil {
t.Fatalf("expected error containing %q, got nil", tc.want)
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error %q does not contain %q", err, tc.want)
}
})
}
}