Files
goclaw/internal/http/builtin_tools.go
T
Goon 75c570e951 feat(security): credentialed exec + HTTP RBAC + API key cache (#197)
- 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
2026-03-15 20:13:18 +07:00

114 lines
3.6 KiB
Go

package http
import (
"encoding/json"
"log/slog"
"net/http"
"github.com/nextlevelbuilder/goclaw/internal/bus"
"github.com/nextlevelbuilder/goclaw/internal/i18n"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
// BuiltinToolsHandler handles built-in tool management endpoints.
// Built-in tools are seeded at startup; only enabled and settings are editable.
type BuiltinToolsHandler struct {
store store.BuiltinToolStore
token string
msgBus *bus.MessageBus
}
// NewBuiltinToolsHandler creates a handler for built-in tool management endpoints.
func NewBuiltinToolsHandler(s store.BuiltinToolStore, token string, msgBus *bus.MessageBus) *BuiltinToolsHandler {
return &BuiltinToolsHandler{store: s, token: token, msgBus: msgBus}
}
// RegisterRoutes registers all built-in tool routes on the given mux.
func (h *BuiltinToolsHandler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /v1/tools/builtin", h.auth(h.handleList))
mux.HandleFunc("GET /v1/tools/builtin/{name}", h.auth(h.handleGet))
mux.HandleFunc("PUT /v1/tools/builtin/{name}", h.auth(h.handleUpdate))
}
func (h *BuiltinToolsHandler) auth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
}
func (h *BuiltinToolsHandler) emitCacheInvalidate(key string) {
if h.msgBus == nil {
return
}
h.msgBus.Broadcast(bus.Event{
Name: protocol.EventCacheInvalidate,
Payload: bus.CacheInvalidatePayload{Kind: bus.CacheKindBuiltinTools, Key: key},
})
}
func (h *BuiltinToolsHandler) handleList(w http.ResponseWriter, r *http.Request) {
result, err := h.store.List(r.Context())
if err != nil {
slog.Error("builtin_tools.list", "error", err)
locale := extractLocale(r)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToList, "tools")})
return
}
writeJSON(w, http.StatusOK, map[string]any{"tools": result})
}
func (h *BuiltinToolsHandler) handleGet(w http.ResponseWriter, r *http.Request) {
locale := extractLocale(r)
name := r.PathValue("name")
if name == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "name")})
return
}
def, err := h.store.Get(r.Context(), name)
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "tool", name)})
return
}
writeJSON(w, http.StatusOK, def)
}
func (h *BuiltinToolsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
locale := extractLocale(r)
name := r.PathValue("name")
if name == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "name")})
return
}
var updates map[string]any
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&updates); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
return
}
// Only allow enabled and settings to be updated
allowed := make(map[string]any)
if v, ok := updates["enabled"]; ok {
allowed["enabled"] = v
}
if v, ok := updates["settings"]; ok {
allowed["settings"] = v
}
if len(allowed) == 0 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidUpdates)})
return
}
if err := h.store.Update(r.Context(), name, allowed); err != nil {
slog.Error("builtin_tools.update", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
emitAudit(h.msgBus, r, "builtin_tool.updated", "builtin_tool", name)
h.emitCacheInvalidate(name)
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}