From a15fc65e2a20cca35396fd1edc5610851bcf5d98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Aug 2026 07:30:14 +0000 Subject: [PATCH 1/4] Initial plan From da76d8379e28ddbddbd9007dc88faa775c573db5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Aug 2026 07:41:00 +0000 Subject: [PATCH 2/4] initial plan: fix threat detection engine error banner Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/daily-byok-ollama-test.lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/daily-byok-ollama-test.lock.yml b/.github/workflows/daily-byok-ollama-test.lock.yml index 857f15a3b9a..38ffad98ff2 100644 --- a/.github/workflows/daily-byok-ollama-test.lock.yml +++ b/.github/workflows/daily-byok-ollama-test.lock.yml @@ -1451,7 +1451,7 @@ jobs: printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"targets\":{\"copilot\":{\"host\":\"host.docker.internal:11434\"}}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"defaultAiCreditsPricing\":{\"input\":0.000001,\"output\":0.000001},\"targets\":{\"copilot\":{\"host\":\"host.docker.internal:11434\"}},\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" From 94ca20ac26de57e7986259c01d0e8bc0e6933198 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Aug 2026 07:51:18 +0000 Subject: [PATCH 3/4] fix: use distinct XML marker for threat detection engine errors vs real threats Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/generate_footer.test.cjs | 5 +- actions/setup/js/messages.test.cjs | 2 + actions/setup/js/messages_run_status.cjs | 4 +- .../setup/js/push_to_pull_request_branch.cjs | 11 +-- actions/setup/js/threat_detection_warning.cjs | 68 +++++++++++++------ .../js/threat_detection_warning.test.cjs | 19 +++++- .../js/update_activation_comment.test.cjs | 4 ++ 7 files changed, 84 insertions(+), 29 deletions(-) diff --git a/actions/setup/js/generate_footer.test.cjs b/actions/setup/js/generate_footer.test.cjs index e150ea86133..ebea285cdc9 100644 --- a/actions/setup/js/generate_footer.test.cjs +++ b/actions/setup/js/generate_footer.test.cjs @@ -487,7 +487,8 @@ describe("generate_footer.cjs", () => { expect(result).toContain("> [!WARNING]"); expect(result).toContain("threat detection engine error"); - expect(result).toContain(""); + expect(result).toContain(""); + expect(result).not.toContain(""); expect(result).not.toContain("> [!CAUTION]"); expect(result).not.toContain("agentic threat detected"); expect(result).toContain("failed to produce results"); @@ -501,6 +502,8 @@ describe("generate_footer.cjs", () => { expect(result).toContain("> [!WARNING]"); expect(result).toContain("threat detection engine error"); + expect(result).toContain(""); + expect(result).not.toContain(""); expect(result).not.toContain("> [!CAUTION]"); expect(result).not.toContain("agentic threat detected"); expect(result).toContain("could not be parsed"); diff --git a/actions/setup/js/messages.test.cjs b/actions/setup/js/messages.test.cjs index 2f0e8283df7..d19c06eec65 100644 --- a/actions/setup/js/messages.test.cjs +++ b/actions/setup/js/messages.test.cjs @@ -1412,6 +1412,8 @@ describe("messages.cjs", () => { expect(result).toContain("> [!WARNING]"); expect(result).toContain("threat detection engine error"); + expect(result).toContain(""); + expect(result).not.toContain(""); expect(result).not.toContain("> [!CAUTION]"); expect(result).not.toContain("agentic threat detected"); expect(result).toContain("threat detection engine failed"); diff --git a/actions/setup/js/messages_run_status.cjs b/actions/setup/js/messages_run_status.cjs index 445e7cfc925..19fb4e87a45 100644 --- a/actions/setup/js/messages_run_status.cjs +++ b/actions/setup/js/messages_run_status.cjs @@ -8,7 +8,7 @@ */ const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cjs"); -const { getDetectionReasonText, getThreatDetectedMarkerTemplate, normalizeThreatKinds, isToolingFailureReason } = require("./threat_detection_warning.cjs"); +const { getDetectionReasonText, getThreatDetectedMarkerTemplate, getThreatEngineErrorMarkerTemplate, normalizeThreatKinds, isToolingFailureReason } = require("./threat_detection_warning.cjs"); /** * Renders a message using a custom template from config or a default template. @@ -154,7 +154,7 @@ function getDetectionWarningMessage(ctx) { const reasonText = getDetectionReasonText(ctx.reason); const isEngineError = isToolingFailureReason(ctx.reason); if (isEngineError) { - const defaultTemplate = `> [!WARNING]\n> threat detection engine error\n> The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.\n> ${getThreatDetectedMarkerTemplate()}\n>\n>
\n> Details\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n>
`; + const defaultTemplate = `> [!WARNING]\n> threat detection engine error\n> The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.\n> ${getThreatEngineErrorMarkerTemplate()}\n>\n>
\n> Details\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n>
`; return renderConfiguredMessage("detectionEngineError", defaultTemplate, { ...ctx, reasonText, threatKinds: normalizeThreatKinds(ctx.reason) }); } const defaultTemplate = `> [!CAUTION]\n> agentic threat detected\n> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.\n> ${getThreatDetectedMarkerTemplate()}\n>\n>
\n> Details\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n>
`; diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index e29228d2487..aa550fd2070 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -21,7 +21,7 @@ const { withGitHubHostToken } = require("./git_auth_helpers.cjs"); const { ensureFullHistoryForBundle, extractBundlePrerequisiteCommits, isShallowOrSparseCheckout, linearizeRangeAsCommit, ensureSafeDirectoryTrust } = require("./git_helpers.cjs"); const { normalizeCommitSHA } = require("./commit_sha_helpers.cjs"); const { findRepoCheckout } = require("./find_repo_checkout.cjs"); -const { getThreatDetectedMarker } = require("./threat_detection_warning.cjs"); +const { getThreatDetectedMarker, isToolingFailureReason } = require("./threat_detection_warning.cjs"); const { attachExecutionState } = require("./safe_output_execution_metadata.cjs"); const { resolveTransportPaths } = require("./resolve_transport_paths.cjs"); @@ -1304,10 +1304,13 @@ async function main(config = {}) { // For fork-backed PRs, use an owner-qualified head reference. const reviewHeadRef = pushRemoteUrl ? `${pushRepoParts.owner}:${reviewBranchName}` : reviewBranchName; const detectionReasonEnv = process.env.GH_AW_DETECTION_REASON || "unknown"; + const isEngineError = isToolingFailureReason(detectionReasonEnv); const prBody = [ - "> [!CAUTION]", - "> agentic threat detected", - "> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.", + isEngineError ? "> [!WARNING]" : "> [!CAUTION]", + isEngineError ? "> threat detection engine error" : "> agentic threat detected", + isEngineError + ? "> The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding." + : "> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.", `> ${getThreatDetectedMarker(detectionReasonEnv)}`, ">", `> **Reason:** ${detectionReasonEnv}`, diff --git a/actions/setup/js/threat_detection_warning.cjs b/actions/setup/js/threat_detection_warning.cjs index 4202bffae5f..3df81dfac63 100644 --- a/actions/setup/js/threat_detection_warning.cjs +++ b/actions/setup/js/threat_detection_warning.cjs @@ -20,25 +20,6 @@ function normalizeThreatKinds(reason) { return kinds.length > 0 ? Array.from(new Set(kinds)).join(",") : "unknown"; } -/** - * Returns the XML marker used to identify threat-detected output. - * - * @param {string | undefined | null} reason - * @returns {string} - */ -function getThreatDetectedMarker(reason) { - return ""; -} - -/** - * Returns the marker template for configured message rendering. - * - * @returns {string} - */ -function getThreatDetectedMarkerTemplate() { - return ""; -} - /** * Returns a human-readable reason text for detection warnings. * @@ -69,10 +50,59 @@ function isToolingFailureReason(reason) { return normalized === "agent_failure" || normalized === "parse_error"; } +/** + * Returns the XML marker used to identify threat-engine-error output. + * This marker is distinct from the real-threat marker so that automated tools + * can distinguish a tooling failure from an actual security finding. + * + * @returns {string} + */ +function getThreatEngineErrorMarker() { + return ""; +} + +/** + * Returns the marker template for configured engine-error message rendering. + * + * @returns {string} + */ +function getThreatEngineErrorMarkerTemplate() { + return ""; +} + +/** + * Returns the XML marker used to identify threat-detected output. + * When the reason indicates a tooling failure (agent_failure, parse_error) a + * distinct engine-error marker is returned so automated tools can distinguish + * "detection engine crashed" from "detection engine found something". + * + * @param {string | undefined | null} reason + * @returns {string} + */ +function getThreatDetectedMarker(reason) { + if (isToolingFailureReason(reason)) { + return getThreatEngineErrorMarker(); + } + return ""; +} + +/** + * Returns the marker template for configured message rendering. + * Always returns the real-threat marker; use getThreatEngineErrorMarkerTemplate() + * for tooling-failure templates where the reason is known at template-build time. + * + * @returns {string} + */ +function getThreatDetectedMarkerTemplate() { + return ""; +} + module.exports = { normalizeThreatKinds, getThreatDetectedMarker, getThreatDetectedMarkerTemplate, + getThreatEngineErrorMarker, + getThreatEngineErrorMarkerTemplate, getDetectionReasonText, isToolingFailureReason, }; diff --git a/actions/setup/js/threat_detection_warning.test.cjs b/actions/setup/js/threat_detection_warning.test.cjs index f9366c5afbb..ec84af11200 100644 --- a/actions/setup/js/threat_detection_warning.test.cjs +++ b/actions/setup/js/threat_detection_warning.test.cjs @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { normalizeThreatKinds, getThreatDetectedMarker, getThreatDetectedMarkerTemplate, getDetectionReasonText, isToolingFailureReason } from "./threat_detection_warning.cjs"; +import { normalizeThreatKinds, getThreatDetectedMarker, getThreatDetectedMarkerTemplate, getThreatEngineErrorMarker, getThreatEngineErrorMarkerTemplate, getDetectionReasonText, isToolingFailureReason } from "./threat_detection_warning.cjs"; describe("threat_detection_warning", () => { describe("normalizeThreatKinds", () => { @@ -15,10 +15,23 @@ describe("threat_detection_warning", () => { }); describe("marker helpers", () => { - it("emits the normative threat marker", () => { - expect(getThreatDetectedMarker("threat_detected,parse_error")).toBe(""); + it("emits the normative threat marker for real threats", () => { + expect(getThreatDetectedMarker("threat_detected")).toBe(""); + expect(getThreatDetectedMarker(null)).toBe(""); + expect(getThreatDetectedMarker(undefined)).toBe(""); + expect(getThreatDetectedMarker("")).toBe(""); expect(getThreatDetectedMarkerTemplate()).toBe(""); }); + + it("emits the engine-error marker for tooling failures", () => { + expect(getThreatDetectedMarker("agent_failure")).toBe(""); + expect(getThreatDetectedMarker("parse_error")).toBe(""); + }); + + it("getThreatEngineErrorMarker always returns the engine-error marker", () => { + expect(getThreatEngineErrorMarker()).toBe(""); + expect(getThreatEngineErrorMarkerTemplate()).toBe(""); + }); }); describe("getDetectionReasonText", () => { diff --git a/actions/setup/js/update_activation_comment.test.cjs b/actions/setup/js/update_activation_comment.test.cjs index faa5a85f368..65000d9b384 100644 --- a/actions/setup/js/update_activation_comment.test.cjs +++ b/actions/setup/js/update_activation_comment.test.cjs @@ -84,6 +84,10 @@ const createTestableFunction = scriptContent => { parse_error: "The threat detection results could not be parsed.", }; const reasonText = reasons[reason] || "The threat detection analysis could not be completed."; + const isEngineError = reason === "agent_failure" || reason === "parse_error"; + if (isEngineError) { + return `> [!WARNING]\n> threat detection engine error\n> The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.\n> \n>\n> ${reasonText}`; + } return `> [!CAUTION]\n> agentic threat detected\n> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.\n> \n>\n> ${reasonText}`; }, }; From dbb215a98fd7d1fd0dfed97987bf0a3c44ac6184 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:33:01 +0000 Subject: [PATCH 4/4] fix threat warning review coverage Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/messages_run_status.cjs | 7 +- .../setup/js/push_to_pull_request_branch.cjs | 14 +- .../js/push_to_pull_request_branch.test.cjs | 126 ++++++++++-------- actions/setup/js/threat_detection_warning.cjs | 26 ++++ .../js/threat_detection_warning.test.cjs | 26 +++- .../js/update_activation_comment.test.cjs | 59 +++++--- .../docs/specs/safe-outputs-specification.md | 18 ++- 7 files changed, 193 insertions(+), 83 deletions(-) diff --git a/actions/setup/js/messages_run_status.cjs b/actions/setup/js/messages_run_status.cjs index 19fb4e87a45..6796c7a60ee 100644 --- a/actions/setup/js/messages_run_status.cjs +++ b/actions/setup/js/messages_run_status.cjs @@ -8,7 +8,7 @@ */ const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cjs"); -const { getDetectionReasonText, getThreatDetectedMarkerTemplate, getThreatEngineErrorMarkerTemplate, normalizeThreatKinds, isToolingFailureReason } = require("./threat_detection_warning.cjs"); +const { getDetectionReasonText, getThreatWarningPresentation, normalizeThreatKinds, isToolingFailureReason } = require("./threat_detection_warning.cjs"); /** * Renders a message using a custom template from config or a default template. @@ -152,12 +152,13 @@ function getCommitPushedMessage(ctx) { */ function getDetectionWarningMessage(ctx) { const reasonText = getDetectionReasonText(ctx.reason); + const presentation = getThreatWarningPresentation(ctx.reason); const isEngineError = isToolingFailureReason(ctx.reason); if (isEngineError) { - const defaultTemplate = `> [!WARNING]\n> threat detection engine error\n> The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.\n> ${getThreatEngineErrorMarkerTemplate()}\n>\n>
\n> Details\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n>
`; + const defaultTemplate = `> [!${presentation.admonition}]\n> ${presentation.title}\n> ${presentation.summary}\n> ${presentation.marker}\n>\n>
\n> Details\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n>
`; return renderConfiguredMessage("detectionEngineError", defaultTemplate, { ...ctx, reasonText, threatKinds: normalizeThreatKinds(ctx.reason) }); } - const defaultTemplate = `> [!CAUTION]\n> agentic threat detected\n> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.\n> ${getThreatDetectedMarkerTemplate()}\n>\n>
\n> Details\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n>
`; + const defaultTemplate = `> [!${presentation.admonition}]\n> ${presentation.title}\n> ${presentation.summary}\n> ${presentation.marker}\n>\n>
\n> Details\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n>
`; return renderConfiguredMessage("detectionWarning", defaultTemplate, { ...ctx, reasonText, threatKinds: normalizeThreatKinds(ctx.reason) }); } diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index aa550fd2070..28410c54fb1 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -21,7 +21,7 @@ const { withGitHubHostToken } = require("./git_auth_helpers.cjs"); const { ensureFullHistoryForBundle, extractBundlePrerequisiteCommits, isShallowOrSparseCheckout, linearizeRangeAsCommit, ensureSafeDirectoryTrust } = require("./git_helpers.cjs"); const { normalizeCommitSHA } = require("./commit_sha_helpers.cjs"); const { findRepoCheckout } = require("./find_repo_checkout.cjs"); -const { getThreatDetectedMarker, isToolingFailureReason } = require("./threat_detection_warning.cjs"); +const { getThreatWarningPresentation } = require("./threat_detection_warning.cjs"); const { attachExecutionState } = require("./safe_output_execution_metadata.cjs"); const { resolveTransportPaths } = require("./resolve_transport_paths.cjs"); @@ -1304,14 +1304,12 @@ async function main(config = {}) { // For fork-backed PRs, use an owner-qualified head reference. const reviewHeadRef = pushRemoteUrl ? `${pushRepoParts.owner}:${reviewBranchName}` : reviewBranchName; const detectionReasonEnv = process.env.GH_AW_DETECTION_REASON || "unknown"; - const isEngineError = isToolingFailureReason(detectionReasonEnv); + const warning = getThreatWarningPresentation(detectionReasonEnv); const prBody = [ - isEngineError ? "> [!WARNING]" : "> [!CAUTION]", - isEngineError ? "> threat detection engine error" : "> agentic threat detected", - isEngineError - ? "> The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding." - : "> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.", - `> ${getThreatDetectedMarker(detectionReasonEnv)}`, + `> [!${warning.admonition}]`, + `> ${warning.title}`, + `> ${warning.summary}`, + `> ${warning.marker}`, ">", `> **Reason:** ${detectionReasonEnv}`, ">", diff --git a/actions/setup/js/push_to_pull_request_branch.test.cjs b/actions/setup/js/push_to_pull_request_branch.test.cjs index 1f3178f2569..ab9849b8e7c 100644 --- a/actions/setup/js/push_to_pull_request_branch.test.cjs +++ b/actions/setup/js/push_to_pull_request_branch.test.cjs @@ -253,6 +253,57 @@ describe("push_to_pull_request_branch.cjs", () => { return module; } + async function runFallbackPullRequestScenario(branch, detectionReason = "unknown") { + createPatchFile(branch); + process.env.GH_AW_DETECTION_REASON = detectionReason; + + mockExec.exec.mockResolvedValueOnce(0); // fetch + mockExec.exec.mockResolvedValueOnce(0); // rev-parse + mockExec.exec.mockResolvedValueOnce(0); // checkout + + mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "before-sha\n", stderr: "" }); // git rev-parse HEAD (before patch) + + mockExec.exec.mockResolvedValueOnce(0); // git am + + const originalGetExecOutput = mockExec.getExecOutput; + mockExec.getExecOutput = vi.fn().mockImplementation(async (cmd, args) => { + const argList = Array.isArray(args) ? args : []; + if (argList[0] === "rev-parse" && argList[1] === "origin/feature-branch^{commit}") { + return { exitCode: 0, stdout: "1111111111111111111111111111111111111111\n", stderr: "" }; + } + if (argList[0] === "rev-list" && argList[1] === "--merges") { + return { exitCode: 0, stdout: "0\n", stderr: "" }; + } + if (argList[0] === "rev-list" && argList[1] === "--parents") { + return { + exitCode: 0, + stdout: "2222222222222222222222222222222222222222 1111111111111111111111111111111111111111\n", + stderr: "", + }; + } + if (argList[0] === "ls-remote" && argList[2] === "refs/heads/feature-branch") { + return { exitCode: 0, stdout: "1111111111111111111111111111111111111111\trefs/heads/feature-branch\n", stderr: "" }; + } + if (argList[0] === "log") { + if (argList.includes(".github/workflows/")) { + return { exitCode: 0, stdout: "", stderr: "" }; + } + return { exitCode: 0, stdout: "Test commit\n", stderr: "" }; + } + if (argList[0] === "diff-tree") { + return { exitCode: 0, stdout: "", stderr: "" }; + } + return originalGetExecOutput(cmd, args); + }); + + mockGithub.graphql.mockRejectedValueOnce(new Error("GraphQL error: branch protection")); + mockExec.exec.mockRejectedValueOnce(new Error("! [rejected] feature-branch -> feature-branch (non-fast-forward)")); + + const module = await loadModule(); + const handler = await module.main({}); + return handler({ branch }, {}); + } + /** * Helper to create a valid patch file at the canonical path derived from the * message branch. The privileged handler always re-derives the patch path @@ -1171,59 +1222,7 @@ index 0000000..abc1234 }); it("should create fallback pull request on non-fast-forward push rejection by default", async () => { - const patchPath = createPatchFile("should-create-fallback-pull-request-on-non-fast-forward-push"); - - // Set up successful operations until push - mockExec.exec.mockResolvedValueOnce(0); // fetch - mockExec.exec.mockResolvedValueOnce(0); // rev-parse - mockExec.exec.mockResolvedValueOnce(0); // checkout - - mockExec.getExecOutput.mockResolvedValueOnce({ exitCode: 0, stdout: "before-sha\n", stderr: "" }); // git rev-parse HEAD (before patch) - - mockExec.exec.mockResolvedValueOnce(0); // git am - - const originalGetExecOutput = mockExec.getExecOutput; - mockExec.getExecOutput = vi.fn().mockImplementation(async (cmd, args) => { - const argList = Array.isArray(args) ? args : []; - if (argList[0] === "rev-parse" && argList[1] === "origin/feature-branch^{commit}") { - return { exitCode: 0, stdout: "1111111111111111111111111111111111111111\n", stderr: "" }; - } - if (argList[0] === "rev-list" && argList[1] === "--merges") { - return { exitCode: 0, stdout: "0\n", stderr: "" }; - } - if (argList[0] === "rev-list" && argList[1] === "--parents") { - return { - exitCode: 0, - stdout: "2222222222222222222222222222222222222222 1111111111111111111111111111111111111111\n", - stderr: "", - }; - } - if (argList[0] === "ls-remote" && argList[2] === "refs/heads/feature-branch") { - return { exitCode: 0, stdout: "1111111111111111111111111111111111111111\trefs/heads/feature-branch\n", stderr: "" }; - } - if (argList[0] === "log") { - // Pre-flight workflow check targets .github/workflows/; return empty to avoid - // short-circuiting the fallback path with a workflows_scope_required error. - if (argList.includes(".github/workflows/")) { - return { exitCode: 0, stdout: "", stderr: "" }; - } - return { exitCode: 0, stdout: "Test commit\n", stderr: "" }; - } - if (argList[0] === "diff-tree") { - return { exitCode: 0, stdout: "", stderr: "" }; - } - return originalGetExecOutput(cmd, args); - }); - - // GraphQL call fails, triggering fallback to git push - mockGithub.graphql.mockRejectedValueOnce(new Error("GraphQL error: branch protection")); - - // Fallback git push also fails with non-fast-forward - mockExec.exec.mockRejectedValueOnce(new Error("! [rejected] feature-branch -> feature-branch (non-fast-forward)")); - - const module = await loadModule(); - const handler = await module.main({}); - const result = await handler({ branch: "should-create-fallback-pull-request-on-non-fast-forward-push" }, {}); + const result = await runFallbackPullRequestScenario("should-create-fallback-pull-request-on-non-fast-forward-push"); expect(result.success).toBe(true); expect(result.fallback_used).toBe(true); @@ -1466,6 +1465,27 @@ index 0000000..abc1234 mockExec.getExecOutput = savedGetExecOutput; }); + it.each([ + ["agent_failure", "> [!WARNING]", "> threat detection engine error", "", ""], + ["threat_detected", "> [!CAUTION]", "> agentic threat detected", "", ""], + ])("should create a review PR body for %s with the correct admonition and marker", async (reason, admonition, title, expectedMarker, unexpectedMarker) => { + process.env.GH_AW_DETECTION_CONCLUSION = "warning"; + process.env.GH_AW_DETECTION_REASON = reason; + mockContext.runId = 12345; + createPatchFile(`review-branch-body-${reason}`); + + const module = await loadModule(); + const handler = await module.main({}); + const result = await handler({ branch: `review-branch-body-${reason}` }, {}); + + expect(result.success).toBe(true); + const [params] = mockGithub.rest.pulls.create.mock.calls.at(-1); + expect(params.body).toContain(admonition); + expect(params.body).toContain(title); + expect(params.body).toContain(expectedMarker); + expect(params.body).not.toContain(unexpectedMarker); + }); + it("should skip non-fatally when review branch is rejected for workflows scope (timeout variant, agent has none)", async () => { process.env.GH_AW_DETECTION_CONCLUSION = "warning"; createPatchFile("review-branch-workflows-scope-timeout"); diff --git a/actions/setup/js/threat_detection_warning.cjs b/actions/setup/js/threat_detection_warning.cjs index 3df81dfac63..ba8dadd783d 100644 --- a/actions/setup/js/threat_detection_warning.cjs +++ b/actions/setup/js/threat_detection_warning.cjs @@ -70,6 +70,31 @@ function getThreatEngineErrorMarkerTemplate() { return ""; } +/** + * Returns the review-warning presentation associated with a detection reason. + * Centralizing these fields keeps admonition copy and marker routing in sync + * across status messages, footers, and fallback pull request bodies. + * + * @param {string | undefined | null} reason + * @returns {{admonition: string, title: string, summary: string, marker: string}} + */ +function getThreatWarningPresentation(reason) { + if (isToolingFailureReason(reason)) { + return { + admonition: "WARNING", + title: "threat detection engine error", + summary: "The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.", + marker: getThreatEngineErrorMarker(), + }; + } + return { + admonition: "CAUTION", + title: "agentic threat detected", + summary: "Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.", + marker: getThreatDetectedMarker(reason), + }; +} + /** * Returns the XML marker used to identify threat-detected output. * When the reason indicates a tooling failure (agent_failure, parse_error) a @@ -99,6 +124,7 @@ function getThreatDetectedMarkerTemplate() { module.exports = { normalizeThreatKinds, + getThreatWarningPresentation, getThreatDetectedMarker, getThreatDetectedMarkerTemplate, getThreatEngineErrorMarker, diff --git a/actions/setup/js/threat_detection_warning.test.cjs b/actions/setup/js/threat_detection_warning.test.cjs index ec84af11200..18c6b68396e 100644 --- a/actions/setup/js/threat_detection_warning.test.cjs +++ b/actions/setup/js/threat_detection_warning.test.cjs @@ -1,5 +1,14 @@ import { describe, it, expect } from "vitest"; -import { normalizeThreatKinds, getThreatDetectedMarker, getThreatDetectedMarkerTemplate, getThreatEngineErrorMarker, getThreatEngineErrorMarkerTemplate, getDetectionReasonText, isToolingFailureReason } from "./threat_detection_warning.cjs"; +import { + normalizeThreatKinds, + getThreatWarningPresentation, + getThreatDetectedMarker, + getThreatDetectedMarkerTemplate, + getThreatEngineErrorMarker, + getThreatEngineErrorMarkerTemplate, + getDetectionReasonText, + isToolingFailureReason, +} from "./threat_detection_warning.cjs"; describe("threat_detection_warning", () => { describe("normalizeThreatKinds", () => { @@ -32,6 +41,21 @@ describe("threat_detection_warning", () => { expect(getThreatEngineErrorMarker()).toBe(""); expect(getThreatEngineErrorMarkerTemplate()).toBe(""); }); + + it("returns a centralized warning presentation for tooling failures and threats", () => { + expect(getThreatWarningPresentation("agent_failure")).toEqual({ + admonition: "WARNING", + title: "threat detection engine error", + summary: "The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.", + marker: "", + }); + expect(getThreatWarningPresentation("threat_detected")).toEqual({ + admonition: "CAUTION", + title: "agentic threat detected", + summary: "Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.", + marker: "", + }); + }); }); describe("getDetectionReasonText", () => { diff --git a/actions/setup/js/update_activation_comment.test.cjs b/actions/setup/js/update_activation_comment.test.cjs index 65000d9b384..1a2962b943b 100644 --- a/actions/setup/js/update_activation_comment.test.cjs +++ b/actions/setup/js/update_activation_comment.test.cjs @@ -1,12 +1,13 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { readFileSync } from "fs"; import path from "path"; +import * as threatDetectionWarning from "./threat_detection_warning.cjs"; const createTestableFunction = scriptContent => { const beforeMainCall = scriptContent.match(/^([\s\S]*?)\s*module\.exports\s*=\s*{[\s\S]*?};?\s*$/); if (!beforeMainCall) throw new Error("Could not extract script content before module.exports"); let scriptBody = beforeMainCall[1]; // Mock the error_helpers and messages_core modules - const mockRequire = module => { + const mockRequire = (module, threatDetectionWarning) => { if (module === "./error_helpers.cjs") { return { getErrorMessage: error => (error instanceof Error ? error.message : String(error)) }; } @@ -56,6 +57,11 @@ const createTestableFunction = scriptContent => { const def = "Commit pushed: [`{short_sha}`]({commit_url})"; return messages?.commitPushed ? renderTmpl(messages.commitPushed, tc) : renderTmpl(def, tc); }, + getDetectionWarningMessage: ctx => { + const reasonText = threatDetectionWarning.getDetectionReasonText(ctx.reason); + const warning = threatDetectionWarning.getThreatWarningPresentation(ctx.reason); + return `> [!${warning.admonition}]\n> ${warning.title}\n> ${warning.summary}\n> ${warning.marker}\n>\n> ${reasonText}`; + }, }; } if (module === "./templatable.cjs") { @@ -74,24 +80,17 @@ const createTestableFunction = scriptContent => { if (module === "./messages_footer.cjs") { return { getFooterMessage: ctx => `> Generated by [${ctx.workflowName}](${ctx.runUrl})`, - getDetectionCautionAlert: () => { + getDetectionCautionAlert: (workflowName, runUrl) => { const conclusion = process.env.GH_AW_DETECTION_CONCLUSION; if (conclusion !== "warning") return ""; const reason = process.env.GH_AW_DETECTION_REASON || ""; - const reasons = { - threat_detected: "Potential security threats were detected in the agent output.", - agent_failure: "The threat detection engine failed to produce results.", - parse_error: "The threat detection results could not be parsed.", - }; - const reasonText = reasons[reason] || "The threat detection analysis could not be completed."; - const isEngineError = reason === "agent_failure" || reason === "parse_error"; - if (isEngineError) { - return `> [!WARNING]\n> threat detection engine error\n> The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.\n> \n>\n> ${reasonText}`; - } - return `> [!CAUTION]\n> agentic threat detected\n> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.\n> \n>\n> ${reasonText}`; + return mockRequire("./messages_run_status.cjs", threatDetectionWarning).getDetectionWarningMessage({ workflowName, runUrl, reason }); }, }; } + if (module === "./threat_detection_warning.cjs") { + return threatDetectionWarning; + } if (module === "./workflow_metadata_helpers.cjs") { return { buildWorkflowRunUrl: (ctx, repo) => { @@ -162,7 +161,7 @@ const createTestableFunction = scriptContent => { throw new Error(`Module ${module} not mocked in test`); }; return new Function( - `\n const { github, core, context, process } = arguments[0];\n const require = ${mockRequire.toString()};\n \n ${scriptBody}\n \n return { updateActivationComment, updateActivationCommentWithCommit, updateActivationCommentWithMessage };\n ` + `\n const { github, core, context, process, threatDetectionWarning } = arguments[0];\n const mockRequire = ${mockRequire.toString()};\n const require = module => mockRequire(module, threatDetectionWarning);\n \n ${scriptBody}\n \n return { updateActivationComment, updateActivationCommentWithCommit, updateActivationCommentWithMessage };\n ` ); }; describe("update_activation_comment.cjs", () => { @@ -171,7 +170,13 @@ describe("update_activation_comment.cjs", () => { const scriptPath = path.join(process.cwd(), "update_activation_comment.cjs"), scriptContent = readFileSync(scriptPath, "utf8"); ((createFunctionFromScript = createTestableFunction(scriptContent)), - (mockDependencies = { github: { graphql: vi.fn(), request: vi.fn() }, core: { info: vi.fn(), warning: vi.fn(), setFailed: vi.fn() }, context: { repo: { owner: "testowner", repo: "testrepo" } }, process: { env: {} } })); + (mockDependencies = { + github: { graphql: vi.fn(), request: vi.fn() }, + core: { info: vi.fn(), warning: vi.fn(), setFailed: vi.fn() }, + context: { repo: { owner: "testowner", repo: "testrepo" } }, + process: { env: {} }, + threatDetectionWarning, + })); }), it("should skip comment when no activation comment ID or triggering context exists", async () => { mockDependencies.process.env.GH_AW_COMMENT_ID = ""; @@ -223,6 +228,30 @@ describe("update_activation_comment.cjs", () => { }) ); }), + it.each([ + ["agent_failure", "> threat detection engine error", "", ""], + ["threat_detected", "> agentic threat detected", "", ""], + ])("should include the %s detection banner in created activation comments", async (reason, titleLine, expectedMarker, unexpectedMarker) => { + mockDependencies.process.env.GH_AW_COMMENT_ID = ""; + mockDependencies.process.env.GH_AW_DETECTION_CONCLUSION = "warning"; + mockDependencies.process.env.GH_AW_DETECTION_REASON = reason; + mockDependencies.context = { + ...mockDependencies.context, + runId: 12345, + payload: { pull_request: { number: 10 } }, + }; + mockDependencies.github.request.mockResolvedValue({ + data: { id: 789012, html_url: "https://github.com/testowner/testrepo/issues/10#issuecomment-789012" }, + }); + + const { updateActivationComment } = createFunctionFromScript(mockDependencies); + await updateActivationComment(mockDependencies.github, mockDependencies.context, mockDependencies.core, "https://github.com/testowner/testrepo/pull/42", 42); + + const [, requestParams] = mockDependencies.github.request.mock.calls[0]; + expect(requestParams.body).toContain(titleLine); + expect(requestParams.body).toContain(expectedMarker); + expect(requestParams.body).not.toContain(unexpectedMarker); + }), it("should skip update when GH_AW_COMMENT_ID is not set and no target issue number", async () => { mockDependencies.process.env.GH_AW_COMMENT_ID = ""; const { updateActivationCommentWithMessage } = createFunctionFromScript(mockDependencies); diff --git a/docs/src/content/docs/specs/safe-outputs-specification.md b/docs/src/content/docs/specs/safe-outputs-specification.md index 47891e78e92..e88b2f45b02 100644 --- a/docs/src/content/docs/specs/safe-outputs-specification.md +++ b/docs/src/content/docs/specs/safe-outputs-specification.md @@ -4577,7 +4577,7 @@ Operations execute in: When threat detection executes in `warn` mode and reports a threat signal for a safe output, implementations MUST apply a type-specific fallback policy before any safe output side effect is committed. -**Requirement WTD1 (Reviewable Annotation)**: For safe output types classified as **Reviewable** in Table WTD-A, implementations MUST convert the output into a review-first artifact that includes all of the following: +**Requirement WTD1 (Reviewable Annotation)**: For safe output types classified as **Reviewable** in Table WTD-A, implementations MUST convert the output into a review-first artifact that includes all of the following when threat detection reports an actual threat verdict (`threat_detected`): 1. A prominent caution section: @@ -4588,7 +4588,18 @@ When threat detection executes in `warn` mode and reports a threat signal for a 2. A visible threat label string: `agentic threat detected`. 3. An XML comment marker in emitted markdown content: ``. -**Requirement WTD2 (Convertible Fallback)**: For safe output types classified as **Convertible**, implementations MUST transform the operation into the mapped Reviewable type before execution. For this specification, `push_to_pull_request_branch` (also referred to as `update-pull-request-branch`) MUST fall back to `create_pull_request` with the WTD1 caution, label, and XML marker. +**Requirement WTD1a (Threat Engine Error Annotation)**: When warn-mode threat detection produces a tooling-failure reason (`agent_failure` or `parse_error`) instead of a real threat verdict, implementations MUST emit a review-first artifact with all of the following: + +1. A prominent warning section: + + > [!WARNING] + > threat detection engine error + > The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding. + +2. A visible warning label string: `threat detection engine error`. +3. An XML comment marker in emitted markdown content: ``. + +**Requirement WTD2 (Convertible Fallback)**: For safe output types classified as **Convertible**, implementations MUST transform the operation into the mapped Reviewable type before execution. For this specification, `push_to_pull_request_branch` (also referred to as `update-pull-request-branch`) MUST fall back to `create_pull_request` with the WTD1 or WTD1a review annotation that matches the detection reason. **Requirement WTD3 (Non-Reviewable Abort)**: For safe output types classified as **Abort**, implementations MUST NOT apply the original safe output. Implementations MUST activate a threat-detected code path, emit an explicit failure summary, and return a machine-readable threat-detected error outcome. @@ -4639,7 +4650,7 @@ When threat detection executes in `warn` mode and reports a threat signal for a **Compliance Testing**: -- **T-WTD-001**: Reviewable outputs include CAUTION block, label text `agentic threat detected`, and XML comment marker. +- **T-WTD-001**: Reviewable outputs include the threat-appropriate annotation: WTD1 CAUTION block/label/marker for `threat_detected`, or WTD1a WARNING block/label/marker for `agent_failure` and `parse_error`. - **T-WTD-002**: `push_to_pull_request_branch` in warn-mode threat failure is converted to `create_pull_request`. - **T-WTD-003**: Abort-class outputs are not applied and produce threat-detected error outcomes. @@ -5448,6 +5459,7 @@ This specification revision aligns with directly relevant `CHANGELOG.md` entries - **Added**: `add_comment` status-comment reuse extension semantics in Section 7.1 for `target: "status"` behavior and issue/PR-only restrictions. - **Added**: Changelog alignment subsection mapping safe-output/reviewer changelog items to this specification revision. - **Updated**: Publication metadata to 1.21.0. +- **Clarified**: Section 10.5 now distinguishes real threat verdict annotations (``) from threat-engine tooling failures (``). **Version 1.20.0** (2026-05-15):