From 246d9c3d28995804d2230c22d11f15a064b5ad7d Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Mon, 24 Aug 2026 12:58:22 +0800 Subject: [PATCH 1/3] fix(ci): neutralize legacy ##[ commands in autofix stdout echoes A review finding about workflow-command injection carried its payload strings (::error::forged, ##[add-matcher]) verbatim as example text. Prepare echoed feedback.md raw, the runner parsed the mid-line ##[add-matcher] and failed the step before the agent ran, and the consecutive-failure breaker burned the takeover window of #9761 in about 70 minutes while reporting it as a 100/100 round cap. The existing neutralization covered only the modern :: syntax; the runner also parses the legacy ##[name] form, even mid-line. Extend every untrusted-content echo in the autofix family (prepare feedback echo, both artifact dump loops, the gate's failure/handoff/no-action echoes, the deferred-findings dumps and upsert re-emit loops) to neutralize both prefixes, and pin the census plus a behavioral ##[ case in the contract tests. --- .github/scripts/autofix-push-and-report.sh | 7 +- .../run-autofix-review-verification.sh | 24 +++-- .github/scripts/upsert-deferred-issue.sh | 16 +-- .github/workflows/qwen-autofix.yml | 47 +++++--- scripts/tests/qwen-autofix-workflow.test.js | 102 +++++++++++++++--- 5 files changed, 147 insertions(+), 49 deletions(-) diff --git a/.github/scripts/autofix-push-and-report.sh b/.github/scripts/autofix-push-and-report.sh index ff75361be88..f0129f0126c 100755 --- a/.github/scripts/autofix-push-and-report.sh +++ b/.github/scripts/autofix-push-and-report.sh @@ -324,8 +324,9 @@ run_deferred_upsert() { if [[ "${UPSERT_OUT}" != *'__upsert_child_live__'* ]]; then echo "::warning::deferred-findings upsert child never started (loader trace mode or exec failure); NOT persisted this round" fi - # The child's output is agent-reachable content, so a line-start - # `::` is neutralized before it reaches this step's stdout. + # The child's output is agent-reachable content, so both workflow-command + # syntaxes are neutralized before it reaches this step's stdout (`##[` + # parses mid-line too — #9761). while IFS= read -r _upsert_line; do # Wrapper-authored lines carry a marker and are emitted # VERBATIM so they still render as GitHub annotations; the @@ -335,7 +336,7 @@ run_deferred_upsert() { elif [[ "${_upsert_line}" == __upsert_trusted__* ]]; then printf '%s\n' "${_upsert_line#__upsert_trusted__}" else - printf '%s\n' "${_upsert_line//::/;;}" + printf '%s\n' "${_upsert_line//::/;;}" | sed 's/##\[/##[/g' fi done <<< "${UPSERT_OUT}" } diff --git a/.github/scripts/run-autofix-review-verification.sh b/.github/scripts/run-autofix-review-verification.sh index 4ca5790d58b..46222d32d6e 100755 --- a/.github/scripts/run-autofix-review-verification.sh +++ b/.github/scripts/run-autofix-review-verification.sh @@ -236,7 +236,10 @@ fi if [[ -f "${WORKDIR}/failure.md" && -n "$(git status --porcelain)" ]]; then echo "❌ Agent wrote failure.md after leaving a dirty workspace:" git status --short - cat "${WORKDIR}/failure.md" + # Agent-written content on step stdout: both workflow-command syntaxes + # parse here (`##[` mid-line too — measured on #9761). Same reason + # 'Show run artifacts' neutralizes these files. + sed -e 's/::/;;/g' -e 's/##\[/##[/g' "${WORKDIR}/failure.md" echo "outcome=failed" >> "${GITHUB_OUTPUT}" echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}" if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then @@ -247,7 +250,7 @@ fi if [[ -f "${WORKDIR}/failure.md" ]]; then echo "🛑 Agent aborted intentionally:" - cat "${WORKDIR}/failure.md" + sed -e 's/::/;;/g' -e 's/##\[/##[/g' "${WORKDIR}/failure.md" echo "outcome=failed" >> "${GITHUB_OUTPUT}" echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}" if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then @@ -277,7 +280,7 @@ if [[ -s "${WORKDIR}/handoff.md" && -n "$(git status --porcelain)" \ && "${AUDIT_VERDICT:-}" != 'conflict' ]]; then echo "❌ Agent wrote handoff.md after leaving a dirty workspace:" git status --short - sed 's/::/;;/g' "${WORKDIR}/handoff.md" + sed -e 's/::/;;/g' -e 's/##\[/##[/g' "${WORKDIR}/handoff.md" echo "outcome=dirty_handoff" >> "${GITHUB_OUTPUT}" echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}" if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then @@ -299,7 +302,7 @@ if [[ -s "${WORKDIR}/handoff.md" && "${committed_rc:-0}" -eq 1 \ && "${AUDIT_VERDICT:-}" != 'conflict' ]]; then echo "❌ Agent wrote handoff.md but the round HAS a commit — a brake violation:" git log --oneline "origin/${BRANCH}..${BRANCH}" - sed 's/::/;;/g' "${WORKDIR}/handoff.md" + sed -e 's/::/;;/g' -e 's/##\[/##[/g' "${WORKDIR}/handoff.md" echo "outcome=committed_handoff" >> "${GITHUB_OUTPUT}" echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}" if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then @@ -321,10 +324,11 @@ if git diff --quiet "origin/${BRANCH}...${BRANCH}" \ && [[ -s "${WORKDIR}/handoff.md" ]] \ && [[ "${AUDIT_VERDICT:-}" != 'conflict' ]]; then echo "🤝 Branch unchanged with a handoff — the agent stopped under instruction and deferred this item to a human:" - # Agent-written content: a line-start `::` would be parsed as a workflow - # command (::error::, ::add-mask::), the same reason 'Show run artifacts' - # neutralizes these files. - sed 's/::/;;/g' "${WORKDIR}/handoff.md" + # Agent-written content: both workflow-command syntaxes parse on step + # stdout — a line-start `::` (::error::, ::add-mask::) AND `##[` even + # mid-line (a quoted `##[add-matcher]` fails the step; measured on + #9761). The same reason 'Show run artifacts' neutralizes these files. + sed -e 's/::/;;/g' -e 's/##\[/##[/g' "${WORKDIR}/handoff.md" echo "outcome=handoff" >> "${GITHUB_OUTPUT}" echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}" if [[ "${AUDIT_VERDICT_RECORDED:-false}" == 'true' ]]; then @@ -586,7 +590,9 @@ if git diff --quiet "origin/${BRANCH}...${BRANCH}"; then # no-commit handoff was classified before the structural checks above. if [[ -s "${WORKDIR}/no-action.md" ]]; then echo "🟰 No action needed:" - cat "${WORKDIR}/no-action.md" + # Both command syntaxes, like every other echo of agent-written files + # (`##[` parses mid-line too — #9761). + sed -e 's/::/;;/g' -e 's/##\[/##[/g' "${WORKDIR}/no-action.md" echo "verified_head=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" echo "outcome=noop" >> "${GITHUB_OUTPUT}" echo "kiss_audit=${KISS_AUDIT:-false}" >> "${GITHUB_OUTPUT}" diff --git a/.github/scripts/upsert-deferred-issue.sh b/.github/scripts/upsert-deferred-issue.sh index e383a63d620..1e63e19e6fb 100755 --- a/.github/scripts/upsert-deferred-issue.sh +++ b/.github/scripts/upsert-deferred-issue.sh @@ -49,9 +49,10 @@ gh_reason() { local r='' [[ -n "${GH_ERR}" && -s "${GH_ERR}" ]] && r="$(tr '\r\n\t' ' ' < "${GH_ERR}" | head -c 200)" - # `::` neutralized like every other agent/API-derived echo: an API error - # body is not trusted to be free of workflow-command syntax. - r="$(printf '%s' "${r}" | sed 's/::/;;/g')" + # Both workflow-command syntaxes neutralized like every other + # agent/API-derived echo: an API error body is not trusted to be free of + # them (`##[` parses mid-line too — #9761). + r="$(printf '%s' "${r}" | sed -e 's/::/;;/g' -e 's/##\[/##[/g')" [[ -n "${r// /}" ]] && printf '%s' "${r}" || printf 'no stderr captured' } gh_err_reset() { [[ -n "${GH_ERR}" ]] && : > "${GH_ERR}"; } @@ -67,9 +68,10 @@ OWN_FINDINGS="${WORKDIR}/deferred-findings.json" # filters this round's feedback out of every later round, and the next run's # workspace reset deletes the file — nothing re-derives them. So each abort # says so and dumps what it had, for manual recovery from the run log. -# `::` is neutralized in the dump: the content is agent-influenced and a -# raw `::` at line start would be parsed as a workflow command (same reason -# `