Files
Agent-System-Backend/internal/server/platform_api.go
T
weichenandCursor cd5b82186d Add the public extension surface and platform services
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>
2026-10-01 10:35:02 +00:00

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