proto.go

  1package server
  2
  3import (
  4	"crypto/sha256"
  5	"encoding/hex"
  6	"encoding/json"
  7	"fmt"
  8	"log/slog"
  9	"net/http"
 10	"os"
 11	"path/filepath"
 12
 13	"github.com/charmbracelet/crush/internal/app"
 14	"github.com/charmbracelet/crush/internal/config"
 15	"github.com/charmbracelet/crush/internal/db"
 16	"github.com/charmbracelet/crush/internal/lsp"
 17	"github.com/charmbracelet/crush/internal/proto"
 18	"github.com/charmbracelet/crush/internal/session"
 19)
 20
 21type controllerV1 struct {
 22	*Server
 23}
 24
 25func (c *controllerV1) handleGetConfig(w http.ResponseWriter, r *http.Request) {
 26	jsonEncode(w, c.cfg)
 27}
 28
 29func (c *controllerV1) handleGetInstances(w http.ResponseWriter, r *http.Request) {
 30	instances := []proto.Instance{}
 31	for _, ins := range c.instances.Seq2() {
 32		instances = append(instances, proto.Instance{
 33			ID:   ins.ID(),
 34			Path: ins.Path(),
 35			YOLO: ins.cfg.Permissions != nil && ins.cfg.Permissions.SkipRequests,
 36		})
 37	}
 38	jsonEncode(w, instances)
 39}
 40
 41func (c *controllerV1) handleGetInstanceLSPDiagnostics(w http.ResponseWriter, r *http.Request) {
 42	id := r.PathValue("id")
 43	ins, ok := c.instances.Get(id)
 44	if !ok {
 45		c.logError(r, "instance not found", "id", id)
 46		jsonError(w, http.StatusNotFound, "instance not found")
 47		return
 48	}
 49
 50	var lsp *lsp.Client
 51	lspName := r.PathValue("lsp")
 52	for name, client := range ins.LSPClients.Seq2() {
 53		if name == lspName {
 54			lsp = client
 55			break
 56		}
 57	}
 58
 59	if lsp == nil {
 60		c.logError(r, "LSP client not found", "id", id, "lsp", lspName)
 61		jsonError(w, http.StatusNotFound, "LSP client not found")
 62		return
 63	}
 64
 65	diagnostics := lsp.GetDiagnostics()
 66	jsonEncode(w, diagnostics)
 67}
 68
 69func (c *controllerV1) handleGetInstanceLSPs(w http.ResponseWriter, r *http.Request) {
 70	id := r.PathValue("id")
 71	ins, ok := c.instances.Get(id)
 72	if !ok {
 73		c.logError(r, "instance not found", "id", id)
 74		jsonError(w, http.StatusNotFound, "instance not found")
 75		return
 76	}
 77
 78	lspClients := ins.GetLSPStates()
 79	jsonEncode(w, lspClients)
 80}
 81
 82func (c *controllerV1) handleGetInstanceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) {
 83	id := r.PathValue("id")
 84	ins, ok := c.instances.Get(id)
 85	if !ok {
 86		c.logError(r, "instance not found", "id", id)
 87		jsonError(w, http.StatusNotFound, "instance not found")
 88		return
 89	}
 90
 91	sid := r.PathValue("sid")
 92	queued := ins.App.CoderAgent.QueuedPrompts(sid)
 93	jsonEncode(w, queued)
 94}
 95
 96func (c *controllerV1) handlePostInstanceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) {
 97	id := r.PathValue("id")
 98	ins, ok := c.instances.Get(id)
 99	if !ok {
100		c.logError(r, "instance not found", "id", id)
101		jsonError(w, http.StatusNotFound, "instance not found")
102		return
103	}
104
105	sid := r.PathValue("sid")
106	ins.App.CoderAgent.ClearQueue(sid)
107}
108
109func (c *controllerV1) handleGetInstanceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) {
110	id := r.PathValue("id")
111	ins, ok := c.instances.Get(id)
112	if !ok {
113		c.logError(r, "instance not found", "id", id)
114		jsonError(w, http.StatusNotFound, "instance not found")
115		return
116	}
117
118	sid := r.PathValue("sid")
119	if err := ins.App.CoderAgent.Summarize(r.Context(), sid); err != nil {
120		c.logError(r, "failed to summarize session", "error", err, "id", id, "sid", sid)
121		jsonError(w, http.StatusInternalServerError, "failed to summarize session")
122		return
123	}
124}
125
126func (c *controllerV1) handlePostInstanceAgentSessionCancel(w http.ResponseWriter, r *http.Request) {
127	id := r.PathValue("id")
128	ins, ok := c.instances.Get(id)
129	if !ok {
130		c.logError(r, "instance not found", "id", id)
131		jsonError(w, http.StatusNotFound, "instance not found")
132		return
133	}
134
135	sid := r.PathValue("sid")
136	if ins.App.CoderAgent != nil {
137		ins.App.CoderAgent.Cancel(sid)
138	}
139}
140
141func (c *controllerV1) handleGetInstanceAgentSession(w http.ResponseWriter, r *http.Request) {
142	id := r.PathValue("id")
143	ins, ok := c.instances.Get(id)
144	if !ok {
145		c.logError(r, "instance not found", "id", id)
146		jsonError(w, http.StatusNotFound, "instance not found")
147		return
148	}
149
150	sid := r.PathValue("sid")
151	se, err := ins.App.Sessions.Get(r.Context(), sid)
152	if err != nil {
153		c.logError(r, "failed to get session", "error", err, "id", id, "sid", sid)
154		jsonError(w, http.StatusInternalServerError, "failed to get session")
155		return
156	}
157
158	var isSessionBusy bool
159	if ins.App.CoderAgent != nil {
160		isSessionBusy = ins.App.CoderAgent.IsSessionBusy(sid)
161	}
162
163	jsonEncode(w, proto.AgentSession{
164		Session: se,
165		IsBusy:  isSessionBusy,
166	})
167}
168
169func (c *controllerV1) handlePostInstanceAgent(w http.ResponseWriter, r *http.Request) {
170	id := r.PathValue("id")
171	ins, ok := c.instances.Get(id)
172	if !ok {
173		c.logError(r, "instance not found", "id", id)
174		jsonError(w, http.StatusNotFound, "instance not found")
175		return
176	}
177
178	w.Header().Set("Accept", "application/json")
179
180	var msg proto.AgentMessage
181	if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
182		c.logError(r, "failed to decode request", "error", err)
183		jsonError(w, http.StatusBadRequest, "failed to decode request")
184		return
185	}
186
187	if ins.App.CoderAgent == nil {
188		c.logError(r, "coder agent not initialized", "id", id)
189		jsonError(w, http.StatusBadRequest, "coder agent not initialized")
190		return
191	}
192
193	// NOTE: This needs to be on the server's context because the agent runs
194	// the request asynchronously.
195	// TODO: Look into this one more and make it work synchronously.
196	if _, err := ins.App.CoderAgent.Run(c.ctx, msg.SessionID, msg.Prompt, msg.Attachments...); err != nil {
197		c.logError(r, "failed to enqueue message", "error", err, "id", id, "sid", msg.SessionID)
198		jsonError(w, http.StatusInternalServerError, "failed to enqueue message")
199		return
200	}
201}
202
203func (c *controllerV1) handleGetInstanceAgent(w http.ResponseWriter, r *http.Request) {
204	id := r.PathValue("id")
205	ins, ok := c.instances.Get(id)
206	if !ok {
207		c.logError(r, "instance not found", "id", id)
208		jsonError(w, http.StatusNotFound, "instance not found")
209		return
210	}
211
212	var agentInfo proto.AgentInfo
213	if ins.App.CoderAgent != nil {
214		agentInfo = proto.AgentInfo{
215			Model:  ins.App.CoderAgent.Model(),
216			IsBusy: ins.App.CoderAgent.IsBusy(),
217		}
218	}
219	jsonEncode(w, agentInfo)
220}
221
222func (c *controllerV1) handlePostInstanceAgentUpdate(w http.ResponseWriter, r *http.Request) {
223	id := r.PathValue("id")
224	ins, ok := c.instances.Get(id)
225	if !ok {
226		c.logError(r, "instance not found", "id", id)
227		jsonError(w, http.StatusNotFound, "instance not found")
228		return
229	}
230
231	if err := ins.App.UpdateAgentModel(); err != nil {
232		c.logError(r, "failed to update agent model", "error", err)
233		jsonError(w, http.StatusInternalServerError, "failed to update agent model")
234		return
235	}
236}
237
238func (c *controllerV1) handlePostInstanceAgentInit(w http.ResponseWriter, r *http.Request) {
239	id := r.PathValue("id")
240	ins, ok := c.instances.Get(id)
241	if !ok {
242		c.logError(r, "instance not found", "id", id)
243		jsonError(w, http.StatusNotFound, "instance not found")
244		return
245	}
246
247	if err := ins.App.InitCoderAgent(); err != nil {
248		c.logError(r, "failed to initialize coder agent", "error", err)
249		jsonError(w, http.StatusInternalServerError, "failed to initialize coder agent")
250		return
251	}
252}
253
254func (c *controllerV1) handleGetInstanceSessionHistory(w http.ResponseWriter, r *http.Request) {
255	id := r.PathValue("id")
256	ins, ok := c.instances.Get(id)
257	if !ok {
258		c.logError(r, "instance not found", "id", id)
259		jsonError(w, http.StatusNotFound, "instance not found")
260		return
261	}
262
263	sid := r.PathValue("sid")
264	historyItems, err := ins.App.History.ListBySession(r.Context(), sid)
265	if err != nil {
266		c.logError(r, "failed to list history", "error", err, "id", id, "sid", sid)
267		jsonError(w, http.StatusInternalServerError, "failed to list history")
268		return
269	}
270
271	jsonEncode(w, historyItems)
272}
273
274func (c *controllerV1) handleGetInstanceSessionMessages(w http.ResponseWriter, r *http.Request) {
275	id := r.PathValue("id")
276	ins, ok := c.instances.Get(id)
277	if !ok {
278		c.logError(r, "instance not found", "id", id)
279		jsonError(w, http.StatusNotFound, "instance not found")
280		return
281	}
282
283	sid := r.PathValue("sid")
284	messages, err := ins.App.Messages.List(r.Context(), sid)
285	if err != nil {
286		c.logError(r, "failed to list messages", "error", err, "id", id, "sid", sid)
287		jsonError(w, http.StatusInternalServerError, "failed to list messages")
288		return
289	}
290
291	jsonEncode(w, messages)
292}
293
294func (c *controllerV1) handleGetInstanceSession(w http.ResponseWriter, r *http.Request) {
295	id := r.PathValue("id")
296	ins, ok := c.instances.Get(id)
297	if !ok {
298		c.logError(r, "instance not found", "id", id)
299		jsonError(w, http.StatusNotFound, "instance not found")
300		return
301	}
302
303	sid := r.PathValue("sid")
304	session, err := ins.App.Sessions.Get(r.Context(), sid)
305	if err != nil {
306		c.logError(r, "failedto get session", "error", err, "id", id, "sid", sid)
307		jsonError(w, http.StatusInternalServerError, "failed to get session")
308		return
309	}
310
311	jsonEncode(w, session)
312}
313
314func (c *controllerV1) handlePostInstanceSessions(w http.ResponseWriter, r *http.Request) {
315	id := r.PathValue("id")
316	ins, ok := c.instances.Get(id)
317	if !ok {
318		c.logError(r, "instance not found", "id", id)
319		jsonError(w, http.StatusNotFound, "instance not found")
320		return
321	}
322
323	var args session.Session
324	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
325		c.logError(r, "failed to decode request", "error", err)
326		jsonError(w, http.StatusBadRequest, "failed to decode request")
327		return
328	}
329
330	sess, err := ins.App.Sessions.Create(r.Context(), args.Title)
331	if err != nil {
332		c.logError(r, "failed to create session", "error", err, "id", id)
333		jsonError(w, http.StatusInternalServerError, "failed to create session")
334		return
335	}
336
337	jsonEncode(w, sess)
338}
339
340func (c *controllerV1) handleGetInstanceSessions(w http.ResponseWriter, r *http.Request) {
341	id := r.PathValue("id")
342	ins, ok := c.instances.Get(id)
343	if !ok {
344		c.logError(r, "instance not found", "id", id)
345		jsonError(w, http.StatusNotFound, "instance not found")
346		return
347	}
348
349	sessions, err := ins.App.Sessions.List(r.Context())
350	if err != nil {
351		c.logError(r, "failed to list sessions", "error", err)
352		jsonError(w, http.StatusInternalServerError, "failed to list sessions")
353		return
354	}
355
356	jsonEncode(w, sessions)
357}
358
359func (c *controllerV1) handlePostInstancePermissionsGrant(w http.ResponseWriter, r *http.Request) {
360	id := r.PathValue("id")
361	ins, ok := c.instances.Get(id)
362	if !ok {
363		c.logError(r, "instance not found", "id", id)
364		jsonError(w, http.StatusNotFound, "instance not found")
365		return
366	}
367
368	var req proto.PermissionGrant
369	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
370		c.logError(r, "failed to decode request", "error", err)
371		jsonError(w, http.StatusBadRequest, "failed to decode request")
372		return
373	}
374
375	switch req.Action {
376	case proto.PermissionAllow:
377		ins.App.Permissions.Grant(req.Permission)
378	case proto.PermissionAllowForSession:
379		ins.App.Permissions.GrantPersistent(req.Permission)
380	case proto.PermissionDeny:
381		ins.App.Permissions.Deny(req.Permission)
382	default:
383		c.logError(r, "invalid permission action", "action", req.Action)
384		jsonError(w, http.StatusBadRequest, "invalid permission action")
385		return
386	}
387}
388
389func (c *controllerV1) handlePostInstancePermissionsSkip(w http.ResponseWriter, r *http.Request) {
390	id := r.PathValue("id")
391	ins, ok := c.instances.Get(id)
392	if !ok {
393		c.logError(r, "instance not found", "id", id)
394		jsonError(w, http.StatusNotFound, "instance not found")
395		return
396	}
397
398	var req proto.PermissionSkipRequest
399	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
400		c.logError(r, "failed to decode request", "error", err)
401		jsonError(w, http.StatusBadRequest, "failed to decode request")
402		return
403	}
404
405	ins.App.Permissions.SetSkipRequests(req.Skip)
406}
407
408func (c *controllerV1) handleGetInstancePermissionsSkip(w http.ResponseWriter, r *http.Request) {
409	id := r.PathValue("id")
410	ins, ok := c.instances.Get(id)
411	if !ok {
412		c.logError(r, "instance not found", "id", id)
413		jsonError(w, http.StatusNotFound, "instance not found")
414		return
415	}
416
417	skip := ins.App.Permissions.SkipRequests()
418	jsonEncode(w, proto.PermissionSkipRequest{Skip: skip})
419}
420
421func (c *controllerV1) handleGetInstanceEvents(w http.ResponseWriter, r *http.Request) {
422	flusher := http.NewResponseController(w)
423	id := r.PathValue("id")
424	ins, ok := c.instances.Get(id)
425	if !ok {
426		c.logError(r, "instance not found", "id", id)
427		jsonError(w, http.StatusNotFound, "instance not found")
428		return
429	}
430
431	w.Header().Set("Content-Type", "text/event-stream")
432	w.Header().Set("Cache-Control", "no-cache")
433	w.Header().Set("Connection", "keep-alive")
434
435	for {
436		select {
437		case <-r.Context().Done():
438			c.logDebug(r, "stopping event stream")
439			return
440		case ev := <-ins.App.Events():
441			c.logDebug(r, "sending event", "event", fmt.Sprintf("%T %+v", ev, ev))
442			data, err := json.Marshal(ev)
443			if err != nil {
444				c.logError(r, "failed to marshal event", "error", err)
445				continue
446			}
447
448			fmt.Fprintf(w, "data: %s\n\n", data)
449			flusher.Flush()
450		}
451	}
452}
453
454func (c *controllerV1) handleGetInstanceConfig(w http.ResponseWriter, r *http.Request) {
455	id := r.PathValue("id")
456	ins, ok := c.instances.Get(id)
457	if !ok {
458		c.logError(r, "instance not found", "id", id)
459		jsonError(w, http.StatusNotFound, "instance not found")
460		return
461	}
462
463	jsonEncode(w, ins.cfg)
464}
465
466func (c *controllerV1) handleDeleteInstances(w http.ResponseWriter, r *http.Request) {
467	var ids []string
468	id := r.URL.Query().Get("id")
469	if id != "" {
470		ids = append(ids, id)
471	}
472
473	// Get IDs from body
474	var args []proto.Instance
475	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
476		c.logError(r, "failed to decode request", "error", err)
477		jsonError(w, http.StatusBadRequest, "failed to decode request")
478		return
479	}
480	ids = append(ids, func() []string {
481		out := make([]string, len(args))
482		for i, arg := range args {
483			out[i] = arg.ID
484		}
485		return out
486	}()...)
487
488	for _, id := range ids {
489		ins, ok := c.instances.Get(id)
490		if ok {
491			ins.App.Shutdown()
492		}
493		c.instances.Del(id)
494	}
495}
496
497func (c *controllerV1) handlePostInstances(w http.ResponseWriter, r *http.Request) {
498	var args proto.Instance
499	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
500		c.logError(r, "failed to decode request", "error", err)
501		jsonError(w, http.StatusBadRequest, "failed to decode request")
502		return
503	}
504
505	hasher := sha256.New()
506	hasher.Write([]byte(filepath.Clean(args.Path)))
507	id := hex.EncodeToString(hasher.Sum(nil))
508	if existing, ok := c.instances.Get(id); ok {
509		jsonEncode(w, proto.Instance{
510			ID:   existing.ID(),
511			Path: existing.Path(),
512			// TODO: Investigate if this makes sense.
513			YOLO:    existing.cfg.Permissions != nil && existing.cfg.Permissions.SkipRequests,
514			Debug:   existing.cfg.Options.Debug,
515			DataDir: existing.cfg.Options.DataDirectory,
516		})
517		return
518	}
519
520	cfg, err := config.Init(args.Path, args.DataDir, args.Debug)
521	if err != nil {
522		c.logError(r, "failed to initialize config", "error", err)
523		jsonError(w, http.StatusBadRequest, fmt.Sprintf("failed to initialize config: %v", err))
524		return
525	}
526
527	if cfg.Permissions == nil {
528		cfg.Permissions = &config.Permissions{}
529	}
530	cfg.Permissions.SkipRequests = args.YOLO
531
532	if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
533		c.logError(r, "failed to create data directory", "error", err)
534		jsonError(w, http.StatusInternalServerError, "failed to create data directory")
535		return
536	}
537
538	// Connect to DB; this will also run migrations.
539	conn, err := db.Connect(c.ctx, cfg.Options.DataDirectory)
540	if err != nil {
541		c.logError(r, "failed to connect to database", "error", err)
542		jsonError(w, http.StatusInternalServerError, "failed to connect to database")
543		return
544	}
545
546	appInstance, err := app.New(c.ctx, conn, cfg)
547	if err != nil {
548		slog.Error("failed to create app instance", "error", err)
549		jsonError(w, http.StatusInternalServerError, "failed to create app instance")
550		return
551	}
552
553	ins := &Instance{
554		App:   appInstance,
555		State: InstanceStateCreated,
556		id:    id,
557		path:  args.Path,
558		cfg:   cfg,
559	}
560
561	c.instances.Set(id, ins)
562	jsonEncode(w, proto.Instance{
563		ID:   id,
564		Path: args.Path,
565		YOLO: cfg.Permissions.SkipRequests,
566	})
567}
568
569func createDotCrushDir(dir string) error {
570	if err := os.MkdirAll(dir, 0o700); err != nil {
571		return fmt.Errorf("failed to create data directory: %q %w", dir, err)
572	}
573
574	gitIgnorePath := filepath.Join(dir, ".gitignore")
575	if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
576		if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
577			return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
578		}
579	}
580
581	return nil
582}
583
584func jsonEncode(w http.ResponseWriter, v any) {
585	w.Header().Set("Content-Type", "application/json")
586	_ = json.NewEncoder(w).Encode(v)
587}
588
589func jsonError(w http.ResponseWriter, status int, message string) {
590	w.Header().Set("Content-Type", "application/json")
591	w.WriteHeader(status)
592	_ = json.NewEncoder(w).Encode(proto.Error{Message: message})
593}