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>
115 lines
3.4 KiB
Go
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
|
|
}
|