HOMELAB-1679: fix(forgejo): pin FORGEJO_BASE_URL and FORGEJO_LANDING_IDENTITY on GO-gated repos (LR-20261111-017, homelab-93 F5) #132
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "lr-20261111-017-f5"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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_asas 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'spr.base.repo.full_namelike every other gate choice:FORGEJO_BASE_URL(and verdict-cli's--base-url) is ignored: every read, theGET /useridentity 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_IDENTITYis ignored: the merge credential must answerGET /useras the default landing login; the server whitelist remains the outer boundary.reportGatedIgnoredhelper #131 extracted (no duplicate). Non-gated repos unchanged (both levers honored as today).Proven
tests/landing-gated-transport-pins-f5.test.ts: 6 red / 3 green-controls on the old code, 9/9 green after — identity demandedbogus-login-xand the merge POST dialled the stub origin on the old code; pinned after.assertMergeIdentitydemands the pinned login AND dials the pinned base; merge tool, census and--base-urlall pin once per call, before the first read.vitest run: 58 files / 1287 pass; the 1 failure is the known pre-existingmerge-identityambient-token case (identical with the change stashed).tsc --noEmitclean.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 nameSeatA'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 offpr.base.repo.full_namefrom 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-urlis 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 (from0ceb0ff, 865 lines).r3 (stacked on
69d9993): the census leg — seatA's r2 hold closedThe census's first read per repo now goes through
gateProbeConfig(built-in base); gating and the transport pin key off the SERVER'spr.base.repo.full_nameper row; gated/unreadable rows never leave the built-in base; non-gated rows keepFORGEJO_BASE_URLfor 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. Newtests/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 on69d9993), 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 (from69d9993, 378 lines).B1+S2 round (stacked on
e29e447, commit561c545): review-3c's blocking fix + S2assertMergeIdentitybuilds the credential-carrying config withbaseUrl: 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).HEAD
561c545121; patches: piext-f5-diff.patch (full, 2408) + piext-132-interdiff-b1s2.patch (frome29e447, 813) + earlier interdiffs.Default-deny round (stacked on
6835d30, commita6d13fb): NODE_OPTIONS invertedOn 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_FLAGSis 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-sizexrefuses instead of widening.Which of 3c's S3 holes this closes — and which remain OPEN (honest list)
--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.-r— the short--requirealias does not start with--, sonodeOptionFlagNamesdrops it before the check; quoted flags — a token like"--requirecarries a leading quote, fails thestartsWith("--")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 affectfetch, 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 (from6835d30, 369 lines).r5 round (stacked on
a6d13fb, commit3462f12): review-3c's one-shape fixreview-3c measured on Node 22 that
-r x,"--require" x,"-r" x,--max-old-space-size=1024 -r x,--no-warnings "--require" xand-C xall passed the default-deny and loaded a hook (one replacedfetchand 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-sizeentry 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.execArgvandNODE_USE_ENV_PROXYremain named follow-ups.HEAD
3462f12e82; patches: piext-f5-diff.patch (full, 2617) + piext-132-interdiff-r5.patch (froma6d13fb, 231) + earlier interdiffs.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:
Holds:
--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.--repois 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 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.pinGatedTransport(api.ts:952-1006) returns{...config, baseUrl: DEFAULT_BASE_URL}andlandingIdentity: DEFAULT_LANDING_IDENTITYwhenview.gated. Traced every caller:c = transport.config, so the wholeassertLandingLock → collectLandingEvidenceread set (getPR, commit status, paged comments/reviews/inline, changed files, branch protection) leaves at the pinned base;write(index.ts:915-917) then feedsresolveLandingCredentialAtRuntime, the identity check, the whitelist read, the merge POST and the read-back;assertMergeIdentity(api.ts:1340-1585, newtransport/viewopts) rebuilds the credential asassertedwithtransport.config.baseUrland uses it forGET /user;mergeAsLandingwrites throughidentity.config, which isasserted.config— so the merge POST cannot go to a session-set origin while the token stays the landing token;listPRsand passespinnedinto everycollectLandingEvidence;verdict-cli(verdict-cli.ts:913-928) pinsargs.baseUrlonce, before the first read; every read/write in the run is throughargs.baseUrl(apiCall/paged/loginOf/submitReview/verify), no second resolution fromenv.FORGEJO_BASE_URL;--base-urland 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 pathslock.gated ≡ isGoGatedRepo(requested)— an alias is refused, never censused or merged under the caller's spelling. The extrawritere-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 (
grepforFORGEJO_BASE_URLoutside tests: onlyloadConfigat api.ts:127 and the non-landing comment-as/seat-post entry points), and the CLI still loads under its documentednode --experimental-strip-typesentry point (smoke run past imports, no ERR_MODULE_NOT_FOUND).The line fires once per call, by name only.
reportGatedIgnored(landing.ts:3141-3160) prints name + repo key + consequence and never a value; thesetpredicate 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 andassertMergeIdentityreceivestransport, so it does not re-pin: the end-to-end test measures exactly oneFORGEJO_BASE_URLand oneFORGEJO_LANDING_IDENTITYline and absence of the stub base and bogus login. Census/CLI pin once; the consequence clauses say what the pin did.Non-gated repos still honour both levers.
pinGatedTransportreturns the config untouched andresolveLandingIdentity(expected, env)when!view.gated; two control tests prove both levers end-to-end at the helper level (identity demanded = the env login,GET /userdialled at the session base, zero lines) and the CLI control proves--base-urlis honoured off the gated list. Full suite green at the head confirms no regression on the non-gated paths.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) to8c666fband ran the new file — 6 failed / 3 passed, exactly the bogus-login cases, the stub-originGET /usercase, the merge-POST-origin case, theverdict-cli --base-urlgated case and the end-to-end tool case; restored to the head and got 9/9 back (file diff identical after restore).tsc --noEmitclean;vitest runat the head 1287 pass / 1 fail, and that one (tests/merge-identity.test.ts) fails identically with the base sources in place — ambientFORGEJO_LANDING_TOKEN_FILEin 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.mdstates 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 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 intoassertLandingLock→collectLandingEvidence→getPR(opts.probe ?? c)at :590-596),collectLandingEvidencepinning ONCE offidentity.view(the server'sbase.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 insidecollectState(:522-527) before any later read;loginOf(:970) and the verdict POST (:1139) both come after it and read the pinnedargs.baseUrl. The write reuseslock.transport(index.ts:988-992) soassertMergeIdentity/mergeAsLanding/readMergeOutcomestay on the origin the probe proved, with one line per ignored lever per call.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 withrepo_ref=x/Homelabmerges 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.FORGEJO_BASE_URL is set and ignoredand 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.tests/merge-identity.test.ts:144ambient-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) })whererepoRefis parsed straight fromparams.repo_ref, and each row's evidence read is then handed thatpinnedconfig with noprobe. 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/pullsthenGET /api/v1/repos/x/Homelab/pulls/79— and never asked the sanctioned forge; the proxy can answerbase.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 letFORGEJO_BASE_URLgovern 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 APPROVED head=69d9993e45faafc20a6c585e802333da84f2b7e2 session=LR-20261111-017-seatB-20261111T1905Z
Fresh read-only clone, exact head
69d9993(verified via the PR record: Head sha matches, base8c666fb). Judged the full base..head diff (8 files, +1173/-44).gateProbeConfig(index.ts:979) is the config of the FIRST read,getPRinsidecollectLandingEvidence(index.ts:390); identity is decided frompr.base.repo.full_namethere withserverRepoView(..., {requireReported:true})(index.ts:398), the pin is paid ONCE from that view (index.ts:408), and every later evidence read takestc = transport.config(CI, comments, reviews, inline, files, required-CI). The write reuseslock.transport: credential staging onwrite.config(index.ts:1010),assertMergeIdentity({transport: write})which dials the transport base forGET /user(api.ts:1585),mergeAsLanding(identity)writing withidentity.config(api.ts:1861), read-back onwrite.config.configured(the env base) is used nowhere in the write path after the probe.collectState: probe at the built-in base (verdict-cli.ts:498), gate from the server name, pin (verdict-cli.ts:522),args.baseUrlnarrowed 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.pinGatedTransportcall 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.--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.--base-urlorigin while the first read of eachcollectStateopens on the default.FORGEJO_BASE_URL is set and ignoredand oneFORGEJO_LANDING_IDENTITY is set and ignoredline per call, by name, and the test asserts neither the stub base nor the bogus login appears in stderr; same shape asserted forassertMergeIdentityand the verdict run.reportGatedIgnoredcarries 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 base8c666fb(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
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 on69d9993; 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-017LD-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
getPR(opts.probe ?? c)(forgejo/index.ts:392) whereprobe = gateProbeConfig(configured)(index.ts:981 → api.ts:112) — built-in DEFAULT_BASE_URL; the pin is paid once, keyed on the SERVER'spr.base.repo.full_name(index.ts:410), fail-closed togated:truewhen the name is unreadable/absent. Every subsequent evidence read usestc(index.ts:437,448,460,480,511,548,565);GET /userand the merge POST usewrite.config/identity.config(index.ts:1021, 1103; api.ts:1871), so the write cannot leave on another origin.listPRs(probe, …)(index.ts:1208) at the built-in base; each row's first read is the same probe viacollectLandingEvidence(..., { 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).gateProbeConfig({ baseUrl: args.baseUrl, … }).baseUrl(verdict-cli.ts:498); pin once at :522 onserverRepoView's view;args.baseUrlnarrowed at :527, so every later read (status/commits/reviews/files/branch_protections),GET /userand the formal-review POST inherit the pinned origin.grepforpinGatedTransport/isGoGatedRepo/repoKeyfed from a parsedrepo_refor--repofinds only comments — the string overloadresolveLandingPolicy(env, repoRef)(landing.ts:3211) has no tool caller; all four real call sites pass a server-derived{ref,gated}view.FORGEJO_BASE_URLonly after the probe proved the name non-gated against the sanctioned forge (control test asserts origins [default, default, then env…]).FORGEJO_BASE_URL is set and ignoredline and that the stub origin string never appears in stderr.Evidence
69d9993goes 2/3 red (first-read-origin assertion red, expected default / received proxy); the f5b server-keyed file on0ceb0ffgoes 5/7 red.8c666fb— pre-existing, not a hold.tsc --noEmitclean.Holds: none.
model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261111T1916Z
LD-VERDICT APPROVED head=e29e447e3dddb6c388bc571f54cbb29328d9d652 session=LR-20261111-017-seatB-20261111T1916Z
Exact head verified: forgejo_get_pr reports Head sha
e29e447e3d(base8c666fb), 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):
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 usestc = transport.config(index.ts:426): commit status, head date, comments, reviews, inline comments, files, required CI contexts. The write leg reuseslock.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.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 getcback. No pre-pin from parsed repo_ref remains.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.
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.
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.
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.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):
model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261111T1916Z
--importand--use-system-cain NODE_OPTIONS on a gated landing (LR-20261111-017, homelab-93 S2 round 3b)LD-VERDICT REQUEST_CHANGES head=6835d3009f4a37eaabf1fd452dfe341864f65835 session=LR-20261111-017-seatA-20261111T2136Z
Head confirmed exact (
6835d3009f, base8c666fbec4, branch lr-20261111-017-f5); no refusal needed. Judged the full base..head diff, with the walk concentrated on561c545and6835d30.What I checked (measured, not read off the description):
assertMergeIdentitybuilds the credential-carrying config from the CONSTANTDEFAULT_BASE_URL(forgejo/api.ts:1690) on every repo, gated or not; the proof carries that config non-enumerably andmergeAsLandingPOSTs toidentity.config(forgejo/api.ts:1880), soGET /userand 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 zeroGET /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.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.--importand--use-system-caare inGATED_NODE_OPTION_FLAGS(forgejo/api.ts:118-125) with consequence text; boundary regex(^|\s)flag(=|\s|$)means--import /tmp/h.mjsand--import=…refuse while--importx/--use-system-caxdo not.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 ate29e447, 15 of 30 tests across the four files fail (relay probe, 13 S2, both flipped controls) and 15 pass.git diff --name-status 8c666fb..6835d30does not list forgejo/seat-post.ts or forgejo/comment-as.ts — that S1 stays a named follow-up.vitest run: 62 files, 61 pass; 1311 tests pass, 1 fail — tests/merge-identity.test.ts "merges with the dedicated landing credential", the ambientFORGEJO_LANDING_TOKEN_FILEcase, reproduced at base8c666fband passing with that env unset, so pre-existing and not a hold.tsc --noEmitclean.HOLD (1)
GATED_NODE_OPTION_FLAGS) — the ESM loader lever is only half closed.--loaderand--experimental-loaderare Node-accepted inNODE_OPTIONS(Node v22.23.4, measured here) andgatedProcessInterpositionreturnsundefinedfor both (forgejo/api.ts:1023), so the S2 refusal never fires for them. Measured end to end: withNODE_OPTIONS="--experimental-loader=<hook>"a session hook rewrote forgejo/api.ts at load time and a gatedpinGatedTransportreturned normally (base = the session's origin, identitypi-landing) — the base pin and the refusal both gone — whereas the same call with--requirethrowsGatedProcessEnvRefusal. That is exactly the lever6835d30adds--importfor ("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--loaderand--experimental-loaderto 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-denyNODE_OPTIONSand allow only the benign flags it knowingly accepts (--max-old-space-sizeetc., the boundary test already covers that shape).Notes, not holds:
--openssl-configis also accepted inNODE_OPTIONSand 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 APPROVED head=6835d3009f4a37eaabf1fd452dfe341864f65835 session=LR-20261111-017-seatB-20261111T2136Z
Reviewed the full base..head diff (8c666fb..6835d30, 11 files) with focus on
561c545and6835d30, read-only clone, no other seat's verdict consulted.(1) Credential-carrying config — traced.
assertMergeIdentitybuildsassertedwithbaseUrl: DEFAULT_BASE_URLand dialsGET /useronasserted.config(api.ts:1688-1697); the proof carries that same config (api.ts:1734) andmergeAsLandingPOSTs withidentity.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.ForgejoConfigis only{baseUrl, apiKey}(types.ts:8-11), so overriding baseUrl fully pins the origin; no other field can carry a session-chosen transport.credential.configreaches the wire nowhere else (grep: api.ts:1666 pinning input, api.ts:1690 the override). Off the gated list only read-only traffic followsFORGEJO_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 ate29e447the 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:
assertGatedProcessTrustis called insidepinGatedTransport's gated branch (api.ts:1101) beforereportGatedIgnored, and the merge path reaches it fromcollectLandingEvidence(index.ts:409) right after thegateProbeConfigPR 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
561c545the 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...6835d30for 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 run1 failed | 1311 passed, the one failure being the pre-existing merge-identity ambient-token case;tsc --noEmitexit 0.No holds.
Non-blocking notes (same class, named follow-ups, none of them a reason to withhold this change):
NODE_OPTIONS="-r /tmp/h.cjs"executes code andNODE_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-rand--experimental-loaderis cheap; the honest fix for the whole class stays F1 (env not session-settable), as the code itself says.args.baseUrlafter 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.readMergeOutcomereads 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 REQUEST_CHANGES head=a6d13fb475e939ef0cc281a2ab142f2be6f06f72 session=LR-20261111-017-seatB-20261111T2146Z
Checked the full base (8c666fb)..head diff at
a6d13fb, and thea6d13fbdelta on its own (fresh read-only clone, branch lr-20261111-017-f5).What holds up:
--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.=:--max-old-space-sizex=4096,--no-warningsx,--importx=...,--use-system-caxall refuse and name the token they refused; nothing widens the list by prefix.--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--requireand not its value; blank/unset is not a value; the non-gated row (now carrying loader/openssl flags too) stays unchanged.forgejo/api.tsswapped 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.HOLDS
nodeOptionFlagNameskeeps 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, itsGET /userand its merge POST with session-chosen code already in the process. That is exactly the lever--requireis 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-rto the consequence map, plus a-r /tmp/hook.jsrow 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-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 ofgatedProcessInterpositionon head and on6835d30. Suite:npm ci,tsc --noEmitclean,vitest run1318 pass / 1 fail — the failure istests/merge-identity.test.ts(ambient-token case), identical on6835d30, pre-existing, not a hold.Verified good:
=(forgejo/api.ts:163-167):--max-old-space-sizex,--no-warningsx,--stack-sizesall refuse, so a near-name cannot widen the list.--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. On6835d30the same probe lets--loader,--experimental-loader,--openssl-configand 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.--require; no refusal line echoes a value (checked every line for path leaks).assertGatedProcessTrust(forgejo/api.ts:1149-1156); the non-gated control test now carries the loader/openssl flags and still passes.a6d13fb(api.ts, its test, docs/forgejo.md only);seat-post.tsandcomment-as.tsare absent from the full base..head diff.Holds:
nodeOptionFlagNameskeeps 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 replacedglobalThis.fetchintercepted the pinned default origin (HOOK-INTERCEPTED https://forgejo.aaron.reynoza.org/api/v1/user).gatedProcessInterposition({NODE_OPTIONS:"-r /tmp/hook.cjs"})isundefinedon head (and on6835d30), 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 ownprocess.allowedNodeEnvironmentFlagslists exactly two short forms,-rand-C, and-rloads code. Minimal fix in the existing intent: refuse ANY token beginning with-whose name is not onBENIGN_NODE_OPTION_FLAGS(true default-deny — this also covers-C), optionally cross-checked againstprocess.allowedNodeEnvironmentFlags; add a gated row for-r <file>and for the mixed form that fails on6835d30.Nit, explicitly not a hold:
2. forgejo/api.ts:122 and :132 —
--stack-sizeis 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 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 as3462f12e82, matching the pin. No code edited.What I checked, measured on Node v22.23.4:
gatedProcessInterpositiondirectly 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.jsrefuse too, so the check is default-deny rather than an alias list (forgejo/api.ts:117, :1042-:1045).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.=valuetokens 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.--stack-sizeis 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 inNODE_OPTIONS, and docs/forgejo.md:849-:851 lists the allow-list without it.tests/landing-gated-process-env-s2.test.tsred (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.assertMergeIdentityalways dialsDEFAULT_BASE_URLand the proof carries that config (api.ts:1713), andmergeAsLandingwrites withidentity.config(api.ts:2008). S2 process-trust refusal still runs insidepinGatedTransportbefore any further read/identity GET/write, and only on gated repos. F5b is honest on all three paths: merge probegateProbeConfig(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 measureurl.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 --noEmitclean.No holds.
model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-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:
-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 probeGET 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)."--no-warnings"passes (Node accepts it), quoted--require/-rrefuse; 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.-r,--require,-C), never the hook path.--stack-sizeis 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.tsc --noEmitclean. 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):
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.=") 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.model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261111T2221Z
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
-r xanswered undefined = allowed; and the--max-old-space-size=4096xnear-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/-Cand never the path.--stack-sizeis 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-upsprocess.execArgvandNODE_USE_ENV_PROXYare named (api.ts:122-125).Holds: none.
Non-blocking observations (file:line)
=, 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.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".model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-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 =
3462f12e82, base8c666fbe; diff judged base..head, 11 files). No comments or reviews of this PR were read.What I checked and measured
-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 definedfor the r5 case, plus the round-4/5 mixed case) — 2 failed / 18 passed there, 20/20 green at HEAD.-r,"-r"and a mixed--max-old-space-size=1024 -rin NODE_OPTIONS do load code (a require hook ran), so the shape check closes measured bypasses; and that--stack-sizeis 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.pr.base.repo.full_nameon 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).complete: gatedfail-closed arguments and now take the pinned config (index.ts:433-560 passtc); 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-urlis 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).--stack-sizeappears 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.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 untouched8c666fbeworktree (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-nodeabsent from this sandbox's symlinked node_modules, i.e. the platform npm-cache fault), and two pi-swarm-suite view/idle-wake files failing onrequire.resolve("@earendil-works/pi-agent-core/package.json")in my linker setup — all environment-side, none in forgejo logic.tsc -p forgejo/tsconfig.json --noEmitclean (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
originfield 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=7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 session=LR-20261111-017-seatB-20261112T0220Z
TREE-EQUAL confirmed.
git diff 3462f12e8260a4f8d65408367bdb68dbbcedd251..7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 --statprints NOTHING; both commits' trees are24e7162f7c(same as declared); head has the single parent3462f12e. 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:collectLandingEvidencedoes its one deciding read atgateProbeConfig(built-in base), resolvesserverRepoView(..., {requireReported:true}), and only then pins —forgejo/index.ts:392-425(probe, refuse-on-unreadable, pin), census per rowforgejo/index.ts:1284-1292with the listing itself at the probe:1268, verdict-cli's first read at the probeforgejo/verdict-cli.ts:489-501then pin:518-525. Caller-keyed pre-pin is gone from all three.pinGatedTransportis{ref: undefined, gated: true}in that branch, so even the refuse path could not leave the default origin. verdict-cli treats an unreadable name viagoGatedFromServerName(gated).assertMergeIdentitybuilds the credential-bearing config as{baseUrl: DEFAULT_BASE_URL}and stores it on the non-enumerable proof (forgejo/api.ts:1721-1740), andmergeAsLandingwrites withidentity.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 oldsetsemantics.^--(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-sizegone 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.{complete: gated}on comments/reviews/inline/files and the refusal-on-truncation paths are unchanged.docs/forgejo.mdupdated 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 ismerge-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 base8c666fbein 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.tsover 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, thoughdocs/forgejo.mddoes not repeat those two names); atforgejo/api.ts:146a 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=valueforms.model: omniroute/deepseek-flash
session: LR-20261111-017-seatB-20261112T0220Z
LD-VERDICT APPROVED head=7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 session=LR-20261111-017-seatA-20261112T0220Z
TREE-EQUAL confirmed first:
git diff 3462f12e8260a4f8d65408367bdb68dbbcedd251..7fa5d92d928d7bc7d6edd6e7ff84073cca6f5a32 --statprints nothing, both trees are24e7162f7c, and the remote branchlr-20261111-017-f5is exactly7fa5d92d— 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..7fa5d92ddiff (11 files, forgejo/* + docs/forgejo.md):gateProbeConfigat the built-in default, and gating + the pin key onpr.base.repo.full_name. Absent name is fail-closed gated (goGatedFromServerName), so a caller spelling such asx/Homelabcannot 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 sharedreportGatedIgnored(forgejo/landing.ts:3127) — no duplicate helper; non-gated repos untouched.assertMergeIdentitybuilds its config fromDEFAULT_BASE_URLon EVERY repo, soGET /userand the merge POST never carry the token to an env/flag base; the token still comes only fromcredential, never from the transport; read-back useswrite.config.collectLandingEvidencemoved fromctotc, completeness flags unchanged; census rows keep the built-in origin when gated or unreadable.-r,-C, quoted, mixed and near-name forms refuse, the refusal names the flag only,--stack-sizedropped,execArgvandNODE_USE_ENV_PROXYnamed 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 beingforgejo/tests/merge-identity.test.ts(ambient-token case), reproduced identically on untouched base8c666fbin 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 --noEmitclean. Red→green verified myself: restoring a6d13fb'sapi.tsover 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_PROXYremain open by name.model: omniroute/deepseek-flash
session: LR-20261111-017-seatA-20261112T0220Z