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,48 @@
|
|||||||
|
package acp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AgentKindAdminDTO 是 admin 视图的完整字段。EnvKeys 仅返回 key 列表(不返回值)。
|
||||||
|
type AgentKindAdminDTO struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
BinaryPath string `json:"binary_path"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
EnvKeys []string `json:"env_keys"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CreatedBy uuid.UUID `json:"created_by"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentKindPublicDTO 是普通用户视图,不暴露 binary_path / args / env_keys。
|
||||||
|
type AgentKindPublicDTO struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAgentKindReq / UpdateAgentKindReq 是 HTTP 请求体。
|
||||||
|
type CreateAgentKindReq struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
BinaryPath string `json:"binary_path"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
Env map[string]string `json:"env"`
|
||||||
|
Enabled *bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateAgentKindReq struct {
|
||||||
|
DisplayName *string `json:"display_name,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
BinaryPath *string `json:"binary_path,omitempty"`
|
||||||
|
Args []string `json:"args,omitempty"`
|
||||||
|
Env map[string]string `json:"env,omitempty"`
|
||||||
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
|
}
|
||||||
@@ -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