Files
stack/packages/mosaic/framework/tools/git
Hermes Agent 16481ece3d
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
fix(tools): attribute read-back to acting identity and paginate fully (#865)
Round 2 remediation for the read-back verification in issue-comment.sh and
pr-review.sh.

BLOCKER A (invocation attribution): id-above-boundary + content/state match
only proves temporal ordering — a concurrent write from a different identity
could satisfy it while this tea invocation created nothing. Both wrappers now
resolve the acting identity once via curl GET /api/v1/user and additionally
require the accepted record's author login to equal that identity. Residual
same-identity same-body/state concurrency is documented in-code (tea 0.11.1
emits no reliable created-record id to close it further).

BLOCKER B (pagination): the comments and reviews list reads now walk every
page (?limit=&page=1,2,… until a short/empty page) for both the pre-write
boundary and the post-write read-back, so a record beyond page 1 is still
found.

Adds regressions: concurrent different-identity write fails closed (comments
and reviews); a matching review beyond page 1 is still found. README updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:02:25 -05:00
..
2026-07-12 20:49:43 +00:00
2026-07-12 20:49:43 +00:00

Git provider wrappers

These scripts provide host-aware GitHub and Gitea issue, pull-request, milestone, and CI operations.

Durable review provenance

A successful provider write command—or a wrapper message based only on that command's exit code—is not durable review provenance. Review comments, approvals, and change requests count as durable provenance only after the wrapper reads the created provider record back and verifies that it was created by this write.

pr-review.sh therefore fails closed when a Gitea comment cannot be written, its created comment ID cannot be identified, or provider read-back does not match. It reports comment success only after that read-back verification passes. The approve and request-changes actions apply the same discipline to the review state itself: they record the maximum existing review id before invoking tea pr approve/reject, then require a review whose id is strictly greater than that boundary, whose author login equals the acting identity (resolved via GET /api/v1/user for the token in use), whose state matches the requested action (APPROVED / REQUEST_CHANGES), and whose reviewed commit equals the PR's current head. tea's exit code alone is never treated as evidence the review landed.

issue-comment.sh applies the same fail-closed, boundary-bounded read-back to issue comments: it records the maximum existing comment id before posting via tea comment, then re-fetches the issue's comments via the Gitea REST API and requires a comment whose id is strictly greater than that boundary, whose author login equals the acting identity, and whose body exactly matches what was submitted. Bounding the read-back by the pre-write id is essential — a body-only match across all history would falsely report success if tea comment silently no-ops (the #865 bug) while an identically-bodied comment already existed from a prior run. Gitea comment and review ids are monotonic, so id > boundary reliably means "created after this write began".

Invocation attribution, not just temporal ordering. id > boundary alone only proves a record was created after the write began; it would still be satisfied by a concurrent write from a different identity while this tea invocation created nothing. Both wrappers therefore additionally require the accepted record's author login to equal the identity the API token authenticates as, narrowing the match to this invocation's writer. The one residual window — a concurrent write by the same identity with an identical body/state inside the boundary window — cannot be eliminated without a tea-emitted created-record id, which tea 0.11.1 does not reliably provide; it is strictly narrower than temporal-only matching and is documented in-code.

Full pagination. Gitea paginates list endpoints, so a single-page read of the comments or reviews list would false-negative once the freshly created record lands beyond the first page. Both the pre-write boundary computation and the post-write read-back walk every page (?limit=&page=1,2,… until a short/empty page) so the match is exhaustive regardless of how many comments or reviews already exist.

tea invocation notes (Gitea)

  • tea v0.11.1 has no comment subcommand under tea pr or tea issue. The correct invocation is the top-level tea comment <index> <body> [--repo ...] [--login ...]. The tea pr comment / tea issue comment forms don't error — tea silently falls through to a no-op and still exits 0, producing a false-success write (#865). Always use the top-level form.
  • tea pr approve and tea pr reject take an optional review comment/reason as a trailing positional argument, not a --comment/-comment flag (that flag does not exist on those subcommands). pr-review.sh avoids this positional form entirely for the approve/reject actions and instead posts any review comment through the same durable, read-back-verified comment API used for the comment action (see #835/#812) — the trailing-positional form remains available to callers who invoke tea directly, but is not used by these wrappers.

--login passthrough

Both pr-review.sh and issue-comment.sh accept an optional --login <name> flag that overrides the automatically detected Gitea tea login for that single invocation. The override is appended to the tea command line after the detected default (get_gitea_repo_args() / get_gitea_login[_for_host]()), because tea honors only the last --login flag on its command line — an override placed before the default would be silently clobbered by it. Callers who need a different login than the host default should pass --login <reviewer-login> rather than relying on ordering tricks or re-invoking tea login globally.

As a durable successor to this mechanism, consider giving each reviewer/approver slot its own dedicated Gitea login credential, so that author≠reviewer holds at the credential level rather than relying on wrapper-level --login bookkeeping. This is a recommendation for future hardening, not something implemented by this flag.