@@ -4,15 +4,92 @@ import (
44 "context"
55 "errors"
66 "fmt"
7+ "strings"
8+ "time"
79
10+ "github.com/threatwinds/go-sdk/catcher"
811 "github.com/utmstack/utmstack/backend/modules/socai/dto"
912 "github.com/utmstack/utmstack/backend/modules/socai/repository"
1013 "github.com/utmstack/utmstack/backend/modules/socai/verifier"
14+ "github.com/utmstack/utmstack/backend/pkg/instanceconfig"
1115 "github.com/utmstack/utmstack/backend/pkg/secret"
1216)
1317
1418var ErrVerificationFailed = errors .New ("connection verification failed" )
1519
20+ // threatWindsAIPath is the CM proxy path for the AI chat-completions endpoint
21+ // (see backend/modules/threatintel: same reverse-proxy target, same id/key auth).
22+ const threatWindsAIPath = "/proxy/api/ai/v1/chat/completions"
23+ const threatWindsDefaultModel = "silas-1.0"
24+
25+ var ErrInstanceNotRegistered = errors .New ("instance not registered yet — cannot use the ThreatWinds provider" )
26+
27+ const ensureDefaultRetryInterval = 30 * time .Second
28+
29+ // StartEnsureDefaultLoop provisions the default ThreatWinds config in the
30+ // background, retrying until it succeeds. A fresh install's backend can
31+ // start before the updater service (a separate host process installed after
32+ // the Docker stack — see installer/install.go) finishes registering the
33+ // instance and writing instance-config.yml, so a single boot-time attempt
34+ // isn't enough; this keeps checking every 30s until the instance is
35+ // registered or a config already exists.
36+ func (s * ConfigService ) StartEnsureDefaultLoop () {
37+ if s .EnsureDefault () {
38+ return
39+ }
40+ go func () {
41+ for range time .Tick (ensureDefaultRetryInterval ) {
42+ if s .EnsureDefault () {
43+ return
44+ }
45+ }
46+ }()
47+ }
48+
49+ func (s * ConfigService ) EnsureDefault () bool {
50+ existing , err := s .store .Load ()
51+ if err != nil {
52+ catcher .Warn ("socai: failed to check existing config for default provisioning" , map [string ]any {"error" : err .Error ()})
53+ return false
54+ }
55+ if existing != nil {
56+ return true
57+ }
58+
59+ inst := instanceconfig .Get ()
60+ if inst == nil || inst .Server == "" || inst .InstanceID == "" || inst .InstanceKey == "" {
61+ return false
62+ }
63+
64+ idEnc , err := s .cipher .Encrypt (inst .InstanceID )
65+ if err != nil {
66+ catcher .Warn ("socai: failed to encrypt default ThreatWinds credentials" , map [string ]any {"error" : err .Error ()})
67+ return false
68+ }
69+ keyEnc , err := s .cipher .Encrypt (inst .InstanceKey )
70+ if err != nil {
71+ catcher .Warn ("socai: failed to encrypt default ThreatWinds credentials" , map [string ]any {"error" : err .Error ()})
72+ return false
73+ }
74+
75+ fc := & repository.FileConfig {
76+ Provider : "threatwinds" ,
77+ Model : threatWindsDefaultModel ,
78+ URL : strings .TrimRight (inst .Server , "/" ) + threatWindsAIPath ,
79+ AuthType : "none" ,
80+ CustomHeaders : map [string ]string {"id" : idEnc , "key" : keyEnc },
81+ MaxTokens : 4096 ,
82+ MaxToolIterations : 12 ,
83+ AutoAnalyze : true ,
84+ }
85+ if err := s .store .Save (fc ); err != nil {
86+ catcher .Warn ("socai: failed to save default ThreatWinds config" , map [string ]any {"error" : err .Error ()})
87+ return false
88+ }
89+ catcher .Info ("socai: auto-configured default ThreatWinds provider" , nil )
90+ return true
91+ }
92+
1693// connectionVerifier pings the LLM endpoint with the candidate credentials.
1794type connectionVerifier interface {
1895 Verify (ctx context.Context , c verifier.Config ) error
@@ -94,14 +171,29 @@ func (s *ConfigService) Update(ctx context.Context, req dto.ConfigRequest) (*dto
94171 authType = "bearer"
95172 }
96173
174+ url := req .URL
175+ authHeaderName := req .AuthHeaderName
176+
177+ if req .Provider == "threatwinds" {
178+ inst := instanceconfig .Get ()
179+ if inst == nil || inst .Server == "" || inst .InstanceID == "" || inst .InstanceKey == "" {
180+ return nil , ErrInstanceNotRegistered
181+ }
182+ url = strings .TrimRight (inst .Server , "/" ) + threatWindsAIPath
183+ authType = "none"
184+ authHeaderName = ""
185+ apiKeyPlain = ""
186+ headersPlain = map [string ]string {"id" : inst .InstanceID , "key" : inst .InstanceKey }
187+ }
188+
97189 // 2. Verify the connection before writing anything.
98190 if err := s .verifier .Verify (ctx , verifier.Config {
99191 Provider : req .Provider ,
100- URL : req . URL ,
192+ URL : url ,
101193 Model : req .Model ,
102194 APIKey : apiKeyPlain ,
103195 AuthType : authType ,
104- AuthHeaderName : req . AuthHeaderName ,
196+ AuthHeaderName : authHeaderName ,
105197 CustomHeaders : headersPlain ,
106198 }); err != nil {
107199 return nil , fmt .Errorf ("%w: %v" , ErrVerificationFailed , err )
@@ -136,10 +228,10 @@ func (s *ConfigService) Update(ctx context.Context, req dto.ConfigRequest) (*dto
136228 fc := & repository.FileConfig {
137229 Provider : req .Provider ,
138230 Model : req .Model ,
139- URL : req . URL ,
231+ URL : url ,
140232 APIKey : apiKeyEnc ,
141233 AuthType : authType ,
142- AuthHeaderName : req . AuthHeaderName ,
234+ AuthHeaderName : authHeaderName ,
143235 CustomHeaders : headersEnc ,
144236 MaxTokens : maxTokens ,
145237 MaxToolIterations : maxIters ,
0 commit comments