From 465532d4286652fb9d991cc4aa070f5a102319a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:31:54 +0000 Subject: [PATCH 1/4] Initial plan From 93b547cf44f7ff665541ea9c1bfabac810dbd400 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:47:56 +0000 Subject: [PATCH 2/4] fix(copilot): set arc-dind HOME before copilot path setup exports Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- pkg/workflow/copilot_engine_execution.go | 6 ++++- pkg/workflow/firewall_args_test.go | 33 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index 17eb9c4b4b6..d56cc8f23d2 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -491,9 +491,13 @@ func (e *CopilotEngine) buildCopilotAWFPathSetup(workflowData *WorkflowData, cus if customCommandScriptSetup != "" { pathSetup = customCommandScriptSetup + "\n" + pathSetup } + homeExport := "" + if isArcDindTopology(workflowData) { + homeExport = fmt.Sprintf("export HOME=%s\n", awfArcDindHomePathExpr) + } // Write the Copilot settings file before AWF starts. The file is created on the host and mounted // into the container, where the Copilot CLI reads it to disable the rubber-duck sub-agent. - return buildCopilotSettingsCleanupAndExitCodeTrap() + buildCopilotSettingsSetup(buildCopilotSettingsContent(workflowData), customCommandScriptSetup != "") + buildCopilotMCPConfigExport(workflowData) + pathSetup + return homeExport + buildCopilotSettingsCleanupAndExitCodeTrap() + buildCopilotSettingsSetup(buildCopilotSettingsContent(workflowData), customCommandScriptSetup != "") + buildCopilotMCPConfigExport(workflowData) + pathSetup } func (e *CopilotEngine) buildCopilotDirectCommand(workflowData *WorkflowData, copilotCommand, customCommandScriptSetup, mkdirCommands, logFile string) string { diff --git a/pkg/workflow/firewall_args_test.go b/pkg/workflow/firewall_args_test.go index 0b5a8b4b6d9..07f3c64dc35 100644 --- a/pkg/workflow/firewall_args_test.go +++ b/pkg/workflow/firewall_args_test.go @@ -293,6 +293,7 @@ func TestFirewallArgsInCopilotEngine(t *testing.T) { RunnerConfig: &RunnerConfig{ Topology: RunnerTopologyArcDind, }, + Tools: map[string]any{"github": map[string]any{}}, NetworkPermissions: &NetworkPermissions{ Firewall: &FirewallConfig{ Enabled: true, @@ -345,6 +346,38 @@ func TestFirewallArgsInCopilotEngine(t *testing.T) { if strings.Contains(stepContent, "/usr/local/bin/copilot") { t.Error("Expected arc-dind command not to reference /usr/local/bin/copilot") } + + homeExport := "export HOME=${RUNNER_TEMP}/gh-aw/home" + firstHomeExportIdx := strings.Index(stepContent, homeExport) + if firstHomeExportIdx < 0 { + t.Fatalf("Expected path setup to export HOME for arc-dind:\n%s", stepContent) + } + if strings.Count(stepContent, homeExport) < 2 { + t.Fatalf("Expected both path-setup and engine-command HOME exports for arc-dind:\n%s", stepContent) + } + + settingsSetupIdx := strings.Index(stepContent, `mkdir -p "$HOME/.copilot"`) + if settingsSetupIdx < 0 { + t.Fatalf("Expected Copilot settings setup to be present:\n%s", stepContent) + } + xdgExportIdx := strings.Index(stepContent, `export XDG_CONFIG_HOME="$HOME"`) + if xdgExportIdx < 0 { + t.Fatalf("Expected XDG_CONFIG_HOME export to be present:\n%s", stepContent) + } + mcpConfigExportIdx := strings.Index(stepContent, `export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json"`) + if mcpConfigExportIdx < 0 { + t.Fatalf("Expected GH_AW_MCP_CONFIG export when MCP is enabled:\n%s", stepContent) + } + + if firstHomeExportIdx > settingsSetupIdx { + t.Fatalf("Expected arc-dind HOME export to run before Copilot settings setup:\n%s", stepContent) + } + if firstHomeExportIdx > xdgExportIdx { + t.Fatalf("Expected arc-dind HOME export to run before XDG_CONFIG_HOME export:\n%s", stepContent) + } + if firstHomeExportIdx > mcpConfigExportIdx { + t.Fatalf("Expected arc-dind HOME export to run before GH_AW_MCP_CONFIG export:\n%s", stepContent) + } }) t.Run("AWF command includes allow-urls with ssl-bump enabled", func(t *testing.T) { From 60a9f64c6ce90c4464f2164cfc8e4693fb366ab3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:37:45 +0000 Subject: [PATCH 3/4] fix(copilot/arc-dind): add HOME export to MCP gateway step for arc-dind+firewall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Start MCP Gateway" step (producer) was writing the Copilot MCP config to $HOME/.copilot/mcp-config.json using the step's original HOME (/home/runner). The Copilot execution step (consumer) already overrides HOME to ${RUNNER_TEMP}/gh-aw/home before evaluating GH_AW_MCP_CONFIG, so it looked for the config at ${RUNNER_TEMP}/gh-aw/home/.copilot/mcp-config.json. GitHub Actions step-local exports do not persist between steps, so each step must independently override HOME. The fix mirrors the condition in buildCopilotAWFPathSetup: emit the HOME export only when both arc-dind topology and firewall are enabled (the only case where the execution step also overrides HOME). Added TestArcDind_ProducerConsumerHomeParity as a regression guard covering the complete step sequence — both the gateway (producer) and execution (consumer) steps — ensuring they both export HOME before $HOME-derived paths are evaluated and that neither embeds a literal /home/runner. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- pkg/workflow/copilot_home_expansion_test.go | 94 +++++++++++++++++++++ pkg/workflow/copilot_mcp.go | 18 ++++ 2 files changed, 112 insertions(+) diff --git a/pkg/workflow/copilot_home_expansion_test.go b/pkg/workflow/copilot_home_expansion_test.go index d24c1f9aa6d..3b575f7b192 100644 --- a/pkg/workflow/copilot_home_expansion_test.go +++ b/pkg/workflow/copilot_home_expansion_test.go @@ -7,12 +7,19 @@ // 1. String-level assertions on the helpers in copilot_mcp.go and // copilot_engine_execution.go to lock the generated snippets so any future // regression flips a focused test rather than only the broader goldens. +// // 2. Bash integration tests that actually execute the generated snippets // under a few HOME values to confirm: // - $HOME expands as expected // - quoting survives a HOME containing spaces and other special chars // - the EXIT trap fires and uses the runtime HOME, not the definition-time HOME // - the rubber-duck settings file is written to the resolved path +// +// 3. ARC/DinD producer-consumer path alignment: the "Start MCP Gateway" step +// (producer) and the Copilot execution step (consumer) must both override +// HOME to the same daemon-visible writable path before any $HOME-derived +// config paths are evaluated, since GitHub Actions step-local exports do +// not persist between steps. package workflow import ( @@ -354,3 +361,90 @@ func TestBashIntegration_RenderMCPConfig_MkdirPath(t *testing.T) { }) } } + +// TestArcDind_ProducerConsumerHomeParity is a regression guard for the ARC/DinD +// producer-consumer HOME path mismatch. +// +// In arc-dind topology, the "Start MCP Gateway" step (producer) writes the Copilot +// MCP config to $HOME/.copilot/mcp-config.json, and the Copilot execution step +// (consumer) reads it via GH_AW_MCP_CONFIG=$HOME/.copilot/mcp-config.json. +// GitHub Actions step-local exports do not persist between steps, so each step must +// independently override HOME to the daemon-visible writable path +// (${RUNNER_TEMP}/gh-aw/home) before any $HOME-derived paths are evaluated. +// Without this, the producer writes to /home/runner/.copilot/mcp-config.json while +// the consumer looks for the file at ${RUNNER_TEMP}/gh-aw/home/.copilot/mcp-config.json. +func TestArcDind_ProducerConsumerHomeParity(t *testing.T) { + arcDindHome := "export HOME=${RUNNER_TEMP}/gh-aw/home" + arcDindMkdir := `mkdir -p "$HOME/.copilot"` + + workflowData := &WorkflowData{ + Name: "arc-dind-home-parity", + EngineConfig: &EngineConfig{ + ID: "copilot", + }, + RunnerConfig: &RunnerConfig{ + Topology: RunnerTopologyArcDind, + }, + Tools: map[string]any{"github": map[string]any{}}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + }, + }, + } + + // --- Producer: "Start MCP Gateway" step --- + // generateMCPGatewaySetup emits the step that calls RenderMCPConfig, + // which writes $HOME/.copilot/mcp-config.json. + ensureDefaultMCPGatewayConfig(workflowData) + var gatewayYAML strings.Builder + err := generateMCPGatewaySetup( + &gatewayYAML, + workflowData.Tools, + []string{"github"}, + NewCopilotEngine(), + workflowData, + false, + nil, + ) + require.NoError(t, err) + gatewayStep := gatewayYAML.String() + + // The producer step must override HOME before creating the .copilot directory. + gatewayHomeIdx := strings.Index(gatewayStep, arcDindHome) + if gatewayHomeIdx < 0 { + t.Fatalf("Start MCP Gateway step must export arc-dind HOME before mkdir:\n%s", gatewayStep) + } + gatewayMkdirIdx := strings.Index(gatewayStep, arcDindMkdir) + if gatewayMkdirIdx < 0 { + t.Fatalf("Start MCP Gateway step must contain mkdir -p \"$HOME/.copilot\":\n%s", gatewayStep) + } + if gatewayHomeIdx > gatewayMkdirIdx { + t.Fatalf("Start MCP Gateway: HOME export must come before mkdir -p $HOME/.copilot:\n%s", gatewayStep) + } + + // --- Consumer: Copilot execution step --- + engine := NewCopilotEngine() + steps := engine.GetExecutionSteps(workflowData, "test.log") + stepContent := requireCopilotExecutionStep(t, steps) + + // The consumer step must also override HOME before GH_AW_MCP_CONFIG is exported. + execHomeIdx := strings.Index(stepContent, arcDindHome) + if execHomeIdx < 0 { + t.Fatalf("Copilot execution step must export arc-dind HOME:\n%s", stepContent) + } + mcpConfigExport := `export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json"` + execMCPConfigIdx := strings.Index(stepContent, mcpConfigExport) + if execMCPConfigIdx < 0 { + t.Fatalf("Copilot execution step must contain GH_AW_MCP_CONFIG export:\n%s", stepContent) + } + if execHomeIdx > execMCPConfigIdx { + t.Fatalf("Copilot execution step: HOME export must come before GH_AW_MCP_CONFIG export:\n%s", stepContent) + } + + // Both steps must use $HOME-based paths (no literal /home/runner). + assert.NotContains(t, gatewayStep, "/home/runner/.copilot", + "Start MCP Gateway step must not embed a literal /home/runner/.copilot") + assert.NotContains(t, stepContent, "/home/runner/.copilot", + "Copilot execution step must not embed a literal /home/runner/.copilot") +} diff --git a/pkg/workflow/copilot_mcp.go b/pkg/workflow/copilot_mcp.go index 7f4f5bac853..a7c54120965 100644 --- a/pkg/workflow/copilot_mcp.go +++ b/pkg/workflow/copilot_mcp.go @@ -1,6 +1,7 @@ package workflow import ( + "fmt" "strings" "github.com/github/gh-aw/pkg/logger" @@ -18,6 +19,23 @@ func copilotMCPToolFilter(toolName string) bool { func (e *CopilotEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]any, mcpTools []string, workflowData *WorkflowData) error { copilotMCPLog.Printf("Rendering MCP config for Copilot engine: mcpTools=%d", len(mcpTools)) + // For ARC/DinD topology with firewall enabled, override HOME to the daemon-visible + // writable path before creating the .copilot config directory. GitHub Actions steps + // do not inherit step-local exports from preceding steps, so each step that uses + // $HOME-based paths must set HOME independently. + // + // The condition mirrors buildCopilotAWFPathSetup in copilot_engine_execution.go, which + // sets HOME only for arc-dind and is only called when the firewall is enabled. This + // ensures the producer (MCP gateway step) and the consumer (Copilot execution step) + // both resolve $HOME to ${RUNNER_TEMP}/gh-aw/home, so they write and read the MCP + // config from the same path: ${RUNNER_TEMP}/gh-aw/home/.copilot/mcp-config.json. + // + // When the firewall is disabled, both steps use the runner's default HOME (/home/runner), + // and no override is needed because they already agree on the path. + if isArcDindTopology(workflowData) && isFirewallEnabled(workflowData) { + fmt.Fprintf(yaml, " export HOME=%s\n", awfArcDindHomePathExpr) + } + // Create the Copilot CLI config directory under the runtime $HOME. The Copilot CLI // resolves its config dir as ~/.copilot, which is /home/runner/.copilot on standard // GitHub-hosted runners but may differ on self-hosted or containerized runners. From 28e8010e9387fd9322d512782c3db340d9de022c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:26:23 +0000 Subject: [PATCH 4/4] fix: add //go:build !integration tag to copilot_home_expansion_test.go The file referenced requireCopilotExecutionStep which is defined in enable_api_proxy_test.go (tagged //go:build !integration). Without the matching tag, verify-integration-build failed with: undefined: requireCopilotExecutionStep Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/copilot_home_expansion_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/workflow/copilot_home_expansion_test.go b/pkg/workflow/copilot_home_expansion_test.go index 3b575f7b192..e132eefb934 100644 --- a/pkg/workflow/copilot_home_expansion_test.go +++ b/pkg/workflow/copilot_home_expansion_test.go @@ -1,3 +1,5 @@ +//go:build !integration + // Tests guarding the $HOME-based shell expansion logic that resolves the // Copilot CLI config directory at runtime (instead of the hard-coded // /home/runner that broke self-hosted and containerized runners).