diff --git a/.github/workflows/smoke-checkout-pr-dispatch.lock.yml b/.github/workflows/smoke-checkout-pr-dispatch.lock.yml
index da8cf360d0b..bb61f3eb769 100644
--- a/.github/workflows/smoke-checkout-pr-dispatch.lock.yml
+++ b/.github/workflows/smoke-checkout-pr-dispatch.lock.yml
@@ -880,7 +880,7 @@ jobs:
# --allow-tool shell(wc)
# --allow-tool shell(yq)
# --allow-tool write
- timeout-minutes: 10
+ timeout-minutes: 20
run: |
set -o pipefail
printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt
@@ -927,7 +927,7 @@ jobs:
GH_AW_PHASE: agent
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
- GH_AW_TIMEOUT_MINUTES: 10
+ GH_AW_TIMEOUT_MINUTES: 20
GH_AW_VERSION: dev
GITHUB_API_URL: ${{ github.api_url }}
GITHUB_AW: true
diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs
index 58f459c6277..e17d52ec36d 100644
--- a/actions/setup/js/create_pull_request.test.cjs
+++ b/actions/setup/js/create_pull_request.test.cjs
@@ -3593,6 +3593,8 @@ describe("create_pull_request - threat detection caution", () => {
copyPromptTemplate(promptsDir, "manifest_protection_request_review.md");
copyPromptTemplate(promptsDir, "manifest_protection_request_changes_review.md");
copyPromptTemplate(promptsDir, "threat_warning_request_changes_review.md");
+ copyPromptTemplate(promptsDir, "threat_detection_caution.md");
+ copyPromptTemplate(promptsDir, "threat_detection_engine_error.md");
copyPromptTemplate(promptsDir, "safe_outputs_disclosure_header.md");
process.env.GH_AW_PROMPTS_DIR = promptsDir;
diff --git a/actions/setup/js/generate_footer.cjs b/actions/setup/js/generate_footer.cjs
index 77e5af6340f..7c1413d7c64 100644
--- a/actions/setup/js/generate_footer.cjs
+++ b/actions/setup/js/generate_footer.cjs
@@ -2,6 +2,7 @@
///
const { getDetectionReasonText, getThreatDetectedMarker, isToolingFailureReason } = require("./threat_detection_warning.cjs");
+const { getPromptPath, renderTemplateFromFile } = require("./messages_core.cjs");
/**
* Generates a standalone workflow-id XML comment marker for searchability.
@@ -113,12 +114,7 @@ function generateXMLMarker(workflowName, runUrl) {
* admonition is used so reviewers can distinguish "detection engine crashed" from "detection
* engine found something". Actual threat findings (threat_detected) keep [!CAUTION].
*
- * Note: This function is intentionally kept inline (not imported from messages_footer.cjs)
- * because importing messages_footer.cjs here would cause the bundler to inline
- * messages_core.cjs which contains 'GH_AW_SAFE_OUTPUT_MESSAGES:' in a warning message,
- * breaking tests that check for env var declarations.
- *
- * Warning reason text and threat marker formatting are centralized in
+ * Note: Warning reason text and threat marker formatting are centralized in
* threat_detection_warning.cjs to keep warning-mode messaging consistent.
*
* @param {string} workflowName - Name of the workflow
@@ -132,10 +128,11 @@ function getExpiredEntityCautionAlert(workflowName, runUrl) {
}
const detectionReason = process.env.GH_AW_DETECTION_REASON || "";
const reasonText = getDetectionReasonText(detectionReason);
+ const context = { threat_detected_marker: getThreatDetectedMarker(detectionReason), reason_text: reasonText, run_url: runUrl };
if (isToolingFailureReason(detectionReason)) {
- 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> ${getThreatDetectedMarker(detectionReason)}\n>\n> \n> Details
\n>\n> ${reasonText}\n>\n> Review the [workflow run logs](${runUrl}) for details.\n> `;
+ return renderTemplateFromFile(getPromptPath("threat_detection_engine_error.md"), context).trimEnd();
}
- 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> ${getThreatDetectedMarker(detectionReason)}\n>\n> \n> Details
\n>\n> ${reasonText}\n>\n> Review the [workflow run logs](${runUrl}) for details.\n> `;
+ return renderTemplateFromFile(getPromptPath("threat_detection_caution.md"), context).trimEnd();
}
/**
diff --git a/actions/setup/js/generate_footer.test.cjs b/actions/setup/js/generate_footer.test.cjs
index ebea285cdc9..618ee17f1c2 100644
--- a/actions/setup/js/generate_footer.test.cjs
+++ b/actions/setup/js/generate_footer.test.cjs
@@ -1,4 +1,6 @@
-import { describe, it, expect, beforeEach, vi } from "vitest";
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+import { fileURLToPath } from "url";
+import path from "path";
// Mock the global objects that GitHub Actions provides
const mockCore = {
@@ -337,8 +339,14 @@ describe("generate_footer.cjs", () => {
describe("generateExpiredEntityFooter", () => {
let generateExpiredEntityFooter;
let getExpiredEntityCautionAlert;
+ let originalPromptsDir;
beforeEach(async () => {
+ // Point GH_AW_PROMPTS_DIR to the source md/ directory so getPromptPath()
+ // resolves to the local templates. This is needed because RUNNER_TEMP may be
+ // set but the runtime prompts directory is not populated in the test environment.
+ originalPromptsDir = process.env.GH_AW_PROMPTS_DIR;
+ process.env.GH_AW_PROMPTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "../md");
// Reset modules and import fresh
vi.resetModules();
const freshModule = await import("./generate_footer.cjs");
@@ -346,6 +354,14 @@ describe("generate_footer.cjs", () => {
getExpiredEntityCautionAlert = freshModule.getExpiredEntityCautionAlert;
});
+ afterEach(() => {
+ if (originalPromptsDir !== undefined) {
+ process.env.GH_AW_PROMPTS_DIR = originalPromptsDir;
+ } else {
+ delete process.env.GH_AW_PROMPTS_DIR;
+ }
+ });
+
it("should generate footer with 'Closed by' wording and workflow link", () => {
const result = generateExpiredEntityFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "test-workflow");
@@ -486,7 +502,8 @@ describe("generate_footer.cjs", () => {
const result = getExpiredEntityCautionAlert("Test Workflow", "https://github.com/test/repo/actions/runs/123");
expect(result).toContain("> [!WARNING]");
- expect(result).toContain("threat detection engine error");
+ expect(result).toContain("**Threat Detection Engine Failure**");
+ expect(result).toContain("What happened");
expect(result).toContain("");
expect(result).not.toContain("");
expect(result).not.toContain("> [!CAUTION]");
@@ -501,7 +518,8 @@ describe("generate_footer.cjs", () => {
const result = getExpiredEntityCautionAlert("Test Workflow", "https://github.com/test/repo/actions/runs/123");
expect(result).toContain("> [!WARNING]");
- expect(result).toContain("threat detection engine error");
+ expect(result).toContain("**Threat Detection Engine Failure**");
+ expect(result).toContain("What happened");
expect(result).toContain("");
expect(result).not.toContain("");
expect(result).not.toContain("> [!CAUTION]");
diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs
index 31ec2878f7d..ecc666b34cb 100644
--- a/actions/setup/js/handle_agent_failure.test.cjs
+++ b/actions/setup/js/handle_agent_failure.test.cjs
@@ -184,6 +184,14 @@ describe("handle_agent_failure", () => {
fs.writeFileSync(path.join(promptsDir, "daily_cap_rollup_issue.md"), "Daily cap rollup issue body cap={cap} window={window_hours}");
fs.writeFileSync(path.join(promptsDir, "daily_cap_rollup_comment.md"), "Failure suppressed workflow={workflow_name} run={run_url} categories={summary} cap={cap} window={window_hours}h");
fs.writeFileSync(path.join(promptsDir, "optimize_token_consumption_context.md"), "OPTIMIZE CONTEXT guardrail={guardrail_name} run={run_url}");
+ fs.writeFileSync(
+ path.join(promptsDir, "threat_detection_caution.md"),
+ "> [!CAUTION]\n> agentic threat detected\n> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.\n> {threat_detected_marker}\n>\n> \n> Details
\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n> "
+ );
+ fs.writeFileSync(
+ path.join(promptsDir, "threat_detection_engine_error.md"),
+ "> [!WARNING]\n> **Threat Detection Engine Failure** — The analysis engine could not complete. This is a tooling failure, not a security finding.\n> {threat_detected_marker}\n>\n> \n> What happened
\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n> "
+ );
process.env.RUNNER_TEMP = tmpDir;
process.env.GH_AW_WORKFLOW_NAME = "Test Workflow";
diff --git a/actions/setup/js/messages.test.cjs b/actions/setup/js/messages.test.cjs
index d19c06eec65..f4e79aac848 100644
--- a/actions/setup/js/messages.test.cjs
+++ b/actions/setup/js/messages.test.cjs
@@ -1411,7 +1411,8 @@ describe("messages.cjs", () => {
const result = getDetectionCautionAlert("Test Workflow", "https://github.com/test/repo/actions/runs/123");
expect(result).toContain("> [!WARNING]");
- expect(result).toContain("threat detection engine error");
+ expect(result).toContain("**Threat Detection Engine Failure**");
+ expect(result).toContain("What happened");
expect(result).toContain("");
expect(result).not.toContain("");
expect(result).not.toContain("> [!CAUTION]");
diff --git a/actions/setup/js/messages_run_status.cjs b/actions/setup/js/messages_run_status.cjs
index 6796c7a60ee..2834b3e7be8 100644
--- a/actions/setup/js/messages_run_status.cjs
+++ b/actions/setup/js/messages_run_status.cjs
@@ -7,8 +7,8 @@
* for workflow execution notifications.
*/
-const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cjs");
-const { getDetectionReasonText, getThreatWarningPresentation, normalizeThreatKinds, isToolingFailureReason } = require("./threat_detection_warning.cjs");
+const { getMessages, renderTemplate, renderTemplateFromFile, toSnakeCase, getPromptPath } = require("./messages_core.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.
@@ -152,14 +152,16 @@ 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 = `> [!${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 templateFile = isEngineError ? "threat_detection_engine_error.md" : "threat_detection_caution.md";
+ const messageKey = isEngineError ? "detectionEngineError" : "detectionWarning";
+ const markerTemplate = isEngineError ? getThreatEngineErrorMarkerTemplate() : getThreatDetectedMarkerTemplate();
+ const messages = getMessages();
+ const configTemplate = messages?.[messageKey];
+ if (configTemplate) {
+ return renderTemplate(configTemplate, toSnakeCase({ ...ctx, reasonText, threat_detected_marker: markerTemplate, threatKinds: normalizeThreatKinds(ctx.reason) }));
}
- 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) });
+ return renderTemplateFromFile(getPromptPath(templateFile), toSnakeCase({ ...ctx, reasonText, threat_detected_marker: markerTemplate, threatKinds: normalizeThreatKinds(ctx.reason) })).trimEnd();
}
module.exports = {
diff --git a/actions/setup/md/detection_runs_comment.md b/actions/setup/md/detection_runs_comment.md
index 68cf428f937..edb408ab6ac 100644
--- a/actions/setup/md/detection_runs_comment.md
+++ b/actions/setup/md/detection_runs_comment.md
@@ -1,7 +1,14 @@
### {workflow_name}
+Threat detection produced a **{conclusion}** result for this run.
+
+
+Run details
+
| Field | Value |
|---|---|
| Conclusion | `{conclusion}` |
| Reason | `{reason}` |
| Run | [View run]({run_url}) |
+
+
diff --git a/actions/setup/md/threat_detection_caution.md b/actions/setup/md/threat_detection_caution.md
new file mode 100644
index 00000000000..b3577a7703b
--- /dev/null
+++ b/actions/setup/md/threat_detection_caution.md
@@ -0,0 +1,12 @@
+> [!CAUTION]
+> agentic threat detected
+> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.
+> {threat_detected_marker}
+>
+>
+> Details
+>
+> {reason_text}
+>
+> Review the [workflow run logs]({run_url}) for details.
+>
diff --git a/actions/setup/md/threat_detection_engine_error.md b/actions/setup/md/threat_detection_engine_error.md
new file mode 100644
index 00000000000..d910df5df87
--- /dev/null
+++ b/actions/setup/md/threat_detection_engine_error.md
@@ -0,0 +1,11 @@
+> [!WARNING]
+> **Threat Detection Engine Failure** — The analysis engine could not complete. This is a tooling failure, not a security finding.
+> {threat_detected_marker}
+>
+>
+> What happened
+>
+> {reason_text}
+>
+> Review the [workflow run logs]({run_url}) for details.
+>