mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-11 02:10:51 +00:00
75c570e951
- Secure CLI credential injection via AES-256-GCM encrypted env vars - API key management with fine-grained RBAC scopes - resolveAuth/requireAuth middleware across all 25+ HTTP handlers - In-memory API key cache with TTL, negative caching, pubsub invalidation - Sandbox-first execution (fails if unavailable, no silent fallback) - Credential scrubbing, constant-time token comparison, Admin-only CLI creds - SQL migration 000020: secure_cli_binaries + api_keys tables - 14 unit tests for cache and RBAC with race detector Closes #197
142 lines
3.9 KiB
Go
142 lines
3.9 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/permissions"
|
|
"github.com/nextlevelbuilder/goclaw/internal/sessions"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// WakeHandler handles POST /v1/agents/{id}/wake — external trigger API.
|
|
// Allows orchestrators (Paperclip, n8n, etc.) to trigger agent runs via HTTP.
|
|
type WakeHandler struct {
|
|
agents *agent.Router
|
|
token string
|
|
}
|
|
|
|
// NewWakeHandler creates a handler for the wake endpoint.
|
|
func NewWakeHandler(agents *agent.Router, token string) *WakeHandler {
|
|
return &WakeHandler{agents: agents, token: token}
|
|
}
|
|
|
|
// RegisterRoutes registers wake routes on the given mux.
|
|
func (h *WakeHandler) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("POST /v1/agents/{id}/wake", h.handleWake)
|
|
}
|
|
|
|
type wakeRequest struct {
|
|
Message string `json:"message"`
|
|
SessionKey string `json:"session_key,omitempty"`
|
|
UserID string `json:"user_id,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
type wakeResponse struct {
|
|
Content string `json:"content"`
|
|
RunID string `json:"run_id"`
|
|
Usage *wakeUsage `json:"usage,omitempty"`
|
|
}
|
|
|
|
type wakeUsage struct {
|
|
PromptTokens int `json:"prompt_tokens"`
|
|
CompletionTokens int `json:"completion_tokens"`
|
|
TotalTokens int `json:"total_tokens"`
|
|
}
|
|
|
|
func (h *WakeHandler) handleWake(w http.ResponseWriter, r *http.Request) {
|
|
locale := extractLocale(r)
|
|
|
|
// Auth + RBAC check (gateway token or API key, operator required for POST)
|
|
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
|
|
}
|
|
|
|
agentID := r.PathValue("id")
|
|
if agentID == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "agent")})
|
|
return
|
|
}
|
|
|
|
// Limit request body size
|
|
const maxBodySize = 1 << 20 // 1MB
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
|
|
|
|
var req wakeRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidRequest, err.Error())})
|
|
return
|
|
}
|
|
|
|
if req.Message == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "message is required"})
|
|
return
|
|
}
|
|
|
|
loop, err := h.agents.Get(agentID)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "agent", agentID)})
|
|
return
|
|
}
|
|
|
|
// Build session key
|
|
sessionKey := req.SessionKey
|
|
if sessionKey == "" {
|
|
sessionKey = sessions.SessionKey(agentID, "wake-"+uuid.NewString()[:8])
|
|
}
|
|
|
|
userID := req.UserID
|
|
if userID == "" {
|
|
userID = extractUserID(r)
|
|
}
|
|
|
|
ctx := store.WithLocale(r.Context(), locale)
|
|
if userID != "" {
|
|
ctx = store.WithUserID(ctx, userID)
|
|
}
|
|
|
|
runID := uuid.NewString()
|
|
slog.Info("wake request", "agent", agentID, "user", userID, "session", sessionKey)
|
|
|
|
result, err := loop.Run(ctx, agent.RunRequest{
|
|
SessionKey: sessionKey,
|
|
Message: req.Message,
|
|
Channel: "wake",
|
|
ChatID: "api",
|
|
RunID: runID,
|
|
UserID: userID,
|
|
Stream: false,
|
|
})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("agent run failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
resp := wakeResponse{
|
|
Content: result.Content,
|
|
RunID: runID,
|
|
}
|
|
if result.Usage != nil {
|
|
resp.Usage = &wakeUsage{
|
|
PromptTokens: result.Usage.PromptTokens,
|
|
CompletionTokens: result.Usage.CompletionTokens,
|
|
TotalTokens: result.Usage.TotalTokens,
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|