ollie-httpgw/dbus.go

663 lines
19 KiB
Go

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})
}