| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 |
- 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 "= <defaultValue>" 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
- }
- }
|