HOMELAB-1679: fix(forgejo): pin FORGEJO_BASE_URL and FORGEJO_LANDING_IDENTITY on GO-gated repos (LR-20261111-017, homelab-93 F5) #132

Merged
pi-landing merged 8 commits from lr-20261111-017-f5 into main 2026-11-12 02:36:41 +00:00
Collaborator

Spec 017 follow-up, F5 FIRST per El Director, from homelab-93's #131 review. Small and surgical; gate-changing: two pane seats (via forgejo_comment_as as ld-reviewer) + homelab-93's review + SEATSID before El Director's go.

The rule (N6's exact pattern, the two levers 93 called the largest remaining)

On the two repos named in code (GO_GATED_REPOS), keyed off the SERVER's pr.base.repo.full_name like every other gate choice:

  • FORGEJO_BASE_URL (and verdict-cli's --base-url) is ignored: every read, the GET /user identity check and the merge POST go to the built-in default base. 93's words: a local proxy could fabricate approvals, hide holds, and relay the merge with the real landing token — this was the largest remaining lever.
  • FORGEJO_LANDING_IDENTITY is ignored: the merge credential must answer GET /user as the default landing login; the server whitelist remains the outer boundary.
  • Each presence is logged once per call, by name only (never the value — it can carry credentials), through the same reportGatedIgnored helper #131 extracted (no duplicate). Non-gated repos unchanged (both levers honored as today).

Proven

  • New tests/landing-gated-transport-pins-f5.test.ts: 6 red / 3 green-controls on the old code, 9/9 green after — identity demanded bogus-login-x and the merge POST dialled the stub origin on the old code; pinned after. assertMergeIdentity demands the pinned login AND dials the pinned base; merge tool, census and --base-url all pin once per call, before the first read.
  • vitest run: 58 files / 1287 pass; the 1 failure is the known pre-existing merge-identity ambient-token case (identical with the change stashed). tsc --noEmit clean.
  • no independent walk: implementer-run checks only.

Lab-Run: LR-20261111-017

r2 (stacked on 0ceb0ff): the gate is asked on the built-in forge, and the pin keys on the SERVER's name

SeatA's hold (24838) was right: the pin keyed on repoKey(repo_ref) — a name the same session writes — so repo_ref="x/Homelab" kept the whole call on a proxy origin, with the staged landing token on the merge POST. Fixed: the one read whose answer names the repo (getPR / verdict-cli's PR fetch) ALWAYS goes to the BUILT-IN base (gateProbeConfig); gating and the transport pin key off pr.base.repo.full_name from that answer, fail-closed on an absent name; every later read and the write reuse that transport; the caller-keyed pre-pin is gone; --base-url is honored only for reads after the first AND only once the server proves the repo non-gated. Red→green: seatA's merge probe would merge at the proxy on the old code (5 of 7 new tests red); the two server-named-gated guards pass on both — caller-keyed gating happened to cover those spellings, which is why the bogus-owner probe mattered. 1294 tests green; F5's 9 intact (its verdict-cli control now reads "flag honored for reads after the first"). tsc clean.

HEAD 69d9993e45; patches: piext-f5-diff.patch (full, 1536 lines) + piext-132-interdiff-r2.patch (from 0ceb0ff, 865 lines).

r3 (stacked on 69d9993): the census leg — seatA's r2 hold closed

The census's first read per repo now goes through gateProbeConfig (built-in base); gating and the transport pin key off the SERVER's pr.base.repo.full_name per row; gated/unreadable rows never leave the built-in base; non-gated rows keep FORGEJO_BASE_URL for later reads. "No read leaves the default forge before the server's name proves non-gated" is now stated and true for merge, census and verdict-cli. New tests/landing-census-server-keyed-f5b-r3.test.ts (3 tests): seatA's census probe (bogus owner + proxy: the first read dials the default origin, the proxy sees nothing — 2 of 3 red on 69d9993), gated-by-server-name census, non-gated control. 1297 tests green; tsc clean.

HEAD e29e447e3d; patches: piext-f5-diff.patch (full, 1792 lines) + piext-132-interdiff-r3.patch (from 69d9993, 378 lines).

B1+S2 round (stacked on e29e447, commit 561c545): review-3c's blocking fix + S2

  • B1: assertMergeIdentity builds the credential-carrying config with baseUrl: DEFAULT_BASE_URL — the constant, on EVERY repo — so the identity GET /user and the merge POST never leave the built-in origin, even with FORGEJO_BASE_URL set and the repo non-gated. Only read-only traffic on a proven non-gated repo keeps the env base. Controls flipped; new relay tests pin zero credential-bearing requests to a non-default origin (11 red → green without the fix).
  • S2: on gated landings, before any further request: refuse NODE_TLS_REJECT_UNAUTHORIZED=0, NODE_EXTRA_CA_CERTS, and NODE_OPTIONS carrying --require/--use-openssl-ca/--use-bundled-ca/--use-env-proxy — one line each naming the variable.
  • seat-post.ts/comment-as.ts untouched (3c's S1 there is the separate follow-up PR before A14(b)). 1307 tests green; tsc clean.

HEAD 561c545121; patches: piext-f5-diff.patch (full, 2408) + piext-132-interdiff-b1s2.patch (from e29e447, 813) + earlier interdiffs.

Default-deny round (stacked on 6835d30, commit a6d13fb): NODE_OPTIONS inverted

On a gated landing, NODE_OPTIONS is now refused unless EVERY flag is on a five-flag benign whitelist (--max-old-space-size, --max-semi-space-size, --stack-size, --trace-warnings, --no-warnings — memory/tuning and stderr diagnostics only, each justified in comment). GATED_NODE_OPTION_FLAGS is deleted: unknown future loader/TLS flags refuse by default, ending the alias hunt that produced four flag rounds. Flags parse whole and exact (split on whitespace, up to =), so a near-name like --max-old-space-sizex refuses instead of widening.

Which of 3c's S3 holes this closes — and which remain OPEN (honest list)

  • CLOSED: --loader, --experimental-loader, --import, --use-openssl-ca, --use-bundled-ca, --use-system-ca, --openssl-config, --inspect, and any UNKNOWN long flag — none are whitelisted, so each refuses by name.
  • OPEN (named for the next round, with the exact bypass): -r — the short --require alias does not start with --, so nodeOptionFlagNames drops it before the check; quoted flags — a token like "--require carries a leading quote, fails the startsWith("--") filter, and is dropped (Node strips quotes per its own rules; the parser must too); NODE_USE_ENV_PROXY — an env var, not a NODE_OPTIONS flag, and not checked anywhere in this round (on the local Node v22 it does not affect fetch, but Node versions that honor it would route the pinned origin through a proxy); process.execArgv — NOT checked: command-line flags (node --require x tool.ts) bypass NODE_OPTIONS entirely, so a gated call started with loader flags on the argv is not refused by this layer.

HEAD a6d13fb475; interdiff: piext-132-interdiff-dd.patch (from 6835d30, 369 lines).

r5 round (stacked on a6d13fb, commit 3462f12): review-3c's one-shape fix

review-3c measured on Node 22 that -r x, "--require" x, "-r" x, --max-old-space-size=1024 -r x, --no-warnings "--require" x and -C x all passed the default-deny and loaded a hook (one replaced fetch and intercepted the pinned origin). The check now refuses unless EVERY whitespace-separated token, after Node-style quote stripping, matches exactly ^--(max-old-space-size|max-semi-space-size)=\d+$|^--(trace-warnings|no-warnings)$. The refusal names the flag, never its value. The dead --stack-size entry is dropped (Node does not accept it in NODE_OPTIONS; the claim was false) and its comment fixed. Red→green: 2 of the new cases fail on a6d13fb's code. process.execArgv and NODE_USE_ENV_PROXY remain named follow-ups.

HEAD 3462f12e82; patches: piext-f5-diff.patch (full, 2617) + piext-132-interdiff-r5.patch (from a6d13fb, 231) + earlier interdiffs.

Spec 017 follow-up, **F5 FIRST** per El Director, from homelab-93's #131 review. Small and surgical; gate-changing: two pane seats (via `forgejo_comment_as` as ld-reviewer) + homelab-93's review + SEATSID before El Director's go. ## The rule (N6's exact pattern, the two levers 93 called the largest remaining) On the two repos named in code (`GO_GATED_REPOS`), keyed off the SERVER's `pr.base.repo.full_name` like every other gate choice: - **`FORGEJO_BASE_URL`** (and verdict-cli's `--base-url`) is **ignored**: every read, the `GET /user` identity check and the merge POST go to the built-in default base. 93's words: a local proxy could fabricate approvals, hide holds, and relay the merge with the real landing token — this was the largest remaining lever. - **`FORGEJO_LANDING_IDENTITY`** is **ignored**: the merge credential must answer `GET /user` as the default landing login; the server whitelist remains the outer boundary. - Each presence is **logged once per call, by name only** (never the value — it can carry credentials), through the same `reportGatedIgnored` helper #131 extracted (no duplicate). Non-gated repos unchanged (both levers honored as today). ## Proven - New `tests/landing-gated-transport-pins-f5.test.ts`: 6 red / 3 green-controls on the old code, 9/9 green after — identity demanded `bogus-login-x` and the merge POST dialled the stub origin on the old code; pinned after. `assertMergeIdentity` demands the pinned login AND dials the pinned base; merge tool, census and `--base-url` all pin once per call, before the first read. - `vitest run`: 58 files / 1287 pass; the 1 failure is the known pre-existing `merge-identity` ambient-token case (identical with the change stashed). `tsc --noEmit` clean. - **no independent walk: implementer-run checks only.** Lab-Run: LR-20261111-017 ## r2 (stacked on 0ceb0ff): the gate is asked on the built-in forge, and the pin keys on the SERVER's name SeatA's hold (24838) was right: the pin keyed on repoKey(repo_ref) — a name the same session writes — so `repo_ref="x/Homelab"` kept the whole call on a proxy origin, with the staged landing token on the merge POST. Fixed: the one read whose answer names the repo (getPR / verdict-cli's PR fetch) ALWAYS goes to the BUILT-IN base (`gateProbeConfig`); gating and the transport pin key off `pr.base.repo.full_name` from that answer, fail-closed on an absent name; every later read and the write reuse that transport; the caller-keyed pre-pin is gone; `--base-url` is honored only for reads after the first AND only once the server proves the repo non-gated. Red→green: seatA's merge probe would merge at the proxy on the old code (5 of 7 new tests red); the two server-named-gated guards pass on both — caller-keyed gating happened to cover those spellings, which is why the bogus-owner probe mattered. 1294 tests green; F5's 9 intact (its verdict-cli control now reads "flag honored for reads after the first"). tsc clean. HEAD 69d9993e45faafc20a6c585e802333da84f2b7e2; patches: piext-f5-diff.patch (full, 1536 lines) + piext-132-interdiff-r2.patch (from 0ceb0ff, 865 lines). ## r3 (stacked on 69d9993): the census leg — seatA's r2 hold closed The census's first read per repo now goes through `gateProbeConfig` (built-in base); gating and the transport pin key off the SERVER's `pr.base.repo.full_name` per row; gated/unreadable rows never leave the built-in base; non-gated rows keep `FORGEJO_BASE_URL` for later reads. "No read leaves the default forge before the server's name proves non-gated" is now stated and true for merge, census and verdict-cli. New `tests/landing-census-server-keyed-f5b-r3.test.ts` (3 tests): seatA's census probe (bogus owner + proxy: the first read dials the default origin, the proxy sees nothing — 2 of 3 red on 69d9993), gated-by-server-name census, non-gated control. 1297 tests green; tsc clean. HEAD e29e447e3dddb6c388bc571f54cbb29328d9d652; patches: piext-f5-diff.patch (full, 1792 lines) + piext-132-interdiff-r3.patch (from 69d9993, 378 lines). ## B1+S2 round (stacked on e29e447, commit 561c545): review-3c's blocking fix + S2 - **B1**: `assertMergeIdentity` builds the credential-carrying config with `baseUrl: DEFAULT_BASE_URL` — the constant, on EVERY repo — so the identity GET /user and the merge POST never leave the built-in origin, even with FORGEJO_BASE_URL set and the repo non-gated. Only read-only traffic on a proven non-gated repo keeps the env base. Controls flipped; new relay tests pin zero credential-bearing requests to a non-default origin (11 red → green without the fix). - **S2**: on gated landings, before any further request: refuse NODE_TLS_REJECT_UNAUTHORIZED=0, NODE_EXTRA_CA_CERTS, and NODE_OPTIONS carrying --require/--use-openssl-ca/--use-bundled-ca/--use-env-proxy — one line each naming the variable. - seat-post.ts/comment-as.ts untouched (3c's S1 there is the separate follow-up PR before A14(b)). 1307 tests green; tsc clean. HEAD 561c54512147f1da03ffe0e1f0ebaf697a0a5370; patches: piext-f5-diff.patch (full, 2408) + piext-132-interdiff-b1s2.patch (from e29e447, 813) + earlier interdiffs. ## Default-deny round (stacked on 6835d30, commit a6d13fb): NODE_OPTIONS inverted On a gated landing, NODE_OPTIONS is now refused unless EVERY flag is on a five-flag benign whitelist (`--max-old-space-size`, `--max-semi-space-size`, `--stack-size`, `--trace-warnings`, `--no-warnings` — memory/tuning and stderr diagnostics only, each justified in comment). `GATED_NODE_OPTION_FLAGS` is deleted: unknown future loader/TLS flags refuse by default, ending the alias hunt that produced four flag rounds. Flags parse whole and exact (split on whitespace, up to `=`), so a near-name like `--max-old-space-sizex` refuses instead of widening. ### Which of 3c's S3 holes this closes — and which remain OPEN (honest list) - CLOSED: `--loader`, `--experimental-loader`, `--import`, `--use-openssl-ca`, `--use-bundled-ca`, `--use-system-ca`, `--openssl-config`, `--inspect`, and any UNKNOWN long flag — none are whitelisted, so each refuses by name. - OPEN (named for the next round, with the exact bypass): **`-r`** — the short `--require` alias does not start with `--`, so `nodeOptionFlagNames` drops it before the check; **quoted flags** — a token like `"--require` carries a leading quote, fails the `startsWith("--")` filter, and is dropped (Node strips quotes per its own rules; the parser must too); **`NODE_USE_ENV_PROXY`** — an env var, not a NODE_OPTIONS flag, and not checked anywhere in this round (on the local Node v22 it does not affect `fetch`, but Node versions that honor it would route the pinned origin through a proxy); **`process.execArgv`** — NOT checked: command-line flags (`node --require x tool.ts`) bypass NODE_OPTIONS entirely, so a gated call started with loader flags on the argv is not refused by this layer. HEAD a6d13fb475e939ef0cc281a2ab142f2be6f06f72; interdiff: piext-132-interdiff-dd.patch (from 6835d30, 369 lines). ## r5 round (stacked on a6d13fb, commit 3462f12): review-3c's one-shape fix review-3c measured on Node 22 that `-r x`, `"--require" x`, `"-r" x`, `--max-old-space-size=1024 -r x`, `--no-warnings "--require" x` and `-C x` all passed the default-deny and loaded a hook (one replaced `fetch` and intercepted the pinned origin). The check now refuses unless EVERY whitespace-separated token, after Node-style quote stripping, matches exactly `^--(max-old-space-size|max-semi-space-size)=\d+$|^--(trace-warnings|no-warnings)$`. The refusal names the flag, never its value. The dead `--stack-size` entry is dropped (Node does not accept it in NODE_OPTIONS; the claim was false) and its comment fixed. Red→green: 2 of the new cases fail on a6d13fb's code. `process.execArgv` and `NODE_USE_ENV_PROXY` remain named follow-ups. HEAD 3462f12e8260a4f8d65408367bdb68dbbcedd251; patches: piext-f5-diff.patch (full, 2617) + piext-132-interdiff-r5.patch (from a6d13fb, 231) + earlier interdiffs.
fix(forgejo): pin FORGEJO_BASE_URL and FORGEJO_LANDING_IDENTITY on GO-gated repos (LR-20261111-017, homelab-93 F5)
All checks were successful
CI / pr-title (pull_request) Successful in 1s
CI / pi-otel-telemetry (pull_request) Successful in 55s
CI / forgejo-ci (pull_request) Successful in 1m0s
CI / skill-args (pull_request) Successful in 1m0s
CI / plane (pull_request) Successful in 1m18s
tests / python (pull_request) Successful in 11s
CI / mcp-bridge (pull_request) Successful in 37s
CI / nats-intercom (pull_request) Successful in 45s
CI / ask-user-question (pull_request) Successful in 1m14s
CI / refocus (pull_request) Successful in 1m15s
CI / redact (pull_request) Successful in 1m2s
CI / pi-friction (pull_request) Successful in 1m6s
CI / todo (pull_request) Successful in 1m33s
CI / forgejo (pull_request) Successful in 2m27s
CI / pi-swarm-suite (pull_request) Successful in 3m58s
0ceb0ff7b4
The four policy knobs stopped being levers on a GO-gated repo (Amendment 15);
the TRANSPORT was still one. `FORGEJO_BASE_URL` is the largest remaining lever:
a session that points it at a local proxy gets the lock's reads answered by the
proxy — fabricated approvals, hidden holds — and the proxy then relays the merge
POST to the real forge with the real landing token, so the lock judged a record
the forge never wrote. `FORGEJO_LANDING_IDENTITY` is the same class from the
other end: it turns "GET /user must answer pi-landing" into "must answer as the
login the session picked".

So both are ignored on the gated repos, exactly as #131 pinned the four knobs,
through the SAME reporting helper (`reportGatedIgnored`, extracted from
resolveLandingPolicy — one sentence shape, one place that never echoes a value):

- `pinGatedTransport` (api.ts) hands back the config with the base at the
  built-in default and the landing login at `pi-landing`, saying each ignored
  lever once per call BY NAME ONLY (a base URL can carry credentials in some
  setups). Off the gated list the config is returned untouched and both stay
  documented overrides.
- The merge tool pins BEFORE its first read, keyed off the name the caller
  addressed (a local list) so the origin being pinned cannot answer the gate;
  the name the forge reports can only ADD gating. Every read of the call, the
  GET /user identity check, the merge POST and the read-back then leave at the
  default origin.
- `assertMergeIdentity` takes that transport (or a repo view) and demands the
  pinned login; the credential still comes from the credential, never from a
  transport.
- The census and `verdict-cli --base-url` take the same pin through the same
  helper, so no lock read on a gated repo is answered by a session-set origin.

Tests: tests/landing-gated-transport-pins-f5.test.ts (9, new) — each gated case
both ways: gated override buys nothing (and is said exactly once, by name),
non-gated honours it and is silent, gated+unset reads the defaults silently.
The fake records the request ORIGIN, so "the request layer dialled the default"
is measured, not inferred.

Lab-Run: LR-20261111-017
Collaborator

LD-VERDICT REQUEST_CHANGES head=0ceb0ff7b421cd1cbb4ca446450dff1eb823dab3 session=LR-20261111-017-seatA-20261111T1844Z

Reviewed at the pinned head (base 8c666fbec4), diff vs origin/main in my own folder.

Checked and OK:

  1. pinGatedTransport (api.ts:956) pins base and login on a gated view and else returns the config untouched; assertMergeIdentity (api.ts:1499) takes the transport's login and dials the transport's base for GET /user, and the merge tool passes the pinned transport (index.ts:945-948); the census pins before its first read (index.ts:1110); verdict-cli pins args.baseUrl before collectState (verdict-cli.ts:923-929). The forge-reported name is used only to ADD gating (index.ts:915), never to unpin.
  2. Log shape: reportGatedIgnored (landing.ts:3145) emits one line per set lever, name only; the gated merge test asserts exactly one line per lever and no value.
  3. Tests: new file 9/9 green. Scratch check (four source files restored from origin/main, new test + fake.ts kept): 6 failed / 3 passed — the bogus-login and stub-origin cases do go red on the old code. Full suite: 58 files / 1288 tests, 1 failure = tests/merge-identity.test.ts (ambient landing token), the pre-existing case named in the brief.

Holds:

  1. api.ts:968 (gated flag comes from the caller's spelling) + index.ts:904-921. The pin is reached only when repoKey(repo_ref) is on GO_GATED_REPOS (landing.ts:3020) — a name the same session writes. Probe in my own folder (deleted afterwards): with FORGEJO_BASE_URL=https://proxy.invalid and FORGEJO_LANDING_TOKEN staged, forgejo_merge_pr(repo_ref="x/Homelab") had its lock reads answered by the fixture, every call origin was https://proxy.invalid, and the single POST .../merge at that origin carried the landing token. A session that can choose repo_ref therefore keeps the whole call on its own origin and can relay that POST to Homelab/prod: exactly the relay F5 sets out to close. The docs claim ("every read of the call, the GET /user identity check and the merge POST go to the built-in default") does not hold for that call.
  2. verdict-cli.ts:923-929, same root cause: --repo x/Homelab --base-url <proxy> pins nothing, so the run's reads come from the proxy and the verdict POST leaves for the proxy origin with the reviewer credential — a fabricated record can be judged and the comment relayed to a gated repo. --repo is caller-chosen too.

Direction, not a prescription: for a landing write (and the reads a verdict rests on), either do not honour a non-default base at all, or require the repo to be proven non-gated against the built-in forge before leaving the default. As written the gate is keyed on the request's claim, not on the repo.
Nit: api.ts:931 types baseSource as "built-in-default" while the field comment says default-built-in.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261111T1844Z

LD-VERDICT REQUEST_CHANGES head=0ceb0ff7b421cd1cbb4ca446450dff1eb823dab3 session=LR-20261111-017-seatA-20261111T1844Z Reviewed at the pinned head (base 8c666fbec432f70b4853124f8de6ee2e445d1339), diff vs origin/main in my own folder. Checked and OK: 1. pinGatedTransport (api.ts:956) pins base and login on a gated view and else returns the config untouched; assertMergeIdentity (api.ts:1499) takes the transport's login and dials the transport's base for GET /user, and the merge tool passes the pinned transport (index.ts:945-948); the census pins before its first read (index.ts:1110); verdict-cli pins args.baseUrl before collectState (verdict-cli.ts:923-929). The forge-reported name is used only to ADD gating (index.ts:915), never to unpin. 2. Log shape: reportGatedIgnored (landing.ts:3145) emits one line per set lever, name only; the gated merge test asserts exactly one line per lever and no value. 3. Tests: new file 9/9 green. Scratch check (four source files restored from origin/main, new test + fake.ts kept): 6 failed / 3 passed — the bogus-login and stub-origin cases do go red on the old code. Full suite: 58 files / 1288 tests, 1 failure = tests/merge-identity.test.ts (ambient landing token), the pre-existing case named in the brief. Holds: 1. api.ts:968 (gated flag comes from the caller's spelling) + index.ts:904-921. The pin is reached only when repoKey(repo_ref) is on GO_GATED_REPOS (landing.ts:3020) — a name the same session writes. Probe in my own folder (deleted afterwards): with FORGEJO_BASE_URL=https://proxy.invalid and FORGEJO_LANDING_TOKEN staged, forgejo_merge_pr(repo_ref="x/Homelab") had its lock reads answered by the fixture, every call origin was https://proxy.invalid, and the single POST .../merge at that origin carried the landing token. A session that can choose repo_ref therefore keeps the whole call on its own origin and can relay that POST to Homelab/prod: exactly the relay F5 sets out to close. The docs claim ("every read of the call, the GET /user identity check and the merge POST go to the built-in default") does not hold for that call. 2. verdict-cli.ts:923-929, same root cause: `--repo x/Homelab --base-url <proxy>` pins nothing, so the run's reads come from the proxy and the verdict POST leaves for the proxy origin with the reviewer credential — a fabricated record can be judged and the comment relayed to a gated repo. `--repo` is caller-chosen too. Direction, not a prescription: for a landing write (and the reads a verdict rests on), either do not honour a non-default base at all, or require the repo to be proven non-gated against the built-in forge before leaving the default. As written the gate is keyed on the request's claim, not on the repo. Nit: api.ts:931 types baseSource as "built-in-default" while the field comment says `default-built-in`. model: omniroute/deepseek-flash session: LR-20261111-017-seatA-20261111T1844Z
Collaborator

LD-VERDICT APPROVED head=0ceb0ff7b421cd1cbb4ca446450dff1eb823dab3 session=LR-20261111-017-seatB-20261111T1844Z

Reviewed the delta 8c666fb..0ceb0ff (main → the branch head; the PR is one commit on top of main, head verified as the pinned 40-hex). Cloned read-only into my own seat folder, worktree at the exact head, no code touched.

  1. The transport cannot be redirected on a GO-gated repo. pinGatedTransport (api.ts:952-1006) returns {...config, baseUrl: DEFAULT_BASE_URL} and landingIdentity: DEFAULT_LANDING_IDENTITY when view.gated. Traced every caller:
  • merge tool (index.ts:901-906) pins BEFORE its first read, c = transport.config, so the whole assertLandingLock → collectLandingEvidence read set (getPR, commit status, paged comments/reviews/inline, changed files, branch protection) leaves at the pinned base; write (index.ts:915-917) then feeds resolveLandingCredentialAtRuntime, the identity check, the whitelist read, the merge POST and the read-back;
  • assertMergeIdentity (api.ts:1340-1585, new transport/view opts) rebuilds the credential as asserted with transport.config.baseUrl and uses it for GET /user; mergeAsLanding writes through identity.config, which is asserted.config — so the merge POST cannot go to a session-set origin while the token stays the landing token;
  • census (index.ts:1105-1116) pins once before listPRs and passes pinned into every collectLandingEvidence;
  • verdict-cli (verdict-cli.ts:913-928) pins args.baseUrl once, before the first read; every read/write in the run is through args.baseUrl (apiCall/paged/loginOf/submitReview/verify), no second resolution from env.FORGEJO_BASE_URL; --base-url and the env fallback still parse as documented (verdict-cli.ts:212).
    The server name can only ADD gating: serverRepoView(..., requireReported) (landing.ts:3080-3105) refuses a name that disagrees with the request after one URL-decode + case-fold, so in the merge and census paths lock.gated ≡ isGoGatedRepo(requested) — an alias is refused, never censused or merged under the caller's spelling. The extra write re-pin for the server-added case is therefore defensive, not a second route.
    Also confirmed there is no other forge env read on these paths (grep for FORGEJO_BASE_URL outside tests: only loadConfig at api.ts:127 and the non-landing comment-as/seat-post entry points), and the CLI still loads under its documented node --experimental-strip-types entry point (smoke run past imports, no ERR_MODULE_NOT_FOUND).
  1. The line fires once per call, by name only. reportGatedIgnored (landing.ts:3141-3160) prints name + repo key + consequence and never a value; the set predicate compares against the default (api.ts:975-1000), so the already-pinned transport handed to the next stage reports nothing. In the merge tool the pin happens once at the top and assertMergeIdentity receives transport, so it does not re-pin: the end-to-end test measures exactly one FORGEJO_BASE_URL and one FORGEJO_LANDING_IDENTITY line and absence of the stub base and bogus login. Census/CLI pin once; the consequence clauses say what the pin did.

  2. Non-gated repos still honour both levers. pinGatedTransport returns the config untouched and resolveLandingIdentity(expected, env) when !view.gated; two control tests prove both levers end-to-end at the helper level (identity demanded = the env login, GET /user dialled at the session base, zero lines) and the CLI control proves --base-url is honoured off the gated list. Full suite green at the head confirms no regression on the non-gated paths.

  3. The new tests genuinely fail on the old code. Scratch-check in my own worktree: restored the four source files (api.ts, index.ts, landing.ts, verdict-cli.ts) to 8c666fb and ran the new file — 6 failed / 3 passed, exactly the bogus-login cases, the stub-origin GET /user case, the merge-POST-origin case, the verdict-cli --base-url gated case and the end-to-end tool case; restored to the head and got 9/9 back (file diff identical after restore). tsc --noEmit clean; vitest run at the head 1287 pass / 1 fail, and that one (tests/merge-identity.test.ts) fails identically with the base sources in place — ambient FORGEJO_LANDING_TOKEN_FILE in this sandbox, pre-existing, as briefed.

Holds: none.

Non-blocking observations (not holds): (a) the PR description says the pin is "keyed off the SERVER's pr.base.repo.full_name", but the pin is taken from the caller-addressed name (a local, network-free list) precisely so the origin being pinned cannot answer the gate — docs/forgejo.md states this correctly, the description sentence is loose; (b) GatedTransport.baseSource (api.ts:931) is set but never read — dead field; (c) non-gated control coverage exists for the helper and the CLI but not end-to-end through the merge tool/census, though those paths only return the config unchanged.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261111T1844Z

LD-VERDICT APPROVED head=0ceb0ff7b421cd1cbb4ca446450dff1eb823dab3 session=LR-20261111-017-seatB-20261111T1844Z Reviewed the delta `8c666fb..0ceb0ff` (main → the branch head; the PR is one commit on top of main, head verified as the pinned 40-hex). Cloned read-only into my own seat folder, worktree at the exact head, no code touched. 1) The transport cannot be redirected on a GO-gated repo. `pinGatedTransport` (api.ts:952-1006) returns `{...config, baseUrl: DEFAULT_BASE_URL}` and `landingIdentity: DEFAULT_LANDING_IDENTITY` when `view.gated`. Traced every caller: - merge tool (index.ts:901-906) pins BEFORE its first read, `c = transport.config`, so the whole `assertLandingLock → collectLandingEvidence` read set (getPR, commit status, paged comments/reviews/inline, changed files, branch protection) leaves at the pinned base; `write` (index.ts:915-917) then feeds `resolveLandingCredentialAtRuntime`, the identity check, the whitelist read, the merge POST and the read-back; - `assertMergeIdentity` (api.ts:1340-1585, new `transport`/`view` opts) rebuilds the credential as `asserted` with `transport.config.baseUrl` and uses it for `GET /user`; `mergeAsLanding` writes through `identity.config`, which is `asserted.config` — so the merge POST cannot go to a session-set origin while the token stays the landing token; - census (index.ts:1105-1116) pins once before `listPRs` and passes `pinned` into every `collectLandingEvidence`; - `verdict-cli` (verdict-cli.ts:913-928) pins `args.baseUrl` once, before the first read; every read/write in the run is through `args.baseUrl` (apiCall/paged/loginOf/submitReview/verify), no second resolution from `env.FORGEJO_BASE_URL`; `--base-url` and the env fallback still parse as documented (verdict-cli.ts:212). The server name can only ADD gating: `serverRepoView(..., requireReported)` (landing.ts:3080-3105) refuses a name that disagrees with the request after one URL-decode + case-fold, so in the merge and census paths `lock.gated ≡ isGoGatedRepo(requested)` — an alias is refused, never censused or merged under the caller's spelling. The extra `write` re-pin for the server-added case is therefore defensive, not a second route. Also confirmed there is no other forge env read on these paths (`grep` for `FORGEJO_BASE_URL` outside tests: only `loadConfig` at api.ts:127 and the non-landing comment-as/seat-post entry points), and the CLI still loads under its documented `node --experimental-strip-types` entry point (smoke run past imports, no ERR_MODULE_NOT_FOUND). 2) The line fires once per call, by name only. `reportGatedIgnored` (landing.ts:3141-3160) prints name + repo key + consequence and never a value; the `set` predicate compares against the default (api.ts:975-1000), so the already-pinned transport handed to the next stage reports nothing. In the merge tool the pin happens once at the top and `assertMergeIdentity` receives `transport`, so it does not re-pin: the end-to-end test measures exactly one `FORGEJO_BASE_URL` and one `FORGEJO_LANDING_IDENTITY` line and absence of the stub base and bogus login. Census/CLI pin once; the consequence clauses say what the pin did. 3) Non-gated repos still honour both levers. `pinGatedTransport` returns the config untouched and `resolveLandingIdentity(expected, env)` when `!view.gated`; two control tests prove both levers end-to-end at the helper level (identity demanded = the env login, `GET /user` dialled at the session base, zero lines) and the CLI control proves `--base-url` is honoured off the gated list. Full suite green at the head confirms no regression on the non-gated paths. 4) The new tests genuinely fail on the old code. Scratch-check in my own worktree: restored the four source files (`api.ts`, `index.ts`, `landing.ts`, `verdict-cli.ts`) to `8c666fb` and ran the new file — 6 failed / 3 passed, exactly the bogus-login cases, the stub-origin `GET /user` case, the merge-POST-origin case, the `verdict-cli --base-url` gated case and the end-to-end tool case; restored to the head and got 9/9 back (file diff identical after restore). `tsc --noEmit` clean; `vitest run` at the head 1287 pass / 1 fail, and that one (`tests/merge-identity.test.ts`) fails identically with the base sources in place — ambient `FORGEJO_LANDING_TOKEN_FILE` in this sandbox, pre-existing, as briefed. Holds: none. Non-blocking observations (not holds): (a) the PR description says the pin is "keyed off the SERVER's `pr.base.repo.full_name`", but the pin is taken from the caller-addressed name (a local, network-free list) precisely so the origin being pinned cannot answer the gate — `docs/forgejo.md` states this correctly, the description sentence is loose; (b) `GatedTransport.baseSource` (api.ts:931) is set but never read — dead field; (c) non-gated control coverage exists for the helper and the CLI but not end-to-end through the merge tool/census, though those paths only return the config unchanged. model: omniroute/deepseek-flash session: LR-20261111-017-seatB-20261111T1844Z
fix(forgejo): ask the gate on the built-in forge and key the transport pin on the SERVER's name (LR-20261111-017, homelab-93 F5b)
All checks were successful
CI / pr-title (pull_request) Successful in 2s
CI / forgejo-ci (pull_request) Successful in 53s
CI / todo (pull_request) Successful in 58s
CI / pi-otel-telemetry (pull_request) Successful in 1m3s
tests / python (pull_request) Successful in 5s
CI / ask-user-question (pull_request) Successful in 1m7s
CI / skill-args (pull_request) Successful in 1m10s
CI / mcp-bridge (pull_request) Successful in 30s
CI / plane (pull_request) Successful in 1m25s
CI / nats-intercom (pull_request) Successful in 34s
CI / redact (pull_request) Successful in 31s
CI / refocus (pull_request) Successful in 50s
CI / pi-friction (pull_request) Successful in 45s
CI / forgejo (pull_request) Successful in 2m11s
CI / pi-swarm-suite (pull_request) Successful in 3m49s
69d9993e45
F5 pinned FORGEJO_BASE_URL and FORGEJO_LANDING_IDENTITY on a GO-gated repo,
but keyed the pin on `repoKey(repo_ref)` — the CALLER's spelling. seatA
measured the hole (#132, comment 24838): a session that addressed
`repo_ref="x/Homelab"` never engaged the pin, so the proxy at
`FORGEJO_BASE_URL` answered the lock's reads (fabricated approvals, hidden
holds) and the single merge POST carried the staged landing token to that
origin — relayable to the real forge, so the lock judged a record the forge
never wrote. `verdict-cli --repo x/Homelab --base-url <proxy>` had the same
shape for the reviewer credential.

The gate is now decided by the SERVER, asked on the BUILT-IN base:

- `gateProbeConfig` (api.ts) is the config for the ONE read whose answer names
  the repo. The merge tool's first read (getPR) and verdict-cli's first read
  in `collectState` always go there, never to an env/--base-url base, so the
  origin that would answer "which repo is this, and is it gated?" is not an
  origin the session chose.
- Gating keys off `pr.base.repo.full_name` from THAT answer — #131's
  server-name doctrine, applied to the transport. `collectLandingEvidence`
  pins once, from the server's view (fail-closed on an unreadable name), and
  every later read plus the merge write reuse that transport.
- Only a name that proves the repo NON-gated lets the rest of the call honour
  `FORGEJO_BASE_URL` / `--base-url` (reads after the first, and the write),
  exactly today's non-gated behaviour. On a gated repo every read, the
  GET /user identity check and the write stay on the built-in default.
- Docs corrected: "every read … at the built-in default" is now true for the
  gated case and for the first read of every call.

Tests: tests/landing-gated-transport-server-keyed-f5b.test.ts (7, new) — the
two origins are separate route tables, so "the sanctioned forge was dialled,
the proxy saw nothing" is measured. seatA's probe for the merge tool and for
verdict-cli, an absent-name refusal, a gated-by-server-name guard (all
traffic default, one line per lever) and two non-gated controls (first read
default, the rest on the flag). 5 of the 7 fail on 0ceb0ff; the two
server-named-gated guards pass there too — the caller-keyed pin happened to
cover those spellings, which is precisely why the bogus-owner probe was
needed. The F5 round's 9 tests stay green (its verdict-cli control now
asserts the new, narrower contract: the flag is honoured for reads AFTER the
first).

Lab-Run: LR-20261111-017
Collaborator

LD-VERDICT REQUEST_CHANGES head=69d9993e45faafc20a6c585e802333da84f2b7e2 session=LR-20261111-017-seatA-20261111T1905Z

Head verified against the PR (69d9993e45), diff judged base..head 8c666fb..69d9993 (8 files, +1173/−44). Read-only clone in the seat folder.

What I checked and what holds up:

  • gateProbeConfig (forgejo/api.ts:109) is the one spelling of "where the gate is asked", and all three named paths use it: the merge tool's probe (forgejo/index.ts:979-985, passed into assertLandingLock → collectLandingEvidence → getPR(opts.probe ?? c) at :590-596), collectLandingEvidence pinning ONCE off identity.view (the server's base.repo.full_name, :397-414, fail-closed {gated:true} on an absent name), and verdict-cli's PR fetch (forgejo/verdict-cli.ts:494-501) with the pin paid once inside collectState (:522-527) before any later read; loginOf (:970) and the verdict POST (:1139) both come after it and read the pinned args.baseUrl. The write reuses lock.transport (index.ts:988-992) so assertMergeIdentity/mergeAsLanding/readMergeOutcome stay on the origin the probe proved, with one line per ignored lever per call.
  • Red→green: reverting the three implementation files of 69d9993 (api.ts, index.ts, verdict-cli.ts, plus tests/fake.ts) in a scratch copy turns 5 of the 7 new f5b tests red, including both probes (merge with repo_ref=x/Homelab merges at the proxy on the old code; verdict run reads and POSTs at the proxy). On the head, both F5/F5b test files are 16/16 green.
  • Non-gated-by-server-name + env base still honoured after the first read: the two control tests pass (first read default, every later read and the write on the env base).
  • Logs: gated lines fire once per lever, by name only — asserted exactly one FORGEJO_BASE_URL is set and ignored and no occurrence of the stub origin in stderr (tests/landing-gated-transport-server-keyed-f5b.test.ts:167-188); the consequence text names the built-in default, never the session's value.
  • Suite: 1294 passed / 1 failed, the failure being the known pre-existing tests/merge-identity.test.ts:144 ambient-token case (unchanged on the untouched base) — not a hold.

HOLD (1) — forgejo/index.ts:1183-1188 (with forgejo/index.ts:1258-1263). The census still pre-pins on the CALLER's spelling, before its first read: pinGatedTransport(c, { ref: repoRef, gated: isGoGatedRepo(repoRef) }) where repoRef is parsed straight from params.repo_ref, and each row's evidence read is then handed that pinned config with no probe. This is the same caller-keyed lever r2 removed from the merge tool, left on the tool that reports what can land. Measured on this exact head in a scratch copy with two route tables (sanctioned + proxy), repo_ref="x/Homelab", FORGEJO_BASE_URL=https://proxy.invalid: the census dialled only the proxy — GET /api/v1/repos/x/Homelab/pulls then GET /api/v1/repos/x/Homelab/pulls/79 — and never asked the sanctioned forge; the proxy can answer base.repo.full_name = "Homelab/prod" (so the per-row name check agrees), fabricate exact-head verdicts and hide holds in the queue the operator reads. Scope, stated honestly: the census carries no landing credential and cannot write, so this is NOT the merge-POST hole this PR closes; but the commit message's "the caller-keyed pre-pin is gone" is not true yet, and "no read can leave the default forge before the server's name proves non-gated" is not met on this path. Suggested fix shape (same doctrine as the merge leg): probe the census's first read at the built-in base, key the transport on the server's reported name per row (or refuse rows whose reported name cannot be shown ungated), and let FORGEJO_BASE_URL govern later reads only on a proven non-gated repo, with a red test mirroring the one above.

Model/seat caveat: none of the above is a formal review state; this comment is the verdict and nothing else is evidence.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261111T1905Z

LD-VERDICT REQUEST_CHANGES head=69d9993e45faafc20a6c585e802333da84f2b7e2 session=LR-20261111-017-seatA-20261111T1905Z Head verified against the PR (69d9993e45faafc20a6c585e802333da84f2b7e2), diff judged base..head 8c666fb..69d9993 (8 files, +1173/−44). Read-only clone in the seat folder. What I checked and what holds up: - `gateProbeConfig` (forgejo/api.ts:109) is the one spelling of "where the gate is asked", and all three named paths use it: the merge tool's probe (forgejo/index.ts:979-985, passed into `assertLandingLock` → `collectLandingEvidence` → `getPR(opts.probe ?? c)` at :590-596), `collectLandingEvidence` pinning ONCE off `identity.view` (the server's `base.repo.full_name`, :397-414, fail-closed `{gated:true}` on an absent name), and verdict-cli's PR fetch (forgejo/verdict-cli.ts:494-501) with the pin paid once inside `collectState` (:522-527) before any later read; `loginOf` (:970) and the verdict POST (:1139) both come after it and read the pinned `args.baseUrl`. The write reuses `lock.transport` (index.ts:988-992) so `assertMergeIdentity`/`mergeAsLanding`/`readMergeOutcome` stay on the origin the probe proved, with one line per ignored lever per call. - Red→green: reverting the three implementation files of 69d9993 (api.ts, index.ts, verdict-cli.ts, plus tests/fake.ts) in a scratch copy turns 5 of the 7 new f5b tests red, including both probes (merge with `repo_ref=x/Homelab` merges at the proxy on the old code; verdict run reads and POSTs at the proxy). On the head, both F5/F5b test files are 16/16 green. - Non-gated-by-server-name + env base still honoured after the first read: the two control tests pass (first read default, every later read and the write on the env base). - Logs: gated lines fire once per lever, by name only — asserted exactly one `FORGEJO_BASE_URL is set and ignored` and no occurrence of the stub origin in stderr (tests/landing-gated-transport-server-keyed-f5b.test.ts:167-188); the consequence text names the built-in default, never the session's value. - Suite: 1294 passed / 1 failed, the failure being the known pre-existing `tests/merge-identity.test.ts:144` ambient-token case (unchanged on the untouched base) — not a hold. HOLD (1) — forgejo/index.ts:1183-1188 (with forgejo/index.ts:1258-1263). The census still pre-pins on the CALLER's spelling, before its first read: `pinGatedTransport(c, { ref: repoRef, gated: isGoGatedRepo(repoRef) })` where `repoRef` is parsed straight from `params.repo_ref`, and each row's evidence read is then handed that `pinned` config with no `probe`. This is the same caller-keyed lever r2 removed from the merge tool, left on the tool that reports what can land. Measured on this exact head in a scratch copy with two route tables (sanctioned + proxy), `repo_ref="x/Homelab"`, `FORGEJO_BASE_URL=https://proxy.invalid`: the census dialled only the proxy — `GET /api/v1/repos/x/Homelab/pulls` then `GET /api/v1/repos/x/Homelab/pulls/79` — and never asked the sanctioned forge; the proxy can answer `base.repo.full_name = "Homelab/prod"` (so the per-row name check agrees), fabricate exact-head verdicts and hide holds in the queue the operator reads. Scope, stated honestly: the census carries no landing credential and cannot write, so this is NOT the merge-POST hole this PR closes; but the commit message's "the caller-keyed pre-pin is gone" is not true yet, and "no read can leave the default forge before the server's name proves non-gated" is not met on this path. Suggested fix shape (same doctrine as the merge leg): probe the census's first read at the built-in base, key the transport on the server's reported name per row (or refuse rows whose reported name cannot be shown ungated), and let `FORGEJO_BASE_URL` govern later reads only on a proven non-gated repo, with a red test mirroring the one above. Model/seat caveat: none of the above is a formal review state; this comment is the verdict and nothing else is evidence. model: omniroute/deepseek-flash session: LR-20261111-017-seatA-20261111T1905Z
Collaborator

LD-VERDICT APPROVED head=69d9993e45faafc20a6c585e802333da84f2b7e2 session=LR-20261111-017-seatB-20261111T1905Z

Fresh read-only clone, exact head 69d9993 (verified via the PR record: Head sha matches, base 8c666fb). Judged the full base..head diff (8 files, +1173/-44).

  1. Probe-first transport, traced end to end — no landing read or write can leave the built-in forge before the server's name proves non-gated.
    • merge tool: gateProbeConfig (index.ts:979) is the config of the FIRST read, getPR inside collectLandingEvidence (index.ts:390); identity is decided from pr.base.repo.full_name there with serverRepoView(..., {requireReported:true}) (index.ts:398), the pin is paid ONCE from that view (index.ts:408), and every later evidence read takes tc = transport.config (CI, comments, reviews, inline, files, required-CI). The write reuses lock.transport: credential staging on write.config (index.ts:1010), assertMergeIdentity({transport: write}) which dials the transport base for GET /user (api.ts:1585), mergeAsLanding(identity) writing with identity.config (api.ts:1861), read-back on write.config. configured (the env base) is used nowhere in the write path after the probe.
    • verdict-cli collectState: probe at the built-in base (verdict-cli.ts:498), gate from the server name, pin (verdict-cli.ts:522), args.baseUrl narrowed at :527 — so every later read (:532–:647), GET /user (:710) and the verdict POST (:1139) use the pinned origin; off the gated list the flag is left untouched.
    • No caller-keyed pre-pin remains on a write path: pinGatedTransport call sites are index.ts:408 (server view), verdict-cli.ts:522 (server view), api.ts:1556 (already-pinned transport). The one caller-spelling-keyed use left is the census enumeration (index.ts:1186) — read-only, and a row is refused unless the reported name equals the requested one; equal keys imply an equal gate decision, so a gated record cannot be read from a non-default origin.
  2. The red→green is measured, not asserted: with 0ceb0ff's source restored (five files reverted, the new test kept) the f5b file is 5 failed / 2 passed, and the five include both prior-seat probes (merge: bogus owner + a proxy that WOULD merge, staged token; verdict-cli: --repo x/Homelab --base-url stub) plus the absent-name refusal. At head, the two F5 files are 16/16 green. The two cases green on both are exactly the server-named-gated guards — the caller-keyed spelling happened to cover those, which is why the bogus-owner probe mattered.
  3. Non-gated-by-server-name + env base honoured after the first read: merge control shows the sanctioned origin saw ONLY the probe (one call) and every later read and the merge POST left for the env base; verdict-cli control shows the POST leaving for the --base-url origin while the first read of each collectState opens on the default.
  4. Logging: on a gated merge exactly one FORGEJO_BASE_URL is set and ignored and one FORGEJO_LANDING_IDENTITY is set and ignored line per call, by name, and the test asserts neither the stub base nor the bogus login appears in stderr; same shape asserted for assertMergeIdentity and the verdict run. reportGatedIgnored carries names and constant consequences only.

Typecheck tsc --noEmit: clean. Full suite: 1294 passed, 1 failed — tests/merge-identity.test.ts > merges with the dedicated landing credential, never the operator token, which fails identically on the untouched base 8c666fb (ambient landing token); pre-existing, not a hold.

Observation (not a hold): the census's own enumeration read is still keyed on the caller's spelling (index.ts:1186) rather than the server's name; it cannot report a GO-gated repo from a non-default origin (per-row name agreement) and the merge lock never consults it.

No holds.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261111T1905Z

LD-VERDICT APPROVED head=69d9993e45faafc20a6c585e802333da84f2b7e2 session=LR-20261111-017-seatB-20261111T1905Z Fresh read-only clone, exact head 69d9993 (verified via the PR record: Head sha matches, base 8c666fb). Judged the full base..head diff (8 files, +1173/-44). 1. Probe-first transport, traced end to end — no landing read or write can leave the built-in forge before the server's name proves non-gated. - merge tool: `gateProbeConfig` (index.ts:979) is the config of the FIRST read, `getPR` inside `collectLandingEvidence` (index.ts:390); identity is decided from `pr.base.repo.full_name` there with `serverRepoView(..., {requireReported:true})` (index.ts:398), the pin is paid ONCE from that view (index.ts:408), and every later evidence read takes `tc = transport.config` (CI, comments, reviews, inline, files, required-CI). The write reuses `lock.transport`: credential staging on `write.config` (index.ts:1010), `assertMergeIdentity({transport: write})` which dials the transport base for `GET /user` (api.ts:1585), `mergeAsLanding(identity)` writing with `identity.config` (api.ts:1861), read-back on `write.config`. `configured` (the env base) is used nowhere in the write path after the probe. - verdict-cli `collectState`: probe at the built-in base (verdict-cli.ts:498), gate from the server name, pin (verdict-cli.ts:522), `args.baseUrl` narrowed at :527 — so every later read (:532–:647), `GET /user` (:710) and the verdict POST (:1139) use the pinned origin; off the gated list the flag is left untouched. - No caller-keyed pre-pin remains on a write path: `pinGatedTransport` call sites are index.ts:408 (server view), verdict-cli.ts:522 (server view), api.ts:1556 (already-pinned transport). The one caller-spelling-keyed use left is the census enumeration (index.ts:1186) — read-only, and a row is refused unless the reported name equals the requested one; equal keys imply an equal gate decision, so a gated record cannot be read from a non-default origin. 2. The red→green is measured, not asserted: with 0ceb0ff's source restored (five files reverted, the new test kept) the f5b file is 5 failed / 2 passed, and the five include both prior-seat probes (merge: bogus owner + a proxy that WOULD merge, staged token; verdict-cli: `--repo x/Homelab --base-url stub`) plus the absent-name refusal. At head, the two F5 files are 16/16 green. The two cases green on both are exactly the server-named-gated guards — the caller-keyed spelling happened to cover those, which is why the bogus-owner probe mattered. 3. Non-gated-by-server-name + env base honoured after the first read: merge control shows the sanctioned origin saw ONLY the probe (one call) and every later read and the merge POST left for the env base; verdict-cli control shows the POST leaving for the `--base-url` origin while the first read of each `collectState` opens on the default. 4. Logging: on a gated merge exactly one `FORGEJO_BASE_URL is set and ignored` and one `FORGEJO_LANDING_IDENTITY is set and ignored` line per call, by name, and the test asserts neither the stub base nor the bogus login appears in stderr; same shape asserted for `assertMergeIdentity` and the verdict run. `reportGatedIgnored` carries names and constant consequences only. Typecheck `tsc --noEmit`: clean. Full suite: 1294 passed, 1 failed — `tests/merge-identity.test.ts > merges with the dedicated landing credential, never the operator token`, which fails identically on the untouched base 8c666fb (ambient landing token); pre-existing, not a hold. Observation (not a hold): the census's own enumeration read is still keyed on the caller's spelling (index.ts:1186) rather than the server's name; it cannot report a GO-gated repo from a non-default origin (per-row name agreement) and the merge lock never consults it. No holds. model: omniroute/deepseek-flash session: LR-20261111-017-seatB-20261111T1905Z
fix(forgejo): ask the census gate on the built-in forge too, per row and per repo (LR-20261111-017, homelab-93 F5b r3)
All checks were successful
CI / pr-title (pull_request) Successful in 0s
CI / pi-otel-telemetry (pull_request) Successful in 56s
CI / forgejo-ci (pull_request) Successful in 1m10s
CI / plane (pull_request) Successful in 1m31s
CI / skill-args (pull_request) Successful in 39s
tests / python (pull_request) Successful in 5s
CI / todo (pull_request) Successful in 56s
CI / mcp-bridge (pull_request) Successful in 26s
CI / ask-user-question (pull_request) Successful in 48s
CI / nats-intercom (pull_request) Successful in 26s
CI / refocus (pull_request) Successful in 56s
CI / redact (pull_request) Successful in 47s
CI / pi-friction (pull_request) Successful in 49s
CI / forgejo (pull_request) Successful in 1m53s
CI / pi-swarm-suite (pull_request) Successful in 3m58s
e29e447e3d
F5b's commit claimed "the caller-keyed pre-pin is gone". It was not: the
CENSUS path still pre-pinned `pinGatedTransport(c, { ref: repoRef, gated:
isGoGatedRepo(repoRef) })` from the CALLER's parsed `params.repo_ref` BEFORE any
read, and handed each row's evidence read that pinned config with NO probe
(index.ts ~:1183-1188, ~:1258-1263). seatA measured it on this head (#132,
comment 24856): `repo_ref="x/Homelab"` + `FORGEJO_BASE_URL=<proxy>` made the
census dial ONLY the proxy (GET pulls, GET pulls/79) and never ask the
sanctioned forge. A proxy can answer `base.repo.full_name="Homelab/prod"`,
fabricate exact-head verdicts and hide holds, so the operator's queue showed a
record the forge never wrote. No landing credential is on this read-only path,
but the queue is what a person (or an orchestrator) decides from.

The census now takes the same first read the merge leg takes:

- its FIRST read for the repo — the PR listing — goes through `gateProbeConfig`
  (the built-in default), and so does each row's own first read
  (`GET pulls/{n}`, passed as `probe` to `collectLandingEvidence`);
- gating and the transport pin key off the name the SERVER reports
  (`pr.base.repo.full_name`) per row, decided in `collectLandingEvidence` like
  every other path — never off the caller's spelling;
- a row whose reported name is unreadable or disagrees with the request keeps
  #131's refusal: it stays in the queue as `evidence unavailable` rather than
  being censused under the caller's spelling;
- only a row the server proves NON-gated honours `FORGEJO_BASE_URL` for its
  later reads; a gated row never leaves the built-in base. Each ignored lever is
  said once per TOOL CALL, not once per row;
- `isGoGatedRepo` is no longer imported by the census: the pre-pin call is gone,
  so r2's message is now true.

Docs corrected on all three paths (merge tool, census, verdict-cli): "no read
leaves the default forge before the server's name proves the repo non-gated" is
now the contract everywhere, not only on the path that writes.

Tests: tests/landing-census-server-keyed-f5b-r3.test.ts (3, new) — the two
origins are separate route tables, so "the forge was dialled, the proxy saw
nothing" is measured. seatA's census probe (bogus owner, proxy holding a landable
record: first read default, proxy empty, row refused, no fabricated approval in
the queue), a gated-by-server-name census (every read default, hold #105 visible,
one lever line for two rows, no fabricated verdict) and a non-gated control (the
listing and the row's first read on the default, every later read on the env
base, row landable from the proxy's record). 2 of the 3 fail on 69d9993; the
gated-by-server-name guard passes there too — the caller-keyed pin happened to
cover that spelling, which is exactly why the bogus-owner probe was needed.

Lab-Run: LR-20261111-017
Collaborator

LD-VERDICT APPROVED head=e29e447e3dddb6c388bc571f54cbb29328d9d652 session=LR-20261111-017-seatA-20261111T1916Z

Judged the diff at the pinned head only (PR head read back = e29e447e3d, so no mismatch to refuse). Traced all three landing paths from entry to first network call, then ran the suite.

What I checked

  • Merge tool: first call is getPR(opts.probe ?? c) (forgejo/index.ts:392) where probe = gateProbeConfig(configured) (index.ts:981 → api.ts:112) — built-in DEFAULT_BASE_URL; the pin is paid once, keyed on the SERVER's pr.base.repo.full_name (index.ts:410), fail-closed to gated:true when the name is unreadable/absent. Every subsequent evidence read uses tc (index.ts:437,448,460,480,511,548,565); GET /user and the merge POST use write.config/identity.config (index.ts:1021, 1103; api.ts:1871), so the write cannot leave on another origin.
  • Census: listing goes through listPRs(probe, …) (index.ts:1208) at the built-in base; each row's first read is the same probe via collectLandingEvidence(..., { probe, transportLog }) (index.ts:1281-1286), the per-row transport is pinned from that row's server-reported name, and a gated/unreadable row never leaves the built-in base. After the row's evidence there is no further network read in the loop (policy + lock are pure).
  • verdict-cli: first read dialled at gateProbeConfig({ baseUrl: args.baseUrl, … }).baseUrl (verdict-cli.ts:498); pin once at :522 on serverRepoView's view; args.baseUrl narrowed at :527, so every later read (status/commits/reviews/files/branch_protections), GET /user and the formal-review POST inherit the pinned origin.
  • No caller-keyed path left: grep for pinGatedTransport/isGoGatedRepo/repoKey fed from a parsed repo_ref or --repo finds only comments — the string overload resolveLandingPolicy(env, repoRef) (landing.ts:3211) has no tool caller; all four real call sites pass a server-derived {ref,gated} view.
  • Non-gated rows honour FORGEJO_BASE_URL only after the probe proved the name non-gated against the sanctioned forge (control test asserts origins [default, default, then env…]).
  • Logging: name only, once per tool call — the census de-duplicates the line (index.ts:1200-1207), the merge reuses one transport for three stages, and the tests assert exactly one FORGEJO_BASE_URL is set and ignored line and that the stub origin string never appears in stderr.

Evidence

  • New tests at head: 19/19 green (landing-gated-transport-pins-f5 9, …-server-keyed-f5b 7, …-census-server-keyed-f5b-r3 3).
  • Not vacuous: the r3 census test file copied onto 69d9993 goes 2/3 red (first-read-origin assertion red, expected default / received proxy); the f5b server-keyed file on 0ceb0ff goes 5/7 red.
  • Full suite at head: 1297 pass / 1 fail. The single failure is tests/merge-identity.test.ts ("merges with the dedicated landing credential…"), which fails identically on untouched base 8c666fb — pre-existing, not a hold.
  • tsc --noEmit clean.

Holds: none.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261111T1916Z

LD-VERDICT APPROVED head=e29e447e3dddb6c388bc571f54cbb29328d9d652 session=LR-20261111-017-seatA-20261111T1916Z Judged the diff at the pinned head only (PR head read back = e29e447e3dddb6c388bc571f54cbb29328d9d652, so no mismatch to refuse). Traced all three landing paths from entry to first network call, then ran the suite. What I checked - Merge tool: first call is `getPR(opts.probe ?? c)` (forgejo/index.ts:392) where `probe = gateProbeConfig(configured)` (index.ts:981 → api.ts:112) — built-in DEFAULT_BASE_URL; the pin is paid once, keyed on the SERVER's `pr.base.repo.full_name` (index.ts:410), fail-closed to `gated:true` when the name is unreadable/absent. Every subsequent evidence read uses `tc` (index.ts:437,448,460,480,511,548,565); `GET /user` and the merge POST use `write.config`/`identity.config` (index.ts:1021, 1103; api.ts:1871), so the write cannot leave on another origin. - Census: listing goes through `listPRs(probe, …)` (index.ts:1208) at the built-in base; each row's first read is the same probe via `collectLandingEvidence(..., { probe, transportLog })` (index.ts:1281-1286), the per-row transport is pinned from that row's server-reported name, and a gated/unreadable row never leaves the built-in base. After the row's evidence there is no further network read in the loop (policy + lock are pure). - verdict-cli: first read dialled at `gateProbeConfig({ baseUrl: args.baseUrl, … }).baseUrl` (verdict-cli.ts:498); pin once at :522 on `serverRepoView`'s view; `args.baseUrl` narrowed at :527, so every later read (status/commits/reviews/files/branch_protections), `GET /user` and the formal-review POST inherit the pinned origin. - No caller-keyed path left: `grep` for `pinGatedTransport`/`isGoGatedRepo`/`repoKey` fed from a parsed `repo_ref` or `--repo` finds only comments — the string overload `resolveLandingPolicy(env, repoRef)` (landing.ts:3211) has no tool caller; all four real call sites pass a server-derived `{ref,gated}` view. - Non-gated rows honour `FORGEJO_BASE_URL` only after the probe proved the name non-gated against the sanctioned forge (control test asserts origins [default, default, then env…]). - Logging: name only, once per tool call — the census de-duplicates the line (index.ts:1200-1207), the merge reuses one transport for three stages, and the tests assert exactly one `FORGEJO_BASE_URL is set and ignored` line and that the stub origin string never appears in stderr. Evidence - New tests at head: 19/19 green (landing-gated-transport-pins-f5 9, …-server-keyed-f5b 7, …-census-server-keyed-f5b-r3 3). - Not vacuous: the r3 census test file copied onto 69d9993 goes 2/3 red (first-read-origin assertion red, expected default / received proxy); the f5b server-keyed file on 0ceb0ff goes 5/7 red. - Full suite at head: 1297 pass / 1 fail. The single failure is tests/merge-identity.test.ts ("merges with the dedicated landing credential…"), which fails identically on untouched base 8c666fb — pre-existing, not a hold. - `tsc --noEmit` clean. Holds: none. model: omniroute/deepseek-flash session: LR-20261111-017-seatA-20261111T1916Z
Collaborator

LD-VERDICT APPROVED head=e29e447e3dddb6c388bc571f54cbb29328d9d652 session=LR-20261111-017-seatB-20261111T1916Z

Exact head verified: forgejo_get_pr reports Head sha e29e447e3d (base 8c666fb), and the fetched branch lr-20261111-017-f5 resolves to that same 40-hex sha — matches the pin, so no refusal.

What I checked (fresh read-only clone of the branch, diff 8c666fb..e29e447, sources only):

  1. Merge path, entry → first call. forgejo_merge_pr pins nothing itself: it builds the probe (index.ts:981) and asserts the lock; collectLandingEvidence's first network read is getPR(opts.probe ?? c, …) (index.ts:392), i.e. gateProbeConfig → built-in default. Gating keys on serverRepoView(…, requireReported:true) over pr.base.repo.full_name, the pin is paid after that (index.ts:410) and every later read uses tc = transport.config (index.ts:426): commit status, head date, comments, reviews, inline comments, files, required CI contexts. The write leg reuses lock.transport (index.ts:993): runtime credential, assertMergeIdentity (transport, not a second pin), describeMergeWhitelist, readMergeOutcome (index.ts:1103), and mergeAsLanding→mergePR uses the asserted config, so the single merge POST leaves at the pinned origin. Unreadable/absent server name ⇒ view {ref:undefined, gated:true} and an outright refusal, so nothing further is read.

  2. Census path. First read is the repo listing at the probe (index.ts:1199,1208); each row's own first read is the probe ({ probe, transportLog }, index.ts:1283) and the row's transport is then pinned once from the server's name — gated/unreadable rows never leave the built-in base, non-gated rows get c back. No pre-pin from parsed repo_ref remains.

  3. verdict-cli. First read is the PR fetch at gateProbeConfig's base (verdict-cli.ts:498); server name decides (512); the pin runs once (522) and narrows args.baseUrl (527), so every later read, loginOf's GET /user and the verdict POST (which reads args.baseUrl) use the earned origin.

  4. Grep for caller-keyed gating: pinGatedTransport has exactly three call sites — its definition (api.ts:990), index.ts:410, verdict-cli.ts:522 — plus the assertMergeIdentity default (api.ts:1559) that has no production caller passing neither view nor transport. None is fed by a parsed params.repo_ref or argv --repo before a server-name read. No new caller-keyed path in the diff.

  5. Logging: one line per lever per tool call, by name only, never the value (reportGatedIgnored shared with the policy knobs; census de-dupes in index.ts:1202). Tests assert exactly one FORGEJO_BASE_URL line and that the stub origin never appears in the log.

  6. Red→green, scratch-check: worktree at 69d9993 + the r3 test file copied in ⇒ 2 of 3 red, and the first-read-origin assertion fails exactly as measured: expected 'https://proxy.invalid' to be 'https://forgejo.aaron.reynoza.org'. The new test is not vacuous.

  7. Local suite on the exact head: 1297 passed / 1 failed (60 files). The one failure is tests/merge-identity.test.ts › "merges with the dedicated landing credential, never the operator token", and it fails identically on the untouched base 8c666fb (24/25 there) — pre-existing, not a hold. tsc --noEmit clean.

Holds: none.

Observations (non-blocking, no hold asked):

  • comment-as.ts:442 still takes FORGEJO_BASE_URL straight from env; it is untouched by this diff and is not one of the landing paths F5 names, so it is not a new caller-keyed path — but it is the same class of lever and deserves its own ticket.
  • On NON-gated repos the first read now always dials the built-in default, so a deployment whose only reachable forge is FORGEJO_BASE_URL fails closed. That is the stated F5b design (docs/forgejo.md) and is correct for the gate, but it is an operational consequence worth stating to the operator.
  • I could not confirm a CI run attached to this head: the CI listing for this repo returned unrelated historical runs and no run for lr-20261111-017-f5, so the green-CI leg rests on the local run above (1297 pass, 1 pre-existing failure, tsc clean).

model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261111T1916Z

LD-VERDICT APPROVED head=e29e447e3dddb6c388bc571f54cbb29328d9d652 session=LR-20261111-017-seatB-20261111T1916Z Exact head verified: forgejo_get_pr reports Head sha e29e447e3dddb6c388bc571f54cbb29328d9d652 (base 8c666fb), and the fetched branch lr-20261111-017-f5 resolves to that same 40-hex sha — matches the pin, so no refusal. What I checked (fresh read-only clone of the branch, diff 8c666fb..e29e447, sources only): 1. Merge path, entry → first call. forgejo_merge_pr pins nothing itself: it builds the probe (index.ts:981) and asserts the lock; collectLandingEvidence's first network read is `getPR(opts.probe ?? c, …)` (index.ts:392), i.e. gateProbeConfig → built-in default. Gating keys on serverRepoView(…, requireReported:true) over pr.base.repo.full_name, the pin is paid after that (index.ts:410) and every later read uses `tc = transport.config` (index.ts:426): commit status, head date, comments, reviews, inline comments, files, required CI contexts. The write leg reuses `lock.transport` (index.ts:993): runtime credential, assertMergeIdentity (transport, not a second pin), describeMergeWhitelist, readMergeOutcome (index.ts:1103), and mergeAsLanding→mergePR uses the asserted config, so the single merge POST leaves at the pinned origin. Unreadable/absent server name ⇒ view {ref:undefined, gated:true} and an outright refusal, so nothing further is read. 2. Census path. First read is the repo listing at the probe (index.ts:1199,1208); each row's own first read is the probe (`{ probe, transportLog }`, index.ts:1283) and the row's transport is then pinned once from the server's name — gated/unreadable rows never leave the built-in base, non-gated rows get `c` back. No pre-pin from parsed repo_ref remains. 3. verdict-cli. First read is the PR fetch at gateProbeConfig's base (verdict-cli.ts:498); server name decides (512); the pin runs once (522) and narrows args.baseUrl (527), so every later read, loginOf's GET /user and the verdict POST (which reads args.baseUrl) use the earned origin. 4. Grep for caller-keyed gating: pinGatedTransport has exactly three call sites — its definition (api.ts:990), index.ts:410, verdict-cli.ts:522 — plus the assertMergeIdentity default (api.ts:1559) that has no production caller passing neither view nor transport. None is fed by a parsed params.repo_ref or argv --repo before a server-name read. No new caller-keyed path in the diff. 5. Logging: one line per lever per tool call, by name only, never the value (reportGatedIgnored shared with the policy knobs; census de-dupes in index.ts:1202). Tests assert exactly one FORGEJO_BASE_URL line and that the stub origin never appears in the log. 6. Red→green, scratch-check: worktree at 69d9993 + the r3 test file copied in ⇒ 2 of 3 red, and the first-read-origin assertion fails exactly as measured: `expected 'https://proxy.invalid' to be 'https://forgejo.aaron.reynoza.org'`. The new test is not vacuous. 7. Local suite on the exact head: 1297 passed / 1 failed (60 files). The one failure is tests/merge-identity.test.ts › "merges with the dedicated landing credential, never the operator token", and it fails identically on the untouched base 8c666fb (24/25 there) — pre-existing, not a hold. tsc --noEmit clean. Holds: none. Observations (non-blocking, no hold asked): - comment-as.ts:442 still takes FORGEJO_BASE_URL straight from env; it is untouched by this diff and is not one of the landing paths F5 names, so it is not a new caller-keyed path — but it is the same class of lever and deserves its own ticket. - On NON-gated repos the first read now always dials the built-in default, so a deployment whose only reachable forge is FORGEJO_BASE_URL fails closed. That is the stated F5b design (docs/forgejo.md) and is correct for the gate, but it is an operational consequence worth stating to the operator. - I could not confirm a CI run attached to this head: the CI listing for this repo returned unrelated historical runs and no run for lr-20261111-017-f5, so the green-CI leg rests on the local run above (1297 pass, 1 pre-existing failure, tsc clean). model: omniroute/deepseek-flash session: LR-20261111-017-seatB-20261111T1916Z
fix(forgejo): send the landing credential only to the built-in default origin, and refuse a TLS/proxy interposed process on gated landings (LR-20261111-017, homelab-93 B1+S2 r3)
All checks were successful
CI / pr-title (pull_request) Successful in 1s
CI / forgejo-ci (pull_request) Successful in 1m12s
CI / pi-otel-telemetry (pull_request) Successful in 1m25s
CI / plane (pull_request) Successful in 1m28s
CI / skill-args (pull_request) Successful in 54s
CI / ask-user-question (pull_request) Successful in 53s
tests / python (pull_request) Successful in 6s
CI / refocus (pull_request) Successful in 1m1s
CI / mcp-bridge (pull_request) Successful in 23s
CI / todo (pull_request) Successful in 1m18s
CI / redact (pull_request) Successful in 28s
CI / nats-intercom (pull_request) Successful in 30s
CI / pi-friction (pull_request) Successful in 43s
CI / forgejo (pull_request) Successful in 2m22s
CI / pi-swarm-suite (pull_request) Successful in 4m31s
561c545121
B1 (BLOCKING). F5/F5b pinned the base and keyed the gate on the SERVER's name,
which closed the fabricated-record half for GATED repos. The token half stayed
open: off the gated list the merge tool kept the caller's config, so
`resolveLandingCredentialAtRuntime` built the landing credential ON that base and
BOTH credential-bearing calls went there — the `GET /user` identity assertion and
the merge POST, with the staged pi-landing token in `Authorization`.

No fake repo is needed to exploit it. Set `FORGEJO_BASE_URL=<proxy>` and call
`forgejo_merge_pr` on any REAL non-gated PR: the probe at the sanctioned forge
honestly answers non-gated, the proxy answers the remaining reads and receives
the merge POST with a token it can replay as
`POST /repos/Homelab/prod/pulls/N/merge` — a GATED PR merged with no lock, no
seats and no GO record. A credential is the one byte stream that must never
follow a session-settable address: reads can be re-derived, a relayed merge
cannot be un-merged.

Fix, at the transport boundary: `assertMergeIdentity` builds the
credential-carrying config with `baseUrl: DEFAULT_BASE_URL`, the constant, full
stop — on every repo, gated or not. `mergeAsLanding` takes the write credential
from that same proof config, so the POST follows. An env/flag base that differs
is not consulted for those two calls (on a gated repo the ignored lever is still
logged once, by name). Read-only evidence traffic on a proven non-gated repo
keeps honouring `FORGEJO_BASE_URL`, as documented.

S2 (same round, cheap). A gated landing also refuses a process-level
interposition — one line, before any further read, identity check or write:
`NODE_TLS_REJECT_UNAUTHORIZED=0`, `NODE_EXTRA_CA_CERTS` set, and `NODE_OPTIONS`
carrying `--require`, `--use-openssl-ca`, `--use-bundled-ca` or
`--use-env-proxy`. A pinned URL is only as good as the host and the CA that
answer it, and a session-set value is not a gate (B3/N6 class). `--require`
cannot be defended in-process at all, which is why the answer is a refusal; the
real fix for that residual is F1 (env not agent-settable). The refusal runs
inside `pinGatedTransport`'s gated branch, i.e. immediately after the one probe
that decides gated-ness and before any other request leaves. Non-gated calls are
unchanged.

Tests (red -> green, measured by stashing api.ts/index.ts):
- tests/landing-credential-default-origin-b1.test.ts (2, new): the relay probe —
  stub base + non-gated repo + staged token: the stub serves the reads and sees
  NO `GET /user`, NO merge POST and no call carrying the landing token, while the
  default origin sees both; a gated repo under the same env keeps every request on
  the default and still logs the lever once, by name.
- tests/landing-gated-process-env-s2.test.ts (8, new): each of the seven
  interpositions refuses on a gated repo with ONE line naming the variable (never
  the CA path), with no identity read, no POST and only the probe on the wire; a
  non-gated repo keeps today's behaviour under the same env.
- tests/landing-gated-transport-server-keyed-f5b.test.ts (flipped control) and
  tests/landing-gated-transport-pins-f5.test.ts (flipped control): the token used
  to be asserted AT the proxy/stub; both now assert the built-in default for
  `GET /user` and the POST.
  Without the source fix: 11 failed / 15 passed across those four files.

Full suite: Test Files 1 failed | 61 passed (62); Tests 1 failed | 1307 passed
(1308) — the single failure is the known pre-existing merge-identity ambient
`FORGEJO_LANDING_TOKEN_FILE` test. `tsc --noEmit` clean.

Docs: docs/forgejo.md states the credential rule on every repo and the S2 refusal
(and no longer says the non-gated write honours the env base).

Lab-Run: LR-20261111-017
fix(forgejo): refuse --import and --use-system-ca in NODE_OPTIONS on a gated landing (LR-20261111-017, homelab-93 S2 round 3b)
All checks were successful
CI / pr-title (pull_request) Successful in 1s
CI / mcp-bridge (pull_request) Successful in 41s
CI / forgejo-ci (pull_request) Successful in 51s
tests / python (pull_request) Successful in 6s
CI / skill-args (pull_request) Successful in 53s
CI / refocus (pull_request) Successful in 56s
CI / nats-intercom (pull_request) Successful in 19s
CI / pi-otel-telemetry (pull_request) Successful in 1m12s
CI / ask-user-question (pull_request) Successful in 1m14s
CI / todo (pull_request) Successful in 1m16s
CI / redact (pull_request) Successful in 50s
CI / pi-friction (pull_request) Successful in 56s
CI / plane (pull_request) Successful in 1m44s
CI / forgejo (pull_request) Successful in 2m26s
CI / pi-swarm-suite (pull_request) Successful in 4m5s
6835d3009f
`GATED_NODE_OPTION_FLAGS` listed `--require` but not `--import`, so a session
could set `NODE_OPTIONS="--import /tmp/h.mjs"` and run an ESM loader hook in
this process: the S2 gated-landing refusal never fired and the hook executed
(reproduced live on Node v22.23.4 by the reviewer). An ESM loader hook is the
same lever as `--require`, and `--use-system-ca` is the same lever as the other
`--use-*-ca` flags — both were missing from the refused set and fully bypassed
the refusal.

Add both, with the `--import` / `--use-system-ca` consequences named (code
loader, CA store) so the one-line refusal still says what the flag DID. The
existing boundary-aware match `(^|\s)flag(=|\s|$)` is kept: `--import /tmp/h.mjs`
and `--import=…` refuse, `--importx` / `--use-system-cax` do not.

Tests: the parameterized S2 table gains three gated cases (`--import <path>`,
`--import=<path>`, `--use-system-ca`) plus a boundary case proving a longer flag
that only starts with a refused one is not refused; the non-gated case now
carries all the new flags too and stays unchanged. docs/forgejo.md enumerates the
flags and is updated. Red→green verified by stashing forgejo/api.ts.

Lab-Run: LR-20261111-017
Collaborator

LD-VERDICT REQUEST_CHANGES head=6835d3009f4a37eaabf1fd452dfe341864f65835 session=LR-20261111-017-seatA-20261111T2136Z

Head confirmed exact (6835d3009f, base 8c666fbec4, branch lr-20261111-017-f5); no refusal needed. Judged the full base..head diff, with the walk concentrated on 561c545 and 6835d30.

What I checked (measured, not read off the description):

  1. Credential path. assertMergeIdentity builds the credential-carrying config from the CONSTANT DEFAULT_BASE_URL (forgejo/api.ts:1690) on every repo, gated or not; the proof carries that config non-enumerably and mergeAsLanding POSTs to identity.config (forgejo/api.ts:1880), so GET /user and the merge POST are the same origin. The relay test (tests/landing-credential-default-origin-b1.test.ts) really measures it: the stub origin holds a complete mergeable record yet sees zero GET /user, zero merge POST and zero requests carrying the landing token, while the built-in origin sees exactly one of each. Both flipped controls now assert the token at the built-in default.
  2. S2 refusal placement. Fires inside pinGatedTransport's gated branch (forgejo/api.ts:1100), i.e. after the one probe that decides gated-ness and before any further read, identity check or write; refusal is one line, names the variable, never the value; exactly one request leaves (the probe) on each of the 11 gated cases, and the non-gated control is unchanged.
  3. Flag set and matching. --import and --use-system-ca are in GATED_NODE_OPTION_FLAGS (forgejo/api.ts:118-125) with consequence text; boundary regex (^|\s)flag(=|\s|$) means --import /tmp/h.mjs and --import=… refuse while --importx / --use-system-cax do not.
  4. Red→green, re-measured by stashing: with forgejo/api.ts at 561c545, 4 of the 12 S2 tests fail (the three new gated flag cases plus the boundary case) and 8 pass; at head all 12 pass. With api.ts+index.ts at e29e447, 15 of 30 tests across the four files fail (relay probe, 13 S2, both flipped controls) and 15 pass.
  5. Untouched surface. git diff --name-status 8c666fb..6835d30 does not list forgejo/seat-post.ts or forgejo/comment-as.ts — that S1 stays a named follow-up.
  6. Suite/typecheck. vitest run: 62 files, 61 pass; 1311 tests pass, 1 fail — tests/merge-identity.test.ts "merges with the dedicated landing credential", the ambient FORGEJO_LANDING_TOKEN_FILE case, reproduced at base 8c666fb and passing with that env unset, so pre-existing and not a hold. tsc --noEmit clean.

HOLD (1)

  1. forgejo/api.ts:118 (GATED_NODE_OPTION_FLAGS) — the ESM loader lever is only half closed. --loader and --experimental-loader are Node-accepted in NODE_OPTIONS (Node v22.23.4, measured here) and gatedProcessInterposition returns undefined for both (forgejo/api.ts:1023), so the S2 refusal never fires for them. Measured end to end: with NODE_OPTIONS="--experimental-loader=<hook>" a session hook rewrote forgejo/api.ts at load time and a gated pinGatedTransport returned normally (base = the session's origin, identity pi-landing) — the base pin and the refusal both gone — whereas the same call with --require throws GatedProcessEnvRefusal. That is exactly the lever 6835d30 adds --import for ("an ESM loader hook is the same lever as a CJS one"), one alias away, and it defeats the F5 pin itself, not only S2. Fix: add --loader and --experimental-loader to the refused set with a test row each and update docs/forgejo.md:832-833; better, so this alias hunt does not run a fourth round, make a gated call default-deny NODE_OPTIONS and allow only the benign flags it knowingly accepts (--max-old-space-size etc., the boundary test already covers that shape).

Notes, not holds: --openssl-config is also accepted in NODE_OPTIONS and is not listed (weaker, CA/provider surface) — worth folding into the same pass. describeMergeWhitelist(write.config, …) is a read on the env base with the operator credential, but it is read-only and only on an identity-mismatch refusal path, so I do not count it against the credential-origin rule.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261111T2136Z

LD-VERDICT REQUEST_CHANGES head=6835d3009f4a37eaabf1fd452dfe341864f65835 session=LR-20261111-017-seatA-20261111T2136Z Head confirmed exact (6835d3009f4a37eaabf1fd452dfe341864f65835, base 8c666fbec432f70b4853124f8de6ee2e445d1339, branch lr-20261111-017-f5); no refusal needed. Judged the full base..head diff, with the walk concentrated on 561c545 and 6835d30. What I checked (measured, not read off the description): 1. Credential path. `assertMergeIdentity` builds the credential-carrying config from the CONSTANT `DEFAULT_BASE_URL` (forgejo/api.ts:1690) on every repo, gated or not; the proof carries that config non-enumerably and `mergeAsLanding` POSTs to `identity.config` (forgejo/api.ts:1880), so `GET /user` and the merge POST are the same origin. The relay test (tests/landing-credential-default-origin-b1.test.ts) really measures it: the stub origin holds a complete mergeable record yet sees zero `GET /user`, zero merge POST and zero requests carrying the landing token, while the built-in origin sees exactly one of each. Both flipped controls now assert the token at the built-in default. 2. S2 refusal placement. Fires inside `pinGatedTransport`'s gated branch (forgejo/api.ts:1100), i.e. after the one probe that decides gated-ness and before any further read, identity check or write; refusal is one line, names the variable, never the value; exactly one request leaves (the probe) on each of the 11 gated cases, and the non-gated control is unchanged. 3. Flag set and matching. `--import` and `--use-system-ca` are in `GATED_NODE_OPTION_FLAGS` (forgejo/api.ts:118-125) with consequence text; boundary regex `(^|\s)flag(=|\s|$)` means `--import /tmp/h.mjs` and `--import=…` refuse while `--importx` / `--use-system-cax` do not. 4. Red→green, re-measured by stashing: with forgejo/api.ts at 561c545, 4 of the 12 S2 tests fail (the three new gated flag cases plus the boundary case) and 8 pass; at head all 12 pass. With api.ts+index.ts at e29e447, 15 of 30 tests across the four files fail (relay probe, 13 S2, both flipped controls) and 15 pass. 5. Untouched surface. `git diff --name-status 8c666fb..6835d30` does not list forgejo/seat-post.ts or forgejo/comment-as.ts — that S1 stays a named follow-up. 6. Suite/typecheck. `vitest run`: 62 files, 61 pass; 1311 tests pass, 1 fail — tests/merge-identity.test.ts "merges with the dedicated landing credential", the ambient `FORGEJO_LANDING_TOKEN_FILE` case, reproduced at base 8c666fb and passing with that env unset, so pre-existing and not a hold. `tsc --noEmit` clean. HOLD (1) 1. forgejo/api.ts:118 (`GATED_NODE_OPTION_FLAGS`) — the ESM loader lever is only half closed. `--loader` and `--experimental-loader` are Node-accepted in `NODE_OPTIONS` (Node v22.23.4, measured here) and `gatedProcessInterposition` returns `undefined` for both (forgejo/api.ts:1023), so the S2 refusal never fires for them. Measured end to end: with `NODE_OPTIONS="--experimental-loader=<hook>"` a session hook rewrote forgejo/api.ts at load time and a gated `pinGatedTransport` returned normally (base = the session's origin, identity `pi-landing`) — the base pin and the refusal both gone — whereas the same call with `--require` throws `GatedProcessEnvRefusal`. That is exactly the lever 6835d30 adds `--import` for ("an ESM loader hook is the same lever as a CJS one"), one alias away, and it defeats the F5 pin itself, not only S2. Fix: add `--loader` and `--experimental-loader` to the refused set with a test row each and update docs/forgejo.md:832-833; better, so this alias hunt does not run a fourth round, make a gated call default-deny `NODE_OPTIONS` and allow only the benign flags it knowingly accepts (`--max-old-space-size` etc., the boundary test already covers that shape). Notes, not holds: `--openssl-config` is also accepted in `NODE_OPTIONS` and is not listed (weaker, CA/provider surface) — worth folding into the same pass. `describeMergeWhitelist(write.config, …)` is a read on the env base with the operator credential, but it is read-only and only on an identity-mismatch refusal path, so I do not count it against the credential-origin rule. model: omniroute/deepseek-flash session: LR-20261111-017-seatA-20261111T2136Z
Collaborator

LD-VERDICT APPROVED head=6835d3009f4a37eaabf1fd452dfe341864f65835 session=LR-20261111-017-seatB-20261111T2136Z

Reviewed the full base..head diff (8c666fb..6835d30, 11 files) with focus on 561c545 and 6835d30, read-only clone, no other seat's verdict consulted.

(1) Credential-carrying config — traced. assertMergeIdentity builds asserted with baseUrl: DEFAULT_BASE_URL and dials GET /user on asserted.config (api.ts:1688-1697); the proof carries that same config (api.ts:1734) and mergeAsLanding POSTs with identity.config (api.ts:1697→mergePR), so the identity GET and the merge POST are on the built-in origin on every repo, gated or not. ForgejoConfig is only {baseUrl, apiKey} (types.ts:8-11), so overriding baseUrl fully pins the origin; no other field can carry a session-chosen transport. credential.config reaches the wire nowhere else (grep: api.ts:1666 pinning input, api.ts:1690 the override). Off the gated list only read-only traffic follows FORGEJO_BASE_URL — as documented. The new relay test measures it rather than asserting intent: the two origins are separate route tables, the proxy holds a complete mergeable record, and it must see zero identity reads, zero merge POSTs and zero landing-token requests (tests/landing-credential-default-origin-b1.test.ts:157-192), with the gated control still logging the lever once by name (lines 195-206). Independent red→green: with api.ts/index.ts at e29e447 the relay probe fails (1 failed | 1 passed); at head 2/2 pass. merge-identity.test.ts's single failure reproduces with the base api.ts/index.ts/landing.ts/verdict-cli.ts checked out, so it is pre-existing, not a hold.

(2) S2 refusals — fire on gated landings, after the ONE probe and before anything else: assertGatedProcessTrust is called inside pinGatedTransport's gated branch (api.ts:1101) before reportGatedIgnored, and the merge path reaches it from collectLandingEvidence (index.ts:409) right after the gateProbeConfig PR read (index.ts:395). Each refusal is one line naming the variable and no value (api.ts:1007-1062); the S2 test proves the message has one line, omits the CA path and the token, and that the only request on either origin is the probe (tests/landing-gated-process-env-s2.test.ts:186-204). Flag list at head: --require, --import, --use-openssl-ca, --use-bundled-ca, --use-system-ca, --use-env-proxy (api.ts:118-125); matching is boundary-anchored (^|\s)flag(=|\s|$) (api.ts:1023-1025), and --importx / --use-system-cax / --max-old-space-size do not refuse (test lines 233-241). Non-gated behaviour unchanged under all the new flags (test lines 209-230).

(3) Red→green for the round-3b cases — checked by checkout, not by commit message: with api.ts at 561c545 the S2 file is 4 failed | 8 passed (both --import spellings, --use-system-ca, and the boundary case), and at head 12/12.

(4) seat-post.ts and comment-as.ts are untouched across base..head: git diff --stat 8c666fb...6835d30 for both files is empty and no commit in the range touches them.

Local runs: the four focus files 30/30 green at head (S2 12, B1 2, F5 9, F5b 7); full vitest run 1 failed | 1311 passed, the one failure being the pre-existing merge-identity ambient-token case; tsc --noEmit exit 0.

No holds.

Non-blocking notes (same class, named follow-ups, none of them a reason to withhold this change):

  • NODE_OPTIONS matching is prefix-complete but not spelling-complete. Measured on the runtime in use (Node v22.23.4): NODE_OPTIONS="-r /tmp/h.cjs" executes code and NODE_OPTIONS="--experimental-loader /tmp/l.mjs" is accepted and loads an ESM loader hook, and neither is in GATED_NODE_OPTION_FLAGS (api.ts:118-125), so S2 passes while the loader runs. Adding -r and --experimental-loader is cheap; the honest fix for the whole class stays F1 (env not session-settable), as the code itself says.
  • The verdict POST in verdict-cli still follows args.baseUrl after the pin (verdict-cli.ts:527), i.e. on a proven non-gated repo the reviewer credential can be sent to a session-set origin — the B1 doctrine, deferred by the named S1 follow-up alongside seat-post.ts/comment-as.ts. Not introduced or worsened by these two commits.
  • readMergeOutcome reads the merged sha back with the ordinary read key at the configured base (index.ts:986): read-only, documented override behaviour, cannot un-merge anything.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261111T2136Z

LD-VERDICT APPROVED head=6835d3009f4a37eaabf1fd452dfe341864f65835 session=LR-20261111-017-seatB-20261111T2136Z Reviewed the full base..head diff (8c666fb..6835d30, 11 files) with focus on 561c545 and 6835d30, read-only clone, no other seat's verdict consulted. (1) Credential-carrying config — traced. `assertMergeIdentity` builds `asserted` with `baseUrl: DEFAULT_BASE_URL` and dials `GET /user` on `asserted.config` (api.ts:1688-1697); the proof carries that same config (api.ts:1734) and `mergeAsLanding` POSTs with `identity.config` (api.ts:1697→mergePR), so the identity GET and the merge POST are on the built-in origin on every repo, gated or not. `ForgejoConfig` is only `{baseUrl, apiKey}` (types.ts:8-11), so overriding baseUrl fully pins the origin; no other field can carry a session-chosen transport. `credential.config` reaches the wire nowhere else (grep: api.ts:1666 pinning input, api.ts:1690 the override). Off the gated list only read-only traffic follows `FORGEJO_BASE_URL` — as documented. The new relay test measures it rather than asserting intent: the two origins are separate route tables, the proxy holds a complete mergeable record, and it must see zero identity reads, zero merge POSTs and zero landing-token requests (tests/landing-credential-default-origin-b1.test.ts:157-192), with the gated control still logging the lever once by name (lines 195-206). Independent red→green: with api.ts/index.ts at e29e447 the relay probe fails (1 failed | 1 passed); at head 2/2 pass. merge-identity.test.ts's single failure reproduces with the base api.ts/index.ts/landing.ts/verdict-cli.ts checked out, so it is pre-existing, not a hold. (2) S2 refusals — fire on gated landings, after the ONE probe and before anything else: `assertGatedProcessTrust` is called inside `pinGatedTransport`'s gated branch (api.ts:1101) before `reportGatedIgnored`, and the merge path reaches it from `collectLandingEvidence` (index.ts:409) right after the `gateProbeConfig` PR read (index.ts:395). Each refusal is one line naming the variable and no value (api.ts:1007-1062); the S2 test proves the message has one line, omits the CA path and the token, and that the only request on either origin is the probe (tests/landing-gated-process-env-s2.test.ts:186-204). Flag list at head: --require, --import, --use-openssl-ca, --use-bundled-ca, --use-system-ca, --use-env-proxy (api.ts:118-125); matching is boundary-anchored `(^|\s)flag(=|\s|$)` (api.ts:1023-1025), and --importx / --use-system-cax / --max-old-space-size do not refuse (test lines 233-241). Non-gated behaviour unchanged under all the new flags (test lines 209-230). (3) Red→green for the round-3b cases — checked by checkout, not by commit message: with api.ts at 561c545 the S2 file is 4 failed | 8 passed (both --import spellings, --use-system-ca, and the boundary case), and at head 12/12. (4) seat-post.ts and comment-as.ts are untouched across base..head: `git diff --stat 8c666fb...6835d30` for both files is empty and no commit in the range touches them. Local runs: the four focus files 30/30 green at head (S2 12, B1 2, F5 9, F5b 7); full `vitest run` 1 failed | 1311 passed, the one failure being the pre-existing merge-identity ambient-token case; `tsc --noEmit` exit 0. No holds. Non-blocking notes (same class, named follow-ups, none of them a reason to withhold this change): - NODE_OPTIONS matching is prefix-complete but not spelling-complete. Measured on the runtime in use (Node v22.23.4): `NODE_OPTIONS="-r /tmp/h.cjs"` executes code and `NODE_OPTIONS="--experimental-loader /tmp/l.mjs"` is accepted and loads an ESM loader hook, and neither is in GATED_NODE_OPTION_FLAGS (api.ts:118-125), so S2 passes while the loader runs. Adding `-r` and `--experimental-loader` is cheap; the honest fix for the whole class stays F1 (env not session-settable), as the code itself says. - The verdict POST in verdict-cli still follows `args.baseUrl` after the pin (verdict-cli.ts:527), i.e. on a proven non-gated repo the reviewer credential can be sent to a session-set origin — the B1 doctrine, deferred by the named S1 follow-up alongside seat-post.ts/comment-as.ts. Not introduced or worsened by these two commits. - `readMergeOutcome` reads the merged sha back with the ordinary read key at the configured base (index.ts:986): read-only, documented override behaviour, cannot un-merge anything. model: omniroute/deepseek-flash session: LR-20261111-017-seatB-20261111T2136Z
fix(forgejo): default-deny NODE_OPTIONS on a gated landing (LR-20261111-017, homelab-93 S2 round 4)
All checks were successful
CI / pr-title (pull_request) Successful in 3s
CI / nats-intercom (pull_request) Successful in 37s
CI / mcp-bridge (pull_request) Successful in 39s
CI / skill-args (pull_request) Successful in 59s
tests / python (pull_request) Successful in 9s
CI / pi-otel-telemetry (pull_request) Successful in 1m11s
CI / todo (pull_request) Successful in 1m8s
CI / refocus (pull_request) Successful in 1m4s
CI / ask-user-question (pull_request) Successful in 56s
CI / forgejo-ci (pull_request) Successful in 1m20s
CI / plane (pull_request) Successful in 1m13s
CI / pi-friction (pull_request) Successful in 56s
CI / redact (pull_request) Successful in 47s
CI / forgejo (pull_request) Successful in 2m55s
CI / pi-swarm-suite (pull_request) Successful in 4m17s
a6d13fb475
The refusal has been an allow-alias hunt for four rounds: `--require`,
`--import`, the `--use-*-ca` set and `--use-env-proxy` were each added after a
seat measured the previous gap. Round 4 measured the next one on Node v22.23.4:
`NODE_OPTIONS="--experimental-loader=<hook>"` installs a session ESM loader hook
that REWROTE forgejo/api.ts at load time, so a gated `pinGatedTransport`
returned normally with neither the base pin nor the refusal in it. `--loader`
is the same flag under its shorter name and `--openssl-config` is the unlisted
TLS-stack alias queued behind it. Naming aliases cannot converge.

Invert the check. A gated landing now refuses NODE_OPTIONS unless EVERY flag it
carries is on a small benign allow-list — `--max-old-space-size`,
`--max-semi-space-size`, `--stack-size` (V8 memory/stack tuning),
`--trace-warnings`, `--no-warnings` (stderr diagnostics). None of them can name
code, a module, a loader or a CA store, which is the property that lets the list
stay small. Flag names are read whole on the existing boundary convention (split
on whitespace, name up to the first `=`, exact match), so `--max-old-space-sizex`
is refused as the unknown flag it is and a longer near-name can never widen the
list. The one refusal line names the offending flag and never its value.

Unknown future loader/TLS flags therefore refuse by default. The six previously
listed flags still refuse — they are simply no longer named, because being off
the benign list is now the whole reason. Non-gated calls are untouched: the
check only runs on the gated branch.

Tests: a gated row each for the six + `--loader`, `--experimental-loader`,
`--openssl-config` (both `=` and space forms for the loaders) and one alias no
round has measured; a pure-whitelist value passes and the merge completes; a
mixed value refuses naming `--require`; whole-name matching cases; the non-gated
case now carries the loader and openssl flags too and stays unchanged.
docs/forgejo.md describes the default-deny + allow-list. Red→green verified by
stashing forgejo/api.ts only: 7 of the 19 rows fail on 6835d30's check
(`--experimental-loader`, `--loader`, `--openssl-config`, the unknown alias and
the inversion cases), all 19 pass after.

Lab-Run: LR-20261111-017
Collaborator

LD-VERDICT REQUEST_CHANGES head=a6d13fb475e939ef0cc281a2ab142f2be6f06f72 session=LR-20261111-017-seatB-20261111T2146Z

Checked the full base (8c666fb)..head diff at a6d13fb, and the a6d13fb delta on its own (fresh read-only clone, branch lr-20261111-017-f5).

What holds up:

  • The allow-list is exactly the five benign flags (--max-old-space-size, --max-semi-space-size, --stack-size, --trace-warnings, --no-warnings), each with its own justification, and none can name code, a module, a loader or a CA store.
  • Name parsing is whole-name up to =: --max-old-space-sizex=4096, --no-warningsx, --importx=..., --use-system-cax all refuse and name the token they refused; nothing widens the list by prefix.
  • Every dangerous name refuses at head: --require, --import, --use-openssl-ca, --use-bundled-ca, --use-system-ca, --use-env-proxy, --loader, --experimental-loader, --openssl-config, and an unmeasured alias. A pure-whitelist value passes and the merge completes with no request touching a proxy origin; a mixed value refuses naming --require and not its value; blank/unset is not a value; the non-gated row (now carrying loader/openssl flags too) stays unchanged.
  • Red→green is as claimed: with 6835d30's forgejo/api.ts swapped in, 7 of 19 rows in tests/landing-gated-process-env-s2.test.ts fail (both loader forms, --openssl-config, the unknown alias, and the three inversion rows); all 19 pass at head.
  • Full suite: 61 files, 1318 pass; the single failure is the known pre-existing merge-identity ambient-token case (base behaviour, not a hold).
  • Scope: docs/forgejo.md, forgejo/api.ts and that one test file. seat-post.ts and comment-as.ts untouched.

HOLDS

  1. forgejo/api.ts:166 — the short form of a banned flag walks straight past the default-deny. nodeOptionFlagNames keeps only tokens beginning with --, and the comment at forgejo/api.ts:156-160 justifies that with a premise that is false on the runtime this ships on, Node v22.23.4: NODE_OPTIONS="-r /tmp/h_r.js" node -e ... was accepted and the file ran (its output appeared before the body's). Measured against this head: gatedProcessInterposition({NODE_OPTIONS:"-r /tmp/hook.js"}) returns undefined, and so does "--max-old-space-size=4096 -r /tmp/hook.js" — i.e. a gated landing does its reads, its GET /user and its merge POST with session-chosen code already in the process. That is exactly the lever --require is on the list to stop and the same load-time-rewrite class this round set out to close structurally, so "any flag outside the list refuses" is not yet true. (-r=<path> is rejected by Node itself; the space form is the live one.) Fix direction: read any token beginning with - as a flag name — or refuse single-dash tokens outright — and add -r to the consequence map, plus a -r /tmp/hook.js row and a mixed-value row in tests/landing-gated-process-env-s2.test.ts.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261111T2146Z

LD-VERDICT REQUEST_CHANGES head=a6d13fb475e939ef0cc281a2ab142f2be6f06f72 session=LR-20261111-017-seatB-20261111T2146Z Checked the full base (8c666fb)..head diff at a6d13fb, and the a6d13fb delta on its own (fresh read-only clone, branch lr-20261111-017-f5). What holds up: - The allow-list is exactly the five benign flags (`--max-old-space-size`, `--max-semi-space-size`, `--stack-size`, `--trace-warnings`, `--no-warnings`), each with its own justification, and none can name code, a module, a loader or a CA store. - Name parsing is whole-name up to `=`: `--max-old-space-sizex=4096`, `--no-warningsx`, `--importx=...`, `--use-system-cax` all refuse and name the token they refused; nothing widens the list by prefix. - Every dangerous name refuses at head: `--require`, `--import`, `--use-openssl-ca`, `--use-bundled-ca`, `--use-system-ca`, `--use-env-proxy`, `--loader`, `--experimental-loader`, `--openssl-config`, and an unmeasured alias. A pure-whitelist value passes and the merge completes with no request touching a proxy origin; a mixed value refuses naming `--require` and not its value; blank/unset is not a value; the non-gated row (now carrying loader/openssl flags too) stays unchanged. - Red→green is as claimed: with 6835d30's `forgejo/api.ts` swapped in, 7 of 19 rows in tests/landing-gated-process-env-s2.test.ts fail (both loader forms, `--openssl-config`, the unknown alias, and the three inversion rows); all 19 pass at head. - Full suite: 61 files, 1318 pass; the single failure is the known pre-existing merge-identity ambient-token case (base behaviour, not a hold). - Scope: docs/forgejo.md, forgejo/api.ts and that one test file. seat-post.ts and comment-as.ts untouched. HOLDS 1. forgejo/api.ts:166 — the short form of a banned flag walks straight past the default-deny. `nodeOptionFlagNames` keeps only tokens beginning with `--`, and the comment at forgejo/api.ts:156-160 justifies that with a premise that is false on the runtime this ships on, Node v22.23.4: `NODE_OPTIONS="-r /tmp/h_r.js" node -e ...` was accepted and the file ran (its output appeared before the body's). Measured against this head: `gatedProcessInterposition({NODE_OPTIONS:"-r /tmp/hook.js"})` returns undefined, and so does `"--max-old-space-size=4096 -r /tmp/hook.js"` — i.e. a gated landing does its reads, its `GET /user` and its merge POST with session-chosen code already in the process. That is exactly the lever `--require` is on the list to stop and the same load-time-rewrite class this round set out to close structurally, so "any flag outside the list refuses" is not yet true. (`-r=<path>` is rejected by Node itself; the space form is the live one.) Fix direction: read any token beginning with `-` as a flag name — or refuse single-dash tokens outright — and add `-r` to the consequence map, plus a `-r /tmp/hook.js` row and a mixed-value row in tests/landing-gated-process-env-s2.test.ts. model: omniroute/deepseek-flash session: LR-20261111-017-seatB-20261111T2146Z
Collaborator

LD-VERDICT REQUEST_CHANGES head=a6d13fb475e939ef0cc281a2ab142f2be6f06f72 session=LR-20261111-017-seatA-20261111T2146Z

Reviewed a clean clone at that exact head (PR head confirmed = a6d13fb), full base..head diff plus the round-4 delta, with independent probes of gatedProcessInterposition on head and on 6835d30. Suite: npm ci, tsc --noEmit clean, vitest run 1318 pass / 1 fail — the failure is tests/merge-identity.test.ts (ambient-token case), identical on 6835d30, pre-existing, not a hold.

Verified good:

  • The allow-list is exactly the five flags with per-flag justifications (forgejo/api.ts:129-135, comment 106-126), and flag names are read whole up to = (forgejo/api.ts:163-167): --max-old-space-sizex, --no-warningsx, --stack-sizes all refuse, so a near-name cannot widen the list.
  • Every dangerous row refuses on a gated landing at head: --require (bare, =, space), --import, --loader (both forms), --experimental-loader (both forms), --openssl-config, --use-openssl-ca, --use-bundled-ca, --use-system-ca, --use-env-proxy, and an unmeasured alias. On 6835d30 the same probe lets --loader, --experimental-loader, --openssl-config and the unknown alias through; a6d13fb's test file against 6835d30's api.ts gives 7 failed / 12 passed of 19 — red→green reproduced independently, matching the commit message.
  • A pure-allow-list value passes and the merge completes; a mixed value refuses naming --require; no refusal line echoes a value (checked every line for path leaks).
  • Non-gated unchanged: the non-gated branch returns before assertGatedProcessTrust (forgejo/api.ts:1149-1156); the non-gated control test now carries the loader/openssl flags and still passes.
  • Nothing else moved in a6d13fb (api.ts, its test, docs/forgejo.md only); seat-post.ts and comment-as.ts are absent from the full base..head diff.

Holds:

  1. forgejo/api.ts:166 — nodeOptionFlagNames keeps only tokens that start with --, so default-deny never sees a single-dash short flag, and Node v22.23.4 accepts -r <file> in NODE_OPTIONS. Measured on this box: NODE_OPTIONS="-r /tmp/hook.cjs" node -e ... runs the CJS hook in-process, and a hook that replaced globalThis.fetch intercepted the pinned default origin (HOOK-INTERCEPTED https://forgejo.aaron.reynoza.org/api/v1/user). gatedProcessInterposition({NODE_OPTIONS:"-r /tmp/hook.cjs"}) is undefined on head (and on 6835d30), and the mixed --no-warnings -r <file> form passes too — so a gated landing still proceeds with session code loaded, the same lever as --require, which api.ts:1105 states "cannot be defended in-process at all". Node's own process.allowedNodeEnvironmentFlags lists exactly two short forms, -r and -C, and -r loads code. Minimal fix in the existing intent: refuse ANY token beginning with - whose name is not on BENIGN_NODE_OPTION_FLAGS (true default-deny — this also covers -C), optionally cross-checked against process.allowedNodeEnvironmentFlags; add a gated row for -r <file> and for the mixed form that fails on 6835d30.

Nit, explicitly not a hold:
2. forgejo/api.ts:122 and :132 — --stack-size is not an allowable NODE_OPTIONS flag at all on v22.23.4 (node: --stack-size is not allowed in NODE_OPTIONS; process.allowedNodeEnvironmentFlags.has("--stack-size") is false), so that entry is inert and its "V8 stack limit / worst case a crash" justification describes a surface Node does not expose from this variable. Fail-safe, so not blocking, but the list is presented as five flags each earned.

Not read: any comment or review on this PR (judged the diff and the checks only).

model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261111T2146Z

LD-VERDICT REQUEST_CHANGES head=a6d13fb475e939ef0cc281a2ab142f2be6f06f72 session=LR-20261111-017-seatA-20261111T2146Z Reviewed a clean clone at that exact head (PR head confirmed = a6d13fb), full base..head diff plus the round-4 delta, with independent probes of `gatedProcessInterposition` on head and on 6835d30. Suite: `npm ci`, `tsc --noEmit` clean, `vitest run` 1318 pass / 1 fail — the failure is `tests/merge-identity.test.ts` (ambient-token case), identical on 6835d30, pre-existing, not a hold. Verified good: - The allow-list is exactly the five flags with per-flag justifications (forgejo/api.ts:129-135, comment 106-126), and flag names are read whole up to `=` (forgejo/api.ts:163-167): `--max-old-space-sizex`, `--no-warningsx`, `--stack-sizes` all refuse, so a near-name cannot widen the list. - Every dangerous row refuses on a gated landing at head: `--require` (bare, `=`, space), `--import`, `--loader` (both forms), `--experimental-loader` (both forms), `--openssl-config`, `--use-openssl-ca`, `--use-bundled-ca`, `--use-system-ca`, `--use-env-proxy`, and an unmeasured alias. On 6835d30 the same probe lets `--loader`, `--experimental-loader`, `--openssl-config` and the unknown alias through; a6d13fb's test file against 6835d30's api.ts gives 7 failed / 12 passed of 19 — red→green reproduced independently, matching the commit message. - A pure-allow-list value passes and the merge completes; a mixed value refuses naming `--require`; no refusal line echoes a value (checked every line for path leaks). - Non-gated unchanged: the non-gated branch returns before `assertGatedProcessTrust` (forgejo/api.ts:1149-1156); the non-gated control test now carries the loader/openssl flags and still passes. - Nothing else moved in a6d13fb (api.ts, its test, docs/forgejo.md only); `seat-post.ts` and `comment-as.ts` are absent from the full base..head diff. Holds: 1. forgejo/api.ts:166 — `nodeOptionFlagNames` keeps only tokens that start with `--`, so default-deny never sees a single-dash short flag, and Node v22.23.4 accepts `-r <file>` in NODE_OPTIONS. Measured on this box: `NODE_OPTIONS="-r /tmp/hook.cjs" node -e ...` runs the CJS hook in-process, and a hook that replaced `globalThis.fetch` intercepted the pinned default origin (`HOOK-INTERCEPTED https://forgejo.aaron.reynoza.org/api/v1/user`). `gatedProcessInterposition({NODE_OPTIONS:"-r /tmp/hook.cjs"})` is `undefined` on head (and on 6835d30), and the mixed `--no-warnings -r <file>` form passes too — so a gated landing still proceeds with session code loaded, the same lever as `--require`, which api.ts:1105 states "cannot be defended in-process at all". Node's own `process.allowedNodeEnvironmentFlags` lists exactly two short forms, `-r` and `-C`, and `-r` loads code. Minimal fix in the existing intent: refuse ANY token beginning with `-` whose name is not on `BENIGN_NODE_OPTION_FLAGS` (true default-deny — this also covers `-C`), optionally cross-checked against `process.allowedNodeEnvironmentFlags`; add a gated row for `-r <file>` and for the mixed form that fails on 6835d30. Nit, explicitly not a hold: 2. forgejo/api.ts:122 and :132 — `--stack-size` is not an allowable NODE_OPTIONS flag at all on v22.23.4 (`node: --stack-size is not allowed in NODE_OPTIONS`; `process.allowedNodeEnvironmentFlags.has("--stack-size")` is false), so that entry is inert and its "V8 stack limit / worst case a crash" justification describes a surface Node does not expose from this variable. Fail-safe, so not blocking, but the list is presented as five flags each earned. Not read: any comment or review on this PR (judged the diff and the checks only). model: omniroute/deepseek-flash session: LR-20261111-017-seatA-20261111T2146Z
fix(forgejo): one exact benign shape for NODE_OPTIONS, quote-stripped, values never named (LR-20261111-017, review-3c r5)
Some checks failed
CI / pr-title (pull_request) Successful in 0s
CI / mcp-bridge (pull_request) Successful in 27s
CI / pi-otel-telemetry (pull_request) Failing after 44s
tests / python (pull_request) Successful in 8s
CI / todo (pull_request) Successful in 56s
CI / skill-args (pull_request) Successful in 1m12s
CI / nats-intercom (pull_request) Successful in 40s
CI / ask-user-question (pull_request) Successful in 1m17s
CI / refocus (pull_request) Successful in 1m18s
CI / pi-friction (pull_request) Successful in 55s
CI / forgejo-ci (pull_request) Successful in 1m30s
CI / redact (pull_request) Successful in 49s
CI / plane (pull_request) Successful in 1m47s
CI / forgejo (pull_request) Successful in 2m9s
CI / pi-swarm-suite (pull_request) Successful in 3m56s
3462f12e82
review-3c measured on Node 22 that '-r x', '"--require" x', '"-r" x',
'--max-old-space-size=1024 -r x', '--no-warnings "--require" x' and '-C x'
all passed the default-deny and loaded a hook (one replaced fetch and
intercepted the pinned origin). The check now refuses unless EVERY
whitespace-separated token, after Node-style quote stripping, matches exactly
^--(max-old-space-size|max-semi-space-size)=\d+$|^--(trace-warnings|no-warnings)$.
The dead --stack-size entry is dropped (Node does not accept it in NODE_OPTIONS)
and its false comment fixed. The refusal names the flag, never its value.
execArgv and NODE_USE_ENV_PROXY remain named follow-ups.

Lab-Run: LR-20261111-017
Collaborator

LD-VERDICT APPROVED head=3462f12e8260a4f8d65408367bdb68dbbcedd251 session=LR-20261111-017-seatA-20261111T2221Z

Reviewed the full base..head diff (merge-base 8c666fb → 3462f12) in a fresh read-only clone of the branch; PR head read as 3462f12e82, matching the pin. No code edited.

What I checked, measured on Node v22.23.4:

  1. The six measured bypasses all refuse. Not just the suite: I imported gatedProcessInterposition directly and drove it with -r x, "--require" x, "-r" x, --max-old-space-size=1024 -r x, --no-warnings "--require" x, -C x — every one refuses, and the refusal names the offending token's flag. --loader x, '--require' x, --require=/tmp/x.js refuse too, so the check is default-deny rather than an alias list (forgejo/api.ts:117, :1042-:1045).
  2. Quote stripping mirrors Node, verified against Node itself: NODE_OPTIONS='"--require" /tmp/nt/m.cjs' really does load the module on this Node, so the quoted spelling is live; the code strips one layer of matching quotes (forgejo/api.ts:127-:136) and then applies the same single pattern. Measured: bare --no-warnings, '--no-warnings', "--no-warnings", --max-semi-space-size=64, '--max-old-space-size=512', and whitespace-padded benign values all pass; quoted and mixed dangerous forms all refuse.
  3. The refusal never names a value. =value tokens are reported as their flag only (offending.split("=")[0], api.ts:1048); I confirmed no value substring appears in the message for --require=/secret/path.js, --loader=/srv/x.mjs, --max-old-space-size=4096x. Non-blocking observation (no hold): a token with an attached non-= suffix (-r/tmp/x.js) is refused but echoed whole, path included; on Node 22 that token is not honoured at all (node: -r/tmp/x.js is not allowed in NODE_OPTIONS, measured), so nothing Node would act on leaks — still, the flag-name extraction is the only thing between that class and an echo.
  4. --stack-size is gone from code, tests and docs — the sole remaining occurrence is the comment in api.ts:112 stating it is deliberately absent because Node does not accept it in NODE_OPTIONS, and docs/forgejo.md:849-:851 lists the allow-list without it.
  5. The r5 tests actually bite: restoring a6d13fb's api.ts over the current one in a copy of the tree turns tests/landing-gated-process-env-s2.test.ts red (2 failures — the ROUND 4/5 cases and the round-5 six-form case), which is the seat's red-check, and it is red for the right reason.
  6. Earlier rounds intact: B1 credential pin is still a constant, not the transport's base — assertMergeIdentity always dials DEFAULT_BASE_URL and the proof carries that config (api.ts:1713), and mergeAsLanding writes with identity.config (api.ts:2008). S2 process-trust refusal still runs inside pinGatedTransport before any further read/identity GET/write, and only on gated repos. F5b is honest on all three paths: merge probe gateProbeConfig(configured) (index.ts:981), census repo listing AND each row's first PR read probed (index.ts:1207, row probe passed at :1289), verdict-cli first read at the built-in base with the base narrowed from the SERVER's name afterwards (verdict-cli.ts:498, :527). Origin-keyed tests measure url.origin, not paths.

Suite: 61/62 files, 1319/1320 tests pass. The one failure (tests/merge-identity.test.ts, landing-token assertion) also fails on the untouched base commit in a clean copy — pre-existing, not a hold. npx tsc --noEmit clean.

No holds.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261111T2221Z

LD-VERDICT APPROVED head=3462f12e8260a4f8d65408367bdb68dbbcedd251 session=LR-20261111-017-seatA-20261111T2221Z Reviewed the full base..head diff (merge-base 8c666fb → 3462f12) in a fresh read-only clone of the branch; PR head read as 3462f12e8260a4f8d65408367bdb68dbbcedd251, matching the pin. No code edited. What I checked, measured on Node v22.23.4: 1. The six measured bypasses all refuse. Not just the suite: I imported `gatedProcessInterposition` directly and drove it with `-r x`, `"--require" x`, `"-r" x`, `--max-old-space-size=1024 -r x`, `--no-warnings "--require" x`, `-C x` — every one refuses, and the refusal names the offending token's flag. `--loader x`, `'--require' x`, `--require=/tmp/x.js` refuse too, so the check is default-deny rather than an alias list (forgejo/api.ts:117, :1042-:1045). 2. Quote stripping mirrors Node, verified against Node itself: `NODE_OPTIONS='"--require" /tmp/nt/m.cjs'` really does load the module on this Node, so the quoted spelling is live; the code strips one layer of matching quotes (forgejo/api.ts:127-:136) and then applies the same single pattern. Measured: bare `--no-warnings`, `'--no-warnings'`, `"--no-warnings"`, `--max-semi-space-size=64`, `'--max-old-space-size=512'`, and whitespace-padded benign values all pass; quoted and mixed dangerous forms all refuse. 3. The refusal never names a value. `=value` tokens are reported as their flag only (`offending.split("=")[0]`, api.ts:1048); I confirmed no value substring appears in the message for `--require=/secret/path.js`, `--loader=/srv/x.mjs`, `--max-old-space-size=4096x`. Non-blocking observation (no hold): a token with an attached non-`=` suffix (`-r/tmp/x.js`) is refused but echoed whole, path included; on Node 22 that token is not honoured at all (`node: -r/tmp/x.js is not allowed in NODE_OPTIONS`, measured), so nothing Node would act on leaks — still, the flag-name extraction is the only thing between that class and an echo. 4. `--stack-size` is gone from code, tests and docs — the sole remaining occurrence is the comment in api.ts:112 stating it is deliberately absent because Node does not accept it in `NODE_OPTIONS`, and docs/forgejo.md:849-:851 lists the allow-list without it. 5. The r5 tests actually bite: restoring a6d13fb's api.ts over the current one in a copy of the tree turns `tests/landing-gated-process-env-s2.test.ts` red (2 failures — the ROUND 4/5 cases and the round-5 six-form case), which is the seat's red-check, and it is red for the right reason. 6. Earlier rounds intact: B1 credential pin is still a constant, not the transport's base — `assertMergeIdentity` always dials `DEFAULT_BASE_URL` and the proof carries that config (api.ts:1713), and `mergeAsLanding` writes with `identity.config` (api.ts:2008). S2 process-trust refusal still runs inside `pinGatedTransport` before any further read/identity GET/write, and only on gated repos. F5b is honest on all three paths: merge probe `gateProbeConfig(configured)` (index.ts:981), census repo listing AND each row's first PR read probed (index.ts:1207, row probe passed at :1289), verdict-cli first read at the built-in base with the base narrowed from the SERVER's name afterwards (verdict-cli.ts:498, :527). Origin-keyed tests measure `url.origin`, not paths. Suite: 61/62 files, 1319/1320 tests pass. The one failure (`tests/merge-identity.test.ts`, landing-token assertion) also fails on the untouched base commit in a clean copy — pre-existing, not a hold. `npx tsc --noEmit` clean. No holds. model: omniroute/deepseek-flash session: LR-20261111-017-seatA-20261111T2221Z
Collaborator

LD-VERDICT APPROVED head=3462f12e8260a4f8d65408367bdb68dbbcedd251 session=LR-20261111-017-seatB-20261111T2221Z

Reviewed the full base..head diff (base 8c666fb, all rounds: F5 pins, server-name keying, census leg, credential-pinned-to-default-origin, this NODE_OPTIONS shape check) in my own read-only clone. PR head re-read and equal to the pinned 40 hex.

What I verified independently:

  • The six measured bypasses all refuse, at unit level AND at tool level: I ran a gated merge (Homelab/prod, FORGEJO_BASE_URL pointed at a stub origin) with each of -r x, "--require" x, "-r" x, --max-old-space-size=1024 -r x, --no-warnings "--require" x, -C x. Each throws GatedProcessEnvRefusal; the stub origin records ZERO requests; the only request that leaves is the single probe GET pulls/79; no request carries the landing token. I also confirmed on Node v22.23.4 that the first five really do execute the hook (so the hole was real, not theoretical).
  • Red→green control: with a6d13fb's api.ts restored in a COPY of the head tree, tests/landing-gated-process-env-s2.test.ts is 2 failed / 18 passed (the r5 cases go red); at head it is 20/20.
  • Quote stripping mirrors Node for the measured forms: quoted benign "--no-warnings" passes (Node accepts it), quoted --require / -r refuse; single quotes are stripped too (stricter than Node, i.e. safe direction). I found no direction in which Node's quoting could make a dangerous token satisfy the pattern.
  • Value naming: for every measured-bypass shape the refusal names only a flag (-r, --require, -C), never the hook path.
  • --stack-size is gone from code, tests and docs; only the comment recording its absence remains, and Node really rejects it in NODE_OPTIONS (measured), so dropping it was correct.
  • Earlier rounds intact: B1 (assertMergeIdentity builds the credential-carrying config with DEFAULT_BASE_URL on every repo, and mergeAsLanding writes through identity.config, so GET /user and the POST never follow an env base), S2 env refusals, F5 pins, F5b server-name keying, census probe leg — code read end to end and their suites green.
  • Suite: 62 files / 1320 tests pass; tsc --noEmit clean. With this sandbox's ambient FORGEJO_LANDING_TOKEN_FILE set, merge-identity.test.ts fails exactly as it does on the untouched base — env artifact, not a hold; unset, it passes.

Holds: none.

Notes (non-blocking):

  1. forgejo/api.ts:1046 — the comment says "Name the flag, never its value", but the code echoes the offending TOKEN. When the first non-matching token is not a flag (NODE_OPTIONS="--trace-warnings /tmp/x"), the value/path is named; docs/forgejo.md:854 repeats the same broader claim. Not exploitable in the shapes measured and nothing leaves the process (it is the caller's own env in its own stderr/tool result), so not a hold — but the sentence is broader than the code.
  2. docs/forgejo.md:845-850 still describes the round-4 parser ("a flag name being read whole … up to the first =") and does not mention quote stripping or that EVERY token must match the one pattern; r5 changed only the flag-list line. The normative claims (default-deny, unlisted refuses) remain true; the mechanism sentence is stale.
  3. The r5 cases are unit-level on gatedProcessInterposition only — the six shapes are not in the tool-level INTERPOSITIONS table. I verified that path by hand (refuses, nothing else leaves), so it is not a gap in behaviour; cheap to add next round.
  4. Residual, named and not claimed closed: process.execArgv and NODE_USE_ENV_PROXY.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261111T2221Z

LD-VERDICT APPROVED head=3462f12e8260a4f8d65408367bdb68dbbcedd251 session=LR-20261111-017-seatB-20261111T2221Z Reviewed the full base..head diff (base 8c666fb, all rounds: F5 pins, server-name keying, census leg, credential-pinned-to-default-origin, this NODE_OPTIONS shape check) in my own read-only clone. PR head re-read and equal to the pinned 40 hex. What I verified independently: - The six measured bypasses all refuse, at unit level AND at tool level: I ran a gated merge (Homelab/prod, FORGEJO_BASE_URL pointed at a stub origin) with each of `-r x`, `"--require" x`, `"-r" x`, `--max-old-space-size=1024 -r x`, `--no-warnings "--require" x`, `-C x`. Each throws GatedProcessEnvRefusal; the stub origin records ZERO requests; the only request that leaves is the single probe `GET pulls/79`; no request carries the landing token. I also confirmed on Node v22.23.4 that the first five really do execute the hook (so the hole was real, not theoretical). - Red→green control: with a6d13fb's api.ts restored in a COPY of the head tree, tests/landing-gated-process-env-s2.test.ts is 2 failed / 18 passed (the r5 cases go red); at head it is 20/20. - Quote stripping mirrors Node for the measured forms: quoted benign `"--no-warnings"` passes (Node accepts it), quoted `--require` / `-r` refuse; single quotes are stripped too (stricter than Node, i.e. safe direction). I found no direction in which Node's quoting could make a dangerous token satisfy the pattern. - Value naming: for every measured-bypass shape the refusal names only a flag (`-r`, `--require`, `-C`), never the hook path. - `--stack-size` is gone from code, tests and docs; only the comment recording its absence remains, and Node really rejects it in NODE_OPTIONS (measured), so dropping it was correct. - Earlier rounds intact: B1 (assertMergeIdentity builds the credential-carrying config with DEFAULT_BASE_URL on every repo, and mergeAsLanding writes through identity.config, so GET /user and the POST never follow an env base), S2 env refusals, F5 pins, F5b server-name keying, census probe leg — code read end to end and their suites green. - Suite: 62 files / 1320 tests pass; `tsc --noEmit` clean. With this sandbox's ambient FORGEJO_LANDING_TOKEN_FILE set, merge-identity.test.ts fails exactly as it does on the untouched base — env artifact, not a hold; unset, it passes. Holds: none. Notes (non-blocking): 1. forgejo/api.ts:1046 — the comment says "Name the flag, never its value", but the code echoes the offending TOKEN. When the first non-matching token is not a flag (`NODE_OPTIONS="--trace-warnings /tmp/x"`), the value/path is named; docs/forgejo.md:854 repeats the same broader claim. Not exploitable in the shapes measured and nothing leaves the process (it is the caller's own env in its own stderr/tool result), so not a hold — but the sentence is broader than the code. 2. docs/forgejo.md:845-850 still describes the round-4 parser ("a flag name being read whole … up to the first `=`") and does not mention quote stripping or that EVERY token must match the one pattern; r5 changed only the flag-list line. The normative claims (default-deny, unlisted refuses) remain true; the mechanism sentence is stale. 3. The r5 cases are unit-level on gatedProcessInterposition only — the six shapes are not in the tool-level INTERPOSITIONS table. I verified that path by hand (refuses, nothing else leaves), so it is not a gap in behaviour; cheap to add next round. 4. Residual, named and not claimed closed: process.execArgv and NODE_USE_ENV_PROXY. model: omniroute/deepseek-flash session: LR-20261111-017-seatB-20261111T2221Z
Collaborator

LD-VERDICT APPROVED head=3462f12e8260a4f8d65408367bdb68dbbcedd251 session=LR-20261111-017-seatA-20261112T0137Z

Independent seat pass: read-only clone at the pinned head (PR head confirmed equal to 3462f12e82), full base..head diff read (11 files, +2237/−43), PR unit tests run only inside my clone; no image-level harness, no credential read, no code edited.

WHAT I CHECKED

  • PR tests: 5 new files / 41 tests green on head. forgejo suite on head 1319/1320 (61/62 files). The one failure, merge-identity.test.ts "merges with the dedicated landing credential, never the operator token", fails identically with base forgejo/ restored in a separate copy — pre-existing, not a hold. pi-otel-telemetry is the known platform npm-cache fault; the pi-swarm-suite failures are my symlinked node_modules resolution, neither PR-related.
  • r5 shape check, scratch-verified: restored a6d13fb's api.ts over a copy → exactly 2 red (the six-form loop, -r x answered undefined = allowed; and the --max-old-space-size=4096x near-name); all green on head. Independently probed all six measured forms on head (-r x, "-r" x, "--require" x, -C x, mixed --max-old-space-size=1024 -r x, --no-warnings "--require" x): every one refuses, naming -r/--require/-C and never the path. --stack-size is gone: the pattern (api.ts:117) is the single regex, and GATED_NODE_OPTION_FLAGS / NODE_OPTION_CONSEQUENCE / nodeOptionFlagNames are deleted everywhere (no dead entries). Follow-ups process.execArgv and NODE_USE_ENV_PROXY are named (api.ts:122-125).
  • Earlier rounds intact. B1: assertMergeIdentity rebuilds the credential config from the DEFAULT_BASE_URL constant on every repo, and mergeAsLanding posts with identity.config, so GET /user and the merge POST never leave the built-in origin; the relay test asserts zero landing-token requests at the proxy and passes. F5b/server-name rule: the gate read (getPR), the census listing and each row's GET pulls/{n} all go through gateProbeConfig; the pin keys on pr.base.repo.full_name; an unreadable name is pinned as gated (fail-closed) and refuses. S2: TLS_REJECT=0 / EXTRA_CA_CERTS / off-shape NODE_OPTIONS refuse, one line, before any further read (20 tests green). verdict-cli narrows args.baseUrl after that first read, so its POST is pinned too.
  • No value echoes in the r5 refusals (flag name only); no personal names in the diff.

Holds: none.

Non-blocking observations (file:line)

  1. docs/forgejo.md:848 still describes the round-4 parser ("split on whitespace, up to the first =, exact match") and never states the r5 rule (Node-style quote stripping, single-dash refusal). Every conclusion it draws holds on head; only the mechanism sentence is stale.
  2. api.ts:1048 names offending.split("=")[0] — right for every flag form, but a bare non-flag token (e.g. NODE_OPTIONS="/some/path") is echoed whole as the "flag". It is the caller's own env value, never the landing token, and only when no flag precedes it; worth a one-line follow-up if the rule is meant to be "flags only, always".
  3. api.ts:122-125 names execArgv and NODE_USE_ENV_PROXY as open follow-ups in code only; docs/forgejo.md does not repeat them, so its "default-deny" paragraph can read as complete.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261112T0137Z

LD-VERDICT APPROVED head=3462f12e8260a4f8d65408367bdb68dbbcedd251 session=LR-20261111-017-seatA-20261112T0137Z Independent seat pass: read-only clone at the pinned head (PR head confirmed equal to 3462f12e8260a4f8d65408367bdb68dbbcedd251), full base..head diff read (11 files, +2237/−43), PR unit tests run only inside my clone; no image-level harness, no credential read, no code edited. WHAT I CHECKED - PR tests: 5 new files / 41 tests green on head. forgejo suite on head 1319/1320 (61/62 files). The one failure, merge-identity.test.ts "merges with the dedicated landing credential, never the operator token", fails identically with base forgejo/ restored in a separate copy — pre-existing, not a hold. pi-otel-telemetry is the known platform npm-cache fault; the pi-swarm-suite failures are my symlinked node_modules resolution, neither PR-related. - r5 shape check, scratch-verified: restored a6d13fb's api.ts over a copy → exactly 2 red (the six-form loop, `-r x` answered undefined = allowed; and the `--max-old-space-size=4096x` near-name); all green on head. Independently probed all six measured forms on head (`-r x`, `"-r" x`, `"--require" x`, `-C x`, mixed `--max-old-space-size=1024 -r x`, `--no-warnings "--require" x`): every one refuses, naming `-r`/`--require`/`-C` and never the path. `--stack-size` is gone: the pattern (api.ts:117) is the single regex, and GATED_NODE_OPTION_FLAGS / NODE_OPTION_CONSEQUENCE / nodeOptionFlagNames are deleted everywhere (no dead entries). Follow-ups `process.execArgv` and `NODE_USE_ENV_PROXY` are named (api.ts:122-125). - Earlier rounds intact. B1: assertMergeIdentity rebuilds the credential config from the DEFAULT_BASE_URL constant on every repo, and mergeAsLanding posts with identity.config, so GET /user and the merge POST never leave the built-in origin; the relay test asserts zero landing-token requests at the proxy and passes. F5b/server-name rule: the gate read (getPR), the census listing and each row's GET pulls/{n} all go through gateProbeConfig; the pin keys on pr.base.repo.full_name; an unreadable name is pinned as gated (fail-closed) and refuses. S2: TLS_REJECT=0 / EXTRA_CA_CERTS / off-shape NODE_OPTIONS refuse, one line, before any further read (20 tests green). verdict-cli narrows args.baseUrl after that first read, so its POST is pinned too. - No value echoes in the r5 refusals (flag name only); no personal names in the diff. Holds: none. Non-blocking observations (file:line) 1. docs/forgejo.md:848 still describes the round-4 parser ("split on whitespace, up to the first `=`, exact match") and never states the r5 rule (Node-style quote stripping, single-dash refusal). Every conclusion it draws holds on head; only the mechanism sentence is stale. 2. api.ts:1048 names `offending.split("=")[0]` — right for every flag form, but a bare non-flag token (e.g. NODE_OPTIONS="/some/path") is echoed whole as the "flag". It is the caller's own env value, never the landing token, and only when no flag precedes it; worth a one-line follow-up if the rule is meant to be "flags only, always". 3. api.ts:122-125 names execArgv and NODE_USE_ENV_PROXY as open follow-ups in code only; docs/forgejo.md does not repeat them, so its "default-deny" paragraph can read as complete. model: omniroute/deepseek-flash session: LR-20261111-017-seatA-20261112T0137Z
Collaborator

LD-VERDICT APPROVED head=3462f12e8260a4f8d65408367bdb68dbbcedd251 session=LR-20261111-017-seatB-20261112T0137Z

Independent seat pass, own read-only clone at the exact head (PR head read back = 3462f12e82, base 8c666fbe; diff judged base..head, 11 files). No comments or reviews of this PR were read.

What I checked and measured

  1. r5 shape check (forgejo/api.ts:1701-1712 BENIGN_NODE_OPTION_PATTERN, stripNodeOptionQuotes api.ts:1686-1699, refusal naming api.ts:1726-1738). Own scratch probe (not the PR's test file) against HEAD's api.ts: all six measured forms refuse — -r /tmp/hook.js, "-r" /tmp/hook.js, "--require" /tmp/hook.js, -C /tmp, --max-old-space-size=1024 -r /tmp/hook.js, --no-warnings "--require" /tmp/hook.js — each naming the flag and never the value. Extra aliases also refuse: -r=/tmp/hook.js, --require=/…, --import=/…, --loader=/…, --experimental-loader, --use-system-ca, --use-env-proxy, --openssl-config, -e=…, --stack-size=2048; --max-old-space-sizex=4096, --no-warningsx, --trace-warnings=1, --max-old-space-size=abc, --max-old-space-size= all refuse (default-deny, exact end-to-end). The four benign shapes and their quoted form pass. Red direction verified: a copy with a6d13fb's api.ts restored makes 2 of the new s2 cases go red (expected undefined to be defined for the r5 case, plus the round-4/5 mixed case) — 2 failed / 18 passed there, 20/20 green at HEAD.
  2. Empirically confirmed on the local Node v22.23.4 that -r, "-r" and a mixed --max-old-space-size=1024 -r in NODE_OPTIONS do load code (a require hook ran), so the shape check closes measured bypasses; and that --stack-size is rejected by Node in NODE_OPTIONS (node: --stack-size= is not allowed in NODE_OPTIONS), so dropping it removed a false claim, not a guard.
  3. Server-name rule / probe: the gate is decided from pr.base.repo.full_name on a read always dialled at the built-in default (api.ts:160 gateProbeConfig; index.ts:390 getPR(opts.probe ?? c …); index.ts:410-417 pin keyed on the server view, fail-closed on an unreadable name; verdict-cli.ts:494-527 probe then pin). Census: listing dialled through the probe (index.ts:1207, 1220) and each row's first read probed (index.ts:1284-1287); no caller-spelling key survives (grep: repoKey/isGoGatedRepo appear only in comments in index.ts/verdict-cli.ts).
  4. B1 relay: assertMergeIdentity builds the credential-carrying config with the DEFAULT_BASE_URL constant on every repo (api.ts:1704-1713), and the merge POST goes through mergePR(identity.config) (api.ts:2006). Measured by landing-credential-default-origin-b1.test.ts, whose fake records the request origin (tests/fake.ts:47,103): zero credential-bearing requests at the stub origin, one GET /user and one merge POST at the built-in default with the staged landing token. Green.
  5. Earlier rounds intact: the gated/unreadable evidence reads keep their complete: gated fail-closed arguments and now take the pinned config (index.ts:433-560 pass tc); S2 refusals still fire before any request after the probe (api.ts:1015-1065, called from pinGatedTransport api.ts:1130); policy levers use the shared reportGatedIgnored (landing.ts:3137-3155, 3213-3229) — one sentence shape, no duplicate. --base-url is honored only for reads after the first and only on a server-proved non-gated repo (verdict-cli.ts:214, 527-532 with all later reads through args.baseUrl).
  6. No dead entries, no value leaks: GATED_NODE_OPTION_FLAGS is gone (grep clean), --stack-size appears only in the comment that says it is absent (api.ts:112), no test lists it as benign; every ignored lever is logged by NAME only and the refusal splits the flag off at = (api.ts:1139-1160, 1726-1738); the b1 test asserts the stub base string never appears in stderr.
  7. Tests in my clone (PR unit tests only): vitest run forgejo/tests = 61 files passed / 1 failed, 1319 passed / 1 failed — the single failure is forgejo/tests/merge-identity.test.ts:144 (expected '7ad6…dfa' to be 'landing-dedicated-credential'), reproduced identically on an untouched 8c666fbe worktree (1 failed / 24 passed both sides) with an ambient FORGEJO_LANDING_TOKEN in this sandbox's env: pre-existing, not a hold. Full-repo run: 4 files failed — the same merge-identity case, pi-otel-telemetry/reload.test.ts (module @opentelemetry/sdk-trace-node absent from this sandbox's symlinked node_modules, i.e. the platform npm-cache fault), and two pi-swarm-suite view/idle-wake files failing on require.resolve("@earendil-works/pi-agent-core/package.json") in my linker setup — all environment-side, none in forgejo logic. tsc -p forgejo/tsconfig.json --noEmit clean (exit 0).

No holds.
NOTE (non-blocking): docs/forgejo.md:854-857 still describes round 4's parsing ("a flag name being read whole, split on whitespace up to the first =") and does not state r5's quote stripping, the exact =<digits> shape for the two memory flags, or that single-dash tokens now refuse — accurate in substance (default-deny), stale in mechanism; worth one line in the next doc touch.
NOTE (non-blocking): a malformed NODE_OPTIONS token with no = (a bare path standing where a flag is expected) is echoed verbatim as "the flag" (api.ts:1729). No measured bypass form reaches it and no credential class lives there, but the "never a value" guarantee is per-token, not per-token-kind.
NOTE (non-blocking): on a non-gated merge the failure message and the credential origin field still print the caller's configured base (index.ts:1040, api.ts:1714) while both credential-bearing calls actually go to the built-in default — cosmetically inconsistent, not a lever.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261112T0137Z

LD-VERDICT APPROVED head=3462f12e8260a4f8d65408367bdb68dbbcedd251 session=LR-20261111-017-seatB-20261112T0137Z Independent seat pass, own read-only clone at the exact head (PR head read back = 3462f12e8260a4f8d65408367bdb68dbbcedd251, base 8c666fbe; diff judged base..head, 11 files). No comments or reviews of this PR were read. What I checked and measured 1. r5 shape check (forgejo/api.ts:1701-1712 BENIGN_NODE_OPTION_PATTERN, stripNodeOptionQuotes api.ts:1686-1699, refusal naming api.ts:1726-1738). Own scratch probe (not the PR's test file) against HEAD's api.ts: all six measured forms refuse — `-r /tmp/hook.js`, `"-r" /tmp/hook.js`, `"--require" /tmp/hook.js`, `-C /tmp`, `--max-old-space-size=1024 -r /tmp/hook.js`, `--no-warnings "--require" /tmp/hook.js` — each naming the flag and never the value. Extra aliases also refuse: `-r=/tmp/hook.js`, `--require=/…`, `--import=/…`, `--loader=/…`, `--experimental-loader`, `--use-system-ca`, `--use-env-proxy`, `--openssl-config`, `-e=…`, `--stack-size=2048`; `--max-old-space-sizex=4096`, `--no-warningsx`, `--trace-warnings=1`, `--max-old-space-size=abc`, `--max-old-space-size=` all refuse (default-deny, exact end-to-end). The four benign shapes and their quoted form pass. Red direction verified: a copy with a6d13fb's api.ts restored makes 2 of the new s2 cases go red (`expected undefined to be defined` for the r5 case, plus the round-4/5 mixed case) — 2 failed / 18 passed there, 20/20 green at HEAD. 2. Empirically confirmed on the local Node v22.23.4 that `-r`, `"-r"` and a mixed `--max-old-space-size=1024 -r` in NODE_OPTIONS do load code (a require hook ran), so the shape check closes measured bypasses; and that `--stack-size` is rejected by Node in NODE_OPTIONS (`node: --stack-size= is not allowed in NODE_OPTIONS`), so dropping it removed a false claim, not a guard. 3. Server-name rule / probe: the gate is decided from `pr.base.repo.full_name` on a read always dialled at the built-in default (api.ts:160 gateProbeConfig; index.ts:390 getPR(opts.probe ?? c …); index.ts:410-417 pin keyed on the server view, fail-closed on an unreadable name; verdict-cli.ts:494-527 probe then pin). Census: listing dialled through the probe (index.ts:1207, 1220) and each row's first read probed (index.ts:1284-1287); no caller-spelling key survives (grep: repoKey/isGoGatedRepo appear only in comments in index.ts/verdict-cli.ts). 4. B1 relay: assertMergeIdentity builds the credential-carrying config with the DEFAULT_BASE_URL constant on every repo (api.ts:1704-1713), and the merge POST goes through mergePR(identity.config) (api.ts:2006). Measured by landing-credential-default-origin-b1.test.ts, whose fake records the request origin (tests/fake.ts:47,103): zero credential-bearing requests at the stub origin, one GET /user and one merge POST at the built-in default with the staged landing token. Green. 5. Earlier rounds intact: the gated/unreadable evidence reads keep their `complete: gated` fail-closed arguments and now take the pinned config (index.ts:433-560 pass `tc`); S2 refusals still fire before any request after the probe (api.ts:1015-1065, called from pinGatedTransport api.ts:1130); policy levers use the shared reportGatedIgnored (landing.ts:3137-3155, 3213-3229) — one sentence shape, no duplicate. `--base-url` is honored only for reads after the first and only on a server-proved non-gated repo (verdict-cli.ts:214, 527-532 with all later reads through args.baseUrl). 6. No dead entries, no value leaks: GATED_NODE_OPTION_FLAGS is gone (grep clean), `--stack-size` appears only in the comment that says it is absent (api.ts:112), no test lists it as benign; every ignored lever is logged by NAME only and the refusal splits the flag off at `=` (api.ts:1139-1160, 1726-1738); the b1 test asserts the stub base string never appears in stderr. 7. Tests in my clone (PR unit tests only): `vitest run forgejo/tests` = 61 files passed / 1 failed, 1319 passed / 1 failed — the single failure is forgejo/tests/merge-identity.test.ts:144 (`expected '7ad6…dfa' to be 'landing-dedicated-credential'`), reproduced identically on an untouched 8c666fbe worktree (1 failed / 24 passed both sides) with an ambient FORGEJO_LANDING_TOKEN in this sandbox's env: pre-existing, not a hold. Full-repo run: 4 files failed — the same merge-identity case, pi-otel-telemetry/reload.test.ts (module `@opentelemetry/sdk-trace-node` absent from this sandbox's symlinked node_modules, i.e. the platform npm-cache fault), and two pi-swarm-suite view/idle-wake files failing on `require.resolve("@earendil-works/pi-agent-core/package.json")` in my linker setup — all environment-side, none in forgejo logic. `tsc -p forgejo/tsconfig.json --noEmit` clean (exit 0). No holds. NOTE (non-blocking): docs/forgejo.md:854-857 still describes round 4's parsing ("a flag name being read whole, split on whitespace up to the first `=`") and does not state r5's quote stripping, the exact `=<digits>` shape for the two memory flags, or that single-dash tokens now refuse — accurate in substance (default-deny), stale in mechanism; worth one line in the next doc touch. NOTE (non-blocking): a malformed NODE_OPTIONS token with no `=` (a bare path standing where a flag is expected) is echoed verbatim as "the flag" (api.ts:1729). No measured bypass form reaches it and no credential class lives there, but the "never a value" guarantee is per-token, not per-token-kind. NOTE (non-blocking): on a non-gated merge the failure message and the credential `origin` field still print the caller's configured base (index.ts:1040, api.ts:1714) while both credential-bearing calls actually go to the built-in default — cosmetically inconsistent, not a lever. model: omniroute/deepseek-flash session: LR-20261111-017-seatB-20261112T0137Z
ci: re-run pi-otel-telemetry after ETXTBSY flake, no code change
All checks were successful
CI / todo (pull_request) Successful in 1m1s
CI / pr-title (pull_request) Successful in 1s
CI / forgejo-ci (pull_request) Successful in 52s
CI / pi-otel-telemetry (pull_request) Successful in 56s
tests / python (pull_request) Successful in 9s
CI / skill-args (pull_request) Successful in 1m14s
CI / mcp-bridge (pull_request) Successful in 29s
CI / plane (pull_request) Successful in 1m29s
CI / pi-friction (pull_request) Successful in 42s
CI / nats-intercom (pull_request) Successful in 44s
CI / refocus (pull_request) Successful in 50s
CI / redact (pull_request) Successful in 51s
CI / ask-user-question (pull_request) Successful in 55s
CI / forgejo (pull_request) Successful in 2m43s
CI / pi-swarm-suite (pull_request) Successful in 4m43s
7fa5d92d92
The only red leg on 3462f12e was the runner flake (npm ci ETXTBSY under the
15-job burst, homelab-88). This empty commit exists solely to re-fire CI;
the tree is byte-identical to 3462f12e (TREE-EQUAL proof in the run record).

Lab-Run: LR-20261111-017
Collaborator

LD-VERDICT APPROVED head=7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 session=LR-20261111-017-seatB-20261112T0220Z

TREE-EQUAL confirmed. git diff 3462f12e8260a4f8d65408367bdb68dbbcedd251..7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 --stat prints NOTHING; both commits' trees are 24e7162f7c (same as declared); head has the single parent 3462f12e. So the head is the disclosed empty re-fire commit and the code under review is exactly the r5 tree. PR head sha read back before judging: matches the pinned 40 hex.

What I checked on base 8c666fbe..head 7fa5d92d (11 files, +2237/-43), fresh read-only clone at the head:

  1. Gate keyed on the SERVER's name, on all three paths: collectLandingEvidence does its one deciding read at gateProbeConfig (built-in base), resolves serverRepoView(..., {requireReported:true}), and only then pins — forgejo/index.ts:392-425 (probe, refuse-on-unreadable, pin), census per row forgejo/index.ts:1284-1292 with the listing itself at the probe :1268, verdict-cli's first read at the probe forgejo/verdict-cli.ts:489-501 then pin :518-525. Caller-keyed pre-pin is gone from all three.
  2. Fail-closed edges: absent/unreadable server name ⇒ refuse on merge/census, and the transport passed to pinGatedTransport is {ref: undefined, gated: true} in that branch, so even the refuse path could not leave the default origin. verdict-cli treats an unreadable name via goGatedFromServerName (gated).
  3. Credential pinned on EVERY repo: assertMergeIdentity builds the credential-bearing config as {baseUrl: DEFAULT_BASE_URL} and stores it on the non-enumerable proof (forgejo/api.ts:1721-1740), and mergeAsLanding writes with identity.config (:2006) — so both credential-bearing calls (GET /user and the merge POST) are on the built-in default even on a non-gated repo, while read-only traffic still honours the env. Gated lever lines are by name only (reportGatedIgnored, forgejo/landing.ts:3130-3157), one per lever per call (the census dedups by line), and the policy-knob refactor to the same helper preserves the old set semantics.
  4. r5 shape check: ONE pattern ^--(max-old-space-size|max-semi-space-size)=\d+$|^--(trace-warnings|no-warnings)$ over whitespace-split, quote-stripped tokens, default-deny (forgejo/api.ts:110-152); --stack-size gone with its false comment; refusal names the flag (split("=")[0]), never the value. All six measured bypasses (-r x, "--require" x, "-r" x, --max-old-space-size=1024 -r x, --no-warnings "--require" x, -C x) assert a refusal that does not contain the hook path; the quoted-benign case still passes; near-names (--max-old-space-sizex, --no-warningsx, --importx=) refuse.
  5. Fail-closed evidence reads (v14.0.3) intact: only the config argument changed to the earned transport; {complete: gated} on comments/reviews/inline/files and the refusal-on-truncation paths are unchanged.
  6. Docs docs/forgejo.md updated to match the code (pin, probe, credential origin, default-deny NODE_OPTIONS) — no claim in the docs exceeds the code.

Unit tests, inside my clone only (no image harness): vitest run = 61 files pass / 1 failed, 1319 tests pass / 1 failed. The single failure is merge-identity.test.ts > merge write credential > merges with the dedicated landing credential, never the operator token (ambient-token case, expected 'landing-dedicated-credential'); it fails with the IDENTICAL test name and assertion on the untouched base 8c666fbe in a separate worktree ⇒ pre-existing, not a hold. The six F5/F5b/B1/S2/server-name test files: 54/54 green. tsc --noEmit: clean.

r5 red→green sanity, done on a COPY only (original clone left clean): restoring a6d13fb's forgejo/api.ts over the copy makes exactly 2 of the 20 S2 cases red (the round-4/5 mixed-form case and the round-5 short/quoted/mixed case); at head all 20 are green.

Holds: none.

Non-blocking notes (all disclosed, none of them a new lever I measured): process.execArgv and NODE_USE_ENV_PROXY remain OPEN and are named in forgejo/api.ts:122-125 (the former is not closable in-process; the docs paragraph about the residual matches, though docs/forgejo.md does not repeat those two names); at forgejo/api.ts:146 a bare non-flag token (e.g. a stray path with no =) is echoed whole, since the first offender ends the scan before any later value — that is a path, never a credential, but the comment's "never its value" is exact only for --flag=value forms.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261112T0220Z

LD-VERDICT APPROVED head=7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 session=LR-20261111-017-seatB-20261112T0220Z TREE-EQUAL confirmed. `git diff 3462f12e8260a4f8d65408367bdb68dbbcedd251..7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 --stat` prints NOTHING; both commits' trees are 24e7162f7c95c26f3e86eb0108f189ecb844d0f1 (same as declared); head has the single parent 3462f12e. So the head is the disclosed empty re-fire commit and the code under review is exactly the r5 tree. PR head sha read back before judging: matches the pinned 40 hex. What I checked on base 8c666fbe..head 7fa5d92d (11 files, +2237/-43), fresh read-only clone at the head: 1. Gate keyed on the SERVER's name, on all three paths: `collectLandingEvidence` does its one deciding read at `gateProbeConfig` (built-in base), resolves `serverRepoView(..., {requireReported:true})`, and only then pins — `forgejo/index.ts:392-425` (probe, refuse-on-unreadable, pin), census per row `forgejo/index.ts:1284-1292` with the listing itself at the probe `:1268`, verdict-cli's first read at the probe `forgejo/verdict-cli.ts:489-501` then pin `:518-525`. Caller-keyed pre-pin is gone from all three. 2. Fail-closed edges: absent/unreadable server name ⇒ refuse on merge/census, and the transport passed to `pinGatedTransport` is `{ref: undefined, gated: true}` in that branch, so even the refuse path could not leave the default origin. verdict-cli treats an unreadable name via `goGatedFromServerName` (gated). 3. Credential pinned on EVERY repo: `assertMergeIdentity` builds the credential-bearing config as `{baseUrl: DEFAULT_BASE_URL}` and stores it on the non-enumerable proof (`forgejo/api.ts:1721-1740`), and `mergeAsLanding` writes with `identity.config` (`:2006`) — so both credential-bearing calls (GET /user and the merge POST) are on the built-in default even on a non-gated repo, while read-only traffic still honours the env. Gated lever lines are by name only (`reportGatedIgnored`, `forgejo/landing.ts:3130-3157`), one per lever per call (the census dedups by line), and the policy-knob refactor to the same helper preserves the old `set` semantics. 4. r5 shape check: ONE pattern `^--(max-old-space-size|max-semi-space-size)=\d+$|^--(trace-warnings|no-warnings)$` over whitespace-split, quote-stripped tokens, default-deny (`forgejo/api.ts:110-152`); `--stack-size` gone with its false comment; refusal names the flag (`split("=")[0]`), never the value. All six measured bypasses (`-r x`, `"--require" x`, `"-r" x`, `--max-old-space-size=1024 -r x`, `--no-warnings "--require" x`, `-C x`) assert a refusal that does not contain the hook path; the quoted-benign case still passes; near-names (`--max-old-space-sizex`, `--no-warningsx`, `--importx=`) refuse. 5. Fail-closed evidence reads (v14.0.3) intact: only the config argument changed to the earned transport; `{complete: gated}` on comments/reviews/inline/files and the refusal-on-truncation paths are unchanged. 6. Docs `docs/forgejo.md` updated to match the code (pin, probe, credential origin, default-deny NODE_OPTIONS) — no claim in the docs exceeds the code. Unit tests, inside my clone only (no image harness): `vitest run` = 61 files pass / 1 failed, 1319 tests pass / 1 failed. The single failure is `merge-identity.test.ts > merge write credential > merges with the dedicated landing credential, never the operator token` (ambient-token case, expected 'landing-dedicated-credential'); it fails with the IDENTICAL test name and assertion on the untouched base 8c666fbe in a separate worktree ⇒ pre-existing, not a hold. The six F5/F5b/B1/S2/server-name test files: 54/54 green. `tsc --noEmit`: clean. r5 red→green sanity, done on a COPY only (original clone left clean): restoring a6d13fb's `forgejo/api.ts` over the copy makes exactly 2 of the 20 S2 cases red (the round-4/5 mixed-form case and the round-5 short/quoted/mixed case); at head all 20 are green. Holds: none. Non-blocking notes (all disclosed, none of them a new lever I measured): process.execArgv and NODE_USE_ENV_PROXY remain OPEN and are named in `forgejo/api.ts:122-125` (the former is not closable in-process; the docs paragraph about the residual matches, though `docs/forgejo.md` does not repeat those two names); at `forgejo/api.ts:146` a bare non-flag token (e.g. a stray path with no `=`) is echoed whole, since the first offender ends the scan before any later value — that is a path, never a credential, but the comment's "never its value" is exact only for `--flag=value` forms. model: omniroute/deepseek-flash session: LR-20261111-017-seatB-20261112T0220Z
Collaborator

LD-VERDICT APPROVED head=7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 session=LR-20261111-017-seatA-20261112T0220Z

TREE-EQUAL confirmed first: git diff 3462f12e8260a4f8d65408367bdb68dbbcedd251..7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 --stat prints nothing, both trees are 24e7162f7c, and the remote branch lr-20261111-017-f5 is exactly 7fa5d92d — the pinned head. So the stack I judged is the same tree the earlier rounds cleared; the re-fire moved no code.

Judged the full 8c666fbec432f70b4853124f8de6ee2e445d1339..7fa5d92d diff (11 files, forgejo/* + docs/forgejo.md):

  • Server-name rule: the first read of every landing path (merge tool, each census row, verdict-cli's PR fetch) goes through gateProbeConfig at the built-in default, and gating + the pin key on pr.base.repo.full_name. Absent name is fail-closed gated (goGatedFromServerName), so a caller spelling such as x/Homelab cannot keep a call on the env origin, and every later read reuses the transport the server earned.
  • pinGatedTransport (forgejo/api.ts:1070) pins base + landing identity once per call and reports each ignored lever by NAME only, values never echoed, through the shared reportGatedIgnored (forgejo/landing.ts:3127) — no duplicate helper; non-gated repos untouched.
  • Landing credential (B1): assertMergeIdentity builds its config from DEFAULT_BASE_URL on EVERY repo, so GET /user and the merge POST never carry the token to an env/flag base; the token still comes only from credential, never from the transport; read-back uses write.config.
  • Fail-closed evidence reads (v14.0.3) preserved: every evidence read in collectLandingEvidence moved from c to tc, completeness flags unchanged; census rows keep the built-in origin when gated or unreadable.
  • r5 shape check: one exact pattern over quote-stripped tokens; -r, -C, quoted, mixed and near-name forms refuse, the refusal names the flag only, --stack-size dropped, execArgv and NODE_USE_ENV_PROXY named as open follow-ups in code and docs. The quote handling errs the safe way: only tokens that need no stripping are accepted, so any Node-side normalisation lands on the refuse side.

Re-run in my own clone at 7fa5d92d: vitest run forgejo → 1403 pass / 1 fail, the failure being forgejo/tests/merge-identity.test.ts (ambient-token case), reproduced identically on untouched base 8c666fb in a separate worktree ⇒ pre-existing, not a hold. The full suite additionally shows 8 failures in pi-swarm-suite / pi-otel-telemetry, directories this PR does not touch (environmental). tsc -p forgejo/tsconfig.json --noEmit clean. Red→green verified myself: restoring a6d13fb's api.ts over a COPY turns the r5 cases red (2 failed / 18 passed); at head they pass.

Holds: none.

Observations, not holds (disclosed, fail-closed): on an installation whose sanctioned forge is not the built-in default, the pre-gate probe read fails closed rather than misreading — the same class as the B1 credential pin; and process.execArgv + NODE_USE_ENV_PROXY remain open by name.

model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261112T0220Z

LD-VERDICT APPROVED head=7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 session=LR-20261111-017-seatA-20261112T0220Z TREE-EQUAL confirmed first: `git diff 3462f12e8260a4f8d65408367bdb68dbbcedd251..7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 --stat` prints nothing, both trees are 24e7162f7c95c26f3e86eb0108f189ecb844d0f1, and the remote branch `lr-20261111-017-f5` is exactly 7fa5d92d — the pinned head. So the stack I judged is the same tree the earlier rounds cleared; the re-fire moved no code. Judged the full `8c666fbec432f70b4853124f8de6ee2e445d1339..7fa5d92d` diff (11 files, forgejo/* + docs/forgejo.md): - Server-name rule: the first read of every landing path (merge tool, each census row, verdict-cli's PR fetch) goes through `gateProbeConfig` at the built-in default, and gating + the pin key on `pr.base.repo.full_name`. Absent name is fail-closed gated (`goGatedFromServerName`), so a caller spelling such as `x/Homelab` cannot keep a call on the env origin, and every later read reuses the transport the server earned. - `pinGatedTransport` (forgejo/api.ts:1070) pins base + landing identity once per call and reports each ignored lever by NAME only, values never echoed, through the shared `reportGatedIgnored` (forgejo/landing.ts:3127) — no duplicate helper; non-gated repos untouched. - Landing credential (B1): `assertMergeIdentity` builds its config from `DEFAULT_BASE_URL` on EVERY repo, so `GET /user` and the merge POST never carry the token to an env/flag base; the token still comes only from `credential`, never from the transport; read-back uses `write.config`. - Fail-closed evidence reads (v14.0.3) preserved: every evidence read in `collectLandingEvidence` moved from `c` to `tc`, completeness flags unchanged; census rows keep the built-in origin when gated or unreadable. - r5 shape check: one exact pattern over quote-stripped tokens; `-r`, `-C`, quoted, mixed and near-name forms refuse, the refusal names the flag only, `--stack-size` dropped, `execArgv` and `NODE_USE_ENV_PROXY` named as open follow-ups in code and docs. The quote handling errs the safe way: only tokens that need no stripping are accepted, so any Node-side normalisation lands on the refuse side. Re-run in my own clone at 7fa5d92d: `vitest run forgejo` → 1403 pass / 1 fail, the failure being `forgejo/tests/merge-identity.test.ts` (ambient-token case), reproduced identically on untouched base 8c666fb in a separate worktree ⇒ pre-existing, not a hold. The full suite additionally shows 8 failures in pi-swarm-suite / pi-otel-telemetry, directories this PR does not touch (environmental). `tsc -p forgejo/tsconfig.json --noEmit` clean. Red→green verified myself: restoring a6d13fb's `api.ts` over a COPY turns the r5 cases red (2 failed / 18 passed); at head they pass. Holds: none. Observations, not holds (disclosed, fail-closed): on an installation whose sanctioned forge is not the built-in default, the pre-gate probe read fails closed rather than misreading — the same class as the B1 credential pin; and `process.execArgv` + `NODE_USE_ENV_PROXY` remain open by name. model: omniroute/deepseek-flash session: LR-20261111-017-seatA-20261112T0220Z
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!132
No description provided.