[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
`https://agenthub.fritzlab.net/bug-<id>` URL. Every PR has a separate
`## Attribution` section containing the canonical `Authored-By`
product/model watermark. The action asks Gitea to render the body and checks
the rendered `<h2>` sections; fenced, commented, scripted, or collapsed
copies do not satisfy the visible provenance contract.
product/model watermark. The action asks Gitea to render the body with a
bounded 5-second connection and 15-second total wait, then checks visible
`<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
commits on `chore/` branches, ends with
+35 -7
View File
@@ -32,16 +32,44 @@ if [ -z "${SERVER_URL}" ] || [ -z "${TOKEN}" ]; then
elif ! RENDERED_BODY=$(printf '%s' "${BODY}" |
jq -Rs '{Text: ., Mode: "gfm"}' |
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 "Content-Type: application/json" \
--data-binary @- "${SERVER_URL%/}/api/v1/markdown"); then
echo "FAIL[check-naming]: Gitea could not render the PR body"
FAILED=1
fi
if printf '%s\n' "${RENDERED_BODY}" | grep -Eiq '<details([[:space:]>])'; then
echo "FAIL[check-naming]: rendered PR body must not contain collapsed details"
FAILED=1
fi
# A collapsed disclosure is valid supporting content, but provenance inside one
# is not visible by default. Remove details subtrees before locating sections.
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)
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--
}
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
tag=""
}
}
else if (char == "<") {
in_tag=1
tag=char
}
else if (details_depth == 0) printf "%s", char
}
}
')
# ---- branch form ----
# <role>/bug-<id>/<kebab>
@@ -81,7 +109,7 @@ if [ "${BRANCH_KIND}" = "role-bug" ]; then
FAILED=1
fi
tracking=$(printf '%s\n' "${RENDERED_BODY}" | awk '
tracking=$(printf '%s\n' "${VISIBLE_BODY}" | awk '
/<h2[^>]*>Tracking<\/h2>/ { in_section=1; next }
/<h2[^>]*>/ && in_section { exit }
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"
FAILED=1
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}'"
FAILED=1
fi
@@ -106,7 +134,7 @@ elif [ "${BRANCH_KIND}" = "chore" ] && [ -n "${TITLE_BUG}" ]; then
fi
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[^>]*>/ && in_section { exit }
in_section { print }
+28
View File
@@ -2,6 +2,29 @@
# Deterministic Gitea Markdown renderer stub for check.sh contract tests.
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
exit 0
fi
@@ -34,12 +57,17 @@ printf '%s\n' "${PR_BODY}" | awk '
in_script=1
next
}
if (lower ~ /^[ ]{0,3}<\/details([[:blank:]>])/) {
print "</details>"
next
}
if (lower ~ /^[ ]{0,3}<details([[:blank:]>])/) {
print "<details>"
next
}
if (line ~ /^## Tracking[[:space:]]*$/) print "<h2>Tracking</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:].-]+>/) {
email=line
sub(/^.*</, "", email)
+7 -1
View File
@@ -178,10 +178,16 @@ check_contract_pass "fenced code examples remain available" \
"$BASE" "$GOOD_HEAD"
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>' \
"$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" \
$'<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"
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"
BAD_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD)