HTTP API
Complete Server
The example mounts server.Handler in a host-owned HTTP server, protects all routes with a bearer token, disables skill administration, limits chat bodies, and persists threads using JSONL sessions. It binds loopback only.
export OPENAI_API_KEY='your-provider-key'
export AGENT_MODEL='your-model'
export CHAT_API_TOKEN='your-local-access-token'
go run ./example/http-serverSESSION_DIR selects the storage directory; it defaults to ./sessions. OPENAI_BASE_URL selects a compatible provider endpoint if needed. Keep tokens out of shell history and source control in real deployments.
package main
import (
"context"
"crypto/subtle"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/covoyage/covonaut/agentcore"
"github.com/covoyage/covonaut/provider/chatcompat"
"github.com/covoyage/covonaut/server"
"github.com/covoyage/covonaut/session"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
apiKey, model, token := os.Getenv("OPENAI_API_KEY"), os.Getenv("AGENT_MODEL"), os.Getenv("CHAT_API_TOKEN")
if apiKey == "" || model == "" || token == "" {
return errors.New("set OPENAI_API_KEY, AGENT_MODEL, and CHAT_API_TOKEN")
}
directory := os.Getenv("SESSION_DIR")
if directory == "" {
directory = "./sessions"
}
sessions, err := session.NewFileStore(directory)
if err != nil {
return err
}
cfg := agentcore.NewConfig(
agentcore.WithModel(model),
agentcore.WithProvider(chatcompat.New(chatcompat.Config{
APIKey: apiKey, BaseURL: os.Getenv("OPENAI_BASE_URL"),
})),
agentcore.WithStore(session.NewAgentStore(sessions, ".")),
agentcore.WithStreaming(true),
agentcore.WithMaxTurns(10),
)
cfg.DisableSkillRegistryAPI = true
cfg.DisableSkillReloadAPI = true
application := server.New(cfg)
defer application.Close()
application.SetMaxRequestBodyBytes(1 << 20)
handler := application.Handler()
protected := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
provided := strings.TrimPrefix(request.Header.Get("Authorization"), "Bearer ")
if !strings.HasPrefix(request.Header.Get("Authorization"), "Bearer ") ||
subtle.ConstantTimeCompare([]byte(provided), []byte(token)) != 1 {
http.Error(writer, "unauthorized", http.StatusUnauthorized)
return
}
handler.ServeHTTP(writer, request)
})
httpServer := &http.Server{
Addr: "127.0.0.1:8080", Handler: protected,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
IdleTimeout: 2 * time.Minute,
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
finished := make(chan error, 1)
go func() { finished <- httpServer.ListenAndServe() }()
fmt.Println("Chat API: http://127.0.0.1:8080")
select {
case err := <-finished:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return httpServer.Shutdown(shutdownCtx)
}
}Chat Requests
curl http://127.0.0.1:8080/api/chat \
-H "Authorization: Bearer $CHAT_API_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"message":"Hello","stream":false}'A session-backed store creates a thread when none is supplied. Keep the returned thread_id and send it with subsequent requests to continue that conversation. For streaming, set stream to true and use a client that parses SSE; curl -N is useful for local inspection.
model, response_format, thinking, and skills are per-request settings. The effective precedence is server defaults, then saved thread configuration, then supplied request overrides. An omitted setting is not necessarily a request to clear a saved setting.
Threads, Branches, and State
Sessions and HTTP lists thread endpoints. Branch a historical message using its returned entry_id, not an assumed array position. Thread config and thinking endpoints can persist or clear their overrides using their documented request envelope.
Generic /api/states endpoints are separate from the session-backed thread view. The /agui/{path} mount uses AG-UI request semantics, not the ChatRequest JSON above.
Security and Lifetime
The framework server is not a complete authentication service. The example's shared token is suitable for a local single-user demo, not tenant isolation. Production hosts must authorize every thread/state operation, bind ownership to an authenticated user, restrict allowed model overrides, and enforce quotas.
CORS is a browser policy, not authentication. Configure explicit origins when needed; do not expose unrestricted state endpoints behind a permissive proxy. SetMaxRequestBodyBytes limits the server's chat/admin decoding paths, but do not assume it also limits a separately mounted AG-UI handler. Apply host-level body limits and rate limits to every route.
When mounting Handler, the host owns HTTP shutdown; close the application after draining requests. Long-lived SSE needs appropriate proxy buffering and timeout configuration. Bound model/tool execution separately: HTTP read timeouts alone do not bound reasoning duration.