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..9d02b4ac921 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/; @@ -403,19 +405,40 @@ function detectCopilotAuthFailureStage(output) { } /** - * Build a more actionable Copilot auth diagnostic when a 401 came from the gh-aw API proxy. + * @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 * @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" && envFlagEnabled(env.S2STOKENS)) { + return renderTemplateFromFile(COPILOT_REQUESTS_PROXY_AUTH_403_TEMPLATE_PATH, { + selected_model: selectedModel, + stage, + }); + } + 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. " + @@ -1005,6 +1028,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 29ec757029d..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, @@ -972,6 +973,59 @@ 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("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/"); + 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"); + 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", () => { const diagnostic = buildCopilotProxyAuthFailureDiagnostic("Validating token with provider.\nAuthentication failed with provider at http://localhost:10002 (HTTP 401).", { COPILOT_MODEL: "gpt-4.1" }); @@ -997,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:/; 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.