mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-16 00:14:57 +00:00
feat: expand audit logging via pub/sub event pattern
Replace direct ActivityStore injection with event-driven audit system. Handlers emit audit events via msgBus.Broadcast(), a single subscriber with buffered channel persists to activity_logs table. Coverage expanded from 3 agent CRUD actions to ~65 audit points across all HTTP handlers and WebSocket RPC methods including agents, providers, skills, MCP servers, cron, sessions, teams, pairing, and more.
This commit is contained in:
1 parent
6eb33f9cea
commit
9115169c03
37 files changed
+241
-56
No files matched your search
+43
-1
@@ -655,7 +655,7 @@ func runGateway() {
|
||||
server.SetPolicyEngine(permPE)
|
||||
server.SetPairingService(pgStores.Pairing)
|
||||
server.SetMessageBus(msgBus)
|
||||
server.SetOAuthHandler(httpapi.NewOAuthHandler(cfg.Gateway.Token, pgStores.Providers, pgStores.ConfigSecrets, providerRegistry))
|
||||
server.SetOAuthHandler(httpapi.NewOAuthHandler(cfg.Gateway.Token, pgStores.Providers, pgStores.ConfigSecrets, providerRegistry, msgBus))
|
||||
|
||||
// contextFileInterceptor is created inside wireExtras.
|
||||
// Declared here so it can be passed to registerAllMethods → AgentsMethods
|
||||
@@ -821,6 +821,43 @@ func runGateway() {
|
||||
// Wire channel event subscribers (cache invalidation, pairing, cascade disable)
|
||||
wireChannelEventSubscribers(msgBus, server, pgStores, channelMgr, instanceLoader, pairingMethods, cfg)
|
||||
|
||||
// Audit log subscriber — persists audit events to activity_logs table.
|
||||
// Uses a buffered channel with a single worker to avoid unbounded goroutines.
|
||||
var auditCh chan bus.AuditEventPayload
|
||||
if pgStores.Activity != nil {
|
||||
auditCh = make(chan bus.AuditEventPayload, 256)
|
||||
msgBus.Subscribe(bus.TopicAudit, func(evt bus.Event) {
|
||||
if evt.Name != protocol.EventAuditLog {
|
||||
return
|
||||
}
|
||||
payload, ok := evt.Payload.(bus.AuditEventPayload)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case auditCh <- payload:
|
||||
default:
|
||||
slog.Warn("audit.queue_full", "action", payload.Action)
|
||||
}
|
||||
})
|
||||
go func() {
|
||||
for payload := range auditCh {
|
||||
if err := pgStores.Activity.Log(context.Background(), &store.ActivityLog{
|
||||
ActorType: payload.ActorType,
|
||||
ActorID: payload.ActorID,
|
||||
Action: payload.Action,
|
||||
EntityType: payload.EntityType,
|
||||
EntityID: payload.EntityID,
|
||||
IPAddress: payload.IPAddress,
|
||||
Details: payload.Details,
|
||||
}); err != nil {
|
||||
slog.Warn("audit.log_failed", "action", payload.Action, "error", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
slog.Info("audit subscriber registered")
|
||||
}
|
||||
|
||||
// Setup graceful shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -1001,6 +1038,11 @@ func runGateway() {
|
||||
channelMgr.StopAll(context.Background())
|
||||
pgStores.Cron.Stop()
|
||||
|
||||
// Drain audit log queue before closing DB
|
||||
if auditCh != nil {
|
||||
close(auditCh)
|
||||
}
|
||||
|
||||
// Close provider resources (e.g. Claude CLI temp files)
|
||||
providerRegistry.Close()
|
||||
|
||||
|
||||
@@ -106,19 +106,19 @@ func wireChannelRPCMethods(server *gateway.Server, pgStores *store.Stores, chann
|
||||
|
||||
// Register channel instances WS RPC methods
|
||||
if pgStores.ChannelInstances != nil {
|
||||
methods.NewChannelInstancesMethods(pgStores.ChannelInstances, msgBus).Register(server.Router())
|
||||
methods.NewChannelInstancesMethods(pgStores.ChannelInstances, msgBus, msgBus).Register(server.Router())
|
||||
zalomethods.NewQRMethods(pgStores.ChannelInstances, msgBus).Register(server.Router())
|
||||
zalomethods.NewContactsMethods(pgStores.ChannelInstances).Register(server.Router())
|
||||
}
|
||||
|
||||
// Register agent links WS RPC methods
|
||||
if pgStores.AgentLinks != nil && pgStores.Agents != nil {
|
||||
methods.NewAgentLinksMethods(pgStores.AgentLinks, pgStores.Agents, agentRouter, msgBus).Register(server.Router())
|
||||
methods.NewAgentLinksMethods(pgStores.AgentLinks, pgStores.Agents, agentRouter, msgBus, msgBus).Register(server.Router())
|
||||
}
|
||||
|
||||
// Register agent teams WS RPC methods
|
||||
if pgStores.Teams != nil {
|
||||
methods.NewTeamsMethods(pgStores.Teams, pgStores.Agents, pgStores.AgentLinks, agentRouter, msgBus).Register(server.Router())
|
||||
methods.NewTeamsMethods(pgStores.Teams, pgStores.Agents, pgStores.AgentLinks, agentRouter, msgBus, msgBus).Register(server.Router())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +28,6 @@ func wireHTTP(stores *store.Stores, token string, msgBus *bus.MessageBus, toolsR
|
||||
summoner = httpapi.NewAgentSummoner(stores.Agents, providerReg, msgBus)
|
||||
}
|
||||
agentsH = httpapi.NewAgentsHandler(stores.Agents, token, msgBus, summoner, isOwner)
|
||||
if stores.Activity != nil {
|
||||
agentsH.SetActivityStore(stores.Activity)
|
||||
}
|
||||
}
|
||||
|
||||
if stores != nil && stores.Skills != nil {
|
||||
@@ -60,6 +57,7 @@ func wireHTTP(stores *store.Stores, token string, msgBus *bus.MessageBus, toolsR
|
||||
|
||||
if stores != nil && stores.Providers != nil {
|
||||
providersH = httpapi.NewProvidersHandler(stores.Providers, stores.ConfigSecrets, token, providerReg, gatewayAddr)
|
||||
providersH.SetMessageBus(msgBus)
|
||||
if stores.MCP != nil {
|
||||
providersH.SetMCPServerLookup(buildMCPServerLookup(stores.MCP))
|
||||
}
|
||||
|
||||
@@ -17,15 +17,15 @@ func registerAllMethods(server *gateway.Server, agents *agent.Router, sessStore
|
||||
|
||||
// Phase 1: Core methods
|
||||
methods.NewChatMethods(agents, sessStore, server.RateLimiter()).Register(router)
|
||||
methods.NewAgentsMethods(agents, cfg, cfgPath, workspace, agentStore, contextFileInterceptor).Register(router)
|
||||
methods.NewSessionsMethods(sessStore).Register(router)
|
||||
methods.NewAgentsMethods(agents, cfg, cfgPath, workspace, agentStore, contextFileInterceptor, msgBus).Register(router)
|
||||
methods.NewSessionsMethods(sessStore, msgBus).Register(router)
|
||||
methods.NewConfigMethods(cfg, cfgPath, configSecretsStore, msgBus).Register(router)
|
||||
|
||||
// Phase 2: Skills (uses SkillStore interface — PG or File)
|
||||
methods.NewSkillsMethods(skillStore).Register(router)
|
||||
|
||||
// Phase 2: Cron (store created externally, shared with gateway)
|
||||
methods.NewCronMethods(cronStore).Register(router)
|
||||
methods.NewCronMethods(cronStore, msgBus).Register(router)
|
||||
|
||||
// Phase 2: Pairing (store created externally, shared with channel manager).
|
||||
// OnApprove callback is set later by the caller after channel manager is created.
|
||||
@@ -36,7 +36,7 @@ func registerAllMethods(server *gateway.Server, agents *agent.Router, sessStore
|
||||
methods.NewUsageMethods(sessStore).Register(router)
|
||||
|
||||
// Phase 2: Exec approval (always registered — returns empty when manager is nil)
|
||||
methods.NewExecApprovalMethods(execApprovalMgr).Register(router)
|
||||
methods.NewExecApprovalMethods(execApprovalMgr, msgBus).Register(router)
|
||||
|
||||
// Phase 2: Send (outbound message routing)
|
||||
methods.NewSendMethods(msgBus).Register(router)
|
||||
|
||||
+17
-1
@@ -1,6 +1,9 @@
|
||||
package bus
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// MediaFile represents an inbound media file with its MIME type.
|
||||
// Used throughout the media pipeline to preserve content type from channel download to storage.
|
||||
@@ -77,6 +80,7 @@ const (
|
||||
TopicCacheGroupFileWriters = "cache:group_file_writers"
|
||||
TopicCacheSkillGrants = "cache:skill_grants"
|
||||
TopicCacheMCP = "cache:mcp"
|
||||
TopicAudit = "audit"
|
||||
TopicChannelStreaming = "channel-streaming"
|
||||
TopicConfigChanged = "config:changed"
|
||||
TopicPairingRevoked = "pairing:revoked"
|
||||
@@ -102,6 +106,18 @@ type AgentStatusChangedPayload struct {
|
||||
NewStatus string `json:"new_status"`
|
||||
}
|
||||
|
||||
// AuditEventPayload carries audit log data emitted by handlers.
|
||||
// A single subscriber persists these to the activity_logs table.
|
||||
type AuditEventPayload struct {
|
||||
ActorType string `json:"actor_type"`
|
||||
ActorID string `json:"actor_id"`
|
||||
Action string `json:"action"`
|
||||
EntityType string `json:"entity_type"`
|
||||
EntityID string `json:"entity_id"`
|
||||
IPAddress string `json:"ip_address,omitempty"`
|
||||
Details json.RawMessage `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// CacheInvalidatePayload signals cache layers to evict stale entries.
|
||||
// Used with protocol.EventCacheInvalidate events.
|
||||
type CacheInvalidatePayload struct {
|
||||
|
||||
@@ -20,10 +20,11 @@ type AgentLinksMethods struct {
|
||||
agentStore store.AgentStore
|
||||
agentRouter *agent.Router // for cache invalidation when links change
|
||||
msgBus *bus.MessageBus // for pub/sub cache invalidation
|
||||
eventBus bus.EventPublisher
|
||||
}
|
||||
|
||||
func NewAgentLinksMethods(linkStore store.AgentLinkStore, agentStore store.AgentStore, agentRouter *agent.Router, msgBus *bus.MessageBus) *AgentLinksMethods {
|
||||
return &AgentLinksMethods{linkStore: linkStore, agentStore: agentStore, agentRouter: agentRouter, msgBus: msgBus}
|
||||
func NewAgentLinksMethods(linkStore store.AgentLinkStore, agentStore store.AgentStore, agentRouter *agent.Router, msgBus *bus.MessageBus, eventBus bus.EventPublisher) *AgentLinksMethods {
|
||||
return &AgentLinksMethods{linkStore: linkStore, agentStore: agentStore, agentRouter: agentRouter, msgBus: msgBus, eventBus: eventBus}
|
||||
}
|
||||
|
||||
func (m *AgentLinksMethods) Register(router *gateway.MethodRouter) {
|
||||
@@ -181,6 +182,7 @@ func (m *AgentLinksMethods) handleCreate(ctx context.Context, client *gateway.Cl
|
||||
m.agentRouter.InvalidateAgent(targetAgent.AgentKey)
|
||||
}
|
||||
m.emitTeamCacheInvalidate()
|
||||
emitAudit(m.eventBus, client, "agent_link.created", "agent_link", link.ID.String())
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"link": link,
|
||||
@@ -280,6 +282,7 @@ func (m *AgentLinksMethods) handleUpdate(ctx context.Context, client *gateway.Cl
|
||||
// Invalidate affected agents so AGENTS.md gets regenerated
|
||||
m.invalidateLinkAgents(ctx, linkID)
|
||||
m.emitTeamCacheInvalidate()
|
||||
emitAudit(m.eventBus, client, "agent_link.updated", "agent_link", linkID.String())
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
@@ -349,6 +352,7 @@ func (m *AgentLinksMethods) handleDelete(ctx context.Context, client *gateway.Cl
|
||||
m.invalidateLinkAgentsByID(ctx, link.SourceAgentID, link.TargetAgentID)
|
||||
}
|
||||
m.emitTeamCacheInvalidate()
|
||||
emitAudit(m.eventBus, client, "agent_link.deleted", "agent_link", linkID.String())
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
@@ -24,10 +25,11 @@ type AgentsMethods struct {
|
||||
workspace string
|
||||
agentStore store.AgentStore
|
||||
interceptor *tools.ContextFileInterceptor // invalidated on file writes
|
||||
eventBus bus.EventPublisher
|
||||
}
|
||||
|
||||
func NewAgentsMethods(agents *agent.Router, cfg *config.Config, cfgPath, workspace string, agentStore store.AgentStore, interceptor *tools.ContextFileInterceptor) *AgentsMethods {
|
||||
return &AgentsMethods{agents: agents, cfg: cfg, cfgPath: cfgPath, workspace: workspace, agentStore: agentStore, interceptor: interceptor}
|
||||
func NewAgentsMethods(agents *agent.Router, cfg *config.Config, cfgPath, workspace string, agentStore store.AgentStore, interceptor *tools.ContextFileInterceptor, eventBus bus.EventPublisher) *AgentsMethods {
|
||||
return &AgentsMethods{agents: agents, cfg: cfg, cfgPath: cfgPath, workspace: workspace, agentStore: agentStore, interceptor: interceptor, eventBus: eventBus}
|
||||
}
|
||||
|
||||
// isOwnerUser checks if the given user ID is in the configured owner IDs.
|
||||
|
||||
@@ -163,4 +163,5 @@ func (m *AgentsMethods) handleCreate(ctx context.Context, client *gateway.Client
|
||||
"name": params.Name,
|
||||
"workspace": ws,
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "agent.created", "agent", agentID)
|
||||
}
|
||||
@@ -97,4 +97,5 @@ func (m *AgentsMethods) handleDelete(ctx context.Context, client *gateway.Client
|
||||
"agentId": params.AgentID,
|
||||
"removedBindings": removedBindings,
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "agent.deleted", "agent", params.AgentID)
|
||||
}
|
||||
@@ -159,4 +159,5 @@ func (m *AgentsMethods) handleUpdate(ctx context.Context, client *gateway.Client
|
||||
"ok": true,
|
||||
"agentId": params.AgentID,
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "agent.updated", "agent", params.AgentID)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package methods
|
||||
|
||||
import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
|
||||
// emitAudit broadcasts an audit event via eventBus for async persistence.
|
||||
func emitAudit(pub bus.EventPublisher, client *gateway.Client, action, entityType, entityID string) {
|
||||
if pub == nil {
|
||||
return
|
||||
}
|
||||
pub.Broadcast(bus.Event{
|
||||
Name: protocol.EventAuditLog,
|
||||
Payload: bus.AuditEventPayload{
|
||||
ActorType: "user",
|
||||
ActorID: client.UserID(),
|
||||
Action: action,
|
||||
EntityType: entityType,
|
||||
EntityID: entityID,
|
||||
IPAddress: client.RemoteAddr(),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -16,13 +16,14 @@ import (
|
||||
|
||||
// ChannelInstancesMethods handles channel instance CRUD via WebSocket RPC.
|
||||
type ChannelInstancesMethods struct {
|
||||
store store.ChannelInstanceStore
|
||||
msgBus *bus.MessageBus
|
||||
store store.ChannelInstanceStore
|
||||
msgBus *bus.MessageBus
|
||||
eventBus bus.EventPublisher
|
||||
}
|
||||
|
||||
// NewChannelInstancesMethods creates a new handler for channel instance management.
|
||||
func NewChannelInstancesMethods(s store.ChannelInstanceStore, msgBus *bus.MessageBus) *ChannelInstancesMethods {
|
||||
return &ChannelInstancesMethods{store: s, msgBus: msgBus}
|
||||
func NewChannelInstancesMethods(s store.ChannelInstanceStore, msgBus *bus.MessageBus, eventBus bus.EventPublisher) *ChannelInstancesMethods {
|
||||
return &ChannelInstancesMethods{store: s, msgBus: msgBus, eventBus: eventBus}
|
||||
}
|
||||
|
||||
// Register registers all channel instance RPC methods.
|
||||
@@ -141,6 +142,7 @@ func (m *ChannelInstancesMethods) handleCreate(ctx context.Context, client *gate
|
||||
}
|
||||
|
||||
m.emitCacheInvalidate()
|
||||
emitAudit(m.eventBus, client, "channel_instance.created", "channel_instance", inst.ID.String())
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, maskInstance(*inst)))
|
||||
}
|
||||
|
||||
@@ -173,6 +175,7 @@ func (m *ChannelInstancesMethods) handleUpdate(ctx context.Context, client *gate
|
||||
}
|
||||
|
||||
m.emitCacheInvalidate()
|
||||
emitAudit(m.eventBus, client, "channel_instance.updated", "channel_instance", id.String())
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"status": "updated"}))
|
||||
}
|
||||
|
||||
@@ -209,6 +212,7 @@ func (m *ChannelInstancesMethods) handleDelete(ctx context.Context, client *gate
|
||||
}
|
||||
|
||||
m.emitCacheInvalidate()
|
||||
emitAudit(m.eventBus, client, "channel_instance.deleted", "channel_instance", id.String())
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"status": "deleted"}))
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ func (m *ConfigMethods) handleApply(ctx context.Context, client *gateway.Client,
|
||||
}
|
||||
m.cfg.ApplyEnvOverrides()
|
||||
m.broadcastChanged()
|
||||
emitAudit(m.eventBus, client, "config.applied", "config", "gateway")
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
@@ -164,6 +165,7 @@ func (m *ConfigMethods) handlePatch(ctx context.Context, client *gateway.Client,
|
||||
}
|
||||
m.cfg.ApplyEnvOverrides()
|
||||
m.broadcastChanged()
|
||||
emitAudit(m.eventBus, client, "config.patched", "config", "gateway")
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"log/slog"
|
||||
"regexp"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
@@ -16,11 +17,12 @@ var cronSlugRe = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`)
|
||||
|
||||
// CronMethods handles cron.list, cron.create, cron.update, cron.delete, cron.toggle.
|
||||
type CronMethods struct {
|
||||
service store.CronStore
|
||||
service store.CronStore
|
||||
eventBus bus.EventPublisher
|
||||
}
|
||||
|
||||
func NewCronMethods(service store.CronStore) *CronMethods {
|
||||
return &CronMethods{service: service}
|
||||
func NewCronMethods(service store.CronStore, eventBus bus.EventPublisher) *CronMethods {
|
||||
return &CronMethods{service: service, eventBus: eventBus}
|
||||
}
|
||||
|
||||
func (m *CronMethods) Register(router *gateway.MethodRouter) {
|
||||
@@ -87,6 +89,7 @@ func (m *CronMethods) handleCreate(ctx context.Context, client *gateway.Client,
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"job": job,
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "cron.created", "cron", job.ID)
|
||||
}
|
||||
|
||||
func (m *CronMethods) handleDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
@@ -111,6 +114,7 @@ func (m *CronMethods) handleDelete(ctx context.Context, client *gateway.Client,
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"deleted": true,
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "cron.deleted", "cron", params.JobID)
|
||||
}
|
||||
|
||||
func (m *CronMethods) handleToggle(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
@@ -137,6 +141,7 @@ func (m *CronMethods) handleToggle(ctx context.Context, client *gateway.Client,
|
||||
"jobId": params.JobID,
|
||||
"enabled": params.Enabled,
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "cron.toggled", "cron", params.JobID)
|
||||
}
|
||||
|
||||
func (m *CronMethods) handleStatus(_ context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
@@ -172,6 +177,7 @@ func (m *CronMethods) handleUpdate(ctx context.Context, client *gateway.Client,
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"job": job,
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "cron.updated", "cron", jobID)
|
||||
}
|
||||
|
||||
func (m *CronMethods) handleRun(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
@@ -208,6 +214,7 @@ func (m *CronMethods) handleRun(ctx context.Context, client *gateway.Client, req
|
||||
"ok": true,
|
||||
"ran": true,
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "cron.run", "cron", jobID)
|
||||
|
||||
go func() {
|
||||
if _, _, err := m.service.RunJob(jobID, force); err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
@@ -13,11 +14,12 @@ import (
|
||||
|
||||
// ExecApprovalMethods handles exec.approval.list, exec.approval.approve, exec.approval.deny.
|
||||
type ExecApprovalMethods struct {
|
||||
manager *tools.ExecApprovalManager
|
||||
manager *tools.ExecApprovalManager
|
||||
eventBus bus.EventPublisher
|
||||
}
|
||||
|
||||
func NewExecApprovalMethods(manager *tools.ExecApprovalManager) *ExecApprovalMethods {
|
||||
return &ExecApprovalMethods{manager: manager}
|
||||
func NewExecApprovalMethods(manager *tools.ExecApprovalManager, eventBus bus.EventPublisher) *ExecApprovalMethods {
|
||||
return &ExecApprovalMethods{manager: manager, eventBus: eventBus}
|
||||
}
|
||||
|
||||
func (m *ExecApprovalMethods) Register(router *gateway.MethodRouter) {
|
||||
@@ -91,6 +93,7 @@ func (m *ExecApprovalMethods) handleApprove(ctx context.Context, client *gateway
|
||||
"resolved": true,
|
||||
"decision": string(decision),
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "exec.approved", "exec", params.ID)
|
||||
}
|
||||
|
||||
func (m *ExecApprovalMethods) handleDeny(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
@@ -121,4 +124,5 @@ func (m *ExecApprovalMethods) handleDeny(ctx context.Context, client *gateway.Cl
|
||||
"resolved": true,
|
||||
"decision": "deny",
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "exec.denied", "exec", params.ID)
|
||||
}
|
||||
@@ -113,6 +113,7 @@ func (m *PairingMethods) handleApprove(ctx context.Context, client *gateway.Clie
|
||||
m.broadcaster(*protocol.NewEvent(protocol.EventDevicePairRes, map[string]any{"action": "approved"}))
|
||||
}
|
||||
|
||||
emitAudit(m.msgBus, client, "pairing.approved", "pairing", params.Code)
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"paired": paired,
|
||||
}))
|
||||
@@ -141,6 +142,7 @@ func (m *PairingMethods) handleDeny(ctx context.Context, client *gateway.Client,
|
||||
m.broadcaster(*protocol.NewEvent(protocol.EventDevicePairRes, map[string]any{"action": "denied"}))
|
||||
}
|
||||
|
||||
emitAudit(m.msgBus, client, "pairing.denied", "pairing", params.Code)
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"denied": true,
|
||||
}))
|
||||
@@ -191,6 +193,7 @@ func (m *PairingMethods) handleRevoke(ctx context.Context, client *gateway.Clien
|
||||
})
|
||||
}
|
||||
|
||||
emitAudit(m.msgBus, client, "pairing.revoked", "pairing", params.SenderID)
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"revoked": true,
|
||||
}))
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/gateway"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
@@ -13,10 +14,11 @@ import (
|
||||
// SessionsMethods handles sessions.list, sessions.preview, sessions.patch, sessions.delete, sessions.reset.
|
||||
type SessionsMethods struct {
|
||||
sessions store.SessionStore
|
||||
eventBus bus.EventPublisher
|
||||
}
|
||||
|
||||
func NewSessionsMethods(sess store.SessionStore) *SessionsMethods {
|
||||
return &SessionsMethods{sessions: sess}
|
||||
func NewSessionsMethods(sess store.SessionStore, eventBus bus.EventPublisher) *SessionsMethods {
|
||||
return &SessionsMethods{sessions: sess, eventBus: eventBus}
|
||||
}
|
||||
|
||||
func (m *SessionsMethods) Register(router *gateway.MethodRouter) {
|
||||
@@ -120,6 +122,7 @@ func (m *SessionsMethods) handlePatch(ctx context.Context, client *gateway.Clien
|
||||
"ok": true,
|
||||
"key": params.Key,
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "session.patched", "session", params.Key)
|
||||
}
|
||||
|
||||
func (m *SessionsMethods) handleDelete(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
@@ -138,6 +141,7 @@ func (m *SessionsMethods) handleDelete(ctx context.Context, client *gateway.Clie
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "session.deleted", "session", params.Key)
|
||||
}
|
||||
|
||||
func (m *SessionsMethods) handleReset(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
||||
@@ -153,4 +157,5 @@ func (m *SessionsMethods) handleReset(ctx context.Context, client *gateway.Clien
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"ok": true,
|
||||
}))
|
||||
emitAudit(m.eventBus, client, "session.reset", "session", params.Key)
|
||||
}
|
||||
@@ -20,10 +20,11 @@ type TeamsMethods struct {
|
||||
linkStore store.AgentLinkStore // for auto-creating bidirectional links
|
||||
agentRouter *agent.Router // for cache invalidation
|
||||
msgBus *bus.MessageBus // for pub/sub cache invalidation
|
||||
eventBus bus.EventPublisher
|
||||
}
|
||||
|
||||
func NewTeamsMethods(teamStore store.TeamStore, agentStore store.AgentStore, linkStore store.AgentLinkStore, agentRouter *agent.Router, msgBus *bus.MessageBus) *TeamsMethods {
|
||||
return &TeamsMethods{teamStore: teamStore, agentStore: agentStore, linkStore: linkStore, agentRouter: agentRouter, msgBus: msgBus}
|
||||
func NewTeamsMethods(teamStore store.TeamStore, agentStore store.AgentStore, linkStore store.AgentLinkStore, agentRouter *agent.Router, msgBus *bus.MessageBus, eventBus bus.EventPublisher) *TeamsMethods {
|
||||
return &TeamsMethods{teamStore: teamStore, agentStore: agentStore, linkStore: linkStore, agentRouter: agentRouter, msgBus: msgBus, eventBus: eventBus}
|
||||
}
|
||||
|
||||
// emitTeamCacheInvalidate broadcasts a cache invalidation event for team data.
|
||||
@@ -159,6 +160,7 @@ func (m *TeamsMethods) handleCreate(ctx context.Context, client *gateway.Client,
|
||||
// Invalidate agent + team tool caches so TEAM.md gets injected
|
||||
m.invalidateTeamCaches(ctx, team.ID)
|
||||
|
||||
emitAudit(m.eventBus, client, "team.created", "team", team.ID.String())
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
||||
"team": team,
|
||||
}))
|
||||
|
||||
@@ -107,6 +107,7 @@ func (m *TeamsMethods) handleDelete(ctx context.Context, client *gateway.Client,
|
||||
}
|
||||
}
|
||||
|
||||
emitAudit(m.eventBus, client, "team.deleted", "team", teamID.String())
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
// Emit team.deleted event
|
||||
@@ -224,6 +225,7 @@ func (m *TeamsMethods) handleUpdate(ctx context.Context, client *gateway.Client,
|
||||
}
|
||||
|
||||
m.invalidateTeamCaches(ctx, teamID)
|
||||
emitAudit(m.eventBus, client, "team.updated", "team", teamID.String())
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ func (m *TeamsMethods) handleAddMember(ctx context.Context, client *gateway.Clie
|
||||
|
||||
// Invalidate caches for all team members
|
||||
m.invalidateTeamCaches(ctx, teamID)
|
||||
emitAudit(m.eventBus, client, "team.member_added", "team", teamID.String())
|
||||
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
@@ -183,6 +184,7 @@ func (m *TeamsMethods) handleRemoveMember(ctx context.Context, client *gateway.C
|
||||
}
|
||||
}
|
||||
|
||||
emitAudit(m.eventBus, client, "team.member_removed", "team", teamID.String())
|
||||
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
||||
|
||||
// Emit team.member.removed event
|
||||
|
||||
+3
-22
@@ -23,12 +23,8 @@ type AgentsHandler struct {
|
||||
msgBus *bus.MessageBus // for cache invalidation events (nil = no events)
|
||||
summoner *AgentSummoner // LLM-based agent setup (nil = disabled)
|
||||
isOwner func(string) bool // checks if user ID is a system owner (nil = no owners configured)
|
||||
activity store.ActivityStore // optional audit logging (nil = disabled)
|
||||
}
|
||||
|
||||
// SetActivityStore sets the optional activity audit store.
|
||||
func (h *AgentsHandler) SetActivityStore(a store.ActivityStore) { h.activity = a }
|
||||
|
||||
// NewAgentsHandler creates a handler for agent management endpoints.
|
||||
// isOwner is a function that checks if a user ID is in GOCLAW_OWNER_IDS (nil = disabled).
|
||||
func NewAgentsHandler(agents store.AgentStore, token string, msgBus *bus.MessageBus, summoner *AgentSummoner, isOwner func(string) bool) *AgentsHandler {
|
||||
@@ -51,21 +47,6 @@ func (h *AgentsHandler) emitCacheInvalidate(kind, key string) {
|
||||
})
|
||||
}
|
||||
|
||||
// logActivity records an audit log entry if the activity store is set.
|
||||
func (h *AgentsHandler) logActivity(r *http.Request, actorID, action, entityType, entityID string) {
|
||||
if h.activity == nil {
|
||||
return
|
||||
}
|
||||
h.activity.Log(r.Context(), &store.ActivityLog{
|
||||
ActorType: "user",
|
||||
ActorID: actorID,
|
||||
Action: action,
|
||||
EntityType: entityType,
|
||||
EntityID: entityID,
|
||||
IPAddress: r.RemoteAddr,
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all agent management routes on the given mux.
|
||||
func (h *AgentsHandler) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /v1/agents", h.authMiddleware(h.handleList))
|
||||
@@ -203,7 +184,7 @@ func (h *AgentsHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
go h.summoner.SummonAgent(req.ID, req.Provider, req.Model, description)
|
||||
}
|
||||
|
||||
h.logActivity(r, userID, "agent.created", "agent", req.ID.String())
|
||||
emitAudit(h.msgBus, r, "agent.created", "agent", req.ID.String())
|
||||
writeJSON(w, http.StatusCreated, req)
|
||||
}
|
||||
|
||||
@@ -299,7 +280,7 @@ func (h *AgentsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
h.logActivity(r, userID, "agent.updated", "agent", id.String())
|
||||
emitAudit(h.msgBus, r, "agent.updated", "agent", id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"ok": "true"})
|
||||
}
|
||||
|
||||
@@ -332,6 +313,6 @@ func (h *AgentsHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
h.emitCacheInvalidate(bus.CacheKindAgent, ag.AgentKey)
|
||||
h.emitCacheInvalidate(bus.CacheKindBootstrap, id.String())
|
||||
|
||||
h.logActivity(r, userID, "agent.deleted", "agent", id.String())
|
||||
emitAudit(h.msgBus, r, "agent.deleted", "agent", id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"ok": "true"})
|
||||
}
|
||||
@@ -134,6 +134,7 @@ func (h *AgentsHandler) handleSetInstanceFile(w http.ResponseWriter, r *http.Req
|
||||
// Invalidate caches so the agent picks up the change immediately
|
||||
h.emitCacheInvalidate(bus.CacheKindBootstrap, id.String())
|
||||
|
||||
emitAudit(h.msgBus, r, "agent_instance.file_set", "agent_instance", id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
@@ -184,5 +185,6 @@ func (h *AgentsHandler) handleUpdateInstanceMetadata(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "agent_instance.metadata_updated", "agent_instance", id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||
}
|
||||
@@ -84,6 +84,7 @@ func (h *AgentsHandler) handleShare(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "agent.shared", "agent", id.String())
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"ok": "true"})
|
||||
}
|
||||
|
||||
@@ -117,6 +118,7 @@ func (h *AgentsHandler) handleRevokeShare(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "agent.share_revoked", "agent", id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"ok": "true"})
|
||||
}
|
||||
|
||||
@@ -168,6 +170,7 @@ func (h *AgentsHandler) handleRegenerate(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
go h.summoner.RegenerateAgent(id, ag.Provider, ag.Model, req.Prompt)
|
||||
|
||||
emitAudit(h.msgBus, r, "agent.regenerated", "agent", id.String())
|
||||
writeJSON(w, http.StatusAccepted, map[string]string{"ok": "true", "status": store.AgentStatusSummoning})
|
||||
}
|
||||
|
||||
@@ -213,6 +216,7 @@ func (h *AgentsHandler) handleResummon(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
go h.summoner.SummonAgent(id, ag.Provider, ag.Model, description)
|
||||
|
||||
emitAudit(h.msgBus, r, "agent.resummoned", "agent", id.String())
|
||||
writeJSON(w, http.StatusAccepted, map[string]string{"ok": "true", "status": store.AgentStatusSummoning})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
||||
)
|
||||
|
||||
// emitAudit broadcasts an audit event via msgBus for async persistence.
|
||||
func emitAudit(msgBus *bus.MessageBus, r *http.Request, action, entityType, entityID string) {
|
||||
if msgBus == nil {
|
||||
return
|
||||
}
|
||||
actorID := store.UserIDFromContext(r.Context())
|
||||
if actorID == "" {
|
||||
actorID = extractUserID(r)
|
||||
}
|
||||
if actorID == "" {
|
||||
actorID = "system"
|
||||
}
|
||||
msgBus.Broadcast(bus.Event{
|
||||
Name: protocol.EventAuditLog,
|
||||
Payload: bus.AuditEventPayload{
|
||||
ActorType: "user",
|
||||
ActorID: actorID,
|
||||
Action: action,
|
||||
EntityType: entityType,
|
||||
EntityID: entityID,
|
||||
IPAddress: r.RemoteAddr,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -116,6 +116,7 @@ func (h *BuiltinToolsHandler) handleUpdate(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "builtin_tool.updated", "builtin_tool", name)
|
||||
h.emitCacheInvalidate(name)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||
}
|
||||
@@ -181,6 +181,7 @@ func (h *ChannelInstancesHandler) handleCreate(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
h.emitCacheInvalidate()
|
||||
emitAudit(h.msgBus, r, "channel_instance.created", "channel_instance", inst.ID.String())
|
||||
writeJSON(w, http.StatusCreated, maskInstanceHTTP(*inst))
|
||||
}
|
||||
|
||||
@@ -222,6 +223,7 @@ func (h *ChannelInstancesHandler) handleUpdate(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
h.emitCacheInvalidate()
|
||||
emitAudit(h.msgBus, r, "channel_instance.updated", "channel_instance", id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
@@ -251,6 +253,7 @@ func (h *ChannelInstancesHandler) handleDelete(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
h.emitCacheInvalidate()
|
||||
emitAudit(h.msgBus, r, "channel_instance.deleted", "channel_instance", id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ func (h *CustomToolsHandler) handleCreate(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "custom_tool.created", "custom_tool", def.ID.String())
|
||||
h.emitCacheInvalidate(def.ID.String())
|
||||
writeJSON(w, http.StatusCreated, def)
|
||||
}
|
||||
@@ -200,6 +201,7 @@ func (h *CustomToolsHandler) handleUpdate(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "custom_tool.updated", "custom_tool", id.String())
|
||||
h.emitCacheInvalidate(id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||
}
|
||||
@@ -218,6 +220,7 @@ func (h *CustomToolsHandler) handleDelete(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "custom_tool.deleted", "custom_tool", id.String())
|
||||
h.emitCacheInvalidate(id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
@@ -147,6 +147,7 @@ func (h *MCPHandler) handleCreateServer(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
h.emitCacheInvalidate()
|
||||
emitAudit(h.msgBus, r, "mcp_server.created", "mcp_server", srv.ID.String())
|
||||
writeJSON(w, http.StatusCreated, srv)
|
||||
}
|
||||
|
||||
@@ -195,6 +196,7 @@ func (h *MCPHandler) handleUpdateServer(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
h.emitCacheInvalidate()
|
||||
emitAudit(h.msgBus, r, "mcp_server.updated", "mcp_server", id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
@@ -213,5 +215,6 @@ func (h *MCPHandler) handleDeleteServer(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
h.emitCacheInvalidate()
|
||||
emitAudit(h.msgBus, r, "mcp_server.deleted", "mcp_server", id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
@@ -51,6 +51,7 @@ func (h *MCPHandler) handleGrantAgent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
h.emitCacheInvalidate()
|
||||
emitAudit(h.msgBus, r, "mcp_server.agent_granted", "mcp_server", serverID.String())
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"status": "granted"})
|
||||
}
|
||||
|
||||
@@ -75,6 +76,7 @@ func (h *MCPHandler) handleRevokeAgent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
h.emitCacheInvalidate()
|
||||
emitAudit(h.msgBus, r, "mcp_server.agent_revoked", "mcp_server", serverID.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "revoked"})
|
||||
}
|
||||
|
||||
@@ -157,6 +159,7 @@ func (h *MCPHandler) handleGrantUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
h.emitCacheInvalidate()
|
||||
emitAudit(h.msgBus, r, "mcp_server.user_granted", "mcp_server", serverID.String())
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"status": "granted"})
|
||||
}
|
||||
|
||||
@@ -181,5 +184,6 @@ func (h *MCPHandler) handleRevokeUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
h.emitCacheInvalidate()
|
||||
emitAudit(h.msgBus, r, "mcp_server.user_revoked", "mcp_server", serverID.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "revoked"})
|
||||
}
|
||||
@@ -37,6 +37,7 @@ func (h *MCPHandler) handleCreateRequest(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "mcp_request.created", "mcp_request", req.ID.String())
|
||||
writeJSON(w, http.StatusCreated, req)
|
||||
}
|
||||
|
||||
@@ -80,6 +81,7 @@ func (h *MCPHandler) handleReviewRequest(w http.ResponseWriter, r *http.Request)
|
||||
h.emitCacheInvalidate()
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "mcp_request.reviewed", "mcp_request", requestID.String())
|
||||
status := "rejected"
|
||||
if req.Approved {
|
||||
status = "approved"
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/oauth"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
@@ -22,18 +23,20 @@ type OAuthHandler struct {
|
||||
provStore store.ProviderStore
|
||||
secretStore store.ConfigSecretsStore
|
||||
providerReg *providers.Registry
|
||||
msgBus *bus.MessageBus
|
||||
|
||||
mu sync.Mutex
|
||||
pending *oauth.PendingLogin // active OAuth flow (if any)
|
||||
}
|
||||
|
||||
// NewOAuthHandler creates a handler for OAuth endpoints.
|
||||
func NewOAuthHandler(token string, provStore store.ProviderStore, secretStore store.ConfigSecretsStore, providerReg *providers.Registry) *OAuthHandler {
|
||||
func NewOAuthHandler(token string, provStore store.ProviderStore, secretStore store.ConfigSecretsStore, providerReg *providers.Registry, msgBus *bus.MessageBus) *OAuthHandler {
|
||||
return &OAuthHandler{
|
||||
token: token,
|
||||
provStore: provStore,
|
||||
secretStore: secretStore,
|
||||
providerReg: providerReg,
|
||||
msgBus: msgBus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +118,7 @@ func (h *OAuthHandler) handleStart(w http.ResponseWriter, r *http.Request) {
|
||||
// Wait for callback in background, save token when done
|
||||
go h.waitForCallback(pending)
|
||||
|
||||
emitAudit(h.msgBus, r, "oauth.login_started", "oauth", "openai")
|
||||
writeJSON(w, http.StatusOK, map[string]any{"auth_url": pending.AuthURL})
|
||||
}
|
||||
|
||||
@@ -204,6 +208,7 @@ func (h *OAuthHandler) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
h.providerReg.Unregister(oauth.DefaultProviderName)
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "oauth.logout", "oauth", "openai")
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "logged out"})
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ func (m *mockSecretsStore) GetAll(_ context.Context) (map[string]string, error)
|
||||
// --- helper ---
|
||||
|
||||
func newTestOAuthHandler(token string) *OAuthHandler {
|
||||
return NewOAuthHandler(token, newMockProviderStore(), newMockSecretsStore(), nil)
|
||||
return NewOAuthHandler(token, newMockProviderStore(), newMockSecretsStore(), nil, nil)
|
||||
}
|
||||
|
||||
// --- tests ---
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/oauth"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
@@ -20,9 +21,10 @@ type ProvidersHandler struct {
|
||||
secretStore store.ConfigSecretsStore
|
||||
token string
|
||||
providerReg *providers.Registry
|
||||
gatewayAddr string // for injecting MCP bridge into Claude CLI providers
|
||||
gatewayAddr string // for injecting MCP bridge into Claude CLI providers
|
||||
mcpLookup providers.MCPServerLookup // optional: resolves per-agent MCP servers
|
||||
cliMu sync.Mutex // serializes Claude CLI provider create to prevent duplicates
|
||||
cliMu sync.Mutex // serializes Claude CLI provider create to prevent duplicates
|
||||
msgBus *bus.MessageBus
|
||||
}
|
||||
|
||||
// NewProvidersHandler creates a handler for provider management endpoints.
|
||||
@@ -30,6 +32,12 @@ func NewProvidersHandler(s store.ProviderStore, secretStore store.ConfigSecretsS
|
||||
return &ProvidersHandler{store: s, secretStore: secretStore, token: token, providerReg: providerReg, gatewayAddr: gatewayAddr}
|
||||
}
|
||||
|
||||
// SetMessageBus sets the message bus for audit event broadcasting.
|
||||
// Must be called before serving requests (not thread-safe).
|
||||
func (h *ProvidersHandler) SetMessageBus(msgBus *bus.MessageBus) {
|
||||
h.msgBus = msgBus
|
||||
}
|
||||
|
||||
// SetMCPServerLookup sets the per-agent MCP server lookup for Claude CLI providers.
|
||||
// Must be called before serving requests (not thread-safe).
|
||||
func (h *ProvidersHandler) SetMCPServerLookup(lookup providers.MCPServerLookup) {
|
||||
@@ -189,6 +197,7 @@ func (h *ProvidersHandler) handleCreateProvider(w http.ResponseWriter, r *http.R
|
||||
// Register in-memory so verify/chat work without restart
|
||||
h.registerInMemory(&p)
|
||||
|
||||
emitAudit(h.msgBus, r, "provider.created", "provider", p.ID.String())
|
||||
maskAPIKey(&p)
|
||||
writeJSON(w, http.StatusCreated, p)
|
||||
}
|
||||
@@ -284,6 +293,7 @@ func (h *ProvidersHandler) handleUpdateProvider(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "provider.updated", "provider", id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
@@ -311,5 +321,6 @@ func (h *ProvidersHandler) handleDeleteProvider(w http.ResponseWriter, r *http.R
|
||||
h.providerReg.Unregister(providerName)
|
||||
}
|
||||
|
||||
emitAudit(h.msgBus, r, "provider.deleted", "provider", id.String())
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
@@ -124,6 +124,7 @@ func (h *SkillsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
h.skills.BumpVersion()
|
||||
emitAudit(h.msgBus, r, "skill.updated", "skill", idStr)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"ok": "true"})
|
||||
}
|
||||
|
||||
@@ -146,6 +147,7 @@ func (h *SkillsHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
h.skills.BumpVersion()
|
||||
emitAudit(h.msgBus, r, "skill.deleted", "skill", idStr)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"ok": "true"})
|
||||
}
|
||||
|
||||
@@ -371,5 +373,6 @@ func (h *SkillsHandler) handleToggle(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
h.skills.BumpVersion()
|
||||
emitAudit(h.msgBus, r, "skill.toggled", "skill", idStr)
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "enabled": body.Enabled, "status": newStatus})
|
||||
}
|
||||
@@ -69,6 +69,7 @@ func (h *SkillsHandler) handleGrantAgent(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
h.skills.BumpVersion()
|
||||
h.emitCacheInvalidate(bus.CacheKindSkillGrants, "")
|
||||
emitAudit(h.msgBus, r, "skill.grant_changed", "skill", idStr)
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"ok": "true"})
|
||||
}
|
||||
|
||||
@@ -95,6 +96,7 @@ func (h *SkillsHandler) handleRevokeAgent(w http.ResponseWriter, r *http.Request
|
||||
|
||||
h.skills.BumpVersion()
|
||||
h.emitCacheInvalidate(bus.CacheKindSkillGrants, "")
|
||||
emitAudit(h.msgBus, r, "skill.grant_changed", "skill", idStr)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"ok": "true"})
|
||||
}
|
||||
|
||||
@@ -131,6 +133,7 @@ func (h *SkillsHandler) handleGrantUser(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
h.skills.BumpVersion()
|
||||
h.emitCacheInvalidate(bus.CacheKindSkillGrants, "")
|
||||
emitAudit(h.msgBus, r, "skill.grant_changed", "skill", idStr)
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"ok": "true"})
|
||||
}
|
||||
|
||||
@@ -155,6 +158,7 @@ func (h *SkillsHandler) handleRevokeUser(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
h.skills.BumpVersion()
|
||||
h.emitCacheInvalidate(bus.CacheKindSkillGrants, "")
|
||||
emitAudit(h.msgBus, r, "skill.grant_changed", "skill", idStr)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"ok": "true"})
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +185,7 @@ func (h *SkillsHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
h.skills.BumpVersion()
|
||||
emitAudit(h.msgBus, r, "skill.uploaded", "skill", slug)
|
||||
slog.Info("skill uploaded", "id", id, "slug", slug, "version", version, "size", header.Size)
|
||||
|
||||
// Scan and check dependencies
|
||||
|
||||
@@ -74,6 +74,9 @@ const (
|
||||
// Cache invalidation events (internal, not forwarded to WS clients).
|
||||
EventCacheInvalidate = "cache.invalidate"
|
||||
|
||||
// Audit log event (internal, not forwarded to WS clients).
|
||||
EventAuditLog = "audit.log"
|
||||
|
||||
// Zalo Personal QR login events (client-scoped, not broadcast).
|
||||
EventZaloPersonalQRCode = "zalo.personal.qr.code"
|
||||
EventZaloPersonalQRDone = "zalo.personal.qr.done"
|
||||
|
||||
Reference in new issue
Block a user