You've already forked agentic-coding-workflow
feat(acp): AgentKindService + unit tests (fake repo + real encryptor)
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
package acp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||
)
|
||||
|
||||
type agentKindService struct {
|
||||
repo Repository
|
||||
enc *crypto.Encryptor
|
||||
audit audit.Recorder
|
||||
}
|
||||
|
||||
// NewAgentKindService 构造 AgentKindService。
|
||||
func NewAgentKindService(repo Repository, enc *crypto.Encryptor, rec audit.Recorder) AgentKindService {
|
||||
return &agentKindService{repo: repo, enc: enc, audit: rec}
|
||||
}
|
||||
|
||||
func (s *agentKindService) Create(ctx context.Context, c Caller, in CreateAgentKindInput) (*AgentKind, error) {
|
||||
if !c.IsAdmin {
|
||||
return nil, errs.New(errs.CodeForbidden, "admin only")
|
||||
}
|
||||
if in.Name == "" || in.DisplayName == "" || in.BinaryPath == "" {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "name / display_name / binary_path required")
|
||||
}
|
||||
encrypted, err := encryptEnv(s.enc, in.Env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args := in.Args
|
||||
if args == nil {
|
||||
args = []string{}
|
||||
}
|
||||
k := &AgentKind{
|
||||
ID: uuid.New(), Name: in.Name, DisplayName: in.DisplayName, Description: in.Description,
|
||||
BinaryPath: in.BinaryPath, Args: args, EncryptedEnv: encrypted, Enabled: in.Enabled,
|
||||
CreatedBy: c.UserID,
|
||||
}
|
||||
out, err := s.repo.CreateAgentKind(ctx, k)
|
||||
if err != nil {
|
||||
if IsUniqueViolation(err) {
|
||||
return nil, errs.New(errs.CodeConflict, "agent kind name already exists")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
s.recordAudit(ctx, c, "acp.agent_kind.create", out.ID.String(),
|
||||
map[string]any{"name": out.Name, "binary_path": out.BinaryPath})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *agentKindService) List(ctx context.Context, c Caller) ([]*AgentKind, error) {
|
||||
if c.IsAdmin {
|
||||
return s.repo.ListAgentKinds(ctx)
|
||||
}
|
||||
return s.repo.ListEnabledAgentKinds(ctx)
|
||||
}
|
||||
|
||||
func (s *agentKindService) Get(ctx context.Context, c Caller, id uuid.UUID) (*AgentKind, error) {
|
||||
k, err := s.repo.GetAgentKindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !c.IsAdmin && !k.Enabled {
|
||||
return nil, errs.New(errs.CodeAcpAgentKindNotFound, "agent kind not found")
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func (s *agentKindService) Update(ctx context.Context, c Caller, id uuid.UUID, in UpdateAgentKindInput) (*AgentKind, error) {
|
||||
if !c.IsAdmin {
|
||||
return nil, errs.New(errs.CodeForbidden, "admin only")
|
||||
}
|
||||
cur, err := s.repo.GetAgentKindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changed := map[string]any{}
|
||||
if in.DisplayName != nil && *in.DisplayName != cur.DisplayName {
|
||||
cur.DisplayName = *in.DisplayName
|
||||
changed["display_name"] = "<changed>"
|
||||
}
|
||||
if in.Description != nil && *in.Description != cur.Description {
|
||||
cur.Description = *in.Description
|
||||
changed["description"] = "<changed>"
|
||||
}
|
||||
if in.BinaryPath != nil && *in.BinaryPath != cur.BinaryPath {
|
||||
cur.BinaryPath = *in.BinaryPath
|
||||
changed["binary_path"] = "<changed>"
|
||||
}
|
||||
if in.Args != nil {
|
||||
cur.Args = in.Args
|
||||
changed["args"] = "<changed>"
|
||||
}
|
||||
if in.Env != nil {
|
||||
encrypted, eerr := encryptEnv(s.enc, in.Env)
|
||||
if eerr != nil {
|
||||
return nil, eerr
|
||||
}
|
||||
cur.EncryptedEnv = encrypted
|
||||
changed["env"] = "<changed>"
|
||||
}
|
||||
if in.Enabled != nil && *in.Enabled != cur.Enabled {
|
||||
cur.Enabled = *in.Enabled
|
||||
changed["enabled"] = *in.Enabled
|
||||
}
|
||||
if len(changed) == 0 {
|
||||
return cur, nil
|
||||
}
|
||||
out, err := s.repo.UpdateAgentKind(ctx, cur)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.recordAudit(ctx, c, "acp.agent_kind.update", out.ID.String(),
|
||||
map[string]any{"changed_fields": changed})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *agentKindService) Delete(ctx context.Context, c Caller, id uuid.UUID) error {
|
||||
if !c.IsAdmin {
|
||||
return errs.New(errs.CodeForbidden, "admin only")
|
||||
}
|
||||
cur, err := s.repo.GetAgentKindByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.repo.DeleteAgentKind(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
s.recordAudit(ctx, c, "acp.agent_kind.delete", id.String(),
|
||||
map[string]any{"name": cur.Name})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *agentKindService) recordAudit(ctx context.Context, c Caller, action, targetID string, meta map[string]any) {
|
||||
if s.audit == nil {
|
||||
return
|
||||
}
|
||||
uid := c.UserID
|
||||
_ = s.audit.Record(context.WithoutCancel(ctx), audit.Entry{
|
||||
UserID: &uid, Action: action, TargetType: "acp_agent_kind", TargetID: targetID, Metadata: meta,
|
||||
})
|
||||
}
|
||||
|
||||
// encryptEnv 把 map[string]string 序列化后加密。nil → 返回 nil(清空)。
|
||||
func encryptEnv(enc *crypto.Encryptor, env map[string]string) ([]byte, error) {
|
||||
if env == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if len(env) == 0 {
|
||||
return enc.Encrypt([]byte(`{}`))
|
||||
}
|
||||
b, err := json.Marshal(env)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "marshal env")
|
||||
}
|
||||
return enc.Encrypt(b)
|
||||
}
|
||||
|
||||
// decryptEnv 解密并反序列化 env map。Part 2 的 supervisor.Spawn 用。
|
||||
func decryptEnv(enc *crypto.Encryptor, encrypted []byte) (map[string]string, error) {
|
||||
if len(encrypted) == 0 {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
plain, err := enc.Decrypt(encrypted)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "decrypt env")
|
||||
}
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal(plain, &m); err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "unmarshal env")
|
||||
}
|
||||
if m == nil {
|
||||
m = map[string]string{}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
Reference in New Issue
Block a user