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,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
|
||||
}
|
||||
Reference in New Issue
Block a user