mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-17 00:16:57 +00:00
Three related fixes to agent router identity handling: 1. Canonicalize on resolve — Router.Get now stores entries under the canonical tenantID:agent_key after a successful resolver call, regardless of whether the caller passed a UUID or agent_key. Prevents fragmentation where the same logical agent would occupy two cache slots. Callers that pass the UUID form are now un-cached and resolve on every call; all production callers pass agent_key today. 2. Exact-segment match — matchAgentCacheKey helper replaces HasSuffix in Router.Remove and Router.InvalidateAgent. Prevents substring collisions like "tenantX:sub-foo" being wiped when invalidating "foo". Rejects empty agentKey to guard against wildcard wipes. 3. Tenant-scoped IsRunning (C6) — Router.IsRunning now accepts ctx and uses agentCacheKey for lookup. Pre-fix, the bare r.agents[agentID] lookup always returned false in tenant-scoped deployments, causing agents.list to report every live agent as idle. Updates the sole caller at gateway/methods/agents.go:134. Phase 2 of agent identity hardening (TD-3).
149 lines
4.8 KiB
Go
149 lines
4.8 KiB
Go
package methods
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
|
|
"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"
|
|
"github.com/nextlevelbuilder/goclaw/internal/permissions"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/internal/tools"
|
|
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
|
)
|
|
|
|
// AgentsMethods handles agents.list, agents.create, agents.update, agents.delete,
|
|
// agents.files.list/get/set, agent.identity.get.
|
|
type AgentsMethods struct {
|
|
agents *agent.Router
|
|
cfg *config.Config
|
|
cfgPath string
|
|
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, 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.
|
|
func (m *AgentsMethods) isOwnerUser(userID string) bool {
|
|
return canSeeAll(permissions.RoleViewer, m.cfg.Gateway.OwnerIDs, userID)
|
|
}
|
|
|
|
func (m *AgentsMethods) Register(router *gateway.MethodRouter) {
|
|
router.Register(protocol.MethodAgent, m.handleAgent)
|
|
router.Register(protocol.MethodAgentWait, m.handleAgentWait)
|
|
router.Register(protocol.MethodAgentsList, m.handleList)
|
|
router.Register(protocol.MethodAgentsCreate, m.handleCreate)
|
|
router.Register(protocol.MethodAgentsUpdate, m.handleUpdate)
|
|
router.Register(protocol.MethodAgentsDelete, m.handleDelete)
|
|
router.Register(protocol.MethodAgentsFileList, m.handleFilesList)
|
|
router.Register(protocol.MethodAgentsFileGet, m.handleFilesGet)
|
|
router.Register(protocol.MethodAgentsFileSet, m.handleFilesSet)
|
|
router.Register(protocol.MethodAgentIdentityGet, m.handleIdentityGet)
|
|
}
|
|
|
|
type agentParams struct {
|
|
AgentID string `json:"agentId"`
|
|
}
|
|
|
|
func (m *AgentsMethods) handleAgent(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
var params agentParams
|
|
if req.Params != nil {
|
|
json.Unmarshal(req.Params, ¶ms)
|
|
}
|
|
if params.AgentID == "" {
|
|
params.AgentID = "default"
|
|
}
|
|
|
|
loop, err := m.agents.Get(ctx, params.AgentID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, err.Error()))
|
|
return
|
|
}
|
|
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
|
"id": loop.ID(),
|
|
"isRunning": loop.IsRunning(),
|
|
}))
|
|
}
|
|
|
|
func (m *AgentsMethods) handleAgentWait(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
var params agentParams
|
|
if req.Params != nil {
|
|
json.Unmarshal(req.Params, ¶ms)
|
|
}
|
|
if params.AgentID == "" {
|
|
params.AgentID = "default"
|
|
}
|
|
|
|
loop, err := m.agents.Get(ctx, params.AgentID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrNotFound, err.Error()))
|
|
return
|
|
}
|
|
|
|
// Return current status (blocking wait is a future enhancement).
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
|
"id": loop.ID(),
|
|
"status": "idle",
|
|
}))
|
|
}
|
|
|
|
func (m *AgentsMethods) handleList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
if m.agentStore != nil {
|
|
locale := store.LocaleFromContext(ctx)
|
|
userID := client.UserID()
|
|
if userID == "" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgUserCtxRequired)))
|
|
return
|
|
}
|
|
|
|
var agents []store.AgentData
|
|
var err error
|
|
if m.isOwnerUser(userID) {
|
|
agents, err = m.agentStore.List(ctx, "")
|
|
} else {
|
|
agents, err = m.agentStore.ListAccessible(ctx, userID)
|
|
}
|
|
if err != nil {
|
|
slog.Warn("agents.list: store query failed", "error", err)
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgFailedToList, "agents")))
|
|
return
|
|
}
|
|
|
|
infos := make([]map[string]any, 0, len(agents))
|
|
for _, a := range agents {
|
|
if a.Status != store.AgentStatusActive {
|
|
continue
|
|
}
|
|
infos = append(infos, map[string]any{
|
|
"id": a.AgentKey,
|
|
"name": a.DisplayName,
|
|
"model": a.Model,
|
|
"provider": a.Provider,
|
|
"agentType": a.AgentType,
|
|
"status": a.Status,
|
|
"isRunning": m.agents.IsRunning(ctx, a.AgentKey),
|
|
})
|
|
}
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
|
"agents": infos,
|
|
}))
|
|
return
|
|
}
|
|
|
|
// Fallback: return router-cached agents.
|
|
infos := m.agents.ListInfo()
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
|
|
"agents": infos,
|
|
}))
|
|
}
|