matrixcloud / api /server.go
github-actions[bot]
Deploy from 4dfd54f2
857a91b
Raw
History Blame Contribute Delete
7.02 kB
// Package api exposes the matrix-runtime HTTP API: health, capabilities, jobs
// and the sandbox compatibility aliases used by MatrixHub.
package api
import (
"context"
"encoding/json"
"net"
"net/http"
"strings"
"time"
"github.com/agent-matrix/matrix-runtime/internal/config"
"github.com/agent-matrix/matrix-runtime/internal/email"
"github.com/agent-matrix/matrix-runtime/internal/jobs"
"github.com/agent-matrix/matrix-runtime/internal/store"
"github.com/agent-matrix/matrix-runtime/web"
)
// Server wires the job manager, user store, email sender and config to routes.
type Server struct {
cfg *config.Config
manager *jobs.Manager
store *store.Store
email *email.Sender
limiter *rateLimiter
}
// NewServer builds a Server. store may be nil if the user database could not be
// opened; auth endpoints then return 503.
func NewServer(cfg *config.Config, mgr *jobs.Manager, st *store.Store) *Server {
s := &Server{cfg: cfg, manager: mgr, store: st, email: email.NewFromEnv()}
if cfg.RateLimitRPM > 0 {
s.limiter = newRateLimiter(cfg.RateLimitRPM)
}
return s
}
// Handler returns the configured HTTP handler with all routes registered.
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/health", s.handleHealth)
mux.HandleFunc("GET /v1/ready", s.handleReady)
mux.HandleFunc("GET /v1/version", s.handleVersion)
mux.HandleFunc("GET /v1/capabilities", s.handleCapabilities)
mux.HandleFunc("GET /v1/runtimes", s.handleListRuntimes)
mux.HandleFunc("GET /v1/catalog", s.handleCatalog)
mux.HandleFunc("GET /v1/policies", s.handlePolicies)
// Multitenant auth (users + sessions, SQLite or Postgres/Neon).
mux.HandleFunc("POST /v1/auth/signup", s.handleSignup)
mux.HandleFunc("POST /v1/auth/login", s.handleLogin)
mux.HandleFunc("GET /v1/auth/me", s.handleMe)
mux.HandleFunc("POST /v1/auth/logout", s.handleLogout)
// Password recovery + email verification (delivered via Resend).
mux.HandleFunc("POST /v1/auth/forgot", s.handleForgotPassword)
mux.HandleFunc("POST /v1/auth/reset", s.handleResetPassword)
mux.HandleFunc("POST /v1/auth/verify", s.handleVerifyEmail)
// Hosted control plane: runtimes, join tokens, BYO provider creds, usage.
mux.HandleFunc("GET /v1/cloud/runtimes", s.handleCloudListRuntimes)
mux.HandleFunc("POST /v1/cloud/runtimes/register", s.handleCloudRegisterRuntime)
mux.HandleFunc("POST /v1/cloud/runtimes/heartbeat", s.handleCloudHeartbeat)
mux.HandleFunc("GET /v1/cloud/join-tokens", s.handleCloudListJoinTokens)
mux.HandleFunc("POST /v1/cloud/join-tokens", s.handleCloudMintJoinToken)
mux.HandleFunc("GET /v1/cloud/providers", s.handleCloudListProviders)
mux.HandleFunc("POST /v1/cloud/providers", s.handleCloudSetProvider)
mux.HandleFunc("GET /v1/cloud/usage", s.handleCloudUsage)
mux.HandleFunc("GET /v1/cloud/audit", s.handleCloudAudit)
mux.HandleFunc("GET /v1/model-sources/huggingface/search", s.handleHFSearch)
mux.HandleFunc("POST /v1/model-sources/resolve", s.handleResolveSource)
mux.HandleFunc("GET /v1/model-profiles", s.handleListProfiles)
mux.HandleFunc("POST /v1/model-profiles", s.handleImportProfile)
mux.HandleFunc("POST /v1/model-profiles/{id}/attach", s.handleAttachProfile)
mux.HandleFunc("GET /v1/model-installations", s.handleListInstallations)
// MatrixShell — real local Python sandbox (install / status / exec).
mux.HandleFunc("GET /v1/matrixshell/status", s.handleMatrixShellStatus)
mux.HandleFunc("POST /v1/matrixshell/install", s.handleMatrixShellInstall)
mux.HandleFunc("POST /v1/matrixshell/exec", s.handleMatrixShellExec)
mux.HandleFunc("POST /v1/jobs", s.handleCreateJob)
mux.HandleFunc("GET /v1/jobs", s.handleListJobs)
mux.HandleFunc("GET /v1/jobs/{job_id}", s.handleGetJob)
mux.HandleFunc("GET /v1/jobs/{job_id}/events", s.handleJobEvents)
mux.HandleFunc("DELETE /v1/jobs/{job_id}", s.handleDeleteJob)
mux.HandleFunc("POST /v1/sandbox/sessions", s.handleCreateSandbox)
mux.HandleFunc("GET /v1/sandbox/sessions/{session_id}", s.handleGetSandbox)
mux.HandleFunc("GET /v1/sandbox/sessions/{session_id}/events", s.handleSandboxEvents)
mux.HandleFunc("GET /v1/sandbox/sessions/{session_id}/tools", s.handleSandboxTools)
mux.HandleFunc("POST /v1/sandbox/sessions/{session_id}/tools/call", s.handleSandboxToolCall)
mux.HandleFunc("DELETE /v1/sandbox/sessions/{session_id}", s.handleDeleteSandbox)
// API docs (OpenAPI spec + a self-contained viewer), public.
mux.HandleFunc("GET /openapi.yaml", s.handleOpenAPISpec)
mux.HandleFunc("GET /docs", s.handleDocs)
// System: storage usage.
mux.HandleFunc("GET /v1/system/storage", s.handleStorage)
// Enterprise console (static SPA) served from the embedded web assets.
mux.Handle("/", s.consoleHandler())
// Outermost: rate limiting (protects auth + writes); then auth.
return s.withRateLimit(s.withAuth(mux))
}
// consoleHandler serves the embedded console, falling back to index.html for
// client-side routes (single-page app).
func (s *Server) consoleHandler() http.Handler {
assets := web.Static()
fileServer := http.FileServer(http.FS(assets))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Never let the SPA shadow the API namespace.
if strings.HasPrefix(r.URL.Path, "/v1/") {
http.NotFound(w, r)
return
}
// Serve the asset when it exists; otherwise hand back the app shell.
p := strings.TrimPrefix(r.URL.Path, "/")
if p == "" {
p = "index.html"
}
if f, err := assets.Open(p); err == nil {
_ = f.Close()
fileServer.ServeHTTP(w, r)
return
}
r2 := r.Clone(r.Context())
r2.URL.Path = "/"
fileServer.ServeHTTP(w, r2)
})
}
// Run starts the HTTP server and blocks until ctx is cancelled, then performs
// a graceful shutdown.
// Run binds addr and serves until ctx is cancelled. Kept for compatibility;
// prefer Serve with a pre-bound listener (e.g. from a port-fallback search).
func (s *Server) Run(ctx context.Context, addr string) error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(ctx, ln)
}
// Serve serves HTTP on the given listener until ctx is cancelled, then performs
// a graceful shutdown.
func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
srv := &http.Server{
Handler: s.Handler(),
ReadHeaderTimeout: 10 * time.Second,
}
errCh := make(chan error, 1)
go func() { errCh <- srv.Serve(ln) }()
select {
case err := <-errCh:
return err
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return srv.Shutdown(shutdownCtx)
}
}
// writeJSON serialises v as JSON with the given status code.
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
// writeError writes a structured JSON error.
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]any{"error": msg, "status": status})
}