ScuttleBot

scuttlebot / cmd / scuttlectl / internal / apiclient / apiclient.go
Source Blame History 264 lines
4c4aa94… lmata 1 // Package apiclient is a minimal HTTP client for the scuttlebot REST API.
4c4aa94… lmata 2 package apiclient
4c4aa94… lmata 3
4c4aa94… lmata 4 import (
4c4aa94… lmata 5 "bytes"
4c4aa94… lmata 6 "encoding/json"
4c4aa94… lmata 7 "fmt"
4c4aa94… lmata 8 "io"
4c4aa94… lmata 9 "net/http"
5ac549c… lmata 10 "strings"
4c4aa94… lmata 11 )
4c4aa94… lmata 12
4c4aa94… lmata 13 // Client calls the scuttlebot REST API.
4c4aa94… lmata 14 type Client struct {
4c4aa94… lmata 15 base string
4c4aa94… lmata 16 token string
4c4aa94… lmata 17 http *http.Client
4c4aa94… lmata 18 }
4c4aa94… lmata 19
4c4aa94… lmata 20 // New creates a Client targeting baseURL (e.g. "http://localhost:8080") with
4c4aa94… lmata 21 // the given bearer token.
4c4aa94… lmata 22 func New(baseURL, token string) *Client {
4c4aa94… lmata 23 return &Client{base: baseURL, token: token, http: &http.Client{}}
4c4aa94… lmata 24 }
4c4aa94… lmata 25
4c4aa94… lmata 26 // Status returns the raw JSON bytes from GET /v1/status.
4c4aa94… lmata 27 func (c *Client) Status() (json.RawMessage, error) {
4c4aa94… lmata 28 return c.get("/v1/status")
4c4aa94… lmata 29 }
4c4aa94… lmata 30
4c4aa94… lmata 31 // ListAgents returns the raw JSON bytes from GET /v1/agents.
4c4aa94… lmata 32 func (c *Client) ListAgents() (json.RawMessage, error) {
4c4aa94… lmata 33 return c.get("/v1/agents")
4c4aa94… lmata 34 }
4c4aa94… lmata 35
4c4aa94… lmata 36 // GetAgent returns the raw JSON bytes from GET /v1/agents/{nick}.
4c4aa94… lmata 37 func (c *Client) GetAgent(nick string) (json.RawMessage, error) {
4c4aa94… lmata 38 return c.get("/v1/agents/" + nick)
4c4aa94… lmata 39 }
4c4aa94… lmata 40
4c4aa94… lmata 41 // RegisterAgent sends POST /v1/agents/register and returns raw JSON.
4c4aa94… lmata 42 func (c *Client) RegisterAgent(nick, agentType string, channels []string) (json.RawMessage, error) {
4c4aa94… lmata 43 body := map[string]any{"nick": nick}
4c4aa94… lmata 44 if agentType != "" {
4c4aa94… lmata 45 body["type"] = agentType
4c4aa94… lmata 46 }
4c4aa94… lmata 47 if len(channels) > 0 {
4c4aa94… lmata 48 body["channels"] = channels
4c4aa94… lmata 49 }
4c4aa94… lmata 50 return c.post("/v1/agents/register", body)
4c4aa94… lmata 51 }
4c4aa94… lmata 52
4c4aa94… lmata 53 // RevokeAgent sends POST /v1/agents/{nick}/revoke.
4c4aa94… lmata 54 func (c *Client) RevokeAgent(nick string) error {
4c4aa94… lmata 55 _, err := c.post("/v1/agents/"+nick+"/revoke", nil)
4c4aa94… lmata 56 return err
4c4aa94… lmata 57 }
4c4aa94… lmata 58
4c4aa94… lmata 59 // RotateAgent sends POST /v1/agents/{nick}/rotate and returns raw JSON.
4c4aa94… lmata 60 func (c *Client) RotateAgent(nick string) (json.RawMessage, error) {
4c4aa94… lmata 61 return c.post("/v1/agents/"+nick+"/rotate", nil)
4c4aa94… lmata 62 }
4c4aa94… lmata 63
5ac549c… lmata 64 // DeleteAgent sends DELETE /v1/agents/{nick}.
5ac549c… lmata 65 func (c *Client) DeleteAgent(nick string) error {
5ac549c… lmata 66 _, err := c.doNoBody("DELETE", "/v1/agents/"+nick)
5ac549c… lmata 67 return err
5ac549c… lmata 68 }
5ac549c… lmata 69
5ac549c… lmata 70 // ChannelUsers sends GET /v1/channels/{channel}/users and returns raw JSON.
5ac549c… lmata 71 func (c *Client) ChannelUsers(channel string) (json.RawMessage, error) {
5ac549c… lmata 72 return c.get("/v1/channels/" + channel + "/users")
5ac549c… lmata 73 }
5ac549c… lmata 74
5ac549c… lmata 75 // DeleteChannel sends DELETE /v1/channels/{channel}.
5ac549c… lmata 76 func (c *Client) DeleteChannel(channel string) error {
5ac549c… lmata 77 channel = strings.TrimPrefix(channel, "#")
5ac549c… lmata 78 _, err := c.doNoBody("DELETE", "/v1/channels/"+channel)
5ac549c… lmata 79 return err
5ac549c… lmata 80 }
5ac549c… lmata 81
5ac549c… lmata 82 // ListChannels sends GET /v1/channels and returns raw JSON.
5ac549c… lmata 83 func (c *Client) ListChannels() (json.RawMessage, error) {
5ac549c… lmata 84 return c.get("/v1/channels")
5ac549c… lmata 85 }
5ac549c… lmata 86
73ef90f… lmata 87 // ListLLMBackends sends GET /v1/llm/backends and returns raw JSON.
73ef90f… lmata 88 func (c *Client) ListLLMBackends() (json.RawMessage, error) {
73ef90f… lmata 89 return c.get("/v1/llm/backends")
73ef90f… lmata 90 }
73ef90f… lmata 91
5ac549c… lmata 92 // GetLLMBackend sends GET /v1/llm/backends and finds the named backend, returning raw JSON.
5ac549c… lmata 93 func (c *Client) GetLLMBackend(name string) (json.RawMessage, error) {
5ac549c… lmata 94 raw, err := c.get("/v1/llm/backends")
5ac549c… lmata 95 if err != nil {
5ac549c… lmata 96 return nil, err
5ac549c… lmata 97 }
5ac549c… lmata 98 var resp struct {
5ac549c… lmata 99 Backends []json.RawMessage `json:"backends"`
5ac549c… lmata 100 }
5ac549c… lmata 101 if err := json.Unmarshal(raw, &resp); err != nil {
5ac549c… lmata 102 return nil, err
5ac549c… lmata 103 }
5ac549c… lmata 104 for _, b := range resp.Backends {
5ac549c… lmata 105 var named struct {
5ac549c… lmata 106 Name string `json:"name"`
5ac549c… lmata 107 }
5ac549c… lmata 108 if json.Unmarshal(b, &named) == nil && named.Name == name {
5ac549c… lmata 109 return b, nil
5ac549c… lmata 110 }
5ac549c… lmata 111 }
5ac549c… lmata 112 return nil, fmt.Errorf("backend %q not found", name)
5ac549c… lmata 113 }
5ac549c… lmata 114
5ac549c… lmata 115 // CreateLLMBackend sends POST /v1/llm/backends.
5ac549c… lmata 116 func (c *Client) CreateLLMBackend(cfg map[string]any) error {
5ac549c… lmata 117 _, err := c.post("/v1/llm/backends", cfg)
5ac549c… lmata 118 return err
5ac549c… lmata 119 }
5ac549c… lmata 120
5ac549c… lmata 121 // DeleteLLMBackend sends DELETE /v1/llm/backends/{name}.
5ac549c… lmata 122 func (c *Client) DeleteLLMBackend(name string) error {
5ac549c… lmata 123 _, err := c.doNoBody("DELETE", "/v1/llm/backends/"+name)
5ac549c… lmata 124 return err
5ac549c… lmata 125 }
5ac549c… lmata 126
5ac549c… lmata 127 // ListAdmins sends GET /v1/admins and returns raw JSON.
5ac549c… lmata 128 func (c *Client) ListAdmins() (json.RawMessage, error) {
5ac549c… lmata 129 return c.get("/v1/admins")
5ac549c… lmata 130 }
5ac549c… lmata 131
5ac549c… lmata 132 // AddAdmin sends POST /v1/admins and returns raw JSON.
5ac549c… lmata 133 func (c *Client) AddAdmin(username, password string) (json.RawMessage, error) {
5ac549c… lmata 134 return c.post("/v1/admins", map[string]string{"username": username, "password": password})
5ac549c… lmata 135 }
5ac549c… lmata 136
5ac549c… lmata 137 // RemoveAdmin sends DELETE /v1/admins/{username}.
5ac549c… lmata 138 func (c *Client) RemoveAdmin(username string) error {
5ac549c… lmata 139 _, err := c.doNoBody("DELETE", "/v1/admins/"+username)
5ac549c… lmata 140 return err
68677f9… noreply 141 }
68677f9… noreply 142
68677f9… noreply 143 // ListAPIKeys returns GET /v1/api-keys.
68677f9… noreply 144 func (c *Client) ListAPIKeys() (json.RawMessage, error) {
68677f9… noreply 145 return c.get("/v1/api-keys")
68677f9… noreply 146 }
68677f9… noreply 147
68677f9… noreply 148 // CreateAPIKey sends POST /v1/api-keys.
68677f9… noreply 149 func (c *Client) CreateAPIKey(name string, scopes []string, expiresIn string) (json.RawMessage, error) {
68677f9… noreply 150 body := map[string]any{"name": name, "scopes": scopes}
68677f9… noreply 151 if expiresIn != "" {
68677f9… noreply 152 body["expires_in"] = expiresIn
68677f9… noreply 153 }
68677f9… noreply 154 return c.post("/v1/api-keys", body)
68677f9… noreply 155 }
68677f9… noreply 156
68677f9… noreply 157 // RevokeAPIKey sends DELETE /v1/api-keys/{id}.
68677f9… noreply 158 func (c *Client) RevokeAPIKey(id string) error {
68677f9… noreply 159 _, err := c.doNoBody("DELETE", "/v1/api-keys/"+id)
68677f9… noreply 160 return err
a1cd907… noreply 161 }
a1cd907… noreply 162
a1cd907… noreply 163 // GetTopology returns GET /v1/topology.
a1cd907… noreply 164 func (c *Client) GetTopology() (json.RawMessage, error) {
a1cd907… noreply 165 return c.get("/v1/topology")
a1cd907… noreply 166 }
a1cd907… noreply 167
a1cd907… noreply 168 // ProvisionChannel sends POST /v1/channels.
a1cd907… noreply 169 func (c *Client) ProvisionChannel(name string) (json.RawMessage, error) {
a1cd907… noreply 170 return c.post("/v1/channels", map[string]string{"name": name})
a1cd907… noreply 171 }
a1cd907… noreply 172
a1cd907… noreply 173 // DropChannel sends DELETE /v1/topology/channels/{channel}.
a1cd907… noreply 174 func (c *Client) DropChannel(channel string) error {
a1cd907… noreply 175 _, err := c.doNoBody("DELETE", "/v1/topology/channels/"+strings.TrimPrefix(channel, "#"))
a1cd907… noreply 176 return err
a1cd907… noreply 177 }
a1cd907… noreply 178
a1cd907… noreply 179 // GetConfig returns GET /v1/config.
a1cd907… noreply 180 func (c *Client) GetConfig() (json.RawMessage, error) {
a1cd907… noreply 181 return c.get("/v1/config")
a1cd907… noreply 182 }
a1cd907… noreply 183
a1cd907… noreply 184 // GetConfigHistory returns GET /v1/config/history.
a1cd907… noreply 185 func (c *Client) GetConfigHistory() (json.RawMessage, error) {
a1cd907… noreply 186 return c.get("/v1/config/history")
a1cd907… noreply 187 }
a1cd907… noreply 188
a1cd907… noreply 189 // GetSettings returns GET /v1/settings.
a1cd907… noreply 190 func (c *Client) GetSettings() (json.RawMessage, error) {
a1cd907… noreply 191 return c.get("/v1/settings")
5ac549c… lmata 192 }
5ac549c… lmata 193
5ac549c… lmata 194 // SetAdminPassword sends PUT /v1/admins/{username}/password.
5ac549c… lmata 195 func (c *Client) SetAdminPassword(username, password string) error {
5ac549c… lmata 196 _, err := c.put("/v1/admins/"+username+"/password", map[string]string{"password": password})
5ac549c… lmata 197 return err
5ac549c… lmata 198 }
5ac549c… lmata 199
4c4aa94… lmata 200 func (c *Client) get(path string) (json.RawMessage, error) {
4c4aa94… lmata 201 return c.do("GET", path, nil)
4c4aa94… lmata 202 }
4c4aa94… lmata 203
4c4aa94… lmata 204 func (c *Client) post(path string, body any) (json.RawMessage, error) {
4c4aa94… lmata 205 var buf bytes.Buffer
4c4aa94… lmata 206 if body != nil {
4c4aa94… lmata 207 if err := json.NewEncoder(&buf).Encode(body); err != nil {
4c4aa94… lmata 208 return nil, err
4c4aa94… lmata 209 }
4c4aa94… lmata 210 }
4c4aa94… lmata 211 return c.do("POST", path, &buf)
5ac549c… lmata 212 }
5ac549c… lmata 213
5ac549c… lmata 214 func (c *Client) put(path string, body any) (json.RawMessage, error) {
5ac549c… lmata 215 var buf bytes.Buffer
5ac549c… lmata 216 if body != nil {
5ac549c… lmata 217 if err := json.NewEncoder(&buf).Encode(body); err != nil {
5ac549c… lmata 218 return nil, err
5ac549c… lmata 219 }
5ac549c… lmata 220 }
5ac549c… lmata 221 return c.do("PUT", path, &buf)
5ac549c… lmata 222 }
5ac549c… lmata 223
5ac549c… lmata 224 func (c *Client) doNoBody(method, path string) (json.RawMessage, error) {
5ac549c… lmata 225 return c.do(method, path, nil)
4c4aa94… lmata 226 }
4c4aa94… lmata 227
4c4aa94… lmata 228 func (c *Client) do(method, path string, body io.Reader) (json.RawMessage, error) {
4c4aa94… lmata 229 req, err := http.NewRequest(method, c.base+path, body)
4c4aa94… lmata 230 if err != nil {
4c4aa94… lmata 231 return nil, err
4c4aa94… lmata 232 }
4c4aa94… lmata 233 req.Header.Set("Authorization", "Bearer "+c.token)
4c4aa94… lmata 234 if body != nil {
4c4aa94… lmata 235 req.Header.Set("Content-Type", "application/json")
4c4aa94… lmata 236 }
4c4aa94… lmata 237
4c4aa94… lmata 238 resp, err := c.http.Do(req)
4c4aa94… lmata 239 if err != nil {
4c4aa94… lmata 240 return nil, fmt.Errorf("request failed: %w", err)
4c4aa94… lmata 241 }
4c4aa94… lmata 242 defer resp.Body.Close()
4c4aa94… lmata 243
4c4aa94… lmata 244 data, err := io.ReadAll(resp.Body)
4c4aa94… lmata 245 if err != nil {
4c4aa94… lmata 246 return nil, err
4c4aa94… lmata 247 }
4c4aa94… lmata 248
4c4aa94… lmata 249 if resp.StatusCode >= 400 {
4c4aa94… lmata 250 // Try to extract error message from JSON body.
4c4aa94… lmata 251 var apiErr struct {
4c4aa94… lmata 252 Error string `json:"error"`
4c4aa94… lmata 253 }
4c4aa94… lmata 254 if json.Unmarshal(data, &apiErr) == nil && apiErr.Error != "" {
4c4aa94… lmata 255 return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, apiErr.Error)
4c4aa94… lmata 256 }
4c4aa94… lmata 257 return nil, fmt.Errorf("API error %d", resp.StatusCode)
4c4aa94… lmata 258 }
4c4aa94… lmata 259
4c4aa94… lmata 260 if len(data) == 0 {
4c4aa94… lmata 261 return nil, nil
4c4aa94… lmata 262 }
4c4aa94… lmata 263 return json.RawMessage(data), nil
4c4aa94… lmata 264 }

Keyboard Shortcuts

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