|
1
|
// Package config defines scuttlebot's configuration schema. |
|
2
|
package config |
|
3
|
|
|
4
|
import ( |
|
5
|
"fmt" |
|
6
|
"os" |
|
7
|
"time" |
|
8
|
|
|
9
|
"gopkg.in/yaml.v3" |
|
10
|
) |
|
11
|
|
|
12
|
// Config is the top-level scuttlebot configuration. |
|
13
|
type Config struct { |
|
14
|
Ergo ErgoConfig `yaml:"ergo"` |
|
15
|
Datastore DatastoreConfig `yaml:"datastore"` |
|
16
|
Bridge BridgeConfig `yaml:"bridge"` |
|
17
|
TLS TLSConfig `yaml:"tls"` |
|
18
|
LLM LLMConfig `yaml:"llm"` |
|
19
|
Topology TopologyConfig `yaml:"topology"` |
|
20
|
History ConfigHistoryConfig `yaml:"config_history"` |
|
21
|
AgentPolicy AgentPolicyConfig `yaml:"agent_policy" json:"agent_policy"` |
|
22
|
Logging LoggingConfig `yaml:"logging" json:"logging"` |
|
23
|
|
|
24
|
// APIAddr is the address for scuttlebot's own HTTP management API. |
|
25
|
// Ignored when TLS.Domain is set (HTTPS runs on :443, HTTP on :80). |
|
26
|
// Default: ":8080" |
|
27
|
APIAddr string `yaml:"api_addr"` |
|
28
|
|
|
29
|
// MCPAddr is the address for the MCP server. |
|
30
|
// Default: ":8081" |
|
31
|
MCPAddr string `yaml:"mcp_addr"` |
|
32
|
} |
|
33
|
|
|
34
|
// AgentPolicyConfig defines requirements applied to all registering agents. |
|
35
|
type AgentPolicyConfig struct { |
|
36
|
RequireCheckin bool `yaml:"require_checkin" json:"require_checkin"` |
|
37
|
CheckinChannel string `yaml:"checkin_channel" json:"checkin_channel"` |
|
38
|
RequiredChannels []string `yaml:"required_channels" json:"required_channels"` |
|
39
|
} |
|
40
|
|
|
41
|
// LoggingConfig configures message logging. |
|
42
|
type LoggingConfig struct { |
|
43
|
Enabled bool `yaml:"enabled" json:"enabled"` |
|
44
|
Dir string `yaml:"dir" json:"dir"` |
|
45
|
Format string `yaml:"format" json:"format"` // "jsonl" | "csv" | "text" |
|
46
|
Rotation string `yaml:"rotation" json:"rotation"` // "none" | "daily" | "weekly" | "size" |
|
47
|
MaxSizeMB int `yaml:"max_size_mb" json:"max_size_mb"` |
|
48
|
PerChannel bool `yaml:"per_channel" json:"per_channel"` |
|
49
|
MaxAgeDays int `yaml:"max_age_days" json:"max_age_days"` |
|
50
|
} |
|
51
|
|
|
52
|
// ConfigHistoryConfig controls config write-back history retention. |
|
53
|
type ConfigHistoryConfig struct { |
|
54
|
// Keep is the number of config snapshots to retain in Dir. |
|
55
|
// 0 disables history. Default: 20. |
|
56
|
Keep int `yaml:"keep" json:"keep"` |
|
57
|
|
|
58
|
// Dir is the directory for config snapshots. |
|
59
|
// Default: {ergo.data_dir}/config-history |
|
60
|
Dir string `yaml:"dir" json:"dir,omitempty"` |
|
61
|
} |
|
62
|
|
|
63
|
// LLMConfig configures the omnibus LLM gateway used by oracle and any other |
|
64
|
// bot or service that needs language model access. |
|
65
|
type LLMConfig struct { |
|
66
|
// Backends is the list of configured LLM backends. |
|
67
|
// Each backend has a unique Name used to reference it from bot configs. |
|
68
|
Backends []LLMBackendConfig `yaml:"backends"` |
|
69
|
} |
|
70
|
|
|
71
|
// LLMBackendConfig configures a single LLM backend instance. |
|
72
|
type LLMBackendConfig struct { |
|
73
|
// Name is a unique identifier for this backend (e.g. "openai-main", "local-ollama"). |
|
74
|
// Used when referencing the backend from bot configs. |
|
75
|
Name string `yaml:"name"` |
|
76
|
|
|
77
|
// Backend is the provider type. Supported values: |
|
78
|
// Native: anthropic, gemini, bedrock, ollama |
|
79
|
// OpenAI-compatible: openai, openrouter, together, groq, fireworks, mistral, |
|
80
|
// ai21, huggingface, deepseek, cerebras, xai, |
|
81
|
// litellm, lmstudio, jan, localai, vllm, anythingllm |
|
82
|
Backend string `yaml:"backend"` |
|
83
|
|
|
84
|
// APIKey is the authentication key for cloud backends. |
|
85
|
APIKey string `yaml:"api_key"` |
|
86
|
|
|
87
|
// BaseURL overrides the default base URL for OpenAI-compatible backends. |
|
88
|
// Required for custom self-hosted endpoints without a known default. |
|
89
|
BaseURL string `yaml:"base_url"` |
|
90
|
|
|
91
|
// Model is the default model ID. If empty, the first discovered model |
|
92
|
// that passes the allow/block filter is used. |
|
93
|
Model string `yaml:"model"` |
|
94
|
|
|
95
|
// Region is the AWS region (e.g. "us-east-1"). Bedrock only. |
|
96
|
Region string `yaml:"region"` |
|
97
|
|
|
98
|
// AWSKeyID is the AWS access key ID. Bedrock only. |
|
99
|
AWSKeyID string `yaml:"aws_key_id"` |
|
100
|
|
|
101
|
// AWSSecretKey is the AWS secret access key. Bedrock only. |
|
102
|
AWSSecretKey string `yaml:"aws_secret_key"` |
|
103
|
|
|
104
|
// Allow is a list of regex patterns. If non-empty, only model IDs matching |
|
105
|
// at least one pattern are returned by model discovery. |
|
106
|
Allow []string `yaml:"allow"` |
|
107
|
|
|
108
|
// Block is a list of regex patterns. Matching model IDs are excluded |
|
109
|
// from model discovery results. |
|
110
|
Block []string `yaml:"block"` |
|
111
|
|
|
112
|
// Default marks this backend as the one used when no backend is specified |
|
113
|
// in a bot's config. Only one backend should have Default: true. |
|
114
|
Default bool `yaml:"default"` |
|
115
|
} |
|
116
|
|
|
117
|
// TLSConfig configures automatic HTTPS via Let's Encrypt. |
|
118
|
type TLSConfig struct { |
|
119
|
// Domain enables TLS. When set, scuttlebot obtains a certificate from |
|
120
|
// Let's Encrypt for this domain and serves HTTPS on :443. |
|
121
|
Domain string `yaml:"domain"` |
|
122
|
|
|
123
|
// Email is sent to Let's Encrypt for certificate expiry notifications. |
|
124
|
Email string `yaml:"email"` |
|
125
|
|
|
126
|
// CertDir is the directory for the certificate cache. |
|
127
|
// Default: {Ergo.DataDir}/certs |
|
128
|
CertDir string `yaml:"cert_dir"` |
|
129
|
|
|
130
|
// AllowInsecure keeps plain HTTP running on :80 alongside HTTPS. |
|
131
|
// The ACME HTTP-01 challenge always runs on :80 regardless. |
|
132
|
// Default: true |
|
133
|
AllowInsecure bool `yaml:"allow_insecure"` |
|
134
|
} |
|
135
|
|
|
136
|
// ErgoConfig holds settings for the managed Ergo IRC server. |
|
137
|
type ErgoConfig struct { |
|
138
|
// External disables subprocess management. When true, scuttlebot expects |
|
139
|
// ergo to already be running and reachable at APIAddr and IRCAddr. |
|
140
|
// Use this in Docker/K8s deployments where ergo runs as a separate container. |
|
141
|
External bool `yaml:"external"` |
|
142
|
|
|
143
|
// BinaryPath is the path to the ergo binary. Defaults to "ergo" (looks in PATH). |
|
144
|
// Unused when External is true. |
|
145
|
BinaryPath string `yaml:"binary_path"` |
|
146
|
|
|
147
|
// DataDir is the directory where Ergo stores ircd.db and generated config. |
|
148
|
// Unused when External is true. |
|
149
|
DataDir string `yaml:"data_dir"` |
|
150
|
|
|
151
|
// NetworkName is the human-readable IRC network name. |
|
152
|
NetworkName string `yaml:"network_name"` |
|
153
|
|
|
154
|
// ServerName is the IRC server hostname (e.g. "irc.example.com"). |
|
155
|
ServerName string `yaml:"server_name"` |
|
156
|
|
|
157
|
// IRCAddr is the address Ergo listens for IRC connections on. |
|
158
|
// Default: "127.0.0.1:6667" (loopback plaintext for private networks). |
|
159
|
// Set to ":6667" or ":6697" to accept connections from outside the host. |
|
160
|
IRCAddr string `yaml:"irc_addr"` |
|
161
|
|
|
162
|
// APIAddr is the address of Ergo's HTTP management API. |
|
163
|
// Default: "127.0.0.1:8089" (loopback only). |
|
164
|
APIAddr string `yaml:"api_addr"` |
|
165
|
|
|
166
|
// APIToken is the bearer token for Ergo's HTTP API. |
|
167
|
// scuttlebot generates this on first start and stores it. |
|
168
|
APIToken string `yaml:"api_token"` |
|
169
|
|
|
170
|
// RequireSASL enforces SASL authentication for all IRC connections. |
|
171
|
// When true, only agents and users with registered NickServ accounts |
|
172
|
// can connect. Recommended for public deployments. |
|
173
|
// Default: false |
|
174
|
RequireSASL bool `yaml:"require_sasl"` |
|
175
|
|
|
176
|
// DefaultChannelModes sets channel modes applied when a new channel is |
|
177
|
// created. Common values: "+n" (no external messages), "+Rn" (registered |
|
178
|
// users only). Default: "+n" |
|
179
|
DefaultChannelModes string `yaml:"default_channel_modes"` |
|
180
|
|
|
181
|
// TLSDomain enables a public TLS listener on TLSAddr with Let's Encrypt |
|
182
|
// (ACME TLS-ALPN-01). When set, IRCAddr is kept as an internal plaintext |
|
183
|
// listener for system bots, and a second TLS listener is added for |
|
184
|
// external agents. Leave empty to use IRCAddr as the only listener. |
|
185
|
TLSDomain string `yaml:"tls_domain"` |
|
186
|
|
|
187
|
// TLSAddr is the address for the public TLS IRC listener. |
|
188
|
// Only used when TLSDomain is set. Default: "0.0.0.0:6697" |
|
189
|
TLSAddr string `yaml:"tls_addr"` |
|
190
|
|
|
191
|
// History configures persistent message history storage. |
|
192
|
History HistoryConfig `yaml:"history"` |
|
193
|
} |
|
194
|
|
|
195
|
// HistoryConfig configures Ergo's persistent message history. |
|
196
|
type HistoryConfig struct { |
|
197
|
// Enabled enables persistent history storage. |
|
198
|
Enabled bool `yaml:"enabled"` |
|
199
|
|
|
200
|
// PostgresDSN is the Postgres connection string for persistent history. |
|
201
|
// Recommended. If empty and Enabled is true, MySQL config is used instead. |
|
202
|
PostgresDSN string `yaml:"postgres_dsn"` |
|
203
|
|
|
204
|
// MySQL is the MySQL connection config for persistent history. |
|
205
|
MySQL MySQLConfig `yaml:"mysql"` |
|
206
|
} |
|
207
|
|
|
208
|
// MySQLConfig holds MySQL connection settings for Ergo history. |
|
209
|
type MySQLConfig struct { |
|
210
|
Host string `yaml:"host"` |
|
211
|
Port int `yaml:"port"` |
|
212
|
User string `yaml:"user"` |
|
213
|
Password string `yaml:"password"` |
|
214
|
Database string `yaml:"database"` |
|
215
|
} |
|
216
|
|
|
217
|
// BridgeConfig configures the IRC bridge bot that powers the web chat UI. |
|
218
|
type BridgeConfig struct { |
|
219
|
// Enabled controls whether the bridge bot starts. Default: true. |
|
220
|
Enabled bool `yaml:"enabled"` |
|
221
|
|
|
222
|
// Nick is the IRC nick for the bridge bot. Default: "bridge". |
|
223
|
Nick string `yaml:"nick"` |
|
224
|
|
|
225
|
// Password is the SASL PLAIN passphrase for the bridge's NickServ account. |
|
226
|
// Auto-generated on first start if empty. |
|
227
|
Password string `yaml:"password"` |
|
228
|
|
|
229
|
// Channels is the list of IRC channels the bridge joins on startup. |
|
230
|
Channels []string `yaml:"channels"` |
|
231
|
|
|
232
|
// BufferSize is the number of messages to keep per channel. Default: 200. |
|
233
|
BufferSize int `yaml:"buffer_size"` |
|
234
|
|
|
235
|
// WebUserTTLMinutes controls how long HTTP bridge sender nicks remain visible |
|
236
|
// in the channel user list after their last post. Default: 5. |
|
237
|
WebUserTTLMinutes int `yaml:"web_user_ttl_minutes"` |
|
238
|
} |
|
239
|
|
|
240
|
// DatastoreConfig configures scuttlebot's own state store (separate from Ergo). |
|
241
|
type DatastoreConfig struct { |
|
242
|
// Driver is "sqlite" or "postgres". Default: "sqlite". |
|
243
|
Driver string `yaml:"driver"` |
|
244
|
|
|
245
|
// DSN is the data source name. |
|
246
|
// For sqlite: path to the .db file. |
|
247
|
// For postgres: connection string. |
|
248
|
DSN string `yaml:"dsn"` |
|
249
|
} |
|
250
|
|
|
251
|
// TopologyConfig is the top-level channel topology declaration. |
|
252
|
// It defines static channels provisioned at startup and dynamic channel type |
|
253
|
// rules applied when agents create channels at runtime. |
|
254
|
type TopologyConfig struct { |
|
255
|
// Nick is the IRC nick used by the topology manager to provision channels |
|
256
|
// via ChanServ. Defaults to "topology". |
|
257
|
Nick string `yaml:"nick" json:"nick"` |
|
258
|
|
|
259
|
// Channels are static channels provisioned at daemon startup. |
|
260
|
Channels []StaticChannelConfig `yaml:"channels" json:"channels"` |
|
261
|
|
|
262
|
// Types are prefix-based rules applied to dynamically created channels. |
|
263
|
// The first matching prefix wins. |
|
264
|
Types []ChannelTypeConfig `yaml:"types" json:"types"` |
|
265
|
} |
|
266
|
|
|
267
|
// StaticChannelConfig describes a channel that is provisioned at startup. |
|
268
|
type StaticChannelConfig struct { |
|
269
|
// Name is the full channel name including the # prefix (e.g. "#general"). |
|
270
|
Name string `yaml:"name" json:"name"` |
|
271
|
|
|
272
|
// Topic is the initial channel topic. |
|
273
|
Topic string `yaml:"topic" json:"topic,omitempty"` |
|
274
|
|
|
275
|
// Ops is a list of nicks to grant channel operator (+o) access. |
|
276
|
Ops []string `yaml:"ops" json:"ops,omitempty"` |
|
277
|
|
|
278
|
// Voice is a list of nicks to grant voice (+v) access. |
|
279
|
Voice []string `yaml:"voice" json:"voice,omitempty"` |
|
280
|
|
|
281
|
// Autojoin is a list of bot nicks to invite when the channel is provisioned. |
|
282
|
Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"` |
|
283
|
|
|
284
|
// Modes is a list of channel modes to set after provisioning (e.g. "+m" for moderated). |
|
285
|
Modes []string `yaml:"modes" json:"modes,omitempty"` |
|
286
|
|
|
287
|
// OnJoinMessage is sent to agents when they join this channel. |
|
288
|
// Supports template variables: {nick}, {channel}. |
|
289
|
OnJoinMessage string `yaml:"on_join_message" json:"on_join_message,omitempty"` |
|
290
|
} |
|
291
|
|
|
292
|
// ChannelTypeConfig defines policy rules for a class of dynamically created channels. |
|
293
|
// Matched by prefix against channel names (e.g. prefix "task." matches "#task.gh-42"). |
|
294
|
type ChannelTypeConfig struct { |
|
295
|
// Name is a human-readable type identifier (e.g. "task", "sprint", "incident"). |
|
296
|
Name string `yaml:"name" json:"name"` |
|
297
|
|
|
298
|
// Prefix is matched against channel names after stripping the leading #. |
|
299
|
// The first matching type wins. (e.g. "task." matches "#task.gh-42") |
|
300
|
Prefix string `yaml:"prefix" json:"prefix"` |
|
301
|
|
|
302
|
// Autojoin is a list of bot nicks to invite when a channel of this type is created. |
|
303
|
Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"` |
|
304
|
|
|
305
|
// Supervision is the coordination channel where summaries should surface. |
|
306
|
Supervision string `yaml:"supervision" json:"supervision,omitempty"` |
|
307
|
|
|
308
|
// Modes is a list of channel modes to set when provisioning (e.g. "+m" for moderated). |
|
309
|
Modes []string `yaml:"modes" json:"modes,omitempty"` |
|
310
|
|
|
311
|
// Ephemeral marks channels of this type for automatic cleanup. |
|
312
|
Ephemeral bool `yaml:"ephemeral" json:"ephemeral,omitempty"` |
|
313
|
|
|
314
|
// TTL is the maximum lifetime of an ephemeral channel with no non-bot members. |
|
315
|
// Zero means no TTL; cleanup only occurs when the channel is empty. |
|
316
|
TTL Duration `yaml:"ttl" json:"ttl,omitempty"` |
|
317
|
|
|
318
|
// OnJoinMessage is sent to agents when they join a channel of this type. |
|
319
|
OnJoinMessage string `yaml:"on_join_message" json:"on_join_message,omitempty"` |
|
320
|
} |
|
321
|
|
|
322
|
// Duration wraps time.Duration for YAML/JSON marshalling ("72h", "30m", etc.). |
|
323
|
type Duration struct { |
|
324
|
time.Duration |
|
325
|
} |
|
326
|
|
|
327
|
func (d Duration) MarshalJSON() ([]byte, error) { |
|
328
|
if d.Duration == 0 { |
|
329
|
return []byte(`"0s"`), nil |
|
330
|
} |
|
331
|
return []byte(`"` + d.Duration.String() + `"`), nil |
|
332
|
} |
|
333
|
|
|
334
|
func (d *Duration) UnmarshalJSON(b []byte) error { |
|
335
|
s := string(b) |
|
336
|
if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { |
|
337
|
return fmt.Errorf("config: duration must be a quoted string, got %s", s) |
|
338
|
} |
|
339
|
dur, err := time.ParseDuration(s[1 : len(s)-1]) |
|
340
|
if err != nil { |
|
341
|
return fmt.Errorf("config: invalid duration %s: %w", s, err) |
|
342
|
} |
|
343
|
d.Duration = dur |
|
344
|
return nil |
|
345
|
} |
|
346
|
|
|
347
|
func (d *Duration) UnmarshalYAML(value *yaml.Node) error { |
|
348
|
var s string |
|
349
|
if err := value.Decode(&s); err != nil { |
|
350
|
return err |
|
351
|
} |
|
352
|
dur, err := time.ParseDuration(s) |
|
353
|
if err != nil { |
|
354
|
return fmt.Errorf("config: invalid duration %q: %w", s, err) |
|
355
|
} |
|
356
|
d.Duration = dur |
|
357
|
return nil |
|
358
|
} |
|
359
|
|
|
360
|
// MarshalYAML encodes Duration as a human-readable string ("72h", "30m"). |
|
361
|
func (d Duration) MarshalYAML() (any, error) { |
|
362
|
if d.Duration == 0 { |
|
363
|
return "0s", nil |
|
364
|
} |
|
365
|
return d.Duration.String(), nil |
|
366
|
} |
|
367
|
|
|
368
|
// Save marshals c to YAML and writes it to path atomically (write to a temp |
|
369
|
// file in the same directory, then rename). Comments in the original file are |
|
370
|
// not preserved after the first save. |
|
371
|
func (c *Config) Save(path string) error { |
|
372
|
data, err := yaml.Marshal(c) |
|
373
|
if err != nil { |
|
374
|
return fmt.Errorf("config: marshal: %w", err) |
|
375
|
} |
|
376
|
// Write to a sibling temp file then rename for atomic replacement. |
|
377
|
tmp := path + ".tmp" |
|
378
|
if err := os.WriteFile(tmp, data, 0o600); err != nil { |
|
379
|
return fmt.Errorf("config: write %s: %w", tmp, err) |
|
380
|
} |
|
381
|
if err := os.Rename(tmp, path); err != nil { |
|
382
|
_ = os.Remove(tmp) |
|
383
|
return fmt.Errorf("config: rename %s → %s: %w", tmp, path, err) |
|
384
|
} |
|
385
|
return nil |
|
386
|
} |
|
387
|
|
|
388
|
// Defaults fills in zero values with sensible defaults. |
|
389
|
func (c *Config) Defaults() { |
|
390
|
if c.Ergo.BinaryPath == "" { |
|
391
|
c.Ergo.BinaryPath = "ergo" |
|
392
|
} |
|
393
|
if c.Ergo.DataDir == "" { |
|
394
|
c.Ergo.DataDir = "./data/ergo" |
|
395
|
} |
|
396
|
if c.Ergo.NetworkName == "" { |
|
397
|
c.Ergo.NetworkName = "scuttlebot" |
|
398
|
} |
|
399
|
if c.Ergo.ServerName == "" { |
|
400
|
c.Ergo.ServerName = "irc.scuttlebot.local" |
|
401
|
} |
|
402
|
if c.Ergo.IRCAddr == "" { |
|
403
|
c.Ergo.IRCAddr = "127.0.0.1:6667" |
|
404
|
} |
|
405
|
if c.Ergo.TLSDomain != "" && c.Ergo.TLSAddr == "" { |
|
406
|
c.Ergo.TLSAddr = "0.0.0.0:6697" |
|
407
|
} |
|
408
|
if c.Ergo.APIAddr == "" { |
|
409
|
c.Ergo.APIAddr = "127.0.0.1:8089" |
|
410
|
} |
|
411
|
if c.Datastore.Driver == "" { |
|
412
|
c.Datastore.Driver = "sqlite" |
|
413
|
} |
|
414
|
if c.Datastore.DSN == "" { |
|
415
|
c.Datastore.DSN = "./data/scuttlebot.db" |
|
416
|
} |
|
417
|
if c.APIAddr == "" { |
|
418
|
c.APIAddr = "127.0.0.1:8080" |
|
419
|
} |
|
420
|
if c.MCPAddr == "" { |
|
421
|
c.MCPAddr = "127.0.0.1:8081" |
|
422
|
} |
|
423
|
if c.Ergo.DefaultChannelModes == "" { |
|
424
|
c.Ergo.DefaultChannelModes = "+n" |
|
425
|
} |
|
426
|
if !c.Bridge.Enabled && c.Bridge.Nick == "" { |
|
427
|
c.Bridge.Enabled = true // enabled by default |
|
428
|
} |
|
429
|
if c.TLS.Domain != "" && !c.TLS.AllowInsecure { |
|
430
|
c.TLS.AllowInsecure = true // HTTP always on by default |
|
431
|
} |
|
432
|
if c.Bridge.Nick == "" { |
|
433
|
c.Bridge.Nick = "bridge" |
|
434
|
} |
|
435
|
if c.Bridge.BufferSize == 0 { |
|
436
|
c.Bridge.BufferSize = 200 |
|
437
|
} |
|
438
|
if c.Bridge.WebUserTTLMinutes == 0 { |
|
439
|
c.Bridge.WebUserTTLMinutes = 5 |
|
440
|
} |
|
441
|
if c.Topology.Nick == "" { |
|
442
|
c.Topology.Nick = "topology" |
|
443
|
} |
|
444
|
if c.History.Keep == 0 { |
|
445
|
c.History.Keep = 20 |
|
446
|
} |
|
447
|
} |
|
448
|
|
|
449
|
func envStr(key string) string { return os.Getenv(key) } |
|
450
|
|
|
451
|
// LoadFile reads a YAML config file into c. Missing file is not an error — |
|
452
|
// returns nil so callers can treat an absent config file as "use defaults". |
|
453
|
// Call Defaults() first, then LoadFile(), then ApplyEnv() so that file values |
|
454
|
// override defaults and env values override the file. |
|
455
|
func (c *Config) LoadFile(path string) error { |
|
456
|
data, err := os.ReadFile(path) |
|
457
|
if os.IsNotExist(err) { |
|
458
|
return nil |
|
459
|
} |
|
460
|
if err != nil { |
|
461
|
return fmt.Errorf("config: read %s: %w", path, err) |
|
462
|
} |
|
463
|
return c.LoadFromBytes(data) |
|
464
|
} |
|
465
|
|
|
466
|
// LoadFromBytes parses YAML config bytes into c. |
|
467
|
func (c *Config) LoadFromBytes(data []byte) error { |
|
468
|
if err := yaml.Unmarshal(data, c); err != nil { |
|
469
|
return fmt.Errorf("config: parse: %w", err) |
|
470
|
} |
|
471
|
return nil |
|
472
|
} |
|
473
|
|
|
474
|
// ApplyEnv overrides config values with SCUTTLEBOT_* environment variables. |
|
475
|
// Call after Defaults() to allow env to override defaults. |
|
476
|
// |
|
477
|
// Supported variables: |
|
478
|
// |
|
479
|
// SCUTTLEBOT_API_ADDR — scuttlebot HTTP API listen address (e.g. ":8080") |
|
480
|
// SCUTTLEBOT_DB_DRIVER — "sqlite" or "postgres" |
|
481
|
// SCUTTLEBOT_DB_DSN — datastore connection string |
|
482
|
// SCUTTLEBOT_ERGO_EXTERNAL — "true" to skip subprocess management |
|
483
|
// SCUTTLEBOT_ERGO_API_ADDR — ergo HTTP API address (e.g. "http://ergo:8089") |
|
484
|
// SCUTTLEBOT_ERGO_API_TOKEN — ergo HTTP API bearer token |
|
485
|
// SCUTTLEBOT_ERGO_IRC_ADDR — ergo IRC listen/connect address (e.g. "ergo:6667") |
|
486
|
// SCUTTLEBOT_ERGO_NETWORK_NAME — IRC network name |
|
487
|
// SCUTTLEBOT_ERGO_SERVER_NAME — IRC server hostname |
|
488
|
func (c *Config) ApplyEnv() { |
|
489
|
if v := envStr("SCUTTLEBOT_API_ADDR"); v != "" { |
|
490
|
c.APIAddr = v |
|
491
|
} |
|
492
|
if v := envStr("SCUTTLEBOT_DB_DRIVER"); v != "" { |
|
493
|
c.Datastore.Driver = v |
|
494
|
} |
|
495
|
if v := envStr("SCUTTLEBOT_DB_DSN"); v != "" { |
|
496
|
c.Datastore.DSN = v |
|
497
|
} |
|
498
|
if v := envStr("SCUTTLEBOT_ERGO_EXTERNAL"); v == "true" || v == "1" { |
|
499
|
c.Ergo.External = true |
|
500
|
} |
|
501
|
if v := envStr("SCUTTLEBOT_ERGO_API_ADDR"); v != "" { |
|
502
|
c.Ergo.APIAddr = v |
|
503
|
} |
|
504
|
if v := envStr("SCUTTLEBOT_ERGO_API_TOKEN"); v != "" { |
|
505
|
c.Ergo.APIToken = v |
|
506
|
} |
|
507
|
if v := envStr("SCUTTLEBOT_ERGO_IRC_ADDR"); v != "" { |
|
508
|
c.Ergo.IRCAddr = v |
|
509
|
} |
|
510
|
if v := envStr("SCUTTLEBOT_ERGO_NETWORK_NAME"); v != "" { |
|
511
|
c.Ergo.NetworkName = v |
|
512
|
} |
|
513
|
if v := envStr("SCUTTLEBOT_ERGO_SERVER_NAME"); v != "" { |
|
514
|
c.Ergo.ServerName = v |
|
515
|
} |
|
516
|
if v := envStr("SCUTTLEBOT_MCP_ADDR"); v != "" { |
|
517
|
c.MCPAddr = v |
|
518
|
} |
|
519
|
} |
|
520
|
|