From 6d5853bb40062fcb93287eafa99d17f2a06d5c89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Sep 2025 07:48:53 +0000 Subject: [PATCH 1/7] Initial plan From 1600b8496753a31ab3a062fe4774fd18263a1476 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Sep 2025 08:06:52 +0000 Subject: [PATCH 2/7] Implement strict mode frontmatter field for deny-by-default network permissions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/frontmatter.md | 53 ++++ pkg/cli/templates/instructions.md | 5 + pkg/parser/schema_test.go | 25 ++ pkg/parser/schemas/main_workflow_schema.json | 4 + pkg/workflow/compiler.go | 20 ++ pkg/workflow/compiler_test.go | 245 +++++++++++++++++++ pkg/workflow/engine.go | 10 + 7 files changed, 362 insertions(+) diff --git a/docs/frontmatter.md b/docs/frontmatter.md index c8601a1fff5..e71eeff659f 100644 --- a/docs/frontmatter.md +++ b/docs/frontmatter.md @@ -22,6 +22,7 @@ The YAML frontmatter supports standard GitHub Actions properties plus additional - `tools`: Available tools and MCP servers for the AI engine - `cache`: Cache configuration for workflow dependencies - `safe-outputs`: [Safe Output Processing](safe-outputs.md) for automatic issue creation and comment posting. +- `strict`: Enable strict mode to enforce deny-by-default permissions for engine and MCP servers ## Trigger Events (`on:`) @@ -283,6 +284,58 @@ engine: - "*.safe-domain.org" ``` +## Strict Mode (`strict:`) + +Strict mode enforces deny-by-default permissions for both engine and MCP servers even when no explicit permissions are configured. This provides a zero-trust security model that adheres to security best practices. + +```yaml +strict: true # Enable strict mode (default: false) +``` + +### Behavior + +When strict mode is enabled: + +1. **No explicit network permissions**: Automatically enforces deny-all policy + ```yaml + strict: true + engine: claude + # No engine.permissions.network specified + # Result: All network access is denied (same as empty allowed list) + ``` + +2. **Explicit network permissions**: Uses the specified permissions normally + ```yaml + strict: true + engine: + id: claude + permissions: + network: + allowed: ["api.github.com"] + # Result: Only api.github.com is accessible + ``` + +3. **Strict mode disabled**: Maintains backwards-compatible behavior + ```yaml + strict: false # or omitted entirely + engine: claude + # No engine.permissions.network specified + # Result: Unrestricted network access (backwards compatible) + ``` + +### Use Cases + +- **Security-first workflows**: When you want to ensure no accidental network access +- **Compliance requirements**: For environments requiring deny-by-default policies +- **Zero-trust environments**: When explicit permissions should always be required +- **Migration assistance**: Gradually migrate existing workflows to explicit permissions + +### Compatibility + +- Only applies to engines that support network permissions (currently Claude) +- Non-Claude engines ignore strict mode setting +- Backwards compatible when `strict: false` or omitted + ## Safe Outputs Configuration (`safe-outputs:`) See [Safe Outputs Processing](safe-outputs.md) for automatic issue creation, comment posting and other safe outputs. diff --git a/pkg/cli/templates/instructions.md b/pkg/cli/templates/instructions.md index 1d8cdb6e884..e6046d329b6 100644 --- a/pkg/cli/templates/instructions.md +++ b/pkg/cli/templates/instructions.md @@ -76,6 +76,11 @@ The YAML frontmatter supports these fields: - "*.trusted-domain.com" ``` +- **`strict:`** - Enable strict mode for deny-by-default permissions (boolean, default: false) + ```yaml + strict: true # Enforce deny-all network permissions when no explicit permissions set + ``` + - **`tools:`** - Tool configuration for coding agent - `github:` - GitHub API tools - `claude:` - Claude-specific tools diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index 52905021065..f94100c4e93 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -474,6 +474,31 @@ func TestValidateMainWorkflowFrontmatterWithSchema(t *testing.T) { wantErr: true, errContains: "additional properties 'invalid_prop' not allowed", }, + { + name: "valid strict mode true", + frontmatter: map[string]any{ + "on": "push", + "strict": true, + }, + wantErr: false, + }, + { + name: "valid strict mode false", + frontmatter: map[string]any{ + "on": "push", + "strict": false, + }, + wantErr: false, + }, + { + name: "invalid strict mode as string", + frontmatter: map[string]any{ + "on": "push", + "strict": "true", + }, + wantErr: true, + errContains: "want boolean", + }, { name: "valid claude engine with network permissions", frontmatter: map[string]any{ diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 206b4d1e817..c4452bb0a94 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -972,6 +972,10 @@ } ] }, + "strict": { + "type": "boolean", + "description": "Enable strict mode to enforce deny-by-default permissions for engine and MCP servers even when permissions are not explicitly set" + }, "safe-outputs": { "type": "object", "description": "Output configuration for automatic safe outputs", diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 7027550e13d..116630c3b53 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -464,6 +464,26 @@ func (c *Compiler) parseWorkflowFile(markdownPath string) (*WorkflowData, error) // Extract AI engine setting from frontmatter engineSetting, engineConfig := c.extractEngineConfig(result.Frontmatter) + // Extract strict mode setting from frontmatter + strictMode := c.extractStrictMode(result.Frontmatter) + + // Apply strict mode: inject deny-all network permissions if strict mode is enabled + // and no explicit network permissions are configured + if strictMode && engineConfig != nil && engineConfig.ID == "claude" { + if engineConfig.Permissions == nil || engineConfig.Permissions.Network == nil { + // Initialize permissions structure if needed + if engineConfig.Permissions == nil { + engineConfig.Permissions = &EnginePermissions{} + } + if engineConfig.Permissions.Network == nil { + // Inject deny-all network permissions (empty allowed list) + engineConfig.Permissions.Network = &NetworkPermissions{ + Allowed: []string{}, // Empty list means deny-all + } + } + } + } + // Override with command line AI engine setting if provided if c.engineOverride != "" { originalEngineSetting := engineSetting diff --git a/pkg/workflow/compiler_test.go b/pkg/workflow/compiler_test.go index 8dc2bd5cbf4..6341fd68099 100644 --- a/pkg/workflow/compiler_test.go +++ b/pkg/workflow/compiler_test.go @@ -2933,6 +2933,251 @@ func TestGenerateJobName(t *testing.T) { } } +func TestStrictModeNetworkPermissions(t *testing.T) { + compiler := NewCompiler(false, "", "test") + + tmpDir := t.TempDir() + + t.Run("strict mode disabled with no permissions (default behavior)", func(t *testing.T) { + testContent := `--- +on: push +engine: claude +strict: false +--- + +# Test Workflow + +This is a test workflow without network permissions. +` + testFile := filepath.Join(tmpDir, "no-strict-workflow.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + err := compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected compilation error: %v", err) + } + + // Read the compiled output + lockFile := filepath.Join(tmpDir, "no-strict-workflow.lock.yml") + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + // Should not contain network hook setup (no restrictions) + if strings.Contains(string(lockContent), "Generate Network Permissions Hook") { + t.Error("Should not contain network hook setup when strict mode is disabled and no permissions set") + } + if strings.Contains(string(lockContent), ".claude/settings.json") { + t.Error("Should not reference settings.json when strict mode is disabled and no permissions set") + } + }) + + t.Run("strict mode enabled with no explicit permissions (should enforce deny-all)", func(t *testing.T) { + testContent := `--- +on: push +engine: claude +strict: true +--- + +# Test Workflow + +This is a test workflow with strict mode but no explicit network permissions. +` + testFile := filepath.Join(tmpDir, "strict-no-perms-workflow.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + err := compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected compilation error: %v", err) + } + + // Read the compiled output + lockFile := filepath.Join(tmpDir, "strict-no-perms-workflow.lock.yml") + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + // Should contain network hook setup (deny-all enforcement) + if !strings.Contains(string(lockContent), "Generate Network Permissions Hook") { + t.Error("Should contain network hook setup when strict mode is enabled") + } + if !strings.Contains(string(lockContent), ".claude/settings.json") { + t.Error("Should reference settings.json when strict mode is enabled") + } + // Should have empty ALLOWED_DOMAINS array for deny-all + if !strings.Contains(string(lockContent), "ALLOWED_DOMAINS = []") { + t.Error("Should have empty ALLOWED_DOMAINS array for deny-all policy") + } + }) + + t.Run("strict mode enabled with explicit network permissions (should use explicit permissions)", func(t *testing.T) { + testContent := `--- +on: push +engine: + id: claude + permissions: + network: + allowed: ["example.com", "api.github.com"] +strict: true +--- + +# Test Workflow + +This is a test workflow with strict mode and explicit network permissions. +` + testFile := filepath.Join(tmpDir, "strict-with-perms-workflow.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + err := compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected compilation error: %v", err) + } + + // Read the compiled output + lockFile := filepath.Join(tmpDir, "strict-with-perms-workflow.lock.yml") + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + // Should contain network hook setup with specified domains + if !strings.Contains(string(lockContent), "Generate Network Permissions Hook") { + t.Error("Should contain network hook setup when strict mode is enabled with explicit permissions") + } + if !strings.Contains(string(lockContent), `"example.com"`) { + t.Error("Should contain example.com in allowed domains") + } + if !strings.Contains(string(lockContent), `"api.github.com"`) { + t.Error("Should contain api.github.com in allowed domains") + } + }) + + t.Run("strict mode not specified (should default to false)", func(t *testing.T) { + testContent := `--- +on: push +engine: claude +--- + +# Test Workflow + +This is a test workflow without strict mode specified. +` + testFile := filepath.Join(tmpDir, "no-strict-field-workflow.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + err := compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected compilation error: %v", err) + } + + // Read the compiled output + lockFile := filepath.Join(tmpDir, "no-strict-field-workflow.lock.yml") + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + // Should not contain network hook setup (default behavior) + if strings.Contains(string(lockContent), "Generate Network Permissions Hook") { + t.Error("Should not contain network hook setup when strict mode is not specified") + } + }) + + t.Run("strict mode with non-claude engine (should be ignored)", func(t *testing.T) { + testContent := `--- +on: push +engine: codex +strict: true +--- + +# Test Workflow + +This is a test workflow with strict mode and codex engine. +` + testFile := filepath.Join(tmpDir, "strict-codex-workflow.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + err := compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected compilation error: %v", err) + } + + // Read the compiled output + lockFile := filepath.Join(tmpDir, "strict-codex-workflow.lock.yml") + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + // Should not contain claude-specific network hook setup + if strings.Contains(string(lockContent), "Generate Network Permissions Hook") { + t.Error("Should not contain network hook setup for non-claude engines") + } + }) +} + +func TestExtractStrictMode(t *testing.T) { + compiler := NewCompiler(false, "", "test") + + tests := []struct { + name string + frontmatter map[string]any + expected bool + }{ + { + name: "strict mode true", + frontmatter: map[string]any{"strict": true}, + expected: true, + }, + { + name: "strict mode false", + frontmatter: map[string]any{"strict": false}, + expected: false, + }, + { + name: "strict mode not specified", + frontmatter: map[string]any{"on": "push"}, + expected: false, + }, + { + name: "strict mode as string (should default to false)", + frontmatter: map[string]any{"strict": "true"}, + expected: false, + }, + { + name: "empty frontmatter", + frontmatter: map[string]any{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := compiler.extractStrictMode(tt.frontmatter) + if result != tt.expected { + t.Errorf("extractStrictMode() = %v, want %v", result, tt.expected) + } + }) + } +} + func TestMCPImageField(t *testing.T) { // Create temporary directory for test files tmpDir, err := os.MkdirTemp("", "mcp-container-test") diff --git a/pkg/workflow/engine.go b/pkg/workflow/engine.go index 0feb416b553..930bb054d75 100644 --- a/pkg/workflow/engine.go +++ b/pkg/workflow/engine.go @@ -99,6 +99,16 @@ func (c *Compiler) extractEngineConfig(frontmatter map[string]any) (string, *Eng return "", nil } +// extractStrictMode extracts strict mode setting from frontmatter +func (c *Compiler) extractStrictMode(frontmatter map[string]any) bool { + if strict, exists := frontmatter["strict"]; exists { + if strictBool, ok := strict.(bool); ok { + return strictBool + } + } + return false // Default to false if not specified or not a boolean +} + // validateEngine validates that the given engine ID is supported func (c *Compiler) validateEngine(engineID string) error { if engineID == "" { From 990641efa8e85f676ca03917a0ede55004d91f88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Sep 2025 12:13:53 +0000 Subject: [PATCH 3/7] Implement strict mode permissions validation - empty default and write warnings Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler.go | 16 +++++++++++--- pkg/workflow/engine.go | 45 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 116630c3b53..5c8157b979c 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -719,7 +719,7 @@ func (c *Compiler) parseWorkflowFile(markdownPath string) (*WorkflowData, error) } // Apply defaults - c.applyDefaults(workflowData, markdownPath) + c.applyDefaults(workflowData, markdownPath, strictMode) // Apply pull request draft filter if specified c.applyPullRequestDraftFilter(workflowData, result.Frontmatter) @@ -921,7 +921,7 @@ func (c *Compiler) extractCommandName(frontmatter map[string]any) string { } // applyDefaults applies default values for missing workflow sections -func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string) { +func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string, strictMode bool) { // Check if this is a command trigger workflow (by checking if user specified "on.command") isCommandTrigger := false if data.On == "" { @@ -1019,7 +1019,17 @@ func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string) { } if data.Permissions == "" { - data.Permissions = `permissions: read-all` + if strictMode { + // In strict mode, default to empty permissions instead of read-all + // User must explicitly specify permissions in frontmatter + fmt.Println(console.FormatWarningMessage("Strict mode enabled: No permissions specified. User must explicitly define permissions in frontmatter.")) + } else { + // Default behavior: use read-all permissions + data.Permissions = `permissions: read-all` + } + } else if strictMode { + // In strict mode, validate permissions and warn about write permissions + c.validatePermissionsInStrictMode(data.Permissions) } // Generate concurrency configuration using the dedicated concurrency module diff --git a/pkg/workflow/engine.go b/pkg/workflow/engine.go index 930bb054d75..132c42953c2 100644 --- a/pkg/workflow/engine.go +++ b/pkg/workflow/engine.go @@ -1,6 +1,11 @@ package workflow -import "fmt" +import ( + "fmt" + "strings" + + "github.com/githubnext/gh-aw/pkg/console" +) // EngineConfig represents the parsed engine configuration type EngineConfig struct { @@ -109,6 +114,44 @@ func (c *Compiler) extractStrictMode(frontmatter map[string]any) bool { return false // Default to false if not specified or not a boolean } +// validatePermissionsInStrictMode checks permissions in strict mode and warns about write permissions +func (c *Compiler) validatePermissionsInStrictMode(permissions string) { + if permissions == "" { + return + } + + // Check for write permissions in the permissions string + writePermissions := []string{ + "write-all", + "write", + "contents: write", + "issues: write", + "pull-requests: write", + "actions: write", + "checks: write", + "deployments: write", + "discussions: write", + "packages: write", + "pages: write", + "repository-projects: write", + "security-events: write", + "statuses: write", + "attestations: write", + } + + hasWritePermissions := false + for _, writePattern := range writePermissions { + if strings.Contains(permissions, writePattern) { + hasWritePermissions = true + break + } + } + + if hasWritePermissions { + fmt.Println(console.FormatWarningMessage("Strict mode: Found 'write' permissions. Consider using 'read' permissions only for better security.")) + } +} + // validateEngine validates that the given engine ID is supported func (c *Compiler) validateEngine(engineID string) error { if engineID == "" { From a4b3df58709de64a34bbe665e41f106d98ac68b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Sep 2025 12:42:40 +0000 Subject: [PATCH 4/7] Inject 'permissions: {}' in strict mode for deny-all permissions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../example-engine-network-permissions.lock.yml | 3 ++- .github/workflows/issue-triage.lock.yml | 9 ++++++++- .../workflows/test-claude-add-issue-comment.lock.yml | 2 +- .../workflows/test-claude-add-issue-labels.lock.yml | 2 +- .github/workflows/test-claude-command.lock.yml | 2 +- .github/workflows/test-claude-create-issue.lock.yml | 2 +- .../workflows/test-claude-create-pull-request.lock.yml | 2 +- .github/workflows/test-claude-mcp.lock.yml | 2 +- .github/workflows/test-claude-update-issue.lock.yml | 2 +- .../workflows/test-codex-add-issue-comment.lock.yml | 2 +- .github/workflows/test-codex-add-issue-labels.lock.yml | 2 +- .github/workflows/test-codex-command.lock.yml | 2 +- .github/workflows/test-codex-create-issue.lock.yml | 2 +- .../workflows/test-codex-create-pull-request.lock.yml | 2 +- .github/workflows/test-codex-mcp.lock.yml | 2 +- .github/workflows/test-codex-update-issue.lock.yml | 2 +- .github/workflows/test-proxy.lock.yml | 2 +- .github/workflows/weekly-research.lock.yml | 10 +++++++++- pkg/workflow/compiler.go | 3 ++- 19 files changed, 36 insertions(+), 19 deletions(-) diff --git a/.github/workflows/example-engine-network-permissions.lock.yml b/.github/workflows/example-engine-network-permissions.lock.yml index 2dba6f898d2..e738db3792d 100644 --- a/.github/workflows/example-engine-network-permissions.lock.yml +++ b/.github/workflows/example-engine-network-permissions.lock.yml @@ -9,7 +9,8 @@ on: - main workflow_dispatch: null -permissions: {} +permissions: + contents: read concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index eee44b9e71f..0674351c42e 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -9,7 +9,14 @@ on: - opened - reopened -permissions: {} +permissions: + actions: read + checks: read + contents: read + issues: write + models: read + pull-requests: read + statuses: read concurrency: cancel-in-progress: true diff --git a/.github/workflows/test-claude-add-issue-comment.lock.yml b/.github/workflows/test-claude-add-issue-comment.lock.yml index 88d1eca7517..7b24afb673f 100644 --- a/.github/workflows/test-claude-add-issue-comment.lock.yml +++ b/.github/workflows/test-claude-add-issue-comment.lock.yml @@ -9,7 +9,7 @@ name: "Test Claude Add Issue Comment" - opened - reopened -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-claude-add-issue-labels.lock.yml b/.github/workflows/test-claude-add-issue-labels.lock.yml index b99dfc186b7..61772bfbd62 100644 --- a/.github/workflows/test-claude-add-issue-labels.lock.yml +++ b/.github/workflows/test-claude-add-issue-labels.lock.yml @@ -9,7 +9,7 @@ name: "Test Claude Add Issue Labels" - opened - reopened -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-claude-command.lock.yml b/.github/workflows/test-claude-command.lock.yml index c06a3c68e34..9657cb189f8 100644 --- a/.github/workflows/test-claude-command.lock.yml +++ b/.github/workflows/test-claude-command.lock.yml @@ -13,7 +13,7 @@ on: pull_request_review_comment: types: [created, edited] -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}" diff --git a/.github/workflows/test-claude-create-issue.lock.yml b/.github/workflows/test-claude-create-issue.lock.yml index da5be232e8c..458d67e0819 100644 --- a/.github/workflows/test-claude-create-issue.lock.yml +++ b/.github/workflows/test-claude-create-issue.lock.yml @@ -6,7 +6,7 @@ name: "Test Claude Create Issue" on: workflow_dispatch: null -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-claude-create-pull-request.lock.yml b/.github/workflows/test-claude-create-pull-request.lock.yml index de0a102742e..a1346f4e670 100644 --- a/.github/workflows/test-claude-create-pull-request.lock.yml +++ b/.github/workflows/test-claude-create-pull-request.lock.yml @@ -6,7 +6,7 @@ name: "Test Claude Create Pull Request" on: workflow_dispatch: null -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-claude-mcp.lock.yml b/.github/workflows/test-claude-mcp.lock.yml index 4bb49b78d5c..d97cb608473 100644 --- a/.github/workflows/test-claude-mcp.lock.yml +++ b/.github/workflows/test-claude-mcp.lock.yml @@ -6,7 +6,7 @@ name: "Test Claude Mcp" "on": workflow_dispatch: null -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-claude-update-issue.lock.yml b/.github/workflows/test-claude-update-issue.lock.yml index c6e2aa194e1..13dd5669c79 100644 --- a/.github/workflows/test-claude-update-issue.lock.yml +++ b/.github/workflows/test-claude-update-issue.lock.yml @@ -9,7 +9,7 @@ name: "Test Claude Update Issue" - opened - reopened -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-codex-add-issue-comment.lock.yml b/.github/workflows/test-codex-add-issue-comment.lock.yml index 1b187d7ebc2..0a39074804f 100644 --- a/.github/workflows/test-codex-add-issue-comment.lock.yml +++ b/.github/workflows/test-codex-add-issue-comment.lock.yml @@ -9,7 +9,7 @@ name: "Test Codex Add Issue Comment" - opened - reopened -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-codex-add-issue-labels.lock.yml b/.github/workflows/test-codex-add-issue-labels.lock.yml index 2d7655645cb..baeefa9c362 100644 --- a/.github/workflows/test-codex-add-issue-labels.lock.yml +++ b/.github/workflows/test-codex-add-issue-labels.lock.yml @@ -9,7 +9,7 @@ name: "Test Codex Add Issue Labels" - opened - reopened -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-codex-command.lock.yml b/.github/workflows/test-codex-command.lock.yml index 29d978be318..ad2ea2a077d 100644 --- a/.github/workflows/test-codex-command.lock.yml +++ b/.github/workflows/test-codex-command.lock.yml @@ -13,7 +13,7 @@ on: pull_request_review_comment: types: [created, edited] -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}" diff --git a/.github/workflows/test-codex-create-issue.lock.yml b/.github/workflows/test-codex-create-issue.lock.yml index 8cf0b2d00eb..ca2eba39fa6 100644 --- a/.github/workflows/test-codex-create-issue.lock.yml +++ b/.github/workflows/test-codex-create-issue.lock.yml @@ -6,7 +6,7 @@ name: "Test Codex Create Issue" on: workflow_dispatch: null -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-codex-create-pull-request.lock.yml b/.github/workflows/test-codex-create-pull-request.lock.yml index 1bc241ef433..97caddb2719 100644 --- a/.github/workflows/test-codex-create-pull-request.lock.yml +++ b/.github/workflows/test-codex-create-pull-request.lock.yml @@ -6,7 +6,7 @@ name: "Test Codex Create Pull Request" on: workflow_dispatch: null -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-codex-mcp.lock.yml b/.github/workflows/test-codex-mcp.lock.yml index 197e5f92ff6..4e7b4567621 100644 --- a/.github/workflows/test-codex-mcp.lock.yml +++ b/.github/workflows/test-codex-mcp.lock.yml @@ -6,7 +6,7 @@ name: "Test Codex Mcp" "on": workflow_dispatch: null -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-codex-update-issue.lock.yml b/.github/workflows/test-codex-update-issue.lock.yml index c41013ef32e..460ffa4c480 100644 --- a/.github/workflows/test-codex-update-issue.lock.yml +++ b/.github/workflows/test-codex-update-issue.lock.yml @@ -9,7 +9,7 @@ name: "Test Codex Update Issue" - opened - reopened -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-proxy.lock.yml b/.github/workflows/test-proxy.lock.yml index de585f626a9..3fc63b9e57f 100644 --- a/.github/workflows/test-proxy.lock.yml +++ b/.github/workflows/test-proxy.lock.yml @@ -9,7 +9,7 @@ on: - main workflow_dispatch: null -permissions: {} +permissions: read-all concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" diff --git a/.github/workflows/weekly-research.lock.yml b/.github/workflows/weekly-research.lock.yml index 2444c41e5f6..4b193ce3c9f 100644 --- a/.github/workflows/weekly-research.lock.yml +++ b/.github/workflows/weekly-research.lock.yml @@ -8,7 +8,15 @@ on: - cron: 0 9 * * 1 workflow_dispatch: null -permissions: {} +permissions: + actions: read + checks: read + contents: read + discussions: read + issues: write + models: read + pull-requests: read + statuses: read concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 5c8157b979c..eb9508786c3 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -1023,6 +1023,7 @@ func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string, strict // In strict mode, default to empty permissions instead of read-all // User must explicitly specify permissions in frontmatter fmt.Println(console.FormatWarningMessage("Strict mode enabled: No permissions specified. User must explicitly define permissions in frontmatter.")) + data.Permissions = `permissions: {}` } else { // Default behavior: use read-all permissions data.Permissions = `permissions: read-all` @@ -1528,7 +1529,7 @@ func (c *Compiler) generateYAML(data *WorkflowData) (string, error) { // Write basic workflow structure yaml.WriteString(fmt.Sprintf("name: \"%s\"\n", data.Name)) yaml.WriteString(data.On + "\n\n") - yaml.WriteString("permissions: {}\n\n") + yaml.WriteString(data.Permissions + "\n\n") yaml.WriteString(data.Concurrency + "\n\n") yaml.WriteString(data.RunName + "\n\n") From 0edc76ba7903475da21777969c9fe05648c7b041 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Wed, 3 Sep 2025 14:26:04 +0000 Subject: [PATCH 5/7] Refactor permissions in workflow files to use empty object --- .../example-engine-network-permissions.lock.yml | 3 +-- .github/workflows/issue-triage.lock.yml | 9 +-------- .../workflows/test-claude-add-issue-comment.lock.yml | 2 +- .../workflows/test-claude-add-issue-labels.lock.yml | 2 +- .github/workflows/test-claude-command.lock.yml | 2 +- .github/workflows/test-claude-create-issue.lock.yml | 2 +- .../workflows/test-claude-create-pull-request.lock.yml | 2 +- .github/workflows/test-claude-mcp.lock.yml | 2 +- .github/workflows/test-claude-update-issue.lock.yml | 2 +- .../workflows/test-codex-add-issue-comment.lock.yml | 2 +- .github/workflows/test-codex-add-issue-labels.lock.yml | 2 +- .github/workflows/test-codex-command.lock.yml | 2 +- .github/workflows/test-codex-create-issue.lock.yml | 2 +- .../workflows/test-codex-create-pull-request.lock.yml | 2 +- .github/workflows/test-codex-mcp.lock.yml | 2 +- .github/workflows/test-codex-update-issue.lock.yml | 2 +- .github/workflows/test-proxy.lock.yml | 2 +- .github/workflows/weekly-research.lock.yml | 10 +--------- pkg/workflow/compiler.go | 2 +- 19 files changed, 19 insertions(+), 35 deletions(-) diff --git a/.github/workflows/example-engine-network-permissions.lock.yml b/.github/workflows/example-engine-network-permissions.lock.yml index e738db3792d..2dba6f898d2 100644 --- a/.github/workflows/example-engine-network-permissions.lock.yml +++ b/.github/workflows/example-engine-network-permissions.lock.yml @@ -9,8 +9,7 @@ on: - main workflow_dispatch: null -permissions: - contents: read +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index 0674351c42e..eee44b9e71f 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -9,14 +9,7 @@ on: - opened - reopened -permissions: - actions: read - checks: read - contents: read - issues: write - models: read - pull-requests: read - statuses: read +permissions: {} concurrency: cancel-in-progress: true diff --git a/.github/workflows/test-claude-add-issue-comment.lock.yml b/.github/workflows/test-claude-add-issue-comment.lock.yml index 4dd7d396961..00895d7c714 100644 --- a/.github/workflows/test-claude-add-issue-comment.lock.yml +++ b/.github/workflows/test-claude-add-issue-comment.lock.yml @@ -9,7 +9,7 @@ name: "Test Claude Add Issue Comment" - opened - reopened -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-claude-add-issue-labels.lock.yml b/.github/workflows/test-claude-add-issue-labels.lock.yml index 3b3764fdbfa..23451b32279 100644 --- a/.github/workflows/test-claude-add-issue-labels.lock.yml +++ b/.github/workflows/test-claude-add-issue-labels.lock.yml @@ -9,7 +9,7 @@ name: "Test Claude Add Issue Labels" - opened - reopened -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-claude-command.lock.yml b/.github/workflows/test-claude-command.lock.yml index 118352b7b9e..61cb4cf19ad 100644 --- a/.github/workflows/test-claude-command.lock.yml +++ b/.github/workflows/test-claude-command.lock.yml @@ -13,7 +13,7 @@ on: pull_request_review_comment: types: [created, edited] -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}" diff --git a/.github/workflows/test-claude-create-issue.lock.yml b/.github/workflows/test-claude-create-issue.lock.yml index 28d2ff88afe..bd775ffd5db 100644 --- a/.github/workflows/test-claude-create-issue.lock.yml +++ b/.github/workflows/test-claude-create-issue.lock.yml @@ -6,7 +6,7 @@ name: "Test Claude Create Issue" on: workflow_dispatch: null -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-claude-create-pull-request.lock.yml b/.github/workflows/test-claude-create-pull-request.lock.yml index ce52a84a0b3..d3dc1d6d688 100644 --- a/.github/workflows/test-claude-create-pull-request.lock.yml +++ b/.github/workflows/test-claude-create-pull-request.lock.yml @@ -6,7 +6,7 @@ name: "Test Claude Create Pull Request" on: workflow_dispatch: null -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-claude-mcp.lock.yml b/.github/workflows/test-claude-mcp.lock.yml index 33ab4ea0428..fdc53158926 100644 --- a/.github/workflows/test-claude-mcp.lock.yml +++ b/.github/workflows/test-claude-mcp.lock.yml @@ -6,7 +6,7 @@ name: "Test Claude Mcp" "on": workflow_dispatch: null -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-claude-update-issue.lock.yml b/.github/workflows/test-claude-update-issue.lock.yml index 60d9c12b4b2..1157c275078 100644 --- a/.github/workflows/test-claude-update-issue.lock.yml +++ b/.github/workflows/test-claude-update-issue.lock.yml @@ -9,7 +9,7 @@ name: "Test Claude Update Issue" - opened - reopened -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-codex-add-issue-comment.lock.yml b/.github/workflows/test-codex-add-issue-comment.lock.yml index a2097ad3ada..da8a1482544 100644 --- a/.github/workflows/test-codex-add-issue-comment.lock.yml +++ b/.github/workflows/test-codex-add-issue-comment.lock.yml @@ -9,7 +9,7 @@ name: "Test Codex Add Issue Comment" - opened - reopened -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-codex-add-issue-labels.lock.yml b/.github/workflows/test-codex-add-issue-labels.lock.yml index 434afdd7b22..a4618034aeb 100644 --- a/.github/workflows/test-codex-add-issue-labels.lock.yml +++ b/.github/workflows/test-codex-add-issue-labels.lock.yml @@ -9,7 +9,7 @@ name: "Test Codex Add Issue Labels" - opened - reopened -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-codex-command.lock.yml b/.github/workflows/test-codex-command.lock.yml index 0c81f00d70c..6ff932c72f6 100644 --- a/.github/workflows/test-codex-command.lock.yml +++ b/.github/workflows/test-codex-command.lock.yml @@ -13,7 +13,7 @@ on: pull_request_review_comment: types: [created, edited] -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}" diff --git a/.github/workflows/test-codex-create-issue.lock.yml b/.github/workflows/test-codex-create-issue.lock.yml index 6ea3e7998e0..2570f47854b 100644 --- a/.github/workflows/test-codex-create-issue.lock.yml +++ b/.github/workflows/test-codex-create-issue.lock.yml @@ -6,7 +6,7 @@ name: "Test Codex Create Issue" on: workflow_dispatch: null -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-codex-create-pull-request.lock.yml b/.github/workflows/test-codex-create-pull-request.lock.yml index d423ab4d292..502ae2845e9 100644 --- a/.github/workflows/test-codex-create-pull-request.lock.yml +++ b/.github/workflows/test-codex-create-pull-request.lock.yml @@ -6,7 +6,7 @@ name: "Test Codex Create Pull Request" on: workflow_dispatch: null -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-codex-mcp.lock.yml b/.github/workflows/test-codex-mcp.lock.yml index f01866f1687..f5d971756df 100644 --- a/.github/workflows/test-codex-mcp.lock.yml +++ b/.github/workflows/test-codex-mcp.lock.yml @@ -6,7 +6,7 @@ name: "Test Codex Mcp" "on": workflow_dispatch: null -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/.github/workflows/test-codex-update-issue.lock.yml b/.github/workflows/test-codex-update-issue.lock.yml index 3613444dda5..9e6c76fcbeb 100644 --- a/.github/workflows/test-codex-update-issue.lock.yml +++ b/.github/workflows/test-codex-update-issue.lock.yml @@ -9,7 +9,7 @@ name: "Test Codex Update Issue" - opened - reopened -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" diff --git a/.github/workflows/test-proxy.lock.yml b/.github/workflows/test-proxy.lock.yml index 2c32c93cc51..7862e2149da 100644 --- a/.github/workflows/test-proxy.lock.yml +++ b/.github/workflows/test-proxy.lock.yml @@ -9,7 +9,7 @@ on: - main workflow_dispatch: null -permissions: read-all +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" diff --git a/.github/workflows/weekly-research.lock.yml b/.github/workflows/weekly-research.lock.yml index 4b193ce3c9f..2444c41e5f6 100644 --- a/.github/workflows/weekly-research.lock.yml +++ b/.github/workflows/weekly-research.lock.yml @@ -8,15 +8,7 @@ on: - cron: 0 9 * * 1 workflow_dispatch: null -permissions: - actions: read - checks: read - contents: read - discussions: read - issues: write - models: read - pull-requests: read - statuses: read +permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index cf177656189..4cf73f20c18 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -1609,7 +1609,7 @@ func (c *Compiler) generateYAML(data *WorkflowData) (string, error) { // Write basic workflow structure yaml.WriteString(fmt.Sprintf("name: \"%s\"\n", data.Name)) yaml.WriteString(data.On + "\n\n") - yaml.WriteString(data.Permissions + "\n\n") + yaml.WriteString("permissions: {}\n\n") yaml.WriteString(data.Concurrency + "\n\n") yaml.WriteString(data.RunName + "\n\n") From bcd4ff94baa748af68f3ea91af2d9b70922e86be Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Wed, 3 Sep 2025 14:29:05 +0000 Subject: [PATCH 6/7] Refactor permissions in test-claude-create-issue workflow and add network permissions validation script --- .../test-claude-create-issue.lock.yml | 110 +++++++++++++++++- .github/workflows/test-claude-create-issue.md | 2 +- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-claude-create-issue.lock.yml b/.github/workflows/test-claude-create-issue.lock.yml index bd775ffd5db..9c58c79dec9 100644 --- a/.github/workflows/test-claude-create-issue.lock.yml +++ b/.github/workflows/test-claude-create-issue.lock.yml @@ -16,12 +16,119 @@ run-name: "Test Claude Create Issue" jobs: test-claude-create-issue: runs-on: ubuntu-latest - permissions: read-all + permissions: {} outputs: output: ${{ steps.collect_output.outputs.output }} steps: - name: Checkout repository uses: actions/checkout@v5 + - name: Generate Network Permissions Hook + run: | + mkdir -p .claude/hooks + cat > .claude/hooks/network_permissions.py << 'EOF' + #!/usr/bin/env python3 + """ + Network permissions validator for Claude Code engine. + Generated by gh-aw from engine network permissions configuration. + """ + + import json + import sys + import urllib.parse + import re + + # Domain whitelist (populated during generation) + ALLOWED_DOMAINS = [] + + def extract_domain(url_or_query): + """Extract domain from URL or search query.""" + if not url_or_query: + return None + + if url_or_query.startswith(('http://', 'https://')): + return urllib.parse.urlparse(url_or_query).netloc.lower() + + # Check for domain patterns in search queries + match = re.search(r'site:([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})', url_or_query) + if match: + return match.group(1).lower() + + return None + + def is_domain_allowed(domain): + """Check if domain is allowed.""" + if not domain: + # If no domain detected, allow only if not under deny-all policy + return bool(ALLOWED_DOMAINS) # False if empty list (deny-all), True if has domains + + # Empty allowed domains means deny all + if not ALLOWED_DOMAINS: + return False + + for pattern in ALLOWED_DOMAINS: + regex = pattern.replace('.', r'\.').replace('*', '.*') + if re.match(f'^{regex}$', domain): + return True + return False + + # Main logic + try: + data = json.load(sys.stdin) + tool_name = data.get('tool_name', '') + tool_input = data.get('tool_input', {}) + + if tool_name not in ['WebFetch', 'WebSearch']: + sys.exit(0) # Allow other tools + + target = tool_input.get('url') or tool_input.get('query', '') + domain = extract_domain(target) + + # For WebSearch, apply domain restrictions consistently + # If no domain detected in search query, check if restrictions are in place + if tool_name == 'WebSearch' and not domain: + # Since this hook is only generated when network permissions are configured, + # empty ALLOWED_DOMAINS means deny-all policy + if not ALLOWED_DOMAINS: # Empty list means deny all + print(f"Network access blocked: deny-all policy in effect", file=sys.stderr) + print(f"No domains are allowed for WebSearch", file=sys.stderr) + sys.exit(2) # Block under deny-all policy + else: + print(f"Network access blocked for WebSearch: no specific domain detected", file=sys.stderr) + print(f"Allowed domains: {', '.join(ALLOWED_DOMAINS)}", file=sys.stderr) + sys.exit(2) # Block general searches when domain allowlist is configured + + if not is_domain_allowed(domain): + print(f"Network access blocked for domain: {domain}", file=sys.stderr) + print(f"Allowed domains: {', '.join(ALLOWED_DOMAINS)}", file=sys.stderr) + sys.exit(2) # Block with feedback to Claude + + sys.exit(0) # Allow + + except Exception as e: + print(f"Network validation error: {e}", file=sys.stderr) + sys.exit(2) # Block on errors + + EOF + chmod +x .claude/hooks/network_permissions.py + - name: Generate Claude Settings + run: | + cat > .claude/settings.json << 'EOF' + { + "hooks": { + "PreToolUse": [ + { + "matcher": "WebFetch|WebSearch", + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/network_permissions.py" + } + ] + } + ] + } + } + EOF - name: Setup agent output id: setup_agent_output uses: actions/github-script@v7 @@ -222,6 +329,7 @@ jobs: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} mcp_config: /tmp/mcp-config/mcp-servers.json prompt_file: /tmp/aw-prompts/prompt.txt + settings: .claude/settings.json timeout_minutes: 5 env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} diff --git a/.github/workflows/test-claude-create-issue.md b/.github/workflows/test-claude-create-issue.md index 629e4c9be17..c15a7c12921 100644 --- a/.github/workflows/test-claude-create-issue.md +++ b/.github/workflows/test-claude-create-issue.md @@ -4,7 +4,7 @@ on: engine: id: claude - +strict: true safe-outputs: create-issue: title-prefix: "[claude-test] " From 2146c850401858d1e75473f131b6f9d3563e86f8 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Wed, 3 Sep 2025 14:39:00 +0000 Subject: [PATCH 7/7] Refactor strict mode handling by extracting related functions into a new file and removing redundant code --- pkg/workflow/compiler.go | 2 -- pkg/workflow/engine.go | 51 ---------------------------------------- pkg/workflow/strict.go | 29 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 53 deletions(-) create mode 100644 pkg/workflow/strict.go diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 4cf73f20c18..2343a5a5a63 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -1024,8 +1024,6 @@ func (c *Compiler) applyDefaults(data *WorkflowData, markdownPath string, strict if data.Permissions == "" { if strictMode { // In strict mode, default to empty permissions instead of read-all - // User must explicitly specify permissions in frontmatter - fmt.Println(console.FormatWarningMessage("Strict mode enabled: No permissions specified. User must explicitly define permissions in frontmatter.")) data.Permissions = `permissions: {}` } else { // Default behavior: use read-all permissions diff --git a/pkg/workflow/engine.go b/pkg/workflow/engine.go index 132c42953c2..d1f62d09660 100644 --- a/pkg/workflow/engine.go +++ b/pkg/workflow/engine.go @@ -2,9 +2,6 @@ package workflow import ( "fmt" - "strings" - - "github.com/githubnext/gh-aw/pkg/console" ) // EngineConfig represents the parsed engine configuration @@ -104,54 +101,6 @@ func (c *Compiler) extractEngineConfig(frontmatter map[string]any) (string, *Eng return "", nil } -// extractStrictMode extracts strict mode setting from frontmatter -func (c *Compiler) extractStrictMode(frontmatter map[string]any) bool { - if strict, exists := frontmatter["strict"]; exists { - if strictBool, ok := strict.(bool); ok { - return strictBool - } - } - return false // Default to false if not specified or not a boolean -} - -// validatePermissionsInStrictMode checks permissions in strict mode and warns about write permissions -func (c *Compiler) validatePermissionsInStrictMode(permissions string) { - if permissions == "" { - return - } - - // Check for write permissions in the permissions string - writePermissions := []string{ - "write-all", - "write", - "contents: write", - "issues: write", - "pull-requests: write", - "actions: write", - "checks: write", - "deployments: write", - "discussions: write", - "packages: write", - "pages: write", - "repository-projects: write", - "security-events: write", - "statuses: write", - "attestations: write", - } - - hasWritePermissions := false - for _, writePattern := range writePermissions { - if strings.Contains(permissions, writePattern) { - hasWritePermissions = true - break - } - } - - if hasWritePermissions { - fmt.Println(console.FormatWarningMessage("Strict mode: Found 'write' permissions. Consider using 'read' permissions only for better security.")) - } -} - // validateEngine validates that the given engine ID is supported func (c *Compiler) validateEngine(engineID string) error { if engineID == "" { diff --git a/pkg/workflow/strict.go b/pkg/workflow/strict.go new file mode 100644 index 00000000000..0e30ef20410 --- /dev/null +++ b/pkg/workflow/strict.go @@ -0,0 +1,29 @@ +package workflow + +import ( + "fmt" + "strings" + + "github.com/githubnext/gh-aw/pkg/console" +) + +// extractStrictMode extracts strict mode setting from frontmatter +func (c *Compiler) extractStrictMode(frontmatter map[string]any) bool { + if strict, exists := frontmatter["strict"]; exists { + if strictBool, ok := strict.(bool); ok { + return strictBool + } + } + return false // Default to false if not specified or not a boolean +} + +// validatePermissionsInStrictMode checks permissions in strict mode and warns about write permissions +func (c *Compiler) validatePermissionsInStrictMode(permissions string) { + if permissions == "" { + return + } + hasWritePermissions := strings.Contains(permissions, "write") + if hasWritePermissions { + fmt.Println(console.FormatWarningMessage("Strict mode: Found 'write' permissions. Consider using 'read' permissions only for better security.")) + } +}