From fa8b5bdf5430aab08974b0d152f663c36fa2995b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 1 Aug 2026 06:38:12 +0000
Subject: [PATCH 1/8] chore: start threat detection engine error template
improvements
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 cc448118f87f9ad7a74c151aa69fff52d5e7d18b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 1 Aug 2026 06:43:58 +0000
Subject: [PATCH 2/8] improve: threat detection engine error template with
progressive disclosure
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/generate_footer.cjs | 2 +-
actions/setup/js/generate_footer.test.cjs | 4 ++--
actions/setup/js/messages.test.cjs | 2 +-
actions/setup/js/messages_run_status.cjs | 2 +-
actions/setup/md/detection_runs_comment.md | 7 +++++++
5 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/actions/setup/js/generate_footer.cjs b/actions/setup/js/generate_footer.cjs
index 77e5af6340f..e7adc3a020d 100644
--- a/actions/setup/js/generate_footer.cjs
+++ b/actions/setup/js/generate_footer.cjs
@@ -133,7 +133,7 @@ function getExpiredEntityCautionAlert(workflowName, runUrl) {
const detectionReason = process.env.GH_AW_DETECTION_REASON || "";
const reasonText = getDetectionReasonText(detectionReason);
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 `> [!WARNING]\n> **Threat Detection Engine Failure** — The analysis engine could not complete. This is a tooling failure, not a security finding.\n> ${getThreatDetectedMarker(detectionReason)}\n>\n> \n> What happened
\n>\n> ${reasonText}\n>\n> Review the [workflow run logs](${runUrl}) for details.\n> `;
}
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> `;
}
diff --git a/actions/setup/js/generate_footer.test.cjs b/actions/setup/js/generate_footer.test.cjs
index e150ea86133..a47d22bed63 100644
--- a/actions/setup/js/generate_footer.test.cjs
+++ b/actions/setup/js/generate_footer.test.cjs
@@ -486,7 +486,7 @@ 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("");
expect(result).not.toContain("> [!CAUTION]");
expect(result).not.toContain("agentic threat detected");
@@ -500,7 +500,7 @@ 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).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..bcc481b79b6 100644
--- a/actions/setup/js/messages.test.cjs
+++ b/actions/setup/js/messages.test.cjs
@@ -1411,7 +1411,7 @@ 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).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..82935f240e0 100644
--- a/actions/setup/js/messages_run_status.cjs
+++ b/actions/setup/js/messages_run_status.cjs
@@ -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 Failure** — The analysis engine could not complete. This is a tooling failure, not a security finding.\n> ${getThreatDetectedMarkerTemplate()}\n>\n> \n> What happened
\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/md/detection_runs_comment.md b/actions/setup/md/detection_runs_comment.md
index 68cf428f937..528339e2cf2 100644
--- a/actions/setup/md/detection_runs_comment.md
+++ b/actions/setup/md/detection_runs_comment.md
@@ -1,7 +1,14 @@
### {workflow_name}
+The threat detection engine could not complete analysis for this run. This is a tooling failure, not a security finding.
+
+
+Run details
+
| Field | Value |
|---|---|
| Conclusion | `{conclusion}` |
| Reason | `{reason}` |
| Run | [View run]({run_url}) |
+
+
From 55a994480aecd0524febd34de03120a04b0bcd3d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 1 Aug 2026 07:06:39 +0000
Subject: [PATCH 3/8] Apply remaining changes
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/generate_footer.cjs | 56 +++++++++++++++++--
actions/setup/js/messages_run_status.cjs | 14 +++--
actions/setup/md/threat_detection_caution.md | 12 ++++
.../setup/md/threat_detection_engine_error.md | 11 ++++
4 files changed, 81 insertions(+), 12 deletions(-)
create mode 100644 actions/setup/md/threat_detection_caution.md
create mode 100644 actions/setup/md/threat_detection_engine_error.md
diff --git a/actions/setup/js/generate_footer.cjs b/actions/setup/js/generate_footer.cjs
index e7adc3a020d..b4ed3fd45ac 100644
--- a/actions/setup/js/generate_footer.cjs
+++ b/actions/setup/js/generate_footer.cjs
@@ -1,8 +1,50 @@
// @ts-check
///
+const fs = require("fs");
+const path = require("path");
const { getDetectionReasonText, getThreatDetectedMarker, isToolingFailureReason } = require("./threat_detection_warning.cjs");
+/**
+ * Resolves the path to a named template file in the prompts directory.
+ * Uses GH_AW_PROMPTS_DIR if set, otherwise falls back to the source md/ directory.
+ * Intentionally does not import from messages_core.cjs — see getExpiredEntityCautionAlert note.
+ * @param {string} filename - Template filename (e.g. "threat_detection_engine_error.md")
+ * @returns {string} Absolute path to the template file
+ */
+function resolveLocalTemplatePath(filename) {
+ const promptsDir = process.env.GH_AW_PROMPTS_DIR;
+ if (promptsDir) return `${promptsDir}/${filename}`;
+ return path.join(__dirname, "../md", filename);
+}
+
+/**
+ * Renders a template string by replacing {key} placeholders with context values.
+ * Intentionally does not import renderTemplate from messages_core.cjs — see getExpiredEntityCautionAlert note.
+ * @param {string} template - Template string with {key} placeholders
+ * @param {Record} context - Key-value pairs for substitution
+ * @returns {string} Rendered string
+ */
+function renderLocalTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ return value !== undefined && value !== null ? String(value) : match;
+ });
+}
+
+/**
+ * Reads a template file and renders it with the given context.
+ * Intentionally does not import renderTemplateFromFile from messages_core.cjs — see getExpiredEntityCautionAlert note.
+ * @param {string} filename - Template filename (e.g. "threat_detection_engine_error.md")
+ * @param {Record} context - Key-value pairs for substitution
+ * @returns {string} Rendered template content (trailing newline trimmed)
+ */
+function renderLocalTemplateFile(filename, context) {
+ const filePath = resolveLocalTemplatePath(filename);
+ const template = fs.readFileSync(filePath, "utf8");
+ return renderLocalTemplate(template.trimEnd(), context);
+}
+
/**
* Generates a standalone workflow-id XML comment marker for searchability.
* This marker enables finding all items (issues, discussions, PRs, comments)
@@ -113,10 +155,11 @@ 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.
+ * Note: Template rendering uses local helpers (resolveLocalTemplatePath / renderLocalTemplateFile)
+ * instead of importing from messages_core.cjs, because importing messages_core.cjs (directly
+ * or transitively via messages_footer.cjs) would cause the bundler to inline
+ * '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
* threat_detection_warning.cjs to keep warning-mode messaging consistent.
@@ -132,10 +175,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 Failure** — The analysis engine could not complete. This is a tooling failure, not a security finding.\n> ${getThreatDetectedMarker(detectionReason)}\n>\n> \n> What happened
\n>\n> ${reasonText}\n>\n> Review the [workflow run logs](${runUrl}) for details.\n> `;
+ return renderLocalTemplateFile("threat_detection_engine_error.md", context);
}
- 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 renderLocalTemplateFile("threat_detection_caution.md", context);
}
/**
diff --git a/actions/setup/js/messages_run_status.cjs b/actions/setup/js/messages_run_status.cjs
index 82935f240e0..66056b73724 100644
--- a/actions/setup/js/messages_run_status.cjs
+++ b/actions/setup/js/messages_run_status.cjs
@@ -7,7 +7,7 @@
* for workflow execution notifications.
*/
-const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cjs");
+const { getMessages, renderTemplate, renderTemplateFromFile, toSnakeCase, getPromptPath } = require("./messages_core.cjs");
const { getDetectionReasonText, getThreatDetectedMarkerTemplate, normalizeThreatKinds, isToolingFailureReason } = require("./threat_detection_warning.cjs");
/**
@@ -153,12 +153,14 @@ function getCommitPushedMessage(ctx) {
function getDetectionWarningMessage(ctx) {
const reasonText = getDetectionReasonText(ctx.reason);
const isEngineError = isToolingFailureReason(ctx.reason);
- if (isEngineError) {
- const defaultTemplate = `> [!WARNING]\n> **Threat Detection Engine Failure** — The analysis engine could not complete. This is a tooling failure, not a security finding.\n> ${getThreatDetectedMarkerTemplate()}\n>\n> \n> What happened
\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 messages = getMessages();
+ const configTemplate = messages?.[messageKey];
+ if (configTemplate) {
+ return renderTemplate(configTemplate, toSnakeCase({ ...ctx, reasonText, threat_detected_marker: getThreatDetectedMarkerTemplate(), 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> `;
- return renderConfiguredMessage("detectionWarning", defaultTemplate, { ...ctx, reasonText, threatKinds: normalizeThreatKinds(ctx.reason) });
+ return renderTemplateFromFile(getPromptPath(templateFile), toSnakeCase({ ...ctx, reasonText, threat_detected_marker: getThreatDetectedMarkerTemplate(), threatKinds: normalizeThreatKinds(ctx.reason) })).trimEnd();
}
module.exports = {
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.
+>
From bda0e8dc89a20d78fe063e0f3c7806bcf2d5824c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 1 Aug 2026 07:59:24 +0000
Subject: [PATCH 4/8] refactor: extract template helpers to
prompt_file_helpers.cjs and use from generate_footer.cjs
- Create prompt_file_helpers.cjs with getPromptPath (with ../md fallback),
renderTemplate, and renderTemplateFromFile
- Update messages_core.cjs to import and re-export these from prompt_file_helpers.cjs
instead of defining them inline
- Update generate_footer.cjs to remove the three inline local helpers
(resolveLocalTemplatePath, renderLocalTemplate, renderLocalTemplateFile)
and import from prompt_file_helpers.cjs
- Update generate_footer.test.cjs to set GH_AW_PROMPTS_DIR to the source md/
directory (consistent with other test files) so template rendering tests work
when RUNNER_TEMP is set but runtime prompts aren't populated
- Update messages_core.test.cjs: the getPromptPath fallback now returns a
../md path instead of throwing
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/generate_footer.cjs | 56 +++------------------
actions/setup/js/generate_footer.test.cjs | 18 ++++++-
actions/setup/js/messages_core.cjs | 52 +-------------------
actions/setup/js/messages_core.test.cjs | 5 +-
actions/setup/js/prompt_file_helpers.cjs | 59 +++++++++++++++++++++++
5 files changed, 87 insertions(+), 103 deletions(-)
create mode 100644 actions/setup/js/prompt_file_helpers.cjs
diff --git a/actions/setup/js/generate_footer.cjs b/actions/setup/js/generate_footer.cjs
index b4ed3fd45ac..11657ed1cff 100644
--- a/actions/setup/js/generate_footer.cjs
+++ b/actions/setup/js/generate_footer.cjs
@@ -1,49 +1,8 @@
// @ts-check
///
-const fs = require("fs");
-const path = require("path");
const { getDetectionReasonText, getThreatDetectedMarker, isToolingFailureReason } = require("./threat_detection_warning.cjs");
-
-/**
- * Resolves the path to a named template file in the prompts directory.
- * Uses GH_AW_PROMPTS_DIR if set, otherwise falls back to the source md/ directory.
- * Intentionally does not import from messages_core.cjs — see getExpiredEntityCautionAlert note.
- * @param {string} filename - Template filename (e.g. "threat_detection_engine_error.md")
- * @returns {string} Absolute path to the template file
- */
-function resolveLocalTemplatePath(filename) {
- const promptsDir = process.env.GH_AW_PROMPTS_DIR;
- if (promptsDir) return `${promptsDir}/${filename}`;
- return path.join(__dirname, "../md", filename);
-}
-
-/**
- * Renders a template string by replacing {key} placeholders with context values.
- * Intentionally does not import renderTemplate from messages_core.cjs — see getExpiredEntityCautionAlert note.
- * @param {string} template - Template string with {key} placeholders
- * @param {Record} context - Key-value pairs for substitution
- * @returns {string} Rendered string
- */
-function renderLocalTemplate(template, context) {
- return template.replace(/\{(\w+)\}/g, (match, key) => {
- const value = context[key];
- return value !== undefined && value !== null ? String(value) : match;
- });
-}
-
-/**
- * Reads a template file and renders it with the given context.
- * Intentionally does not import renderTemplateFromFile from messages_core.cjs — see getExpiredEntityCautionAlert note.
- * @param {string} filename - Template filename (e.g. "threat_detection_engine_error.md")
- * @param {Record} context - Key-value pairs for substitution
- * @returns {string} Rendered template content (trailing newline trimmed)
- */
-function renderLocalTemplateFile(filename, context) {
- const filePath = resolveLocalTemplatePath(filename);
- const template = fs.readFileSync(filePath, "utf8");
- return renderLocalTemplate(template.trimEnd(), context);
-}
+const { getPromptPath, renderTemplateFromFile } = require("./prompt_file_helpers.cjs");
/**
* Generates a standalone workflow-id XML comment marker for searchability.
@@ -155,11 +114,10 @@ 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: Template rendering uses local helpers (resolveLocalTemplatePath / renderLocalTemplateFile)
- * instead of importing from messages_core.cjs, because importing messages_core.cjs (directly
- * or transitively via messages_footer.cjs) would cause the bundler to inline
- * 'GH_AW_SAFE_OUTPUT_MESSAGES:' in a warning message, breaking tests that check for env var
- * declarations.
+ * Note: Template rendering uses prompt_file_helpers.cjs instead of messages_core.cjs,
+ * because importing messages_core.cjs (directly or transitively via messages_footer.cjs)
+ * would cause the bundler to inline '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
* threat_detection_warning.cjs to keep warning-mode messaging consistent.
@@ -177,9 +135,9 @@ function getExpiredEntityCautionAlert(workflowName, runUrl) {
const reasonText = getDetectionReasonText(detectionReason);
const context = { threat_detected_marker: getThreatDetectedMarker(detectionReason), reason_text: reasonText, run_url: runUrl };
if (isToolingFailureReason(detectionReason)) {
- return renderLocalTemplateFile("threat_detection_engine_error.md", context);
+ return renderTemplateFromFile(getPromptPath("threat_detection_engine_error.md"), context).trimEnd();
}
- return renderLocalTemplateFile("threat_detection_caution.md", context);
+ 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 a47d22bed63..129706c2cbe 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");
diff --git a/actions/setup/js/messages_core.cjs b/actions/setup/js/messages_core.cjs
index 1203f513713..3b797957765 100644
--- a/actions/setup/js/messages_core.cjs
+++ b/actions/setup/js/messages_core.cjs
@@ -26,7 +26,7 @@
*/
const { getErrorMessage } = require("./error_helpers.cjs");
-const fs = require("fs");
+const { getPromptPath, renderTemplate, renderTemplateFromFile } = require("./prompt_file_helpers.cjs");
/**
* @typedef {Object} SafeOutputMessages
@@ -72,23 +72,6 @@ function getMessages() {
}
}
-/**
- * Replace placeholders in a template string with values from context.
- * Supports {key} syntax for placeholder replacement.
- * @param {string} template - Template string with {key} placeholders
- * @param {Record} context - Key-value pairs for replacement
- * @returns {string} Template with placeholders replaced
- */
-function renderTemplate(template, context) {
- return template.replace(/\{(\w+)\}/g, (match, key) => {
- const value = context[key];
- if (value === undefined || value === null) {
- return match;
- }
- return String(value);
- });
-}
-
/**
* Render a comma-separated files list into markdown inline code spans.
* - Trims each entry and drops empty segments
@@ -115,39 +98,6 @@ function renderFilesList(value) {
.join(", ");
}
-/**
- * Resolve the absolute path to a prompt template file.
- * Prefers GH_AW_PROMPTS_DIR when set, otherwise falls back to
- * ${RUNNER_TEMP}/gh-aw/prompts (the runtime location used in production).
- * Throws if neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set.
- * @param {string} name - Template filename (e.g. "agent_timeout.md")
- * @returns {string} Absolute path to the prompt template file
- */
-function getPromptPath(name) {
- const promptsDir = process.env.GH_AW_PROMPTS_DIR || (process.env.RUNNER_TEMP ? `${process.env.RUNNER_TEMP}/gh-aw/prompts` : null);
- if (!promptsDir) {
- throw new Error("Cannot resolve prompt path: neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set");
- }
- return `${promptsDir}/${name}`;
-}
-
-/**
- * Read a template file and render it with the given context.
- * Combines file loading and template rendering into a single helper.
- * @param {string} templatePath - Absolute path to the template file
- * @param {Record} context - Key-value pairs for replacement
- * @returns {string} Rendered template with placeholders replaced
- */
-function renderTemplateFromFile(templatePath, context) {
- let template;
- try {
- template = fs.readFileSync(templatePath, "utf8");
- } catch (err) {
- throw new Error(`Failed to read file ${templatePath}: ${String(err)}`, { cause: err });
- }
- return renderTemplate(template, context);
-}
-
/**
* Convert context object keys to snake_case for template rendering.
* Also keeps original camelCase keys for backwards compatibility.
diff --git a/actions/setup/js/messages_core.test.cjs b/actions/setup/js/messages_core.test.cjs
index 683ec3f53b6..9c5895d7747 100644
--- a/actions/setup/js/messages_core.test.cjs
+++ b/actions/setup/js/messages_core.test.cjs
@@ -269,9 +269,10 @@ describe("messages_core.cjs", () => {
expect(getPromptPath("bar.md")).toBe("/tmp/runner/gh-aw/prompts/bar.md");
});
- it("should throw when neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set", async () => {
+ it("should fall back to md/ directory when neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set", async () => {
const { getPromptPath } = await import("./messages_core.cjs?" + Date.now());
- expect(() => getPromptPath("any.md")).toThrow("Cannot resolve prompt path: neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set");
+ const result = getPromptPath("any.md");
+ expect(result).toMatch(/actions\/setup\/md\/any\.md$/);
});
});
diff --git a/actions/setup/js/prompt_file_helpers.cjs b/actions/setup/js/prompt_file_helpers.cjs
new file mode 100644
index 00000000000..20493f8d053
--- /dev/null
+++ b/actions/setup/js/prompt_file_helpers.cjs
@@ -0,0 +1,59 @@
+// @ts-check
+///
+
+const fs = require("fs");
+const path = require("path");
+
+/**
+ * Resolve the absolute path to a prompt template file.
+ * Prefers GH_AW_PROMPTS_DIR when set, otherwise falls back to
+ * ${RUNNER_TEMP}/gh-aw/prompts (the runtime location used in production).
+ * When neither is set, falls back to the source md/ directory relative to
+ * this file (used in local development and unit tests).
+ * @param {string} name - Template filename (e.g. "agent_timeout.md")
+ * @returns {string} Absolute path to the prompt template file
+ */
+function getPromptPath(name) {
+ const promptsDir = process.env.GH_AW_PROMPTS_DIR || (process.env.RUNNER_TEMP ? `${process.env.RUNNER_TEMP}/gh-aw/prompts` : null) || path.join(__dirname, "../md");
+ return `${promptsDir}/${name}`;
+}
+
+/**
+ * Replace placeholders in a template string with values from context.
+ * Supports {key} syntax for placeholder replacement.
+ * @param {string} template - Template string with {key} placeholders
+ * @param {Record} context - Key-value pairs for replacement
+ * @returns {string} Template with placeholders replaced
+ */
+function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ if (value === undefined || value === null) {
+ return match;
+ }
+ return String(value);
+ });
+}
+
+/**
+ * Read a template file and render it with the given context.
+ * Combines file loading and template rendering into a single helper.
+ * @param {string} templatePath - Absolute path to the template file
+ * @param {Record} context - Key-value pairs for replacement
+ * @returns {string} Rendered template with placeholders replaced
+ */
+function renderTemplateFromFile(templatePath, context) {
+ let template;
+ try {
+ template = fs.readFileSync(templatePath, "utf8");
+ } catch (err) {
+ throw new Error(`Failed to read file ${templatePath}: ${String(err)}`, { cause: err });
+ }
+ return renderTemplate(template, context);
+}
+
+module.exports = {
+ getPromptPath,
+ renderTemplate,
+ renderTemplateFromFile,
+};
From 8f769c1edb31f19dfef47daa4ae4e89510cf2e16 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 1 Aug 2026 08:14:53 +0000
Subject: [PATCH 5/8] refactor: remove prompt_file_helpers.cjs and use existing
helpers from messages_core.cjs
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/generate_footer.cjs | 9 +---
actions/setup/js/messages_core.cjs | 52 ++++++++++++++++++++-
actions/setup/js/messages_core.test.cjs | 5 +-
actions/setup/js/prompt_file_helpers.cjs | 59 ------------------------
4 files changed, 55 insertions(+), 70 deletions(-)
delete mode 100644 actions/setup/js/prompt_file_helpers.cjs
diff --git a/actions/setup/js/generate_footer.cjs b/actions/setup/js/generate_footer.cjs
index 11657ed1cff..7c1413d7c64 100644
--- a/actions/setup/js/generate_footer.cjs
+++ b/actions/setup/js/generate_footer.cjs
@@ -2,7 +2,7 @@
///
const { getDetectionReasonText, getThreatDetectedMarker, isToolingFailureReason } = require("./threat_detection_warning.cjs");
-const { getPromptPath, renderTemplateFromFile } = require("./prompt_file_helpers.cjs");
+const { getPromptPath, renderTemplateFromFile } = require("./messages_core.cjs");
/**
* Generates a standalone workflow-id XML comment marker for searchability.
@@ -114,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: Template rendering uses prompt_file_helpers.cjs instead of messages_core.cjs,
- * because importing messages_core.cjs (directly or transitively via messages_footer.cjs)
- * would cause the bundler to inline '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
diff --git a/actions/setup/js/messages_core.cjs b/actions/setup/js/messages_core.cjs
index 3b797957765..1203f513713 100644
--- a/actions/setup/js/messages_core.cjs
+++ b/actions/setup/js/messages_core.cjs
@@ -26,7 +26,7 @@
*/
const { getErrorMessage } = require("./error_helpers.cjs");
-const { getPromptPath, renderTemplate, renderTemplateFromFile } = require("./prompt_file_helpers.cjs");
+const fs = require("fs");
/**
* @typedef {Object} SafeOutputMessages
@@ -72,6 +72,23 @@ function getMessages() {
}
}
+/**
+ * Replace placeholders in a template string with values from context.
+ * Supports {key} syntax for placeholder replacement.
+ * @param {string} template - Template string with {key} placeholders
+ * @param {Record} context - Key-value pairs for replacement
+ * @returns {string} Template with placeholders replaced
+ */
+function renderTemplate(template, context) {
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ const value = context[key];
+ if (value === undefined || value === null) {
+ return match;
+ }
+ return String(value);
+ });
+}
+
/**
* Render a comma-separated files list into markdown inline code spans.
* - Trims each entry and drops empty segments
@@ -98,6 +115,39 @@ function renderFilesList(value) {
.join(", ");
}
+/**
+ * Resolve the absolute path to a prompt template file.
+ * Prefers GH_AW_PROMPTS_DIR when set, otherwise falls back to
+ * ${RUNNER_TEMP}/gh-aw/prompts (the runtime location used in production).
+ * Throws if neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set.
+ * @param {string} name - Template filename (e.g. "agent_timeout.md")
+ * @returns {string} Absolute path to the prompt template file
+ */
+function getPromptPath(name) {
+ const promptsDir = process.env.GH_AW_PROMPTS_DIR || (process.env.RUNNER_TEMP ? `${process.env.RUNNER_TEMP}/gh-aw/prompts` : null);
+ if (!promptsDir) {
+ throw new Error("Cannot resolve prompt path: neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set");
+ }
+ return `${promptsDir}/${name}`;
+}
+
+/**
+ * Read a template file and render it with the given context.
+ * Combines file loading and template rendering into a single helper.
+ * @param {string} templatePath - Absolute path to the template file
+ * @param {Record} context - Key-value pairs for replacement
+ * @returns {string} Rendered template with placeholders replaced
+ */
+function renderTemplateFromFile(templatePath, context) {
+ let template;
+ try {
+ template = fs.readFileSync(templatePath, "utf8");
+ } catch (err) {
+ throw new Error(`Failed to read file ${templatePath}: ${String(err)}`, { cause: err });
+ }
+ return renderTemplate(template, context);
+}
+
/**
* Convert context object keys to snake_case for template rendering.
* Also keeps original camelCase keys for backwards compatibility.
diff --git a/actions/setup/js/messages_core.test.cjs b/actions/setup/js/messages_core.test.cjs
index 9c5895d7747..683ec3f53b6 100644
--- a/actions/setup/js/messages_core.test.cjs
+++ b/actions/setup/js/messages_core.test.cjs
@@ -269,10 +269,9 @@ describe("messages_core.cjs", () => {
expect(getPromptPath("bar.md")).toBe("/tmp/runner/gh-aw/prompts/bar.md");
});
- it("should fall back to md/ directory when neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set", async () => {
+ it("should throw when neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set", async () => {
const { getPromptPath } = await import("./messages_core.cjs?" + Date.now());
- const result = getPromptPath("any.md");
- expect(result).toMatch(/actions\/setup\/md\/any\.md$/);
+ expect(() => getPromptPath("any.md")).toThrow("Cannot resolve prompt path: neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set");
});
});
diff --git a/actions/setup/js/prompt_file_helpers.cjs b/actions/setup/js/prompt_file_helpers.cjs
deleted file mode 100644
index 20493f8d053..00000000000
--- a/actions/setup/js/prompt_file_helpers.cjs
+++ /dev/null
@@ -1,59 +0,0 @@
-// @ts-check
-///
-
-const fs = require("fs");
-const path = require("path");
-
-/**
- * Resolve the absolute path to a prompt template file.
- * Prefers GH_AW_PROMPTS_DIR when set, otherwise falls back to
- * ${RUNNER_TEMP}/gh-aw/prompts (the runtime location used in production).
- * When neither is set, falls back to the source md/ directory relative to
- * this file (used in local development and unit tests).
- * @param {string} name - Template filename (e.g. "agent_timeout.md")
- * @returns {string} Absolute path to the prompt template file
- */
-function getPromptPath(name) {
- const promptsDir = process.env.GH_AW_PROMPTS_DIR || (process.env.RUNNER_TEMP ? `${process.env.RUNNER_TEMP}/gh-aw/prompts` : null) || path.join(__dirname, "../md");
- return `${promptsDir}/${name}`;
-}
-
-/**
- * Replace placeholders in a template string with values from context.
- * Supports {key} syntax for placeholder replacement.
- * @param {string} template - Template string with {key} placeholders
- * @param {Record} context - Key-value pairs for replacement
- * @returns {string} Template with placeholders replaced
- */
-function renderTemplate(template, context) {
- return template.replace(/\{(\w+)\}/g, (match, key) => {
- const value = context[key];
- if (value === undefined || value === null) {
- return match;
- }
- return String(value);
- });
-}
-
-/**
- * Read a template file and render it with the given context.
- * Combines file loading and template rendering into a single helper.
- * @param {string} templatePath - Absolute path to the template file
- * @param {Record} context - Key-value pairs for replacement
- * @returns {string} Rendered template with placeholders replaced
- */
-function renderTemplateFromFile(templatePath, context) {
- let template;
- try {
- template = fs.readFileSync(templatePath, "utf8");
- } catch (err) {
- throw new Error(`Failed to read file ${templatePath}: ${String(err)}`, { cause: err });
- }
- return renderTemplate(template, context);
-}
-
-module.exports = {
- getPromptPath,
- renderTemplate,
- renderTemplateFromFile,
-};
From 7a731b9e241089f9cd21f10c3bfa5ae3a5c5622b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 1 Aug 2026 09:14:18 +0000
Subject: [PATCH 6/8] fix: add missing threat detection template copies to
create_pull_request test setup
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
actions/setup/js/create_pull_request.test.cjs | 2 ++
1 file changed, 2 insertions(+)
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;
From 7245cdaacc221e7a1bcf8434ec743b9dea7fe52d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 1 Aug 2026 19:04:41 +0000
Subject: [PATCH 7/8] fix: neutral detection_runs_comment summary and stronger
test assertions for progressive disclosure
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
actions/setup/js/generate_footer.test.cjs | 6 ++++--
actions/setup/js/messages.test.cjs | 3 ++-
actions/setup/md/detection_runs_comment.md | 2 +-
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/actions/setup/js/generate_footer.test.cjs b/actions/setup/js/generate_footer.test.cjs
index 129706c2cbe..2db01ca37d7 100644
--- a/actions/setup/js/generate_footer.test.cjs
+++ b/actions/setup/js/generate_footer.test.cjs
@@ -502,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 Failure");
+ expect(result).toContain("**Threat Detection Engine Failure**");
+ expect(result).toContain("What happened");
expect(result).toContain("");
expect(result).not.toContain("> [!CAUTION]");
expect(result).not.toContain("agentic threat detected");
@@ -516,7 +517,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 Failure");
+ expect(result).toContain("**Threat Detection Engine Failure**");
+ expect(result).toContain("What happened");
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 bcc481b79b6..2019c3c8624 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 Failure");
+ expect(result).toContain("**Threat Detection Engine Failure**");
+ expect(result).toContain("What happened");
expect(result).not.toContain("> [!CAUTION]");
expect(result).not.toContain("agentic threat detected");
expect(result).toContain("threat detection engine failed");
diff --git a/actions/setup/md/detection_runs_comment.md b/actions/setup/md/detection_runs_comment.md
index 528339e2cf2..edb408ab6ac 100644
--- a/actions/setup/md/detection_runs_comment.md
+++ b/actions/setup/md/detection_runs_comment.md
@@ -1,6 +1,6 @@
### {workflow_name}
-The threat detection engine could not complete analysis for this run. This is a tooling failure, not a security finding.
+Threat detection produced a **{conclusion}** result for this run.
Run details
From 8f8b3b9ed39f43eac460f0ebf482eec95181cddc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 1 Aug 2026 19:38:48 +0000
Subject: [PATCH 8/8] fix: add missing threat detection template files to
handle_agent_failure test setup
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/handle_agent_failure.test.cjs | 8 ++++++++
1 file changed, 8 insertions(+)
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";