ScuttleBot

scuttlebot / internal / api / server.go
Blame History Raw 157 lines
1
// Package api implements the scuttlebot HTTP management API.
2
//
3
// /v1/ endpoints require a valid Bearer token.
4
// /ui/ is served unauthenticated (static web UI).
5
// /v1/channels/{channel}/stream uses ?token= query param (EventSource limitation).
6
package api
7
8
import (
9
"log/slog"
10
"net/http"
11
12
"github.com/conflicthq/scuttlebot/internal/auth"
13
"github.com/conflicthq/scuttlebot/internal/config"
14
"github.com/conflicthq/scuttlebot/internal/registry"
15
)
16
17
// Server is the scuttlebot HTTP API server.
18
type Server struct {
19
registry *registry.Registry
20
apiKeys *auth.APIKeyStore
21
log *slog.Logger
22
bridge chatBridge // nil if bridge is disabled
23
policies *PolicyStore // nil if not configured
24
admins adminStore // nil if not configured
25
llmCfg *config.LLMConfig // nil if no LLM backends configured
26
topoMgr topologyManager // nil if topology not configured
27
cfgStore *ConfigStore // nil if config write-back not configured
28
loginRL *loginRateLimiter
29
tlsDomain string // empty if no TLS
30
}
31
32
// New creates a new API Server. Pass nil for b to disable the chat bridge.
33
// Pass nil for admins to disable admin authentication endpoints.
34
// Pass nil for llmCfg to disable AI/LLM management endpoints.
35
// Pass nil for topo to disable topology provisioning endpoints.
36
// Pass nil for cfgStore to disable config read/write endpoints.
37
func New(reg *registry.Registry, apiKeys *auth.APIKeyStore, b chatBridge, ps *PolicyStore, admins adminStore, llmCfg *config.LLMConfig, topo topologyManager, cfgStore *ConfigStore, tlsDomain string, log *slog.Logger) *Server {
38
return &Server{
39
registry: reg,
40
apiKeys: apiKeys,
41
log: log,
42
bridge: b,
43
policies: ps,
44
admins: admins,
45
llmCfg: llmCfg,
46
topoMgr: topo,
47
cfgStore: cfgStore,
48
loginRL: newLoginRateLimiter(),
49
tlsDomain: tlsDomain,
50
}
51
}
52
53
// Handler returns the HTTP handler with all routes registered.
54
// /v1/ routes require a valid Bearer token. /ui/ is served unauthenticated.
55
// Scoped routes additionally check the API key's scopes.
56
func (s *Server) Handler() http.Handler {
57
apiMux := http.NewServeMux()
58
59
// Read-scope: status, metrics (also accessible with any scope via admin).
60
apiMux.HandleFunc("GET /v1/status", s.requireScope(auth.ScopeRead, s.handleStatus))
61
apiMux.HandleFunc("GET /v1/metrics", s.requireScope(auth.ScopeRead, s.handleMetrics))
62
63
// Policies — admin scope.
64
if s.policies != nil {
65
apiMux.HandleFunc("GET /v1/settings", s.requireScope(auth.ScopeRead, s.handleGetSettings))
66
apiMux.HandleFunc("GET /v1/settings/policies", s.requireScope(auth.ScopeRead, s.handleGetPolicies))
67
apiMux.HandleFunc("PUT /v1/settings/policies", s.requireScope(auth.ScopeAdmin, s.handlePutPolicies))
68
apiMux.HandleFunc("PATCH /v1/settings/policies", s.requireScope(auth.ScopeAdmin, s.handlePatchPolicies))
69
}
70
71
// Agents — agents scope.
72
apiMux.HandleFunc("GET /v1/agents", s.requireScope(auth.ScopeAgents, s.handleListAgents))
73
apiMux.HandleFunc("GET /v1/agents/{nick}", s.requireScope(auth.ScopeAgents, s.handleGetAgent))
74
apiMux.HandleFunc("PATCH /v1/agents/{nick}", s.requireScope(auth.ScopeAgents, s.handleUpdateAgent))
75
apiMux.HandleFunc("POST /v1/agents/register", s.requireScope(auth.ScopeAgents, s.handleRegister))
76
apiMux.HandleFunc("POST /v1/agents/{nick}/rotate", s.requireScope(auth.ScopeAgents, s.handleRotate))
77
apiMux.HandleFunc("POST /v1/agents/{nick}/adopt", s.requireScope(auth.ScopeAgents, s.handleAdopt))
78
apiMux.HandleFunc("POST /v1/agents/{nick}/revoke", s.requireScope(auth.ScopeAgents, s.handleRevoke))
79
apiMux.HandleFunc("DELETE /v1/agents/{nick}", s.requireScope(auth.ScopeAgents, s.handleDelete))
80
apiMux.HandleFunc("POST /v1/agents/bulk-delete", s.requireScope(auth.ScopeAgents, s.handleBulkDeleteAgents))
81
82
// Channels — channels scope (read), chat scope (send).
83
if s.bridge != nil {
84
apiMux.HandleFunc("GET /v1/channels", s.requireScope(auth.ScopeChannels, s.handleListChannels))
85
apiMux.HandleFunc("POST /v1/channels/{channel}/join", s.requireScope(auth.ScopeChannels, s.handleJoinChannel))
86
apiMux.HandleFunc("DELETE /v1/channels/{channel}", s.requireScope(auth.ScopeChannels, s.handleDeleteChannel))
87
apiMux.HandleFunc("GET /v1/channels/{channel}/messages", s.requireScope(auth.ScopeChannels, s.handleChannelMessages))
88
apiMux.HandleFunc("POST /v1/channels/{channel}/messages", s.requireScope(auth.ScopeChat, s.handleSendMessage))
89
apiMux.HandleFunc("POST /v1/channels/{channel}/presence", s.requireScope(auth.ScopeChat, s.handleChannelPresence))
90
apiMux.HandleFunc("GET /v1/channels/{channel}/users", s.requireScope(auth.ScopeChannels, s.handleChannelUsers))
91
apiMux.HandleFunc("GET /v1/channels/{channel}/config", s.requireScope(auth.ScopeChannels, s.handleGetChannelConfig))
92
apiMux.HandleFunc("PUT /v1/channels/{channel}/config", s.requireScope(auth.ScopeAdmin, s.handlePutChannelConfig))
93
}
94
95
// Topology — topology scope.
96
if s.topoMgr != nil {
97
apiMux.HandleFunc("POST /v1/channels", s.requireScope(auth.ScopeTopology, s.handleProvisionChannel))
98
apiMux.HandleFunc("DELETE /v1/topology/channels/{channel}", s.requireScope(auth.ScopeTopology, s.handleDropChannel))
99
apiMux.HandleFunc("GET /v1/topology", s.requireScope(auth.ScopeTopology, s.handleGetTopology))
100
}
101
// Blocker escalation — agents can signal they're stuck.
102
if s.bridge != nil {
103
apiMux.HandleFunc("POST /v1/agents/{nick}/blocker", s.requireScope(auth.ScopeAgents, s.handleAgentBlocker))
104
}
105
106
// Instructions — available even without topology (uses policies store).
107
apiMux.HandleFunc("GET /v1/channels/{channel}/instructions", s.requireScope(auth.ScopeTopology, s.handleGetInstructions))
108
apiMux.HandleFunc("PUT /v1/channels/{channel}/instructions", s.requireScope(auth.ScopeTopology, s.handlePutInstructions))
109
apiMux.HandleFunc("DELETE /v1/channels/{channel}/instructions", s.requireScope(auth.ScopeTopology, s.handleDeleteInstructions))
110
111
// Config — config scope.
112
if s.cfgStore != nil {
113
apiMux.HandleFunc("GET /v1/config", s.requireScope(auth.ScopeConfig, s.handleGetConfig))
114
apiMux.HandleFunc("PUT /v1/config", s.requireScope(auth.ScopeConfig, s.handlePutConfig))
115
apiMux.HandleFunc("GET /v1/config/history", s.requireScope(auth.ScopeConfig, s.handleGetConfigHistory))
116
apiMux.HandleFunc("GET /v1/config/history/{filename}", s.requireScope(auth.ScopeConfig, s.handleGetConfigHistoryEntry))
117
}
118
119
// Admin — admin scope.
120
if s.admins != nil {
121
apiMux.HandleFunc("GET /v1/admins", s.requireScope(auth.ScopeAdmin, s.handleAdminList))
122
apiMux.HandleFunc("POST /v1/admins", s.requireScope(auth.ScopeAdmin, s.handleAdminAdd))
123
apiMux.HandleFunc("DELETE /v1/admins/{username}", s.requireScope(auth.ScopeAdmin, s.handleAdminRemove))
124
apiMux.HandleFunc("PUT /v1/admins/{username}/password", s.requireScope(auth.ScopeAdmin, s.handleAdminSetPassword))
125
}
126
127
// API key management — admin scope.
128
apiMux.HandleFunc("GET /v1/api-keys", s.requireScope(auth.ScopeAdmin, s.handleListAPIKeys))
129
apiMux.HandleFunc("POST /v1/api-keys", s.requireScope(auth.ScopeAdmin, s.handleCreateAPIKey))
130
apiMux.HandleFunc("DELETE /v1/api-keys/{id}", s.requireScope(auth.ScopeAdmin, s.handleRevokeAPIKey))
131
132
// LLM / AI gateway — bots scope.
133
apiMux.HandleFunc("GET /v1/llm/backends", s.requireScope(auth.ScopeBots, s.handleLLMBackends))
134
apiMux.HandleFunc("POST /v1/llm/backends", s.requireScope(auth.ScopeBots, s.handleLLMBackendCreate))
135
apiMux.HandleFunc("PUT /v1/llm/backends/{name}", s.requireScope(auth.ScopeBots, s.handleLLMBackendUpdate))
136
apiMux.HandleFunc("DELETE /v1/llm/backends/{name}", s.requireScope(auth.ScopeBots, s.handleLLMBackendDelete))
137
apiMux.HandleFunc("GET /v1/llm/backends/{name}/models", s.requireScope(auth.ScopeBots, s.handleLLMModels))
138
apiMux.HandleFunc("POST /v1/llm/discover", s.requireScope(auth.ScopeBots, s.handleLLMDiscover))
139
apiMux.HandleFunc("GET /v1/llm/known", s.requireScope(auth.ScopeBots, s.handleLLMKnown))
140
apiMux.HandleFunc("POST /v1/llm/complete", s.requireScope(auth.ScopeBots, s.handleLLMComplete))
141
142
outer := http.NewServeMux()
143
outer.HandleFunc("POST /login", s.handleLogin)
144
outer.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
145
http.Redirect(w, r, "/ui/", http.StatusFound)
146
})
147
outer.Handle("/ui/", s.uiFileServer())
148
// SSE stream uses ?token= auth (EventSource can't send headers), registered
149
// on outer so it bypasses the Bearer-token authMiddleware on /v1/.
150
if s.bridge != nil {
151
outer.HandleFunc("GET /v1/channels/{channel}/stream", s.handleChannelStream)
152
}
153
outer.Handle("/v1/", s.authMiddleware(apiMux))
154
155
return outer
156
}
157

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button