Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 164 additions & 1 deletion .github/workflows/qwen-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3325,6 +3325,169 @@ jobs:
printf '%s%s\n' "$esc" "$truncated"
printf '</code></pre>\n\n</details>\n\n'
}
# Render report.md as MARKDOWN instead of an escaped <pre> dump.
# The report is a curated bilingual document (tables, nested
# <details>, headings); the pre/code embedding displayed it as a
# wall of raw source (#8140's comment was the exhibit). The section
# is wrapped in a collapsed <details> so it still costs one line in
# the conversation but renders as real markdown when opened.
#
# A node sanitizer (not sed) does the escaping so it can tell code
# regions apart from prose. CommonMark does NOT decode entities in
# code spans or fenced blocks — they render literally there — so
# escaping & < > @ unconditionally showed the reader &amp;&amp;,
# &lt;T&gt;, &#64;pkg inside the very commands, generic types, and
# scoped-package paths a verification report is read to copy (the
# #8140 symptom, relocated into code). The security floor now rests
# on four line-independent guarantees:
# 1. in PROSE every < is escaped, then only the structural tags
# the report uses as raw HTML (details/summary) are un-escaped
# back to live tags — no other tag can form, so
# <img>/<script>/onerror never render. & and > are left alone
# in prose (a decoded entity yields text, never markup, and an
# unescaped > cannot open a tag once < is escaped), which keeps
# &&, ->, and blockquotes readable. Inside code spans/fences
# < & @ are left alone too: <img> and @mentions are inert under
# a code/pre ancestor, and escaping them there only mangles the
# rendered text;
# 2. the comment-open token is broken EVERYWHERE, prose and code
# alike (<!-- becomes an escaped no-op, the autofix-proven
# neutralizer). The break must stay global: the upsert greps
# the RAW body for the running marker and a fence prints
# verbatim, so a forged marker inside a fence must not survive;
# 3. in prose @ becomes &#64; — renders identically, never fires a
# mention (a mention cannot fire under a code/pre ancestor, so
# code keeps a literal @);
# 4. <details> folds are balanced over PROSE only — a </details>
# quoted in a code span/fence is inert text and is no longer
# counted — surplus closers are dropped (they would otherwise
# close the wrapping fold early) and unclosed opens are closed
# at the end, so a malformed report can neither swallow the
# footer nor escape its wrapper.
# Accepted tradeoff (named, not accidental): rendering promotes the
# report from inert text to parsed markdown, so [links](…) and
# ![images](…) now render live where emit_block showed them literal.
# report.md is agent output from a sandbox that ran PR code; images
# are camo-proxied (mostly noise) and a phishing link under the bot
# identity is the residual surface. Defusing link targets is a
# deliberate follow-up, not done here.
# An OVERSIZED report falls back to emit_block wholesale (cut
# markdown dangles fences/folds), as does any sanitizer failure.
emit_report() {
local file="$1" max="$2"
[ -n "$file" ] && [ -f "$file" ] || return 0
if [ "$(wc -c < "$file")" -gt "$max" ]; then
echo "::warning::emit_report fell back to escaped embedding (report exceeds size cap) for $file" >&2
emit_block 'Verification report (report.md, truncated)' "$file" "$max"
return 0
fi
local san_file="${TMPDIR:-/tmp}/verify-report-$$"
# Node is present on every runner that runs this job (emit_block
# already shells out to it for the UTF-8-safe cut). The script
# rides inside a single-quoted shell string, so it uses double
# quotes throughout and carries no single quotes of its own.
if ! node -e '
const fs = require("node:fs");
const text = fs.readFileSync(process.argv[1], "utf8").replace(/\u0000/g, "");
function escProse(s) {
return s
.replace(/</g, "&lt;")
.replace(/&lt;(\/?)(details|summary)>/g, "<$1$2>")
.replace(/&lt;!--/g, "&lt;!\\-\\-")
.replace(/@/g, "&#64;");
}
function escCode(s) {
return s.replace(/<!--/g, "&lt;!\\-\\-");
}
let depth = 0;
function balance(s) {
return s.replace(/<\/?details>/g, function (t) {
if (t === "<details>") { depth += 1; return t; }
if (depth > 0) { depth -= 1; return t; }
return "";
});
}
function proseLine(line) {
let out = "";
let buf = "";
let i = 0;
const flush = function () {
if (buf) { out += balance(escProse(buf)); buf = ""; }
};
while (i < line.length) {
if (line[i] !== "`") { buf += line[i]; i += 1; continue; }
let j = i;
while (j < line.length && line[j] === "`") j += 1;
const run = j - i;
let k = j, cs = -1, ce = -1;
while (k < line.length) {
if (line[k] === "`") {
let m = k;
while (m < line.length && line[m] === "`") m += 1;
if (m - k === run) { cs = k; ce = m; break; }
k = m;
} else k += 1;
}
if (cs === -1) { buf += line.slice(i, j); i = j; }
else { flush(); out += escCode(line.slice(i, ce)); i = ce; }
}
flush();
return out;
}
const lines = text.split("\n");
const out = [];
let inFence = false, fc = "", fl = 0;
for (const line of lines) {
if (!inFence) {
const m = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
if (m && !(m[1][0] === "`" && m[2].indexOf("`") !== -1)) {
inFence = true; fc = m[1][0]; fl = m[1].length;
out.push(escCode(line));
continue;
}
out.push(proseLine(line));
} else {
const cm = line.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/);
if (cm && cm[1][0] === fc && cm[1].length >= fl) inFence = false;
out.push(escCode(line));
}
}
let result = out.join("\n");
if (depth > 0) {
if (result && !result.endsWith("\n")) result += "\n";
result += "</details>\n".repeat(depth);
}
process.stdout.write(result);
' "$file" > "$san_file"; then
echo "::warning::emit_report fell back to escaped embedding (sanitize failed) for $file" >&2
rm -f "$san_file"
emit_block 'Verification report (report.md, truncated)' "$file" "$max"
return 0
fi
# Wrap in a collapsed <details> (one-line footprint, markdown when
# opened) and bound the WHOLE emitted section — wrapper, balanced
# report, appended closers — so the cap is a true bound on what
# lands in the comment. The old normal path skipped the header
# bytes the deficit branch did budget; folding the wrapper into
# the measured output removes that asymmetry and the separate
# fold-closer gate (closers now land in san_file before this gate).
local out_file="${TMPDIR:-/tmp}/verify-report-out-$$"
{
printf '<details>\n<summary>Verification report</summary>\n\n'
cat "$san_file"
printf '\n</details>\n'
} > "$out_file"
rm -f "$san_file"
if [ "$(wc -c < "$out_file")" -gt "$max" ]; then
echo "::warning::emit_report fell back to escaped embedding (sanitized output exceeds size cap) for $file" >&2
rm -f "$out_file"
emit_block 'Verification report (report.md, truncated)' "$file" "$max"
return 0
fi
cat "$out_file"
rm -f "$out_file"
printf '\n'
}

# Host the agent's evidence images (if any) on a per-PR branch
# (pr-assets/<N>-verify, matching hand-run convention) and build
Expand Down Expand Up @@ -3695,7 +3858,7 @@ jobs:
if [ -n "${MISSING_REPORT_NOTE:-}" ]; then
printf '%s\n\n' "$MISSING_REPORT_NOTE"
fi
emit_block 'Verification report (report.md)' "$REPORT" 45000
emit_report "$REPORT" 45000
if [ -n "$EVIDENCE_SECTION" ]; then
printf '%s' "$EVIDENCE_SECTION"
fi
Expand Down
Loading
Loading