package workflow import ( "fmt" "regexp" "strconv" "strings" ) // APIDefinition represents a third-party API endpoint configuration type APIDefinition struct { ID string `json:"id"` // Unique API identifier Method string `json:"method"` // HTTP method (GET, POST, PUT, PATCH, DELETE) URL string `json:"url"` // API endpoint URL (may contain {pathParam} placeholders) Auth string `json:"auth"` // Authentication credential reference (e.g., "SYSVAR.apiKey") Headers map[string]string `json:"headers"` // Static request headers Desc string `json:"desc"` // Human-readable description } // Registry defines external resources and global boundaries for workflow execution type Registry struct { Services []string `json:"services"` // Service signatures (VL format) APIs []APIDefinition `json:"apis"` // Third-party API endpoint definitions Components []string `json:"components"` // Component IDs Params []string `json:"params"` // Input parameters (VL format, read-only during execution) Vars []string `json:"vars"` // Global variables (VL format) Files FilesRegistry `json:"files"` // File access boundaries Docs map[string]string `json:"docs"` // Semantic document references (docId -> description) Schemas map[string]map[string]interface{} `json:"schemas"` // JSON Schema definitions for reuse (v3.9+, schemaId -> schema object) } // FilesRegistry defines file input/output boundaries type FilesRegistry struct { Inputs []string `json:"inputs"` // Read-only input files (path patterns) Artifacts []string `json:"artifacts"` // Temporary writable files (path patterns) } // ServiceSignature represents a parsed service signature type ServiceSignature struct { Name string // Service name Parameters []ParameterDef // Input parameters Returns []ParameterDef // Output fields } // ParameterDef represents a parameter definition type ParameterDef struct { Name string // Parameter name Type string // Parameter type (STRING, INT, BOOL, OBJECT, ARRAY, FILE_REF, etc.) } // VariableDeclaration represents a parsed variable declaration type VariableDeclaration struct { Name string // Variable name (with $ prefix) Type string // Variable type } // ParamDeclaration represents a parsed parameter declaration type ParamDeclaration struct { Name string // Parameter name (without $ prefix) Type string // Parameter type Default *string // Optional default value literal (spec §3.2, e.g. "3" for INT=3) } // ParseServiceSignature parses a VL-format service signature // Format: "ServiceName(param1(TYPE1), param2(TYPE2)) RETURN result1(TYPE1), result2(TYPE2)" func ParseServiceSignature(signature string) (*ServiceSignature, error) { // Example: "PlannerService(prd(STRING), rulesFile(FILE_REF)) RETURN plan(OBJECT)" // Match service name and parameters re := regexp.MustCompile(`^(\w+)\((.*?)\)\s+RETURN\s+(.*)$`) matches := re.FindStringSubmatch(strings.TrimSpace(signature)) if len(matches) != 4 { return nil, fmt.Errorf("invalid service signature format: %s", signature) } serviceName := matches[1] paramsStr := matches[2] returnsStr := matches[3] sig := &ServiceSignature{ Name: serviceName, } // Parse parameters if paramsStr != "" { params, err := parseParameters(paramsStr) if err != nil { return nil, fmt.Errorf("failed to parse parameters: %w", err) } sig.Parameters = params } // Parse return values if returnsStr != "" { returns, err := parseParameters(returnsStr) if err != nil { return nil, fmt.Errorf("failed to parse return values: %w", err) } sig.Returns = returns } return sig, nil } // parseParameters parses parameter list in format: "param1(TYPE1), param2(TYPE2)" func parseParameters(paramsStr string) ([]ParameterDef, error) { var params []ParameterDef // Split by comma, but need to handle nested parentheses parts := strings.Split(paramsStr, ",") for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } // Match paramName(TYPE) re := regexp.MustCompile(`^(\w+)\(([^)]+)\)$`) matches := re.FindStringSubmatch(part) if len(matches) != 3 { return nil, fmt.Errorf("invalid parameter format: %s", part) } params = append(params, ParameterDef{ Name: matches[1], Type: matches[2], }) } return params, nil } // ParseVariableDeclaration parses a VL-format variable declaration // Format: "$varName(TYPE)" or "$varName([TYPE])" for arrays func ParseVariableDeclaration(declaration string) (*VariableDeclaration, error) { // Example: "$keyword(STRING)", "$items([OBJECT])", "$result(OBJECT)" re := regexp.MustCompile(`^(\$\w+)\((.+)\)$`) matches := re.FindStringSubmatch(strings.TrimSpace(declaration)) if len(matches) != 3 { return nil, fmt.Errorf("invalid variable declaration format: %s", declaration) } return &VariableDeclaration{ Name: matches[1], Type: matches[2], }, nil } // ParseParamDeclaration parses a VL-format parameter declaration. // Supported formats: // - "paramName(TYPE)" — no default // - "paramName([TYPE])" — array type, no default // - "paramName(TYPE) = value" — with default value (spec §3.2) // // Examples: "userId(STRING)", "filters([OBJECT])", "maxRetries(INT) = 3" func ParseParamDeclaration(declaration string) (*ParamDeclaration, error) { // Optional trailing "= " after the closing paren. re := regexp.MustCompile(`^([a-zA-Z]\w*)\((.+)\)(?:\s*=\s*(.+))?$`) matches := re.FindStringSubmatch(strings.TrimSpace(declaration)) if len(matches) < 3 { return nil, fmt.Errorf("invalid parameter declaration format: %s", declaration) } decl := &ParamDeclaration{ Name: matches[1], Type: matches[2], } // matches[3] is the optional default value (empty string if not present) if len(matches) == 4 && matches[3] != "" { defaultVal := strings.TrimSpace(matches[3]) // Strip surrounding quotes from string literals, e.g. "\"hello\"" → "hello" if len(defaultVal) >= 2 && defaultVal[0] == '"' && defaultVal[len(defaultVal)-1] == '"' { defaultVal = defaultVal[1 : len(defaultVal)-1] } decl.Default = &defaultVal } return decl, nil } // ValidateRegistry validates the registry structure and constraints func (r *Registry) ValidateRegistry() error { // Check for duplicate service names serviceNames := make(map[string]bool) for _, sig := range r.Services { parsed, err := ParseServiceSignature(sig) if err != nil { return fmt.Errorf("invalid service signature: %w", err) } if serviceNames[parsed.Name] { return fmt.Errorf("duplicate service name: %s", parsed.Name) } serviceNames[parsed.Name] = true } // Check for duplicate API IDs apiIDs := make(map[string]bool) for _, api := range r.APIs { if apiIDs[api.ID] { return fmt.Errorf("duplicate API ID: %s", api.ID) } apiIDs[api.ID] = true // Validate required fields if api.Method == "" { return fmt.Errorf("API %s: method is required", api.ID) } if api.URL == "" { return fmt.Errorf("API %s: URL is required", api.ID) } // Validate HTTP method validMethods := map[string]bool{ "GET": true, "POST": true, "PUT": true, "PATCH": true, "DELETE": true, } if !validMethods[strings.ToUpper(api.Method)] { return fmt.Errorf("API %s: invalid HTTP method: %s", api.ID, api.Method) } } // Ensure APIs is initialized if r.APIs == nil { r.APIs = []APIDefinition{} } // Check for duplicate component IDs componentIDs := make(map[string]bool) for _, comp := range r.Components { if componentIDs[comp] { return fmt.Errorf("duplicate component ID: %s", comp) } componentIDs[comp] = true } // Check for duplicate parameter names paramNames := make(map[string]bool) for _, paramDecl := range r.Params { parsed, err := ParseParamDeclaration(paramDecl) if err != nil { return fmt.Errorf("invalid parameter declaration: %w", err) } if paramNames[parsed.Name] { return fmt.Errorf("duplicate parameter name: %s", parsed.Name) } paramNames[parsed.Name] = true } // Check for duplicate variable names varNames := make(map[string]bool) for _, varDecl := range r.Vars { parsed, err := ParseVariableDeclaration(varDecl) if err != nil { return fmt.Errorf("invalid variable declaration: %w", err) } if varNames[parsed.Name] { return fmt.Errorf("duplicate variable name: %s", parsed.Name) } varNames[parsed.Name] = true } // Ensure files section has both inputs and artifacts if r.Files.Inputs == nil { r.Files.Inputs = []string{} } if r.Files.Artifacts == nil { r.Files.Artifacts = []string{} } // Ensure docs is initialized if r.Docs == nil { r.Docs = make(map[string]string) } // Ensure schemas is initialized if r.Schemas == nil { r.Schemas = make(map[string]map[string]interface{}) } // Validate schema IDs are not empty for schemaID := range r.Schemas { if schemaID == "" { return fmt.Errorf("schema ID cannot be empty") } } return nil } // HasDoc checks if a document ID is registered func (r *Registry) HasDoc(docID string) bool { if r.Docs == nil { return false } _, ok := r.Docs[docID] return ok } // GetDocDescription retrieves the description for a document ID func (r *Registry) GetDocDescription(docID string) (string, bool) { if r.Docs == nil { return "", false } desc, ok := r.Docs[docID] return desc, ok } // GetServiceSignature retrieves a parsed service signature by name func (r *Registry) GetServiceSignature(serviceName string) (*ServiceSignature, error) { for _, sig := range r.Services { parsed, err := ParseServiceSignature(sig) if err != nil { return nil, err } if parsed.Name == serviceName { return parsed, nil } } return nil, fmt.Errorf("service not found: %s", serviceName) } // HasComponent checks if a component is registered func (r *Registry) HasComponent(componentID string) bool { for _, comp := range r.Components { if comp == componentID { return true } } return false } // GetAPIDefinition retrieves an API definition by ID func (r *Registry) GetAPIDefinition(apiID string) (*APIDefinition, error) { for i := range r.APIs { if r.APIs[i].ID == apiID { return &r.APIs[i], nil } } return nil, fmt.Errorf("API not found: %s", apiID) } // HasAPI checks if an API is registered func (r *Registry) HasAPI(apiID string) bool { for _, api := range r.APIs { if api.ID == apiID { return true } } return false } // GetVariableDeclarations returns all parsed variable declarations func (r *Registry) GetVariableDeclarations() (map[string]*VariableDeclaration, error) { result := make(map[string]*VariableDeclaration) for _, varDecl := range r.Vars { parsed, err := ParseVariableDeclaration(varDecl) if err != nil { return nil, err } result[parsed.Name] = parsed } return result, nil } // GetParamDeclarations returns all parsed parameter declarations func (r *Registry) GetParamDeclarations() (map[string]*ParamDeclaration, error) { result := make(map[string]*ParamDeclaration) for _, paramDecl := range r.Params { parsed, err := ParseParamDeclaration(paramDecl) if err != nil { return nil, err } result[parsed.Name] = parsed } return result, nil } // IsInputPathAllowed checks if a read path is within allowed inputs func (r *Registry) IsInputPathAllowed(path string) bool { return matchesAnyPattern(path, r.Files.Inputs) } // IsArtifactPathAllowed checks if a write path is within allowed artifacts func (r *Registry) IsArtifactPathAllowed(path string) bool { return matchesAnyPattern(path, r.Files.Artifacts) } // matchesAnyPattern checks if a path matches any of the given patterns // Supports glob-like patterns with * wildcard func matchesAnyPattern(path string, patterns []string) bool { for _, pattern := range patterns { if matchPattern(path, pattern) { return true } } return false } // matchPattern performs simple glob-style pattern matching func matchPattern(path, pattern string) bool { // Simple implementation: * matches any characters // For production use, consider using filepath.Match or a more robust library if !strings.Contains(pattern, "*") { return path == pattern } // Convert glob pattern to regex regexPattern := strings.ReplaceAll(regexp.QuoteMeta(pattern), `\*`, `.*`) matched, err := regexp.MatchString("^"+regexPattern+"$", path) if err != nil { return false } return matched } // HasSchema checks if a schema ID is registered (v3.9+) func (r *Registry) HasSchema(schemaID string) bool { if r.Schemas == nil { return false } _, ok := r.Schemas[schemaID] return ok } // GetSchema retrieves a schema by ID (v3.9+) func (r *Registry) GetSchema(schemaID string) (map[string]interface{}, error) { if r.Schemas == nil { return nil, fmt.Errorf("no schemas registered") } schema, ok := r.Schemas[schemaID] if !ok { return nil, fmt.Errorf("schema not found: %s", schemaID) } return schema, nil } // CoerceParamDefault converts the string default value of a ParamDeclaration to // the appropriate Go type based on the declared type (spec §3.2). // // Supported type coercions: // - INT → int64 // - FLOAT → float64 // - BOOL → bool // - all others (STRING, OBJECT, ARRAY, etc.) → string (already stripped of quotes) // // Returns nil, nil if the declaration has no default. func CoerceParamDefault(decl *ParamDeclaration) (interface{}, error) { if decl.Default == nil { return nil, nil } raw := *decl.Default switch strings.ToUpper(decl.Type) { case "INT": v, err := strconv.ParseInt(raw, 10, 64) if err != nil { return nil, fmt.Errorf("param %s: cannot parse default %q as INT: %w", decl.Name, raw, err) } return v, nil case "FLOAT": v, err := strconv.ParseFloat(raw, 64) if err != nil { return nil, fmt.Errorf("param %s: cannot parse default %q as FLOAT: %w", decl.Name, raw, err) } return v, nil case "BOOL": v, err := strconv.ParseBool(raw) if err != nil { return nil, fmt.Errorf("param %s: cannot parse default %q as BOOL: %w", decl.Name, raw, err) } return v, nil default: // STRING and all other types: return as-is (quotes already stripped by ParseParamDeclaration) return raw, nil } }