HOMELAB-2526: fix(forgejo): enforce pi-landing merge identity #76

Merged
aaron merged 1 commit from plane/HOMELAB-2526-merge-identity into main 2026-11-03 00:32:57 +00:00
Owner

What

forgejo_merge_pr wrote with whatever token sat in FORGEJO_API_KEY and never asked the API whose token it was. On a repository whose live merge whitelist is ["pi-landing"] that write is a 405 (GITS-316); on one that still admits the operator (infra-core/live was ["pi-landing","aaron",…]) it succeeds and the landing is attributed to aaron — the operator's name on a landing they never performed (GITS-359). The tool also printed PR #N merged: undefined, because Forgejo answers the merge POST with 200 and an empty body, so neither the merged SHA nor merged_by was reported.

Three defects, all measured red-first through fake-API tests:

  1. no landing credential source existed, so the shared operator token was the only credential the merge leg could use;
  2. nothing read GET /user before the write, so a merge could not be refused for being attributed to the wrong identity;
  3. the result echoed the merge response (an absent message) instead of reading the merged commit back from the PR.

The contract

forgejo/api.ts (and the tool description / prompt guidelines, since docs/forgejo.md is held by open PR #74):

Env var Meaning
FORGEJO_LANDING_TOKEN_FILE path to a 0600 file holding the landing token (preferred: a token in a file is not in ps, shell history or an env dump)
FORGEJO_LANDING_TOKEN the landing token inline, when no file is used
FORGEJO_LANDING_IDENTITY the login a landing must be attributed to; default pi-landing
  • resolveLandingCredential — …_TOKEN_FILE → …_TOKEN → the configured FORGEJO_API_KEY. A landing source that is set but unreadable or empty REFUSES; it never degrades to the shared token for the write.
  • assertMergeIdentity — GET /user with that credential must answer the landing login. Missing / mismatched / unreadable refuses, and the identity read happens immediately before the merge POST (asserted: no GET between the identity read and the write).
  • mergeAsLanding — the one merge write. It takes the credential from a branded proof minted by the assertion, so a hand-rolled {ok: true} cannot reach the POST and a caller cannot assert one identity then write with another. The token rides on the proof non-enumerably and never serialises into a tool result.
  • readMergeOutcome — merged state, merge_commit_sha and merged_by from the PR read-back, with the base-branch head as a labelled fallback when a build exposes no merge_commit_sha. Unknown facts are reported as unknown / unverified: the word undefined can no longer reach the output.

Ordinary reads and every non-merge tool keep using FORGEJO_API_KEY; the landing credential is only used for the merge POST and its identity read. The landing lock is untouched and still runs first.

Verification (red → green)

Red on origin/main fd410c3 with the new test file present — forgejo/tests/merge-identity.test.ts, 13 failed / 3 passed:

× merges with the dedicated landing credential, never the operator token
  → expected 'operator-shared-credential' to be 'landing-dedicated-credential'
× refuses when the landing credential is the operator login        → it merged
× refuses any readable login that is not the landing identity      → promise resolved instead of rejecting
× refuses an unreadable identity (GET /user 403)                   → it merged
× asserts the identity immediately before the merge write          → no GET /user at all (-1)
× never prints `undefined` when Forgejo answers 200 with an empty body
  → expected 'PR #79 merged: undefined…' not to contain 'undefined'
× reports the merged commit sha and merged_by from the read-back   → details.mergedSha undefined

Green after the fix:

$ npx vitest run tests/merge-identity.test.ts
 Test Files  1 passed (1)
      Tests  25 passed (25)

$ npx vitest run
 Test Files  13 passed (13)
      Tests  290 passed (290)      # 265 pre-existing, untouched + 25 new

$ npx tsc --noEmit                  # exit 0

Every refusal test asserts zero mutating requests (POST/PUT/PATCH/DELETE), not merely zero merge POSTs. The 265 pre-existing tests pass unmodified — including all five landing-lock fake suites — so no lock leg moved.

Scope

Files: forgejo/api.ts, forgejo/index.ts, forgejo/types.ts, forgejo/tests/fake.ts (identity/merge/branch routes + per-credential GET /user + header capture), new forgejo/tests/merge-identity.test.ts.

Out of scope by construction: forgejo/landing.ts and forgejo/tests/landing-blockers.test.ts (held by PR #74), docs/forgejo.md (held by PR #74), branch-protection settings, the signing-path classifier, and any live credential — this worker holds no real landing token (GITS-317 remains operator-queued). No self-review, no verdict, no merge.

Collision check (before the first edit and again before the push)

Open PRs on this repo: #74 (docs/forgejo.md, forgejo/landing.ts, forgejo/tests/landing-blockers.test.ts) and #68 (pi-swarm-suite/src/core/index.ts). No file in this PR overlaps either.

## What `forgejo_merge_pr` wrote with whatever token sat in `FORGEJO_API_KEY` and never asked the API whose token it was. On a repository whose live merge whitelist is `["pi-landing"]` that write is a 405 (GITS-316); on one that still admits the operator (`infra-core/live` was `["pi-landing","aaron",…]`) it **succeeds** and the landing is attributed to `aaron` — the operator's name on a landing they never performed (GITS-359). The tool also printed `PR #N merged: undefined`, because Forgejo answers the merge POST with 200 and an **empty body**, so neither the merged SHA nor `merged_by` was reported. Three defects, all measured red-first through fake-API tests: 1. no landing credential source existed, so the shared operator token was the only credential the merge leg could use; 2. nothing read `GET /user` before the write, so a merge could not be refused for being attributed to the wrong identity; 3. the result echoed the merge response (an absent `message`) instead of reading the merged commit back from the PR. ## The contract `forgejo/api.ts` (and the tool description / prompt guidelines, since `docs/forgejo.md` is held by open PR #74): | Env var | Meaning | |---|---| | `FORGEJO_LANDING_TOKEN_FILE` | path to a **0600** file holding the landing token (preferred: a token in a file is not in `ps`, shell history or an env dump) | | `FORGEJO_LANDING_TOKEN` | the landing token inline, when no file is used | | `FORGEJO_LANDING_IDENTITY` | the login a landing must be attributed to; default `pi-landing` | - `resolveLandingCredential` — `…_TOKEN_FILE` → `…_TOKEN` → the configured `FORGEJO_API_KEY`. A landing source that is **set but unreadable or empty REFUSES**; it never degrades to the shared token for the write. - `assertMergeIdentity` — `GET /user` with **that** credential must answer the landing login. Missing / mismatched / unreadable refuses, and the identity read happens **immediately before** the merge POST (asserted: no GET between the identity read and the write). - `mergeAsLanding` — the one merge write. It takes the credential from a branded proof minted by the assertion, so a hand-rolled `{ok: true}` cannot reach the POST and a caller cannot assert one identity then write with another. The token rides on the proof non-enumerably and never serialises into a tool result. - `readMergeOutcome` — merged state, `merge_commit_sha` and `merged_by` from the **PR read-back**, with the base-branch head as a **labelled** fallback when a build exposes no `merge_commit_sha`. Unknown facts are reported as `unknown` / `unverified`: the word `undefined` can no longer reach the output. Ordinary reads and every non-merge tool keep using `FORGEJO_API_KEY`; the landing credential is only used for the merge POST and its identity read. The landing lock is untouched and still runs first. ## Verification (red → green) Red on `origin/main` `fd410c3` with the new test file present — `forgejo/tests/merge-identity.test.ts`, 13 failed / 3 passed: ``` × merges with the dedicated landing credential, never the operator token → expected 'operator-shared-credential' to be 'landing-dedicated-credential' × refuses when the landing credential is the operator login → it merged × refuses any readable login that is not the landing identity → promise resolved instead of rejecting × refuses an unreadable identity (GET /user 403) → it merged × asserts the identity immediately before the merge write → no GET /user at all (-1) × never prints `undefined` when Forgejo answers 200 with an empty body → expected 'PR #79 merged: undefined…' not to contain 'undefined' × reports the merged commit sha and merged_by from the read-back → details.mergedSha undefined ``` Green after the fix: ``` $ npx vitest run tests/merge-identity.test.ts Test Files 1 passed (1) Tests 25 passed (25) $ npx vitest run Test Files 13 passed (13) Tests 290 passed (290) # 265 pre-existing, untouched + 25 new $ npx tsc --noEmit # exit 0 ``` Every refusal test asserts **zero mutating requests** (`POST`/`PUT`/`PATCH`/`DELETE`), not merely zero merge POSTs. The 265 pre-existing tests pass unmodified — including all five landing-lock fake suites — so no lock leg moved. ## Scope Files: `forgejo/api.ts`, `forgejo/index.ts`, `forgejo/types.ts`, `forgejo/tests/fake.ts` (identity/merge/branch routes + per-credential `GET /user` + header capture), new `forgejo/tests/merge-identity.test.ts`. Out of scope by construction: `forgejo/landing.ts` and `forgejo/tests/landing-blockers.test.ts` (held by PR #74), `docs/forgejo.md` (held by PR #74), branch-protection settings, the signing-path classifier, and any live credential — this worker holds no real landing token (GITS-317 remains operator-queued). No self-review, no verdict, no merge. ## Collision check (before the first edit and again before the push) Open PRs on this repo: #74 (`docs/forgejo.md`, `forgejo/landing.ts`, `forgejo/tests/landing-blockers.test.ts`) and #68 (`pi-swarm-suite/src/core/index.ts`). No file in this PR overlaps either.
HOMELAB-2526: fix(forgejo): enforce pi-landing merge identity
All checks were successful
CI / pr-title (pull_request) Successful in 0s
CI / skill-args (pull_request) Successful in 31s
CI / mcp-bridge (pull_request) Successful in 22s
CI / nats-intercom (pull_request) Successful in 23s
CI / pi-otel-telemetry (pull_request) Successful in 49s
CI / ask-user-question (pull_request) Successful in 36s
CI / forgejo-ci (pull_request) Successful in 53s
CI / refocus (pull_request) Successful in 43s
CI / todo (pull_request) Successful in 1m2s
CI / redact (pull_request) Successful in 34s
CI / pi-friction (pull_request) Successful in 47s
CI / plane (pull_request) Successful in 1m12s
CI / pi-swarm-suite (pull_request) Successful in 3m44s
CI / forgejo (pull_request) Successful in 3m52s
8ea27b6d0d
`forgejo_merge_pr` wrote with whatever token sat in `FORGEJO_API_KEY` and never
asked the API whose token it was. On a repository whose live merge whitelist is
`["pi-landing"]` that write is a 405 (GITS-316); on one that still admits the
operator (`infra-core/live` was `["pi-landing","aaron",…]`) it SUCCEEDS and the
landing is attributed to `aaron` — the operator's name on a landing they never
performed (GITS-359). The tool also printed `PR #N merged: undefined`, because
Forgejo answers the merge POST with 200 and an empty body, so neither the merged
SHA nor `merged_by` was reported.

Three defects, all measured red-first through fake-API tests:

- no landing credential source existed, so the shared operator token was the
  only credential the merge leg could use;
- nothing read `GET /user` before the write, so the merge could not be refused
  for being attributed to the wrong identity;
- the result echoed the merge response (absent `message`) instead of reading the
  merged commit back from the PR.

The contract (forgejo/api.ts, documented on the tool too):

- `resolveLandingCredential` — `FORGEJO_LANDING_TOKEN_FILE` (0600 file,
  preferred: a token in a file is not in `ps`, history or an env dump), then
  `FORGEJO_LANDING_TOKEN`, then the configured `FORGEJO_API_KEY`. A landing
  source that is SET but unreadable or empty REFUSES; it never degrades to the
  shared token for the write.
- `assertMergeIdentity` — `GET /user` with THAT credential must answer
  `FORGEJO_LANDING_IDENTITY` (default `pi-landing`). Missing/mismatched/
  unreadable refuses, and the identity read happens immediately before the POST.
- `mergeAsLanding` — the one merge write. It takes the credential from a
  branded proof minted by the assertion (a hand-rolled `{ok:true}` cannot reach
  the POST), so a caller cannot assert one identity and write with another. The
  token rides on the proof non-enumerably and never serialises.
- `readMergeOutcome` — merged state, `merge_commit_sha` and `merged_by` from the
  PR read-back, with the base-branch head as a LABELLED fallback when a build
  exposes no `merge_commit_sha`. Unknown facts are reported as unknown: the word
  `undefined` can no longer reach the output.

Ordinary reads and every non-merge tool keep using `FORGEJO_API_KEY`; the
landing credential is only used for the merge POST and its identity read. The
landing lock is untouched and still runs first — `forgejo/landing.ts`,
`docs/forgejo.md` (held by open PR #74) and branch-protection settings are out
of scope here.

Verification: forgejo suite 290/290 (25 new) on a tree whose 265 pre-existing
tests never moved; `tsc --noEmit` clean; every refusal test asserts ZERO
mutating requests.
Collaborator

Verdict: APPROVED

model: omniroute/deepseek-flash
head: 8ea27b6d0d

Independent GENERAL review, fresh context. No edits, no merge, no other PR, no formal review submitted. The exact head was re-fetched live from the API immediately before this post and compared to the sha in my working note: unchanged, 8ea27b6d0d. Poster identity was proved before any write: GET /api/v1/user answered login=ld-reviewer with the sanctioned reviewer credential, read through the OpenBao OIDC path into a 0700 scratch dir with 0600 files and the Bao token self-revoked; no credential touched argv, nothing was printed.

I re-measured the claims instead of reading the worker's logs. At this head the new identity suite passes 25/25, the full forgejo suite passes 290/290 across 13 files, and tsc --noEmit exits 0 with empty output. The diff is additive: 265 pre-existing tests pass unmodified, no existing test was edited. CI run 170 on this exact sha is 14/14 success. Scope is exactly the five declared files; forgejo/landing.ts, the blocker suite, docs/forgejo.md and .github are untouched, so the collision with PR #74 stays avoided.

The contract was attacked rather than accepted. Precedence is file, then env token, then the shared credential, and a dedicated source that is set but unreadable or empty refuses before any request at all, including the identity read, so it cannot even read back the operator credential it was forbidden to use. When no landing source is configured the shared credential is used but still identity-asserted: with FORGEJO_API_KEY belonging to the operator the merge is refused with zero mutating requests of any verb, and it proceeds only when that key's own /user really is pi-landing. That closes GITS-359 on the path a deployed harness takes without any configuration change. GET /user runs on the exact credential the write carries, immediately before it, with no read in between, and login matching is case- and whitespace-tolerant. The branded proof makes assert-one-write-another structurally impossible because the write takes its credential from the proof and the caller has no credential argument; hand-rolled proofs throw and send nothing. I scanned tool results, error text and every recorded request for all three credential values: none leaks, and no refusal is anything but pre-write. Read reporting is truthful: the merged sha and merged_by come from the PR read-back, the base-branch head is used only as a labelled fallback, an unreadable read-back is reported as unknown with a note, and the string undefined cannot reach the output on any path, including the empty-200 merge answer that produced the original defect. Ordinary reads still use FORGEJO_API_KEY and never touch the landing contract, and the landing lock still runs and refuses first.

Observations only, none blocking, recorded in my working note: the tool description says a missing landing credential refuses when the code deliberately falls back to an identity-asserted shared credential, so the wording overstates the guarantee; the shared-credential fallback has no tool-level test, only a unit-level proof; the documented 0600 mode of the token file is never enforced, a 0644 file is accepted; FORGEJO_LANDING_IDENTITY is an unrestricted override, so setting it to aaron reproduces the original attribution for anyone who can already set the API key; MergeAcceptance.accepted is computed and never consumed; and mergePR remains exported unguarded though nothing in the repo calls it. Together these are hardening and surface items.

forgejo is dangerous class, so this is the general lens only: a premium second opinion on a different declared model is still required before this lands.

## Verdict: APPROVED model: omniroute/deepseek-flash head: 8ea27b6d0dfd069b18bd740a93ac115535099af4 Independent GENERAL review, fresh context. No edits, no merge, no other PR, no formal review submitted. The exact head was re-fetched live from the API immediately before this post and compared to the sha in my working note: unchanged, 8ea27b6d0dfd069b18bd740a93ac115535099af4. Poster identity was proved before any write: GET /api/v1/user answered login=ld-reviewer with the sanctioned reviewer credential, read through the OpenBao OIDC path into a 0700 scratch dir with 0600 files and the Bao token self-revoked; no credential touched argv, nothing was printed. I re-measured the claims instead of reading the worker's logs. At this head the new identity suite passes 25/25, the full forgejo suite passes 290/290 across 13 files, and tsc --noEmit exits 0 with empty output. The diff is additive: 265 pre-existing tests pass unmodified, no existing test was edited. CI run 170 on this exact sha is 14/14 success. Scope is exactly the five declared files; forgejo/landing.ts, the blocker suite, docs/forgejo.md and .github are untouched, so the collision with PR #74 stays avoided. The contract was attacked rather than accepted. Precedence is file, then env token, then the shared credential, and a dedicated source that is set but unreadable or empty refuses before any request at all, including the identity read, so it cannot even read back the operator credential it was forbidden to use. When no landing source is configured the shared credential is used but still identity-asserted: with FORGEJO_API_KEY belonging to the operator the merge is refused with zero mutating requests of any verb, and it proceeds only when that key's own /user really is pi-landing. That closes GITS-359 on the path a deployed harness takes without any configuration change. GET /user runs on the exact credential the write carries, immediately before it, with no read in between, and login matching is case- and whitespace-tolerant. The branded proof makes assert-one-write-another structurally impossible because the write takes its credential from the proof and the caller has no credential argument; hand-rolled proofs throw and send nothing. I scanned tool results, error text and every recorded request for all three credential values: none leaks, and no refusal is anything but pre-write. Read reporting is truthful: the merged sha and merged_by come from the PR read-back, the base-branch head is used only as a labelled fallback, an unreadable read-back is reported as unknown with a note, and the string undefined cannot reach the output on any path, including the empty-200 merge answer that produced the original defect. Ordinary reads still use FORGEJO_API_KEY and never touch the landing contract, and the landing lock still runs and refuses first. Observations only, none blocking, recorded in my working note: the tool description says a missing landing credential refuses when the code deliberately falls back to an identity-asserted shared credential, so the wording overstates the guarantee; the shared-credential fallback has no tool-level test, only a unit-level proof; the documented 0600 mode of the token file is never enforced, a 0644 file is accepted; FORGEJO_LANDING_IDENTITY is an unrestricted override, so setting it to aaron reproduces the original attribution for anyone who can already set the API key; MergeAcceptance.accepted is computed and never consumed; and mergePR remains exported unguarded though nothing in the repo calls it. Together these are hardening and surface items. forgejo is dangerous class, so this is the general lens only: a premium second opinion on a different declared model is still required before this lands.
Collaborator

Verdict: APPROVED

model: omniroute/kimi-pool
head: 8ea27b6d0d

Independent PREMIUM review, fresh context. I approve this change: it fully satisfies the HOMELAB-2526 contract and I found no defect at this head. No edits, no merge, no other PR, no formal review submitted. The exact head was re-fetched live from the API before this decision and is unchanged at 8ea27b6d0d, with CI run 170 green 14/14 on that sha. Poster identity was proved before any write: GET /api/v1/user answered login=ld-reviewer using the sanctioned reviewer credential through the OpenBao OIDC path into a 0700 scratch dir with 0600 files, Bao token self-revoked, no credential value in argv or output.

I re-measured rather than trusted the logs: the new merge-identity suite passes 25/25, the full forgejo suite passes 290/290 across 13 files with all 265 pre-existing tests unmodified, and tsc --noEmit exits 0 with empty output, all rerun read-only in the worker worktree. The diff is exactly the five declared files; forgejo/landing.ts, the blocker suite and docs/forgejo.md are untouched, so the PR #74 collision stays avoided.

I verified each contract leg directly against the code and the fake-API tests. Credential precedence is file, then env token, then the shared credential; a dedicated source that is set but unreadable or empty refuses before any request at all, including the identity read, so it never touches the operator credential. With no dedicated source configured the shared credential is used but still identity-asserted, so an operator-owned key cannot land anything and the GITS-359 attribution defect is closed on every path. The identity read is the last read before the write, and the branded proof binds the exact credential to the POST: the write takes its credential from the proof, the caller has no credential argument, hand-rolled proofs throw and send nothing, and no serialised result, refusal reason or recorded request carries a token. Every refusal is pre-write with zero mutations, ordinary reads keep using FORGEJO_API_KEY untouched, and the landing lock still runs and refuses first. Reporting is truthful: the merged sha and merged_by come from the PR read-back with a labelled base-branch fallback, unknown facts are reported as unknown, and the string undefined cannot reach the output on any path, including the empty-200 merge answer that produced the original defect.

Minor hardening suggestions only, none blocking, recorded in my working note: align the set-but-blank env token with the file leg's empty refusal for consistency, consider enforcing or softening the documented 0600 file mode, consider logging a note when the FORGEJO_LANDING_IDENTITY override differs from the pi-landing default, note that MergeAcceptance.accepted is computed but currently unused, and consider a deprecation comment on the still-exported mergePR. These are polish suggestions, my approval stands unchanged.

forgejo is dangerous class: this premium verdict on omniroute/kimi-pool joins the general verdict on omniroute/deepseek-flash, two verdicts on different declared models on the exact head, one premium, with green CI and no open holds or blockers.

## Verdict: APPROVED model: omniroute/kimi-pool head: 8ea27b6d0dfd069b18bd740a93ac115535099af4 Independent PREMIUM review, fresh context. I approve this change: it fully satisfies the HOMELAB-2526 contract and I found no defect at this head. No edits, no merge, no other PR, no formal review submitted. The exact head was re-fetched live from the API before this decision and is unchanged at 8ea27b6d0dfd069b18bd740a93ac115535099af4, with CI run 170 green 14/14 on that sha. Poster identity was proved before any write: GET /api/v1/user answered login=ld-reviewer using the sanctioned reviewer credential through the OpenBao OIDC path into a 0700 scratch dir with 0600 files, Bao token self-revoked, no credential value in argv or output. I re-measured rather than trusted the logs: the new merge-identity suite passes 25/25, the full forgejo suite passes 290/290 across 13 files with all 265 pre-existing tests unmodified, and tsc --noEmit exits 0 with empty output, all rerun read-only in the worker worktree. The diff is exactly the five declared files; forgejo/landing.ts, the blocker suite and docs/forgejo.md are untouched, so the PR #74 collision stays avoided. I verified each contract leg directly against the code and the fake-API tests. Credential precedence is file, then env token, then the shared credential; a dedicated source that is set but unreadable or empty refuses before any request at all, including the identity read, so it never touches the operator credential. With no dedicated source configured the shared credential is used but still identity-asserted, so an operator-owned key cannot land anything and the GITS-359 attribution defect is closed on every path. The identity read is the last read before the write, and the branded proof binds the exact credential to the POST: the write takes its credential from the proof, the caller has no credential argument, hand-rolled proofs throw and send nothing, and no serialised result, refusal reason or recorded request carries a token. Every refusal is pre-write with zero mutations, ordinary reads keep using FORGEJO_API_KEY untouched, and the landing lock still runs and refuses first. Reporting is truthful: the merged sha and merged_by come from the PR read-back with a labelled base-branch fallback, unknown facts are reported as unknown, and the string undefined cannot reach the output on any path, including the empty-200 merge answer that produced the original defect. Minor hardening suggestions only, none blocking, recorded in my working note: align the set-but-blank env token with the file leg's empty refusal for consistency, consider enforcing or softening the documented 0600 file mode, consider logging a note when the FORGEJO_LANDING_IDENTITY override differs from the pi-landing default, note that MergeAcceptance.accepted is computed but currently unused, and consider a deprecation comment on the still-exported mergePR. These are polish suggestions, my approval stands unchanged. forgejo is dangerous class: this premium verdict on omniroute/kimi-pool joins the general verdict on omniroute/deepseek-flash, two verdicts on different declared models on the exact head, one premium, with green CI and no open holds or blockers.
aaron merged commit 0317c29987 into main 2026-11-03 00:32:57 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/pi-extensions!76
No description provided.