[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
5 changed files with 125 additions and 34 deletions
Showing only changes of commit c07ea023c0 - Show all commits
+7 -3
View File
@@ -38,6 +38,8 @@ jobs:
pr-title: ${{ github.event.pull_request.title }}
pr-author: ${{ github.event.pull_request.user.login }}
pr-body: ${{ github.event.pull_request.body }}
server-url: ${{ github.server_url }}
token: ${{ github.token }}
base-sha: ${{ github.event.pull_request.base.sha }}
head-sha: ${{ github.event.pull_request.head.sha }}
```
@@ -53,6 +55,8 @@ job — the step still fails, but the job cannot block the PR.
| `pr-title` | yes | PR title — `github.event.pull_request.title` |
| `pr-author` | no | PR author login — `github.event.pull_request.user.login`; `dfritz` is exempt |
| `pr-body` | yes | PR description — `github.event.pull_request.body` |
| `server-url` | yes | Gitea server URL — `github.server_url` |
| `token` | yes | Gitea Actions token — `github.token` |
| `base-sha` | yes | Base commit — `github.event.pull_request.base.sha` |
| `head-sha` | yes | Head commit — `github.event.pull_request.head.sha` |
@@ -73,9 +77,9 @@ 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. Agent-authored PR bodies cannot contain HTML
comment or fenced code delimiters, so required provenance cannot be hidden
from readers.
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.
4. **Commit attribution** — every commit in `base-sha..head-sha`, including
commits on `chore/` branches, ends with
+8
View File
@@ -29,6 +29,12 @@ inputs:
pr-body:
description: PR description — github.event.pull_request.body.
required: true
server-url:
description: Gitea server URL — github.server_url.
required: true
token:
description: Gitea Actions token — github.token.
required: true
base-sha:
description: Base commit SHA — github.event.pull_request.base.sha.
required: true
@@ -46,6 +52,8 @@ runs:
PR_TITLE: ${{ inputs.pr-title }}
PR_AUTHOR: ${{ inputs.pr-author }}
PR_BODY: ${{ inputs.pr-body }}
GITEA_SERVER_URL: ${{ inputs.server-url }}
GITEA_TOKEN: ${{ inputs.token }}
BASE_SHA: ${{ inputs.base-sha }}
HEAD_SHA: ${{ inputs.head-sha }}
run: bash "${{ github.action_path }}/check.sh"
+24 -15
View File
@@ -10,6 +10,8 @@ AUTHOR="${PR_AUTHOR:-}"
BODY="${PR_BODY:-}"
BASE="${BASE_SHA:-}"
HEAD="${HEAD_SHA:-}"
SERVER_URL="${GITEA_SERVER_URL:-}"
TOKEN="${GITEA_TOKEN:-}"
# Break-glass: dfritz is exempt from all naming checks.
if [ "${AUTHOR}" = "dfritz" ]; then
1
@@ -21,16 +23,23 @@ FAILED=0
BRANCH_KIND="invalid"
BRANCH_BUG=""
# Gitea hides HTML comments. Reject their delimiters so required provenance is
# always visible, then parse the raw body without a second Markdown renderer.
VISIBLE_BODY="${BODY}"
if printf '%s\n' "${BODY}" | grep -Fq '<!--' ||
printf '%s\n' "${BODY}" | grep -Fq -- '-->'; then
echo "FAIL[check-naming]: PR body must not contain HTML comment delimiters"
# Gitea's renderer is the visibility contract. Validate its output instead of
# maintaining a second Markdown parser in this action.
Outdated
Review

Blocker: this gate rejects only HTML comment delimiters. Wrap canonical Tracking and Attribution sections in <script>...</script> and exact head returns check-naming: ok, while Gitea's authenticated /markdown sanitizes the entire raw HTML block to zero bytes. Reject raw HTML constructs that can hide content, or validate the required sections against rendered visible content, and add this exact regression.

Blocker: this gate rejects only HTML comment delimiters. Wrap canonical Tracking and Attribution sections in `<script>...</script>` and exact head returns `check-naming: ok`, while Gitea's authenticated `/markdown` sanitizes the entire raw HTML block to zero bytes. Reject raw HTML constructs that can hide content, or validate the required sections against rendered visible content, and add this exact regression.
RENDERED_BODY=""
Outdated
Review

Blocker: CommonMark closing code fences may only be followed by spaces or tabs, but this clears in_fence for any same-character run. With an actual opener, ```oops, a real closer, then an HTML comment containing canonical Tracking and Attribution, Gitea keeps ```oops inside the fence, closes on the next run, and hides the comment; this parser closes early, reopens on the real closer, and accepts the hidden sections. Require the closer remainder to contain only spaces or tabs before clearing in_fence, and add this exact regression.

Blocker: CommonMark closing code fences may only be followed by spaces or tabs, but this clears `in_fence` for any same-character run. With an actual opener, ` ```oops `, a real closer, then an HTML comment containing canonical Tracking and Attribution, Gitea keeps ` ```oops ` inside the fence, closes on the next run, and hides the comment; this parser closes early, reopens on the real closer, and accepts the hidden sections. Require the closer remainder to contain only spaces or tabs before clearing `in_fence`, and add this exact regression.
Outdated
Review

Blocker: you submit an opening three-backtick fence, then ```oops, then canonical Tracking and Attribution. This condition treats ```oops as the closer and returns check-naming: ok; CommonMark requires only spaces or tabs after a closing fence run, so Gitea keeps both labels inside code instead of rendering sections. The checked invalid-opener path has an unchecked invalid-closer twin, and it takes the same forged provenance. Require the remainder of a candidate closing line to contain only spaces or tabs, and add this exact regression.

Blocker: you submit an opening three-backtick fence, then ` ```oops `, then canonical Tracking and Attribution. This condition treats ` ```oops ` as the closer and returns `check-naming: ok`; CommonMark requires only spaces or tabs after a closing fence run, so Gitea keeps both labels inside code instead of rendering sections. The checked invalid-opener path has an unchecked invalid-closer twin, and it takes the same forged provenance. Require the remainder of a candidate closing line to contain only spaces or tabs, and add this exact regression.
if [ -z "${SERVER_URL}" ] || [ -z "${TOKEN}" ]; then
architect marked this conversation as resolved Outdated
Outdated
Review

You can put the visible literal <!-- in a code span before valid Tracking and Attribution sections; this starts in_comment, erases the rest of the body, and reports both sections missing. Respect Markdown code spans/fences and cover this recovery case.

You can put the visible literal `<!--` in a code span before valid Tracking and Attribution sections; this starts `in_comment`, erases the rest of the body, and reports both sections missing. Respect Markdown code spans/fences and cover this recovery case.
echo "FAIL[check-naming]: server-url and token are required to render the PR body"
FAILED=1
Outdated
Review

Blocker: this treats every backtick run of length 3+ as a fence opener, but CommonMark forbids a backtick in a backtick fence's info string. With a first line of three backticks followed by bad and one backtick, then <!--, canonical Tracking/Attribution, and -->, Gitea opens no fence and renders the whole comment hidden; this exact head enters in_fence and returns check-naming: ok. Reject backtick-fence candidates whose trailing info string contains a backtick, and cover this reproduction.

Blocker: this treats every backtick run of length 3+ as a fence opener, but CommonMark forbids a backtick in a backtick fence's info string. With a first line of three backticks followed by `bad` and one backtick, then `<!--`, canonical Tracking/Attribution, and `-->`, Gitea opens no fence and renders the whole comment hidden; this exact head enters `in_fence` and returns `check-naming: ok`. Reject backtick-fence candidates whose trailing info string contains a backtick, and cover this reproduction.
Outdated
Review

Blocker: you submit a body beginning with three backticks followed by html, a backtick, and oops, then put canonical Tracking and Attribution inside <!-- ... -->; Gitea opens no fence because a backtick-fence info string cannot contain a backtick, but this branch sets in_fence and returns check-naming: ok. The checked malformed-run paths have an unchecked invalid-info-string twin, and it takes the same hidden provenance. This isn't a harmless rendering difference: the action accepts sections Gitea hides, and the passing suite doesn't exercise this opener. Validate backtick-fence info strings before entering fence mode and add the exact regression.

Blocker: you submit a body beginning with three backticks followed by `html`, a backtick, and `oops`, then put canonical Tracking and Attribution inside `<!-- ... -->`; Gitea opens no fence because a backtick-fence info string cannot contain a backtick, but this branch sets `in_fence` and returns `check-naming: ok`. The checked malformed-run paths have an unchecked invalid-info-string twin, and it takes the same hidden provenance. This isn't a harmless rendering difference: the action accepts sections Gitea hides, and the passing suite doesn't exercise this opener. Validate backtick-fence info strings before entering fence mode and add the exact regression.
Outdated
Review

Blocker: this fence branch runs before the later in_comment handling. After <!--, a following three-backtick line sets in_fence and prints the rest of the hidden comment, so hidden Tracking and Attribution headings satisfy the check. Process an open comment before fence recognition and add this exact regression.

Blocker: this fence branch runs before the later `in_comment` handling. After `<!--`, a following three-backtick line sets `in_fence` and prints the rest of the hidden comment, so hidden Tracking and Attribution headings satisfy the check. Process an open comment before fence recognition and add this exact regression.
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"}' |
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.
--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
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.
fi
if printf '%s\n' "${BODY}" | grep -Eq '^[[:blank:]]*(```|~~~)'; then
echo "FAIL[check-naming]: PR body must not contain fenced code delimiters"
if printf '%s\n' "${RENDERED_BODY}" | grep -Eiq '<details([[:space:]>])'; then
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.
echo "FAIL[check-naming]: rendered PR body must not contain collapsed details"
FAILED=1
fi
8
@@ -72,9 +81,9 @@ if [ "${BRANCH_KIND}" = "role-bug" ]; then
FAILED=1
fi
tracking=$(printf '%s\n' "${VISIBLE_BODY}" | awk '
/^## Tracking[[:space:]]*$/ { in_section=1; next }
/^## / && in_section { exit }
tracking=$(printf '%s\n' "${RENDERED_BODY}" | awk '
/<h2[^>]*>Tracking<\/h2>/ { in_section=1; next }
/<h2[^>]*>/ && in_section { exit }
in_section { print }
')
if [ -z "${tracking}" ]; then
1
@@ -97,12 +106,12 @@ elif [ "${BRANCH_KIND}" = "chore" ] && [ -n "${TITLE_BUG}" ]; then
fi
if [ "${BRANCH_KIND}" != "invalid" ]; then
attribution=$(printf '%s\n' "${VISIBLE_BODY}" | awk '
/^## Attribution[[:space:]]*$/ { in_section=1; next }
/^## / && in_section { exit }
attribution=$(printf '%s\n' "${RENDERED_BODY}" | awk '
/<h2[^>]*>Attribution<\/h2>/ { in_section=1; next }
/<h2[^>]*>/ && in_section { exit }
in_section { print }
')
watermark_re='^[-*]?[[:space:]]*Authored-By: .+ \(.+\) <noreply@[[:alnum:].-]+>$'
watermark_re='Authored-By: .+ \(.+\) <a href="mailto:noreply@[[:alnum:].-]+"[^>]*>noreply@[[:alnum:].-]+</a>'
if ! printf '%s\n' "${attribution}" | grep -qE "${watermark_re}"; then
echo "FAIL[check-naming]: ## Attribution must contain an Authored-By product/model watermark"
FAILED=1
Executable
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Deterministic Gitea Markdown renderer stub for check.sh contract tests.
set -euo pipefail
if printf '%s\n' "${PR_BODY}" | grep -Fq '<!--'; then
exit 0
fi
printf '%s\n' "${PR_BODY}" | awk '
{
line=$0
lower=tolower(line)
pos=1
while (pos <= 4 && substr(line, pos, 1) == " ") pos++
char=substr(line, pos, 1)
run=0
if (char == "`" || char == "~") while (substr(line, pos + run, 1) == char) run++
rest=substr(line, pos + run)
if (in_fence) {
if (pos <= 4 && char == fence_char && run >= fence_run && rest ~ /^[[:blank:]]*$/) in_fence=0
next
}
if (in_script) {
if (lower ~ /<\/script[[:blank:]]*>/) in_script=0
next
}
if (pos <= 4 && run >= 3 && (char == "~" || index(rest, "`") == 0)) {
in_fence=1
fence_char=char
fence_run=run
next
}
if (lower ~ /^[ ]{0,3}<script([[:blank:]>])/) {
in_script=1
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 ~ /Authored-By:.*<noreply@[[:alnum:].-]+>/) {
email=line
sub(/^.*</, "", email)
sub(/>.*/, "", email)
sub(/ <noreply@[[:alnum:].-]+>.*/, "", line)
print line " <a href=\"mailto:" email "\">" email "</a>"
}
else {
gsub(/</, "\\&lt;", line)
gsub(/>/, "\\&gt;", line)
print line
}
}
'
+30 -16
View File
@@ -3,6 +3,7 @@
set -euo pipefail
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/check.sh"
TEST_BIN="$(cd "$(dirname "$0")" && pwd)/bin"
PASS=0
FAIL=0
FIXTURES=$(mktemp -d)
@@ -28,7 +29,8 @@ check() {
body=$(canonical_body "${bug:-bug-test}")
local out rc=0
out=$(cd "${FIXTURES}" && HEAD_BRANCH="$branch" PR_TITLE="$title" PR_AUTHOR="$author" \
PR_BODY="$body" BASE_SHA="$BASE" HEAD_SHA="$GOOD_HEAD" bash "$SCRIPT" 2>&1) || rc=$?
PR_BODY="$body" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" PATH="${TEST_BIN}:$PATH" \
BASE_SHA="$BASE" HEAD_SHA="$GOOD_HEAD" bash "$SCRIPT" 2>&1) || rc=$?
local got_lines=0
echo "$out" | grep -qE "^FAIL" && got_lines=1 || true
if [ "$rc" -eq "$want_fail" ] && [ "$got_lines" -eq "$want_fail" ]; then
@@ -46,7 +48,8 @@ check_contract() {
local out rc=0
out=$(cd "${FIXTURES}" && HEAD_BRANCH="architect/bug-x7k2m9/contract" \
PR_TITLE="[bug-x7k2m9] Enforce contract" PR_AUTHOR="architect" \
PR_BODY="$body" BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$?
PR_BODY="$body" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" PATH="${TEST_BIN}:$PATH" \
BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$?
if [ "$rc" -eq 1 ] && printf '%s\n' "$out" | grep -Fq "$diagnostic"; then
echo "PASS [$desc]"
PASS=$((PASS + 1))
@@ -62,7 +65,8 @@ check_contract_pass() {
local out rc=0
out=$(cd "${FIXTURES}" && HEAD_BRANCH="architect/bug-x7k2m9/contract" \
PR_TITLE="[bug-x7k2m9] Enforce contract" PR_AUTHOR="architect" \
PR_BODY="$body" BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$?
PR_BODY="$body" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" PATH="${TEST_BIN}:$PATH" \
BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$?
if [ "$rc" -eq 0 ] && printf '%s\n' "$out" | grep -Fq "check-naming: ok"; then
echo "PASS [$desc]"
PASS=$((PASS + 1))
@@ -78,7 +82,8 @@ check_chore_contract() {
local out rc=0
out=$(cd "${FIXTURES}" && HEAD_BRANCH="chore/contract" \
PR_TITLE="Improve delivery contract" PR_AUTHOR="dev" \
PR_BODY="$body" BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$?
PR_BODY="$body" GITEA_SERVER_URL="https://code.test" GITEA_TOKEN="test-token" PATH="${TEST_BIN}:$PATH" \
BASE_SHA="$base" HEAD_SHA="$head" bash "$SCRIPT" 2>&1) || rc=$?
if [ "$rc" -eq 1 ] && printf '%s\n' "$out" | grep -Fq "$diagnostic"; then
echo "PASS [$desc]"
PASS=$((PASS + 1))
@@ -140,34 +145,43 @@ check_contract "attribution must be in its own section" \
"$BASE" "$GOOD_HEAD" "## Attribution must contain"
check_contract "hidden provenance does not satisfy the visible contract" \
$'<!--\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-->' \
"$BASE" "$GOOD_HEAD" "PR body must not contain HTML comment delimiters"
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "comment removal cannot synthesize section headings" \
$'<!-- hidden -->## Tracking\n- Fixes bug-x7k2m9 — https://agenthub.fritzlab.net/bug-x7k2m9\n\n<!-- hidden -->## Attribution\n- Authored-By: Codex (GPT-5) <noreply@openai.com>' \
"$BASE" "$GOOD_HEAD" "PR body must not contain HTML comment delimiters"
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "four-space indented backticks do not expose comments" \
$' ````\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-->' \
"$BASE" "$GOOD_HEAD" "PR body must not contain HTML comment delimiters"
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "unequal backtick runs do not expose comments" \
$'`<!--``\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-->' \
"$BASE" "$GOOD_HEAD" "PR body must not contain HTML comment delimiters"
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "invalid backtick fence info does not expose comments" \
$'```html`oops\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-->' \
"$BASE" "$GOOD_HEAD" "PR body must not contain HTML comment delimiters"
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "invalid fence closer does not expose comments" \
$'```html\n```oops\n```\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-->' \
"$BASE" "$GOOD_HEAD" "PR body must not contain HTML comment delimiters"
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "fences inside comments do not expose provenance" \
$'<!--\n```html\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-->' \
"$BASE" "$GOOD_HEAD" "PR body must not contain HTML comment delimiters"
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "escaped backticks do not hide a comment opener" \
$'\\`<!--\\`\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-->' \
"$BASE" "$GOOD_HEAD" "PR body must not contain HTML comment delimiters"
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "fenced provenance does not satisfy the visible contract" \
$'```\n```oops\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" "PR body must not contain fenced code delimiters"
check_contract "fenced code delimiters are rejected" \
$'```text\nvisible example\n```\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" "PR body must not contain fenced code delimiters"
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract "indented fence closer does not expose provenance" \
$'```\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```' \
"$BASE" "$GOOD_HEAD" "PR body must contain a non-empty ## Tracking section"
check_contract_pass "fenced code examples remain available" \
$'```html\n<script>example only</script>\n```\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 "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"
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"
git -C "${FIXTURES}" commit --allow-empty -q -m "unwatermarked change"
BAD_HEAD=$(git -C "${FIXTURES}" rev-parse HEAD)