Skip to content

Commit 606a62d

Browse files
authored
Refactor validation.go into domain-specific validation files (#3758)
1 parent 9263045 commit 606a62d

5 files changed

Lines changed: 1028 additions & 831 deletions

File tree

pkg/workflow/agent_validation.go

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
// Package workflow provides agent file and feature support validation.
2+
//
3+
// # Agent Validation
4+
//
5+
// This file validates agent-specific configuration and feature compatibility
6+
// for agentic workflows. It ensures that:
7+
// - Custom agent files exist when specified
8+
// - Engine features are supported (HTTP transport, max-turns, web-search)
9+
// - Workflow triggers have appropriate security constraints
10+
//
11+
// # Validation Functions
12+
//
13+
// - validateAgentFile() - Validates custom agent file exists
14+
// - validateHTTPTransportSupport() - Validates HTTP MCP compatibility with engine
15+
// - validateMaxTurnsSupport() - Validates max-turns feature support
16+
// - validateWebSearchSupport() - Validates web-search feature support (warning)
17+
// - validateWorkflowRunBranches() - Validates workflow_run has branch restrictions
18+
//
19+
// # Validation Patterns
20+
//
21+
// This file uses several patterns:
22+
// - File existence validation: Agent files
23+
// - Feature compatibility checks: Engine capabilities
24+
// - Security validation: workflow_run branch restrictions
25+
// - Warning vs error: Some validations warn instead of fail
26+
//
27+
// # Security Considerations
28+
//
29+
// The validateWorkflowRunBranches function enforces security best practices:
30+
// - In strict mode: Errors when workflow_run lacks branch restrictions
31+
// - In normal mode: Warns when workflow_run lacks branch restrictions
32+
//
33+
// # When to Add Validation Here
34+
//
35+
// Add validation to this file when:
36+
// - It validates custom agent file configuration
37+
// - It checks engine feature compatibility
38+
// - It validates agent-specific requirements
39+
// - It enforces security constraints on triggers
40+
//
41+
// For general validation, see validation.go.
42+
// For detailed documentation, see specs/validation-architecture.md
43+
package workflow
44+
45+
import (
46+
"errors"
47+
"fmt"
48+
"os"
49+
"path/filepath"
50+
51+
"github.com/githubnext/gh-aw/pkg/console"
52+
"github.com/githubnext/gh-aw/pkg/logger"
53+
"github.com/goccy/go-yaml"
54+
)
55+
56+
var agentValidationLog = logger.New("workflow:agent_validation")
57+
58+
// validateAgentFile validates that the custom agent file specified in imports exists
59+
func (c *Compiler) validateAgentFile(workflowData *WorkflowData, markdownPath string) error {
60+
// Check if agent file is specified in imports
61+
if workflowData.AgentFile == "" {
62+
return nil // No agent file specified, no validation needed
63+
}
64+
65+
agentPath := workflowData.AgentFile
66+
agentValidationLog.Printf("Validating agent file exists: %s", agentPath)
67+
68+
var fullAgentPath string
69+
70+
// Check if agentPath is already absolute
71+
if filepath.IsAbs(agentPath) {
72+
// Use the path as-is (for backward compatibility with tests)
73+
fullAgentPath = agentPath
74+
} else {
75+
// Agent file path is relative to repository root (e.g., ".github/agents/file.md")
76+
// Need to resolve it relative to the markdown file's directory
77+
markdownDir := filepath.Dir(markdownPath)
78+
// Navigate up from .github/workflows to repository root
79+
repoRoot := filepath.Join(markdownDir, "..", "..")
80+
fullAgentPath = filepath.Join(repoRoot, agentPath)
81+
}
82+
83+
// Check if the file exists
84+
if _, err := os.Stat(fullAgentPath); err != nil {
85+
if os.IsNotExist(err) {
86+
formattedErr := console.FormatError(console.CompilerError{
87+
Position: console.ErrorPosition{
88+
File: markdownPath,
89+
Line: 1,
90+
Column: 1,
91+
},
92+
Type: "error",
93+
Message: fmt.Sprintf("agent file '%s' does not exist. Ensure the file exists in the repository and is properly imported.", agentPath),
94+
})
95+
return errors.New(formattedErr)
96+
}
97+
// Other error (permissions, etc.)
98+
formattedErr := console.FormatError(console.CompilerError{
99+
Position: console.ErrorPosition{
100+
File: markdownPath,
101+
Line: 1,
102+
Column: 1,
103+
},
104+
Type: "error",
105+
Message: fmt.Sprintf("failed to access agent file '%s': %v", agentPath, err),
106+
})
107+
return errors.New(formattedErr)
108+
}
109+
110+
if c.verbose {
111+
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(
112+
fmt.Sprintf("✓ Agent file exists: %s", agentPath)))
113+
}
114+
115+
return nil
116+
}
117+
118+
// validateHTTPTransportSupport validates that HTTP MCP servers are only used with engines that support HTTP transport
119+
func (c *Compiler) validateHTTPTransportSupport(tools map[string]any, engine CodingAgentEngine) error {
120+
if engine.SupportsHTTPTransport() {
121+
// Engine supports HTTP transport, no validation needed
122+
return nil
123+
}
124+
125+
// Engine doesn't support HTTP transport, check for HTTP MCP servers
126+
for toolName, toolConfig := range tools {
127+
if config, ok := toolConfig.(map[string]any); ok {
128+
if hasMcp, mcpType := hasMCPConfig(config); hasMcp && mcpType == "http" {
129+
return fmt.Errorf("tool '%s' uses HTTP transport which is not supported by engine '%s' (only stdio transport is supported)", toolName, engine.GetID())
130+
}
131+
}
132+
}
133+
134+
return nil
135+
}
136+
137+
// validateMaxTurnsSupport validates that max-turns is only used with engines that support this feature
138+
func (c *Compiler) validateMaxTurnsSupport(frontmatter map[string]any, engine CodingAgentEngine) error {
139+
// Check if max-turns is specified in the engine config
140+
engineSetting, engineConfig := c.ExtractEngineConfig(frontmatter)
141+
_ = engineSetting // Suppress unused variable warning
142+
143+
hasMaxTurns := engineConfig != nil && engineConfig.MaxTurns != ""
144+
145+
if !hasMaxTurns {
146+
// No max-turns specified, no validation needed
147+
return nil
148+
}
149+
150+
// max-turns is specified, check if the engine supports it
151+
if !engine.SupportsMaxTurns() {
152+
return fmt.Errorf("max-turns not supported: engine '%s' does not support the max-turns feature", engine.GetID())
153+
}
154+
155+
// Engine supports max-turns - additional validation could be added here if needed
156+
// For now, we rely on JSON schema validation for format checking
157+
158+
return nil
159+
}
160+
161+
// validateWebSearchSupport validates that web-search tool is only used with engines that support this feature
162+
func (c *Compiler) validateWebSearchSupport(tools map[string]any, engine CodingAgentEngine) {
163+
// Check if web-search tool is requested
164+
_, hasWebSearch := tools["web-search"]
165+
166+
if !hasWebSearch {
167+
// No web-search specified, no validation needed
168+
return
169+
}
170+
171+
// web-search is specified, check if the engine supports it
172+
if !engine.SupportsWebSearch() {
173+
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Engine '%s' does not support the web-search tool. See https://githubnext.github.io/gh-aw/guides/web-search/ for alternatives.", engine.GetID())))
174+
c.IncrementWarningCount()
175+
}
176+
}
177+
178+
// validateWorkflowRunBranches validates that workflow_run triggers include branch restrictions
179+
// This is a security best practice to avoid running on all branches
180+
func (c *Compiler) validateWorkflowRunBranches(workflowData *WorkflowData, markdownPath string) error {
181+
if workflowData.On == "" {
182+
return nil
183+
}
184+
185+
agentValidationLog.Print("Validating workflow_run triggers for branch restrictions")
186+
187+
// Parse the On field as YAML to check for workflow_run
188+
// The On field is a YAML string that starts with "on:" key
189+
var parsedData map[string]any
190+
if err := yaml.Unmarshal([]byte(workflowData.On), &parsedData); err != nil {
191+
// If we can't parse the YAML, skip this validation
192+
agentValidationLog.Printf("Could not parse On field as YAML: %v", err)
193+
return nil
194+
}
195+
196+
// Extract the actual "on" section from the parsed data
197+
onData, hasOn := parsedData["on"]
198+
if !hasOn {
199+
// No "on" key found, skip validation
200+
return nil
201+
}
202+
203+
onMap, isMap := onData.(map[string]any)
204+
if !isMap {
205+
// "on" is not a map, skip validation
206+
return nil
207+
}
208+
209+
// Check if workflow_run is present
210+
workflowRunVal, hasWorkflowRun := onMap["workflow_run"]
211+
if !hasWorkflowRun {
212+
// No workflow_run trigger, no validation needed
213+
return nil
214+
}
215+
216+
// Check if workflow_run has branches field
217+
workflowRunMap, isMap := workflowRunVal.(map[string]any)
218+
if !isMap {
219+
// workflow_run is not a map (unusual), skip validation
220+
return nil
221+
}
222+
223+
_, hasBranches := workflowRunMap["branches"]
224+
if hasBranches {
225+
// Has branch restrictions, validation passed
226+
if c.verbose {
227+
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("✓ workflow_run trigger has branch restrictions"))
228+
}
229+
return nil
230+
}
231+
232+
// workflow_run without branches - this is a warning or error depending on mode
233+
message := "workflow_run trigger should include branch restrictions for security and performance.\n\n" +
234+
"Without branch restrictions, the workflow will run for workflow runs on ALL branches,\n" +
235+
"which can cause unexpected behavior and security issues.\n\n" +
236+
"Suggested fix: Add branch restrictions to your workflow_run trigger:\n" +
237+
"on:\n" +
238+
" workflow_run:\n" +
239+
" workflows: [\"your-workflow\"]\n" +
240+
" types: [completed]\n" +
241+
" branches:\n" +
242+
" - main\n" +
243+
" - develop"
244+
245+
if c.strictMode {
246+
// In strict mode, this is an error
247+
formattedErr := console.FormatError(console.CompilerError{
248+
Position: console.ErrorPosition{
249+
File: markdownPath,
250+
Line: 1,
251+
Column: 1,
252+
},
253+
Type: "error",
254+
Message: message,
255+
})
256+
return errors.New(formattedErr)
257+
}
258+
259+
// In normal mode, this is a warning
260+
formattedWarning := console.FormatError(console.CompilerError{
261+
Position: console.ErrorPosition{
262+
File: markdownPath,
263+
Line: 1,
264+
Column: 1,
265+
},
266+
Type: "warning",
267+
Message: message,
268+
})
269+
fmt.Fprintln(os.Stderr, formattedWarning)
270+
c.IncrementWarningCount()
271+
272+
return nil
273+
}

0 commit comments

Comments
 (0)