Created by GitHub Ace · View Session
Summary
On the external-detector path (features: gh-aw-detection: true), actions/setup/sh/conclude_threat_detection.sh re-implements the conclusion contract in bash and exits before ever invoking threat-detect conclude on the two branches where the detector's diagnostics matter most. The binary's equivalent branches are therefore dead code in production, and the two implementations have drifted.
Depends on the v0.4.0 pin (#50644).
Current code
actions/setup/sh/conclude_threat_detection.sh (46 lines total):
if [ "${RUN_DETECTION:-false}" != "true" ]; then
echo "conclusion=skipped" >> "${GITHUB_OUTPUT}"; ...; exit 0
fi
if [ ! -f "${RESULT_FILE}" ]; then
...
if [ "${continue_on_error}" = "true" ]; then ... exit 0; fi
echo "ERR_SYSTEM: ❌ ${result_message}"; exit 1
fi
threat-detect conclude --result-file "${RESULT_FILE}" # happy path only
What is lost today
| Gap |
Effect |
Upstream spec rule |
| Status-reason mapping bypassed |
Every failure collapses to agent_failure. invalid_report_exhausted and output_write_error never map to parse_error, discarding the granularity #48940 keys severity off. Ironically the script does grep the THREAT_DETECTION_STATUS: line — only to interpolate it into a message string. |
TD-20b |
| Diagnostics bypassed |
No recursive result-directory listing, no detection-log line/byte stats, no echoed THREAT_DETECTION_STATUS: / THREAT_DETECTION_RESULT: marker lines |
TD-20d |
| Verdict step summary bypassed |
No summary block for skipped or engine-failure outcomes — so the new <!-- gh-aw-threat-engine-error --> marker and "Threat Detection Engine Failure" title from v0.4.0 are never emitted on exactly the outcomes they exist for |
TD-20h, TD-20i |
| Hard-fail rule bypassed |
conclude (and the inline .cjs path) hard-fail when DETECTION_AGENTIC_EXECUTION_OUTCOME=failure even in warn mode. The script warns and exits 0 → fails open. A failed detection execution can currently pass the gate on the external path where the inline path would block. |
TD-20b |
| Skip branch violates env contract |
Writes GITHUB_OUTPUT only, never exports GH_AW_DETECTION_CONCLUSION / GH_AW_DETECTION_REASON to GITHUB_ENV |
U-18 |
continue_on_error normalization drift |
Script uses ${...:-true} + lowercasing; detector uses != "false". Two implementations, subtly different. |
— |
Note the last row is a real divergence risk, and the hard-fail row is a security-relevant fail-open, not a cosmetic difference.
threat-detect conclude already covers every branch
Per upstream, conclude in v0.4.0 handles:
RUN_DETECTION != "true" → conclusion=skipped, success=true, reason=, plus the skipped verdict summary block
- missing / unreadable / malformed result file → status-log reason mapping, full diagnostics, engine-error marker + summary block
- warn mode via
GH_AW_DETECTION_CONTINUE_ON_ERROR != "false", with the DETECTION_AGENTIC_EXECUTION_OUTCOME=failure hard-fail override
- writes
conclusion / reason / success to $GITHUB_OUTPUT and exports GH_AW_DETECTION_CONCLUSION / GH_AW_DETECTION_REASON to $GITHUB_ENV
- exits 0 (proceed) / 1 (block), matching the current gate contract
- defaults
--detection-log to <result-file-dir>/detection.log — verified byte-identical to constants.ThreatDetectionLogPath and the script's ${RESULT_DIR}/detection.log
- defaults
--step-summary to ambient $GITHUB_STEP_SUMMARY
Proposed change
Reduce the script to roughly:
RESULT_FILE="${1:-/tmp/gh-aw/threat-detection/detection_result.json}"
RESULT_DIR="$(dirname "${RESULT_FILE}")"
DETECTION_LOG_FILE="${DETECTION_LOG_FILE:-${RESULT_DIR}/detection.log}"
if ! command -v threat-detect >/dev/null 2>&1; then
# the one failure conclude cannot handle itself
...
fi
threat-detect conclude \
--result-file "${RESULT_FILE}" \
--detection-log "${DETECTION_LOG_FILE}"
Pass --detection-log explicitly even though the default matches, so the contract is robust to path-layout changes and visible in the compiled lock file.
Prerequisite already satisfied: delegating on every branch requires threat-detect on the host PATH in all cases. install_threat_detect_binary.sh installs to /usr/local/bin (or $HOME/.local/bin + $GITHUB_PATH in rootless mode) on the runner before the AWF run, so this holds. Keep the command -v guard as the sole remaining shell-side special case.
Open behavioral question to settle in this issue
threat-detect conclude has a fail-closed rule the .cjs does not:
mustFail := c.executionFailed && (reason == "agent_failure" || reason == "parse_error")
i.e. in warn mode it still fails closed when the engine step also failed. setDetectionFailure in parse_threat_detection_results.cjs always warns and proceeds, per ADR 29031-threat-detection-resilience-in-warn-mode. Delegating makes this a behavior change for every workflow currently in warn mode, appearing silently the moment a workflow sets gh-aw-detection: true.
Two options, and upstream is happy either way — they just want the decision recorded:
- Upstream drops
mustFail to match ADR 29031 exactly.
- gh-aw adopts it on the inline path too, and ADR 29031 is amended.
This decision must be made before merging. Option 2 is the safer security posture but is a behavior change on the inline path and requires an ADR amendment.
Acceptance criteria
References
Summary
On the external-detector path (
features: gh-aw-detection: true),actions/setup/sh/conclude_threat_detection.shre-implements the conclusion contract in bash andexits before ever invokingthreat-detect concludeon the two branches where the detector's diagnostics matter most. The binary's equivalent branches are therefore dead code in production, and the two implementations have drifted.Depends on the
v0.4.0pin (#50644).Current code
actions/setup/sh/conclude_threat_detection.sh(46 lines total):What is lost today
agent_failure.invalid_report_exhaustedandoutput_write_errornever map toparse_error, discarding the granularity #48940 keys severity off. Ironically the script does grep theTHREAT_DETECTION_STATUS:line — only to interpolate it into a message string.THREAT_DETECTION_STATUS:/THREAT_DETECTION_RESULT:marker linesskippedor engine-failure outcomes — so the new<!-- gh-aw-threat-engine-error -->marker and "Threat Detection Engine Failure" title from v0.4.0 are never emitted on exactly the outcomes they exist forconclude(and the inline.cjspath) hard-fail whenDETECTION_AGENTIC_EXECUTION_OUTCOME=failureeven in warn mode. The script warns and exits 0 → fails open. A failed detection execution can currently pass the gate on the external path where the inline path would block.GITHUB_OUTPUTonly, never exportsGH_AW_DETECTION_CONCLUSION/GH_AW_DETECTION_REASONtoGITHUB_ENVcontinue_on_errornormalization drift${...:-true}+ lowercasing; detector uses!= "false". Two implementations, subtly different.Note the last row is a real divergence risk, and the hard-fail row is a security-relevant fail-open, not a cosmetic difference.
threat-detect concludealready covers every branchPer upstream,
concludein v0.4.0 handles:RUN_DETECTION != "true"→conclusion=skipped,success=true,reason=, plus the skipped verdict summary blockGH_AW_DETECTION_CONTINUE_ON_ERROR != "false", with theDETECTION_AGENTIC_EXECUTION_OUTCOME=failurehard-fail overrideconclusion/reason/successto$GITHUB_OUTPUTand exportsGH_AW_DETECTION_CONCLUSION/GH_AW_DETECTION_REASONto$GITHUB_ENV--detection-logto<result-file-dir>/detection.log— verified byte-identical toconstants.ThreatDetectionLogPathand the script's${RESULT_DIR}/detection.log--step-summaryto ambient$GITHUB_STEP_SUMMARYProposed change
Reduce the script to roughly:
Pass
--detection-logexplicitly even though the default matches, so the contract is robust to path-layout changes and visible in the compiled lock file.Prerequisite already satisfied: delegating on every branch requires
threat-detecton the host PATH in all cases.install_threat_detect_binary.shinstalls to/usr/local/bin(or$HOME/.local/bin+$GITHUB_PATHin rootless mode) on the runner before the AWF run, so this holds. Keep thecommand -vguard as the sole remaining shell-side special case.Open behavioral question to settle in this issue
threat-detect concludehas a fail-closed rule the.cjsdoes not:i.e. in warn mode it still fails closed when the engine step also failed.
setDetectionFailureinparse_threat_detection_results.cjsalways warns and proceeds, per ADR29031-threat-detection-resilience-in-warn-mode. Delegating makes this a behavior change for every workflow currently in warn mode, appearing silently the moment a workflow setsgh-aw-detection: true.Two options, and upstream is happy either way — they just want the decision recorded:
mustFailto match ADR 29031 exactly.This decision must be made before merging. Option 2 is the safer security posture but is a behavior change on the inline path and requires an ADR amendment.
Acceptance criteria
mustFaildecision recorded (ADR amendment or upstream issue comment).command -v threat-detectguard remains.--detection-logpassed explicitly.pkg/workflow/threat_detection_conclude_script_test.goupdated for the new shape.detection.logis actually written atconstants.ThreatDetectionLogPathbefore the conclude step runs (the AWF log tee should already do this — confirm).make recompile.References
threat-detect conclude, with divergent (and partly broken) semantics gh-aw-threat-detection#694parse_erroris unreachable on the external path — THREAT_DETECTION_STATUS reasons are collapsed toagent_failuregh-aw-threat-detection#693 (reason mapping table)threat-detect concludeemits far less diagnostic output than parse_threat_detection_results.cjs gh-aw-threat-detection#692 (diagnostics)