Created by GitHub Ace · View Session
Part of the gh-aw threat-detection parity review (see tracking issue).
Problem
The gh-aw job-output contract (conclusion / success / reason + GH_AW_DETECTION_*) is implemented twice on the external detector path, and the two implementations disagree.
Layer 1 — actions/setup/sh/conclude_threat_detection.sh (gh-aw):
if [ "${RUN_DETECTION:-false}" != "true" ]; then ...skipped...; exit 0; fi
if [ ! -f "${RESULT_FILE}" ]; then
...
if [ "${continue_on_error}" = "true" ]; then
echo "::warning::${result_message}; continuing..."
echo "conclusion=warning" >> "${GITHUB_OUTPUT}"
echo "success=false" >> "${GITHUB_OUTPUT}"
echo "reason=agent_failure" >> "${GITHUB_OUTPUT}"
exit 0
fi
echo "ERR_SYSTEM: ❌ ${result_message}"
exit 1
fi
threat-detect conclude --result-file "${RESULT_FILE}"
Layer 2 — cmd/threat-detect/conclude.go, which reimplements the skip branch, the missing-file branch, the warn-mode branch and the output/env writes.
Divergences
-
Dead code. The shell short-circuits RUN_DETECTION != true and the missing-result-file case, so conclude.go's fs.ErrNotExist → agent_failure/ERR_SYSTEM branch and its skipped branch are effectively unreachable in production. They are tested but never exercised.
-
Strict mode drops the outputs. In the shell's continue_on_error=false missing-file branch, it exit 1s without writing conclusion or reason to $GITHUB_OUTPUT. Downstream, needs.detection.outputs.detection_reason is the empty string, so getDetectionWarningMessage() / getDetectionReasonText() fall through to the generic "The threat detection analysis could not be completed." and isToolingFailureReason("") is false — i.e. a strict-mode infrastructure failure would render as [!CAUTION] agentic threat detected, exactly the confusion gh-aw#48940 set out to fix.
-
Fail-closed divergence. conclude.go has a rule the gh-aw .cjs does not:
mustFail := c.executionFailed && (reason == "agent_failure" || reason == "parse_error")
In warn mode, gh-aw's setDetectionFailure always warns and proceeds, for every reason (this is the explicit intent of ADR 29031-threat-detection-resilience-in-warn-mode). We fail closed when the engine step also failed. This was a deliberate hardening choice on our side, but it is a behavior change for every workflow currently running warn mode, and it is not documented as such anywhere in gh-aw's docs. It needs to be either upstreamed into the inline path or reverted, not left as a silent difference that appears when a workflow flips to gh-aw-detection: true.
-
GH_AW_DETECTION_CONCLUSION / GH_AW_DETECTION_REASON are exported to $GITHUB_ENV by conclude.go but not by the shell's branches — inconsistent for any later step in the detection job.
Proposed work
- Collapse to a single implementation:
threat-detect conclude owns the whole contract (skip, missing file, unreadable file, malformed file, verdict). Reduce conclude_threat_detection.sh to a thin invocation, or drop it.
- Fix (2) regardless — strict-mode infrastructure failure must still set
reason=agent_failure and conclusion=failure.
- Make an explicit, documented decision on (3). If we keep
mustFail, it must be documented in specs/threat-detection-spec.md and raised with gh-aw so the inline path matches; if not, remove it.
- Add conformance tests covering both layers' matrix:
{RUN_DETECTION} × {result file present/absent/malformed} × {warn/strict} × {execution outcome}.
Part of the gh-aw threat-detection parity review (see tracking issue).
Problem
The gh-aw job-output contract (
conclusion/success/reason+GH_AW_DETECTION_*) is implemented twice on the external detector path, and the two implementations disagree.Layer 1 —
actions/setup/sh/conclude_threat_detection.sh(gh-aw):Layer 2 —
cmd/threat-detect/conclude.go, which reimplements the skip branch, the missing-file branch, the warn-mode branch and the output/env writes.Divergences
Dead code. The shell short-circuits
RUN_DETECTION != trueand the missing-result-file case, soconclude.go'sfs.ErrNotExist→agent_failure/ERR_SYSTEMbranch and itsskippedbranch are effectively unreachable in production. They are tested but never exercised.Strict mode drops the outputs. In the shell's
continue_on_error=falsemissing-file branch, itexit 1s without writingconclusionorreasonto$GITHUB_OUTPUT. Downstream,needs.detection.outputs.detection_reasonis the empty string, sogetDetectionWarningMessage()/getDetectionReasonText()fall through to the generic "The threat detection analysis could not be completed." andisToolingFailureReason("")isfalse— i.e. a strict-mode infrastructure failure would render as[!CAUTION] agentic threat detected, exactly the confusion gh-aw#48940 set out to fix.Fail-closed divergence.
conclude.gohas a rule the gh-aw.cjsdoes not:In warn mode, gh-aw's
setDetectionFailurealways warns and proceeds, for every reason (this is the explicit intent of ADR29031-threat-detection-resilience-in-warn-mode). We fail closed when the engine step also failed. This was a deliberate hardening choice on our side, but it is a behavior change for every workflow currently running warn mode, and it is not documented as such anywhere in gh-aw's docs. It needs to be either upstreamed into the inline path or reverted, not left as a silent difference that appears when a workflow flips togh-aw-detection: true.GH_AW_DETECTION_CONCLUSION/GH_AW_DETECTION_REASONare exported to$GITHUB_ENVbyconclude.gobut not by the shell's branches — inconsistent for any later step in the detection job.Proposed work
threat-detect concludeowns the whole contract (skip, missing file, unreadable file, malformed file, verdict). Reduceconclude_threat_detection.shto a thin invocation, or drop it.reason=agent_failureandconclusion=failure.mustFail, it must be documented inspecs/threat-detection-spec.mdand raised with gh-aw so the inline path matches; if not, remove it.{RUN_DETECTION} × {result file present/absent/malformed} × {warn/strict} × {execution outcome}.