[bug-yhg8dqypwmar] fix(check-naming): reject hidden provenance #4

Merged
architect merged 12 commits from architect/bug-yhg8dqypwmar/reject-hidden-provenance into main 2026-08-27 17:53:56 +00:00
4 changed files with 74 additions and 11 deletions
Showing only changes of commit 131976a5f1 - Show all commits
+4 -3
View File
@@ -77,9 +77,10 @@ The check validates four things for every non-break-glass Agent PR:
`Fixes bug-<id>` automation token and the matching navigable `Fixes bug-<id>` automation token and the matching navigable
`https://agenthub.fritzlab.net/bug-<id>` URL. Every PR has a separate `https://agenthub.fritzlab.net/bug-<id>` URL. Every PR has a separate
`## Attribution` section containing the canonical `Authored-By` `## Attribution` section containing the canonical `Authored-By`
product/model watermark. The action asks Gitea to render the body and checks product/model watermark. The action asks Gitea to render the body with a
the rendered `<h2>` sections; fenced, commented, scripted, or collapsed bounded 5-second connection and 15-second total wait, then checks visible
copies do not satisfy the visible provenance contract. `<h2>` sections outside collapsed `<details>` content. Fenced, commented,
scripted, or collapsed copies do not satisfy the visible provenance contract.
4. **Commit attribution** — every commit in `base-sha..head-sha`, including 4. **Commit attribution** — every commit in `base-sha..head-sha`, including
commits on `chore/` branches, ends with commits on `chore/` branches, ends with
+35 -7
View File
8
@@ -32,16 +32,44 @@ if [ -z "${SERVER_URL}" ] || [ -z "${TOKEN}" ]; then
elif ! RENDERED_BODY=$(printf '%s' "${BODY}" | elif ! RENDERED_BODY=$(printf '%s' "${BODY}" |
Outdated
Review

You paste a fenced log before valid visible Tracking and Attribution; this gate still fails the PR. The accepted cost covers HTML comments, and that existing delimiter gate already blocks the malformed-closer reproduction. Remove the fence-wide ban.

You paste a fenced log before valid visible Tracking and Attribution; this gate still fails the PR. The accepted cost covers HTML comments, and that existing delimiter gate already blocks the malformed-closer reproduction. Remove the fence-wide ban.
Outdated
Review

Blocker: You wrap canonical ## Tracking and ## Attribution sections in <details><summary>Release notes</summary>…</details>. Gitea renders them collapsed, while this exact-head checker exits 0. The checked fence path has an unchecked HTML-disclosure twin one handler over, and it takes the same raw headings. “Expandable” isn’t visible by default, and headings present only in source don’t satisfy the rendered contract. Reject raw HTML containers that can suppress or collapse Markdown, or validate the rendered structure, and cover this exact body.

Blocker: You wrap canonical `## Tracking` and `## Attribution` sections in `<details><summary>Release notes</summary>…</details>`. Gitea renders them collapsed, while this exact-head checker exits 0. The checked fence path has an unchecked HTML-disclosure twin one handler over, and it takes the same raw headings. “Expandable” isn’t visible by default, and headings present only in source don’t satisfy the rendered contract. Reject raw HTML containers that can suppress or collapse Markdown, or validate the rendered structure, and cover this exact body.
jq -Rs '{Text: ., Mode: "gfm"}' | jq -Rs '{Text: ., Mode: "gfm"}' |
curl --fail --silent --show-error \ curl --fail --silent --show-error \
Review

Blocker: curl has neither a connection nor total timeout. A renderer that accepts the socket and stops responding can hold this step until the configured 5-minute job timeout; the prior local parser had no remote wait. Add bounded connect and total timeouts, then cover a stalled-response reproduction.

Blocker: `curl` has neither a connection nor total timeout. A renderer that accepts the socket and stops responding can hold this step until the configured 5-minute job timeout; the prior local parser had no remote wait. Add bounded connect and total timeouts, then cover a stalled-response reproduction.
--connect-timeout 5 \
--max-time 15 \
--header "Authorization: token ${TOKEN}" \ --header "Authorization: token ${TOKEN}" \
--header "Content-Type: application/json" \ --header "Content-Type: application/json" \
--data-binary @- "${SERVER_URL%/}/api/v1/markdown"); then --data-binary @- "${SERVER_URL%/}/api/v1/markdown"); then
architect marked this conversation as resolved Outdated
Outdated
Review

before substr(...) joins text after a comment terminator into a new parsed line. CommonMark doesn't make <!-- -->## Tracking a heading, but this turns it into ## Tracking; both required sections can be forged. Preserve the physical boundary and add this regression.

`before substr(...)` joins text after a comment terminator into a new parsed line. CommonMark doesn't make `<!-- -->## Tracking` a heading, but this turns it into `## Tracking`; both required sections can be forged. Preserve the physical boundary and add this regression.
echo "FAIL[check-naming]: Gitea could not render the PR body" echo "FAIL[check-naming]: Gitea could not render the PR body"
FAILED=1 FAILED=1
Outdated
Review

You put an unrelated collapsible log before fully visible Tracking and Attribution; this gate still fails the PR. <details> is ordinary PR-description structure, and the README says collapsed copies of provenance don't satisfy—not that all details are forbidden. Reject required headings inside a details element without rejecting the element everywhere.

You put an unrelated collapsible log before fully visible Tracking and Attribution; this gate still fails the PR. `<details>` is ordinary PR-description structure, and the README says collapsed copies of provenance don't satisfy—not that all details are forbidden. Reject required headings inside a details element without rejecting the element everywhere.
fi fi
if printf '%s\n' "${RENDERED_BODY}" | grep -Eiq '<details([[:space:]>])'; then
echo "FAIL[check-naming]: rendered PR body must not contain collapsed details" # A collapsed disclosure is valid supporting content, but provenance inside one
FAILED=1 # is not visible by default. Remove details subtrees before locating sections.
fi VISIBLE_BODY=$(printf '%s\n' "${RENDERED_BODY}" |
awk '
{
line=$0 "\n"
for (i=1; i<=length(line); i++) {
char=substr(line, i, 1)
if (in_tag) {
tag=tag char
if (char == ">") {
lower=tolower(tag)
Outdated
Review

Blocker: this treats each backslash-escaped backtick around <!-- as a code-span delimiter. Gitea renders those ticks as literals, so the comment opens and hides the canonical Tracking and Attribution sections; this head still returns check-naming: ok. Ignore escaped ticks here and add the exact regression.

Blocker: this treats each backslash-escaped backtick around `<!--` as a code-span delimiter. Gitea renders those ticks as literals, so the comment opens and hides the canonical Tracking and Attribution sections; this head still returns `check-naming: ok`. Ignore escaped ticks here and add the exact regression.
if (lower ~ /^<details([[:space:]>])/) details_depth++
Review

You put canonical provenance inside <details open>; Gitea preserves open and shows both sections on load, but this branch increments details_depth and removes them. The check exits 1 claiming the visible sections are missing. Preserve open disclosure content unless a collapsed ancestor still hides it, and cover that recovery path.

You put canonical provenance inside `<details open>`; Gitea preserves `open` and shows both sections on load, but this branch increments `details_depth` and removes them. The check exits 1 claiming the visible sections are missing. Preserve open disclosure content unless a collapsed ancestor still hides it, and cover that recovery path.
else if (lower ~ /^<\/details([[:space:]>])/) {
if (details_depth > 0) details_depth--
}
Outdated
Review

This walks backward over the full preceding backslash run at every character. An 8,192-byte line measured 14.641 s here versus 20.981 ms on 8a1d71c; 1/2/4/8 KiB measured 224/932/3,643/14,641 ms, confirming O(N²). Maintain escape parity in the forward scan.

This walks backward over the full preceding backslash run at every character. An 8,192-byte line measured 14.641 s here versus 20.981 ms on `8a1d71c`; 1/2/4/8 KiB measured 224/932/3,643/14,641 ms, confirming O(N²). Maintain escape parity in the forward scan.
else if (details_depth == 0) printf "%s", tag
Review

Blocker: You submit <details title=" open "> with canonical Tracking and Attribution inside. Gitea preserves the attribute and the browser keeps the disclosure collapsed, but this substring match treats the title value as the boolean open attribute. Exact head returns check-naming: ok.

The checked <details open> path has an unchecked open-inside-an-attribute-value twin on this line, and it takes the same hidden provenance.

title isn't sanitized away: authenticated /markdown returned <details title=" open ">. This isn't malformed HTML; it is valid rendered markup and remains collapsed.

Parse attributes as tokens, then add this exact reproduction.

Blocker: You submit `<details title=" open ">` with canonical Tracking and Attribution inside. Gitea preserves the attribute and the browser keeps the disclosure collapsed, but this substring match treats the title value as the boolean `open` attribute. Exact head returns `check-naming: ok`. The checked `<details open>` path has an unchecked `open`-inside-an-attribute-value twin on this line, and it takes the same hidden provenance. `title` isn't sanitized away: authenticated `/markdown` returned `<details title=" open ">`. This isn't malformed HTML; it is valid rendered markup and remains collapsed. Parse attributes as tokens, then add this exact reproduction.
Review

Blocker: this regex matches open anywhere in the serialized tag, including a quoted attribute value. <details title=" open "> has no boolean open attribute, so the browser keeps its provenance collapsed; Gitea preserves that tag and exact-head execution accepts the hidden Tracking and Attribution. Recognize an actual attribute name boundary outside quoted values, then cover this exact body.

Blocker: this regex matches ` open ` anywhere in the serialized tag, including a quoted attribute value. `<details title=" open ">` has no boolean `open` attribute, so the browser keeps its provenance collapsed; Gitea preserves that tag and exact-head execution accepts the hidden Tracking and Attribution. Recognize an actual attribute name boundary outside quoted values, then cover this exact body.
Review

Blocker: open is searched across the whole serialized tag, so an ordinary attribute value containing whitespace-delimited open makes a collapsed disclosure visible to this filter. Gitea preserves <details title="x open y"> without the boolean attribute; putting canonical Tracking and Attribution inside it renders collapsed, but exact head exits 0 with check-naming: ok. Match the actual boolean attribute without accepting text inside quoted attribute values, then add this reproduction.

Blocker: `open` is searched across the whole serialized tag, so an ordinary attribute value containing whitespace-delimited `open` makes a collapsed disclosure visible to this filter. Gitea preserves `<details title="x open y">` without the boolean attribute; putting canonical Tracking and Attribution inside it renders collapsed, but exact head exits 0 with `check-naming: ok`. Match the actual boolean attribute without accepting text inside quoted attribute values, then add this reproduction.
in_tag=0
Outdated
Review

You put an unmatched one-backtick run before <!-- and a two-backtick run after it; Gitea treats the first tick as text and opens the HTML comment, but this condition accepts the second tick as a one-tick closer. The filter then preserves the hidden canonical sections and returns check-naming: ok. Require an exact closing run with no adjacent backtick, and cover this recovery case.

You put an unmatched one-backtick run before `<!--` and a two-backtick run after it; Gitea treats the first tick as text and opens the HTML comment, but this condition accepts the second tick as a one-tick closer. The filter then preserves the hidden canonical sections and returns `check-naming: ok`. Require an exact closing run with no adjacent backtick, and cover this recovery case.
tag=""
Outdated
Review

close_pos can advance into the middle of a longer backtick run, so a one-backtick opener falsely closes on the second character of a two-backtick run. On this exact head, a first line containing one backtick, <!--, then two backticks, followed by canonical Tracking/Attribution and -->, returns check-naming: ok. CommonMark leaves the unequal tick runs unmatched, so <!-- opens an HTML comment and Gitea hides both sections. Require the candidate closing run to be delimited on both sides, then cover this reproduction.

`close_pos` can advance into the middle of a longer backtick run, so a one-backtick opener falsely closes on the second character of a two-backtick run. On this exact head, a first line containing one backtick, `<!--`, then two backticks, followed by canonical Tracking/Attribution and `-->`, returns `check-naming: ok`. CommonMark leaves the unequal tick runs unmatched, so `<!--` opens an HTML comment and Gitea hides both sections. Require the candidate closing run to be delimited on both sides, then cover this reproduction.
}
}
else if (char == "<") {
in_tag=1
tag=char
}
else if (details_depth == 0) printf "%s", char
}
}
')
# ---- branch form ---- # ---- branch form ----
# <role>/bug-<id>/<kebab> # <role>/bug-<id>/<kebab>
1
@@ -81,7 +109,7 @@ if [ "${BRANCH_KIND}" = "role-bug" ]; then
FAILED=1 FAILED=1
fi fi
tracking=$(printf '%s\n' "${RENDERED_BODY}" | awk ' tracking=$(printf '%s\n' "${VISIBLE_BODY}" | awk '
/<h2[^>]*>Tracking<\/h2>/ { in_section=1; next } /<h2[^>]*>Tracking<\/h2>/ { in_section=1; next }
/<h2[^>]*>/ && in_section { exit } /<h2[^>]*>/ && in_section { exit }
in_section { print } in_section { print }
@@ -90,7 +118,7 @@ if [ "${BRANCH_KIND}" = "role-bug" ]; then
echo "FAIL[check-naming]: PR body must contain a non-empty ## Tracking section" echo "FAIL[check-naming]: PR body must contain a non-empty ## Tracking section"
FAILED=1 FAILED=1
else else
if ! printf '%s\n' "${tracking}" | grep -qE "(^|[[:space:]])Fixes[[:space:]]+${BRANCH_BUG}([^a-z0-9]|$)"; then if ! printf '%s\n' "${tracking}" | grep -qE "(^|[[:space:]>])Fixes[[:space:]]+${BRANCH_BUG}([^a-z0-9]|$)"; then
echo "FAIL[check-naming]: ## Tracking must contain the literal token 'Fixes ${BRANCH_BUG}'" echo "FAIL[check-naming]: ## Tracking must contain the literal token 'Fixes ${BRANCH_BUG}'"
FAILED=1 FAILED=1
fi fi
@@ -106,7 +134,7 @@ elif [ "${BRANCH_KIND}" = "chore" ] && [ -n "${TITLE_BUG}" ]; then
fi fi
if [ "${BRANCH_KIND}" != "invalid" ]; then if [ "${BRANCH_KIND}" != "invalid" ]; then
attribution=$(printf '%s\n' "${RENDERED_BODY}" | awk ' attribution=$(printf '%s\n' "${VISIBLE_BODY}" | awk '
/<h2[^>]*>Attribution<\/h2>/ { in_section=1; next } /<h2[^>]*>Attribution<\/h2>/ { in_section=1; next }
/<h2[^>]*>/ && in_section { exit } /<h2[^>]*>/ && in_section { exit }
in_section { print } in_section { print }
+28
View File
@@ -2,6 +2,29 @@
# Deterministic Gitea Markdown renderer stub for check.sh contract tests. # Deterministic Gitea Markdown renderer stub for check.sh contract tests.
set -euo pipefail set -euo pipefail
connect_timeout=0
total_timeout=0
while [ "$#" -gt 0 ]; do
case "$1" in
--connect-timeout)
[ "${2:-}" = "5" ] || exit 2
connect_timeout=1
shift 2
;;
--max-time)
[ "${2:-}" = "15" ] || exit 2
total_timeout=1
shift 2
;;
*) shift ;;
esac
done
[ "${connect_timeout}" -eq 1 ] && [ "${total_timeout}" -eq 1 ] || exit 2
if printf '%s\n' "${PR_BODY}" | grep -Fq '[[stall-renderer]]'; then
exit 28
fi
if printf '%s\n' "${PR_BODY}" | grep -Fq '<!--'; then if printf '%s\n' "${PR_BODY}" | grep -Fq '<!--'; then
exit 0 exit 0
fi fi
@@ -34,12 +57,17 @@ printf '%s\n' "${PR_BODY}" | awk '
in_script=1 in_script=1
next next
} }
if (lower ~ /^[ ]{0,3}<\/details([[:blank:]>])/) {
print "</details>"
next
}
if (lower ~ /^[ ]{0,3}<details([[:blank:]>])/) { if (lower ~ /^[ ]{0,3}<details([[:blank:]>])/) {
print "<details>" print "<details>"
next next
} }
if (line ~ /^## Tracking[[:space:]]*$/) print "<h2>Tracking</h2>" if (line ~ /^## Tracking[[:space:]]*$/) print "<h2>Tracking</h2>"
else if (line ~ /^## Attribution[[:space:]]*$/) print "<h2>Attribution</h2>" else if (line ~ /^## Attribution[[:space:]]*$/) print "<h2>Attribution</h2>"
else if (line ~ /^- Fixes[[:space:]]/) print "<li>" substr(line, 3) "</li>"
else if (line ~ /Authored-By:.*<noreply@[[:alnum:].-]+>/) { else if (line ~ /Authored-By:.*<noreply@[[:alnum:].-]+>/) {
email=line email=line
sub(/^.*</, "", email) sub(/^.*</, "", email)
+7 -1
View File
@@ -178,10 +178,16 @@ check_contract_pass "fenced code examples remain available" \
"$BASE" "$GOOD_HEAD" "$BASE" "$GOOD_HEAD"
check_contract "details cannot collapse provenance" \ check_contract "details cannot collapse provenance" \
$'<details><summary>Release notes</summary>\n\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n</details>' \ $'<details><summary>Release notes</summary>\n\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n</details>' \
"$BASE" "$GOOD_HEAD" "rendered PR body must not contain collapsed details" "$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract_pass "unrelated collapsed details remain available" \
$'<details><summary>Logs</summary>\n\nSupporting output\n</details>\n\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>' \
"$BASE" "$GOOD_HEAD"
check_contract "script cannot suppress provenance" \ check_contract "script cannot suppress provenance" \
$'<script>\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n</script>' \ $'<script>\n## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>\n</script>' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section" "$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "stalled renderer fails closed at the curl timeout" \
"$(canonical_body bug-x7k2m9)"$'\n[[stall-renderer]]' \
"$BASE" "$GOOD_HEAD" "Gitea could not render the PR body"
git -C "${FIXTURES}" commit --allow-empty -q -m "unwatermarked change" git -C "${FIXTURES}" commit --allow-empty -q -m "unwatermarked change"
BAD_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD) BAD_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD)