From 9b1a6f6158dfd32952462446a1be013335f3a079 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:38:05 +0000 Subject: [PATCH 1/8] Serialize engine.command into script for harness execution Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8eeb28ee-0e4d-481b-a4dc-c419dfceadfe Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-ci.lock.yml | 11 ++++- pkg/workflow/copilot_engine_execution.go | 39 ++++++++++++++--- pkg/workflow/copilot_engine_installation.go | 5 +++ pkg/workflow/copilot_engine_test.go | 46 +++++++++++++++++++++ 4 files changed, 94 insertions(+), 7 deletions(-) diff --git a/.github/workflows/smoke-ci.lock.yml b/.github/workflows/smoke-ci.lock.yml index 5dafd403201..564d82f8bfe 100644 --- a/.github/workflows/smoke-ci.lock.yml +++ b/.github/workflows/smoke-ci.lock.yml @@ -412,6 +412,8 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.24 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -695,13 +697,20 @@ jobs: timeout-minutes: 5 run: | set -o pipefail + mkdir -p /tmp/gh-aw + cat <<'GH_AW_ENGINE_COMMAND_EOF' > /tmp/gh-aw/engine-command.sh + #!/usr/bin/env bash + set -euo pipefail + bash -lc 'if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then safeoutputs add_comment --body "✅ smoke-ci: safeoutputs CLI comment only run (${GITHUB_RUN_ID})"; else echo "smoke-ci: push event - no PR context, skipping add_comment"; fi' + GH_AW_ENGINE_COMMAND_EOF + chmod +x /tmp/gh-aw/engine-command.sh touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN (umask 177 && touch /tmp/gh-aw/agent-stdio.log) # shellcheck disable=SC1003 sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.24 --skip-pull --enable-api-proxy \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && ${GH_AW_NODE_BIN:-node} ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs bash -lc '\''if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then safeoutputs add_comment --body "✅ smoke-ci: safeoutputs CLI comment only run (${GITHUB_RUN_ID})"; else echo "smoke-ci: push event - no PR context, skipping add_comment"; fi'\'' --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && ${GH_AW_NODE_BIN:-node} ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /tmp/gh-aw/engine-command.sh --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index 7a94d3ecec9..d5a0fa16e2b 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -35,6 +35,8 @@ import ( var copilotExecLog = logger.New("workflow:copilot_engine_execution") +const customEngineCommandScriptPath = "/tmp/gh-aw/engine-command.sh" + // GetExecutionSteps returns the GitHub Actions steps for executing GitHub Copilot CLI func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile string) []GitHubActionStep { copilotExecLog.Printf("Generating execution steps for Copilot: workflow=%s, firewall=%v", workflowData.Name, isFirewallEnabled(workflowData)) @@ -162,9 +164,11 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st // Determine which command to use (once for both sandbox and non-sandbox modes) var commandName string + var customCommandScriptSetup string if workflowData.EngineConfig != nil && workflowData.EngineConfig.Command != "" { - commandName = workflowData.EngineConfig.Command - copilotExecLog.Printf("Using custom command: %s", commandName) + commandName = customEngineCommandScriptPath + customCommandScriptSetup = buildEngineCommandScriptSetup(workflowData.EngineConfig.Command) + copilotExecLog.Printf("Using serialized custom command script: %s", commandName) } else if sandboxEnabled { // AWF - use the installed binary directly // The binary is mounted into the AWF container from /usr/local/bin/copilot @@ -241,6 +245,12 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st if mcpCLIPath := GetMCPCLIPathSetup(workflowData); mcpCLIPath != "" { engineCommand = fmt.Sprintf("%s && %s", mcpCLIPath, copilotCommand) } + pathSetup := "touch " + AgentStepSummaryPath + "\n" + + "GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true)\n" + + "export GH_AW_NODE_BIN" + if customCommandScriptSetup != "" { + pathSetup = customCommandScriptSetup + "\n" + pathSetup + } command = BuildAWFCommand(AWFCommandConfig{ EngineName: "copilot", EngineCommand: engineCommand, @@ -258,9 +268,7 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st // the path here (where PATH is still intact) and exporting it, sudo -E // preserves the variable and AWF's --env-all forwards it into the container, // where ${GH_AW_NODE_BIN:-node} resolves to the correct binary. - PathSetup: "touch " + AgentStepSummaryPath + "\n" + - "GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true)\n" + - "export GH_AW_NODE_BIN", + PathSetup: pathSetup, // Exclude every env var whose step-env value is a secret so the agent // cannot read raw token values via bash tools (env / printenv). ExcludeEnvVarNames: ComputeAWFExcludeEnvVarNames(workflowData, []string{"COPILOT_GITHUB_TOKEN"}), @@ -268,10 +276,14 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st } else { // Run copilot command without AWF wrapper. // Prepend a touch command to create the agent step summary file before copilot runs. + preCommandSetup := mkdirCommands.String() + if customCommandScriptSetup != "" { + preCommandSetup = customCommandScriptSetup + "\n" + preCommandSetup + } command = fmt.Sprintf(`set -o pipefail touch %s (umask 177 && touch %s) -%s%s 2>&1 | tee %s`, AgentStepSummaryPath, logFile, mkdirCommands.String(), copilotCommand, logFile) +%s%s 2>&1 | tee %s`, AgentStepSummaryPath, logFile, preCommandSetup, copilotCommand, logFile) } // Use COPILOT_GITHUB_TOKEN: when the copilot-requests feature is enabled, use the GitHub @@ -543,6 +555,21 @@ func extractAddDirPaths(args []string) []string { return dirs } +func buildEngineCommandScriptSetup(command string) string { + delimiter := "GH_AW_ENGINE_COMMAND_EOF" + if strings.Contains(command, delimiter) { + delimiter = "GH_AW_ENGINE_COMMAND_PAYLOAD_EOF" + } + + return fmt.Sprintf(`mkdir -p /tmp/gh-aw +cat <<'%s' > %s +#!/usr/bin/env bash +set -euo pipefail +%s +%s +chmod +x %s`, delimiter, customEngineCommandScriptPath, command, delimiter, customEngineCommandScriptPath) +} + // generateCopilotSessionFileCopyStep generates a step to copy the entire Copilot // session-state directory from ~/.copilot/session-state/ to /tmp/gh-aw/sandbox/agent/logs/ // This ensures all session files (events.jsonl, session.db, plan.md, checkpoints, etc.) diff --git a/pkg/workflow/copilot_engine_installation.go b/pkg/workflow/copilot_engine_installation.go index c1b064b9b79..69ed50edf05 100644 --- a/pkg/workflow/copilot_engine_installation.go +++ b/pkg/workflow/copilot_engine_installation.go @@ -54,6 +54,11 @@ func (e *CopilotEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHu // Skip installation if custom command is specified if workflowData.EngineConfig != nil && workflowData.EngineConfig.Command != "" { copilotInstallLog.Printf("Skipping installation steps: custom command specified (%s)", workflowData.EngineConfig.Command) + // Keep firewall runtime installation when firewall is enabled, since the + // custom engine command still runs inside the AWF harness. + if isFirewallEnabled(workflowData) { + return BuildNpmEngineInstallStepsWithAWF([]GitHubActionStep{}, workflowData) + } return []GitHubActionStep{} } diff --git a/pkg/workflow/copilot_engine_test.go b/pkg/workflow/copilot_engine_test.go index 4afab9261ad..6d49b68f3a8 100644 --- a/pkg/workflow/copilot_engine_test.go +++ b/pkg/workflow/copilot_engine_test.go @@ -1568,6 +1568,24 @@ func TestCopilotEngineSkipInstallationWithCommand(t *testing.T) { if len(steps) != 0 { t.Errorf("Expected 0 installation steps when command is specified, got %d", len(steps)) } + + // Test with custom command + firewall - should still install AWF runtime + workflowData = &WorkflowData{ + EngineConfig: &EngineConfig{Command: "/usr/local/bin/custom-copilot"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + } + steps = engine.GetInstallationSteps(workflowData) + + if len(steps) == 0 { + t.Fatal("Expected installation steps when firewall is enabled with custom command") + } + + installContent := strings.Join([]string(steps[0]), "\n") + if !strings.Contains(installContent, "Install AWF binary") { + t.Errorf("Expected AWF installation step when firewall is enabled with custom command, got:\n%s", installContent) + } } // TestGenerateCopilotSessionFileCopyStep verifies the generated step copies session state files. @@ -1711,6 +1729,34 @@ func TestCopilotEngineDriverScript(t *testing.T) { t.Run("CopilotEngine implements DriverProvider interface", func(t *testing.T) { var _ DriverProvider = engine }) + + t.Run("Execution serializes engine.command into shell script", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + ID: "copilot", + Command: `bash -lc 'echo custom command'`, + }, + Tools: make(map[string]any), + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/agent-stdio.log") + if len(steps) == 0 { + t.Fatal("Expected at least one step") + } + + stepContent := strings.Join([]string(steps[0]), "\n") + + if !strings.Contains(stepContent, "copilot_driver.cjs /tmp/gh-aw/engine-command.sh") { + t.Errorf("Expected driver to run serialized engine command script, got:\n%s", stepContent) + } + if !strings.Contains(stepContent, "cat <<'GH_AW_ENGINE_COMMAND_EOF' > /tmp/gh-aw/engine-command.sh") { + t.Errorf("Expected step to serialize engine.command into script, got:\n%s", stepContent) + } + if !strings.Contains(stepContent, "bash -lc 'echo custom command'") { + t.Errorf("Expected serialized script to contain original engine.command, got:\n%s", stepContent) + } + }) } func TestCopilotEngineNoAskUser(t *testing.T) { From 83ca61a7aea79b9671b43f8f8fb16d6da34df92b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:55:34 +0000 Subject: [PATCH 2/8] Harden engine.command script serialization against heredoc collision Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8eeb28ee-0e4d-481b-a4dc-c419dfceadfe Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-ci.lock.yml | 6 +----- pkg/workflow/copilot_engine_execution.go | 15 +++++---------- pkg/workflow/copilot_engine_test.go | 8 ++++---- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/.github/workflows/smoke-ci.lock.yml b/.github/workflows/smoke-ci.lock.yml index 564d82f8bfe..0b22b775427 100644 --- a/.github/workflows/smoke-ci.lock.yml +++ b/.github/workflows/smoke-ci.lock.yml @@ -698,11 +698,7 @@ jobs: run: | set -o pipefail mkdir -p /tmp/gh-aw - cat <<'GH_AW_ENGINE_COMMAND_EOF' > /tmp/gh-aw/engine-command.sh - #!/usr/bin/env bash - set -euo pipefail - bash -lc 'if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then safeoutputs add_comment --body "✅ smoke-ci: safeoutputs CLI comment only run (${GITHUB_RUN_ID})"; else echo "smoke-ci: push event - no PR context, skipping add_comment"; fi' - GH_AW_ENGINE_COMMAND_EOF + printf IyEvdXNyL2Jpbi9lbnYgYmFzaApzZXQgLWV1byBwaXBlZmFpbApiYXNoIC1sYyAnaWYgWyAiJHtHSVRIVUJfRVZFTlRfTkFNRX0iID0gInB1bGxfcmVxdWVzdCIgXTsgdGhlbiBzYWZlb3V0cHV0cyBhZGRfY29tbWVudCAtLWJvZHkgIuKchSBzbW9rZS1jaTogc2FmZW91dHB1dHMgQ0xJIGNvbW1lbnQgb25seSBydW4gKCR7R0lUSFVCX1JVTl9JRH0pIjsgZWxzZSBlY2hvICJzbW9rZS1jaTogcHVzaCBldmVudCAtIG5vIFBSIGNvbnRleHQsIHNraXBwaW5nIGFkZF9jb21tZW50IjsgZmknCg== | base64 --decode > /tmp/gh-aw/engine-command.sh chmod +x /tmp/gh-aw/engine-command.sh touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index d5a0fa16e2b..e9a2d9f1cd5 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -22,6 +22,7 @@ package workflow import ( + "encoding/base64" "fmt" "maps" "strconv" @@ -556,18 +557,12 @@ func extractAddDirPaths(args []string) []string { } func buildEngineCommandScriptSetup(command string) string { - delimiter := "GH_AW_ENGINE_COMMAND_EOF" - if strings.Contains(command, delimiter) { - delimiter = "GH_AW_ENGINE_COMMAND_PAYLOAD_EOF" - } + scriptContent := fmt.Sprintf("#!/usr/bin/env bash\nset -euo pipefail\n%s\n", command) + scriptContentBase64 := base64.StdEncoding.EncodeToString([]byte(scriptContent)) return fmt.Sprintf(`mkdir -p /tmp/gh-aw -cat <<'%s' > %s -#!/usr/bin/env bash -set -euo pipefail -%s -%s -chmod +x %s`, delimiter, customEngineCommandScriptPath, command, delimiter, customEngineCommandScriptPath) +printf %s | base64 --decode > %s +chmod +x %s`, shellEscapeArg(scriptContentBase64), customEngineCommandScriptPath, customEngineCommandScriptPath) } // generateCopilotSessionFileCopyStep generates a step to copy the entire Copilot diff --git a/pkg/workflow/copilot_engine_test.go b/pkg/workflow/copilot_engine_test.go index 6d49b68f3a8..1af1318903a 100644 --- a/pkg/workflow/copilot_engine_test.go +++ b/pkg/workflow/copilot_engine_test.go @@ -1750,11 +1750,11 @@ func TestCopilotEngineDriverScript(t *testing.T) { if !strings.Contains(stepContent, "copilot_driver.cjs /tmp/gh-aw/engine-command.sh") { t.Errorf("Expected driver to run serialized engine command script, got:\n%s", stepContent) } - if !strings.Contains(stepContent, "cat <<'GH_AW_ENGINE_COMMAND_EOF' > /tmp/gh-aw/engine-command.sh") { - t.Errorf("Expected step to serialize engine.command into script, got:\n%s", stepContent) + if !strings.Contains(stepContent, "base64 --decode > /tmp/gh-aw/engine-command.sh") { + t.Errorf("Expected step to serialize engine.command into script via base64 decode, got:\n%s", stepContent) } - if !strings.Contains(stepContent, "bash -lc 'echo custom command'") { - t.Errorf("Expected serialized script to contain original engine.command, got:\n%s", stepContent) + if !strings.Contains(stepContent, "printf ") { + t.Errorf("Expected script serialization to emit encoded script content, got:\n%s", stepContent) } }) } From 6035630ac635f5d51257323233b80339cc08278a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:16:44 +0000 Subject: [PATCH 3/8] Plan: review and address PR comments Agent-Logs-Url: https://github.com/github/gh-aw/sessions/c6153179-8998-4a0d-8638-f643712a0ed0 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-ci.lock.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/smoke-ci.lock.yml b/.github/workflows/smoke-ci.lock.yml index 8d0d24b0a9e..b80e8d44ebf 100644 --- a/.github/workflows/smoke-ci.lock.yml +++ b/.github/workflows/smoke-ci.lock.yml @@ -1144,3 +1144,4 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore + From a89b304ed8f2700e1d512669bf0795cb9777c9cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:23:04 +0000 Subject: [PATCH 4/8] Address PR review feedback for custom engine command script Agent-Logs-Url: https://github.com/github/gh-aw/sessions/c6153179-8998-4a0d-8638-f643712a0ed0 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/copilot_engine_execution.go | 5 +-- pkg/workflow/copilot_engine_installation.go | 10 +++--- pkg/workflow/copilot_engine_test.go | 39 +++++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index 5275ad1fea6..a69522cadb4 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -558,12 +558,13 @@ func extractAddDirPaths(args []string) []string { } func buildEngineCommandScriptSetup(command string) string { - scriptContent := fmt.Sprintf("#!/usr/bin/env bash\nset -euo pipefail\n%s\n", command) + scriptContent := fmt.Sprintf("#!/usr/bin/env bash\nset -eo pipefail\n%s \"$@\"\n", command) scriptContentBase64 := base64.StdEncoding.EncodeToString([]byte(scriptContent)) return fmt.Sprintf(`mkdir -p /tmp/gh-aw +umask 0177 printf %s | base64 --decode > %s -chmod +x %s`, shellEscapeArg(scriptContentBase64), customEngineCommandScriptPath, customEngineCommandScriptPath) +chmod 700 %s`, shellEscapeArg(scriptContentBase64), customEngineCommandScriptPath, customEngineCommandScriptPath) } // generateCopilotSessionFileCopyStep generates a step to copy the entire Copilot diff --git a/pkg/workflow/copilot_engine_installation.go b/pkg/workflow/copilot_engine_installation.go index 69ed50edf05..1b13a8ad4b1 100644 --- a/pkg/workflow/copilot_engine_installation.go +++ b/pkg/workflow/copilot_engine_installation.go @@ -46,19 +46,21 @@ func (e *CopilotEngine) GetSecretValidationStep(workflowData *WorkflowData) GitH // 2. Sandbox installation (AWF, if needed) // 3. Copilot CLI installation // -// If a custom command is specified in the engine configuration, this function returns -// an empty list of steps, skipping the standard installation process. +// If a custom command is specified in the engine configuration, this function skips +// standard Copilot CLI installation. When firewall is enabled, it still returns AWF +// runtime installation steps required for harness execution. func (e *CopilotEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHubActionStep { copilotInstallLog.Printf("Generating installation steps for Copilot engine: workflow=%s", workflowData.Name) - // Skip installation if custom command is specified + // Skip standard Copilot CLI installation if custom command is specified. if workflowData.EngineConfig != nil && workflowData.EngineConfig.Command != "" { - copilotInstallLog.Printf("Skipping installation steps: custom command specified (%s)", workflowData.EngineConfig.Command) // Keep firewall runtime installation when firewall is enabled, since the // custom engine command still runs inside the AWF harness. if isFirewallEnabled(workflowData) { + copilotInstallLog.Printf("Skipping Copilot CLI installation: custom command specified (%s); keeping AWF runtime installation because firewall is enabled", workflowData.EngineConfig.Command) return BuildNpmEngineInstallStepsWithAWF([]GitHubActionStep{}, workflowData) } + copilotInstallLog.Printf("Skipping installation steps: custom command specified (%s)", workflowData.EngineConfig.Command) return []GitHubActionStep{} } diff --git a/pkg/workflow/copilot_engine_test.go b/pkg/workflow/copilot_engine_test.go index f24bdbb9350..20c08d51ae0 100644 --- a/pkg/workflow/copilot_engine_test.go +++ b/pkg/workflow/copilot_engine_test.go @@ -3,6 +3,7 @@ package workflow import ( + "encoding/base64" "os" "path/filepath" "strings" @@ -1856,6 +1857,44 @@ func TestCopilotEngineNoAskUser(t *testing.T) { } } +func TestBuildEngineCommandScriptSetup(t *testing.T) { + setup := buildEngineCommandScriptSetup("/usr/local/bin/custom-copilot") + + if !strings.Contains(setup, "umask 0177") { + t.Fatalf("Expected restrictive umask in script setup, got:\n%s", setup) + } + if !strings.Contains(setup, "chmod 700 /tmp/gh-aw/engine-command.sh") { + t.Fatalf("Expected owner-only execute permissions, got:\n%s", setup) + } + + const prefix = "printf " + const suffix = " | base64 --decode" + start := strings.Index(setup, prefix) + end := strings.Index(setup, suffix) + if start == -1 || end == -1 || end <= start+len(prefix) { + t.Fatalf("Expected base64-encoded script payload in setup, got:\n%s", setup) + } + + encoded := strings.TrimSpace(setup[start+len(prefix) : end]) + encoded = strings.Trim(encoded, "'") + + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + t.Fatalf("Failed to decode serialized script payload: %v", err) + } + + script := string(decoded) + if !strings.Contains(script, "set -eo pipefail") { + t.Fatalf("Expected script strict mode without -u, got:\n%s", script) + } + if strings.Contains(script, "set -euo pipefail") { + t.Fatalf("Expected script strict mode to drop -u, got:\n%s", script) + } + if !strings.Contains(script, `/usr/local/bin/custom-copilot "$@"`) { + t.Fatalf("Expected custom command to forward driver args, got:\n%s", script) + } +} + func TestCopilotSupportsNoAskUser(t *testing.T) { tests := []struct { name string From 2af47ad49722bd82558cab14768bc06b2791fc48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:30:52 +0000 Subject: [PATCH 5/8] Harden engine-command script test parsing and intent comments Agent-Logs-Url: https://github.com/github/gh-aw/sessions/c6153179-8998-4a0d-8638-f643712a0ed0 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/copilot_engine_execution.go | 2 ++ pkg/workflow/copilot_engine_test.go | 12 +++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index a69522cadb4..c64e205f2b6 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -558,6 +558,8 @@ func extractAddDirPaths(args []string) []string { } func buildEngineCommandScriptSetup(command string) string { + // engine.command intentionally accepts shell-form commands from trusted workflow + // configuration authored in-repo; preserve shell semantics and forward driver args. scriptContent := fmt.Sprintf("#!/usr/bin/env bash\nset -eo pipefail\n%s \"$@\"\n", command) scriptContentBase64 := base64.StdEncoding.EncodeToString([]byte(scriptContent)) diff --git a/pkg/workflow/copilot_engine_test.go b/pkg/workflow/copilot_engine_test.go index 20c08d51ae0..1e01d475cab 100644 --- a/pkg/workflow/copilot_engine_test.go +++ b/pkg/workflow/copilot_engine_test.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "os" "path/filepath" + "regexp" "strings" "testing" @@ -1867,16 +1868,13 @@ func TestBuildEngineCommandScriptSetup(t *testing.T) { t.Fatalf("Expected owner-only execute permissions, got:\n%s", setup) } - const prefix = "printf " - const suffix = " | base64 --decode" - start := strings.Index(setup, prefix) - end := strings.Index(setup, suffix) - if start == -1 || end == -1 || end <= start+len(prefix) { + re := regexp.MustCompile(`(?m)^printf ('[^']+'|"[^"]+"|[^[:space:]]+) \| base64 --decode`) + matches := re.FindStringSubmatch(setup) + if len(matches) < 2 { t.Fatalf("Expected base64-encoded script payload in setup, got:\n%s", setup) } - encoded := strings.TrimSpace(setup[start+len(prefix) : end]) - encoded = strings.Trim(encoded, "'") + encoded := strings.Trim(matches[1], `'"`) decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { From 0548f89968a6355e1ed46883aa7fbecb9981e4c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:40:15 +0000 Subject: [PATCH 6/8] Plan: address heredoc review feedback Agent-Logs-Url: https://github.com/github/gh-aw/sessions/617dee35-e9f3-40fd-87f4-b1eeb3c1fca7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-ci.lock.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke-ci.lock.yml b/.github/workflows/smoke-ci.lock.yml index b80e8d44ebf..0a582734046 100644 --- a/.github/workflows/smoke-ci.lock.yml +++ b/.github/workflows/smoke-ci.lock.yml @@ -698,8 +698,9 @@ jobs: run: | set -o pipefail mkdir -p /tmp/gh-aw - printf IyEvdXNyL2Jpbi9lbnYgYmFzaApzZXQgLWV1byBwaXBlZmFpbApiYXNoIC1sYyAnaWYgWyAiJHtHSVRIVUJfRVZFTlRfTkFNRX0iID0gInB1bGxfcmVxdWVzdCIgXTsgdGhlbiBzYWZlb3V0cHV0cyBhZGRfY29tbWVudCAtLWJvZHkgIuKchSBzbW9rZS1jaTogc2FmZW91dHB1dHMgQ0xJIGNvbW1lbnQgb25seSBydW4gKCR7R0lUSFVCX1JVTl9JRH0pIjsgZWxzZSBlY2hvICJzbW9rZS1jaTogcHVzaCBldmVudCAtIG5vIFBSIGNvbnRleHQsIHNraXBwaW5nIGFkZF9jb21tZW50IjsgZmknCg== | base64 --decode > /tmp/gh-aw/engine-command.sh - chmod +x /tmp/gh-aw/engine-command.sh + umask 0177 + printf IyEvdXNyL2Jpbi9lbnYgYmFzaApzZXQgLWVvIHBpcGVmYWlsCmJhc2ggLWxjICdpZiBbICIke0dJVEhVQl9FVkVOVF9OQU1FfSIgPSAicHVsbF9yZXF1ZXN0IiBdOyB0aGVuIHNhZmVvdXRwdXRzIGFkZF9jb21tZW50IC0tYm9keSAi4pyFIHNtb2tlLWNpOiBzYWZlb3V0cHV0cyBDTEkgY29tbWVudCBvbmx5IHJ1biAoJHtHSVRIVUJfUlVOX0lEfSkiOyBlbHNlIGVjaG8gInNtb2tlLWNpOiBwdXNoIGV2ZW50IC0gbm8gUFIgY29udGV4dCwgc2tpcHBpbmcgYWRkX2NvbW1lbnQiOyBmaScgIiRAIgo= | base64 --decode > /tmp/gh-aw/engine-command.sh + chmod 700 /tmp/gh-aw/engine-command.sh touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN From da653f0f3ab3a2c56a8d5013dc907aab139363aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:52:09 +0000 Subject: [PATCH 7/8] Update; rm -rf / Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-ci.lock.yml | 6 +++- pkg/workflow/copilot_engine_execution.go | 11 ++++--- pkg/workflow/copilot_engine_test.go | 38 ++++++++---------------- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/.github/workflows/smoke-ci.lock.yml b/.github/workflows/smoke-ci.lock.yml index 0a582734046..ecc323bf591 100644 --- a/.github/workflows/smoke-ci.lock.yml +++ b/.github/workflows/smoke-ci.lock.yml @@ -699,7 +699,11 @@ jobs: set -o pipefail mkdir -p /tmp/gh-aw umask 0177 - printf IyEvdXNyL2Jpbi9lbnYgYmFzaApzZXQgLWVvIHBpcGVmYWlsCmJhc2ggLWxjICdpZiBbICIke0dJVEhVQl9FVkVOVF9OQU1FfSIgPSAicHVsbF9yZXF1ZXN0IiBdOyB0aGVuIHNhZmVvdXRwdXRzIGFkZF9jb21tZW50IC0tYm9keSAi4pyFIHNtb2tlLWNpOiBzYWZlb3V0cHV0cyBDTEkgY29tbWVudCBvbmx5IHJ1biAoJHtHSVRIVUJfUlVOX0lEfSkiOyBlbHNlIGVjaG8gInNtb2tlLWNpOiBwdXNoIGV2ZW50IC0gbm8gUFIgY29udGV4dCwgc2tpcHBpbmcgYWRkX2NvbW1lbnQiOyBmaScgIiRAIgo= | base64 --decode > /tmp/gh-aw/engine-command.sh + cat > /tmp/gh-aw/engine-command.sh <<'GH_AW_ENGINE_COMMAND_EOF' + #!/usr/bin/env bash + set -eo pipefail + bash -lc 'if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then safeoutputs add_comment --body "✅ smoke-ci: safeoutputs CLI comment only run (${GITHUB_RUN_ID})"; else echo "smoke-ci: push event - no PR context, skipping add_comment"; fi' "$@" + GH_AW_ENGINE_COMMAND_EOF chmod 700 /tmp/gh-aw/engine-command.sh touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index c64e205f2b6..2798dfccb97 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -22,7 +22,6 @@ package workflow import ( - "encoding/base64" "fmt" "maps" "strconv" @@ -561,12 +560,16 @@ func buildEngineCommandScriptSetup(command string) string { // engine.command intentionally accepts shell-form commands from trusted workflow // configuration authored in-repo; preserve shell semantics and forward driver args. scriptContent := fmt.Sprintf("#!/usr/bin/env bash\nset -eo pipefail\n%s \"$@\"\n", command) - scriptContentBase64 := base64.StdEncoding.EncodeToString([]byte(scriptContent)) + heredocDelimiter := "GH_AW_ENGINE_COMMAND_EOF" + for strings.Contains(scriptContent, heredocDelimiter) { + heredocDelimiter += "_X" + } return fmt.Sprintf(`mkdir -p /tmp/gh-aw umask 0177 -printf %s | base64 --decode > %s -chmod 700 %s`, shellEscapeArg(scriptContentBase64), customEngineCommandScriptPath, customEngineCommandScriptPath) +cat > %s <<'%s' +%s%s +chmod 700 %s`, customEngineCommandScriptPath, heredocDelimiter, scriptContent, heredocDelimiter, customEngineCommandScriptPath) } // generateCopilotSessionFileCopyStep generates a step to copy the entire Copilot diff --git a/pkg/workflow/copilot_engine_test.go b/pkg/workflow/copilot_engine_test.go index 1e01d475cab..b1c0db64da2 100644 --- a/pkg/workflow/copilot_engine_test.go +++ b/pkg/workflow/copilot_engine_test.go @@ -3,10 +3,8 @@ package workflow import ( - "encoding/base64" "os" "path/filepath" - "regexp" "strings" "testing" @@ -1755,11 +1753,11 @@ func TestCopilotEngineDriverScript(t *testing.T) { if !strings.Contains(stepContent, "copilot_driver.cjs /tmp/gh-aw/engine-command.sh") { t.Errorf("Expected driver to run serialized engine command script, got:\n%s", stepContent) } - if !strings.Contains(stepContent, "base64 --decode > /tmp/gh-aw/engine-command.sh") { - t.Errorf("Expected step to serialize engine.command into script via base64 decode, got:\n%s", stepContent) + if !strings.Contains(stepContent, "cat > /tmp/gh-aw/engine-command.sh <<'GH_AW_ENGINE_COMMAND_EOF'") { + t.Errorf("Expected step to serialize engine.command into script via heredoc, got:\n%s", stepContent) } - if !strings.Contains(stepContent, "printf ") { - t.Errorf("Expected script serialization to emit encoded script content, got:\n%s", stepContent) + if !strings.Contains(stepContent, "GH_AW_ENGINE_COMMAND_EOF") { + t.Errorf("Expected step to include heredoc delimiter for script serialization, got:\n%s", stepContent) } }) } @@ -1867,29 +1865,17 @@ func TestBuildEngineCommandScriptSetup(t *testing.T) { if !strings.Contains(setup, "chmod 700 /tmp/gh-aw/engine-command.sh") { t.Fatalf("Expected owner-only execute permissions, got:\n%s", setup) } - - re := regexp.MustCompile(`(?m)^printf ('[^']+'|"[^"]+"|[^[:space:]]+) \| base64 --decode`) - matches := re.FindStringSubmatch(setup) - if len(matches) < 2 { - t.Fatalf("Expected base64-encoded script payload in setup, got:\n%s", setup) - } - - encoded := strings.Trim(matches[1], `'"`) - - decoded, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - t.Fatalf("Failed to decode serialized script payload: %v", err) + if !strings.Contains(setup, "cat > /tmp/gh-aw/engine-command.sh <<'GH_AW_ENGINE_COMMAND_EOF'") { + t.Fatalf("Expected heredoc-based script materialization, got:\n%s", setup) } - - script := string(decoded) - if !strings.Contains(script, "set -eo pipefail") { - t.Fatalf("Expected script strict mode without -u, got:\n%s", script) + if !strings.Contains(setup, "set -eo pipefail") { + t.Fatalf("Expected script strict mode without -u, got:\n%s", setup) } - if strings.Contains(script, "set -euo pipefail") { - t.Fatalf("Expected script strict mode to drop -u, got:\n%s", script) + if strings.Contains(setup, "set -euo pipefail") { + t.Fatalf("Expected script strict mode to drop -u, got:\n%s", setup) } - if !strings.Contains(script, `/usr/local/bin/custom-copilot "$@"`) { - t.Fatalf("Expected custom command to forward driver args, got:\n%s", script) + if !strings.Contains(setup, `/usr/local/bin/custom-copilot "$@"`) { + t.Fatalf("Expected custom command to forward driver args, got:\n%s", setup) } } From 4b99ebbd633aa683e9dce49389859904bcbcbb94 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:54:14 +0000 Subject: [PATCH 8/8] Use heredoc for engine command script serialization Agent-Logs-Url: https://github.com/github/gh-aw/sessions/617dee35-e9f3-40fd-87f4-b1eeb3c1fca7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-ci.lock.yml | 1 + pkg/workflow/copilot_engine_execution.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/smoke-ci.lock.yml b/.github/workflows/smoke-ci.lock.yml index ecc323bf591..fa390f3a1ce 100644 --- a/.github/workflows/smoke-ci.lock.yml +++ b/.github/workflows/smoke-ci.lock.yml @@ -703,6 +703,7 @@ jobs: #!/usr/bin/env bash set -eo pipefail bash -lc 'if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then safeoutputs add_comment --body "✅ smoke-ci: safeoutputs CLI comment only run (${GITHUB_RUN_ID})"; else echo "smoke-ci: push event - no PR context, skipping add_comment"; fi' "$@" + GH_AW_ENGINE_COMMAND_EOF chmod 700 /tmp/gh-aw/engine-command.sh touch /tmp/gh-aw/agent-step-summary.md diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index 2798dfccb97..68d000c29c2 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -568,7 +568,8 @@ func buildEngineCommandScriptSetup(command string) string { return fmt.Sprintf(`mkdir -p /tmp/gh-aw umask 0177 cat > %s <<'%s' -%s%s +%s +%s chmod 700 %s`, customEngineCommandScriptPath, heredocDelimiter, scriptContent, heredocDelimiter, customEngineCommandScriptPath) }