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") } clientType := in.ClientType if clientType == "" { clientType = ClientGeneric } if !clientType.Valid() { return nil, errs.New(errs.CodeInvalidInput, "invalid client_type") } encrypted, err := encryptEnv(s.enc, in.Env) if err != nil { return nil, err } encryptedMCP, err := encryptMCPServers(s.enc, in.MCPServers) if err != nil { return nil, err } args := in.Args if args == nil { args = []string{} } allowlist := in.ToolAllowlist if allowlist == nil { allowlist = []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, ToolAllowlist: allowlist, ClientType: clientType, EncryptedMCPServers: encryptedMCP, 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"] = "" } if in.Description != nil && *in.Description != cur.Description { cur.Description = *in.Description changed["description"] = "" } if in.BinaryPath != nil && *in.BinaryPath != cur.BinaryPath { cur.BinaryPath = *in.BinaryPath changed["binary_path"] = "" } if in.Args != nil { cur.Args = in.Args changed["args"] = "" } if in.Env != nil { encrypted, eerr := encryptEnv(s.enc, in.Env) if eerr != nil { return nil, eerr } cur.EncryptedEnv = encrypted changed["env"] = "" } if in.ToolAllowlist != nil { cur.ToolAllowlist = in.ToolAllowlist changed["tool_allowlist"] = "" } if in.ClientType != nil && *in.ClientType != cur.ClientType { if !in.ClientType.Valid() { return nil, errs.New(errs.CodeInvalidInput, "invalid client_type") } cur.ClientType = *in.ClientType changed["client_type"] = string(*in.ClientType) } if in.MCPServers != nil { encryptedMCP, eerr := encryptMCPServers(s.enc, in.MCPServers) if eerr != nil { return nil, eerr } cur.EncryptedMCPServers = encryptedMCP changed["mcp_servers"] = "" } 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) } // encryptMCPServers 校验并加密 MCP server 列表。nil → nil(不存储)。 func encryptMCPServers(enc *crypto.Encryptor, servers []McpServerSpec) ([]byte, error) { if servers == nil { return nil, nil } if err := validateMCPServers(servers); err != nil { return nil, err } b, err := json.Marshal(servers) if err != nil { return nil, errs.Wrap(err, errs.CodeInternal, "marshal mcp servers") } return enc.Encrypt(b) } // decryptMCPServers 解密 MCP server 列表。空密文 → 空列表。 func decryptMCPServers(enc *crypto.Encryptor, encrypted []byte) ([]McpServerSpec, error) { if len(encrypted) == 0 { return []McpServerSpec{}, nil } plain, err := enc.Decrypt(encrypted) if err != nil { return nil, errs.Wrap(err, errs.CodeInternal, "decrypt mcp servers") } var out []McpServerSpec if err := json.Unmarshal(plain, &out); err != nil { return nil, errs.Wrap(err, errs.CodeInternal, "unmarshal mcp servers") } if out == nil { out = []McpServerSpec{} } return out, nil } // validateMCPServers 校验条目:name 必填且不重复;stdio 需 command,http/sse 需 url。 func validateMCPServers(servers []McpServerSpec) error { seen := map[string]bool{} for _, sv := range servers { if sv.Name == "" { return errs.New(errs.CodeInvalidInput, "mcp server name required") } if sv.Name == AcwMcpServerName { return errs.New(errs.CodeInvalidInput, "mcp server name '"+AcwMcpServerName+"' is reserved") } if seen[sv.Name] { return errs.New(errs.CodeInvalidInput, "duplicate mcp server name: "+sv.Name) } seen[sv.Name] = true switch sv.Type { case McpStdio: if sv.Command == "" { return errs.New(errs.CodeInvalidInput, "mcp server "+sv.Name+": command required for stdio") } case McpHTTP, McpSSE: if sv.URL == "" { return errs.New(errs.CodeInvalidInput, "mcp server "+sv.Name+": url required for "+string(sv.Type)) } default: return errs.New(errs.CodeInvalidInput, "mcp server "+sv.Name+": invalid type") } } return nil } // 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 }