package http import ( "encoding/json" "log/slog" "net/http" "github.com/nextlevelbuilder/goclaw/internal/i18n" "github.com/nextlevelbuilder/goclaw/internal/permissions" "github.com/nextlevelbuilder/goclaw/internal/store" "github.com/nextlevelbuilder/goclaw/internal/tools" ) // ToolsInvokeHandler handles POST /v1/tools/invoke (direct tool invocation). type ToolsInvokeHandler struct { registry *tools.Registry token string agentStore store.AgentStore // nil if not configured } // NewToolsInvokeHandler creates a handler for the tools invoke endpoint. func NewToolsInvokeHandler(registry *tools.Registry, token string, agentStore store.AgentStore) *ToolsInvokeHandler { return &ToolsInvokeHandler{ registry: registry, token: token, agentStore: agentStore, } } type toolsInvokeRequest struct { Tool string `json:"tool"` Action string `json:"action,omitempty"` Args map[string]any `json:"args"` SessionKey string `json:"sessionKey,omitempty"` AgentID string `json:"agentId,omitempty"` DryRun bool `json:"dryRun,omitempty"` Channel string `json:"channel,omitempty"` // tool context: channel name ChatID string `json:"chatId,omitempty"` // tool context: chat ID PeerKind string `json:"peerKind,omitempty"` // tool context: "direct" or "group" } func (h *ToolsInvokeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { locale := extractLocale(r) if r.Method != http.MethodPost { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": i18n.T(locale, i18n.MsgMethodNotAllowed)}) return } auth := resolveAuth(r, h.token) if !auth.Authenticated { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)}) return } if !permissions.HasMinRole(auth.Role, permissions.RoleOperator) { writeJSON(w, http.StatusForbidden, map[string]string{"error": i18n.T(locale, i18n.MsgPermissionDenied, r.URL.Path)}) return } var req toolsInvokeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)}) return } if req.Tool == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "tool")}) return } slog.Info("tools invoke request", "tool", req.Tool, "dry_run", req.DryRun) if req.DryRun { // Just check if tool exists and return its schema tool, ok := h.registry.Get(req.Tool) if !ok { writeToolError(w, http.StatusNotFound, "NOT_FOUND", i18n.T(locale, i18n.MsgNotFound, "tool", req.Tool)) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "tool": req.Tool, "description": tool.Description(), "parameters": tool.Parameters(), "dryRun": true, }) return } // Inject userID and agentID into context for interceptors (bootstrap, memory). ctx := r.Context() if userID := extractUserID(r); userID != "" { ctx = store.WithUserID(ctx, userID) } agentIDStr := req.AgentID if agentIDStr == "" { agentIDStr = extractAgentID(r, "") } if agentIDStr != "" && h.agentStore != nil { ag, err := h.agentStore.GetByKey(ctx, agentIDStr) if err == nil { ctx = store.WithAgentID(ctx, ag.ID) } } // Inject tool context keys (channel, chatID, peerKind) for message routing. if req.Channel != "" { ctx = tools.WithToolChannel(ctx, req.Channel) } if req.ChatID != "" { ctx = tools.WithToolChatID(ctx, req.ChatID) } if req.PeerKind != "" { ctx = tools.WithToolPeerKind(ctx, req.PeerKind) } // Execute the tool args := req.Args if args == nil { args = make(map[string]any) } // If action is specified, add it to args if req.Action != "" { args["action"] = req.Action } result := h.registry.ExecuteWithContext(ctx, req.Tool, args, "http", "api", "direct", "", nil) if result.IsError { writeToolError(w, http.StatusBadRequest, "TOOL_ERROR", result.ForLLM) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "result": map[string]any{ "output": result.ForLLM, "forUser": result.ForUser, "metadata": map[string]any{}, }, }) } func writeToolError(w http.ResponseWriter, status int, code, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]any{ "error": map[string]string{ "code": code, "message": message, }, }) }