You've already forked agentic-coding-workflow
feat(acp): HTTP handler for agent kinds (admin write / user read with field redaction)
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
// Package acp handler.go exposes the ACP module's HTTP / WS endpoints.
|
||||
// Part 1: agent_kinds routes only. Part 2 will add sessions REST + WS upgrader.
|
||||
package acp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||
httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
// AdminLookup resolves whether a user is an admin (mirrors chat/workspace pattern).
|
||||
type AdminLookup interface {
|
||||
IsAdmin(ctx context.Context, userID uuid.UUID) (bool, error)
|
||||
}
|
||||
|
||||
// Handler exposes ACP HTTP endpoints. Part 2 will extend with SessionService and Supervisor.
|
||||
type Handler struct {
|
||||
akSvc AgentKindService
|
||||
resolver middleware.SessionResolver
|
||||
adminLookup AdminLookup
|
||||
enc *crypto.Encryptor
|
||||
}
|
||||
|
||||
// NewHandler constructs Handler (Part 1: agent_kinds only).
|
||||
func NewHandler(ak AgentKindService, resolver middleware.SessionResolver, al AdminLookup, enc *crypto.Encryptor) *Handler {
|
||||
return &Handler{akSvc: ak, resolver: resolver, adminLookup: al, enc: enc}
|
||||
}
|
||||
|
||||
// Mount registers /api/v1/acp/* routes.
|
||||
func (h *Handler) Mount(r chi.Router) {
|
||||
r.Route("/api/v1/acp/agent-kinds", func(r chi.Router) {
|
||||
r.Use(middleware.Auth(h.resolver, middleware.AuthOptions{}))
|
||||
r.Get("/", h.listAgentKinds)
|
||||
r.Post("/", h.createAgentKind)
|
||||
r.Get("/{id}", h.getAgentKind)
|
||||
r.Patch("/{id}", h.updateAgentKind)
|
||||
r.Delete("/{id}", h.deleteAgentKind)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) caller(r *http.Request) (Caller, error) {
|
||||
uid, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
return Caller{}, errs.New(errs.CodeUnauthorized, "not authenticated")
|
||||
}
|
||||
admin, err := h.adminLookup.IsAdmin(r.Context(), uid)
|
||||
if err != nil {
|
||||
return Caller{}, err
|
||||
}
|
||||
return Caller{UserID: uid, IsAdmin: admin}, nil
|
||||
}
|
||||
|
||||
func (h *Handler) listAgentKinds(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := h.caller(r)
|
||||
if err != nil {
|
||||
writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
ks, err := h.akSvc.List(r.Context(), c)
|
||||
if err != nil {
|
||||
writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
if c.IsAdmin {
|
||||
out := make([]AgentKindAdminDTO, 0, len(ks))
|
||||
for _, k := range ks {
|
||||
out = append(out, h.agentKindToAdminDTO(k))
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, out)
|
||||
return
|
||||
}
|
||||
out := make([]AgentKindPublicDTO, 0, len(ks))
|
||||
for _, k := range ks {
|
||||
out = append(out, agentKindToPublicDTO(k))
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *Handler) createAgentKind(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := h.caller(r)
|
||||
if err != nil {
|
||||
writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
var req CreateAgentKindReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad json"))
|
||||
return
|
||||
}
|
||||
enabled := true
|
||||
if req.Enabled != nil {
|
||||
enabled = *req.Enabled
|
||||
}
|
||||
in := CreateAgentKindInput{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
DisplayName: req.DisplayName,
|
||||
Description: req.Description,
|
||||
BinaryPath: strings.TrimSpace(req.BinaryPath),
|
||||
Args: req.Args,
|
||||
Env: req.Env,
|
||||
Enabled: enabled,
|
||||
}
|
||||
out, err := h.akSvc.Create(r.Context(), c, in)
|
||||
if err != nil {
|
||||
writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, h.agentKindToAdminDTO(out))
|
||||
}
|
||||
|
||||
func (h *Handler) getAgentKind(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := h.caller(r)
|
||||
if err != nil {
|
||||
writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
id, perr := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if perr != nil {
|
||||
writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad id"))
|
||||
return
|
||||
}
|
||||
k, err := h.akSvc.Get(r.Context(), c, id)
|
||||
if err != nil {
|
||||
writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
if c.IsAdmin {
|
||||
httpx.WriteJSON(w, http.StatusOK, h.agentKindToAdminDTO(k))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, agentKindToPublicDTO(k))
|
||||
}
|
||||
|
||||
func (h *Handler) updateAgentKind(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := h.caller(r)
|
||||
if err != nil {
|
||||
writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
id, perr := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if perr != nil {
|
||||
writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad id"))
|
||||
return
|
||||
}
|
||||
var req UpdateAgentKindReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad json"))
|
||||
return
|
||||
}
|
||||
in := UpdateAgentKindInput{
|
||||
DisplayName: req.DisplayName,
|
||||
Description: req.Description,
|
||||
BinaryPath: req.BinaryPath,
|
||||
Args: req.Args,
|
||||
Env: req.Env,
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
out, err := h.akSvc.Update(r.Context(), c, id, in)
|
||||
if err != nil {
|
||||
writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, h.agentKindToAdminDTO(out))
|
||||
}
|
||||
|
||||
func (h *Handler) deleteAgentKind(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := h.caller(r)
|
||||
if err != nil {
|
||||
writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
id, perr := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if perr != nil {
|
||||
writeErr(w, r, errs.New(errs.CodeInvalidInput, "bad id"))
|
||||
return
|
||||
}
|
||||
if err := h.akSvc.Delete(r.Context(), c, id); err != nil {
|
||||
writeErr(w, r, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ===== DTO converters =====
|
||||
|
||||
// agentKindToAdminDTO is a Handler method because env-key extraction needs the encryptor.
|
||||
func (h *Handler) agentKindToAdminDTO(k *AgentKind) AgentKindAdminDTO {
|
||||
envKeys := []string{}
|
||||
if len(k.EncryptedEnv) > 0 {
|
||||
if env, err := decryptEnv(h.enc, k.EncryptedEnv); err == nil {
|
||||
for key := range env {
|
||||
envKeys = append(envKeys, key)
|
||||
}
|
||||
sort.Strings(envKeys)
|
||||
}
|
||||
}
|
||||
return AgentKindAdminDTO{
|
||||
ID: k.ID,
|
||||
Name: k.Name,
|
||||
DisplayName: k.DisplayName,
|
||||
Description: k.Description,
|
||||
BinaryPath: k.BinaryPath,
|
||||
Args: append([]string(nil), k.Args...),
|
||||
EnvKeys: envKeys,
|
||||
Enabled: k.Enabled,
|
||||
CreatedBy: k.CreatedBy,
|
||||
CreatedAt: k.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: k.UpdatedAt.UTC().Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
func agentKindToPublicDTO(k *AgentKind) AgentKindPublicDTO {
|
||||
return AgentKindPublicDTO{
|
||||
ID: k.ID,
|
||||
Name: k.Name,
|
||||
DisplayName: k.DisplayName,
|
||||
Enabled: k.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// ===== transport helpers =====
|
||||
|
||||
func writeErr(w http.ResponseWriter, r *http.Request, err error) {
|
||||
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err)
|
||||
}
|
||||
Reference in New Issue
Block a user