Initial commit
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
new file mode 100644
index 0000000..0fd7ee1
--- /dev/null
+++ b/loop/server/loophttp.go
@@ -0,0 +1,753 @@
+// Package server provides HTTP server functionality for the sketch loop.
+package server
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"html"
+	"io"
+	"io/fs"
+	"log/slog"
+	"net/http"
+	"net/http/pprof"
+	"os"
+	"os/exec"
+	"strconv"
+	"strings"
+	"sync"
+	"syscall"
+	"time"
+
+	"github.com/creack/pty"
+	"sketch.dev/ant"
+	"sketch.dev/loop"
+	"sketch.dev/loop/webui"
+)
+
+// terminalSession represents a terminal session with its PTY and the event channel
+type terminalSession struct {
+	pty                *os.File
+	eventsClients      map[chan []byte]bool
+	lastEventClientID  int
+	eventsClientsMutex sync.Mutex
+	cmd                *exec.Cmd
+}
+
+// TerminalMessage represents a message sent from the client for terminal resize events
+type TerminalMessage struct {
+	Type string `json:"type"`
+	Cols uint16 `json:"cols"`
+	Rows uint16 `json:"rows"`
+}
+
+// TerminalResponse represents the response for a new terminal creation
+type TerminalResponse struct {
+	SessionID string `json:"sessionId"`
+}
+
+// Server serves sketch HTTP. Server implements http.Handler.
+type Server struct {
+	mux      *http.ServeMux
+	agent    loop.CodingAgent
+	hostname string
+	logFile  *os.File
+	// Mutex to protect terminalSessions
+	ptyMutex         sync.Mutex
+	terminalSessions map[string]*terminalSession
+}
+
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	s.mux.ServeHTTP(w, r)
+}
+
+// New creates a new HTTP server.
+func New(agent loop.CodingAgent, logFile *os.File) (*Server, error) {
+	s := &Server{
+		mux:              http.NewServeMux(),
+		agent:            agent,
+		hostname:         getHostname(),
+		logFile:          logFile,
+		terminalSessions: make(map[string]*terminalSession),
+	}
+
+	webBundle, err := webui.Build()
+	if err != nil {
+		return nil, fmt.Errorf("failed to build web bundle, did you run 'go generate sketch.dev/loop/...'?: %w", err)
+	}
+
+	s.mux.HandleFunc("/diff", func(w http.ResponseWriter, r *http.Request) {
+		// Check if a specific commit hash was requested
+		commit := r.URL.Query().Get("commit")
+
+		// Get the diff, optionally for a specific commit
+		var diff string
+		var err error
+		if commit != "" {
+			// Validate the commit hash format
+			if !isValidGitSHA(commit) {
+				http.Error(w, fmt.Sprintf("Invalid git commit SHA format: %s", commit), http.StatusBadRequest)
+				return
+			}
+
+			diff, err = agent.Diff(&commit)
+		} else {
+			diff, err = agent.Diff(nil)
+		}
+
+		if err != nil {
+			http.Error(w, fmt.Sprintf("Error generating diff: %v", err), http.StatusInternalServerError)
+			return
+		}
+
+		w.Header().Set("Content-Type", "text/plain")
+		w.Write([]byte(diff))
+	})
+
+	// Handler for initialization called by host sketch binary when inside docker.
+	s.mux.HandleFunc("/init", func(w http.ResponseWriter, r *http.Request) {
+		defer func() {
+			if err := recover(); err != nil {
+				slog.ErrorContext(r.Context(), "/init panic", slog.Any("recovered_err", err))
+
+				// Return an error response to the client
+				http.Error(w, fmt.Sprintf("panic: %v\n", err), http.StatusInternalServerError)
+			}
+		}()
+
+		if r.Method != "POST" {
+			http.Error(w, "POST required", http.StatusBadRequest)
+			return
+		}
+
+		body, err := io.ReadAll(r.Body)
+		r.Body.Close()
+		if err != nil {
+			http.Error(w, "failed to read request body: "+err.Error(), http.StatusBadRequest)
+			return
+		}
+		m := make(map[string]string)
+		if err := json.Unmarshal(body, &m); err != nil {
+			http.Error(w, "bad request body: "+err.Error(), http.StatusBadRequest)
+			return
+		}
+		hostAddr := m["host_addr"]
+		gitRemoteAddr := m["git_remote_addr"]
+		commit := m["commit"]
+		ini := loop.AgentInit{
+			WorkingDir:    "/app",
+			InDocker:      true,
+			Commit:        commit,
+			GitRemoteAddr: gitRemoteAddr,
+			HostAddr:      hostAddr,
+		}
+		if err := agent.Init(ini); err != nil {
+			http.Error(w, "init failed: "+err.Error(), http.StatusInternalServerError)
+			return
+		}
+		w.Header().Set("Content-Type", "application/json")
+		io.WriteString(w, "{}\n")
+	})
+
+	// Handler for /messages?start=N&end=M (start/end are optional)
+	s.mux.HandleFunc("/messages", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+
+		// Extract query parameters for range
+		var start, end int
+		var err error
+
+		currentCount := agent.MessageCount()
+
+		startParam := r.URL.Query().Get("start")
+		if startParam != "" {
+			start, err = strconv.Atoi(startParam)
+			if err != nil {
+				http.Error(w, "Invalid 'start' parameter", http.StatusBadRequest)
+				return
+			}
+		}
+
+		endParam := r.URL.Query().Get("end")
+		if endParam != "" {
+			end, err = strconv.Atoi(endParam)
+			if err != nil {
+				http.Error(w, "Invalid 'end' parameter", http.StatusBadRequest)
+				return
+			}
+		} else {
+			end = currentCount
+		}
+
+		if start < 0 || start > end || end > currentCount {
+			http.Error(w, fmt.Sprintf("Invalid range: start %d end %d currentCount %d", start, end, currentCount), http.StatusBadRequest)
+			return
+		}
+
+		start = max(0, start)
+		end = min(agent.MessageCount(), end)
+		messages := agent.Messages(start, end)
+
+		// Create a JSON encoder with indentation for pretty-printing
+		encoder := json.NewEncoder(w)
+		encoder.SetIndent("", "  ") // Two spaces for each indentation level
+
+		err = encoder.Encode(messages)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+	})
+
+	// Handler for /logs - displays the contents of the log file
+	s.mux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
+		if s.logFile == nil {
+			http.Error(w, "log file not set", http.StatusNotFound)
+			return
+		}
+		logContents, err := os.ReadFile(s.logFile.Name())
+		if err != nil {
+			http.Error(w, "error reading log file: "+err.Error(), http.StatusInternalServerError)
+			return
+		}
+		w.Header().Set("Content-Type", "text/html; charset=utf-8")
+		fmt.Fprintf(w, "<!DOCTYPE html>\n<html>\n<head>\n<title>Sketchy Log File</title>\n</head>\n<body>\n")
+		fmt.Fprintf(w, "<pre>%s</pre>\n", html.EscapeString(string(logContents)))
+		fmt.Fprintf(w, "</body>\n</html>")
+	})
+
+	// Handler for /download - downloads both messages and status as a JSON file
+	s.mux.HandleFunc("/download", func(w http.ResponseWriter, r *http.Request) {
+		// Set headers for file download
+		w.Header().Set("Content-Type", "application/octet-stream")
+
+		// Generate filename with format: sketch-YYYYMMDD-HHMMSS.json
+		timestamp := time.Now().Format("20060102-150405")
+		filename := fmt.Sprintf("sketch-%s.json", timestamp)
+
+		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
+
+		// Get all messages
+		messageCount := agent.MessageCount()
+		messages := agent.Messages(0, messageCount)
+
+		// Get status information (usage and other metadata)
+		totalUsage := agent.TotalUsage()
+		hostname := getHostname()
+		workingDir := getWorkingDir()
+
+		// Create a combined structure with all information
+		downloadData := struct {
+			Messages     []loop.AgentMessage `json:"messages"`
+			MessageCount int                 `json:"message_count"`
+			TotalUsage   ant.CumulativeUsage `json:"total_usage"`
+			Hostname     string              `json:"hostname"`
+			WorkingDir   string              `json:"working_dir"`
+			DownloadTime string              `json:"download_time"`
+		}{
+			Messages:     messages,
+			MessageCount: messageCount,
+			TotalUsage:   totalUsage,
+			Hostname:     hostname,
+			WorkingDir:   workingDir,
+			DownloadTime: time.Now().Format(time.RFC3339),
+		}
+
+		// Marshal the JSON with indentation for better readability
+		jsonData, err := json.MarshalIndent(downloadData, "", "  ")
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		w.Write(jsonData)
+	})
+
+	// The latter doesn't return until the number of messages has changed (from seen
+	// or from when this was called.)
+	s.mux.HandleFunc("/state", func(w http.ResponseWriter, r *http.Request) {
+		pollParam := r.URL.Query().Get("poll")
+		seenParam := r.URL.Query().Get("seen")
+
+		// Get the client's current message count (if provided)
+		clientMessageCount := -1
+		var err error
+		if seenParam != "" {
+			clientMessageCount, err = strconv.Atoi(seenParam)
+			if err != nil {
+				http.Error(w, "Invalid 'seen' parameter", http.StatusBadRequest)
+				return
+			}
+		}
+
+		serverMessageCount := agent.MessageCount()
+
+		// Let lazy clients not have to specify this.
+		if clientMessageCount == -1 {
+			clientMessageCount = serverMessageCount
+		}
+
+		if pollParam == "true" {
+			ch := make(chan string)
+			go func() {
+				// This is your blocking operation
+				agent.WaitForMessageCount(r.Context(), clientMessageCount)
+				close(ch)
+			}()
+			select {
+			case <-r.Context().Done():
+				slog.DebugContext(r.Context(), "abandoned poll request")
+				return
+			case <-time.After(90 * time.Second):
+				// Let the user call /state again to get the latest to limit how long our long polls hang out.
+				slog.DebugContext(r.Context(), "longish poll request")
+				break
+			case <-ch:
+				break
+			}
+		}
+
+		serverMessageCount = agent.MessageCount()
+		totalUsage := agent.TotalUsage()
+
+		w.Header().Set("Content-Type", "application/json")
+
+		state := struct {
+			MessageCount  int                 `json:"message_count"`
+			TotalUsage    ant.CumulativeUsage `json:"total_usage"`
+			Hostname      string              `json:"hostname"`
+			WorkingDir    string              `json:"working_dir"`
+			InitialCommit string              `json:"initial_commit"`
+			Title         string              `json:"title"`
+			OS            string              `json:"os"`
+		}{
+			MessageCount:  serverMessageCount,
+			TotalUsage:    totalUsage,
+			Hostname:      s.hostname,
+			WorkingDir:    getWorkingDir(),
+			InitialCommit: agent.InitialCommit(),
+			Title:         agent.Title(),
+			OS:            agent.OS(),
+		}
+
+		// Create a JSON encoder with indentation for pretty-printing
+		encoder := json.NewEncoder(w)
+		encoder.SetIndent("", "  ") // Two spaces for each indentation level
+
+		err = encoder.Encode(state)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+	})
+
+	s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServerFS(webBundle)))
+
+	// Terminal WebSocket handler
+	// Terminal endpoints - predefined terminals 1-9
+	// TODO: The UI doesn't actually know how to use terminals 2-9!
+	s.mux.HandleFunc("/terminal/events/", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != http.MethodGet {
+			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			return
+		}
+		pathParts := strings.Split(r.URL.Path, "/")
+		if len(pathParts) < 4 {
+			http.Error(w, "Invalid terminal ID", http.StatusBadRequest)
+			return
+		}
+
+		sessionID := pathParts[3]
+		// Validate that the terminal ID is between 1-9
+		if len(sessionID) != 1 || sessionID[0] < '1' || sessionID[0] > '9' {
+			http.Error(w, "Terminal ID must be between 1 and 9", http.StatusBadRequest)
+			return
+		}
+
+		s.handleTerminalEvents(w, r, sessionID)
+	})
+
+	s.mux.HandleFunc("/terminal/input/", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != http.MethodPost {
+			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			return
+		}
+		pathParts := strings.Split(r.URL.Path, "/")
+		if len(pathParts) < 4 {
+			http.Error(w, "Invalid terminal ID", http.StatusBadRequest)
+			return
+		}
+		sessionID := pathParts[3]
+		s.handleTerminalInput(w, r, sessionID)
+	})
+
+	s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		// Serve the timeline.html file directly from the embedded filesystem
+		data, err := fs.ReadFile(webBundle, "timeline.html")
+		if err != nil {
+			http.Error(w, "File not found", http.StatusNotFound)
+			return
+		}
+		w.Header().Set("Content-Type", "text/html")
+		w.Write(data)
+	})
+
+	// Handler for POST /chat
+	s.mux.HandleFunc("/chat", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != http.MethodPost {
+			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			return
+		}
+
+		// Parse the request body
+		var requestBody struct {
+			Message string `json:"message"`
+		}
+
+		decoder := json.NewDecoder(r.Body)
+		if err := decoder.Decode(&requestBody); err != nil {
+			http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
+			return
+		}
+		defer r.Body.Close()
+
+		if requestBody.Message == "" {
+			http.Error(w, "Message cannot be empty", http.StatusBadRequest)
+			return
+		}
+
+		agent.UserMessage(r.Context(), requestBody.Message)
+
+		w.WriteHeader(http.StatusOK)
+	})
+
+	// Handler for /cancel - cancels the current inner loop in progress
+	s.mux.HandleFunc("/cancel", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != http.MethodPost {
+			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			return
+		}
+
+		// Parse the request body (optional)
+		var requestBody struct {
+			Reason     string `json:"reason"`
+			ToolCallID string `json:"tool_call_id"`
+		}
+
+		decoder := json.NewDecoder(r.Body)
+		if err := decoder.Decode(&requestBody); err != nil && err != io.EOF {
+			http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
+			return
+		}
+		defer r.Body.Close()
+
+		cancelReason := "user requested cancellation"
+		if requestBody.Reason != "" {
+			cancelReason = requestBody.Reason
+		}
+
+		if requestBody.ToolCallID != "" {
+			err := agent.CancelToolUse(requestBody.ToolCallID, fmt.Errorf("%s", cancelReason))
+			if err != nil {
+				http.Error(w, err.Error(), http.StatusBadRequest)
+				return
+			}
+			// Return a success response
+			w.Header().Set("Content-Type", "application/json")
+			json.NewEncoder(w).Encode(map[string]string{
+				"status":     "cancelled",
+				"too_use_id": requestBody.ToolCallID,
+				"reason":     cancelReason})
+			return
+		}
+		// Call the CancelInnerLoop method
+		agent.CancelInnerLoop(fmt.Errorf("%s", cancelReason))
+		// Return a success response
+		w.Header().Set("Content-Type", "application/json")
+		json.NewEncoder(w).Encode(map[string]string{"status": "cancelled", "reason": cancelReason})
+	})
+
+	debugMux := initDebugMux()
+	s.mux.HandleFunc("/debug/", func(w http.ResponseWriter, r *http.Request) {
+		debugMux.ServeHTTP(w, r)
+	})
+
+	return s, nil
+}
+
+// Utility functions
+func getHostname() string {
+	hostname, err := os.Hostname()
+	if err != nil {
+		return "unknown"
+	}
+	return hostname
+}
+
+func getWorkingDir() string {
+	wd, err := os.Getwd()
+	if err != nil {
+		return "unknown"
+	}
+	return wd
+}
+
+// createTerminalSession creates a new terminal session with the given ID
+func (s *Server) createTerminalSession(sessionID string) (*terminalSession, error) {
+	// Start a new shell process
+	shellPath := getShellPath()
+	cmd := exec.Command(shellPath)
+
+	// Get working directory from the agent if possible
+	workDir := getWorkingDir()
+	cmd.Dir = workDir
+
+	// Set up environment
+	cmd.Env = append(os.Environ(), "TERM=xterm-256color")
+
+	// Start the command with a pty
+	ptmx, err := pty.Start(cmd)
+	if err != nil {
+		slog.Error("Failed to start pty", "error", err)
+		return nil, err
+	}
+
+	// Create the terminal session
+	session := &terminalSession{
+		pty:           ptmx,
+		eventsClients: make(map[chan []byte]bool),
+		cmd:           cmd,
+	}
+
+	// Start goroutine to read from pty and broadcast to all connected SSE clients
+	go s.readFromPtyAndBroadcast(sessionID, session)
+
+	return session, nil
+} // handleTerminalEvents handles SSE connections for terminal output
+func (s *Server) handleTerminalEvents(w http.ResponseWriter, r *http.Request, sessionID string) {
+	// Check if the session exists, if not, create it
+	s.ptyMutex.Lock()
+	session, exists := s.terminalSessions[sessionID]
+
+	if !exists {
+		// Create a new terminal session
+		var err error
+		session, err = s.createTerminalSession(sessionID)
+		if err != nil {
+			s.ptyMutex.Unlock()
+			http.Error(w, fmt.Sprintf("Failed to create terminal: %v", err), http.StatusInternalServerError)
+			return
+		}
+
+		// Store the new session
+		s.terminalSessions[sessionID] = session
+	}
+	s.ptyMutex.Unlock()
+
+	// Set headers for SSE
+	w.Header().Set("Content-Type", "text/event-stream")
+	w.Header().Set("Cache-Control", "no-cache")
+	w.Header().Set("Connection", "keep-alive")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+
+	// Create a channel for this client
+	events := make(chan []byte, 4096) // Buffer to prevent blocking
+
+	// Register this client's channel
+	session.eventsClientsMutex.Lock()
+	clientID := session.lastEventClientID + 1
+	session.lastEventClientID = clientID
+	session.eventsClients[events] = true
+	session.eventsClientsMutex.Unlock()
+
+	// When the client disconnects, remove their channel
+	defer func() {
+		session.eventsClientsMutex.Lock()
+		delete(session.eventsClients, events)
+		close(events)
+		session.eventsClientsMutex.Unlock()
+	}()
+
+	// Flush to send headers to client immediately
+	if f, ok := w.(http.Flusher); ok {
+		f.Flush()
+	}
+
+	// Send events to the client as they arrive
+	for {
+		select {
+		case <-r.Context().Done():
+			return
+		case data := <-events:
+			// Format as SSE with base64 encoding
+			fmt.Fprintf(w, "data: %s\n\n", base64.StdEncoding.EncodeToString(data))
+
+			// Flush the data immediately
+			if f, ok := w.(http.Flusher); ok {
+				f.Flush()
+			}
+		}
+	}
+}
+
+// handleTerminalInput processes input to the terminal
+func (s *Server) handleTerminalInput(w http.ResponseWriter, r *http.Request, sessionID string) {
+	// Check if the session exists
+	s.ptyMutex.Lock()
+	session, exists := s.terminalSessions[sessionID]
+	s.ptyMutex.Unlock()
+
+	if !exists {
+		http.Error(w, "Terminal session not found", http.StatusNotFound)
+		return
+	}
+
+	// Read the request body (terminal input or resize command)
+	body, err := io.ReadAll(r.Body)
+	if err != nil {
+		http.Error(w, "Failed to read request body", http.StatusBadRequest)
+		return
+	}
+
+	// Check if it's a resize message
+	if len(body) > 0 && body[0] == '{' {
+		var msg TerminalMessage
+		if err := json.Unmarshal(body, &msg); err == nil && msg.Type == "resize" {
+			if msg.Cols > 0 && msg.Rows > 0 {
+				pty.Setsize(session.pty, &pty.Winsize{
+					Cols: msg.Cols,
+					Rows: msg.Rows,
+				})
+
+				// Respond with success
+				w.WriteHeader(http.StatusOK)
+				return
+			}
+		}
+	}
+
+	// Regular terminal input
+	_, err = session.pty.Write(body)
+	if err != nil {
+		slog.Error("Failed to write to pty", "error", err)
+		http.Error(w, "Failed to write to terminal", http.StatusInternalServerError)
+		return
+	}
+
+	// Respond with success
+	w.WriteHeader(http.StatusOK)
+}
+
+// readFromPtyAndBroadcast reads output from the PTY and broadcasts it to all connected clients
+func (s *Server) readFromPtyAndBroadcast(sessionID string, session *terminalSession) {
+	buf := make([]byte, 4096)
+	defer func() {
+		// Clean up when done
+		s.ptyMutex.Lock()
+		delete(s.terminalSessions, sessionID)
+		s.ptyMutex.Unlock()
+
+		// Close the PTY
+		session.pty.Close()
+
+		// Ensure process is terminated
+		if session.cmd.Process != nil {
+			session.cmd.Process.Signal(syscall.SIGTERM)
+			time.Sleep(100 * time.Millisecond)
+			session.cmd.Process.Kill()
+		}
+
+		// Close all client channels
+		session.eventsClientsMutex.Lock()
+		for ch := range session.eventsClients {
+			delete(session.eventsClients, ch)
+			close(ch)
+		}
+		session.eventsClientsMutex.Unlock()
+	}()
+
+	for {
+		n, err := session.pty.Read(buf)
+		if err != nil {
+			if err != io.EOF {
+				slog.Error("Failed to read from pty", "error", err)
+			}
+			break
+		}
+
+		// Make a copy of the data for each client
+		data := make([]byte, n)
+		copy(data, buf[:n])
+
+		// Broadcast to all connected clients
+		session.eventsClientsMutex.Lock()
+		for ch := range session.eventsClients {
+			// Try to send, but don't block if channel is full
+			select {
+			case ch <- data:
+			default:
+				// Channel is full, drop the message for this client
+			}
+		}
+		session.eventsClientsMutex.Unlock()
+	}
+}
+
+// getShellPath returns the path to the shell to use
+func getShellPath() string {
+	// Try to use the user's preferred shell
+	shell := os.Getenv("SHELL")
+	if shell != "" {
+		return shell
+	}
+
+	// Default to bash on Unix-like systems
+	if _, err := os.Stat("/bin/bash"); err == nil {
+		return "/bin/bash"
+	}
+
+	// Fall back to sh
+	return "/bin/sh"
+}
+
+func initDebugMux() *http.ServeMux {
+	mux := http.NewServeMux()
+	mux.HandleFunc("GET /debug/{$}", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "text/html; charset=utf-8")
+		fmt.Fprintf(w, `<!doctype html>
+			<html><head><title>sketch debug</title></head><body>
+			<h1>sketch debug</h1>
+			<ul>
+					<li><a href="/debug/pprof/cmdline">pprof/cmdline</a></li>
+					<li><a href="/debug/pprof/profile">pprof/profile</a></li>
+					<li><a href="/debug/pprof/symbol">pprof/symbol</a></li>
+					<li><a href="/debug/pprof/trace">pprof/trace</a></li>
+					<li><a href="/debug/pprof/goroutine?debug=1">pprof/goroutine?debug=1</a></li>
+					<li><a href="/debug/metrics">metrics</a></li>
+			</ul>
+			</body>
+			</html>
+			`)
+	})
+	mux.HandleFunc("GET /debug/pprof/", pprof.Index)
+	mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
+	mux.HandleFunc("GET /debug/pprof/profile", pprof.Profile)
+	mux.HandleFunc("GET /debug/pprof/symbol", pprof.Symbol)
+	mux.HandleFunc("GET /debug/pprof/trace", pprof.Trace)
+	return mux
+}
+
+// isValidGitSHA validates if a string looks like a valid git SHA hash.
+// Git SHAs are hexadecimal strings of at least 4 characters but typically 7, 8, or 40 characters.
+func isValidGitSHA(sha string) bool {
+	// Git SHA must be a hexadecimal string with at least 4 characters
+	if len(sha) < 4 || len(sha) > 40 {
+		return false
+	}
+
+	// Check if the string only contains hexadecimal characters
+	for _, char := range sha {
+		if !(char >= '0' && char <= '9') && !(char >= 'a' && char <= 'f') && !(char >= 'A' && char <= 'F') {
+			return false
+		}
+	}
+
+	return true
+}