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:
viettranxandClaude Opus 4.6 committed 2026-03-05 10:03:08 +07:00
1 parent 6d9374a1a5
commit 1f8c98d3de
13 files changed
+219 -7

No files matched your search

+57
View File
@@ -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()
+1 -1
View File
@@ -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)