package main import ( "encoding/json" "fmt" "io" "net/http" "sort" "strconv" "strings" "github.com/godbus/dbus/v5" ) const ( dbusService = "org.ollie.SessionManager" dbusPath = "/org/ollie/SessionManager" dbusIface = "org.ollie.SessionManager" ) func dbusConn() (*dbus.Conn, error) { return dbus.ConnectSessionBus() } func dbusCall(method string, args ...interface{}) (*dbus.Call, error) { conn, err := dbusConn() if err != nil { return nil, fmt.Errorf("dbus connect: %w", err) } defer conn.Close() obj := conn.Object(dbusService, dbusPath) call := obj.Call(dbusIface+"."+method, 0, args...) if call.Err != nil { return nil, call.Err } return call, nil } // dbusHandler routes HTTP requests to D-Bus method calls. // // API: // // GET /sessions - ListSessions // POST /sessions - CreateSession // DELETE /sessions/{id} - KillSession // PATCH /sessions/{id} - RenameSession // // POST /sessions/{id}/submit - Submit prompt // POST /sessions/{id}/interrupt - Interrupt // // GET /sessions/{id}/state - GetState // GET /sessions/{id}/chat - GetChat (query: offset) // GET /sessions/{id}/usage - GetUsage // GET /sessions/{id}/cost - GetCost // GET /sessions/{id}/config - GetConfig // POST /sessions/{id}/config - SetConfig // GET /sessions/{id}/context - GetContext // // GET /backends - ListBackends // GET /sessions/{id}/models - ListModels // GET /agents - ListAgents // // GET /sessions/{id}/peers - PeerList // POST /sessions/{id}/peers - PeerAdd // DELETE /sessions/{id}/peers/{peer_id} - PeerRemove // POST /sessions/{id}/peers/{peer_id}/submit - PeerSubmit func dbusHandler(w http.ResponseWriter, r *http.Request) { p := strings.TrimPrefix(r.URL.Path, "/") p = strings.TrimSuffix(p, "/") parts := strings.Split(p, "/") switch { // GET/POST /sessions case len(parts) == 1 && parts[0] == "sessions": switch r.Method { case http.MethodGet: dbusListSessions(w) case http.MethodPost: dbusCreateSession(w, r) default: writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"}) } // /sessions/{id} case len(parts) == 2 && parts[0] == "sessions": sessionID := parts[1] switch r.Method { case http.MethodGet: dbusGetSession(w, sessionID) case http.MethodDelete: dbusKillSession(w, sessionID) case http.MethodPatch: dbusRenameSession(w, r, sessionID) default: writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"}) } // /sessions/{id}/{action} case len(parts) == 3 && parts[0] == "sessions": sessionID := parts[1] action := parts[2] switch action { case "submit": if r.Method != http.MethodPost { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "POST only"}) return } dbusSubmit(w, r, sessionID) case "interrupt": if r.Method != http.MethodPost { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "POST only"}) return } dbusInterrupt(w, sessionID) case "state": if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"}) return } dbusGetState(w, sessionID) case "chat": if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"}) return } dbusGetChat(w, r, sessionID) case "usage": if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"}) return } dbusGetUsage(w, sessionID) case "cost": if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"}) return } dbusGetCost(w, sessionID) case "config": switch r.Method { case http.MethodGet: dbusGetConfig(w, sessionID) case http.MethodPost: dbusSetConfig(w, r, sessionID) default: writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET or POST"}) } case "context": if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"}) return } dbusGetContext(w, sessionID) case "models": if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"}) return } dbusListModels(w, sessionID) case "peers": switch r.Method { case http.MethodGet: dbusPeerList(w, sessionID) case http.MethodPost: dbusPeerAdd(w, r, sessionID) default: writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET or POST"}) } default: writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) } // /sessions/{id}/peers/{peer_id} case len(parts) == 4 && parts[0] == "sessions" && parts[2] == "peers": sessionID := parts[1] peerID := parts[3] switch r.Method { case http.MethodDelete: dbusPeerRemove(w, sessionID, peerID) default: writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "DELETE only"}) } // /sessions/{id}/peers/{peer_id}/submit case len(parts) == 5 && parts[0] == "sessions" && parts[2] == "peers" && parts[4] == "submit": sessionID := parts[1] peerID := parts[3] if r.Method != http.MethodPost { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "POST only"}) return } dbusPeerSubmit(w, r, sessionID, peerID) // GET /backends case len(parts) == 1 && parts[0] == "backends": if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"}) return } dbusListBackends(w) // GET /agents case len(parts) == 1 && parts[0] == "agents": if r.Method != http.MethodGet { writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "GET only"}) return } dbusListAgents(w) default: writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) } } // --- Sessions --- func dbusListSessions(w http.ResponseWriter) { call, err := dbusCall("ListSessions") if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var sessions []string if err := call.Store(&sessions); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } var result []map[string]string for _, entry := range sessions { parts := strings.SplitN(entry, "\t", 4) if len(parts) >= 4 { result = append(result, map[string]string{ "id": parts[0], "state": parts[1], "model": parts[2], "agent": parts[3], }) } } if result == nil { result = []map[string]string{} } sort.Slice(result, func(i, j int) bool { return result[i]["id"] < result[j]["id"] }) writeJSON(w, http.StatusOK, result) } func dbusCreateSession(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` CWD string `json:"cwd"` Backend string `json:"backend"` Model string `json:"model"` Agent string `json:"agent"` Remote string `json:"remote"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil && err != io.EOF { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } call, err := dbusCall("CreateSession", req.CWD, req.Backend, req.Model, req.Agent, "", req.Remote) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var sessionID string if err := call.Store(&sessionID); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if sessionID == "" { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "session creation failed"}) return } // Rename if a name was provided if req.Name != "" { if call, err := dbusCall("RenameSession", sessionID, req.Name); err == nil { var success bool if call.Store(&success) == nil && success { sessionID = req.Name } } } writeJSON(w, http.StatusCreated, map[string]string{"id": sessionID}) } func dbusGetSession(w http.ResponseWriter, sessionID string) { // Return config as the session detail view dbusGetConfig(w, sessionID) } func dbusKillSession(w http.ResponseWriter, sessionID string) { call, err := dbusCall("KillSession", sessionID) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var success bool if err := call.Store(&success); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !success { writeJSON(w, http.StatusNotFound, map[string]string{"error": "session not found"}) return } w.WriteHeader(http.StatusNoContent) } func dbusRenameSession(w http.ResponseWriter, r *http.Request, sessionID string) { var req struct { Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } call, err := dbusCall("RenameSession", sessionID, req.Name) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var success bool if err := call.Store(&success); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !success { writeJSON(w, http.StatusNotFound, map[string]string{"error": "rename failed"}) return } w.WriteHeader(http.StatusNoContent) } // --- Interaction --- func dbusSubmit(w http.ResponseWriter, r *http.Request, sessionID string) { var req struct { Prompt string `json:"prompt"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } call, err := dbusCall("Submit", sessionID, req.Prompt) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var success bool if err := call.Store(&success); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !success { writeJSON(w, http.StatusNotFound, map[string]string{"error": "session not found"}) return } writeJSON(w, http.StatusOK, map[string]bool{"submitted": true}) } func dbusInterrupt(w http.ResponseWriter, sessionID string) { call, err := dbusCall("Interrupt", sessionID) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var success bool if err := call.Store(&success); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !success { writeJSON(w, http.StatusNotFound, map[string]string{"error": "session not found"}) return } writeJSON(w, http.StatusOK, map[string]bool{"interrupted": true}) } // --- State queries --- func dbusGetState(w http.ResponseWriter, sessionID string) { call, err := dbusCall("GetState", sessionID) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var state string if err := call.Store(&state); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"state": state}) } func dbusGetChat(w http.ResponseWriter, r *http.Request, sessionID string) { offsetStr := r.URL.Query().Get("offset") var offset int64 if offsetStr != "" { offset, _ = strconv.ParseInt(offsetStr, 10, 64) } conn, err := dbusConn() if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } defer conn.Close() obj := conn.Object(dbusService, dbusPath) call := obj.Call(dbusIface+".GetChat", 0, sessionID, offset) if call.Err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": call.Err.Error()}) return } var text string var newOffset int64 if err := call.Store(&text, &newOffset); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("X-Chat-Offset", strconv.FormatInt(newOffset, 10)) w.Write([]byte(text)) } func dbusGetUsage(w http.ResponseWriter, sessionID string) { call, err := dbusCall("GetUsage", sessionID) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var usage string if err := call.Store(&usage); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"usage": usage}) } func dbusGetCost(w http.ResponseWriter, sessionID string) { call, err := dbusCall("GetCost", sessionID) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var cost string if err := call.Store(&cost); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"cost": cost}) } // --- Config --- func dbusGetConfig(w http.ResponseWriter, sessionID string) { call, err := dbusCall("GetConfig", sessionID) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var config string if err := call.Store(&config); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } // Parse key=value\n into JSON object m := make(map[string]string) for _, line := range strings.Split(strings.TrimRight(config, "\n"), "\n") { if line == "" { continue } k, v, _ := strings.Cut(line, "=") m[k] = v } writeJSON(w, http.StatusOK, m) } func dbusSetConfig(w http.ResponseWriter, r *http.Request, sessionID string) { var req struct { Key string `json:"key"` Value string `json:"value"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } call, err := dbusCall("SetConfig", sessionID, req.Key, req.Value) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var success bool if err := call.Store(&success); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !success { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "set config failed"}) return } writeJSON(w, http.StatusOK, map[string]bool{"success": true}) } // --- Context --- func dbusGetContext(w http.ResponseWriter, sessionID string) { call, err := dbusCall("GetContext", sessionID) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var context string if err := call.Store(&context); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } // Already JSONL — return as-is with appropriate content type w.Header().Set("Content-Type", "application/x-ndjson") w.Write([]byte(context)) } // --- Backends/Models/Agents --- func dbusListBackends(w http.ResponseWriter) { call, err := dbusCall("ListBackends") if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var backends []string if err := call.Store(&backends); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, backends) } func dbusListModels(w http.ResponseWriter, sessionID string) { call, err := dbusCall("ListModels", sessionID) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var models []string if err := call.Store(&models); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, models) } func dbusListAgents(w http.ResponseWriter) { call, err := dbusCall("ListAgents") if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var agents []string if err := call.Store(&agents); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusOK, agents) } // --- Peers --- func dbusPeerList(w http.ResponseWriter, sessionID string) { call, err := dbusCall("PeerList", sessionID) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var peers []string if err := call.Store(&peers); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if peers == nil { peers = []string{} } writeJSON(w, http.StatusOK, peers) } func dbusPeerAdd(w http.ResponseWriter, r *http.Request, sessionID string) { var req struct { PeerID string `json:"peer_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } call, err := dbusCall("PeerAdd", sessionID, req.PeerID) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var success bool if err := call.Store(&success); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !success { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "peer add failed"}) return } writeJSON(w, http.StatusCreated, map[string]bool{"added": true}) } func dbusPeerRemove(w http.ResponseWriter, sessionID, peerID string) { call, err := dbusCall("PeerRemove", sessionID, peerID) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var success bool if err := call.Store(&success); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !success { writeJSON(w, http.StatusNotFound, map[string]string{"error": "peer not found"}) return } w.WriteHeader(http.StatusNoContent) } func dbusPeerSubmit(w http.ResponseWriter, r *http.Request, sessionID, peerID string) { var req struct { Prompt string `json:"prompt"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } call, err := dbusCall("PeerSubmit", sessionID, peerID, req.Prompt) if err != nil { writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) return } var success bool if err := call.Store(&success); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !success { writeJSON(w, http.StatusNotFound, map[string]string{"error": "peer submit failed"}) return } writeJSON(w, http.StatusOK, map[string]bool{"submitted": true}) }