Files
stack/packages/mosaic/framework/tools/git
Hermes Agent 4822291707
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
fix(git-wrappers): #865 round-7 addendum — conservative YAML recognizer + review/comment hardening
Fold the remaining round-7 audit items into the tea-CLI comment-invocation fix.

Fallback token parser (detect-platform.sh, test-gitea-login-resolution.sh):
Replace the line-by-line scalar fallback with a strict CONSERVATIVE block-YAML
recognizer that reconstructs the same object PyYAML would or fails closed the
instant it meets anything outside the tea-config subset. Closes 5 structural
fail-open classes the old parser missed (nested-shadow logins, block-scalar
shadow, duplicate root key / login name / token field, malformed-after-valid,
extra-document / end-marker). Validated by a 360k-check differential fuzz vs
real PyYAML (0 fail-open) plus explicit forced-PyYAML-absence fixtures.

ITEM 1 (pr-review.sh) current-head TOCTOU: after the exact review-id read-back
succeeds, re-read the live PR head and fail closed if it advanced past the
submitted commit_id, so a review is never reported as covering a superseded tip.

ITEM 2 (pr-review.sh comment action): require the returned resource be a
pull_request (populated pull_request_url); reject a bare issue_url so a plain
issue #N cannot masquerade as a verified PR comment. issue-comment.sh keeps its
broader issue-or-PR acceptance.

ITEM 3a (both wrappers): move the Authorization bearer OUT of curl argv into a
private mode-0600 curl --config file (gitea_write_auth_config), removed on every
exit path, so the token never appears in the process table.

ITEM 3b (pr-review.sh): bind the review body with presence + string-type + exact
equality instead of `(body or "")`, so a non-empty submitted body persisted as
null/missing fails closed.

Tests: add race, plain-issue, argv-capture (no token printed), and null-body
fixtures; broaden temp-leak checks to the auth-config files. Full gate set green
(bash -n, shellcheck -x -S warning, prettier, all 3 REST/resolution suites with
PyYAML and forced-absent, cold TURBO_FORCE turbo 14/14).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 23:01:21 -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.

The write is a direct Gitea REST POST that returns the created record's id. Neither wrapper writes through tea — tea 0.11.1 can silently no-op while exiting 0 and cannot emit the id of a record it creates, so its exit code is worthless as proof of a durable write (#865). Instead:

  • Comments (issue-comment.sh, and the comment action of pr-review.sh) POST /api/v1/repos/{owner}/{repo}/issues/{index}/comments, requiring a 201 and parsing the created comment's id from the response body.
  • Reviews (approve / request-changes) POST /api/v1/repos/{owner}/{repo}/pulls/{index}/reviews with the event (APPROVED / REQUEST_CHANGES), the review body, and commit_id pinned to the PR's current head, then parse the created review's id. The review body travels in the review submit itself — there is no separate detached comment to reconcile (a Gitea REQUEST_CHANGES review requires a non-empty body, which the submit carries).

Verification keys on that exact provider-returned id. The wrapper then GETs that one record directly — GET /issues/comments/{id} or GET /pulls/{n}/reviews/{id} — and requires that its id equals the created id, its author login equals the acting identity (resolved via GET /api/v1/user for the token in use), and, for comments, its body exactly matches what was submitted and its returned web URL belongs to this exact provider and repository (the issue_url / pull_request_url origin — scheme, host, and effective port — and full path, i.e. deployment prefix + exact owner/repo + kind + number, must match; a suffix/endsWith test would accept a look-alike host or a decoy path prefix, so the whole normalized URL is compared). The comment action of pr-review.sh additionally requires the returned resource be a pull request (a populated pull_request_url); a bare issue_url is rejected, so if issue #N exists but PR #N does not, an issue comment cannot be reported as a verified PR comment. (issue-comment.sh legitimately keeps the broader issue-or-PR acceptance.) For reviews, its state matches the requested action, its reviewed commit_id equals the PR head, and its persisted body equals the submitted body — an exact, presence- and type-checked equality (a missing/null persisted body no longer counts as an empty match), because Gitea can finalize/reuse a pending review id whose stored content was authored elsewhere, so the body is bound too. The write, the /user identity lookup, and the read-back all use the same credential — the effective login's token, or the host credential when no login is named — so the write is verified against the identity that actually performed it.

A review's pinned head is re-checked after verification (current-head TOCTOU). The commit_id is pinned to the PR head read before the submit; between that read and the read-back the branch could advance (a force-push or a new commit), leaving a verified review attached to a now-superseded commit while the live tip carries unreviewed code. After the exact-id read-back succeeds, the wrapper re-reads the live PR head (GET …/pulls/{n}) and requires it still equals the submitted SHA; if the head advanced it fails closed (non-zero, no success line) rather than reporting a review that no longer covers the PR's current commit.

This closes the concurrency window rather than documenting it. Because verification keys on the id the create returned, a no-op create yields no id and fails closed with no list-scan fallback, and a concurrent record — even one written by the same identity with an identical body/state — has a different id and cannot be mistaken for this write. There is no residual same-identity window: the earlier boundary-and-author heuristic (accept any id > pre-write-max with a matching author) is replaced entirely by exact-id attribution.

Exact-id read-back is the sole authority. Verification is a direct GET of the one record the create returned; there is no follow-up list enumeration. An earlier redundant pass that re-listed the record's page (?limit=&page=1,2,…) was removed: server-capped page sizes and list-pagination quirks made it a false-failure source (a durable, exact-id-verified record could be missed by a non-exhaustive enumeration), and it added nothing over the authoritative exact-id GET.

Credential handling

The Gitea API token is never passed on a curl command line. An Authorization: token <value> argument would be visible to any local process that can read the process table (ps / /proc/<pid>/cmdline) for the lifetime of the request. Instead, every authenticated curl call writes the header into a private, mode-0600 config file under $TMPDIR and passes it with curl --config <file> (gitea_write_auth_config), so only the file path — never the token — appears in argv. Each such file is unlinked on every exit path (success and failure) by the caller's RETURN trap.

tea invocation notes (Gitea)

  • tea v0.11.1 has no comment subcommand under tea pr or tea issue — the tea pr comment / tea issue comment forms don't error, they silently fall through to a no-op and still exit 0, producing a false-success write (#865). tea's write subcommands (tea comment, tea pr approve/reject) also cannot report the id of the record they create, so their exit code cannot prove a durable write. These wrappers therefore do not write reviews or comments through tea at all; they use direct Gitea REST POSTs that return the created record's id (see "Durable review provenance" above). tea is consulted only to enumerate the login list for host→login resolution.
  • Because the review body is carried in the POST …/reviews submit itself, there is no separate detached review comment, and the historical tea pr approve/reject trailing-positional-argument vs. nonexistent --comment/-comment flag hazard (#835) no longer applies to these wrappers — no review comment is ever passed to tea.

--login override

Both pr-review.sh and issue-comment.sh accept an optional --login <name> flag that overrides the automatically detected Gitea login for that single invocation. The override selects which credential the REST write, the /user identity lookup, and the read-back all use — its token is resolved from the tea config for that login name (get_gitea_token_for_login), falling back to the repo host's credential when no login is named. The resolved login is host- and port-bound: the login's configured URL host and effective port (the scheme's default port — 80 for http, 443 for https — applies when a port is omitted, symmetrically on both sides) must match the repo remote's, so a login name shared across hosts (or an override configured for a different Gitea, including one on a different port of the same host) can never send one host's credential to another — a host or port mismatch fails closed rather than leaking a cross-host token. Resolving the acting identity and the read-back from the same login that performs the write is essential: a write performed under an overridden login must be verified against that login's identity, not the host default's. Callers who need a different login than the host default should pass --login <reviewer-login>.

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.