config: keyproxy mpk refs, loop daemon knobs, redmine status map, gitea section
mpk: key refs resolve through the ukrrs/mopac-keyproxy hop ([keyproxy]
url + token_ref, POST /v1/resolve, short in-memory cache); env:/file:/
literal: refs keep working untouched, so the harness runs with or
without keyproxy up. [loop] gains poll_interval_secs + state_dir for the
self-host daemon; [redmine.status_map] (quoted TOML keys) drives status
transitions; [gitea] carries the optional REPORT-commit step, off by
default. All hosts live in harness.toml, none in code.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
+120
-2
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config is the full harness.toml surface. Defaults live in Default();
|
||||
@@ -17,10 +18,16 @@ type Config struct {
|
||||
Bash BashConfig
|
||||
Demo DemoConfig
|
||||
Events EventsConfig
|
||||
KeyProxy KeyProxyConfig
|
||||
Gitea GiteaConfig
|
||||
}
|
||||
|
||||
// LoopConfig is both the per-turn bound and the `harness loop` daemon
|
||||
// cadence + state location.
|
||||
type LoopConfig struct {
|
||||
MaxRounds int
|
||||
MaxRounds int
|
||||
PollIntervalSecs int // loop: Redmine scan interval (default 120)
|
||||
StateDir string // loop: append-only loop.jsonl + dedup index
|
||||
}
|
||||
|
||||
type RedmineConfig struct {
|
||||
@@ -31,6 +38,10 @@ type RedmineConfig struct {
|
||||
ClassField string
|
||||
DefaultClass string
|
||||
Limit int
|
||||
// StatusMap: issue status name -> status name to set after a REPORT
|
||||
// lands (loop path only; e.g. [redmine.status_map] "In Progress" =
|
||||
// "Done"). Empty map = leave status alone.
|
||||
StatusMap map[string]string
|
||||
}
|
||||
|
||||
type LiteLLMConfig struct {
|
||||
@@ -65,6 +76,26 @@ type DemoConfig struct {
|
||||
Class string
|
||||
}
|
||||
|
||||
// KeyProxyConfig is the ukrrs/mopac-keyproxy resolve hop: mpk: key refs
|
||||
// POST here. All hosts/paths live in this config, never in code.
|
||||
type KeyProxyConfig struct {
|
||||
URL string // hop base url (e.g. http://127.0.0.1:8082)
|
||||
TokenRef string // local ref (env:/file:/literal:) holding the bearer token
|
||||
CacheTTLSecs int // in-memory mpk resolution cache (default 60)
|
||||
}
|
||||
|
||||
// GiteaConfig is the optional REPORT-commit step (off by default). When
|
||||
// commit_reports is true the loop commits each REPORT file to the
|
||||
// configured repo right after writing it.
|
||||
type GiteaConfig struct {
|
||||
URL string
|
||||
KeyRef string
|
||||
Owner string
|
||||
Repo string
|
||||
Branch string
|
||||
CommitReports bool
|
||||
}
|
||||
|
||||
// EventsConfig is the `harness events` webhook receiver surface.
|
||||
type EventsConfig struct {
|
||||
Listen string // bind address (publish via docker -p)
|
||||
@@ -87,11 +118,16 @@ func Default() *Config {
|
||||
return &Config{
|
||||
WorkRoot: ".",
|
||||
ReportDir: "reports",
|
||||
Loop: LoopConfig{MaxRounds: 8},
|
||||
Loop: LoopConfig{
|
||||
MaxRounds: 8,
|
||||
PollIntervalSecs: 120,
|
||||
StateDir: "state/loop",
|
||||
},
|
||||
Redmine: RedmineConfig{
|
||||
ClassField: "Class",
|
||||
DefaultClass: "primary",
|
||||
Limit: 50,
|
||||
StatusMap: map[string]string{},
|
||||
},
|
||||
LiteLLM: LiteLLMConfig{TimeoutSecs: 120, MaxRetries: 2},
|
||||
Models: ModelsConfig{
|
||||
@@ -110,6 +146,7 @@ func Default() *Config {
|
||||
SecretHeader: "X-Discourse-Webhook-Secret",
|
||||
},
|
||||
},
|
||||
KeyProxy: KeyProxyConfig{CacheTTLSecs: 60},
|
||||
Demo: DemoConfig{
|
||||
ID: "demo-1",
|
||||
Subject: "MVP demo: GLM self-description",
|
||||
@@ -132,6 +169,12 @@ func (c *Config) apply(doc TOMLDoc) error {
|
||||
if v, ok := doc.Table("loop").Int("max_rounds"); ok {
|
||||
c.Loop.MaxRounds = int(v)
|
||||
}
|
||||
if v, ok := doc.Table("loop").Int("poll_interval_secs"); ok {
|
||||
c.Loop.PollIntervalSecs = int(v)
|
||||
}
|
||||
if v, ok := doc.Table("loop").String("state_dir"); ok {
|
||||
c.Loop.StateDir = v
|
||||
}
|
||||
|
||||
rm := doc.Table("redmine")
|
||||
if v, ok := rm.String("url"); ok {
|
||||
@@ -155,6 +198,14 @@ func (c *Config) apply(doc TOMLDoc) error {
|
||||
if v, ok := rm.Int("limit"); ok {
|
||||
c.Redmine.Limit = int(v)
|
||||
}
|
||||
for _, k := range doc.Table("redmine", "status_map").Keys() {
|
||||
if v, ok := doc.Table("redmine", "status_map").String(k); ok {
|
||||
if c.Redmine.StatusMap == nil {
|
||||
c.Redmine.StatusMap = map[string]string{}
|
||||
}
|
||||
c.Redmine.StatusMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
lt := doc.Table("litellm")
|
||||
if v, ok := lt.String("base_url"); ok {
|
||||
@@ -230,6 +281,37 @@ func (c *Config) apply(doc TOMLDoc) error {
|
||||
c.Demo.Class = v
|
||||
}
|
||||
|
||||
kp := doc.Table("keyproxy")
|
||||
if v, ok := kp.String("url"); ok {
|
||||
c.KeyProxy.URL = v
|
||||
}
|
||||
if v, ok := kp.String("token_ref"); ok {
|
||||
c.KeyProxy.TokenRef = v
|
||||
}
|
||||
if v, ok := kp.Int("cache_ttl_secs"); ok {
|
||||
c.KeyProxy.CacheTTLSecs = int(v)
|
||||
}
|
||||
|
||||
gt := doc.Table("gitea")
|
||||
if v, ok := gt.String("url"); ok {
|
||||
c.Gitea.URL = v
|
||||
}
|
||||
if v, ok := gt.String("key_ref"); ok {
|
||||
c.Gitea.KeyRef = v
|
||||
}
|
||||
if v, ok := gt.String("owner"); ok {
|
||||
c.Gitea.Owner = v
|
||||
}
|
||||
if v, ok := gt.String("repo"); ok {
|
||||
c.Gitea.Repo = v
|
||||
}
|
||||
if v, ok := gt.String("branch"); ok {
|
||||
c.Gitea.Branch = v
|
||||
}
|
||||
if v, ok := gt.Bool("commit_reports"); ok {
|
||||
c.Gitea.CommitReports = v
|
||||
}
|
||||
|
||||
ev := doc.Table("events")
|
||||
if v, ok := ev.String("listen"); ok {
|
||||
c.Events.Listen = v
|
||||
@@ -327,5 +409,41 @@ func (c *Config) Validate() error {
|
||||
if c.Events.Listen == "" || c.Events.StateDir == "" {
|
||||
return fmt.Errorf("[events]: listen and state_dir must not be empty")
|
||||
}
|
||||
|
||||
if c.Loop.PollIntervalSecs < 1 {
|
||||
return fmt.Errorf("[loop]: poll_interval_secs must be >= 1")
|
||||
}
|
||||
if c.Loop.StateDir == "" {
|
||||
return fmt.Errorf("[loop]: state_dir must not be empty")
|
||||
}
|
||||
|
||||
// Keyproxy is optional; if any piece is set, url + token_ref must be.
|
||||
if c.KeyProxy.URL != "" || c.KeyProxy.TokenRef != "" {
|
||||
if c.KeyProxy.URL == "" {
|
||||
return fmt.Errorf("[keyproxy]: url is required when keyproxy is configured")
|
||||
}
|
||||
if c.KeyProxy.TokenRef == "" {
|
||||
return fmt.Errorf("[keyproxy]: token_ref is required when keyproxy is configured (the bearer token for /v1/resolve)")
|
||||
}
|
||||
if err := CheckKeyRef(c.KeyProxy.TokenRef); err != nil {
|
||||
return fmt.Errorf("[keyproxy]: %w", err)
|
||||
}
|
||||
if strings.HasPrefix(c.KeyProxy.TokenRef, "mpk:") {
|
||||
return fmt.Errorf("[keyproxy]: token_ref must be a local ref (env:/file:/literal:), not mpk:")
|
||||
}
|
||||
if c.KeyProxy.CacheTTLSecs < 1 {
|
||||
return fmt.Errorf("[keyproxy]: bad cache_ttl_secs")
|
||||
}
|
||||
}
|
||||
|
||||
// Gitea REPORT commit is optional; when on, the minimum set must be set.
|
||||
if c.Gitea.CommitReports {
|
||||
if c.Gitea.URL == "" || c.Gitea.KeyRef == "" || c.Gitea.Owner == "" || c.Gitea.Repo == "" {
|
||||
return fmt.Errorf("[gitea]: url, key_ref, owner and repo are required when commit_reports = true")
|
||||
}
|
||||
if err := CheckKeyRef(c.Gitea.KeyRef); err != nil {
|
||||
return fmt.Errorf("[gitea]: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user