HOMELAB-2682: fix(forgejo): refuse expected_identity declaring the operator (scoped) #115

Merged
pi-landing merged 1 commit from plane/HOMELAB-2682-operator-refuse into main 2026-11-07 00:43:50 +00:00
Collaborator

What changed

forgejo_comment_on_pr now refuses a declaration that names the operator.

The rule it enforces (El Director's standing order, quoted in the commit and the
code comment): "NEVER declare expected_identity: aaron — in this session,
every worker, every seat brief, no exception for agents; no writes as Aaron."

assertWriteIdentity already refused a write when nothing was declared and the
token turned out to be the operator's (the branch that landed earlier today). It
still accepted an explicit declaration of aaron — so the one spelling that
says out loud "this is the operator's write" was the one spelling that switched
the guard off. That is now inverted: the refusal happens at resolution, on the
resolved expectation (parameter or FORGEJO_EXPECTED_IDENTITY), before
GET /user, so the call makes zero requests and zero writes.

The refusal message states plainly that declaring the operator's login is
refused and that a different identity must be declared (or none at all, which
keeps the existing no-declaration semantics).

What refused before and posts now — the only case

One case, exactly:

  • Before: forgejo_comment_on_pr with expected_identity: "aaron", or with
    FORGEJO_EXPECTED_IDENTITY=aaron in force, posted the comment as aaron.
    Two tests pinned that as intended behaviour; they were the ones that changed.
  • Now: that call refuses. Aaron, AARON, " aaron " and AaRoN refuse
    identically (the comparison is isOperatorIdentity, already case- and
    space-insensitive).

Nothing else moved:

  • the no-declaration branch is untouched — an undeclared write on the operator's
    token still refuses, and an undeclared write on a non-operator token still posts;
  • a non-operator declaration still wins over the environment and still posts;
  • a declaration that merely mismatches the token is still a mismatch refusal;
  • forgejo_comment_as is untouched — it already refuses the operator by name
    before assertWriteIdentity is reached;
  • no grammar or parse change, no change to any other tool, no new request on any
    path.

Red, then green

Tests first. Red was measured with the source untouched:

  • Red: whole package 5 failed | 1168 passed (1173); the two touched files
    5 failed | 33 passed (38). Every red failure was the measured defect itself:
    3 requests were made (GET /user, POST, read-back) — the comment posted.
    The assertions are at the request layer (h.calls length 0, no GET, no
    mutating verb, nothing recorded as posted), not on the message text alone.
  • Green: whole package 45 files / 1173 tests passed; tsc --noEmit clean
    (exit 0).

The five new/changed tests: param-declared operator refuses with zero requests;
env-declared operator refuses with zero requests; a blank parameter does not
switch the env refusal off; every case/spacing variant refuses; a non-operator
declaration still posts. Plus the two historical pins, rewritten to the new
intended behaviour (one in comment-identity-hardening.test.ts, which keeps its
original purpose — explicit arg beats env — on a non-operator login).

Files

  • forgejo/api.ts — the refusal, one branch in assertWriteIdentity at resolution.
  • forgejo/tests/comment-operator-refuse-2682.test.ts — new section (e); the two
    aaron-posts pins replaced; the file header states the new rule.
  • forgejo/tests/comment-identity-hardening.test.ts — the explicit-wins-over-env
    pin moved to a non-operator login, plus a red test that declaring the operator
    is refused before any request.
  • forgejo/index.ts — the tool description and the expected_identity parameter
    description say the operator's login may not be declared.
  • docs/forgejo.md — one sentence in the comment-write steps.

Untouched

The five historical comments written as aaron (20294, 20295, 20368, 21032,
20685) are untouched: this change adds no request, and nothing here edits, deletes
or resolves a comment. No verdict is asserted or implied here — review is the two
seats' job.

Measured on

The suite's fake Forgejo API (no network). No live write was made while building
this branch.

## What changed `forgejo_comment_on_pr` now refuses a declaration that names the operator. The rule it enforces (El Director's standing order, quoted in the commit and the code comment): **"NEVER declare expected_identity: aaron — in this session, every worker, every seat brief, no exception for agents; no writes as Aaron."** `assertWriteIdentity` already refused a write when *nothing* was declared and the token turned out to be the operator's (the branch that landed earlier today). It still accepted an *explicit* declaration of `aaron` — so the one spelling that says out loud "this is the operator's write" was the one spelling that switched the guard off. That is now inverted: the refusal happens at resolution, on the resolved expectation (parameter or `FORGEJO_EXPECTED_IDENTITY`), before `GET /user`, so the call makes **zero requests and zero writes**. The refusal message states plainly that declaring the operator's login is refused and that a different identity must be declared (or none at all, which keeps the existing no-declaration semantics). ## What refused before and posts now — the only case One case, exactly: - **Before:** `forgejo_comment_on_pr` with `expected_identity: "aaron"`, or with `FORGEJO_EXPECTED_IDENTITY=aaron` in force, posted the comment as `aaron`. Two tests pinned that as intended behaviour; they were the ones that changed. - **Now:** that call refuses. `Aaron`, `AARON`, `" aaron "` and `AaRoN` refuse identically (the comparison is `isOperatorIdentity`, already case- and space-insensitive). Nothing else moved: - the no-declaration branch is untouched — an undeclared write on the operator's token still refuses, and an undeclared write on a non-operator token still posts; - a non-operator declaration still wins over the environment and still posts; - a declaration that merely *mismatches* the token is still a mismatch refusal; - `forgejo_comment_as` is untouched — it already refuses the operator by name before `assertWriteIdentity` is reached; - no grammar or parse change, no change to any other tool, no new request on any path. ## Red, then green Tests first. Red was measured with the source untouched: - **Red:** whole package `5 failed | 1168 passed (1173)`; the two touched files `5 failed | 33 passed (38)`. Every red failure was the measured defect itself: 3 requests were made (`GET /user`, `POST`, read-back) — the comment posted. The assertions are at the request layer (`h.calls` length 0, no `GET`, no mutating verb, nothing recorded as posted), not on the message text alone. - **Green:** whole package `45 files / 1173 tests passed`; `tsc --noEmit` clean (exit 0). The five new/changed tests: param-declared operator refuses with zero requests; env-declared operator refuses with zero requests; a blank parameter does not switch the env refusal off; every case/spacing variant refuses; a non-operator declaration still posts. Plus the two historical pins, rewritten to the new intended behaviour (one in `comment-identity-hardening.test.ts`, which keeps its original purpose — explicit arg beats env — on a non-operator login). ## Files - `forgejo/api.ts` — the refusal, one branch in `assertWriteIdentity` at resolution. - `forgejo/tests/comment-operator-refuse-2682.test.ts` — new section (e); the two aaron-posts pins replaced; the file header states the new rule. - `forgejo/tests/comment-identity-hardening.test.ts` — the explicit-wins-over-env pin moved to a non-operator login, plus a red test that declaring the operator is refused before any request. - `forgejo/index.ts` — the tool description and the `expected_identity` parameter description say the operator's login may not be declared. - `docs/forgejo.md` — one sentence in the comment-write steps. ## Untouched The five historical comments written as `aaron` (20294, 20295, 20368, 21032, 20685) are untouched: this change adds no request, and nothing here edits, deletes or resolves a comment. No verdict is asserted or implied here — review is the two seats' job. ## Measured on The suite's fake Forgejo API (no network). No live write was made while building this branch.
HOMELAB-2682: fix(forgejo): refuse expected_identity declaring the operator (scoped)
All checks were successful
CI / pr-title (pull_request) Successful in 1s
CI / mcp-bridge (pull_request) Successful in 25s
CI / forgejo-ci (pull_request) Successful in 33s
CI / nats-intercom (pull_request) Successful in 36s
CI / pi-friction (pull_request) Successful in 44s
CI / ask-user-question (pull_request) Successful in 48s
tests / python (pull_request) Successful in 9s
CI / skill-args (pull_request) Successful in 50s
CI / pi-otel-telemetry (pull_request) Successful in 1m0s
CI / refocus (pull_request) Successful in 47s
CI / todo (pull_request) Successful in 54s
CI / redact (pull_request) Successful in 33s
CI / plane (pull_request) Successful in 1m31s
CI / forgejo (pull_request) Successful in 1m58s
CI / pi-swarm-suite (pull_request) Successful in 3m21s
7435cbe330
Collaborator

Verdict of record — ld-reviewer — head 7435cbe330: APPROVE

model: omniroute/deepseek-flash
session: 01a1b368-9704-70d7-89c1-cbeff3e60fcd

Blockers

None.

Change surface — is anything changed beyond the operator-declaration leg?

No. git diff 96437d4a..7435cbe3 is 5 files, +158/−27, and every hunk belongs to the
one leg:

  • forgejo/api.ts — one if (expected && isOperatorIdentity(expected)) { return {ok:false,…} }
    branch inside assertWriteIdentity, plus two prose lines on its doc comment and the
    branch's own comment. No other function in the file is touched.
  • forgejo/index.ts — two DESCRIPTION strings only (forgejo_comment_on_pr description
    and its expected_identity parameter description). No code.
  • docs/forgejo.md — one added sentence in the comment-write steps.
  • the two test files — five new/changed cases; the two historical aaron-posts pins
    replaced.

forgejo/comment-as.ts, verdict.ts, landing.ts are byte-identical base↔head
(sha256 43805c03…, 84546148…, 1f7c6a03…), so forgejo_comment_as, the verdict
helper and the landing lock are unchanged by construction, not by assertion. The no-declaration
branch, the mismatch branch, the unreadable-identity branch and the reviewer-verdict branch
are all untouched and re-pinned by tests that still pass.

Refusal ordering (measured in the head source, not inferred): the operator-declaration
return is at forgejo/api.ts:437; the first request on the whole path,
await getAuthenticatedUser(cfg), is at forgejo/api.ts:453. Nothing can be sent
before the refusal.

Differential — deployed (= base 96437d4a) → head 7435cbe3

Eighteen cases run against BOTH trees with the same fake-API harness
(tests/fake.ts mockFetch, which records every fetch before routing, so the request
count is requests actually attempted). One JSON row per case, produced twice and diffed.

# case base 96437d4a head 7435cbe3 requests base→head class
C01 no declaration, token = operator REFUSES REFUSES 1→1 same
C02 no declaration, token = worker PERMITS PERMITS 3→3 same
C03 no declaration + verdict body, token = worker REFUSES REFUSES 1→1 same
C04 no declaration + verdict body, token = operator REFUSES REFUSES 1→1 same
C05 declared non-operator, token matches PERMITS PERMITS 3→3 same
C06 declared non-operator, token = operator (mismatch) REFUSES REFUSES 1→1 same
C07 expected_identity: aaron, token = operator PERMITS REFUSES 3→0 PERMITS→REFUSES
C08 expected_identity: aaron, token = worker REFUSES REFUSES 1→0 same (refusal source: mismatch → operator declaration)
C09 expected_identity: Aaron, token = operator PERMITS REFUSES 3→0 PERMITS→REFUSES
C10 expected_identity: " aaron ", token = operator PERMITS REFUSES 3→0 PERMITS→REFUSES
C11 FORGEJO_EXPECTED_IDENTITY=aaron, no param PERMITS REFUSES 3→0 PERMITS→REFUSES
C12 FORGEJO_EXPECTED_IDENTITY=aaron, blank param PERMITS REFUSES 3→0 PERMITS→REFUSES
C13 FORGEJO_EXPECTED_IDENTITY=ld-reviewer, no param PERMITS PERMITS 3→3 same
C14 env non-operator, param other non-operator (explicit wins) PERMITS PERMITS 3→3 same
C15 env = operator, param non-operator (explicit wins) PERMITS PERMITS 3→3 same
C16 expected_identity: aaron + verdict body, token = operator PERMITS REFUSES 3→0 PERMITS→REFUSES
C17 blank param, no env, token = worker PERMITS PERMITS 3→3 same
C18 env = operator, GET /user unreadable REFUSES REFUSES 1→0 same (refusal source: unreadable → operator declaration)

Counts: 12 same · 6 PERMITS→REFUSES · 0 REFUSES→PERMITS. Every permitted→refused row
declares the operator (parameter or environment, incl. case/space variants, a blank
parameter falling through to an operator env, and a verdict body); no other row changes
verdict, and the two rows whose reason changed (C08, C18) still refuse. comment_as
is not in the matrix because its file is byte-identical: its own suite is 28/28 in both
trees, and its step-2 guard (comment-as.ts:564, isOperatorIdentity(declared)) refuses
an operator identity before the assertWriteIdentity call at comment-as.ts:644 is
reached — the new branch is unreachable there, so its behaviour cannot have moved.

My own suite results

Run by me in /tmp/rv2682/{base,head}, clones of Homelab/pi-extensions at exactly
96437d4a and 7435cbe3, node_modules symlinked from the mgmt checkout, tests offline:

  • head, the two touched files: 2 files passed · 38 tests passed.
  • head, whole package: 45 files passed · 1173 tests passed; tsc --noEmit exit 0.
  • base (i.e. deployed), whole package: 45 files · 1169 tests passed.
  • Red reproduced independently: head's two touched test files copied onto the BASE
    source, source untouched → 2 files failed · 5 failed | 33 passed (38), and all five
    failures are the section-(e) operator-declaration cases.
  • comment-identity-as.test.ts: 28/28 at base and 28/28 at head.
  • CI at the head sha: runs #394 (all jobs success) and #395 success.

Exposure — what I personally read and ran

Read in full: the base..head diff of all five files; the whole assertWriteIdentity at
head (api.ts:290-520); both call sites (index.ts:1156, comment-as.ts:644) and the
comment-as.ts:564 operator guard; tests/fake.ts mockFetch; the diff of both test
files. Ran: the suites and the 18-case differential listed above, all offline.

Deployed-drop provenance is proven by hash, not assumed: ~/.pi/agent/extensions/forgejo
is a source drop with no node_modules; its api.ts, index.ts, comment-as.ts,
verdict.ts, landing.ts sha256 values equal those files at base 96437d4a and are NOT
equal to head (api.ts = 118b7504… at deployed AND at base, 50c67355… at head). So
the table above is measured deployed-behaviour → head-behaviour.

I made no write with FORGEJO_API_KEY, no expected_identity declaring the operator
anywhere, and exactly one write in this review: this verdict comment.

Non-blocking observations (not blockers, post-merge tidy)

  • O1 forgejo/api.ts:297-299 — prose left in the file this PR edits still says "Where the
    identity is DECLARED the operator is a legitimate login". On the comment-write path the
    new branch makes that false, and the new branch's own comment ~140 lines below says the
    opposite. Doc drift inside the touched file.
  • O2 forgejo/tests/comment-operator-refuse-2682.test.ts:25-27 — the file header still
    says "Zero requests" here means zero WRITES. One authenticated GET /user is unavoidable, while section (e) in the same file - and the docs sentence this PR adds -
    now prove zero requests. Stale claim in a file this PR changes.
Verdict of record — ld-reviewer — head 7435cbe3308117e1ff13fd61109e76a4e8ddaa9f: APPROVE model: omniroute/deepseek-flash session: 01a1b368-9704-70d7-89c1-cbeff3e60fcd ## Blockers None. ## Change surface — is anything changed beyond the operator-declaration leg? No. `git diff 96437d4a..7435cbe3` is 5 files, +158/−27, and every hunk belongs to the one leg: - `forgejo/api.ts` — one `if (expected && isOperatorIdentity(expected)) { return {ok:false,…} }` branch inside `assertWriteIdentity`, plus two prose lines on its doc comment and the branch's own comment. No other function in the file is touched. - `forgejo/index.ts` — two DESCRIPTION strings only (`forgejo_comment_on_pr` description and its `expected_identity` parameter description). No code. - `docs/forgejo.md` — one added sentence in the comment-write steps. - the two test files — five new/changed cases; the two historical `aaron`-posts pins replaced. `forgejo/comment-as.ts`, `verdict.ts`, `landing.ts` are byte-identical base↔head (sha256 `43805c03…`, `84546148…`, `1f7c6a03…`), so `forgejo_comment_as`, the verdict helper and the landing lock are unchanged by construction, not by assertion. The no-declaration branch, the mismatch branch, the unreadable-identity branch and the reviewer-verdict branch are all untouched and re-pinned by tests that still pass. Refusal ordering (measured in the head source, not inferred): the operator-declaration `return` is at `forgejo/api.ts:437`; the first request on the whole path, `await getAuthenticatedUser(cfg)`, is at `forgejo/api.ts:453`. Nothing can be sent before the refusal. ## Differential — deployed (= base 96437d4a) → head 7435cbe3 Eighteen cases run against BOTH trees with the same fake-API harness (`tests/fake.ts` `mockFetch`, which records every `fetch` before routing, so the request count is requests actually attempted). One JSON row per case, produced twice and diffed. | # | case | base 96437d4a | head 7435cbe3 | requests base→head | class | |---|---|---|---|---|---| | C01 | no declaration, token = operator | REFUSES | REFUSES | 1→1 | same | | C02 | no declaration, token = worker | PERMITS | PERMITS | 3→3 | same | | C03 | no declaration + verdict body, token = worker | REFUSES | REFUSES | 1→1 | same | | C04 | no declaration + verdict body, token = operator | REFUSES | REFUSES | 1→1 | same | | C05 | declared non-operator, token matches | PERMITS | PERMITS | 3→3 | same | | C06 | declared non-operator, token = operator (mismatch) | REFUSES | REFUSES | 1→1 | same | | C07 | `expected_identity: aaron`, token = operator | PERMITS | REFUSES | 3→0 | PERMITS→REFUSES | | C08 | `expected_identity: aaron`, token = worker | REFUSES | REFUSES | 1→0 | same (refusal source: mismatch → operator declaration) | | C09 | `expected_identity: Aaron`, token = operator | PERMITS | REFUSES | 3→0 | PERMITS→REFUSES | | C10 | `expected_identity: " aaron "`, token = operator | PERMITS | REFUSES | 3→0 | PERMITS→REFUSES | | C11 | `FORGEJO_EXPECTED_IDENTITY=aaron`, no param | PERMITS | REFUSES | 3→0 | PERMITS→REFUSES | | C12 | `FORGEJO_EXPECTED_IDENTITY=aaron`, blank param | PERMITS | REFUSES | 3→0 | PERMITS→REFUSES | | C13 | `FORGEJO_EXPECTED_IDENTITY=ld-reviewer`, no param | PERMITS | PERMITS | 3→3 | same | | C14 | env non-operator, param other non-operator (explicit wins) | PERMITS | PERMITS | 3→3 | same | | C15 | env = operator, param non-operator (explicit wins) | PERMITS | PERMITS | 3→3 | same | | C16 | `expected_identity: aaron` + verdict body, token = operator | PERMITS | REFUSES | 3→0 | PERMITS→REFUSES | | C17 | blank param, no env, token = worker | PERMITS | PERMITS | 3→3 | same | | C18 | env = operator, `GET /user` unreadable | REFUSES | REFUSES | 1→0 | same (refusal source: unreadable → operator declaration) | Counts: **12 same · 6 PERMITS→REFUSES · 0 REFUSES→PERMITS.** Every permitted→refused row declares the operator (parameter or environment, incl. case/space variants, a blank parameter falling through to an operator env, and a verdict body); no other row changes verdict, and the two rows whose *reason* changed (C08, C18) still refuse. `comment_as` is not in the matrix because its file is byte-identical: its own suite is 28/28 in both trees, and its step-2 guard (`comment-as.ts:564`, `isOperatorIdentity(declared)`) refuses an operator `identity` before the `assertWriteIdentity` call at `comment-as.ts:644` is reached — the new branch is unreachable there, so its behaviour cannot have moved. ## My own suite results Run by me in `/tmp/rv2682/{base,head}`, clones of `Homelab/pi-extensions` at exactly 96437d4a and 7435cbe3, `node_modules` symlinked from the mgmt checkout, tests offline: - head, the two touched files: `2 files passed · 38 tests passed`. - head, whole package: `45 files passed · 1173 tests passed`; `tsc --noEmit` exit 0. - base (i.e. deployed), whole package: `45 files · 1169 tests passed`. - Red reproduced independently: head's two touched test files copied onto the BASE source, source untouched → `2 files failed · 5 failed | 33 passed (38)`, and all five failures are the section-(e) operator-declaration cases. - `comment-identity-as.test.ts`: 28/28 at base and 28/28 at head. - CI at the head sha: runs #394 (all jobs success) and #395 success. ## Exposure — what I personally read and ran Read in full: the base..head diff of all five files; the whole `assertWriteIdentity` at head (`api.ts:290-520`); both call sites (`index.ts:1156`, `comment-as.ts:644`) and the `comment-as.ts:564` operator guard; `tests/fake.ts` `mockFetch`; the diff of both test files. Ran: the suites and the 18-case differential listed above, all offline. Deployed-drop provenance is proven by hash, not assumed: `~/.pi/agent/extensions/forgejo` is a source drop with no `node_modules`; its `api.ts`, `index.ts`, `comment-as.ts`, `verdict.ts`, `landing.ts` sha256 values equal those files at base 96437d4a and are NOT equal to head (`api.ts` = `118b7504…` at deployed AND at base, `50c67355…` at head). So the table above is measured deployed-behaviour → head-behaviour. I made no write with `FORGEJO_API_KEY`, no `expected_identity` declaring the operator anywhere, and exactly one write in this review: this verdict comment. ## Non-blocking observations (not blockers, post-merge tidy) - O1 `forgejo/api.ts:297-299` — prose left in the file this PR edits still says "Where the identity is DECLARED the operator is a legitimate login". On the comment-write path the new branch makes that false, and the new branch's own comment ~140 lines below says the opposite. Doc drift inside the touched file. - O2 `forgejo/tests/comment-operator-refuse-2682.test.ts:25-27` — the file header still says `"Zero requests" here means zero WRITES. One authenticated GET /user is unavoidable`, while section (e) in the same file - and the docs sentence this PR adds - now prove zero requests. Stale claim in a file this PR changes.
Collaborator

Verdict of record — ld-reviewer — head 7435cbe330: APPROVE

model: omniroute/deepseek-flash
session: 01a1b36e-4bdb-70d7-89c1-cbf5738dc9d8

Blockers

None.

Scope measured (not read from the description)

git diff 96437d4a 7435cbe3 --stat = 5 files, 158 insertions, 27 deletions, and the
served .diff applies cleanly to a 96437d4a export producing a tree byte-identical to
the 7435cbe3 export:

docs/forgejo.md                                    |   5 +-
forgejo/api.ts                                     |  32 ++++++
forgejo/index.ts                                   |   6 +-
forgejo/tests/comment-identity-hardening.test.ts   |  28 ++++-
forgejo/tests/comment-operator-refuse-2682.test.ts | 114 +++++++++++++++++----

The only functional change is one branch at the top of assertWriteIdentity
(api.ts:437) plus its comments. resolveExpectedIdentity (363-371),
expectedIdentitySource (374-380), isOperatorIdentity (304-307), sameLogin, the
no-declaration operator branch (486), the verdict-without-declaration branch (465) and
the mismatch branch (505) are all unchanged; comment-as.ts, landing.ts,
verdict.ts, verdict-cli.ts, types.ts and package.json hash identically at base
and head. assertWriteIdentity has exactly two callers in the tree (index.ts:1156,
comment-as.ts:644) and both are measured below.

Differential table — base 96437d4a vs head 7435cbe3

Same driver file, both trees, real registered tools, request-layer fake fetch
(h.calls counts HTTP calls; request counts are per call).

case base 96437d4a head 7435cbe3 delta
C1 declared non-operator (param) on worker token posted, 3 req / 2 GET / 1 write, author pi-worker same unchanged
C2 declared operator (param) on operator token posted, 3/2/1, author aaron REFUSED, 0/0/0 permits→refuses
C3 env-only operator declaration on operator token posted, 3/2/1, author aaron REFUSED, 0/0/0 permits→refuses
C4 blank param + operator env posted, 3/2/1, author aaron (src env) REFUSED, 0/0/0 (src env) permits→refuses
C4b blank param + non-operator env (fall-through) posted, 3/2/1, author pi-worker same unchanged
C5 no declaration, operator token REFUSED, 1/1/0, actual aaron same unchanged
C5b no declaration, worker token posted, 3/2/1, author pi-worker same unchanged
C6 mismatched declaration (pi-worker vs operator token) REFUSED, 1/1/0 same unchanged
C6b env non-operator + param operator (explicit wins) posted, 3/2/1, author aaron (src param) REFUSED, 0/0/0 (src param) permits→refuses
C6c operator env + param operator posted, 3/2/1, author aaron (src param) REFUSED, 0/0/0 (src param) permits→refuses
C7 comment_as identity=aaron REFUSED, 0/0/0 same unchanged
C7b comment_as identity=ld-reviewer (staged seat token) posted, 3/2/1, author ld-reviewer same unchanged
C7c comment_as ld-reviewer with expected_identity=aaron REFUSED, 0/0/0 same unchanged

Counts: 13 cases · 8 unchanged · 5 changed · permits→refuses 5 · refuses→permits 0.
Every changed row is a call that DECLARES the operator's login; no declaration, no
non-operator declaration, no mismatch refusal and no comment_as path moved.

Ordering of the refusal

In assertWriteIdentity the new branch returns at api.ts:437 before the first network
call in the function, getAuthenticatedUser (api.ts:456 → GET /user). The comment
tool's only work before that call is parseRepoRef (index.ts:232, pure) and
loadConfig (api.ts:109, pure env read) — measured: every refusal row above made
0 requests, not merely 0 writes.

The zero-request assertions are at the request layer, not a mocked refusal: the suite
stubs global fetch with mockFetch (tests/fake.ts:76) and asserts h.calls,
callsOf(h.calls,"GET"), writes(h.calls) and h.posted are empty. Nothing stubs
assertWriteIdentity or the refusal message.

Suite result (my run, head 7435cbe3)

  • Touched files: tests/comment-operator-refuse-2682.test.ts + comment-identity-hardening.test.ts → 2 files, 38 tests passed.
  • Red reproduced by me: head test files against the 96437d4a source → 5 failed | 33 passed (38), failures showing expected 0, received 3 requests — the defect itself, not a message mismatch.
  • Whole package at head: 45 files, 1173 tests passed.
  • tsc --noEmit: exit 0.
  • CI on this head: run #394 (full matrix) and #395 pass.

Exposure

Read: full served .diff and git diff 96437d4a..7435cbe3; api.ts around
isOperatorIdentity/resolveExpectedIdentity/assertWriteIdentity; the comment and
comment_as tool handlers; both touched test files and tests/fake.ts. Ran: the two
touched files and the whole package at head; the head tests against base source; the
13-case matrix on both trees; tsc --noEmit. Deployed-drop provenance: the mgmt drop at
~/.pi/agent/extensions/forgejo is sha256-identical to the 96437d4a export for all 8
code files (api.ts, comment-as.ts, index.ts, landing.ts, types.ts, verdict.ts,
verdict-cli.ts, package.json), so the differential baseline IS the deployed lock. No
live write was made; the matrix ran only against the fake API.

Verdict of record — ld-reviewer — head 7435cbe3308117e1ff13fd61109e76a4e8ddaa9f: APPROVE model: omniroute/deepseek-flash session: 01a1b36e-4bdb-70d7-89c1-cbf5738dc9d8 ## Blockers None. ## Scope measured (not read from the description) `git diff 96437d4a 7435cbe3 --stat` = 5 files, 158 insertions, 27 deletions, and the served `.diff` applies cleanly to a 96437d4a export producing a tree byte-identical to the 7435cbe3 export: docs/forgejo.md | 5 +- forgejo/api.ts | 32 ++++++ forgejo/index.ts | 6 +- forgejo/tests/comment-identity-hardening.test.ts | 28 ++++- forgejo/tests/comment-operator-refuse-2682.test.ts | 114 +++++++++++++++++---- The only functional change is one branch at the top of `assertWriteIdentity` (api.ts:437) plus its comments. `resolveExpectedIdentity` (363-371), `expectedIdentitySource` (374-380), `isOperatorIdentity` (304-307), `sameLogin`, the no-declaration operator branch (486), the verdict-without-declaration branch (465) and the mismatch branch (505) are all unchanged; `comment-as.ts`, `landing.ts`, `verdict.ts`, `verdict-cli.ts`, `types.ts` and `package.json` hash identically at base and head. `assertWriteIdentity` has exactly two callers in the tree (index.ts:1156, comment-as.ts:644) and both are measured below. ## Differential table — base 96437d4a vs head 7435cbe3 Same driver file, both trees, real registered tools, request-layer fake `fetch` (`h.calls` counts HTTP calls; request counts are per call). | case | base 96437d4a | head 7435cbe3 | delta | |---|---|---|---| | C1 declared non-operator (param) on worker token | posted, 3 req / 2 GET / 1 write, author pi-worker | same | unchanged | | C2 declared operator (param) on operator token | posted, 3/2/1, author aaron | REFUSED, 0/0/0 | **permits→refuses** | | C3 env-only operator declaration on operator token | posted, 3/2/1, author aaron | REFUSED, 0/0/0 | **permits→refuses** | | C4 blank param + operator env | posted, 3/2/1, author aaron (src env) | REFUSED, 0/0/0 (src env) | **permits→refuses** | | C4b blank param + non-operator env (fall-through) | posted, 3/2/1, author pi-worker | same | unchanged | | C5 no declaration, operator token | REFUSED, 1/1/0, actual aaron | same | unchanged | | C5b no declaration, worker token | posted, 3/2/1, author pi-worker | same | unchanged | | C6 mismatched declaration (pi-worker vs operator token) | REFUSED, 1/1/0 | same | unchanged | | C6b env non-operator + param operator (explicit wins) | posted, 3/2/1, author aaron (src param) | REFUSED, 0/0/0 (src param) | **permits→refuses** | | C6c operator env + param operator | posted, 3/2/1, author aaron (src param) | REFUSED, 0/0/0 (src param) | **permits→refuses** | | C7 comment_as identity=aaron | REFUSED, 0/0/0 | same | unchanged | | C7b comment_as identity=ld-reviewer (staged seat token) | posted, 3/2/1, author ld-reviewer | same | unchanged | | C7c comment_as ld-reviewer with expected_identity=aaron | REFUSED, 0/0/0 | same | unchanged | Counts: **13 cases · 8 unchanged · 5 changed · permits→refuses 5 · refuses→permits 0.** Every changed row is a call that DECLARES the operator's login; no declaration, no non-operator declaration, no mismatch refusal and no `comment_as` path moved. ## Ordering of the refusal In `assertWriteIdentity` the new branch returns at api.ts:437 before the first network call in the function, `getAuthenticatedUser` (api.ts:456 → `GET /user`). The comment tool's only work before that call is `parseRepoRef` (index.ts:232, pure) and `loadConfig` (api.ts:109, pure env read) — measured: every refusal row above made **0 requests**, not merely 0 writes. The zero-request assertions are at the request layer, not a mocked refusal: the suite stubs global `fetch` with `mockFetch` (`tests/fake.ts:76`) and asserts `h.calls`, `callsOf(h.calls,"GET")`, `writes(h.calls)` and `h.posted` are empty. Nothing stubs `assertWriteIdentity` or the refusal message. ## Suite result (my run, head 7435cbe3) - Touched files: `tests/comment-operator-refuse-2682.test.ts` + `comment-identity-hardening.test.ts` → **2 files, 38 tests passed**. - Red reproduced by me: head test files against the 96437d4a source → **5 failed | 33 passed (38)**, failures showing `expected 0, received 3` requests — the defect itself, not a message mismatch. - Whole package at head: **45 files, 1173 tests passed**. - `tsc --noEmit`: exit 0. - CI on this head: run #394 (full matrix) and #395 pass. ## Exposure Read: full served `.diff` and `git diff 96437d4a..7435cbe3`; `api.ts` around `isOperatorIdentity`/`resolveExpectedIdentity`/`assertWriteIdentity`; the comment and `comment_as` tool handlers; both touched test files and `tests/fake.ts`. Ran: the two touched files and the whole package at head; the head tests against base source; the 13-case matrix on both trees; `tsc --noEmit`. Deployed-drop provenance: the mgmt drop at `~/.pi/agent/extensions/forgejo` is sha256-identical to the 96437d4a export for all 8 code files (api.ts, comment-as.ts, index.ts, landing.ts, types.ts, verdict.ts, verdict-cli.ts, package.json), so the differential baseline IS the deployed lock. No live write was made; the matrix ran only against the fake API.
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!115
No description provided.