ScuttleBot

fix(config): add json tags to topology/history structs and Duration JSON marshalling GET /v1/config was returning capitalized Go field names for TopologyConfig and ConfigHistoryConfig. Added json struct tags throughout, plus MarshalJSON and UnmarshalJSON on Duration so TTL values round-trip as quoted strings ("72h").

lmata 2026-04-02 17:26 trunk
Commit 0619d47c699e5d9d2250da8486bb4363cf6b426e06d11a1dab74f89e07419165
1 file changed +37 -20
--- internal/config/config.go
+++ internal/config/config.go
@@ -31,15 +31,15 @@
3131
3232
// ConfigHistoryConfig controls config write-back history retention.
3333
type ConfigHistoryConfig struct {
3434
// Keep is the number of config snapshots to retain in Dir.
3535
// 0 disables history. Default: 20.
36
- Keep int `yaml:"keep"`
36
+ Keep int `yaml:"keep" json:"keep"`
3737
3838
// Dir is the directory for config snapshots.
3939
// Default: {ergo.data_dir}/config-history
40
- Dir string `yaml:"dir"`
40
+ Dir string `yaml:"dir" json:"dir,omitempty"`
4141
}
4242
4343
// LLMConfig configures the omnibus LLM gateway used by oracle and any other
4444
// bot or service that needs language model access.
4545
type LLMConfig struct {
@@ -210,69 +210,86 @@
210210
// It defines static channels provisioned at startup and dynamic channel type
211211
// rules applied when agents create channels at runtime.
212212
type TopologyConfig struct {
213213
// Nick is the IRC nick used by the topology manager to provision channels
214214
// via ChanServ. Defaults to "topology".
215
- Nick string `yaml:"nick"`
215
+ Nick string `yaml:"nick" json:"nick"`
216216
217217
// Channels are static channels provisioned at daemon startup.
218
- Channels []StaticChannelConfig `yaml:"channels"`
218
+ Channels []StaticChannelConfig `yaml:"channels" json:"channels"`
219219
220220
// Types are prefix-based rules applied to dynamically created channels.
221221
// The first matching prefix wins.
222
- Types []ChannelTypeConfig `yaml:"types"`
222
+ Types []ChannelTypeConfig `yaml:"types" json:"types"`
223223
}
224224
225225
// StaticChannelConfig describes a channel that is provisioned at startup.
226226
type StaticChannelConfig struct {
227227
// Name is the full channel name including the # prefix (e.g. "#general").
228
- Name string `yaml:"name"`
228
+ Name string `yaml:"name" json:"name"`
229229
230230
// Topic is the initial channel topic.
231
- Topic string `yaml:"topic"`
231
+ Topic string `yaml:"topic" json:"topic,omitempty"`
232232
233233
// Ops is a list of nicks to grant channel operator (+o) access.
234
- Ops []string `yaml:"ops"`
234
+ Ops []string `yaml:"ops" json:"ops,omitempty"`
235235
236236
// Voice is a list of nicks to grant voice (+v) access.
237
- Voice []string `yaml:"voice"`
237
+ Voice []string `yaml:"voice" json:"voice,omitempty"`
238238
239239
// Autojoin is a list of bot nicks to invite when the channel is provisioned.
240
- Autojoin []string `yaml:"autojoin"`
240
+ Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"`
241241
}
242242
243243
// ChannelTypeConfig defines policy rules for a class of dynamically created channels.
244244
// Matched by prefix against channel names (e.g. prefix "task." matches "#task.gh-42").
245245
type ChannelTypeConfig struct {
246246
// Name is a human-readable type identifier (e.g. "task", "sprint", "incident").
247
- Name string `yaml:"name"`
247
+ Name string `yaml:"name" json:"name"`
248248
249249
// Prefix is matched against channel names after stripping the leading #.
250250
// The first matching type wins. (e.g. "task." matches "#task.gh-42")
251
- Prefix string `yaml:"prefix"`
251
+ Prefix string `yaml:"prefix" json:"prefix"`
252252
253253
// Autojoin is a list of bot nicks to invite when a channel of this type is created.
254
- Autojoin []string `yaml:"autojoin"`
254
+ Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"`
255255
256256
// Supervision is the coordination channel where summaries should surface.
257
- // Agents receive this when they create a channel so they know where to also post.
258
- // May be a static channel name (e.g. "#general") or a type prefix pattern
259
- // (e.g. "sprint." — resolved to the most recently created matching channel).
260
- Supervision string `yaml:"supervision"`
257
+ Supervision string `yaml:"supervision" json:"supervision,omitempty"`
261258
262259
// Ephemeral marks channels of this type for automatic cleanup.
263
- Ephemeral bool `yaml:"ephemeral"`
260
+ Ephemeral bool `yaml:"ephemeral" json:"ephemeral,omitempty"`
264261
265262
// TTL is the maximum lifetime of an ephemeral channel with no non-bot members.
266263
// Zero means no TTL; cleanup only occurs when the channel is empty.
267
- TTL Duration `yaml:"ttl"`
264
+ TTL Duration `yaml:"ttl" json:"ttl,omitempty"`
268265
}
269266
270
-// Duration wraps time.Duration for YAML unmarshalling ("72h", "30m", etc.).
267
+// Duration wraps time.Duration for YAML/JSON marshalling ("72h", "30m", etc.).
271268
type Duration struct {
272269
time.Duration
273270
}
271
+
272
+func (d Duration) MarshalJSON() ([]byte, error) {
273
+ if d.Duration == 0 {
274
+ return []byte(`"0s"`), nil
275
+ }
276
+ return []byte(`"` + d.Duration.String() + `"`), nil
277
+}
278
+
279
+func (d *Duration) UnmarshalJSON(b []byte) error {
280
+ s := string(b)
281
+ if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' {
282
+ return fmt.Errorf("config: duration must be a quoted string, got %s", s)
283
+ }
284
+ dur, err := time.ParseDuration(s[1 : len(s)-1])
285
+ if err != nil {
286
+ return fmt.Errorf("config: invalid duration %s: %w", s, err)
287
+ }
288
+ d.Duration = dur
289
+ return nil
290
+}
274291
275292
func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
276293
var s string
277294
if err := value.Decode(&s); err != nil {
278295
return err
279296
--- internal/config/config.go
+++ internal/config/config.go
@@ -31,15 +31,15 @@
31
32 // ConfigHistoryConfig controls config write-back history retention.
33 type ConfigHistoryConfig struct {
34 // Keep is the number of config snapshots to retain in Dir.
35 // 0 disables history. Default: 20.
36 Keep int `yaml:"keep"`
37
38 // Dir is the directory for config snapshots.
39 // Default: {ergo.data_dir}/config-history
40 Dir string `yaml:"dir"`
41 }
42
43 // LLMConfig configures the omnibus LLM gateway used by oracle and any other
44 // bot or service that needs language model access.
45 type LLMConfig struct {
@@ -210,69 +210,86 @@
210 // It defines static channels provisioned at startup and dynamic channel type
211 // rules applied when agents create channels at runtime.
212 type TopologyConfig struct {
213 // Nick is the IRC nick used by the topology manager to provision channels
214 // via ChanServ. Defaults to "topology".
215 Nick string `yaml:"nick"`
216
217 // Channels are static channels provisioned at daemon startup.
218 Channels []StaticChannelConfig `yaml:"channels"`
219
220 // Types are prefix-based rules applied to dynamically created channels.
221 // The first matching prefix wins.
222 Types []ChannelTypeConfig `yaml:"types"`
223 }
224
225 // StaticChannelConfig describes a channel that is provisioned at startup.
226 type StaticChannelConfig struct {
227 // Name is the full channel name including the # prefix (e.g. "#general").
228 Name string `yaml:"name"`
229
230 // Topic is the initial channel topic.
231 Topic string `yaml:"topic"`
232
233 // Ops is a list of nicks to grant channel operator (+o) access.
234 Ops []string `yaml:"ops"`
235
236 // Voice is a list of nicks to grant voice (+v) access.
237 Voice []string `yaml:"voice"`
238
239 // Autojoin is a list of bot nicks to invite when the channel is provisioned.
240 Autojoin []string `yaml:"autojoin"`
241 }
242
243 // ChannelTypeConfig defines policy rules for a class of dynamically created channels.
244 // Matched by prefix against channel names (e.g. prefix "task." matches "#task.gh-42").
245 type ChannelTypeConfig struct {
246 // Name is a human-readable type identifier (e.g. "task", "sprint", "incident").
247 Name string `yaml:"name"`
248
249 // Prefix is matched against channel names after stripping the leading #.
250 // The first matching type wins. (e.g. "task." matches "#task.gh-42")
251 Prefix string `yaml:"prefix"`
252
253 // Autojoin is a list of bot nicks to invite when a channel of this type is created.
254 Autojoin []string `yaml:"autojoin"`
255
256 // Supervision is the coordination channel where summaries should surface.
257 // Agents receive this when they create a channel so they know where to also post.
258 // May be a static channel name (e.g. "#general") or a type prefix pattern
259 // (e.g. "sprint." — resolved to the most recently created matching channel).
260 Supervision string `yaml:"supervision"`
261
262 // Ephemeral marks channels of this type for automatic cleanup.
263 Ephemeral bool `yaml:"ephemeral"`
264
265 // TTL is the maximum lifetime of an ephemeral channel with no non-bot members.
266 // Zero means no TTL; cleanup only occurs when the channel is empty.
267 TTL Duration `yaml:"ttl"`
268 }
269
270 // Duration wraps time.Duration for YAML unmarshalling ("72h", "30m", etc.).
271 type Duration struct {
272 time.Duration
273 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
275 func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
276 var s string
277 if err := value.Decode(&s); err != nil {
278 return err
279
--- internal/config/config.go
+++ internal/config/config.go
@@ -31,15 +31,15 @@
31
32 // ConfigHistoryConfig controls config write-back history retention.
33 type ConfigHistoryConfig struct {
34 // Keep is the number of config snapshots to retain in Dir.
35 // 0 disables history. Default: 20.
36 Keep int `yaml:"keep" json:"keep"`
37
38 // Dir is the directory for config snapshots.
39 // Default: {ergo.data_dir}/config-history
40 Dir string `yaml:"dir" json:"dir,omitempty"`
41 }
42
43 // LLMConfig configures the omnibus LLM gateway used by oracle and any other
44 // bot or service that needs language model access.
45 type LLMConfig struct {
@@ -210,69 +210,86 @@
210 // It defines static channels provisioned at startup and dynamic channel type
211 // rules applied when agents create channels at runtime.
212 type TopologyConfig struct {
213 // Nick is the IRC nick used by the topology manager to provision channels
214 // via ChanServ. Defaults to "topology".
215 Nick string `yaml:"nick" json:"nick"`
216
217 // Channels are static channels provisioned at daemon startup.
218 Channels []StaticChannelConfig `yaml:"channels" json:"channels"`
219
220 // Types are prefix-based rules applied to dynamically created channels.
221 // The first matching prefix wins.
222 Types []ChannelTypeConfig `yaml:"types" json:"types"`
223 }
224
225 // StaticChannelConfig describes a channel that is provisioned at startup.
226 type StaticChannelConfig struct {
227 // Name is the full channel name including the # prefix (e.g. "#general").
228 Name string `yaml:"name" json:"name"`
229
230 // Topic is the initial channel topic.
231 Topic string `yaml:"topic" json:"topic,omitempty"`
232
233 // Ops is a list of nicks to grant channel operator (+o) access.
234 Ops []string `yaml:"ops" json:"ops,omitempty"`
235
236 // Voice is a list of nicks to grant voice (+v) access.
237 Voice []string `yaml:"voice" json:"voice,omitempty"`
238
239 // Autojoin is a list of bot nicks to invite when the channel is provisioned.
240 Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"`
241 }
242
243 // ChannelTypeConfig defines policy rules for a class of dynamically created channels.
244 // Matched by prefix against channel names (e.g. prefix "task." matches "#task.gh-42").
245 type ChannelTypeConfig struct {
246 // Name is a human-readable type identifier (e.g. "task", "sprint", "incident").
247 Name string `yaml:"name" json:"name"`
248
249 // Prefix is matched against channel names after stripping the leading #.
250 // The first matching type wins. (e.g. "task." matches "#task.gh-42")
251 Prefix string `yaml:"prefix" json:"prefix"`
252
253 // Autojoin is a list of bot nicks to invite when a channel of this type is created.
254 Autojoin []string `yaml:"autojoin" json:"autojoin,omitempty"`
255
256 // Supervision is the coordination channel where summaries should surface.
257 Supervision string `yaml:"supervision" json:"supervision,omitempty"`
 
 
 
258
259 // Ephemeral marks channels of this type for automatic cleanup.
260 Ephemeral bool `yaml:"ephemeral" json:"ephemeral,omitempty"`
261
262 // TTL is the maximum lifetime of an ephemeral channel with no non-bot members.
263 // Zero means no TTL; cleanup only occurs when the channel is empty.
264 TTL Duration `yaml:"ttl" json:"ttl,omitempty"`
265 }
266
267 // Duration wraps time.Duration for YAML/JSON marshalling ("72h", "30m", etc.).
268 type Duration struct {
269 time.Duration
270 }
271
272 func (d Duration) MarshalJSON() ([]byte, error) {
273 if d.Duration == 0 {
274 return []byte(`"0s"`), nil
275 }
276 return []byte(`"` + d.Duration.String() + `"`), nil
277 }
278
279 func (d *Duration) UnmarshalJSON(b []byte) error {
280 s := string(b)
281 if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' {
282 return fmt.Errorf("config: duration must be a quoted string, got %s", s)
283 }
284 dur, err := time.ParseDuration(s[1 : len(s)-1])
285 if err != nil {
286 return fmt.Errorf("config: invalid duration %s: %w", s, err)
287 }
288 d.Duration = dur
289 return nil
290 }
291
292 func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
293 var s string
294 if err := value.Decode(&s); err != nil {
295 return err
296

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button