- Add ListPipelines/GetPipeline to CIProvider port with Woodpecker adapter
- Add DNS alias endpoints: GET/POST/DELETE /projects/{id}/domains
- Implement worker executor daemon, build executor, and git operations
- Add build service, worker service, and build audit tracking
- Add worker registry with PostgreSQL adapter and migration
- Add multi-provider code agent interface (Claude Code + OpenCode)
- Add create-and-build combo endpoint
- Update landing-page cookbook to reflect all gaps closed
- Fix tech debt: unified validation, auth scopes, error wrapping, slog patterns
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
270 lines
7.3 KiB
Go
270 lines
7.3 KiB
Go
// Package handlers provides HTTP handlers for the rdev API.
|
|
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
"github.com/orchard9/rdev/internal/port"
|
|
"github.com/orchard9/rdev/pkg/api"
|
|
)
|
|
|
|
// AgentsHandler handles code agent management endpoints.
|
|
type AgentsHandler struct {
|
|
registry port.CodeAgentRegistry
|
|
}
|
|
|
|
// NewAgentsHandler creates a new agents handler.
|
|
func NewAgentsHandler(registry port.CodeAgentRegistry) *AgentsHandler {
|
|
return &AgentsHandler{
|
|
registry: registry,
|
|
}
|
|
}
|
|
|
|
// Mount registers the agent routes.
|
|
func (h *AgentsHandler) Mount(r api.Router) {
|
|
r.Route("/agents", func(r chi.Router) {
|
|
r.Get("/", h.List)
|
|
r.Get("/health", h.Health)
|
|
r.Get("/{provider}", h.GetCapabilities)
|
|
r.Post("/default", h.SetDefault)
|
|
})
|
|
}
|
|
|
|
// AgentDTO is the data transfer object for code agents.
|
|
type AgentDTO struct {
|
|
Provider string `json:"provider"`
|
|
Name string `json:"name"`
|
|
Available bool `json:"available"`
|
|
Default bool `json:"default"`
|
|
Models []string `json:"supported_models,omitempty"`
|
|
DefaultModel string `json:"default_model,omitempty"`
|
|
}
|
|
|
|
// AgentCapabilitiesDTO is the DTO for agent capabilities.
|
|
type AgentCapabilitiesDTO struct {
|
|
Provider string `json:"provider"`
|
|
SupportsSessionContinuation bool `json:"supports_session_continuation"`
|
|
SupportsModelSelection bool `json:"supports_model_selection"`
|
|
SupportsToolControl bool `json:"supports_tool_control"`
|
|
SupportsStreaming bool `json:"supports_streaming"`
|
|
SupportedModels []string `json:"supported_models"`
|
|
DefaultModel string `json:"default_model"`
|
|
MaxPromptLength int `json:"max_prompt_length,omitempty"`
|
|
}
|
|
|
|
// ListAgentsResponse is the response for GET /agents.
|
|
type ListAgentsResponse struct {
|
|
Agents []AgentDTO `json:"agents"`
|
|
DefaultAgent string `json:"default_agent"`
|
|
TotalAgents int `json:"total_agents"`
|
|
AvailableCount int `json:"available_count"`
|
|
}
|
|
|
|
// List returns all registered code agents and their status.
|
|
// GET /agents
|
|
func (h *AgentsHandler) List(w http.ResponseWriter, r *http.Request) {
|
|
if h.registry == nil {
|
|
api.WriteSuccess(w, r, ListAgentsResponse{
|
|
Agents: []AgentDTO{},
|
|
DefaultAgent: "",
|
|
TotalAgents: 0,
|
|
AvailableCount: 0,
|
|
})
|
|
return
|
|
}
|
|
|
|
providers := h.registry.Available()
|
|
defaultProvider := h.registry.DefaultProvider()
|
|
|
|
// Check availability for each agent
|
|
availableAgents := h.registry.AvailableAgents(r.Context())
|
|
availableSet := make(map[domain.AgentProvider]bool)
|
|
for _, agent := range availableAgents {
|
|
availableSet[agent.Provider()] = true
|
|
}
|
|
|
|
agents := make([]AgentDTO, 0, len(providers))
|
|
availableCount := 0
|
|
|
|
for _, provider := range providers {
|
|
agent := h.registry.Get(provider)
|
|
if agent == nil {
|
|
continue
|
|
}
|
|
|
|
caps := agent.Capabilities()
|
|
isAvailable := availableSet[provider]
|
|
isDefault := provider == defaultProvider
|
|
|
|
if isAvailable {
|
|
availableCount++
|
|
}
|
|
|
|
agents = append(agents, AgentDTO{
|
|
Provider: string(provider),
|
|
Name: agent.Name(),
|
|
Available: isAvailable,
|
|
Default: isDefault,
|
|
Models: caps.SupportedModels,
|
|
DefaultModel: caps.DefaultModel,
|
|
})
|
|
}
|
|
|
|
api.WriteSuccess(w, r, ListAgentsResponse{
|
|
Agents: agents,
|
|
DefaultAgent: string(defaultProvider),
|
|
TotalAgents: len(agents),
|
|
AvailableCount: availableCount,
|
|
})
|
|
}
|
|
|
|
// GetCapabilities returns the capabilities of a specific agent.
|
|
// GET /agents/{provider}
|
|
func (h *AgentsHandler) GetCapabilities(w http.ResponseWriter, r *http.Request) {
|
|
providerStr := chi.URLParam(r, "provider")
|
|
provider := domain.AgentProvider(providerStr)
|
|
|
|
if h.registry == nil {
|
|
api.WriteNotFound(w, r, "no agents registered")
|
|
return
|
|
}
|
|
|
|
agent := h.registry.Get(provider)
|
|
if agent == nil {
|
|
api.WriteNotFound(w, r, "agent not found: "+providerStr)
|
|
return
|
|
}
|
|
|
|
caps := agent.Capabilities()
|
|
|
|
api.WriteSuccess(w, r, AgentCapabilitiesDTO{
|
|
Provider: string(caps.Provider),
|
|
SupportsSessionContinuation: caps.SupportsSessionContinuation,
|
|
SupportsModelSelection: caps.SupportsModelSelection,
|
|
SupportsToolControl: caps.SupportsToolControl,
|
|
SupportsStreaming: caps.SupportsStreaming,
|
|
SupportedModels: caps.SupportedModels,
|
|
DefaultModel: caps.DefaultModel,
|
|
MaxPromptLength: caps.MaxPromptLength,
|
|
})
|
|
}
|
|
|
|
// AgentHealthDTO represents the health status of a single agent.
|
|
type AgentHealthDTO struct {
|
|
Provider string `json:"provider"`
|
|
Name string `json:"name"`
|
|
Healthy bool `json:"healthy"`
|
|
Message string `json:"message"`
|
|
Latency string `json:"latency"`
|
|
CheckedAt string `json:"checked_at"`
|
|
}
|
|
|
|
// AgentHealthResponse is the response for GET /agents/health.
|
|
type AgentHealthResponse struct {
|
|
Agents []AgentHealthDTO `json:"agents"`
|
|
HealthyCount int `json:"healthy_count"`
|
|
TotalCount int `json:"total_count"`
|
|
DefaultAgent string `json:"default_agent"`
|
|
DefaultHealth bool `json:"default_healthy"`
|
|
}
|
|
|
|
// Health returns the health status of all registered code agents.
|
|
// GET /agents/health
|
|
func (h *AgentsHandler) Health(w http.ResponseWriter, r *http.Request) {
|
|
if h.registry == nil {
|
|
api.WriteSuccess(w, r, AgentHealthResponse{
|
|
Agents: []AgentHealthDTO{},
|
|
HealthyCount: 0,
|
|
TotalCount: 0,
|
|
})
|
|
return
|
|
}
|
|
|
|
providers := h.registry.Available()
|
|
defaultProvider := h.registry.DefaultProvider()
|
|
|
|
agents := make([]AgentHealthDTO, 0, len(providers))
|
|
healthyCount := 0
|
|
defaultHealthy := false
|
|
|
|
for _, provider := range providers {
|
|
agent := h.registry.Get(provider)
|
|
if agent == nil {
|
|
continue
|
|
}
|
|
|
|
start := time.Now()
|
|
healthy := agent.Available(r.Context())
|
|
latency := time.Since(start)
|
|
|
|
msg := "available"
|
|
if !healthy {
|
|
msg = "unavailable"
|
|
}
|
|
|
|
if healthy {
|
|
healthyCount++
|
|
}
|
|
if provider == defaultProvider {
|
|
defaultHealthy = healthy
|
|
}
|
|
|
|
agents = append(agents, AgentHealthDTO{
|
|
Provider: string(provider),
|
|
Name: agent.Name(),
|
|
Healthy: healthy,
|
|
Message: msg,
|
|
Latency: latency.String(),
|
|
CheckedAt: time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
api.WriteSuccess(w, r, AgentHealthResponse{
|
|
Agents: agents,
|
|
HealthyCount: healthyCount,
|
|
TotalCount: len(agents),
|
|
DefaultAgent: string(defaultProvider),
|
|
DefaultHealth: defaultHealthy,
|
|
})
|
|
}
|
|
|
|
// SetDefaultRequest is the request body for POST /agents/default.
|
|
type SetDefaultRequest struct {
|
|
Provider string `json:"provider"`
|
|
}
|
|
|
|
// SetDefault changes the default code agent.
|
|
// POST /agents/default
|
|
func (h *AgentsHandler) SetDefault(w http.ResponseWriter, r *http.Request) {
|
|
if h.registry == nil {
|
|
api.WriteBadRequest(w, r, "no agents registered")
|
|
return
|
|
}
|
|
|
|
var req SetDefaultRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
api.WriteBadRequest(w, r, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if req.Provider == "" {
|
|
api.WriteBadRequest(w, r, "provider is required")
|
|
return
|
|
}
|
|
|
|
provider := domain.AgentProvider(req.Provider)
|
|
if err := h.registry.SetDefault(provider); err != nil {
|
|
api.WriteBadRequest(w, r, err.Error())
|
|
return
|
|
}
|
|
|
|
api.WriteSuccess(w, r, map[string]any{
|
|
"default_agent": req.Provider,
|
|
"message": "default agent updated",
|
|
})
|
|
}
|