Skip to content

Commit b962430

Browse files
committed
feat: Add engine-specific validation for workflow frontmatter and network permissions
Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com>
1 parent 16e5c8b commit b962430

4 files changed

Lines changed: 130 additions & 2 deletions

File tree

pkg/parser/schema.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,24 @@ var mcpConfigSchema string
2323

2424
// ValidateMainWorkflowFrontmatterWithSchema validates main workflow frontmatter using JSON schema
2525
func ValidateMainWorkflowFrontmatterWithSchema(frontmatter map[string]any) error {
26-
return validateWithSchema(frontmatter, mainWorkflowSchema, "main workflow file")
26+
// First run the standard schema validation
27+
if err := validateWithSchema(frontmatter, mainWorkflowSchema, "main workflow file"); err != nil {
28+
return err
29+
}
30+
31+
// Then run custom validation for engine-specific rules
32+
return validateEngineSpecificRules(frontmatter)
2733
}
2834

2935
// ValidateMainWorkflowFrontmatterWithSchemaAndLocation validates main workflow frontmatter with file location info
3036
func ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter map[string]any, filePath string) error {
31-
return validateWithSchemaAndLocation(frontmatter, mainWorkflowSchema, "main workflow file", filePath)
37+
// First run the standard schema validation with location
38+
if err := validateWithSchemaAndLocation(frontmatter, mainWorkflowSchema, "main workflow file", filePath); err != nil {
39+
return err
40+
}
41+
42+
// Then run custom validation for engine-specific rules
43+
return validateEngineSpecificRules(frontmatter)
3244
}
3345

3446
// ValidateIncludedFileFrontmatterWithSchema validates included file frontmatter using JSON schema
@@ -211,3 +223,40 @@ func min(a, b int) int {
211223
}
212224
return b
213225
}
226+
227+
// validateEngineSpecificRules validates engine-specific rules that are not easily expressed in JSON schema
228+
func validateEngineSpecificRules(frontmatter map[string]any) error {
229+
// Check if engine is configured
230+
engine, ok := frontmatter["engine"]
231+
if !ok {
232+
return nil // No engine specified, nothing to validate
233+
}
234+
235+
// Handle string format engine
236+
if engineStr, ok := engine.(string); ok {
237+
// String format doesn't support permissions, so no validation needed
238+
_ = engineStr
239+
return nil
240+
}
241+
242+
// Handle object format engine
243+
engineMap, ok := engine.(map[string]any)
244+
if !ok {
245+
return nil // Invalid engine format, but this should be caught by schema validation
246+
}
247+
248+
// Check engine ID
249+
engineID, ok := engineMap["id"].(string)
250+
if !ok {
251+
return nil // Missing or invalid ID, but this should be caught by schema validation
252+
}
253+
254+
// Check if codex engine has permissions configured
255+
if engineID == "codex" {
256+
if _, hasPermissions := engineMap["permissions"]; hasPermissions {
257+
return errors.New("Engine permissions are not supported for codex engine. Only Claude engine supports permissions configuration.")
258+
}
259+
}
260+
261+
return nil
262+
}

pkg/parser/schema_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,56 @@ func TestValidateMainWorkflowFrontmatterWithSchema(t *testing.T) {
474474
wantErr: true,
475475
errContains: "additional properties 'invalid_prop' not allowed",
476476
},
477+
{
478+
name: "valid claude engine with network permissions",
479+
frontmatter: map[string]any{
480+
"on": "push",
481+
"engine": map[string]any{
482+
"id": "claude",
483+
"permissions": map[string]any{
484+
"network": map[string]any{
485+
"allowed": []string{"example.com", "*.trusted.com"},
486+
},
487+
},
488+
},
489+
},
490+
wantErr: false,
491+
},
492+
{
493+
name: "invalid codex engine with permissions",
494+
frontmatter: map[string]any{
495+
"on": "push",
496+
"engine": map[string]any{
497+
"id": "codex",
498+
"permissions": map[string]any{
499+
"network": map[string]any{
500+
"allowed": []string{"example.com"},
501+
},
502+
},
503+
},
504+
},
505+
wantErr: true,
506+
errContains: "Engine permissions are not supported for codex engine",
507+
},
508+
{
509+
name: "valid codex engine without permissions",
510+
frontmatter: map[string]any{
511+
"on": "push",
512+
"engine": map[string]any{
513+
"id": "codex",
514+
"model": "gpt-4o",
515+
},
516+
},
517+
wantErr: false,
518+
},
519+
{
520+
name: "valid codex string engine (no permissions possible)",
521+
frontmatter: map[string]any{
522+
"on": "push",
523+
"engine": "codex",
524+
},
525+
wantErr: false,
526+
},
477527
}
478528

479529
for _, tt := range tests {

pkg/workflow/engine_network_hooks.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,10 @@ chmod +x .claude/hooks/network_permissions.py`, hookScript)
115115
}
116116

117117
// HasNetworkPermissions checks if the engine config has network permissions configured
118+
// and if the engine is Claude (network permissions are only supported for Claude engine)
118119
func HasNetworkPermissions(engineConfig *EngineConfig) bool {
119120
return engineConfig != nil &&
121+
engineConfig.ID == "claude" &&
120122
engineConfig.Permissions != nil &&
121123
engineConfig.Permissions.Network != nil &&
122124
len(engineConfig.Permissions.Network.Allowed) > 0

pkg/workflow/engine_network_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,19 @@ func TestNetworkPermissionsHelpers(t *testing.T) {
150150
if !HasNetworkPermissions(config) {
151151
t.Error("Config with network permissions should have network permissions")
152152
}
153+
154+
// Test non-Claude engine with network permissions (should be false)
155+
nonClaudeConfig := &EngineConfig{
156+
ID: "codex",
157+
Permissions: &EnginePermissions{
158+
Network: &NetworkPermissions{
159+
Allowed: []string{"example.com"},
160+
},
161+
},
162+
}
163+
if HasNetworkPermissions(nonClaudeConfig) {
164+
t.Error("Non-Claude engine should not have network permissions even if configured")
165+
}
153166
})
154167

155168
t.Run("GetAllowedDomains", func(t *testing.T) {
@@ -185,6 +198,20 @@ func TestNetworkPermissionsHelpers(t *testing.T) {
185198
if domains[2] != "api.service.org" {
186199
t.Errorf("Expected third domain to be 'api.service.org', got '%s'", domains[2])
187200
}
201+
202+
// Test non-Claude engine with network permissions (should return empty)
203+
nonClaudeConfig := &EngineConfig{
204+
ID: "codex",
205+
Permissions: &EnginePermissions{
206+
Network: &NetworkPermissions{
207+
Allowed: []string{"example.com", "test.org"},
208+
},
209+
},
210+
}
211+
domains = GetAllowedDomains(nonClaudeConfig)
212+
if len(domains) != 0 {
213+
t.Error("Non-Claude engine should return empty domains even if configured")
214+
}
188215
})
189216
}
190217

0 commit comments

Comments
 (0)