From 88cc58bbf8d130ae7eb550fe37c5e9b6340d6a55 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:39:29 +0000 Subject: [PATCH 1/7] Initial plan From 0df91d144b44b5055c7028e5be3f357cf7d429be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:12:11 +0000 Subject: [PATCH 2/7] Implement bash timeout support for neutral tools - Part 1 - Add timeout option to bash tool configuration schema - Add SupportsBashTimeout() capability to engine interface - Implement timeout configuration parsing in claude_engine.go - Add validation to fail compilation if agent doesn't support timeout - Add comprehensive tests for timeout functionality - Update engine capabilities: Claude supports timeout, Codex/Custom do not Still debugging why timeout env vars don't appear in generated workflow. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 35 ++ pkg/workflow/agentic_engine.go | 8 + pkg/workflow/bash_timeout_test.go | 385 +++++++++++++++++++ pkg/workflow/claude_engine.go | 95 ++++- pkg/workflow/codex_engine.go | 1 + pkg/workflow/compiler.go | 24 ++ pkg/workflow/custom_engine.go | 3 +- 7 files changed, 547 insertions(+), 4 deletions(-) create mode 100644 pkg/workflow/bash_timeout_test.go diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 90078304fe8..c504961294c 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -998,6 +998,41 @@ "items": { "type": "string" } + }, + { + "type": "object", + "description": "Bash tool configuration with timeout and optional commands", + "properties": { + "timeout": { + "description": "Timeout for bash commands in seconds", + "oneOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "string", + "pattern": "^[0-9]+(\\.\\d+)?$" + } + ] + }, + "commands": { + "description": "List of allowed bash commands", + "type": "array", + "items": { + "type": "string" + } + }, + "allowed": { + "description": "List of allowed bash commands (alias for commands)", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": ["timeout"] } ] }, diff --git a/pkg/workflow/agentic_engine.go b/pkg/workflow/agentic_engine.go index 210f6fa01d5..3d4fb6a9bef 100644 --- a/pkg/workflow/agentic_engine.go +++ b/pkg/workflow/agentic_engine.go @@ -35,6 +35,9 @@ type CodingAgentEngine interface { // SupportsMaxTurns returns true if this engine supports the max-turns feature SupportsMaxTurns() bool + // SupportsBashTimeout returns true if this engine supports bash timeout configuration + SupportsBashTimeout() bool + // GetDeclaredOutputFiles returns a list of output files that this engine may produce // These files will be automatically uploaded as artifacts if they exist GetDeclaredOutputFiles() []string @@ -64,6 +67,7 @@ type BaseEngine struct { supportsToolsWhitelist bool supportsHTTPTransport bool supportsMaxTurns bool + supportsBashTimeout bool } func (e *BaseEngine) GetID() string { @@ -94,6 +98,10 @@ func (e *BaseEngine) SupportsMaxTurns() bool { return e.supportsMaxTurns } +func (e *BaseEngine) SupportsBashTimeout() bool { + return e.supportsBashTimeout +} + // GetDeclaredOutputFiles returns an empty list by default (engines can override) func (e *BaseEngine) GetDeclaredOutputFiles() []string { return []string{} diff --git a/pkg/workflow/bash_timeout_test.go b/pkg/workflow/bash_timeout_test.go new file mode 100644 index 00000000000..100d61b1c1e --- /dev/null +++ b/pkg/workflow/bash_timeout_test.go @@ -0,0 +1,385 @@ +package workflow + +import ( + "testing" +) + +func TestBashToolTimeout(t *testing.T) { + engine := NewClaudeEngine() + + tests := []struct { + name string + tools map[string]any + expected string + }{ + { + name: "bash with timeout only", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": 30, + }, + }, + expected: "Bash,BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite", + }, + { + name: "bash with timeout and commands", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": 60, + "commands": []any{"echo", "ls"}, + }, + }, + expected: "Bash(echo),Bash(ls),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite", + }, + { + name: "bash with timeout and allowed field for commands", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": 45, + "allowed": []any{"git", "npm"}, + }, + }, + expected: "Bash(git),Bash(npm),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite", + }, + { + name: "bash with commands field should override allowed field", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": 30, + "commands": []any{"echo"}, + "allowed": []any{"git"}, + }, + }, + expected: "Bash(echo),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite", + }, + { + name: "bash with string timeout should work", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": "90", + }, + }, + expected: "Bash,BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := engine.computeAllowedClaudeToolsString(tt.tools, nil) + if result != tt.expected { + t.Errorf("Expected %s, got %s", tt.expected, result) + } + }) + } +} + +func TestExpandNeutralToolsWithBashTimeout(t *testing.T) { + engine := NewClaudeEngine() + + tests := []struct { + name string + input map[string]any + expected map[string]any + }{ + { + name: "bash tool with timeout only", + input: map[string]any{ + "bash": map[string]any{ + "timeout": 30, + }, + }, + expected: map[string]any{ + "claude": map[string]any{ + "allowed": map[string]any{ + "Bash": nil, // All commands allowed when no commands specified + }, + "timeout": map[string]any{ + "bash": 30, + }, + }, + }, + }, + { + name: "bash tool with timeout and commands", + input: map[string]any{ + "bash": map[string]any{ + "timeout": 60, + "commands": []any{"echo", "ls"}, + }, + }, + expected: map[string]any{ + "claude": map[string]any{ + "allowed": map[string]any{ + "Bash": []any{"echo", "ls"}, + }, + "timeout": map[string]any{ + "bash": 60, + }, + }, + }, + }, + { + name: "mixed tools with bash timeout", + input: map[string]any{ + "bash": map[string]any{ + "timeout": 45, + "allowed": []any{"git"}, + }, + "web-fetch": nil, + }, + expected: map[string]any{ + "claude": map[string]any{ + "allowed": map[string]any{ + "Bash": []any{"git"}, + "WebFetch": nil, + }, + "timeout": map[string]any{ + "bash": 45, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := engine.expandNeutralToolsToClaudeTools(tt.input) + + // Check claude section + claudeResult, hasClaudeResult := result["claude"] + claudeExpected, hasClaudeExpected := tt.expected["claude"] + + if hasClaudeExpected != hasClaudeResult { + t.Errorf("Claude section presence mismatch. Expected: %v, Got: %v", hasClaudeExpected, hasClaudeResult) + return + } + + if hasClaudeExpected { + claudeResultMap, ok1 := claudeResult.(map[string]any) + claudeExpectedMap, ok2 := claudeExpected.(map[string]any) + + if !ok1 || !ok2 { + t.Errorf("Claude section type mismatch") + return + } + + // Check allowed section + _, hasAllowedResult := claudeResultMap["allowed"] + _, hasAllowedExpected := claudeExpectedMap["allowed"] + + if hasAllowedExpected != hasAllowedResult { + t.Errorf("Claude allowed section presence mismatch. Expected: %v, Got: %v", hasAllowedExpected, hasAllowedResult) + return + } + + // Check timeout section + timeoutResult, hasTimeoutResult := claudeResultMap["timeout"] + timeoutExpected, hasTimeoutExpected := claudeExpectedMap["timeout"] + + if hasTimeoutExpected != hasTimeoutResult { + t.Errorf("Claude timeout section presence mismatch. Expected: %v, Got: %v", hasTimeoutExpected, hasTimeoutResult) + return + } + + if hasTimeoutExpected { + timeoutResultMap, ok1 := timeoutResult.(map[string]any) + timeoutExpectedMap, ok2 := timeoutExpected.(map[string]any) + + if !ok1 || !ok2 { + t.Errorf("Claude timeout section type mismatch") + return + } + + // Check bash timeout value + bashTimeoutResult, hasBashTimeoutResult := timeoutResultMap["bash"] + bashTimeoutExpected, hasBashTimeoutExpected := timeoutExpectedMap["bash"] + + if hasBashTimeoutExpected != hasBashTimeoutResult { + t.Errorf("Bash timeout presence mismatch. Expected: %v, Got: %v", hasBashTimeoutExpected, hasBashTimeoutResult) + return + } + + if hasBashTimeoutExpected && bashTimeoutResult != bashTimeoutExpected { + t.Errorf("Bash timeout value mismatch. Expected: %v, Got: %v", bashTimeoutExpected, bashTimeoutResult) + } + } + } + }) + } +} + +func TestExtractBashTimeoutEnvVars(t *testing.T) { + engine := NewClaudeEngine() + + tests := []struct { + name string + tools map[string]any + expected map[string]string + }{ + { + name: "no tools", + tools: nil, + expected: map[string]string{}, + }, + { + name: "bash tool without timeout", + tools: map[string]any{ + "bash": []any{"echo"}, + }, + expected: map[string]string{}, + }, + { + name: "bash tool with integer timeout", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": 30, + }, + }, + expected: map[string]string{ + "BASH_DEFAULT_TIMEOUT_MS": "30000", + "BASH_MAX_TIMEOUT_MS": "30000", + }, + }, + { + name: "bash tool with float timeout", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": 45.5, + }, + }, + expected: map[string]string{ + "BASH_DEFAULT_TIMEOUT_MS": "45500", + "BASH_MAX_TIMEOUT_MS": "45500", + }, + }, + { + name: "bash tool with string timeout", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": "60", + }, + }, + expected: map[string]string{ + "BASH_DEFAULT_TIMEOUT_MS": "60000", + "BASH_MAX_TIMEOUT_MS": "60000", + }, + }, + { + name: "bash tool with pre-millisecond timeout string", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": "120000", + }, + }, + expected: map[string]string{ + "BASH_DEFAULT_TIMEOUT_MS": "120000000", + "BASH_MAX_TIMEOUT_MS": "120000000", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := engine.extractBashTimeoutEnvVars(tt.tools) + + if len(result) != len(tt.expected) { + t.Errorf("Expected %d env vars, got %d. Expected: %+v, Got: %+v", len(tt.expected), len(result), tt.expected, result) + return + } + + for expectedKey, expectedValue := range tt.expected { + if actualValue, exists := result[expectedKey]; !exists { + t.Errorf("Expected env var %s not found", expectedKey) + } else if actualValue != expectedValue { + t.Errorf("Expected %s=%s, got %s=%s", expectedKey, expectedValue, expectedKey, actualValue) + } + } + }) + } +} + +func TestValidateBashTimeoutSupport(t *testing.T) { +tests := []struct { +name string +engineID string +tools map[string]any +expectError bool +errorMsg string +}{ +{ +name: "claude engine with bash timeout should pass", +engineID: "claude", +tools: map[string]any{ +"bash": map[string]any{ +"timeout": 30, +}, +}, +expectError: false, +}, +{ +name: "codex engine without bash timeout should pass", +engineID: "codex", +tools: map[string]any{ +"bash": []any{"echo"}, +}, +expectError: false, +}, +{ +name: "codex engine with bash timeout should fail", +engineID: "codex", +tools: map[string]any{ +"bash": map[string]any{ +"timeout": 30, +}, +}, +expectError: true, +errorMsg: "bash tool timeout configuration is not supported by engine 'codex'", +}, +{ +name: "custom engine with bash timeout should fail", +engineID: "custom", +tools: map[string]any{ +"bash": map[string]any{ +"timeout": 45, +"commands": []any{"echo"}, +}, +}, +expectError: true, +errorMsg: "bash tool timeout configuration is not supported by engine 'custom'", +}, +{ +name: "no bash tool should pass for any engine", +engineID: "codex", +tools: map[string]any{ +"web-fetch": nil, +}, +expectError: false, +}, +} + +compiler := NewCompiler(false, "", "test") +engines := NewEngineRegistry() + +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +engine, err := engines.GetEngine(tt.engineID) +if err != nil { +t.Fatalf("Engine %s not found: %v", tt.engineID, err) +} + +err = compiler.validateBashTimeoutSupport(tt.tools, engine) + +if tt.expectError { +if err == nil { +t.Errorf("Expected error but got none") +} else if err.Error() != tt.errorMsg { +t.Errorf("Expected error message \"%s\", got \"%s\"", tt.errorMsg, err.Error()) +} +} else { +if err != nil { +t.Errorf("Expected no error but got: %v", err) +} +} +}) +} +} diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index b00d349b04d..72d06773a0b 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -5,6 +5,7 @@ import ( "fmt" "slices" "sort" + "strconv" "strings" "time" ) @@ -27,8 +28,9 @@ func NewClaudeEngine() *ClaudeEngine { description: "Uses Claude Code with full MCP tool support and allow-listing", experimental: false, supportsToolsWhitelist: true, - supportsHTTPTransport: true, // Claude supports both stdio and HTTP transport - supportsMaxTurns: true, // Claude supports max-turns feature + supportsHTTPTransport: true, // Claude supports both stdio and HTTP transport + supportsMaxTurns: true, // Claude supports max-turns feature + supportsBashTimeout: true, // Claude supports bash timeout configuration }, } } @@ -100,6 +102,15 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str } } + // Add bash timeout environment variables from tools configuration + bashTimeoutEnvVars := e.extractBashTimeoutEnvVars(workflowData.Tools) + for key, value := range bashTimeoutEnvVars { + if claudeEnv != "" { + claudeEnv += "\n" + } + claudeEnv += " " + key + ": " + value + } + inputs := map[string]string{ "prompt_file": "/tmp/aw-prompts/prompt.txt", "anthropic_api_key": "${{ secrets.ANTHROPIC_API_KEY }}", @@ -268,12 +279,44 @@ func (e *ClaudeEngine) expandNeutralToolsToClaudeTools(tools map[string]any) map claudeAllowed = make(map[string]any) } + // Get existing timeout section from Claude section + var claudeTimeout map[string]any + if timeout, hasTimeout := claudeSection["timeout"]; hasTimeout { + if timeoutMap, ok := timeout.(map[string]any); ok { + claudeTimeout = timeoutMap + } else { + claudeTimeout = make(map[string]any) + } + } else { + claudeTimeout = make(map[string]any) + } + // Convert neutral tools to Claude tools if bashTool, hasBash := tools["bash"]; hasBash { // bash -> Bash, KillBash, BashOutput - if bashCommands, ok := bashTool.([]any); ok { + if bashConfig, ok := bashTool.(map[string]any); ok { + // Handle object format with timeout and commands + var commands any = nil + + // Check for commands in "commands" field (preferred) + if cmdArray, hasCommands := bashConfig["commands"]; hasCommands { + commands = cmdArray + } else if allowedArray, hasAllowed := bashConfig["allowed"]; hasAllowed { + // Fallback to "allowed" field for backward compatibility + commands = allowedArray + } + + claudeAllowed["Bash"] = commands + + // Extract timeout if present + if timeout, hasTimeout := bashConfig["timeout"]; hasTimeout { + claudeTimeout["bash"] = timeout + } + } else if bashCommands, ok := bashTool.([]any); ok { + // Handle array format (existing behavior) claudeAllowed["Bash"] = bashCommands } else { + // Handle nil or other format (existing behavior) claudeAllowed["Bash"] = nil // Allow all bash commands } } @@ -302,11 +345,57 @@ func (e *ClaudeEngine) expandNeutralToolsToClaudeTools(tools map[string]any) map // Update claude section claudeSection["allowed"] = claudeAllowed + + // Only add timeout section if we have timeout configurations + if len(claudeTimeout) > 0 { + claudeSection["timeout"] = claudeTimeout + } + result["claude"] = claudeSection return result } +// extractBashTimeoutEnvVars extracts bash timeout configuration and returns environment variables for the Claude agent +func (e *ClaudeEngine) extractBashTimeoutEnvVars(tools map[string]any) map[string]string { + envVars := make(map[string]string) + + if tools == nil { + return envVars + } + + if bashTool, hasBash := tools["bash"]; hasBash { + if bashConfig, ok := bashTool.(map[string]any); ok { + if timeout, hasTimeout := bashConfig["timeout"]; hasTimeout { + // Convert timeout to milliseconds for BASH_DEFAULT_TIMEOUT_MS + timeoutMs := "" + switch t := timeout.(type) { + case int: + timeoutMs = fmt.Sprintf("%d", t*1000) + case float64: + timeoutMs = fmt.Sprintf("%.0f", t*1000) + case string: + // Try to parse as number + if timeoutVal, err := strconv.Atoi(t); err == nil { + timeoutMs = fmt.Sprintf("%d", timeoutVal*1000) + } else { + // If parsing fails, assume it's already in the correct format + timeoutMs = t + } + } + + if timeoutMs != "" { + // Set the bash timeout environment variables + envVars["BASH_DEFAULT_TIMEOUT_MS"] = timeoutMs + envVars["BASH_MAX_TIMEOUT_MS"] = timeoutMs // Use the same value for max timeout + } + } + } + } + + return envVars +} + // computeAllowedClaudeToolsString // 1. validates that only neutral tools are provided (no claude section) // 2. converts neutral tools to Claude-specific tools format diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index e4da6afe816..f139516b56b 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -24,6 +24,7 @@ func NewCodexEngine() *CodexEngine { supportsToolsWhitelist: true, supportsHTTPTransport: false, // Codex only supports stdio transport supportsMaxTurns: false, // Codex does not support max-turns feature + supportsBashTimeout: false, // Codex does not support bash timeout configuration }, } } diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 55b560de8f4..381aa8ed77c 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -581,6 +581,11 @@ func (c *Compiler) parseWorkflowFile(markdownPath string) (*WorkflowData, error) return nil, fmt.Errorf("HTTP transport not supported: %w", err) } + // Validate bash timeout support for the current engine + if err := c.validateBashTimeoutSupport(tools, agenticEngine); err != nil { + return nil, fmt.Errorf("bash timeout not supported: %w", err) + } + if !agenticEngine.SupportsToolsWhitelist() { // For engines that don't support tool whitelists (like codex), ignore tools section and provide warnings fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Using experimental %s support (engine: %s)", agenticEngine.GetDisplayName(), engineSetting))) @@ -4139,6 +4144,25 @@ func (c *Compiler) validateHTTPTransportSupport(tools map[string]any, engine Cod return nil } +// validateBashTimeoutSupport validates that bash timeout configuration is only used with engines that support it +func (c *Compiler) validateBashTimeoutSupport(tools map[string]any, engine CodingAgentEngine) error { + if engine.SupportsBashTimeout() { + // Engine supports bash timeout, no validation needed + return nil + } + + // Engine doesn't support bash timeout, check for bash timeout configuration + if bashTool, hasBash := tools["bash"]; hasBash { + if bashConfig, ok := bashTool.(map[string]any); ok { + if _, hasTimeout := bashConfig["timeout"]; hasTimeout { + return fmt.Errorf("bash tool timeout configuration is not supported by engine '%s'", engine.GetID()) + } + } + } + + return nil +} + // validateMaxTurnsSupport validates that max-turns is only used with engines that support this feature func (c *Compiler) validateMaxTurnsSupport(frontmatter map[string]any, engine CodingAgentEngine) error { // Check if max-turns is specified in the engine config diff --git a/pkg/workflow/custom_engine.go b/pkg/workflow/custom_engine.go index 96979067443..5d73daab811 100644 --- a/pkg/workflow/custom_engine.go +++ b/pkg/workflow/custom_engine.go @@ -20,7 +20,8 @@ func NewCustomEngine() *CustomEngine { experimental: false, supportsToolsWhitelist: false, supportsHTTPTransport: false, - supportsMaxTurns: true, // Custom engine supports max-turns for consistency + supportsMaxTurns: true, // Custom engine supports max-turns for consistency + supportsBashTimeout: false, // Custom engine does not support bash timeout configuration }, } } From 09203b57e83ddaedc2bdd68e1b8ece547edb2487 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:15:12 +0000 Subject: [PATCH 3/7] Changes before error encountered Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/claude_engine.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index 72d06773a0b..18223419abe 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -104,6 +104,8 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str // Add bash timeout environment variables from tools configuration bashTimeoutEnvVars := e.extractBashTimeoutEnvVars(workflowData.Tools) + fmt.Printf("DEBUG: workflowData.Tools: %+v\n", workflowData.Tools) + fmt.Printf("DEBUG: bashTimeoutEnvVars: %+v\n", bashTimeoutEnvVars) for key, value := range bashTimeoutEnvVars { if claudeEnv != "" { claudeEnv += "\n" @@ -361,12 +363,16 @@ func (e *ClaudeEngine) extractBashTimeoutEnvVars(tools map[string]any) map[strin envVars := make(map[string]string) if tools == nil { + fmt.Printf("DEBUG: tools is nil\n") return envVars } if bashTool, hasBash := tools["bash"]; hasBash { + fmt.Printf("DEBUG: Found bash tool: %+v (type: %T)\n", bashTool, bashTool) if bashConfig, ok := bashTool.(map[string]any); ok { + fmt.Printf("DEBUG: bash tool is map: %+v\n", bashConfig) if timeout, hasTimeout := bashConfig["timeout"]; hasTimeout { + fmt.Printf("DEBUG: Found timeout: %+v (type: %T)\n", timeout, timeout) // Convert timeout to milliseconds for BASH_DEFAULT_TIMEOUT_MS timeoutMs := "" switch t := timeout.(type) { @@ -385,12 +391,19 @@ func (e *ClaudeEngine) extractBashTimeoutEnvVars(tools map[string]any) map[strin } if timeoutMs != "" { + fmt.Printf("DEBUG: Setting timeout env vars: %s\n", timeoutMs) // Set the bash timeout environment variables envVars["BASH_DEFAULT_TIMEOUT_MS"] = timeoutMs envVars["BASH_MAX_TIMEOUT_MS"] = timeoutMs // Use the same value for max timeout } + } else { + fmt.Printf("DEBUG: No timeout in bash config\n") } + } else { + fmt.Printf("DEBUG: bash tool is not a map\n") } + } else { + fmt.Printf("DEBUG: No bash tool found\n") } return envVars From f2227a2be1dd0c3adcf24171df89780d96d01a17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 21:32:20 +0000 Subject: [PATCH 4/7] Remove debug statements from Go code and JavaScript workflows - Remove multiple fmt.Printf("DEBUG: ...") statements from claude_engine.go - Remove console.log debug statement from collect_ndjson_output.cjs - Recompile all workflow .lock.yml files to apply changes - Fix linting issue with variable declaration - Ensure all tests pass and validation succeeds Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../test-ai-inference-github-models.lock.yml | 3 +- .../test-claude-add-issue-comment.lock.yml | 3 +- .../test-claude-add-issue-labels.lock.yml | 3 +- .../workflows/test-claude-command.lock.yml | 3 +- .../test-claude-create-issue.lock.yml | 3 +- ...reate-pull-request-review-comment.lock.yml | 3 +- .../test-claude-create-pull-request.lock.yml | 3 +- ...eate-repository-security-advisory.lock.yml | 18 +- .github/workflows/test-claude-mcp.lock.yml | 3 +- .../test-claude-push-to-branch.lock.yml | 3 +- .../test-claude-update-issue.lock.yml | 3 +- .../test-codex-add-issue-comment.lock.yml | 3 +- .../test-codex-add-issue-labels.lock.yml | 3 +- .github/workflows/test-codex-command.lock.yml | 3 +- .../test-codex-create-issue.lock.yml | 3 +- ...reate-pull-request-review-comment.lock.yml | 3 +- .../test-codex-create-pull-request.lock.yml | 3 +- ...eate-repository-security-advisory.lock.yml | 18 +- .github/workflows/test-codex-mcp.lock.yml | 3 +- .../test-codex-push-to-branch.lock.yml | 3 +- .../test-codex-update-issue.lock.yml | 3 +- .../test-custom-safe-outputs.lock.yml | 18 +- .github/workflows/test-proxy.lock.yml | 3 +- pkg/workflow/bash_timeout_test.go | 162 +++++++++--------- pkg/workflow/claude_engine.go | 39 ++--- pkg/workflow/js/collect_ndjson_output.cjs | 3 +- .../js/collect_ndjson_output.test.cjs | 4 +- .../create_repository_security_advisory.cjs | 15 +- ...eate_repository_security_advisory.test.cjs | 50 ++++-- 29 files changed, 204 insertions(+), 183 deletions(-) diff --git a/.github/workflows/test-ai-inference-github-models.lock.yml b/.github/workflows/test-ai-inference-github-models.lock.yml index ac23d5291b3..f3c785ff36c 100644 --- a/.github/workflows/test-ai-inference-github-models.lock.yml +++ b/.github/workflows/test-ai-inference-github-models.lock.yml @@ -698,7 +698,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -772,7 +772,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-add-issue-comment.lock.yml b/.github/workflows/test-claude-add-issue-comment.lock.yml index 95e9d4bcb7a..a7084328cb6 100644 --- a/.github/workflows/test-claude-add-issue-comment.lock.yml +++ b/.github/workflows/test-claude-add-issue-comment.lock.yml @@ -885,7 +885,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -959,7 +959,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-add-issue-labels.lock.yml b/.github/workflows/test-claude-add-issue-labels.lock.yml index f8b65fdf872..4fb4ad59bda 100644 --- a/.github/workflows/test-claude-add-issue-labels.lock.yml +++ b/.github/workflows/test-claude-add-issue-labels.lock.yml @@ -885,7 +885,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -959,7 +959,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-command.lock.yml b/.github/workflows/test-claude-command.lock.yml index f7924c7c455..b5603e73481 100644 --- a/.github/workflows/test-claude-command.lock.yml +++ b/.github/workflows/test-claude-command.lock.yml @@ -1058,7 +1058,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -1132,7 +1132,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-create-issue.lock.yml b/.github/workflows/test-claude-create-issue.lock.yml index 0fbdf6ab4d1..155490a980c 100644 --- a/.github/workflows/test-claude-create-issue.lock.yml +++ b/.github/workflows/test-claude-create-issue.lock.yml @@ -558,7 +558,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -632,7 +632,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml b/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml index 49a801abf1b..17f38a48cc8 100644 --- a/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml +++ b/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml @@ -831,7 +831,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -905,7 +905,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-create-pull-request.lock.yml b/.github/workflows/test-claude-create-pull-request.lock.yml index cde7307289c..8c396904912 100644 --- a/.github/workflows/test-claude-create-pull-request.lock.yml +++ b/.github/workflows/test-claude-create-pull-request.lock.yml @@ -634,7 +634,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -708,7 +708,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-create-repository-security-advisory.lock.yml b/.github/workflows/test-claude-create-repository-security-advisory.lock.yml index c3e6b5f5681..d7bf89c7ee5 100644 --- a/.github/workflows/test-claude-create-repository-security-advisory.lock.yml +++ b/.github/workflows/test-claude-create-repository-security-advisory.lock.yml @@ -820,7 +820,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -894,7 +894,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); @@ -1741,13 +1740,18 @@ jobs: } // Find all create-repository-security-advisory items const securityItems = validatedOutput.items.filter( - /** @param {any} item */ item => item.type === "create-repository-security-advisory" + /** @param {any} item */ item => + item.type === "create-repository-security-advisory" ); if (securityItems.length === 0) { - console.log("No create-repository-security-advisory items found in agent output"); + console.log( + "No create-repository-security-advisory items found in agent output" + ); return; } - console.log(`Found ${securityItems.length} create-repository-security-advisory item(s)`); + console.log( + `Found ${securityItems.length} create-repository-security-advisory item(s)` + ); // Get the max configuration from environment variable const maxFindings = process.env.GITHUB_AW_SECURITY_REPORT_MAX ? parseInt(process.env.GITHUB_AW_SECURITY_REPORT_MAX) @@ -1782,7 +1786,9 @@ jobs: ); // Validate required fields if (!securityItem.file) { - console.log('Missing required field "file" in repository security advisory item'); + console.log( + 'Missing required field "file" in repository security advisory item' + ); continue; } if ( diff --git a/.github/workflows/test-claude-mcp.lock.yml b/.github/workflows/test-claude-mcp.lock.yml index 79fc4531d15..21f820d13b8 100644 --- a/.github/workflows/test-claude-mcp.lock.yml +++ b/.github/workflows/test-claude-mcp.lock.yml @@ -840,7 +840,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -914,7 +914,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-push-to-branch.lock.yml b/.github/workflows/test-claude-push-to-branch.lock.yml index a88e3825f70..54d507bff1f 100644 --- a/.github/workflows/test-claude-push-to-branch.lock.yml +++ b/.github/workflows/test-claude-push-to-branch.lock.yml @@ -727,7 +727,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -801,7 +801,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-update-issue.lock.yml b/.github/workflows/test-claude-update-issue.lock.yml index 5191edd64b7..d4005a3cd57 100644 --- a/.github/workflows/test-claude-update-issue.lock.yml +++ b/.github/workflows/test-claude-update-issue.lock.yml @@ -888,7 +888,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -962,7 +962,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-add-issue-comment.lock.yml b/.github/workflows/test-codex-add-issue-comment.lock.yml index 34deb864dc3..89f72388af3 100644 --- a/.github/workflows/test-codex-add-issue-comment.lock.yml +++ b/.github/workflows/test-codex-add-issue-comment.lock.yml @@ -716,7 +716,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -790,7 +790,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-add-issue-labels.lock.yml b/.github/workflows/test-codex-add-issue-labels.lock.yml index 9c2b8c78acf..e82ba0011b9 100644 --- a/.github/workflows/test-codex-add-issue-labels.lock.yml +++ b/.github/workflows/test-codex-add-issue-labels.lock.yml @@ -716,7 +716,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -790,7 +790,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-command.lock.yml b/.github/workflows/test-codex-command.lock.yml index 174bd80d932..44936597dab 100644 --- a/.github/workflows/test-codex-command.lock.yml +++ b/.github/workflows/test-codex-command.lock.yml @@ -1058,7 +1058,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -1132,7 +1132,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-create-issue.lock.yml b/.github/workflows/test-codex-create-issue.lock.yml index e4753ca11a9..f012ff8d495 100644 --- a/.github/workflows/test-codex-create-issue.lock.yml +++ b/.github/workflows/test-codex-create-issue.lock.yml @@ -389,7 +389,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -463,7 +463,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml b/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml index 663593f9520..934f3c1ab0d 100644 --- a/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml +++ b/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml @@ -662,7 +662,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -736,7 +736,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-create-pull-request.lock.yml b/.github/workflows/test-codex-create-pull-request.lock.yml index 23038b6b589..fcce878aa2f 100644 --- a/.github/workflows/test-codex-create-pull-request.lock.yml +++ b/.github/workflows/test-codex-create-pull-request.lock.yml @@ -455,7 +455,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -529,7 +529,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-create-repository-security-advisory.lock.yml b/.github/workflows/test-codex-create-repository-security-advisory.lock.yml index 78c13da76c8..14ce16fa3cc 100644 --- a/.github/workflows/test-codex-create-repository-security-advisory.lock.yml +++ b/.github/workflows/test-codex-create-repository-security-advisory.lock.yml @@ -651,7 +651,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -725,7 +725,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); @@ -1502,13 +1501,18 @@ jobs: } // Find all create-repository-security-advisory items const securityItems = validatedOutput.items.filter( - /** @param {any} item */ item => item.type === "create-repository-security-advisory" + /** @param {any} item */ item => + item.type === "create-repository-security-advisory" ); if (securityItems.length === 0) { - console.log("No create-repository-security-advisory items found in agent output"); + console.log( + "No create-repository-security-advisory items found in agent output" + ); return; } - console.log(`Found ${securityItems.length} create-repository-security-advisory item(s)`); + console.log( + `Found ${securityItems.length} create-repository-security-advisory item(s)` + ); // Get the max configuration from environment variable const maxFindings = process.env.GITHUB_AW_SECURITY_REPORT_MAX ? parseInt(process.env.GITHUB_AW_SECURITY_REPORT_MAX) @@ -1543,7 +1547,9 @@ jobs: ); // Validate required fields if (!securityItem.file) { - console.log('Missing required field "file" in repository security advisory item'); + console.log( + 'Missing required field "file" in repository security advisory item' + ); continue; } if ( diff --git a/.github/workflows/test-codex-mcp.lock.yml b/.github/workflows/test-codex-mcp.lock.yml index 91fa57b1a8d..de45ffbc591 100644 --- a/.github/workflows/test-codex-mcp.lock.yml +++ b/.github/workflows/test-codex-mcp.lock.yml @@ -668,7 +668,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -742,7 +742,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-push-to-branch.lock.yml b/.github/workflows/test-codex-push-to-branch.lock.yml index dae46027499..be9e303b559 100644 --- a/.github/workflows/test-codex-push-to-branch.lock.yml +++ b/.github/workflows/test-codex-push-to-branch.lock.yml @@ -586,7 +586,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -660,7 +660,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-update-issue.lock.yml b/.github/workflows/test-codex-update-issue.lock.yml index 8ebb40d8693..e0fd5cf5adf 100644 --- a/.github/workflows/test-codex-update-issue.lock.yml +++ b/.github/workflows/test-codex-update-issue.lock.yml @@ -719,7 +719,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -793,7 +793,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-custom-safe-outputs.lock.yml b/.github/workflows/test-custom-safe-outputs.lock.yml index 9e40737666b..35b72eac0b6 100644 --- a/.github/workflows/test-custom-safe-outputs.lock.yml +++ b/.github/workflows/test-custom-safe-outputs.lock.yml @@ -571,7 +571,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -645,7 +645,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); @@ -2143,13 +2142,18 @@ jobs: } // Find all create-repository-security-advisory items const securityItems = validatedOutput.items.filter( - /** @param {any} item */ item => item.type === "create-repository-security-advisory" + /** @param {any} item */ item => + item.type === "create-repository-security-advisory" ); if (securityItems.length === 0) { - console.log("No create-repository-security-advisory items found in agent output"); + console.log( + "No create-repository-security-advisory items found in agent output" + ); return; } - console.log(`Found ${securityItems.length} create-repository-security-advisory item(s)`); + console.log( + `Found ${securityItems.length} create-repository-security-advisory item(s)` + ); // Get the max configuration from environment variable const maxFindings = process.env.GITHUB_AW_SECURITY_REPORT_MAX ? parseInt(process.env.GITHUB_AW_SECURITY_REPORT_MAX) @@ -2184,7 +2188,9 @@ jobs: ); // Validate required fields if (!securityItem.file) { - console.log('Missing required field "file" in repository security advisory item'); + console.log( + 'Missing required field "file" in repository security advisory item' + ); continue; } if ( diff --git a/.github/workflows/test-proxy.lock.yml b/.github/workflows/test-proxy.lock.yml index 1d7bec7fee8..b90883d9d97 100644 --- a/.github/workflows/test-proxy.lock.yml +++ b/.github/workflows/test-proxy.lock.yml @@ -801,7 +801,7 @@ jobs: // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -875,7 +875,6 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/pkg/workflow/bash_timeout_test.go b/pkg/workflow/bash_timeout_test.go index 100d61b1c1e..539cabb0821 100644 --- a/pkg/workflow/bash_timeout_test.go +++ b/pkg/workflow/bash_timeout_test.go @@ -281,12 +281,12 @@ func TestExtractBashTimeoutEnvVars(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := engine.extractBashTimeoutEnvVars(tt.tools) - + if len(result) != len(tt.expected) { t.Errorf("Expected %d env vars, got %d. Expected: %+v, Got: %+v", len(tt.expected), len(result), tt.expected, result) return } - + for expectedKey, expectedValue := range tt.expected { if actualValue, exists := result[expectedKey]; !exists { t.Errorf("Expected env var %s not found", expectedKey) @@ -299,87 +299,87 @@ func TestExtractBashTimeoutEnvVars(t *testing.T) { } func TestValidateBashTimeoutSupport(t *testing.T) { -tests := []struct { -name string -engineID string -tools map[string]any -expectError bool -errorMsg string -}{ -{ -name: "claude engine with bash timeout should pass", -engineID: "claude", -tools: map[string]any{ -"bash": map[string]any{ -"timeout": 30, -}, -}, -expectError: false, -}, -{ -name: "codex engine without bash timeout should pass", -engineID: "codex", -tools: map[string]any{ -"bash": []any{"echo"}, -}, -expectError: false, -}, -{ -name: "codex engine with bash timeout should fail", -engineID: "codex", -tools: map[string]any{ -"bash": map[string]any{ -"timeout": 30, -}, -}, -expectError: true, -errorMsg: "bash tool timeout configuration is not supported by engine 'codex'", -}, -{ -name: "custom engine with bash timeout should fail", -engineID: "custom", -tools: map[string]any{ -"bash": map[string]any{ -"timeout": 45, -"commands": []any{"echo"}, -}, -}, -expectError: true, -errorMsg: "bash tool timeout configuration is not supported by engine 'custom'", -}, -{ -name: "no bash tool should pass for any engine", -engineID: "codex", -tools: map[string]any{ -"web-fetch": nil, -}, -expectError: false, -}, -} + tests := []struct { + name string + engineID string + tools map[string]any + expectError bool + errorMsg string + }{ + { + name: "claude engine with bash timeout should pass", + engineID: "claude", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": 30, + }, + }, + expectError: false, + }, + { + name: "codex engine without bash timeout should pass", + engineID: "codex", + tools: map[string]any{ + "bash": []any{"echo"}, + }, + expectError: false, + }, + { + name: "codex engine with bash timeout should fail", + engineID: "codex", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": 30, + }, + }, + expectError: true, + errorMsg: "bash tool timeout configuration is not supported by engine 'codex'", + }, + { + name: "custom engine with bash timeout should fail", + engineID: "custom", + tools: map[string]any{ + "bash": map[string]any{ + "timeout": 45, + "commands": []any{"echo"}, + }, + }, + expectError: true, + errorMsg: "bash tool timeout configuration is not supported by engine 'custom'", + }, + { + name: "no bash tool should pass for any engine", + engineID: "codex", + tools: map[string]any{ + "web-fetch": nil, + }, + expectError: false, + }, + } -compiler := NewCompiler(false, "", "test") -engines := NewEngineRegistry() + compiler := NewCompiler(false, "", "test") + engines := NewEngineRegistry() -for _, tt := range tests { -t.Run(tt.name, func(t *testing.T) { -engine, err := engines.GetEngine(tt.engineID) -if err != nil { -t.Fatalf("Engine %s not found: %v", tt.engineID, err) -} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + engine, err := engines.GetEngine(tt.engineID) + if err != nil { + t.Fatalf("Engine %s not found: %v", tt.engineID, err) + } -err = compiler.validateBashTimeoutSupport(tt.tools, engine) + err = compiler.validateBashTimeoutSupport(tt.tools, engine) -if tt.expectError { -if err == nil { -t.Errorf("Expected error but got none") -} else if err.Error() != tt.errorMsg { -t.Errorf("Expected error message \"%s\", got \"%s\"", tt.errorMsg, err.Error()) -} -} else { -if err != nil { -t.Errorf("Expected no error but got: %v", err) -} -} -}) -} + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if err.Error() != tt.errorMsg { + t.Errorf("Expected error message \"%s\", got \"%s\"", tt.errorMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + } + }) + } } diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index 7fc0abff0ee..102299c8689 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -28,9 +28,9 @@ func NewClaudeEngine() *ClaudeEngine { description: "Uses Claude Code with full MCP tool support and allow-listing", experimental: false, supportsToolsWhitelist: true, - supportsHTTPTransport: true, // Claude supports both stdio and HTTP transport - supportsMaxTurns: true, // Claude supports max-turns feature - supportsBashTimeout: true, // Claude supports bash timeout configuration + supportsHTTPTransport: true, // Claude supports both stdio and HTTP transport + supportsMaxTurns: true, // Claude supports max-turns feature + supportsBashTimeout: true, // Claude supports bash timeout configuration }, } } @@ -104,8 +104,6 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str // Add bash timeout environment variables from tools configuration bashTimeoutEnvVars := e.extractBashTimeoutEnvVars(workflowData.Tools) - fmt.Printf("DEBUG: workflowData.Tools: %+v\n", workflowData.Tools) - fmt.Printf("DEBUG: bashTimeoutEnvVars: %+v\n", bashTimeoutEnvVars) for key, value := range bashTimeoutEnvVars { if claudeEnv != "" { claudeEnv += "\n" @@ -298,8 +296,8 @@ func (e *ClaudeEngine) expandNeutralToolsToClaudeTools(tools map[string]any) map // bash -> Bash, KillBash, BashOutput if bashConfig, ok := bashTool.(map[string]any); ok { // Handle object format with timeout and commands - var commands any = nil - + var commands any + // Check for commands in "commands" field (preferred) if cmdArray, hasCommands := bashConfig["commands"]; hasCommands { commands = cmdArray @@ -307,9 +305,9 @@ func (e *ClaudeEngine) expandNeutralToolsToClaudeTools(tools map[string]any) map // Fallback to "allowed" field for backward compatibility commands = allowedArray } - + claudeAllowed["Bash"] = commands - + // Extract timeout if present if timeout, hasTimeout := bashConfig["timeout"]; hasTimeout { claudeTimeout["bash"] = timeout @@ -347,12 +345,12 @@ func (e *ClaudeEngine) expandNeutralToolsToClaudeTools(tools map[string]any) map // Update claude section claudeSection["allowed"] = claudeAllowed - + // Only add timeout section if we have timeout configurations if len(claudeTimeout) > 0 { claudeSection["timeout"] = claudeTimeout } - + result["claude"] = claudeSection return result @@ -361,18 +359,14 @@ func (e *ClaudeEngine) expandNeutralToolsToClaudeTools(tools map[string]any) map // extractBashTimeoutEnvVars extracts bash timeout configuration and returns environment variables for the Claude agent func (e *ClaudeEngine) extractBashTimeoutEnvVars(tools map[string]any) map[string]string { envVars := make(map[string]string) - + if tools == nil { - fmt.Printf("DEBUG: tools is nil\n") return envVars } - + if bashTool, hasBash := tools["bash"]; hasBash { - fmt.Printf("DEBUG: Found bash tool: %+v (type: %T)\n", bashTool, bashTool) if bashConfig, ok := bashTool.(map[string]any); ok { - fmt.Printf("DEBUG: bash tool is map: %+v\n", bashConfig) if timeout, hasTimeout := bashConfig["timeout"]; hasTimeout { - fmt.Printf("DEBUG: Found timeout: %+v (type: %T)\n", timeout, timeout) // Convert timeout to milliseconds for BASH_DEFAULT_TIMEOUT_MS timeoutMs := "" switch t := timeout.(type) { @@ -389,23 +383,16 @@ func (e *ClaudeEngine) extractBashTimeoutEnvVars(tools map[string]any) map[strin timeoutMs = t } } - + if timeoutMs != "" { - fmt.Printf("DEBUG: Setting timeout env vars: %s\n", timeoutMs) // Set the bash timeout environment variables envVars["BASH_DEFAULT_TIMEOUT_MS"] = timeoutMs envVars["BASH_MAX_TIMEOUT_MS"] = timeoutMs // Use the same value for max timeout } - } else { - fmt.Printf("DEBUG: No timeout in bash config\n") } - } else { - fmt.Printf("DEBUG: bash tool is not a map\n") } - } else { - fmt.Printf("DEBUG: No bash tool found\n") } - + return envVars } diff --git a/pkg/workflow/js/collect_ndjson_output.cjs b/pkg/workflow/js/collect_ndjson_output.cjs index 18c9e14553e..ac191838832 100644 --- a/pkg/workflow/js/collect_ndjson_output.cjs +++ b/pkg/workflow/js/collect_ndjson_output.cjs @@ -202,7 +202,7 @@ async function main() { // U+0014 (DC4) — represented here as "\u0014" // Escape control characters not allowed in JSON strings (U+0000 through U+001F) // Preserve common JSON escapes for \b, \f, \n, \r, \t and use \uXXXX for the rest. - const _ctrl = {8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r"}; + const _ctrl = { 8: "\\b", 9: "\\t", 10: "\\n", 12: "\\f", 13: "\\r" }; repaired = repaired.replace(/[\u0000-\u001F]/g, ch => { const c = ch.charCodeAt(0); return _ctrl[c] || "\\u" + c.toString(16).padStart(4, "0"); @@ -288,7 +288,6 @@ async function main() { return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error - console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/pkg/workflow/js/collect_ndjson_output.test.cjs b/pkg/workflow/js/collect_ndjson_output.test.cjs index ef4ca8b7034..af0c9ad4e62 100644 --- a/pkg/workflow/js/collect_ndjson_output.test.cjs +++ b/pkg/workflow/js/collect_ndjson_output.test.cjs @@ -932,12 +932,12 @@ Line 3"} const parsedOutput = JSON.parse(outputCall[1]); expect(parsedOutput.items).toHaveLength(1); expect(parsedOutput.items[0].type).toBe("create-issue"); - + // Control chars (0x00, 0x01, 0x02) removed, tab and newline preserved const title = parsedOutput.items[0].title; expect(title).toBe("Control test"); // Control chars actually get removed completely expect(parsedOutput.items[0].body).toBe("End of test"); - + expect(parsedOutput.errors).toHaveLength(0); }); diff --git a/pkg/workflow/js/create_repository_security_advisory.cjs b/pkg/workflow/js/create_repository_security_advisory.cjs index 1a556c2de3a..580c068ce39 100644 --- a/pkg/workflow/js/create_repository_security_advisory.cjs +++ b/pkg/workflow/js/create_repository_security_advisory.cjs @@ -32,14 +32,19 @@ async function main() { // Find all create-repository-security-advisory items const securityItems = validatedOutput.items.filter( - /** @param {any} item */ item => item.type === "create-repository-security-advisory" + /** @param {any} item */ item => + item.type === "create-repository-security-advisory" ); if (securityItems.length === 0) { - console.log("No create-repository-security-advisory items found in agent output"); + console.log( + "No create-repository-security-advisory items found in agent output" + ); return; } - console.log(`Found ${securityItems.length} create-repository-security-advisory item(s)`); + console.log( + `Found ${securityItems.length} create-repository-security-advisory item(s)` + ); // Get the max configuration from environment variable const maxFindings = process.env.GITHUB_AW_SECURITY_REPORT_MAX @@ -80,7 +85,9 @@ async function main() { // Validate required fields if (!securityItem.file) { - console.log('Missing required field "file" in repository security advisory item'); + console.log( + 'Missing required field "file" in repository security advisory item' + ); continue; } diff --git a/pkg/workflow/js/create_repository_security_advisory.test.cjs b/pkg/workflow/js/create_repository_security_advisory.test.cjs index fa7977d2399..669194c8d46 100644 --- a/pkg/workflow/js/create_repository_security_advisory.test.cjs +++ b/pkg/workflow/js/create_repository_security_advisory.test.cjs @@ -54,7 +54,10 @@ describe("create_repository_security_advisory.cjs", () => { afterEach(() => { // Clean up any created files try { - const sarifFile = path.join(process.cwd(), "repository-security-advisory.sarif"); + const sarifFile = path.join( + process.cwd(), + "repository-security-advisory.sarif" + ); if (fs.existsSync(sarifFile)) { fs.unlinkSync(sarifFile); } @@ -157,7 +160,10 @@ describe("create_repository_security_advisory.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); // Check that SARIF file was created - const sarifFile = path.join(process.cwd(), "repository-security-advisory.sarif"); + const sarifFile = path.join( + process.cwd(), + "repository-security-advisory.sarif" + ); expect(fs.existsSync(sarifFile)).toBe(true); // Check SARIF content @@ -229,7 +235,10 @@ describe("create_repository_security_advisory.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); // Check that SARIF file was created with only 1 finding - const sarifFile = path.join(process.cwd(), "repository-security-advisory.sarif"); + const sarifFile = path.join( + process.cwd(), + "repository-security-advisory.sarif" + ); expect(fs.existsSync(sarifFile)).toBe(true); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); @@ -291,7 +300,10 @@ describe("create_repository_security_advisory.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); // Check that SARIF file was created with only the 1 valid finding - const sarifFile = path.join(process.cwd(), "repository-security-advisory.sarif"); + const sarifFile = path.join( + process.cwd(), + "repository-security-advisory.sarif" + ); expect(fs.existsSync(sarifFile)).toBe(true); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); @@ -327,7 +339,10 @@ describe("create_repository_security_advisory.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); - const sarifFile = path.join(process.cwd(), "repository-security-advisory.sarif"); + const sarifFile = path.join( + process.cwd(), + "repository-security-advisory.sarif" + ); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); // Check driver name @@ -361,7 +376,10 @@ describe("create_repository_security_advisory.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); - const sarifFile = path.join(process.cwd(), "repository-security-advisory.sarif"); + const sarifFile = path.join( + process.cwd(), + "repository-security-advisory.sarif" + ); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); // Check default driver name @@ -404,7 +422,10 @@ describe("create_repository_security_advisory.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); - const sarifFile = path.join(process.cwd(), "repository-security-advisory.sarif"); + const sarifFile = path.join( + process.cwd(), + "repository-security-advisory.sarif" + ); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); // Check first result has custom column @@ -466,7 +487,10 @@ describe("create_repository_security_advisory.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); // Only the first valid finding should be processed - const sarifFile = path.join(process.cwd(), "repository-security-advisory.sarif"); + const sarifFile = path.join( + process.cwd(), + "repository-security-advisory.sarif" + ); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); expect(sarifContent.runs[0].results).toHaveLength(1); expect(sarifContent.runs[0].results[0].message.text).toBe( @@ -517,7 +541,10 @@ describe("create_repository_security_advisory.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); - const sarifFile = path.join(process.cwd(), "repository-security-advisory.sarif"); + const sarifFile = path.join( + process.cwd(), + "repository-security-advisory.sarif" + ); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); // Check first result has custom rule ID @@ -590,7 +617,10 @@ describe("create_repository_security_advisory.cjs", () => { await eval(`(async () => { ${securityReportScript} })()`); // Only the first valid finding should be processed - const sarifFile = path.join(process.cwd(), "repository-security-advisory.sarif"); + const sarifFile = path.join( + process.cwd(), + "repository-security-advisory.sarif" + ); const sarifContent = JSON.parse(fs.readFileSync(sarifFile, "utf8")); expect(sarifContent.runs[0].results).toHaveLength(1); expect(sarifContent.runs[0].results[0].message.text).toBe( From d169e3a013241acb02c85f6665aa80778fe538ea Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Wed, 10 Sep 2025 22:11:39 +0000 Subject: [PATCH 5/7] Add logging for invalid JSON input in workflow scripts --- .github/workflows/test-ai-inference-github-models.lock.yml | 1 + .github/workflows/test-claude-add-issue-comment.lock.yml | 1 + .github/workflows/test-claude-add-issue-labels.lock.yml | 1 + .github/workflows/test-claude-command.lock.yml | 1 + .github/workflows/test-claude-create-issue.lock.yml | 1 + .../test-claude-create-pull-request-review-comment.lock.yml | 1 + .github/workflows/test-claude-create-pull-request.lock.yml | 1 + .../test-claude-create-repository-security-advisory.lock.yml | 1 + .github/workflows/test-claude-mcp.lock.yml | 1 + .github/workflows/test-claude-push-to-branch.lock.yml | 1 + .github/workflows/test-claude-update-issue.lock.yml | 1 + .github/workflows/test-codex-add-issue-comment.lock.yml | 1 + .github/workflows/test-codex-add-issue-labels.lock.yml | 1 + .github/workflows/test-codex-command.lock.yml | 1 + .github/workflows/test-codex-create-issue.lock.yml | 1 + .../test-codex-create-pull-request-review-comment.lock.yml | 1 + .github/workflows/test-codex-create-pull-request.lock.yml | 1 + .../test-codex-create-repository-security-advisory.lock.yml | 1 + .github/workflows/test-codex-mcp.lock.yml | 1 + .github/workflows/test-codex-push-to-branch.lock.yml | 1 + .github/workflows/test-codex-update-issue.lock.yml | 1 + .github/workflows/test-custom-safe-outputs.lock.yml | 1 + .github/workflows/test-proxy.lock.yml | 1 + pkg/workflow/js/collect_ndjson_output.cjs | 1 + 24 files changed, 24 insertions(+) diff --git a/.github/workflows/test-ai-inference-github-models.lock.yml b/.github/workflows/test-ai-inference-github-models.lock.yml index f3c785ff36c..1819321a287 100644 --- a/.github/workflows/test-ai-inference-github-models.lock.yml +++ b/.github/workflows/test-ai-inference-github-models.lock.yml @@ -772,6 +772,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-add-issue-comment.lock.yml b/.github/workflows/test-claude-add-issue-comment.lock.yml index a7084328cb6..72ab6e0306e 100644 --- a/.github/workflows/test-claude-add-issue-comment.lock.yml +++ b/.github/workflows/test-claude-add-issue-comment.lock.yml @@ -959,6 +959,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-add-issue-labels.lock.yml b/.github/workflows/test-claude-add-issue-labels.lock.yml index 4fb4ad59bda..f3a497f1144 100644 --- a/.github/workflows/test-claude-add-issue-labels.lock.yml +++ b/.github/workflows/test-claude-add-issue-labels.lock.yml @@ -959,6 +959,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-command.lock.yml b/.github/workflows/test-claude-command.lock.yml index b5603e73481..1a322f6eca7 100644 --- a/.github/workflows/test-claude-command.lock.yml +++ b/.github/workflows/test-claude-command.lock.yml @@ -1132,6 +1132,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-create-issue.lock.yml b/.github/workflows/test-claude-create-issue.lock.yml index 155490a980c..140c7a56dae 100644 --- a/.github/workflows/test-claude-create-issue.lock.yml +++ b/.github/workflows/test-claude-create-issue.lock.yml @@ -632,6 +632,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml b/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml index 17f38a48cc8..0ad23c8fd94 100644 --- a/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml +++ b/.github/workflows/test-claude-create-pull-request-review-comment.lock.yml @@ -905,6 +905,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-create-pull-request.lock.yml b/.github/workflows/test-claude-create-pull-request.lock.yml index 8c396904912..f865722a127 100644 --- a/.github/workflows/test-claude-create-pull-request.lock.yml +++ b/.github/workflows/test-claude-create-pull-request.lock.yml @@ -708,6 +708,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-create-repository-security-advisory.lock.yml b/.github/workflows/test-claude-create-repository-security-advisory.lock.yml index d7bf89c7ee5..3144b8d8c72 100644 --- a/.github/workflows/test-claude-create-repository-security-advisory.lock.yml +++ b/.github/workflows/test-claude-create-repository-security-advisory.lock.yml @@ -894,6 +894,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-mcp.lock.yml b/.github/workflows/test-claude-mcp.lock.yml index 21f820d13b8..2c73de56f22 100644 --- a/.github/workflows/test-claude-mcp.lock.yml +++ b/.github/workflows/test-claude-mcp.lock.yml @@ -914,6 +914,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-push-to-branch.lock.yml b/.github/workflows/test-claude-push-to-branch.lock.yml index 54d507bff1f..e98d685452e 100644 --- a/.github/workflows/test-claude-push-to-branch.lock.yml +++ b/.github/workflows/test-claude-push-to-branch.lock.yml @@ -801,6 +801,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-claude-update-issue.lock.yml b/.github/workflows/test-claude-update-issue.lock.yml index 625379bb247..2b0d8362e3c 100644 --- a/.github/workflows/test-claude-update-issue.lock.yml +++ b/.github/workflows/test-claude-update-issue.lock.yml @@ -962,6 +962,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-add-issue-comment.lock.yml b/.github/workflows/test-codex-add-issue-comment.lock.yml index 89f72388af3..d5720c22d7a 100644 --- a/.github/workflows/test-codex-add-issue-comment.lock.yml +++ b/.github/workflows/test-codex-add-issue-comment.lock.yml @@ -790,6 +790,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-add-issue-labels.lock.yml b/.github/workflows/test-codex-add-issue-labels.lock.yml index e82ba0011b9..7864ed075e2 100644 --- a/.github/workflows/test-codex-add-issue-labels.lock.yml +++ b/.github/workflows/test-codex-add-issue-labels.lock.yml @@ -790,6 +790,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-command.lock.yml b/.github/workflows/test-codex-command.lock.yml index 44936597dab..72368296027 100644 --- a/.github/workflows/test-codex-command.lock.yml +++ b/.github/workflows/test-codex-command.lock.yml @@ -1132,6 +1132,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-create-issue.lock.yml b/.github/workflows/test-codex-create-issue.lock.yml index f012ff8d495..7010bb34082 100644 --- a/.github/workflows/test-codex-create-issue.lock.yml +++ b/.github/workflows/test-codex-create-issue.lock.yml @@ -463,6 +463,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml b/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml index 934f3c1ab0d..56f98108191 100644 --- a/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml +++ b/.github/workflows/test-codex-create-pull-request-review-comment.lock.yml @@ -736,6 +736,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-create-pull-request.lock.yml b/.github/workflows/test-codex-create-pull-request.lock.yml index fcce878aa2f..f79e9670ca0 100644 --- a/.github/workflows/test-codex-create-pull-request.lock.yml +++ b/.github/workflows/test-codex-create-pull-request.lock.yml @@ -529,6 +529,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-create-repository-security-advisory.lock.yml b/.github/workflows/test-codex-create-repository-security-advisory.lock.yml index 14ce16fa3cc..bf695a8e520 100644 --- a/.github/workflows/test-codex-create-repository-security-advisory.lock.yml +++ b/.github/workflows/test-codex-create-repository-security-advisory.lock.yml @@ -725,6 +725,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-mcp.lock.yml b/.github/workflows/test-codex-mcp.lock.yml index de45ffbc591..a20902b6cab 100644 --- a/.github/workflows/test-codex-mcp.lock.yml +++ b/.github/workflows/test-codex-mcp.lock.yml @@ -742,6 +742,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-push-to-branch.lock.yml b/.github/workflows/test-codex-push-to-branch.lock.yml index be9e303b559..f317d0a5d18 100644 --- a/.github/workflows/test-codex-push-to-branch.lock.yml +++ b/.github/workflows/test-codex-push-to-branch.lock.yml @@ -660,6 +660,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-codex-update-issue.lock.yml b/.github/workflows/test-codex-update-issue.lock.yml index ce94c43b600..f83bc615003 100644 --- a/.github/workflows/test-codex-update-issue.lock.yml +++ b/.github/workflows/test-codex-update-issue.lock.yml @@ -793,6 +793,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-custom-safe-outputs.lock.yml b/.github/workflows/test-custom-safe-outputs.lock.yml index 35b72eac0b6..12af6a0c957 100644 --- a/.github/workflows/test-custom-safe-outputs.lock.yml +++ b/.github/workflows/test-custom-safe-outputs.lock.yml @@ -645,6 +645,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/.github/workflows/test-proxy.lock.yml b/.github/workflows/test-proxy.lock.yml index b90883d9d97..1fe0c0a7f77 100644 --- a/.github/workflows/test-proxy.lock.yml +++ b/.github/workflows/test-proxy.lock.yml @@ -875,6 +875,7 @@ jobs: return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); diff --git a/pkg/workflow/js/collect_ndjson_output.cjs b/pkg/workflow/js/collect_ndjson_output.cjs index ac191838832..c4dbd5adf74 100644 --- a/pkg/workflow/js/collect_ndjson_output.cjs +++ b/pkg/workflow/js/collect_ndjson_output.cjs @@ -288,6 +288,7 @@ async function main() { return JSON.parse(repairedJson); } catch (repairError) { // If repair also fails, throw the error + console.log(`invalid input json: ${jsonStr}`); throw new Error( `JSON parsing failed. Original: ${originalError.message}. After attempted repair: ${repairError.message}` ); From 42f9ad42f6755ea259d4e0b568bdb888b81f1353 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Wed, 10 Sep 2025 22:18:31 +0000 Subject: [PATCH 6/7] Add workflow and documentation for Test Claude Bash - Agentic Activity Overview --- .github/workflows/test-claude-bash.lock.yml | 635 ++++++++++++++++++++ .github/workflows/test-claude-bash.md | 24 + 2 files changed, 659 insertions(+) create mode 100644 .github/workflows/test-claude-bash.lock.yml create mode 100644 .github/workflows/test-claude-bash.md diff --git a/.github/workflows/test-claude-bash.lock.yml b/.github/workflows/test-claude-bash.lock.yml new file mode 100644 index 00000000000..7d08540cdf2 --- /dev/null +++ b/.github/workflows/test-claude-bash.lock.yml @@ -0,0 +1,635 @@ +# This file was automatically generated by gh-aw. DO NOT EDIT. +# To update this file, edit the corresponding .md file and run: +# gh aw compile + +name: "Test Claude Bash - Agentic Activity Overview" +on: + workflow_dispatch: null + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Test Claude Bash - Agentic Activity Overview" + +jobs: + test-claude-bash-agentic-activity-overview: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v5 + - name: Generate Claude Settings + run: | + mkdir -p /tmp/.claude + cat > /tmp/.claude/settings.json << 'EOF' + { + "hooks": { + "PreToolUse": [ + { + "matcher": "WebFetch|WebSearch", + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/network_permissions.py" + } + ] + } + ] + } + } + EOF + - 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 allow-list (populated during generation) + ALLOWED_DOMAINS = ["crl3.digicert.com","crl4.digicert.com","ocsp.digicert.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","crl.geotrust.com","ocsp.geotrust.com","crl.thawte.com","ocsp.thawte.com","crl.verisign.com","ocsp.verisign.com","crl.globalsign.com","ocsp.globalsign.com","crls.ssl.com","ocsp.ssl.com","crl.identrust.com","ocsp.identrust.com","crl.sectigo.com","ocsp.sectigo.com","crl.usertrust.com","ocsp.usertrust.com","s.symcb.com","s.symcd.com","json-schema.org","json.schemastore.org","archive.ubuntu.com","security.ubuntu.com","ppa.launchpad.net","keyserver.ubuntu.com","azure.archive.ubuntu.com","api.snapcraft.io","packagecloud.io","packages.cloud.google.com","packages.microsoft.com"] + + 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 web-search: 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: Setup MCPs + run: | + mkdir -p /tmp/mcp-config + cat > /tmp/mcp-config/mcp-servers.json << 'EOF' + { + "mcpServers": { + "github": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "GITHUB_PERSONAL_ACCESS_TOKEN", + "ghcr.io/github/github-mcp-server:sha-09deac4" + ], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" + } + } + } + } + EOF + - name: Create prompt + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + run: | + mkdir -p /tmp/aw-prompts + cat > $GITHUB_AW_PROMPT << 'EOF' + # Test Claude Bash - Agentic Activity Overview + + + Please run `gh aw logs -c 1000` and provide a brief summary of the agentic workflow activity. + + Look at the output and tell me: + 1. How many workflow runs you can see + 2. Which workflows appear most frequently + 3. Any patterns in success/failure rates + + Use the bash tool to run the command and analyze the results. + + EOF + - name: Print prompt to step summary + run: | + echo "## Generated Prompt" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````markdown' >> $GITHUB_STEP_SUMMARY + cat $GITHUB_AW_PROMPT >> $GITHUB_STEP_SUMMARY + echo '``````' >> $GITHUB_STEP_SUMMARY + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + - name: Generate agentic run info + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "claude", + engine_name: "Claude Code", + model: "", + version: "", + workflow_name: "Test Claude Bash - Agentic Activity Overview", + experimental: false, + supports_tools_whitelist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + created_at: new Date().toISOString() + }; + + // Write to /tmp directory to avoid inclusion in PR + const tmpPath = '/tmp/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + - name: Upload agentic run info + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw_info.json + path: /tmp/aw_info.json + if-no-files-found: warn + - name: Execute Claude Code Action + id: agentic_execution + uses: anthropics/claude-code-base-action@v0.0.56 + with: + # Allowed tools (sorted): + # - Bash(gh aw logs:*) + # - BashOutput + # - ExitPlanMode + # - Glob + # - Grep + # - KillBash + # - LS + # - NotebookRead + # - Read + # - Task + # - TodoWrite + # - mcp__github__download_workflow_run_artifact + # - mcp__github__get_code_scanning_alert + # - mcp__github__get_commit + # - mcp__github__get_dependabot_alert + # - mcp__github__get_discussion + # - mcp__github__get_discussion_comments + # - mcp__github__get_file_contents + # - mcp__github__get_issue + # - mcp__github__get_issue_comments + # - mcp__github__get_job_logs + # - mcp__github__get_me + # - mcp__github__get_notification_details + # - mcp__github__get_pull_request + # - mcp__github__get_pull_request_comments + # - mcp__github__get_pull_request_diff + # - mcp__github__get_pull_request_files + # - mcp__github__get_pull_request_reviews + # - mcp__github__get_pull_request_status + # - mcp__github__get_secret_scanning_alert + # - mcp__github__get_tag + # - mcp__github__get_workflow_run + # - mcp__github__get_workflow_run_logs + # - mcp__github__get_workflow_run_usage + # - mcp__github__list_branches + # - mcp__github__list_code_scanning_alerts + # - mcp__github__list_commits + # - mcp__github__list_dependabot_alerts + # - mcp__github__list_discussion_categories + # - mcp__github__list_discussions + # - mcp__github__list_issues + # - mcp__github__list_notifications + # - mcp__github__list_pull_requests + # - mcp__github__list_secret_scanning_alerts + # - mcp__github__list_tags + # - mcp__github__list_workflow_jobs + # - mcp__github__list_workflow_run_artifacts + # - mcp__github__list_workflow_runs + # - mcp__github__list_workflows + # - mcp__github__search_code + # - mcp__github__search_issues + # - mcp__github__search_orgs + # - mcp__github__search_pull_requests + # - mcp__github__search_repositories + # - mcp__github__search_users + allowed_tools: "Bash(gh aw logs:*),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issues,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_secret_scanning_alerts,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + mcp_config: /tmp/mcp-config/mcp-servers.json + prompt_file: /tmp/aw-prompts/prompt.txt + settings: /tmp/.claude/settings.json + timeout_minutes: 5 + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + - name: Capture Agentic Action logs + if: always() + run: | + # Copy the detailed execution file from Agentic Action if available + if [ -n "${{ steps.agentic_execution.outputs.execution_file }}" ] && [ -f "${{ steps.agentic_execution.outputs.execution_file }}" ]; then + cp ${{ steps.agentic_execution.outputs.execution_file }} /tmp/test-claude-bash-agentic-activity-overview.log + else + echo "No execution file output found from Agentic Action" >> /tmp/test-claude-bash-agentic-activity-overview.log + fi + + # Ensure log file exists + touch /tmp/test-claude-bash-agentic-activity-overview.log + - name: Upload engine output files + uses: actions/upload-artifact@v4 + with: + name: agent_outputs + path: | + output.txt + if-no-files-found: ignore + - name: Clean up engine output files + run: | + rm -f output.txt + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@v7 + env: + AGENT_LOG_FILE: /tmp/test-claude-bash-agentic-activity-overview.log + with: + script: | + function main() { + const fs = require("fs"); + try { + // Get the log file path from environment + const logFile = process.env.AGENT_LOG_FILE; + if (!logFile) { + console.log("No agent log file specified"); + return; + } + if (!fs.existsSync(logFile)) { + console.log(`Log file not found: ${logFile}`); + return; + } + const logContent = fs.readFileSync(logFile, "utf8"); + const markdown = parseClaudeLog(logContent); + // Append to GitHub step summary + core.summary.addRaw(markdown).write(); + } catch (error) { + core.error(`Error parsing Claude log: ${error.message}`); + core.setFailed(error.message); + } + } + function parseClaudeLog(logContent) { + try { + const logEntries = JSON.parse(logContent); + if (!Array.isArray(logEntries)) { + return "## Agent Log Summary\n\nLog format not recognized as Claude JSON array.\n"; + } + let markdown = "## 🤖 Commands and Tools\n\n"; + const toolUsePairs = new Map(); // Map tool_use_id to tool_result + const commandSummary = []; // For the succinct summary + // First pass: collect tool results by tool_use_id + for (const entry of logEntries) { + if (entry.type === "user" && entry.message?.content) { + for (const content of entry.message.content) { + if (content.type === "tool_result" && content.tool_use_id) { + toolUsePairs.set(content.tool_use_id, content); + } + } + } + } + // Collect all tool uses for summary + for (const entry of logEntries) { + if (entry.type === "assistant" && entry.message?.content) { + for (const content of entry.message.content) { + if (content.type === "tool_use") { + const toolName = content.name; + const input = content.input || {}; + // Skip internal tools - only show external commands and API calls + if ( + [ + "Read", + "Write", + "Edit", + "MultiEdit", + "LS", + "Grep", + "Glob", + "TodoWrite", + ].includes(toolName) + ) { + continue; // Skip internal file operations and searches + } + // Find the corresponding tool result to get status + const toolResult = toolUsePairs.get(content.id); + let statusIcon = "❓"; + if (toolResult) { + statusIcon = toolResult.is_error === true ? "❌" : "✅"; + } + // Add to command summary (only external tools) + if (toolName === "Bash") { + const formattedCommand = formatBashCommand(input.command || ""); + commandSummary.push(`* ${statusIcon} \`${formattedCommand}\``); + } else if (toolName.startsWith("mcp__")) { + const mcpName = formatMcpName(toolName); + commandSummary.push(`* ${statusIcon} \`${mcpName}(...)\``); + } else { + // Handle other external tools (if any) + commandSummary.push(`* ${statusIcon} ${toolName}`); + } + } + } + } + } + // Add command summary + if (commandSummary.length > 0) { + for (const cmd of commandSummary) { + markdown += `${cmd}\n`; + } + } else { + markdown += "No commands or tools used.\n"; + } + // Add Information section from the last entry with result metadata + markdown += "\n## 📊 Information\n\n"; + // Find the last entry with metadata + const lastEntry = logEntries[logEntries.length - 1]; + if ( + lastEntry && + (lastEntry.num_turns || + lastEntry.duration_ms || + lastEntry.total_cost_usd || + lastEntry.usage) + ) { + if (lastEntry.num_turns) { + markdown += `**Turns:** ${lastEntry.num_turns}\n\n`; + } + if (lastEntry.duration_ms) { + const durationSec = Math.round(lastEntry.duration_ms / 1000); + const minutes = Math.floor(durationSec / 60); + const seconds = durationSec % 60; + markdown += `**Duration:** ${minutes}m ${seconds}s\n\n`; + } + if (lastEntry.total_cost_usd) { + markdown += `**Total Cost:** $${lastEntry.total_cost_usd.toFixed(4)}\n\n`; + } + if (lastEntry.usage) { + const usage = lastEntry.usage; + if (usage.input_tokens || usage.output_tokens) { + markdown += `**Token Usage:**\n`; + if (usage.input_tokens) + markdown += `- Input: ${usage.input_tokens.toLocaleString()}\n`; + if (usage.cache_creation_input_tokens) + markdown += `- Cache Creation: ${usage.cache_creation_input_tokens.toLocaleString()}\n`; + if (usage.cache_read_input_tokens) + markdown += `- Cache Read: ${usage.cache_read_input_tokens.toLocaleString()}\n`; + if (usage.output_tokens) + markdown += `- Output: ${usage.output_tokens.toLocaleString()}\n`; + markdown += "\n"; + } + } + if ( + lastEntry.permission_denials && + lastEntry.permission_denials.length > 0 + ) { + markdown += `**Permission Denials:** ${lastEntry.permission_denials.length}\n\n`; + } + } + markdown += "\n## 🤖 Reasoning\n\n"; + // Second pass: process assistant messages in sequence + for (const entry of logEntries) { + if (entry.type === "assistant" && entry.message?.content) { + for (const content of entry.message.content) { + if (content.type === "text" && content.text) { + // Add reasoning text directly (no header) + const text = content.text.trim(); + if (text && text.length > 0) { + markdown += text + "\n\n"; + } + } else if (content.type === "tool_use") { + // Process tool use with its result + const toolResult = toolUsePairs.get(content.id); + const toolMarkdown = formatToolUse(content, toolResult); + if (toolMarkdown) { + markdown += toolMarkdown; + } + } + } + } + } + return markdown; + } catch (error) { + return `## Agent Log Summary\n\nError parsing Claude log: ${error.message}\n`; + } + } + function formatToolUse(toolUse, toolResult) { + const toolName = toolUse.name; + const input = toolUse.input || {}; + // Skip TodoWrite except the very last one (we'll handle this separately) + if (toolName === "TodoWrite") { + return ""; // Skip for now, would need global context to find the last one + } + // Helper function to determine status icon + function getStatusIcon() { + if (toolResult) { + return toolResult.is_error === true ? "❌" : "✅"; + } + return "❓"; // Unknown by default + } + let markdown = ""; + const statusIcon = getStatusIcon(); + switch (toolName) { + case "Bash": + const command = input.command || ""; + const description = input.description || ""; + // Format the command to be single line + const formattedCommand = formatBashCommand(command); + if (description) { + markdown += `${description}:\n\n`; + } + markdown += `${statusIcon} \`${formattedCommand}\`\n\n`; + break; + case "Read": + const filePath = input.file_path || input.path || ""; + const relativePath = filePath.replace( + /^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, + "" + ); // Remove /home/runner/work/repo/repo/ prefix + markdown += `${statusIcon} Read \`${relativePath}\`\n\n`; + break; + case "Write": + case "Edit": + case "MultiEdit": + const writeFilePath = input.file_path || input.path || ""; + const writeRelativePath = writeFilePath.replace( + /^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, + "" + ); + markdown += `${statusIcon} Write \`${writeRelativePath}\`\n\n`; + break; + case "Grep": + case "Glob": + const query = input.query || input.pattern || ""; + markdown += `${statusIcon} Search for \`${truncateString(query, 80)}\`\n\n`; + break; + case "LS": + const lsPath = input.path || ""; + const lsRelativePath = lsPath.replace( + /^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, + "" + ); + markdown += `${statusIcon} LS: ${lsRelativePath || lsPath}\n\n`; + break; + default: + // Handle MCP calls and other tools + if (toolName.startsWith("mcp__")) { + const mcpName = formatMcpName(toolName); + const params = formatMcpParameters(input); + markdown += `${statusIcon} ${mcpName}(${params})\n\n`; + } else { + // Generic tool formatting - show the tool name and main parameters + const keys = Object.keys(input); + if (keys.length > 0) { + // Try to find the most important parameter + const mainParam = + keys.find(k => + ["query", "command", "path", "file_path", "content"].includes(k) + ) || keys[0]; + const value = String(input[mainParam] || ""); + if (value) { + markdown += `${statusIcon} ${toolName}: ${truncateString(value, 100)}\n\n`; + } else { + markdown += `${statusIcon} ${toolName}\n\n`; + } + } else { + markdown += `${statusIcon} ${toolName}\n\n`; + } + } + } + return markdown; + } + function formatMcpName(toolName) { + // Convert mcp__github__search_issues to github::search_issues + if (toolName.startsWith("mcp__")) { + const parts = toolName.split("__"); + if (parts.length >= 3) { + const provider = parts[1]; // github, etc. + const method = parts.slice(2).join("_"); // search_issues, etc. + return `${provider}::${method}`; + } + } + return toolName; + } + function formatMcpParameters(input) { + const keys = Object.keys(input); + if (keys.length === 0) return ""; + const paramStrs = []; + for (const key of keys.slice(0, 4)) { + // Show up to 4 parameters + const value = String(input[key] || ""); + paramStrs.push(`${key}: ${truncateString(value, 40)}`); + } + if (keys.length > 4) { + paramStrs.push("..."); + } + return paramStrs.join(", "); + } + function formatBashCommand(command) { + if (!command) return ""; + // Convert multi-line commands to single line by replacing newlines with spaces + // and collapsing multiple spaces + let formatted = command + .replace(/\n/g, " ") // Replace newlines with spaces + .replace(/\r/g, " ") // Replace carriage returns with spaces + .replace(/\t/g, " ") // Replace tabs with spaces + .replace(/\s+/g, " ") // Collapse multiple spaces into one + .trim(); // Remove leading/trailing whitespace + // Escape backticks to prevent markdown issues + formatted = formatted.replace(/`/g, "\\`"); + // Truncate if too long (keep reasonable length for summary) + const maxLength = 80; + if (formatted.length > maxLength) { + formatted = formatted.substring(0, maxLength) + "..."; + } + return formatted; + } + function truncateString(str, maxLength) { + if (!str) return ""; + if (str.length <= maxLength) return str; + return str.substring(0, maxLength) + "..."; + } + // Export for testing + if (typeof module !== "undefined" && module.exports) { + module.exports = { + parseClaudeLog, + formatToolUse, + formatBashCommand, + truncateString, + }; + } + main(); + - name: Upload agent logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-claude-bash-agentic-activity-overview.log + path: /tmp/test-claude-bash-agentic-activity-overview.log + if-no-files-found: warn + diff --git a/.github/workflows/test-claude-bash.md b/.github/workflows/test-claude-bash.md new file mode 100644 index 00000000000..b8341a79f43 --- /dev/null +++ b/.github/workflows/test-claude-bash.md @@ -0,0 +1,24 @@ +--- +engine: claude +on: + workflow_dispatch: +permissions: + contents: read +tools: + bash: + timeout: 300 + allowed: + - "gh aw logs" +--- + +# Test Claude Bash - Agentic Activity Overview + + +Please run `gh aw logs -c 1000` and provide a brief summary of the agentic workflow activity. + +Look at the output and tell me: +1. How many workflow runs you can see +2. Which workflows appear most frequently +3. Any patterns in success/failure rates + +Use the bash tool to run the command and analyze the results. From c5ed9040a8f7d4ec00ca91e1b6ab473cc3bd8060 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Sep 2025 22:30:27 +0000 Subject: [PATCH 7/7] Make bash timeout optional in workflow frontmatter Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/test-claude-bash.lock.yml | 4 +- pkg/parser/schemas/main_workflow_schema.json | 3 +- pkg/workflow/bash_timeout_test.go | 48 ++++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-claude-bash.lock.yml b/.github/workflows/test-claude-bash.lock.yml index 7d08540cdf2..d9a147e1c41 100644 --- a/.github/workflows/test-claude-bash.lock.yml +++ b/.github/workflows/test-claude-bash.lock.yml @@ -223,7 +223,7 @@ jobs: uses: anthropics/claude-code-base-action@v0.0.56 with: # Allowed tools (sorted): - # - Bash(gh aw logs:*) + # - Bash(gh aw logs) # - BashOutput # - ExitPlanMode # - Glob @@ -278,7 +278,7 @@ jobs: # - mcp__github__search_pull_requests # - mcp__github__search_repositories # - mcp__github__search_users - allowed_tools: "Bash(gh aw logs:*),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issues,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_secret_scanning_alerts,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" + allowed_tools: "Bash(gh aw logs),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issues,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_secret_scanning_alerts,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} mcp_config: /tmp/mcp-config/mcp-servers.json prompt_file: /tmp/aw-prompts/prompt.txt diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 31779988fcd..a2b5e4c4f33 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -1031,8 +1031,7 @@ } } }, - "additionalProperties": false, - "required": ["timeout"] + "additionalProperties": false } ] }, diff --git a/pkg/workflow/bash_timeout_test.go b/pkg/workflow/bash_timeout_test.go index 539cabb0821..4a477b31ae9 100644 --- a/pkg/workflow/bash_timeout_test.go +++ b/pkg/workflow/bash_timeout_test.go @@ -61,6 +61,22 @@ func TestBashToolTimeout(t *testing.T) { }, expected: "Bash,BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite", }, + { + name: "bash without timeout should work", + tools: map[string]any{ + "bash": map[string]any{ + "commands": []any{"echo", "ls"}, + }, + }, + expected: "Bash(echo),Bash(ls),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite", + }, + { + name: "bash object without timeout or commands should work", + tools: map[string]any{ + "bash": map[string]any{}, + }, + expected: "Bash,BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite", + }, } for _, tt := range tests { @@ -139,6 +155,38 @@ func TestExpandNeutralToolsWithBashTimeout(t *testing.T) { }, }, }, + { + name: "bash tool without timeout should not include timeout section", + input: map[string]any{ + "bash": map[string]any{ + "commands": []any{"echo", "ls"}, + }, + }, + expected: map[string]any{ + "claude": map[string]any{ + "allowed": map[string]any{ + "Bash": []any{"echo", "ls"}, + }, + }, + }, + }, + { + name: "mixed tools with bash without timeout", + input: map[string]any{ + "bash": map[string]any{ + "allowed": []any{"git"}, + }, + "web-fetch": nil, + }, + expected: map[string]any{ + "claude": map[string]any{ + "allowed": map[string]any{ + "Bash": []any{"git"}, + "WebFetch": nil, + }, + }, + }, + }, } for _, tt := range tests {