diff --git a/.github/workflows/cli-consistency-checker.lock.yml b/.github/workflows/cli-consistency-checker.lock.yml index c622fb5cccf..090c080c9c8 100644 --- a/.github/workflows/cli-consistency-checker.lock.yml +++ b/.github/workflows/cli-consistency-checker.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"40925dafe7f07884fa18d348d198d2799520b08785756b2a1e12c921735d15b4","body_hash":"c37885bacb425ffb50bd5d957c98e916b75dc01a26200c4232cf780a2d21a713","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70","copilot-sdk":"1.0.6"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"40925dafe7f07884fa18d348d198d2799520b08785756b2a1e12c921735d15b4","body_hash":"c37885bacb425ffb50bd5d957c98e916b75dc01a26200c4232cf780a2d21a713","agent_id":"copilot","engine_versions":{"copilot":"1.0.70","copilot-sdk":"1.0.6"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -149,7 +149,7 @@ jobs: GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_INFO_FRONTMATTER_EMOJI: "✅" - GH_AW_COMPILED_STRICT: "true" + GH_AW_COMPILED_STRICT: "false" GH_AW_INFO_FEATURES: '{"gh-aw-detection":true}' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: @@ -158,6 +158,11 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); + - name: Enforce strict mode policy + if: ${{ vars.GH_AW_POLICY_STRICT == 'true' }} + run: | + echo "::error::GH_AW_POLICY_STRICT=true but this workflow was not compiled in strict mode. Recompile with --strict or strict: true." + exit 1 - name: Restore daily AIC usage cache id: restore-daily-aic-cache if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index 56bcdac4e7b..f59ba71c0b6 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -51,7 +51,7 @@ const { } = require("./awf_reflect.cjs"); const { emitMissingToolPermissionIssue, hasExpectedSafeOutputs, hasNoopInSafeOutputs } = require("./safeoutputs_cli.cjs"); const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs"); -const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, emitSoftTimeoutSignal } = require("./harness_retry_guard.cjs"); +const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, emitSoftTimeoutSignal, isAuthenticationFailedError } = require("./harness_retry_guard.cjs"); const { MODEL_NOT_SUPPORTED_PATTERN: INVALID_MODEL_ERROR_PATTERN } = require("./detect_agent_errors.cjs"); // Pattern to detect Anthropic API overload errors (HTTP 529). @@ -65,7 +65,6 @@ const OVERLOADED_ERROR_PATTERN = /overloaded_error|"overloaded"/i; // - embedded stream-json result fields (e.g. "api_error_status":429) // - human-readable message text ("rate limit") const RATE_LIMIT_ERROR_PATTERN = /rate_limit_error|429 Too Many Requests|"api_error_status"\s*:\s*429|request rejected \(429\)|rate limit/i; -const AUTHENTICATION_FAILED_PATTERN = /Authentication failed(?:\s*\(Request ID:[^)]+\))?/i; // Pattern to detect a clean max-turns exit from Claude Code. // Claude Code emits a JSON result object with "subtype":"error_max_turns" when the @@ -149,15 +148,6 @@ function isRateLimitError(output) { return RATE_LIMIT_ERROR_PATTERN.test(output); } -/** - * Determines if the collected output contains an authentication failed error. - * @param {string} output - Collected stdout+stderr from the process - * @returns {boolean} - */ -function isAuthenticationFailedError(output) { - return AUTHENTICATION_FAILED_PATTERN.test(output); -} - /** * Determines if the collected output signals a clean max-turns exit. * When Claude Code hits its turn limit it emits a result object with diff --git a/actions/setup/js/claude_harness.test.cjs b/actions/setup/js/claude_harness.test.cjs index 460380e9ae4..03893ba3f46 100644 --- a/actions/setup/js/claude_harness.test.cjs +++ b/actions/setup/js/claude_harness.test.cjs @@ -190,6 +190,23 @@ describe("claude_harness.cjs", () => { expect(isAuthenticationFailedError("Authentication failed (Request ID: C818:3ED713:19D401B:1C446B7:69D653CA)")).toBe(true); }); + it('returns true for Claude Code stream-JSON "error":"authentication_failed" field', () => { + const jsonLine = JSON.stringify({ + type: "assistant", + error: "authentication_failed", + message: { content: [{ type: "text", text: "Not logged in · Please run /login" }] }, + }); + expect(isAuthenticationFailedError(jsonLine)).toBe(true); + }); + + it('returns true for Claude Code "Not logged in" message', () => { + expect(isAuthenticationFailedError("Not logged in · Please run /login")).toBe(true); + }); + + it('returns true for "not logged in" (case-insensitive)', () => { + expect(isAuthenticationFailedError("NOT LOGGED IN")).toBe(true); + }); + describe("isInvalidModelError", () => { it("returns true for model-not-supported errors", () => { expect(isInvalidModelError("Execution failed: CAPIError: 400 The requested model is not supported.")).toBe(true); diff --git a/actions/setup/js/codex_harness.cjs b/actions/setup/js/codex_harness.cjs index c72ba7c1759..0846785c60f 100644 --- a/actions/setup/js/codex_harness.cjs +++ b/actions/setup/js/codex_harness.cjs @@ -51,7 +51,7 @@ const { } = require("./awf_reflect.cjs"); const { emitMissingToolPermissionIssue, hasExpectedSafeOutputs, hasNoopInSafeOutputs } = require("./safeoutputs_cli.cjs"); const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs"); -const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, emitSoftTimeoutSignal } = require("./harness_retry_guard.cjs"); +const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, emitSoftTimeoutSignal, isAuthenticationFailedError } = require("./harness_retry_guard.cjs"); const { MODEL_NOT_SUPPORTED_PATTERN: INVALID_MODEL_ERROR_PATTERN } = require("./detect_agent_errors.cjs"); const { resolveRetryConfig } = require("./harness_retry_config.cjs"); @@ -73,7 +73,6 @@ const TOKEN_PER_MIN_RATE_LIMIT_PATTERN = /Rate limit reached for [^\s]+(?: in or // The backreference \1 requires the two numeric parts of "N/N" to be identical — // "5/5" matches (exhausted) but "1/5", "3/5", "4/5" do not (still retrying). const RECONNECT_EXHAUSTED_PATTERN = /Reconnecting\.\.\.\s+(\d+)\/\1\b/; -const AUTHENTICATION_FAILED_PATTERN = /Authentication failed(?:\s*\(Request ID:[^)]+\))?/i; // Pattern to detect a missing API key at startup — Codex emits this before making any API // calls when neither CODEX_API_KEY nor OPENAI_API_KEY is available in the environment. @@ -115,15 +114,6 @@ function isTokenPerMinuteRateLimitError(output) { return TOKEN_PER_MIN_RATE_LIMIT_PATTERN.test(output); } -/** - * Determines if the collected output contains an authentication failed error. - * @param {string} output - Collected stdout+stderr from the process - * @returns {boolean} - */ -function isAuthenticationFailedError(output) { - return AUTHENTICATION_FAILED_PATTERN.test(output); -} - /** * Determines if the collected output indicates a missing API key at startup. * Codex exits before producing any agent output in this case, so retrying is futile. diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index 1af9a6c7f2b..7468b7aedcf 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -63,7 +63,7 @@ const { } = require("./awf_reflect.cjs"); const { runSafeOutputsCLI, buildMissingToolAlternatives, emitMissingToolPermissionIssue, emitInfrastructureIncomplete, hasExpectedSafeOutputs, hasNoopInSafeOutputs } = require("./safeoutputs_cli.cjs"); const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs"); -const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, emitSoftTimeoutSignal } = require("./harness_retry_guard.cjs"); +const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, emitSoftTimeoutSignal, isAuthenticationFailedError: isCommonAuthenticationFailedError } = require("./harness_retry_guard.cjs"); const { isCAPIQuotaExceededError } = require("./detect_agent_errors.cjs"); const { loadModelsJson } = require("./model_costs.cjs"); const { resolveConfiguredCopilotModel } = require("./resolve_model_alias.cjs"); @@ -113,14 +113,16 @@ const NO_AUTH_INFO_PATTERN = /No authentication information found|Session was no // After a first-attempt auth failure, retrying is futile because the entrypoint unsets // COPILOT_GITHUB_TOKEN between attempts. // -// Also matches the Copilot CAPI 400 response emitted when the supplied token is a +// This pattern covers the Copilot CAPI 400 response emitted when the supplied token is a // Personal Access Token (classic or fine-grained): // "400 400 checking third-party user token: bad request: Personal Access Tokens // are not supported for this endpoint" // PAT rejection is a persistent credential-type problem — retrying with the same // token always produces the same 400. Treating it as an auth failure short-circuits // the retry loop instead of burning all 4 attempts. -const AUTHENTICATION_FAILED_PATTERN = /Authentication failed(?:\s*\(Request ID:[^)]+\))?|checking third-party user token:[^\n]*Personal Access Tokens are not supported/i; +// Common auth failure signals ("Authentication failed", "authentication_failed", "not logged in") +// are handled by isCommonAuthenticationFailedError from harness_retry_guard.cjs. +const COPILOT_PAT_AUTH_FAILED_PATTERN = /checking third-party user token:[^\n]*Personal Access Tokens are not supported/i; // Pattern: Copilot CLI inference access denied const INFERENCE_ACCESS_ERROR_PATTERN = /Access denied by policy settings|invalid access to inference/; // Pattern: Agentic engine process killed by signal (timeout) @@ -368,11 +370,13 @@ function isNoAuthInfoError(output) { /** * Determines if the collected output contains an authentication failed error. + * Covers both the common signals (from harness_retry_guard) and the Copilot-specific + * PAT rejection error. * @param {string} output - Collected stdout+stderr from the process * @returns {boolean} */ function isAuthenticationFailedError(output) { - return AUTHENTICATION_FAILED_PATTERN.test(output); + return isCommonAuthenticationFailedError(output) || COPILOT_PAT_AUTH_FAILED_PATTERN.test(output); } /** diff --git a/actions/setup/js/harness_retry_guard.cjs b/actions/setup/js/harness_retry_guard.cjs index 8d2a18f0d03..e5e7653cbe8 100644 --- a/actions/setup/js/harness_retry_guard.cjs +++ b/actions/setup/js/harness_retry_guard.cjs @@ -21,6 +21,13 @@ const GOAL_ALREADY_ACTIVE_PATTERNS = [/\bthis thread already has a goal\b[\s\S]* // ("Maximum LLM invocations exceeded"). const MAX_RUNS_EXCEEDED_PATTERNS = [/\bmax_runs_exceeded\b/i, /Maximum LLM invocations exceeded/i]; +// Common authentication failure patterns shared across all harnesses. +// Matches: +// - "Authentication failed (Request ID: ...)" — Anthropic/OpenAI direct auth error +// - `"error":"authentication_failed"` — Claude Code stream-JSON error field (e.g. 401 via AWF proxy) +// - "not logged in" — Claude Code message when no credentials are available +const AUTHENTICATION_FAILED_PATTERNS = [/Authentication failed(?:\s*\(Request ID:[^)]+\))?/i, /"error"\s*:\s*"authentication_failed"/i, /not logged in/i]; + /** * @param {unknown} output * @returns {boolean} @@ -30,6 +37,16 @@ function isMaxRunsExceededError(output) { return MAX_RUNS_EXCEEDED_PATTERNS.some(pattern => pattern.test(safeOutput)); } +/** + * Determines if the collected output contains an authentication failed error. + * @param {unknown} output + * @returns {boolean} + */ +function isAuthenticationFailedError(output) { + const safeOutput = typeof output === "string" ? output : ""; + return AUTHENTICATION_FAILED_PATTERNS.some(pattern => pattern.test(safeOutput)); +} + /** * Detect retry guard conditions that should stop harness retries immediately. * @param {unknown} output @@ -81,7 +98,9 @@ if (typeof module !== "undefined" && module.exports) { AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS, GOAL_ALREADY_ACTIVE_PATTERNS, MAX_RUNS_EXCEEDED_PATTERNS, + AUTHENTICATION_FAILED_PATTERNS, isMaxRunsExceededError, + isAuthenticationFailedError, SOFT_TIMEOUT_BUFFER_MS, buildSoftTimeoutGuard, emitSoftTimeoutSignal, diff --git a/actions/setup/js/harness_retry_guard.test.cjs b/actions/setup/js/harness_retry_guard.test.cjs index 0bd91d8f46a..04556c7476a 100644 --- a/actions/setup/js/harness_retry_guard.test.cjs +++ b/actions/setup/js/harness_retry_guard.test.cjs @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); -const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, isMaxRunsExceededError } = require("./harness_retry_guard.cjs"); +const { detectNonRetryableHarnessGuard, buildSoftTimeoutGuard, isMaxRunsExceededError, isAuthenticationFailedError } = require("./harness_retry_guard.cjs"); describe("harness_retry_guard.cjs", () => { it("detects AI credits exceeded markers", () => { @@ -129,6 +129,35 @@ describe("harness_retry_guard.cjs", () => { it("isMaxRunsExceededError ignores unrelated output", () => { expect(isMaxRunsExceededError("transient network timeout")).toBe(false); }); + + it("isAuthenticationFailedError returns true for Anthropic-direct auth failure with request ID", () => { + expect(isAuthenticationFailedError("Authentication failed (Request ID: C818:3ED713:19D401B:1C446B7:69D653CA)")).toBe(true); + }); + + it("isAuthenticationFailedError returns true for bare Authentication failed", () => { + expect(isAuthenticationFailedError("Authentication failed")).toBe(true); + }); + + it('isAuthenticationFailedError returns true for Claude Code stream-JSON "error":"authentication_failed" field', () => { + const jsonLine = JSON.stringify({ type: "result", error: "authentication_failed" }); + expect(isAuthenticationFailedError(jsonLine)).toBe(true); + }); + + it('isAuthenticationFailedError returns true for Claude Code "not logged in" message (case-insensitive)', () => { + expect(isAuthenticationFailedError("Not logged in · Please run /login")).toBe(true); + expect(isAuthenticationFailedError("NOT LOGGED IN")).toBe(true); + }); + + it("isAuthenticationFailedError returns false for unrelated output", () => { + expect(isAuthenticationFailedError("No authentication information found")).toBe(false); + expect(isAuthenticationFailedError("rate_limit_error")).toBe(false); + expect(isAuthenticationFailedError("")).toBe(false); + }); + + it("isAuthenticationFailedError returns false for non-string input", () => { + expect(isAuthenticationFailedError(null)).toBe(false); + expect(isAuthenticationFailedError(undefined)).toBe(false); + }); }); describe("buildSoftTimeoutGuard", () => {