From 41add2d81ddbcbba894a729b54ded899cf927f09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:40:32 +0000 Subject: [PATCH 1/6] Add copilot 403 org billing guidance Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../patch-copilot-403-org-billing-message.md | 5 +++++ actions/setup/js/copilot_harness.cjs | 15 +++++++++++++-- actions/setup/js/copilot_harness.test.cjs | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 .changeset/patch-copilot-403-org-billing-message.md diff --git a/.changeset/patch-copilot-403-org-billing-message.md b/.changeset/patch-copilot-403-org-billing-message.md new file mode 100644 index 00000000000..d01e06879e3 --- /dev/null +++ b/.changeset/patch-copilot-403-org-billing-message.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Improved the Copilot CLI proxy authentication failure message for `permissions.copilot-requests: write` runs so HTTP 403 errors point to Copilot org billing guidance instead of BYOK provider token settings. diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index ddbb8fee9db..96f0f5b3eb3 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -403,19 +403,30 @@ function detectCopilotAuthFailureStage(output) { } /** - * Build a more actionable Copilot auth diagnostic when a 401 came from the gh-aw API proxy. + * Build a more actionable Copilot auth diagnostic when a 401/403 came from the gh-aw API proxy. * @param {string} output * @param {NodeJS.ProcessEnv} [env] * @returns {string} */ function buildCopilotProxyAuthFailureDiagnostic(output, env = process.env) { const authFailure = parseProviderAuthFailure(output); - if (!authFailure || authFailure.statusCode !== "401" || !isLikelyAWFAPIProxyURL(authFailure.providerUrl)) { + if (!authFailure || !isLikelyAWFAPIProxyURL(authFailure.providerUrl)) { return ""; } const selectedModel = typeof env.COPILOT_MODEL === "string" && env.COPILOT_MODEL.trim() ? env.COPILOT_MODEL.trim() : "(unset)"; const stage = detectCopilotAuthFailureStage(output); + if (authFailure.statusCode === "403" && env.S2STOKENS === "true") { + return ( + `Copilot requests authentication failed through the gh-aw API proxy (HTTP 403, model=${selectedModel}, stage=${stage}). ` + + "This workflow is using permissions.copilot-requests: write, so Copilot requests must be allowed through your organization's centralized Copilot billing configuration. " + + "Verify that copilot-requests: write is granted to the workflow or job and that Copilot org billing is enabled for your organization. " + + "See https://github.github.com/gh-aw/reference/billing/ for details." + ); + } + if (authFailure.statusCode !== "401") { + return ""; + } return ( `Copilot authentication failed through the gh-aw API proxy (HTTP 401, model=${selectedModel}, stage=${stage}). ` + "Check that COPILOT_GITHUB_TOKEN is present, unexpired, and authorized for the selected COPILOT_MODEL. " + diff --git a/actions/setup/js/copilot_harness.test.cjs b/actions/setup/js/copilot_harness.test.cjs index 29ec757029d..d76abc37ca0 100644 --- a/actions/setup/js/copilot_harness.test.cjs +++ b/actions/setup/js/copilot_harness.test.cjs @@ -972,6 +972,20 @@ describe("copilot_harness.cjs", () => { expect(diagnostic).not.toContain("COPILOT_PROVIDER_API_KEY"); }); + it("rewrites local proxy 403 errors in copilot-requests mode to org-billing guidance", () => { + const diagnostic = buildCopilotProxyAuthFailureDiagnostic("Authentication failed with provider at http://172.30.0.30:10002 (HTTP 403).\nCheck your COPILOT_PROVIDER_API_KEY or COPILOT_PROVIDER_BEARER_TOKEN.", { + COPILOT_MODEL: "claude-sonnet-4.5", + S2STOKENS: "true", + }); + + expect(diagnostic).toContain("Copilot requests authentication failed"); + expect(diagnostic).toContain("HTTP 403"); + expect(diagnostic).toContain("permissions.copilot-requests: write"); + expect(diagnostic).toContain("centralized Copilot billing"); + expect(diagnostic).toContain("https://github.github.com/gh-aw/reference/billing/"); + expect(diagnostic).not.toContain("COPILOT_PROVIDER_API_KEY"); + }); + it("reports token-validation stage when present in the output", () => { const diagnostic = buildCopilotProxyAuthFailureDiagnostic("Validating token with provider.\nAuthentication failed with provider at http://localhost:10002 (HTTP 401).", { COPILOT_MODEL: "gpt-4.1" }); From e9c0747f47779009adc3903d0cbb7e96b6d623e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:44:29 +0000 Subject: [PATCH 2/6] Harden copilot 403 org billing detection Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_harness.cjs | 14 +++++++++++++- actions/setup/js/copilot_harness.test.cjs | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index 96f0f5b3eb3..d92feb135b9 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -402,6 +402,18 @@ function detectCopilotAuthFailureStage(output) { return "starting the Copilot CLI request"; } +/** + * @param {string | undefined} value + * @returns {boolean} + */ +function envFlagEnabled(value) { + if (typeof value !== "string") { + return false; + } + const normalized = value.trim().toLowerCase(); + return normalized === "true" || normalized === "1" || normalized === "yes"; +} + /** * Build a more actionable Copilot auth diagnostic when a 401/403 came from the gh-aw API proxy. * @param {string} output @@ -416,7 +428,7 @@ function buildCopilotProxyAuthFailureDiagnostic(output, env = process.env) { const selectedModel = typeof env.COPILOT_MODEL === "string" && env.COPILOT_MODEL.trim() ? env.COPILOT_MODEL.trim() : "(unset)"; const stage = detectCopilotAuthFailureStage(output); - if (authFailure.statusCode === "403" && env.S2STOKENS === "true") { + if (authFailure.statusCode === "403" && envFlagEnabled(env.S2STOKENS)) { return ( `Copilot requests authentication failed through the gh-aw API proxy (HTTP 403, model=${selectedModel}, stage=${stage}). ` + "This workflow is using permissions.copilot-requests: write, so Copilot requests must be allowed through your organization's centralized Copilot billing configuration. " + diff --git a/actions/setup/js/copilot_harness.test.cjs b/actions/setup/js/copilot_harness.test.cjs index d76abc37ca0..c3d85f8f2ad 100644 --- a/actions/setup/js/copilot_harness.test.cjs +++ b/actions/setup/js/copilot_harness.test.cjs @@ -986,6 +986,15 @@ describe("copilot_harness.cjs", () => { expect(diagnostic).not.toContain("COPILOT_PROVIDER_API_KEY"); }); + it("treats truthy S2STOKENS values as copilot-requests mode for 403 guidance", () => { + const diagnostic = buildCopilotProxyAuthFailureDiagnostic("Authentication failed with provider at http://172.30.0.30:10002 (HTTP 403).", { + COPILOT_MODEL: "claude-sonnet-4.5", + S2STOKENS: " YES ", + }); + + expect(diagnostic).toContain("Copilot requests authentication failed"); + }); + it("reports token-validation stage when present in the output", () => { const diagnostic = buildCopilotProxyAuthFailureDiagnostic("Validating token with provider.\nAuthentication failed with provider at http://localhost:10002 (HTTP 401).", { COPILOT_MODEL: "gpt-4.1" }); From ef58ece6a2568b52fa904939d70dfe7c7dce0cd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:01:28 +0000 Subject: [PATCH 3/6] Refactor copilot 403 guidance template Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_harness.cjs | 12 ++++++------ actions/setup/md/copilot_requests_proxy_auth_403.md | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 actions/setup/md/copilot_requests_proxy_auth_403.md diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index d92feb135b9..b7eeb5c3895 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -44,6 +44,7 @@ require("./shim.cjs"); const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); +const { renderTemplateFromFile } = require("./messages_core.cjs"); const { runProcess, formatDuration, sleep, isCopilotSDKEnabled, buildCopilotSDKEnv } = require("./process_runner.cjs"); const { buildCopilotSDKServerArgs, getCopilotSDKServerPort, startCopilotSDKServer, stopCopilotSDKServer, waitForCopilotSDKServer } = require("./copilot_sdk_sidecar.cjs"); const { @@ -80,6 +81,7 @@ const PROMPT_FILE_INLINE_THRESHOLD_LABEL = "100KB"; const MAX_ENV_VAR_PREVIEW_LENGTH = 120; const OUTPUT_TAIL_MAX_CHARS = 600; const OUTPUT_TAIL_MAX_LINES = 12; +const COPILOT_REQUESTS_PROXY_AUTH_403_TEMPLATE_PATH = path.join(__dirname, "../md/copilot_requests_proxy_auth_403.md"); // Pattern to detect transient CAPIError 400 in copilot output const CAPI_ERROR_400_PATTERN = /CAPIError:\s*400/; @@ -429,12 +431,10 @@ function buildCopilotProxyAuthFailureDiagnostic(output, env = process.env) { const selectedModel = typeof env.COPILOT_MODEL === "string" && env.COPILOT_MODEL.trim() ? env.COPILOT_MODEL.trim() : "(unset)"; const stage = detectCopilotAuthFailureStage(output); if (authFailure.statusCode === "403" && envFlagEnabled(env.S2STOKENS)) { - return ( - `Copilot requests authentication failed through the gh-aw API proxy (HTTP 403, model=${selectedModel}, stage=${stage}). ` + - "This workflow is using permissions.copilot-requests: write, so Copilot requests must be allowed through your organization's centralized Copilot billing configuration. " + - "Verify that copilot-requests: write is granted to the workflow or job and that Copilot org billing is enabled for your organization. " + - "See https://github.github.com/gh-aw/reference/billing/ for details." - ); + return renderTemplateFromFile(COPILOT_REQUESTS_PROXY_AUTH_403_TEMPLATE_PATH, { + selected_model: selectedModel, + stage, + }); } if (authFailure.statusCode !== "401") { return ""; diff --git a/actions/setup/md/copilot_requests_proxy_auth_403.md b/actions/setup/md/copilot_requests_proxy_auth_403.md new file mode 100644 index 00000000000..cab43b1e3e8 --- /dev/null +++ b/actions/setup/md/copilot_requests_proxy_auth_403.md @@ -0,0 +1 @@ +Copilot requests authentication failed through the gh-aw API proxy (HTTP 403, model={selected_model}, stage={stage}). This workflow is using permissions.copilot-requests: write, so Copilot requests must be allowed through your organization's centralized Copilot billing configuration. Verify that copilot-requests: write is granted to the workflow or job and that Copilot org billing is enabled for your organization. See https://github.github.com/gh-aw/reference/billing/ for details. From 23bce86dbc48f202359050a09c39f3709a45d56d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:46:06 +0000 Subject: [PATCH 4/6] Strengthen copilot 403 test coverage and add envFlagEnabled unit tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_harness.cjs | 13 +++++-- actions/setup/js/copilot_harness.test.cjs | 45 +++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index b7eeb5c3895..c02ccfd7cf5 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -431,10 +431,14 @@ function buildCopilotProxyAuthFailureDiagnostic(output, env = process.env) { const selectedModel = typeof env.COPILOT_MODEL === "string" && env.COPILOT_MODEL.trim() ? env.COPILOT_MODEL.trim() : "(unset)"; const stage = detectCopilotAuthFailureStage(output); if (authFailure.statusCode === "403" && envFlagEnabled(env.S2STOKENS)) { - return renderTemplateFromFile(COPILOT_REQUESTS_PROXY_AUTH_403_TEMPLATE_PATH, { - selected_model: selectedModel, - stage, - }); + try { + return renderTemplateFromFile(COPILOT_REQUESTS_PROXY_AUTH_403_TEMPLATE_PATH, { + selected_model: selectedModel, + stage, + }); + } catch { + return `Copilot requests authentication failed through the gh-aw API proxy (HTTP 403, model=${selectedModel}, stage=${stage}). ` + "Verify that copilot-requests: write is granted and that Copilot org billing is enabled."; + } } if (authFailure.statusCode !== "401") { return ""; @@ -1028,6 +1032,7 @@ if (typeof module !== "undefined" && module.exports) { fetchAWFReflect, fetchModelsFromUrl, buildCopilotProxyAuthFailureDiagnostic, + envFlagEnabled, generateCopilotConnectionToken, buildCopilotSDKServerArgs, getCopilotSDKServerPort, diff --git a/actions/setup/js/copilot_harness.test.cjs b/actions/setup/js/copilot_harness.test.cjs index c3d85f8f2ad..b413140382e 100644 --- a/actions/setup/js/copilot_harness.test.cjs +++ b/actions/setup/js/copilot_harness.test.cjs @@ -17,6 +17,7 @@ const { buildMissingToolAlternatives, buildInfrastructureIncompletePayload, buildCopilotProxyAuthFailureDiagnostic, + envFlagEnabled, buildPromptFileFallbackInstruction, countPermissionDeniedIssues, detectCopilotErrors, @@ -980,6 +981,8 @@ describe("copilot_harness.cjs", () => { expect(diagnostic).toContain("Copilot requests authentication failed"); expect(diagnostic).toContain("HTTP 403"); + expect(diagnostic).toContain("model=claude-sonnet-4.5"); + expect(diagnostic).toContain("stage=starting the Copilot CLI request"); expect(diagnostic).toContain("permissions.copilot-requests: write"); expect(diagnostic).toContain("centralized Copilot billing"); expect(diagnostic).toContain("https://github.github.com/gh-aw/reference/billing/"); @@ -993,6 +996,34 @@ describe("copilot_harness.cjs", () => { }); expect(diagnostic).toContain("Copilot requests authentication failed"); + expect(diagnostic).toContain("https://github.github.com/gh-aw/reference/billing/"); + expect(diagnostic).not.toContain("COPILOT_PROVIDER_API_KEY"); + }); + + it("returns empty string for proxy 403 when S2STOKENS is not set (BYOK mode)", () => { + const diagnostic = buildCopilotProxyAuthFailureDiagnostic("Authentication failed with provider at http://172.30.0.30:10002 (HTTP 403).", { + COPILOT_MODEL: "claude-sonnet-4.5", + }); + + expect(diagnostic).toBe(""); + }); + + it("returns empty string for proxy 403 when S2STOKENS is falsy", () => { + const diagnostic = buildCopilotProxyAuthFailureDiagnostic("Authentication failed with provider at http://172.30.0.30:10002 (HTTP 403).", { + COPILOT_MODEL: "claude-sonnet-4.5", + S2STOKENS: "false", + }); + + expect(diagnostic).toBe(""); + }); + + it("returns empty string for non-proxy 403 even when S2STOKENS is true", () => { + const diagnostic = buildCopilotProxyAuthFailureDiagnostic("Authentication failed with provider at (api.anthropic.com/redacted) (HTTP 403).", { + COPILOT_MODEL: "claude-sonnet-4.5", + S2STOKENS: "true", + }); + + expect(diagnostic).toBe(""); }); it("reports token-validation stage when present in the output", () => { @@ -1020,6 +1051,20 @@ describe("copilot_harness.cjs", () => { }); }); + describe("envFlagEnabled", () => { + it.each(["true", "TRUE", "True", "1", "yes", " YES "])("returns true for '%s'", v => { + expect(envFlagEnabled(v)).toBe(true); + }); + + it.each(["false", "FALSE", "0", "no", "", " "])("returns false for '%s'", v => { + expect(envFlagEnabled(v)).toBe(false); + }); + + it("returns false for undefined", () => { + expect(envFlagEnabled(undefined)).toBe(false); + }); + }); + describe("auth error prevents retry", () => { // Inline the same retry logic as the driver, including auth error check const MCP_POLICY_BLOCKED_PATTERN = /MCP servers were blocked by policy:/; From fdb79345f8c7caf506ea9f501fdcf1a13ba4e3f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:48:48 +0000 Subject: [PATCH 5/6] Fix template literal inconsistency in try/catch fallback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_harness.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index c02ccfd7cf5..a8b1909600c 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -437,7 +437,7 @@ function buildCopilotProxyAuthFailureDiagnostic(output, env = process.env) { stage, }); } catch { - return `Copilot requests authentication failed through the gh-aw API proxy (HTTP 403, model=${selectedModel}, stage=${stage}). ` + "Verify that copilot-requests: write is granted and that Copilot org billing is enabled."; + return `Copilot requests authentication failed through the gh-aw API proxy (HTTP 403, model=${selectedModel}, stage=${stage}). Verify that copilot-requests: write is granted and that Copilot org billing is enabled.`; } } if (authFailure.statusCode !== "401") { From 30d839e2f714c47561569857726d0f90d61921f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:07:14 +0000 Subject: [PATCH 6/6] Remove try/catch from renderTemplateFromFile in buildCopilotProxyAuthFailureDiagnostic Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_harness.cjs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index a8b1909600c..9d02b4ac921 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -431,14 +431,10 @@ function buildCopilotProxyAuthFailureDiagnostic(output, env = process.env) { const selectedModel = typeof env.COPILOT_MODEL === "string" && env.COPILOT_MODEL.trim() ? env.COPILOT_MODEL.trim() : "(unset)"; const stage = detectCopilotAuthFailureStage(output); if (authFailure.statusCode === "403" && envFlagEnabled(env.S2STOKENS)) { - try { - return renderTemplateFromFile(COPILOT_REQUESTS_PROXY_AUTH_403_TEMPLATE_PATH, { - selected_model: selectedModel, - stage, - }); - } catch { - return `Copilot requests authentication failed through the gh-aw API proxy (HTTP 403, model=${selectedModel}, stage=${stage}). Verify that copilot-requests: write is granted and that Copilot org billing is enabled.`; - } + return renderTemplateFromFile(COPILOT_REQUESTS_PROXY_AUTH_403_TEMPLATE_PATH, { + selected_model: selectedModel, + stage, + }); } if (authFailure.statusCode !== "401") { return "";