ScuttleBot

feat: configurable online timeout for agent presence Add online_timeout_secs to agent policy — controls how long since last heartbeat before an agent is considered offline. Default 120s. Configurable from the settings UI. Previously hardcoded as 2 minutes.

lmata 2026-04-03 21:39 trunk
Commit c68066e4704f87451f9a6a5ea565397124bbe4c7e7438dc27bc0819430ff0980
--- cmd/scuttlebot/main.go
+++ cmd/scuttlebot/main.go
@@ -272,10 +272,13 @@
272272
}
273273
}
274274
if bridgeBot != nil {
275275
bridgeBot.SetWebUserTTL(time.Duration(p.Bridge.WebUserTTLMinutes) * time.Minute)
276276
}
277
+ if p.AgentPolicy.OnlineTimeoutSecs > 0 {
278
+ reg.SetOnlineTimeout(time.Duration(p.AgentPolicy.OnlineTimeoutSecs) * time.Second)
279
+ }
277280
botMgr.Sync(ctx, specs)
278281
})
279282
280283
// Initial bot sync from loaded policies.
281284
{
282285
--- cmd/scuttlebot/main.go
+++ cmd/scuttlebot/main.go
@@ -272,10 +272,13 @@
272 }
273 }
274 if bridgeBot != nil {
275 bridgeBot.SetWebUserTTL(time.Duration(p.Bridge.WebUserTTLMinutes) * time.Minute)
276 }
 
 
 
277 botMgr.Sync(ctx, specs)
278 })
279
280 // Initial bot sync from loaded policies.
281 {
282
--- cmd/scuttlebot/main.go
+++ cmd/scuttlebot/main.go
@@ -272,10 +272,13 @@
272 }
273 }
274 if bridgeBot != nil {
275 bridgeBot.SetWebUserTTL(time.Duration(p.Bridge.WebUserTTLMinutes) * time.Minute)
276 }
277 if p.AgentPolicy.OnlineTimeoutSecs > 0 {
278 reg.SetOnlineTimeout(time.Duration(p.AgentPolicy.OnlineTimeoutSecs) * time.Second)
279 }
280 botMgr.Sync(ctx, specs)
281 })
282
283 // Initial bot sync from loaded policies.
284 {
285
--- internal/api/policies.go
+++ internal/api/policies.go
@@ -25,13 +25,14 @@
2525
Config map[string]any `json:"config,omitempty"`
2626
}
2727
2828
// AgentPolicy defines requirements applied to all registering agents.
2929
type AgentPolicy struct {
30
- RequireCheckin bool `json:"require_checkin"`
31
- CheckinChannel string `json:"checkin_channel"`
32
- RequiredChannels []string `json:"required_channels"`
30
+ RequireCheckin bool `json:"require_checkin"`
31
+ CheckinChannel string `json:"checkin_channel"`
32
+ RequiredChannels []string `json:"required_channels"`
33
+ OnlineTimeoutSecs int `json:"online_timeout_secs,omitempty"`
3334
}
3435
3536
// LoggingPolicy configures message logging.
3637
type LoggingPolicy struct {
3738
Enabled bool `json:"enabled"`
3839
--- internal/api/policies.go
+++ internal/api/policies.go
@@ -25,13 +25,14 @@
25 Config map[string]any `json:"config,omitempty"`
26 }
27
28 // AgentPolicy defines requirements applied to all registering agents.
29 type AgentPolicy struct {
30 RequireCheckin bool `json:"require_checkin"`
31 CheckinChannel string `json:"checkin_channel"`
32 RequiredChannels []string `json:"required_channels"`
 
33 }
34
35 // LoggingPolicy configures message logging.
36 type LoggingPolicy struct {
37 Enabled bool `json:"enabled"`
38
--- internal/api/policies.go
+++ internal/api/policies.go
@@ -25,13 +25,14 @@
25 Config map[string]any `json:"config,omitempty"`
26 }
27
28 // AgentPolicy defines requirements applied to all registering agents.
29 type AgentPolicy struct {
30 RequireCheckin bool `json:"require_checkin"`
31 CheckinChannel string `json:"checkin_channel"`
32 RequiredChannels []string `json:"required_channels"`
33 OnlineTimeoutSecs int `json:"online_timeout_secs,omitempty"`
34 }
35
36 // LoggingPolicy configures message logging.
37 type LoggingPolicy struct {
38 Enabled bool `json:"enabled"`
39
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -588,10 +588,15 @@
588588
<div class="setting-row">
589589
<div class="setting-label">required channels</div>
590590
<div class="setting-desc">Channels every agent is added to automatically.</div>
591591
<input type="text" id="policy-required-channels" placeholder="#fleet, #alerts" style="width:220px;padding:4px 8px;font-size:12px">
592592
</div>
593
+ <div class="setting-row">
594
+ <div class="setting-label">online timeout</div>
595
+ <div class="setting-desc">Seconds since last heartbeat before an agent is considered offline. Default: 120.</div>
596
+ <input type="number" id="policy-online-timeout" placeholder="120" min="10" max="3600" style="width:100px;padding:4px 8px;font-size:12px">
597
+ </div>
593598
</div>
594599
<div id="agentpolicy-save-result" style="display:none;margin:0 16px 12px"></div>
595600
</div>
596601
597602
<!-- bridge -->
@@ -2703,10 +2708,11 @@
27032708
27042709
function renderAgentPolicy(p) {
27052710
document.getElementById('policy-checkin-enabled').checked = !!p.require_checkin;
27062711
document.getElementById('policy-checkin-channel').value = p.checkin_channel || '';
27072712
document.getElementById('policy-required-channels').value = (p.required_channels||[]).join(', ');
2713
+ document.getElementById('policy-online-timeout').value = p.online_timeout_secs || '';
27082714
toggleCheckinChannel();
27092715
}
27102716
function toggleCheckinChannel() {
27112717
const on = document.getElementById('policy-checkin-enabled').checked;
27122718
document.getElementById('policy-checkin-row').style.display = on ? '' : 'none';
@@ -2959,10 +2965,11 @@
29592965
saveConfigPatch({
29602966
agent_policy: {
29612967
require_checkin: document.getElementById('policy-checkin-enabled').checked,
29622968
checkin_channel: document.getElementById('policy-checkin-channel').value.trim(),
29632969
required_channels: document.getElementById('policy-required-channels').value.split(',').map(s=>s.trim()).filter(Boolean),
2970
+ online_timeout_secs: parseInt(document.getElementById('policy-online-timeout').value) || 0,
29642971
}
29652972
}, 'agentpolicy-save-result');
29662973
}
29672974
29682975
function saveBridgeConfig() {
29692976
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -588,10 +588,15 @@
588 <div class="setting-row">
589 <div class="setting-label">required channels</div>
590 <div class="setting-desc">Channels every agent is added to automatically.</div>
591 <input type="text" id="policy-required-channels" placeholder="#fleet, #alerts" style="width:220px;padding:4px 8px;font-size:12px">
592 </div>
 
 
 
 
 
593 </div>
594 <div id="agentpolicy-save-result" style="display:none;margin:0 16px 12px"></div>
595 </div>
596
597 <!-- bridge -->
@@ -2703,10 +2708,11 @@
2703
2704 function renderAgentPolicy(p) {
2705 document.getElementById('policy-checkin-enabled').checked = !!p.require_checkin;
2706 document.getElementById('policy-checkin-channel').value = p.checkin_channel || '';
2707 document.getElementById('policy-required-channels').value = (p.required_channels||[]).join(', ');
 
2708 toggleCheckinChannel();
2709 }
2710 function toggleCheckinChannel() {
2711 const on = document.getElementById('policy-checkin-enabled').checked;
2712 document.getElementById('policy-checkin-row').style.display = on ? '' : 'none';
@@ -2959,10 +2965,11 @@
2959 saveConfigPatch({
2960 agent_policy: {
2961 require_checkin: document.getElementById('policy-checkin-enabled').checked,
2962 checkin_channel: document.getElementById('policy-checkin-channel').value.trim(),
2963 required_channels: document.getElementById('policy-required-channels').value.split(',').map(s=>s.trim()).filter(Boolean),
 
2964 }
2965 }, 'agentpolicy-save-result');
2966 }
2967
2968 function saveBridgeConfig() {
2969
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -588,10 +588,15 @@
588 <div class="setting-row">
589 <div class="setting-label">required channels</div>
590 <div class="setting-desc">Channels every agent is added to automatically.</div>
591 <input type="text" id="policy-required-channels" placeholder="#fleet, #alerts" style="width:220px;padding:4px 8px;font-size:12px">
592 </div>
593 <div class="setting-row">
594 <div class="setting-label">online timeout</div>
595 <div class="setting-desc">Seconds since last heartbeat before an agent is considered offline. Default: 120.</div>
596 <input type="number" id="policy-online-timeout" placeholder="120" min="10" max="3600" style="width:100px;padding:4px 8px;font-size:12px">
597 </div>
598 </div>
599 <div id="agentpolicy-save-result" style="display:none;margin:0 16px 12px"></div>
600 </div>
601
602 <!-- bridge -->
@@ -2703,10 +2708,11 @@
2708
2709 function renderAgentPolicy(p) {
2710 document.getElementById('policy-checkin-enabled').checked = !!p.require_checkin;
2711 document.getElementById('policy-checkin-channel').value = p.checkin_channel || '';
2712 document.getElementById('policy-required-channels').value = (p.required_channels||[]).join(', ');
2713 document.getElementById('policy-online-timeout').value = p.online_timeout_secs || '';
2714 toggleCheckinChannel();
2715 }
2716 function toggleCheckinChannel() {
2717 const on = document.getElementById('policy-checkin-enabled').checked;
2718 document.getElementById('policy-checkin-row').style.display = on ? '' : 'none';
@@ -2959,10 +2965,11 @@
2965 saveConfigPatch({
2966 agent_policy: {
2967 require_checkin: document.getElementById('policy-checkin-enabled').checked,
2968 checkin_channel: document.getElementById('policy-checkin-channel').value.trim(),
2969 required_channels: document.getElementById('policy-required-channels').value.split(',').map(s=>s.trim()).filter(Boolean),
2970 online_timeout_secs: parseInt(document.getElementById('policy-online-timeout').value) || 0,
2971 }
2972 }, 'agentpolicy-save-result');
2973 }
2974
2975 function saveBridgeConfig() {
2976
--- internal/registry/registry.go
+++ internal/registry/registry.go
@@ -72,16 +72,17 @@
7272
ChangePassword(name, passphrase string) error
7373
}
7474
7575
// Registry manages registered agents and their credentials.
7676
type Registry struct {
77
- mu sync.RWMutex
78
- agents map[string]*Agent // keyed by nick
79
- provisioner AccountProvisioner
80
- signingKey []byte
81
- dataPath string // path to persist agents JSON; empty = no persistence
82
- db *store.Store // when non-nil, supersedes dataPath
77
+ mu sync.RWMutex
78
+ agents map[string]*Agent // keyed by nick
79
+ provisioner AccountProvisioner
80
+ signingKey []byte
81
+ dataPath string // path to persist agents JSON; empty = no persistence
82
+ db *store.Store // when non-nil, supersedes dataPath
83
+ onlineTimeout time.Duration
8384
}
8485
8586
// New creates a new Registry with the given provisioner and HMAC signing key.
8687
// Call SetDataPath to enable persistence before registering any agents.
8788
func New(provisioner AccountProvisioner, signingKey []byte) *Registry {
@@ -384,20 +385,36 @@
384385
now := time.Now()
385386
a.LastSeen = &now
386387
// Don't persist every heartbeat — just keep in memory.
387388
}
388389
389
-const onlineThreshold = 2 * time.Minute
390
+const defaultOnlineTimeout = 2 * time.Minute
391
+
392
+// SetOnlineTimeout configures how long since last_seen before an agent
393
+// is considered offline. Pass 0 to reset to the default (2 minutes).
394
+func (r *Registry) SetOnlineTimeout(d time.Duration) {
395
+ r.mu.Lock()
396
+ defer r.mu.Unlock()
397
+ r.onlineTimeout = d
398
+}
399
+
400
+func (r *Registry) getOnlineTimeout() time.Duration {
401
+ if r.onlineTimeout > 0 {
402
+ return r.onlineTimeout
403
+ }
404
+ return defaultOnlineTimeout
405
+}
390406
391407
// List returns all registered agents with computed online status.
392408
func (r *Registry) List() []*Agent {
393409
r.mu.RLock()
394410
defer r.mu.RUnlock()
411
+ threshold := r.getOnlineTimeout()
395412
now := time.Now()
396413
var out []*Agent
397414
for _, a := range r.agents {
398
- a.Online = a.LastSeen != nil && now.Sub(*a.LastSeen) < onlineThreshold
415
+ a.Online = a.LastSeen != nil && now.Sub(*a.LastSeen) < threshold
399416
out = append(out, a)
400417
}
401418
return out
402419
}
403420
404421
--- internal/registry/registry.go
+++ internal/registry/registry.go
@@ -72,16 +72,17 @@
72 ChangePassword(name, passphrase string) error
73 }
74
75 // Registry manages registered agents and their credentials.
76 type Registry struct {
77 mu sync.RWMutex
78 agents map[string]*Agent // keyed by nick
79 provisioner AccountProvisioner
80 signingKey []byte
81 dataPath string // path to persist agents JSON; empty = no persistence
82 db *store.Store // when non-nil, supersedes dataPath
 
83 }
84
85 // New creates a new Registry with the given provisioner and HMAC signing key.
86 // Call SetDataPath to enable persistence before registering any agents.
87 func New(provisioner AccountProvisioner, signingKey []byte) *Registry {
@@ -384,20 +385,36 @@
384 now := time.Now()
385 a.LastSeen = &now
386 // Don't persist every heartbeat — just keep in memory.
387 }
388
389 const onlineThreshold = 2 * time.Minute
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
391 // List returns all registered agents with computed online status.
392 func (r *Registry) List() []*Agent {
393 r.mu.RLock()
394 defer r.mu.RUnlock()
 
395 now := time.Now()
396 var out []*Agent
397 for _, a := range r.agents {
398 a.Online = a.LastSeen != nil && now.Sub(*a.LastSeen) < onlineThreshold
399 out = append(out, a)
400 }
401 return out
402 }
403
404
--- internal/registry/registry.go
+++ internal/registry/registry.go
@@ -72,16 +72,17 @@
72 ChangePassword(name, passphrase string) error
73 }
74
75 // Registry manages registered agents and their credentials.
76 type Registry struct {
77 mu sync.RWMutex
78 agents map[string]*Agent // keyed by nick
79 provisioner AccountProvisioner
80 signingKey []byte
81 dataPath string // path to persist agents JSON; empty = no persistence
82 db *store.Store // when non-nil, supersedes dataPath
83 onlineTimeout time.Duration
84 }
85
86 // New creates a new Registry with the given provisioner and HMAC signing key.
87 // Call SetDataPath to enable persistence before registering any agents.
88 func New(provisioner AccountProvisioner, signingKey []byte) *Registry {
@@ -384,20 +385,36 @@
385 now := time.Now()
386 a.LastSeen = &now
387 // Don't persist every heartbeat — just keep in memory.
388 }
389
390 const defaultOnlineTimeout = 2 * time.Minute
391
392 // SetOnlineTimeout configures how long since last_seen before an agent
393 // is considered offline. Pass 0 to reset to the default (2 minutes).
394 func (r *Registry) SetOnlineTimeout(d time.Duration) {
395 r.mu.Lock()
396 defer r.mu.Unlock()
397 r.onlineTimeout = d
398 }
399
400 func (r *Registry) getOnlineTimeout() time.Duration {
401 if r.onlineTimeout > 0 {
402 return r.onlineTimeout
403 }
404 return defaultOnlineTimeout
405 }
406
407 // List returns all registered agents with computed online status.
408 func (r *Registry) List() []*Agent {
409 r.mu.RLock()
410 defer r.mu.RUnlock()
411 threshold := r.getOnlineTimeout()
412 now := time.Now()
413 var out []*Agent
414 for _, a := range r.agents {
415 a.Online = a.LastSeen != nil && now.Sub(*a.LastSeen) < threshold
416 out = append(out, a)
417 }
418 return out
419 }
420
421

Keyboard Shortcuts

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