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