mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-18 12:14:24 +00:00
fix: enforce revoke disconnect, group pairing, and agent disable cascade
- Revoke paired device now force-closes the active WebSocket connection
via pubsub (EventPairingRevoked → Server.DisconnectByPairing)
- Add group-level pairing policy ("pairing") to Discord, WhatsApp, and
Zalo Personal channels (matching existing Telegram/Feishu pattern)
- Block inactive agents at resolver level (chat, cron, delegation all
reject with "agent is inactive")
- Cascade: setting agent status to "inactive" auto-disables linked
channel instances via EventAgentStatusChanged pubsub
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
6d9374a1a5
commit
1f8c98d3de
13 files changed
+219
-7
No files matched your search
@@ -10,6 +10,8 @@ import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bootstrap"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
@@ -850,6 +852,61 @@ func runGateway() {
|
||||
}
|
||||
})
|
||||
|
||||
// Wire pairing revocation → force disconnect active WebSocket sessions.
|
||||
msgBus.Subscribe(bus.TopicPairingRevoked, func(event bus.Event) {
|
||||
if event.Name != bus.EventPairingRevoked {
|
||||
return
|
||||
}
|
||||
payload, ok := event.Payload.(bus.PairingRevokedPayload)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
go server.DisconnectByPairing(payload.SenderID, payload.Channel)
|
||||
})
|
||||
|
||||
// Cascade: when an agent becomes inactive, disable its linked channel instances.
|
||||
if managedStores != nil && managedStores.ChannelInstances != nil {
|
||||
ciStore := managedStores.ChannelInstances
|
||||
msgBus.Subscribe(bus.TopicAgentStatusChanged, func(event bus.Event) {
|
||||
if event.Name != bus.EventAgentStatusChanged {
|
||||
return
|
||||
}
|
||||
payload, ok := event.Payload.(bus.AgentStatusChangedPayload)
|
||||
if !ok || payload.NewStatus != store.AgentStatusInactive {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
agentID, err := uuid.Parse(payload.AgentID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
all, err := ciStore.ListAll(context.Background())
|
||||
if err != nil {
|
||||
slog.Warn("cascade disable: failed to list channel instances", "error", err)
|
||||
return
|
||||
}
|
||||
disabled := 0
|
||||
for _, inst := range all {
|
||||
if inst.AgentID == agentID && inst.Enabled {
|
||||
if err := ciStore.Update(context.Background(), inst.ID, map[string]any{"enabled": false}); err != nil {
|
||||
slog.Warn("cascade disable: failed to disable channel instance", "name", inst.Name, "error", err)
|
||||
} else {
|
||||
disabled++
|
||||
}
|
||||
}
|
||||
}
|
||||
if disabled > 0 {
|
||||
slog.Info("cascade disabled channel instances for inactive agent", "agent_id", payload.AgentID, "count", disabled)
|
||||
// Trigger channel reload so disabled instances are stopped.
|
||||
msgBus.Broadcast(bus.Event{
|
||||
Name: protocol.EventCacheInvalidate,
|
||||
Payload: bus.CacheInvalidatePayload{Kind: bus.CacheKindChannelInstances},
|
||||
})
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Setup graceful shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -29,7 +29,7 @@ func registerAllMethods(server *gateway.Server, agents *agent.Router, sessStore
|
||||
|
||||
// Phase 2: Pairing (store created externally, shared with channel manager).
|
||||
// OnApprove callback is set later by the caller after channel manager is created.
|
||||
pairingMethods := methods.NewPairingMethods(pairingStore)
|
||||
pairingMethods := methods.NewPairingMethods(pairingStore, msgBus)
|
||||
pairingMethods.Register(router)
|
||||
|
||||
// Phase 2: Usage (queries SessionStore for real token data)
|
||||
|
||||
Reference in new issue
Block a user