No pr-edit wrapper: PR body/title edits force a raw PATCH or an identity compromise (pr-metadata is GET-only; issue-edit resolves login by first host match) #1047

Open
opened 2026-08-04 23:32:44 +00:00 by Mos · 1 comment
Contributor

Problem

There is no wrapper for editing a PR body/title, and the gap forces either a raw provider PATCH or an identity compromise. Measured, not assumed:

  • pr-metadata.sh is GET-only — it normalises PR JSON; there is no PATCH/POST/PUT anywhere in it. (Its name suggests mutability; the body does not have it.)
  • The git tools directory has pr-create, pr-merge, pr-close, pr-review, pr-view, pr-diff, pr-list, pr-ci-wait, pr-metadata — and no pr-edit.
  • issue-edit.sh exists but is tea-only with zero curl, resolving --login by first host match — so it authors as whatever login happens to sort first for that host, which is the misattribution class of #1044.

Why this bites: the authority gradient is inverted from the org chart

Enumerated from the provider with a negative control (a fabricated user returns does-not-exist, so the query discriminates):

Layer Repo permission
Worker seats (coders, reviewers, gate) write
Coordination layer (orchestrator, team leads) read

Writes were being routed upward to the layer that cannot perform them. The intuitive assumption — "the coordinator can do more than a worker" — is backwards here, which is why the gap surfaced only at execution, three separate times in one shift.

The order-of-questions fix: (1) who already holds this right? → (2) can I? → (3) what needs granting? Asking (2)/(3) first leads to requesting a production credential widening for something an existing principal can already do. In the live case the answer to (1) was a field (poster) on an API response already fetched twice — a PR body belongs to its poster, who can edit it as itself with no grant at all.

Why a scoped grant is not the workaround

Gitea token scopes are coarse: write:repository covers push AND pr-edit. There is no metadata-only scope, so "PR body and title only, no push" is not expressible on this provider — the nearest available grant hands over git-push authority too.

Ask — a wrapper-backed PR body/title edit operation

pr-edit.sh (or equivalent) that:

  1. Resolves identity explicitly — honours the caller's configured identity; never falls back to a shared/owner credential or a first-host-match login (see #1044).
  2. Prefers the poster route: if the caller is the PR poster, edit under the caller's own token. Otherwise fail closed with a message naming who can perform it.
  3. Read-back enforcement: re-fetch after write and assert the content.
  4. Reports the honest bound (below).
  5. Sets the working transport (UA/host) once, like the other wrappers.

Important residual the wrapper must NOT overclaim

The provider exposes no editor identity on a PR/issue BODY. Even a perfectly authenticated PATCH leaves no visible author — the write is unattributable, the content is verifiable. So the wrapper must report content-verified-by-read-back and must never emit authored-by-X-verified: the second is not checkable and must not be claimed. (This also correctly narrows the impersonation concern for bodies specifically — there is no author field to falsely populate, unlike a commit.)

Provenance

Raised by a code reviewer that was explicitly invited to contest a documented one-off raw-PATCH exception. Its disposition: acceptable for that single provider-only repair because no wrapper exists and the poster used its own scoped identity; must not become silent precedent; the durable fix is a wrapper-backed operation with identity and read-back enforcement. Filing so the exception stops being needed.

Related: #1043 (identity lifecycle), #1044 (fail-open to shared credential), #1045 (mosaic cred epic).

## Problem There is **no wrapper for editing a PR body/title**, and the gap forces either a raw provider `PATCH` or an identity compromise. Measured, not assumed: - `pr-metadata.sh` is **GET-only** — it normalises PR JSON; there is no `PATCH`/`POST`/`PUT` anywhere in it. (Its name suggests mutability; the body does not have it.) - The git tools directory has `pr-create`, `pr-merge`, `pr-close`, `pr-review`, `pr-view`, `pr-diff`, `pr-list`, `pr-ci-wait`, `pr-metadata` — and **no `pr-edit`**. - `issue-edit.sh` exists but is **tea-only with zero `curl`**, resolving `--login` by **first host match** — so it authors as whatever login happens to sort first for that host, which is the misattribution class of #1044. ## Why this bites: the authority gradient is inverted from the org chart Enumerated from the provider with a negative control (a fabricated user returns *does-not-exist*, so the query discriminates): | Layer | Repo permission | |---|---| | Worker seats (coders, reviewers, gate) | **write** | | Coordination layer (orchestrator, team leads) | **read** | **Writes were being routed *upward* to the layer that cannot perform them.** The intuitive assumption — "the coordinator can do more than a worker" — is backwards here, which is why the gap surfaced only at execution, three separate times in one shift. **The order-of-questions fix:** (1) **who already holds this right?** → (2) can I? → (3) what needs granting? Asking (2)/(3) first leads to requesting a production credential widening for something an existing principal can already do. In the live case the answer to (1) was a field (`poster`) on an API response already fetched twice — **a PR body belongs to its poster**, who can edit it as itself with no grant at all. ## Why a scoped grant is not the workaround Gitea token scopes are **coarse**: `write:repository` covers **push AND pr-edit**. There is no metadata-only scope, so "PR body and title only, no push" **is not expressible on this provider** — the nearest available grant hands over git-push authority too. ## Ask — a wrapper-backed PR body/title edit operation `pr-edit.sh` (or equivalent) that: 1. **Resolves identity explicitly** — honours the caller's configured identity; **never** falls back to a shared/owner credential or a first-host-match login (see #1044). 2. **Prefers the poster route**: if the caller is the PR poster, edit under the caller's own token. Otherwise fail closed with a message naming *who can* perform it. 3. **Read-back enforcement**: re-fetch after write and assert the content. 4. **Reports the honest bound** (below). 5. Sets the working transport (UA/host) once, like the other wrappers. ## Important residual the wrapper must NOT overclaim **The provider exposes no editor identity on a PR/issue BODY.** Even a perfectly authenticated `PATCH` leaves **no visible author** — the write is **unattributable**, the content is **verifiable**. So the wrapper must report *content-verified-by-read-back* and must **never** emit *authored-by-X-verified*: the second is not checkable and must not be claimed. (This also correctly narrows the impersonation concern for bodies specifically — there is no author field to falsely populate, unlike a commit.) ## Provenance Raised by a code reviewer that was explicitly invited to contest a documented one-off raw-`PATCH` exception. Its disposition: acceptable for that single provider-only repair because no wrapper exists and the poster used its own scoped identity; **must not become silent precedent**; the durable fix is a wrapper-backed operation with identity and read-back enforcement. Filing so the exception stops being needed. Related: #1043 (identity lifecycle), #1044 (fail-open to shared credential), #1045 (`mosaic cred` epic).
Author
Contributor

Binding the measurements: file hashes, and why they are here instead of a commit

ESTATE: CROSS — the tooling described here is HOMELAB code (mosaicstack/stack), but every behaviour was measured against USC targets. The code is shared; the observed instances are not necessarily.

The problem this comment fixes

The deployed tooling has no version to cite. ~/.config/mosaic is not a git repo — no ref, no commit, nothing a finding can be bound to. (Confirmed directly.) Worse, the framework version is not even locally single-valued: separate stack clones exist at different commits on the same host.

Consequence, stated by the reporting agent better than I can:

The gate I hash-bind can tell me it moved. The tooling my evidence rests on cannot.

A framework change lands upstream, every finding in this issue silently goes stale, and nothing in the record disagrees. A maintainer reading it later cannot tell whether it describes current code.

SHA-256 (first 12) of the deployed files these findings were measured against

file sha256[0:12]
tools/git/pr-metadata.sh d088fe3f825f
tools/git/issue-edit.sh 5aacffa3f664
tools/git/pr-merge.sh 08a65e8584c5
tools/git/detect-platform.sh 02cdb2cf5ec2
tools/git/ci-queue-wait.sh 19cda2f7009c
tools/_lib/credentials.sh a108fab57896
tools/tmux/agent-send.sh a04eb80afd7d

Three independent derivations agree on these (two agents derived them separately; a third re-derived all seven from the deployed copy and matched every one). They are a content binding, not a version — which is the point: they make staleness detectable even though the deployment carries no ref.

Standing rule adopted by the reporting lane

Any tooling finding is VOID if its file hash has moved. RE-DERIVE, do not RE-CITE.

Applies beyond this issue

The same files underpin the measurements in #1043 (pane export allowlist), #1044 (get_gitea_token fail-open), #1046 (codex review wrappers) and #1049 (interleaved tea logins). Treat the hashes above as the binding for those too, and re-derive before acting on any of them.

Suggested framework fix, separate from this issue's ask

Give the deployed tooling a version stamp (embedded commit/tag written at install time, readable by the wrappers), so findings can bind to a ref rather than to content hashes. Content hashes are a workaround for a deployment that cannot say what it is.

## Binding the measurements: file hashes, and why they are here instead of a commit **ESTATE: CROSS** — the tooling described here is **HOMELAB** code (`mosaicstack/stack`), but every behaviour was **measured against USC targets**. The code is shared; the observed *instances* are not necessarily. ### The problem this comment fixes **The deployed tooling has no version to cite.** `~/.config/mosaic` is **not a git repo** — no ref, no commit, nothing a finding can be bound to. (Confirmed directly.) Worse, the framework version is not even locally single-valued: separate stack clones exist at different commits on the same host. Consequence, stated by the reporting agent better than I can: > **The gate I hash-bind can tell me it moved. The tooling my evidence rests on cannot.** A framework change lands upstream, every finding in this issue silently goes stale, **and nothing in the record disagrees.** A maintainer reading it later cannot tell whether it describes current code. ### SHA-256 (first 12) of the deployed files these findings were measured against | file | sha256[0:12] | |---|---| | `tools/git/pr-metadata.sh` | `d088fe3f825f` | | `tools/git/issue-edit.sh` | `5aacffa3f664` | | `tools/git/pr-merge.sh` | `08a65e8584c5` | | `tools/git/detect-platform.sh` | `02cdb2cf5ec2` | | `tools/git/ci-queue-wait.sh` | `19cda2f7009c` | | `tools/_lib/credentials.sh` | `a108fab57896` | | `tools/tmux/agent-send.sh` | `a04eb80afd7d` | **Three independent derivations agree** on these (two agents derived them separately; a third re-derived all seven from the deployed copy and matched every one). They are a *content* binding, not a version — which is the point: they make staleness **detectable** even though the deployment carries no ref. ### Standing rule adopted by the reporting lane > **Any tooling finding is VOID if its file hash has moved. RE-DERIVE, do not RE-CITE.** ### Applies beyond this issue The same files underpin the measurements in **#1043** (pane export allowlist), **#1044** (`get_gitea_token` fail-open), **#1046** (codex review wrappers) and **#1049** (interleaved tea logins). Treat the hashes above as the binding for those too, and re-derive before acting on any of them. ### Suggested framework fix, separate from this issue's ask Give the deployed tooling a **version stamp** (embedded commit/tag written at install time, readable by the wrappers), so findings can bind to a ref rather than to content hashes. Content hashes are a workaround for a deployment that cannot say what it is.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1047