@@ -5,10 +5,8 @@ import (
55 "fmt"
66 "io"
77 "log/slog"
8- "maps"
98 "os"
109 "path/filepath"
11- "slices"
1210 "strconv"
1311 "strings"
1412 "time"
@@ -38,6 +36,18 @@ const (
3836// allowlisting. Order is stable (runner, agent, chat) for error messages.
3937var allowedBackendNames = []string {BackendNameRunner , BackendNameAgent , BackendNameChat }
4038
39+ // backendEnvSuffixes are the per-entry CONTEXTMATRIX_BACKEND_<NAME>_* env
40+ // var suffixes. applyEnvOverrides reads each one; checkBackendEnvKeys
41+ // allowlists the same set — keep the two in sync via this list.
42+ var backendEnvSuffixes = []string {
43+ "_URL" ,
44+ "_API_KEY" ,
45+ "_ENABLED" ,
46+ "_ORCHESTRATOR_SONNET_MODEL" ,
47+ "_ORCHESTRATOR_OPUS_MODEL" ,
48+ "_RECONCILE_INTERVAL" ,
49+ }
50+
4151// BackendConfig is one entry in the backends map: an execution backend CM
4252// can drive over the contextmatrix-protocol webhook contract. Read once at
4353// startup — changing backends requires a CM restart.
@@ -581,7 +591,7 @@ func Load(path string) (*Config, error) {
581591 return nil , err
582592 }
583593
584- if err := checkBackendEnvKeys (cfg ); err != nil {
594+ if err := checkBackendEnvKeys (); err != nil {
585595 return nil , err
586596 }
587597
@@ -618,7 +628,7 @@ func Load(path string) (*Config, error) {
618628 return nil , err
619629 }
620630
621- if err := checkBackendEnvKeys (cfg ); err != nil {
631+ if err := checkBackendEnvKeys (); err != nil {
622632 return nil , err
623633 }
624634
@@ -950,9 +960,27 @@ func applyEnvOverrides(cfg *Config) error {
950960 }
951961 }
952962
953- for name , b := range cfg .Backends {
963+ // Backend entries can be configured entirely via env: a variable for one
964+ // of the allowed names creates the entry when YAML does not declare it,
965+ // so pure-env deployments need no backends stub in the config file.
966+ for _ , name := range allowedBackendNames {
954967 prefix := "CONTEXTMATRIX_BACKEND_" + strings .ToUpper (name )
955968
969+ anySet := false
970+
971+ for _ , suffix := range backendEnvSuffixes {
972+ if os .Getenv (prefix + suffix ) != "" {
973+ anySet = true
974+
975+ break
976+ }
977+ }
978+
979+ b , declared := cfg .Backends [name ]
980+ if ! declared && ! anySet {
981+ continue
982+ }
983+
956984 if v := os .Getenv (prefix + "_URL" ); v != "" {
957985 b .URL = v
958986 }
@@ -985,29 +1013,29 @@ func applyEnvOverrides(cfg *Config) error {
9851013 b .ReconcileInterval = v
9861014 }
9871015
1016+ if cfg .Backends == nil {
1017+ cfg .Backends = map [string ]BackendConfig {}
1018+ }
1019+
9881020 cfg .Backends [name ] = b
9891021 }
9901022
9911023 return nil
9921024}
9931025
9941026// checkBackendEnvKeys rejects CONTEXTMATRIX_BACKEND_<NAME>_* variables that
995- // do not map to a known (name, suffix) pair. The backend name must be in the
996- // closed set (runner, agent, chat) AND be declared in cfg.Backends; the suffix
997- // must be one of the per-entry fields below. A typo'd or stale variable must
998- // fail loudly, not silently configure nothing. NOTE: when adding a new
999- // per-backend env field to applyEnvOverrides, add it to the known set here too.
1000- func checkBackendEnvKeys (cfg * Config ) error {
1027+ // do not map to a known (name, suffix) pair: the name must be in the closed
1028+ // set (runner, agent, chat) and the suffix in backendEnvSuffixes. A typo'd
1029+ // or stale variable must fail loudly, not silently configure nothing. The
1030+ // entry does not need to be declared in YAML — applyEnvOverrides creates it.
1031+ func checkBackendEnvKeys () error {
10011032 known := map [string ]bool {}
10021033
1003- for name := range cfg . Backends {
1034+ for _ , name := range allowedBackendNames {
10041035 pfx := "CONTEXTMATRIX_BACKEND_" + strings .ToUpper (name )
1005- known [pfx + "_URL" ] = true
1006- known [pfx + "_API_KEY" ] = true
1007- known [pfx + "_ENABLED" ] = true
1008- known [pfx + "_ORCHESTRATOR_SONNET_MODEL" ] = true
1009- known [pfx + "_ORCHESTRATOR_OPUS_MODEL" ] = true
1010- known [pfx + "_RECONCILE_INTERVAL" ] = true
1036+ for _ , suffix := range backendEnvSuffixes {
1037+ known [pfx + suffix ] = true
1038+ }
10111039 }
10121040
10131041 for _ , kv := range os .Environ () {
@@ -1017,11 +1045,11 @@ func checkBackendEnvKeys(cfg *Config) error {
10171045 }
10181046
10191047 if ! known [key ] {
1020- return fmt .Errorf ("%s references no configured backend " +
1021- "(allowed names: %s; declared : %v ) — fix the variable name or add the backends entry " ,
1048+ return fmt .Errorf ("%s is not a recognised backend env var " +
1049+ "(names: %s; suffixes : %s ) — fix the variable name" ,
10221050 key ,
10231051 strings .Join (allowedBackendNames , ", " ),
1024- slices . Sorted ( maps . Keys ( cfg . Backends ) ))
1052+ strings . Join ( backendEnvSuffixes , ", " ))
10251053 }
10261054 }
10271055
0 commit comments