From f6992c4e05a0c64386bf09eed5606181ff7f8428 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 04:42:10 +0000 Subject: [PATCH 1/3] Initial plan From 078542e8b7c82da5ea9e45e9e75b4624493d9c83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 04:56:08 +0000 Subject: [PATCH 2/3] BYOK: Skip COPILOT_GITHUB_TOKEN injection when COPILOT_PROVIDER_BASE_URL is set When engine.env contains COPILOT_PROVIDER_BASE_URL (BYOK mode), the Copilot execution step no longer injects COPILOT_GITHUB_TOKEN into the step environment. Previously the token was unconditionally set and could leak to the user's external provider via the api-proxy's fallback ****** causing HTTP 401 errors and constituting a credential-leak risk. Changes: - Detect isBYOKMode via engineEnvHasKey(COPILOT_PROVIDER_BASE_URL) at the top of GetExecutionSteps - Skip COPILOT_GITHUB_TOKEN injection when isBYOKMode is true - Remove COPILOT_GITHUB_TOKEN from AWF --exclude-env list in BYOK mode (nothing to hide when the token is not present) - Add TestCopilotEngineBYOKOmitsCopilotGitHubToken with three sub-tests: token absent with URL+key, token absent with URL only, token present in standard mode Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/copilot_engine_execution.go | 33 +++++++++-- pkg/workflow/copilot_engine_test.go | 72 ++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 5 deletions(-) diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index fdf688d0cd2..5b16acac238 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -47,6 +47,11 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st // Build copilot CLI arguments based on configuration var copilotArgs []string sandboxEnabled := isFirewallEnabled(workflowData) + // isBYOKMode is true when the user has set COPILOT_PROVIDER_BASE_URL in engine.env, + // which routes Copilot requests to a non-GitHub provider. In that mode the GitHub + // identity token (COPILOT_GITHUB_TOKEN) must NOT be injected into the step env: + // forwarding it to a third-party host would be a credential leak. + isBYOKMode := engineEnvHasKey(workflowData, constants.CopilotProviderBaseURL) if sandboxEnabled { // Simplified args for sandbox mode (AWF) copilotArgs = []string{"--add-dir", "/tmp/gh-aw/", "--log-level", "all", "--log-dir", logsFolder} @@ -274,6 +279,14 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st if customCommandScriptSetup != "" { pathSetup = customCommandScriptSetup + "\n" + pathSetup } + // Build the list of core secret var names to hide from the agent shell tools. + // In BYOK mode COPILOT_GITHUB_TOKEN is not injected into the step env at all, + // so there is nothing to exclude. Excluding it unconditionally would produce + // spurious --exclude-env flags when the token is absent. + var copilotCoreSecrets []string + if !isBYOKMode { + copilotCoreSecrets = []string{"COPILOT_GITHUB_TOKEN"} + } command = BuildAWFCommand(AWFCommandConfig{ EngineName: "copilot", EngineCommand: engineCommand, @@ -295,7 +308,7 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st 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"}), + ExcludeEnvVarNames: ComputeAWFExcludeEnvVarNames(workflowData, copilotCoreSecrets), }) } else { // Run copilot command without AWF wrapper. @@ -311,14 +324,19 @@ touch %s %s%s 2>&1 | tee %s`, AgentCLIStartMsPath, AgentStepSummaryPath, logFile, preCommandSetup, copilotCommand, logFile) } - // Use COPILOT_GITHUB_TOKEN: when the copilot-requests feature is enabled, use the GitHub - // Actions token directly (${{ github.token }}). Otherwise use the COPILOT_GITHUB_TOKEN secret. + // COPILOT_GITHUB_TOKEN injection: in BYOK mode (COPILOT_PROVIDER_BASE_URL set), skip + // this entirely. The request goes to a third-party provider; forwarding the GitHub + // identity token would be a credential leak. The token is only needed for GitHub's + // own Copilot backend. When not in BYOK mode, use the GitHub Actions token when the + // copilot-requests feature is enabled, otherwise use the COPILOT_GITHUB_TOKEN secret. // #nosec G101 -- These are NOT hardcoded credentials. They are GitHub Actions expression templates // that the runtime replaces with actual values. The strings "${{ secrets.COPILOT_GITHUB_TOKEN }}" // and "${{ github.token }}" are placeholders, not actual credentials. var copilotGitHubToken string useCopilotRequests := isFeatureEnabled(constants.CopilotRequestsFeatureFlag, workflowData) - if useCopilotRequests { + if isBYOKMode { + copilotExecLog.Print("Skipping COPILOT_GITHUB_TOKEN injection: BYOK mode active (COPILOT_PROVIDER_BASE_URL is set)") + } else if useCopilotRequests { copilotGitHubToken = "${{ github.token }}" copilotExecLog.Print("Using GitHub Actions token as COPILOT_GITHUB_TOKEN (copilot-requests feature enabled)") } else { @@ -328,7 +346,6 @@ touch %s env := map[string]string{ "XDG_CONFIG_HOME": "/home/runner", "COPILOT_AGENT_RUNNER_TYPE": "STANDALONE", - "COPILOT_GITHUB_TOKEN": copilotGitHubToken, // Override GITHUB_STEP_SUMMARY with a path that exists inside the sandbox. // The runner's original path is unreachable within the AWF isolated filesystem; // we create this file before the agent starts and append it to the real @@ -344,6 +361,12 @@ touch %s "GITHUB_SERVER_URL": "${{ github.server_url }}", "GITHUB_API_URL": "${{ github.api_url }}", } + // Inject the GitHub token only when not in BYOK mode. The engine.env merge that + // happens later (maps.Copy(env, workflowData.EngineConfig.Env)) can still override + // or nullify this if the user explicitly sets COPILOT_GITHUB_TOKEN in engine.env. + if !isBYOKMode { + env["COPILOT_GITHUB_TOKEN"] = copilotGitHubToken + } injectWorkflowCallNetworkAllowedEnv(env, workflowData) // When copilot-requests feature is enabled, set S2STOKENS=true to allow the Copilot CLI diff --git a/pkg/workflow/copilot_engine_test.go b/pkg/workflow/copilot_engine_test.go index f116033672f..0815a1fb5a1 100644 --- a/pkg/workflow/copilot_engine_test.go +++ b/pkg/workflow/copilot_engine_test.go @@ -1831,6 +1831,78 @@ func TestCopilotEngineEnvOverridesTokenExpression(t *testing.T) { }) } +// TestCopilotEngineBYOKOmitsCopilotGitHubToken verifies that COPILOT_GITHUB_TOKEN is +// NOT injected into the execution step env when BYOK mode is active +// (i.e. COPILOT_PROVIDER_BASE_URL is set in engine.env). Forwarding the GitHub identity +// token to a third-party provider would be a credential leak. +func TestCopilotEngineBYOKOmitsCopilotGitHubToken(t *testing.T) { + engine := NewCopilotEngine() + + t.Run("COPILOT_GITHUB_TOKEN absent when COPILOT_PROVIDER_BASE_URL is set (BYOK)", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + Env: map[string]string{ + constants.CopilotProviderBaseURL: "https://api.openai.com/v1", + constants.CopilotProviderAPIKey: "${{ secrets.PROVIDER_API_KEY }}", + }, + }, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log") + if len(steps) != 1 { + t.Fatalf("Expected 1 step, got %d", len(steps)) + } + + stepContent := strings.Join([]string(steps[0]), "\n") + + // COPILOT_GITHUB_TOKEN must not appear at all — not even its default expression. + if strings.Contains(stepContent, "COPILOT_GITHUB_TOKEN:") { + t.Errorf("COPILOT_GITHUB_TOKEN should be absent in BYOK mode, got:\n%s", stepContent) + } + }) + + t.Run("COPILOT_GITHUB_TOKEN absent with COPILOT_PROVIDER_BASE_URL only (no API key)", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + Env: map[string]string{ + constants.CopilotProviderBaseURL: "http://localhost:11434/v1", + }, + }, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log") + if len(steps) != 1 { + t.Fatalf("Expected 1 step, got %d", len(steps)) + } + + stepContent := strings.Join([]string(steps[0]), "\n") + + if strings.Contains(stepContent, "COPILOT_GITHUB_TOKEN:") { + t.Errorf("COPILOT_GITHUB_TOKEN should be absent in BYOK mode (local provider), got:\n%s", stepContent) + } + }) + + t.Run("COPILOT_GITHUB_TOKEN present when COPILOT_PROVIDER_BASE_URL is not set (standard mode)", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{}, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log") + if len(steps) != 1 { + t.Fatalf("Expected 1 step, got %d", len(steps)) + } + + stepContent := strings.Join([]string(steps[0]), "\n") + + if !strings.Contains(stepContent, "COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}") { + t.Errorf("COPILOT_GITHUB_TOKEN should be present in standard (non-BYOK) mode, got:\n%s", stepContent) + } + }) +} + func TestCopilotEngineSetsDummyAPIKey(t *testing.T) { engine := NewCopilotEngine() From 8788fc1543db73b03dbb5dec546958c19dc3d07b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 14:14:40 +0000 Subject: [PATCH 3/3] Align BYOK token validation and add AWF regression coverage Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/copilot_engine_installation.go | 5 +++-- pkg/workflow/copilot_engine_test.go | 24 +++++++++++++++++++++ pkg/workflow/secret_validation_test.go | 16 ++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/copilot_engine_installation.go b/pkg/workflow/copilot_engine_installation.go index 65009db31d1..2a549ea60a4 100644 --- a/pkg/workflow/copilot_engine_installation.go +++ b/pkg/workflow/copilot_engine_installation.go @@ -26,7 +26,7 @@ var copilotInstallLog = logger.New("workflow:copilot_engine_installation") // GetSecretValidationStep returns the secret validation step for the Copilot engine. // Returns an empty step if: // - copilot-requests feature is enabled (uses GitHub Actions token instead), or -// - COPILOT_PROVIDER_API_KEY or COPILOT_PROVIDER_BEARER_TOKEN is set in engine.env +// - COPILOT_PROVIDER_BASE_URL, COPILOT_PROVIDER_API_KEY, or COPILOT_PROVIDER_BEARER_TOKEN is set in engine.env // (BYOK mode — the external provider handles authentication, so COPILOT_GITHUB_TOKEN // is not required for model routing). func (e *CopilotEngine) GetSecretValidationStep(workflowData *WorkflowData) GitHubActionStep { @@ -34,7 +34,8 @@ func (e *CopilotEngine) GetSecretValidationStep(workflowData *WorkflowData) GitH copilotInstallLog.Print("Skipping secret validation step: copilot-requests feature enabled, using GitHub Actions token") return GitHubActionStep{} } - if engineEnvHasKey(workflowData, constants.CopilotProviderAPIKey) || + if engineEnvHasKey(workflowData, constants.CopilotProviderBaseURL) || + engineEnvHasKey(workflowData, constants.CopilotProviderAPIKey) || engineEnvHasKey(workflowData, constants.CopilotProviderBearerToken) { copilotInstallLog.Print("Skipping COPILOT_GITHUB_TOKEN validation: BYOK provider credentials are configured") return GitHubActionStep{} diff --git a/pkg/workflow/copilot_engine_test.go b/pkg/workflow/copilot_engine_test.go index 0815a1fb5a1..051b162a15f 100644 --- a/pkg/workflow/copilot_engine_test.go +++ b/pkg/workflow/copilot_engine_test.go @@ -1901,6 +1901,30 @@ func TestCopilotEngineBYOKOmitsCopilotGitHubToken(t *testing.T) { t.Errorf("COPILOT_GITHUB_TOKEN should be present in standard (non-BYOK) mode, got:\n%s", stepContent) } }) + + t.Run("AWF command omits --exclude-env COPILOT_GITHUB_TOKEN in BYOK mode", func(t *testing.T) { + workflowData := &WorkflowData{ + Name: "test-workflow", + EngineConfig: &EngineConfig{ + Env: map[string]string{ + constants.CopilotProviderBaseURL: "http://localhost:11434/v1", + }, + }, + SandboxConfig: &SandboxConfig{ + Agent: &AgentSandboxConfig{Type: SandboxTypeAWF}, + }, + } + + steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log") + if len(steps) != 1 { + t.Fatalf("Expected 1 step, got %d", len(steps)) + } + + stepContent := strings.Join([]string(steps[0]), "\n") + if strings.Contains(stepContent, "--exclude-env COPILOT_GITHUB_TOKEN") { + t.Errorf("AWF command should not exclude COPILOT_GITHUB_TOKEN in BYOK mode, got:\n%s", stepContent) + } + }) } func TestCopilotEngineSetsDummyAPIKey(t *testing.T) { diff --git a/pkg/workflow/secret_validation_test.go b/pkg/workflow/secret_validation_test.go index 41388c57633..e2bebdd29fd 100644 --- a/pkg/workflow/secret_validation_test.go +++ b/pkg/workflow/secret_validation_test.go @@ -164,6 +164,22 @@ func TestCopilotEngineSkipsSecretValidationWhenBYOKBearerTokenSet(t *testing.T) } } +func TestCopilotEngineSkipsSecretValidationWhenBYOKBaseURLOnlySet(t *testing.T) { + engine := NewCopilotEngine() + workflowData := &WorkflowData{ + EngineConfig: &EngineConfig{ + Env: map[string]string{ + "COPILOT_PROVIDER_BASE_URL": "http://localhost:11434/v1", + }, + }, + } + + step := engine.GetSecretValidationStep(workflowData) + if len(step) != 0 { + t.Errorf("Expected empty validation step when BYOK COPILOT_PROVIDER_BASE_URL is set, got:\n%s", strings.Join(step, "\n")) + } +} + func TestCodexEngineHasSecretValidation(t *testing.T) { engine := NewCodexEngine() workflowData := &WorkflowData{}