Why: Downstream products must extend the base through a public package and host API instead of editing internal code. Evidence: go test ./... passed, including plugin v2 assets, sidecar startup, migration v13, pending confirms, HITL timeout, hash embedder, command pairs, and the extension surface snapshot. Impact: apiVersion 2.0-experimental rejects empty network allowlists for declarative HTTP. Hash embeddings stay opt-in via models.embedding.provider=hash. Plugin secrets resolve per owner at execution time. Refs: docs/proposals/2026-10-01-plugin-contract-v2.md Co-authored-by: Cursor <cursoragent@cursor.com>
188 lines
5.4 KiB
Go
188 lines
5.4 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"agent-system-backend/internal/store"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
func (s *Server) createBackgroundTask(w http.ResponseWriter, r *http.Request) {
|
|
var body struct {
|
|
Kind string `json:"kind"`
|
|
Title string `json:"title"`
|
|
ConversationID string `json:"conversationId"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Kind == "" {
|
|
http.Error(w, "kind required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
task, err := s.st.CreateBackgroundTask(s.owner(r), body.Kind, body.Title, body.ConversationID)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, task)
|
|
}
|
|
|
|
func (s *Server) listBackgroundTasks(w http.ResponseWriter, r *http.Request) {
|
|
tasks, err := s.st.ListBackgroundTasks(s.owner(r))
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, orEmptySlice(tasks))
|
|
}
|
|
|
|
func (s *Server) getBackgroundTask(w http.ResponseWriter, r *http.Request) {
|
|
task, err := s.st.GetBackgroundTask(s.owner(r), r.PathValue("id"))
|
|
if err != nil {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, task)
|
|
}
|
|
|
|
func (s *Server) updateBackgroundTask(w http.ResponseWriter, r *http.Request) {
|
|
var body struct {
|
|
Status string `json:"status"`
|
|
Progress int `json:"progress"`
|
|
Error string `json:"error"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Status == "" {
|
|
http.Error(w, "status required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.st.UpdateBackgroundTask(s.owner(r), r.PathValue("id"), body.Status, body.Progress, body.Error); err != nil {
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
return
|
|
}
|
|
s.getBackgroundTask(w, r)
|
|
}
|
|
|
|
func (s *Server) putMemory(w http.ResponseWriter, r *http.Request) {
|
|
var e core.MemoryEntry
|
|
if err := json.NewDecoder(r.Body).Decode(&e); err != nil || strings.TrimSpace(e.Content) == "" {
|
|
http.Error(w, "content required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if e.Scope == "" {
|
|
e.Scope = "user"
|
|
}
|
|
if e.Type == "" {
|
|
e.Type = "fact"
|
|
}
|
|
if e.Source == "" {
|
|
e.Source = "manual"
|
|
}
|
|
saved, err := s.st.PutMemory(s.owner(r), e)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, saved)
|
|
}
|
|
|
|
func (s *Server) listMemory(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
list, err := s.st.ListMemory(s.owner(r), q.Get("scope"), q.Get("projectId"))
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, orEmptySlice(list))
|
|
}
|
|
|
|
func (s *Server) listAudit(w http.ResponseWriter, r *http.Request) {
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
evs, err := s.st.ListAuditEvents(s.owner(r), limit)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, orEmptySlice(evs))
|
|
}
|
|
|
|
func (s *Server) postAudit(w http.ResponseWriter, r *http.Request) {
|
|
var body struct {
|
|
Action string `json:"action"`
|
|
Target string `json:"target"`
|
|
Evidence string `json:"evidence"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Action == "" {
|
|
http.Error(w, "action required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.st.AppendAuditEvent(store.AuditEvent{
|
|
Owner: s.owner(r), Actor: s.owner(r), Action: body.Action, Target: body.Target,
|
|
Evidence: body.Evidence, TraceID: r.Header.Get("X-Trace-Id"),
|
|
}); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) setPluginEnabled(w http.ResponseWriter, r *http.Request) {
|
|
var body struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
http.Error(w, "bad json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.st.SetPluginEnabled(s.owner(r), r.PathValue("id"), body.Enabled); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) llmBudget(w http.ResponseWriter, r *http.Request) {
|
|
budget := int(s.set.Effective(s.owner(r), "").GetNumber("llm.dailyTokenBudget", 0))
|
|
status, err := s.st.AccountTokens(s.owner(r), 0, budget)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"total": status.Total, "budget": budget, "over": status.Over})
|
|
}
|
|
|
|
func (s *Server) exportConversation(w http.ResponseWriter, r *http.Request) {
|
|
owner := s.owner(r)
|
|
id := r.PathValue("id")
|
|
conv, err := s.st.GetConversation(owner, id)
|
|
if err != nil {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
msgs, err := s.st.ListMessages(id)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if r.URL.Query().Get("format") == "md" {
|
|
w.Header().Set("Content-Type", "text/markdown; charset=utf-8")
|
|
fmt.Fprintf(w, "# %s\n\n", conv.Title)
|
|
for _, m := range msgs {
|
|
fmt.Fprintf(w, "## %s\n\n%s\n\n", m.Role, messageText(m))
|
|
}
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"conversation": conv, "messages": msgs})
|
|
}
|
|
|
|
func messageText(m core.Message) string {
|
|
var b strings.Builder
|
|
for _, p := range m.Parts {
|
|
if p.Type == core.PartText {
|
|
b.WriteString(p.Text)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|