CI-red root cause (classification a: my round-4 change fails in the clean/cold
CI env): get_gitea_token_for_login hard-required PyYAML (`import yaml`), which is
absent on CI's node:24-alpine (python3 without py3-yaml). Round-4's --login
override cases were the first to exercise that path, turning the mosaic package
test (test:framework-shell -> test-pr-review-gitea-comment.sh) RED. Fix: add an
indentation-aware line-parser fallback that resolves the SAME per-name token
PyYAML would from tea's flat `logins:` list; PyYAML stays the fast path. This
also repairs a latent production defect (--login overrides were silently
unusable on any PyYAML-less host).
Auditor blockers folded into the same round-5:
1. issue_url vs pull_request_url shape (correctness): Gitea populates WEB (html)
URLs in issue_url/pull_request_url, not API paths, and a PR-conversation
comment carries pull_request_url (issue_url empty). Verification now accepts
either web shape scoped to the repo slug + number, so a durable write is never
rejected for URL shape. Test stubs now emit the REAL Gitea web shapes.
2. Cross-host credential binding (security): get_gitea_token_for_login now takes
the repo host and requires the matched login's configured URL host to equal
it; an override login configured for a different host FAILS CLOSED instead of
sending a cross-host credential. Regression tests added to both suites.
3. Non-exhaustive enumeration (false-fail): removed the redundant, non-exhaustive
post-verification list enumeration (gitea_fetch_all + confirm_*_enumerable)
from both wrappers; the exact-id GET is authoritative. Pagination cases
dropped; a guard asserts no list enumeration is performed.
4. Trap clobbering / temp-file leak (security/hygiene): removing the nested
enumeration eliminates the RETURN-trap nesting that clobbered caller cleanup;
remaining RETURN traps are single/non-nested and clean up on all exit paths.
Temp-file leak regression tests (success + failure paths) added to both suites.
5. README: corrected the exhaustive-pagination claim and documented host-bound
--login selection.
Preserves every round-2/3/4 fix (explicit --login fail-closed at all write
sites, token->identity attribution seam). Gates: cold `pnpm turbo run test
--filter=@mosaicstack/mosaic` green (14/14); full test-*.sh suite green with AND
without PyYAML; bash -n, shellcheck -x -S warning, prettier --check README clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the tea-based write + boundary/author read-back with a direct Gitea
REST POST that returns the created record's id, and verify that exact record.
BLOCKER 2 (credential ordering): resolve the acting identity, the write token,
and the read-back token from the SAME effective login. A --login override now
selects the credential used for the POST, GET /user, and the GET-by-id
read-back, so an overridden write is verified against the identity that
performed it -- not the host default. Login-name resolution is best-effort and
non-fatal (the override always wins; otherwise fall back to the host
credential), so exotic/ported hosts still resolve a token.
BLOCKER 1+3 (attribution + tautological tests): the write is now
POST /issues/{n}/comments or POST /pulls/{n}/reviews (event + body + commit_id
== PR head), parsing the provider-returned created id. Verification GETs that
exact id and checks author == acting identity and body (comments) or state +
commit_id (reviews). Keying on the created id closes the concurrency window:
a no-op create yields no id and fails closed with no list-scan fallback, and a
concurrent same-identity record has a different id. The review body travels in
the review submit, removing the separate detached comment.
Tests: the curl stub now models a real server with persistent on-disk
review/comment state -- a POST actually creates+persists a record and returns
its id, and the read-back reads that same state (no fabricated record for the
wrapper to find). Adds same-identity no-op-concurrent and author-mismatch
fail-closed cases for both comments and reviews, and >page-1 pagination
coverage for both. README "Durable review provenance" refreshed for the REST
mechanism.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>