Files
Agent-System-Backend/internal/workflow/validate.go
weichenandCursor 63ea1a262c Register plugin workflows and drop them on uninstall
Why: Intent routing needs a registry of workflow definitions, and uninstall was leaving intents and workflows in place.
Evidence: go test ./internal/workflow/ ./internal/plugins/ ./internal/app/ passed. Validation rejects duplicate ids, unknown kinds, denied tools, and both step caps. Unregister clears the registry and the intent gate.
Impact: Script steps must stay inside the plugin directory. The server holds the registry for later routing.
Refs: feature/agent-harness-p4
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 15:59:54 +00:00

115 lines
3.4 KiB
Go

package workflow
import (
"fmt"
"strings"
core "agent-system-core"
)
// 声明式工作流与模型生成的工作流用两套上限。
const (
MaxDeclarativeSteps = 20
MaxDynamicSteps = 8
MaxStepRetries = 3
MaxWorkflowRetries = 6
)
// DeniedTools 是工作流步骤和脚本回调都不能调用的工具。
var DeniedTools = map[string]struct{}{
"run_python": {},
"run_shell": {},
"run_workflow": {},
"delegate_task": {},
"squad_orchestrate": {},
"review_findings": {},
}
// Limits 区分声明式和动态工作流。
type Limits struct {
MaxSteps int
AllowScript bool
}
func DeclarativeLimits() Limits {
return Limits{MaxSteps: MaxDeclarativeSteps, AllowScript: true}
}
func DynamicLimits() Limits {
return Limits{MaxSteps: MaxDynamicSteps, AllowScript: false}
}
var stepKinds = map[string]struct{}{
"prompt": {}, "tool": {}, "hitl": {}, "artifact": {}, "card": {}, "script": {},
}
// Validate 检查定义是否能执行。known 报告工具是否已注册;nil 时只检查禁用名单。
func Validate(def core.WorkflowDef, lim Limits, known func(name string) bool) error {
if strings.TrimSpace(def.ID) == "" {
return fmt.Errorf("workflow: id 为空")
}
if lim.MaxSteps <= 0 {
return fmt.Errorf("workflow: %s: 未设置步骤上限", def.ID)
}
if n := len(def.Steps); n == 0 || n > lim.MaxSteps {
return fmt.Errorf("workflow: %s: 步骤数 %d 超出上限 %d", def.ID, n, lim.MaxSteps)
}
if def.MaxRetries > MaxWorkflowRetries {
return fmt.Errorf("workflow: %s: maxRetries %d 超过 %d", def.ID, def.MaxRetries, MaxWorkflowRetries)
}
seen := map[string]struct{}{}
for _, step := range def.Steps {
if strings.TrimSpace(step.ID) == "" {
return fmt.Errorf("workflow: %s: 步骤 id 为空", def.ID)
}
if _, ok := seen[step.ID]; ok {
return fmt.Errorf("workflow: %s: 步骤 id %s 重复", def.ID, step.ID)
}
seen[step.ID] = struct{}{}
if _, ok := stepKinds[step.Kind]; !ok {
return fmt.Errorf("workflow: %s: 步骤 %s 的 kind %q 不合法", def.ID, step.ID, step.Kind)
}
if step.Critic != nil && step.Critic.MaxRetries > MaxStepRetries {
return fmt.Errorf("workflow: %s: 步骤 %s 的重试上限 %d 超过 %d", def.ID, step.ID, step.Critic.MaxRetries, MaxStepRetries)
}
switch step.Kind {
case "tool":
if err := checkTool(def.ID, step, known); err != nil {
return err
}
case "script":
if !lim.AllowScript {
return fmt.Errorf("workflow: %s: 动态工作流不能包含 script 步骤 %s", def.ID, step.ID)
}
if strings.TrimSpace(step.Script) == "" || !scriptPathOK(step.Script) {
return fmt.Errorf("workflow: %s: 步骤 %s 的脚本路径不合法", def.ID, step.ID)
}
}
}
return nil
}
func checkTool(workflowID string, step core.WorkflowStep, known func(name string) bool) error {
name := strings.TrimSpace(step.Tool)
if name == "" {
return fmt.Errorf("workflow: %s: 步骤 %s 缺少工具名", workflowID, step.ID)
}
if _, denied := DeniedTools[name]; denied {
return fmt.Errorf("workflow: %s: 步骤 %s 不能调用 %s", workflowID, step.ID, name)
}
if known != nil && !known(name) {
return fmt.Errorf("workflow: %s: 步骤 %s 的工具 %s 未注册", workflowID, step.ID, name)
}
return nil
}
func scriptPathOK(path string) bool {
if path == "" || strings.HasPrefix(path, "/") || strings.Contains(path, "\\") {
return false
}
if path == ".." || strings.HasPrefix(path, "../") || strings.Contains(path, "/../") {
return false
}
return true
}