HOMELAB-2612: fix(forgejo): classify Gradle dependency metadata as dangerous #90
No reviewers
Labels
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/pi-extensions!90
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-2612-gradle-danger"
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?
Ticket: HOMELAB-2612 (child of epic HOMELAB-2518).
The gap
DEFAULT_LANDING_POLICY.dangerousPathsinforgejo/landing.tsdid not classify Gradle dependency metadata as dangerous class, so a PR that changes dependency selection or dependency integrity could land on the single-verdict routine path. A PR's class is read from its changed paths — a path the list does not name is a path that lands on one verdict.Live instance:
Homelab/ghostty-in-the-shellPR #135 (merged). Its own body says "Dangerous class — this PR changesgradle/verification-metadata.xml, which is on the ADR 0005 dangerous-paths list" — and the census classed it normal. GITS-465 filed that. Read-only evidence; nothing was posted on that PR or on any GITS ticket (G6).PR #135's changed-file list, used verbatim as the fixture:
The change
forgejo/landing.ts— a "Dependency integrity" group beside the other supply-chain entriesWhy these two and not the rest of a Gradle build, in the comment that ships with them:
libs.versions.tomlis the version catalogue — it selects the coordinates and versions the build resolves;verification-metadata.xmlpins the checksums and trusted keys those artifacts must match. Changing either changes what the build fetches and what it trusts, which is a supply-chain act, not build plumbing.Why
**/and not the plain repo-root pathpathMatches()compiles**\/to(?:.*\/)?— zero or more segments — so one pattern per file covers the repo root and a nested module, and the$anchor is what keeps a file that merely names the path out. Probe against the realpathMatches(temporary probe file, run then removed; the same matrix is asserted permanently in the test file):**/gradle/libs.versions.toml**/gradle/verification-metadata.xmlgradle/libs.versions.tomlgradle/verification-metadata.xmlapp/gradle/libs.versions.tomlapp/gradle/verification-metadata.xmlandroid/app/gradle/libs.versions.toml./gradle/libs.versions.toml./app/gradle/verification-metadata.xmlpush/build.gradle.ktsbuild.gradle.ktsgradle/wrapper/gradle-wrapper.propertiesdocs/deps.mddocs/gradle-notes.mddocs/design/gradle/libs.versions.toml.md./-prefixed forms need no extra pattern:pathMatchesstrips a leading./on both sides.docs/forgejo.md— leg 4's dangerous-path listThe two patterns are added to the documented list, plus a paragraph in the same voice as the rest of that section: what a version catalogue selects, what
verification-metadata.xmltrusts, the #135 precedent, why the patterns lead with**/, and whybuild.gradle.kts/ wrapper properties / Gradle prose deliberately stay routine (a false positive pushes ordinary PRs onto the two-session path).Red → green
Red captured before touching
landing.ts, atworking-files/homelab-2518/HOMELAB-2612-red.log:The 8 failing assertion names:
classes PR #135's exact file list dangerous, matching both Gradle filesmatches each Gradle metadata file on its own, at the repo rootmatches the nested-module forms, with and without a leading './'refuses PR #135's file list with ONE session, naming the dangerous panelrefuses the same session approving twice — that is ONE approvalpermits PR #135's file list with TWO independent sessionsdeclares both patterns in DEFAULT_LANDING_POLICY.dangerousPathsstill lets FORGEJO_DANGEROUS_PATHS REPLACE the listThe 3 that already passed are the negative controls (routine paths, build scripts/wrapper/prose, and the routine counterpart of the end-to-end case) — they had to pass before and must keep passing after.
Green at
working-files/homelab-2518/HOMELAB-2612-green.log:npx tsc --noEmit— clean (exit 0)npx vitest run— 20 files / 570 tests passed (baseline on thismain,fb18380: 19 files / 559 tests; so +1 file / +11 tests, nothing else moved)What the tests pin (11 cases,
forgejo/tests/landing-gradle-dangerous.test.ts)dangerous: trueandmatchesis exactly the two Gradle files.app/gradle/...,android/app/gradle/..., and the./-prefixed variants.model:andsession:is refused withdangerous-class change with an incomplete review panel+two independent review sessions, and the refusal namesdangerous paths touched: gradle/verification-metadata.xml, gradle/libs.versions.toml; the same session approving twice is refused as1 independent review session(s); TWO independent sessions are permitted (dangerous class: two independent sessions on the head).push/build.gradle.kts,docs/deps.md,gradle/wrapper/gradle-wrapper.properties,build.gradle.kts,settings.gradle.kts,docs/gradle-notes.md,docs/design/gradle/libs.versions.toml.md,docs/design/gradle/verification-metadata.xml.md.not dangerous class: one exact-head verdict is sufficient) — the class, not the fixture, is what refuses.DEFAULT_LANDING_POLICY.dangerousPaths, andFORGEJO_DANGEROUS_PATHSstill replaces the list (undersecrets/**the Gradle files are routine again, and against the default policy they are not — so the replacement is the only reason).Existing tests asserting
dangerousPathsNone asserted the length or exact contents of the default list, so none needed re-pointing. The only two assertions anywhere on
dangerousPathsareresolveLandingPolicy({FORGEJO_DANGEROUS_PATHS:"secrets/**"}).dangerousPaths === ["secrets/**"](landing-sessions.test.ts:258,landing-record.test.ts:134— both unaffected, because the override replaces the list) andDEFAULT_LANDING_POLICY.dangerousPathstoContain("**/rbac/**")(landing-blockers.test.ts:333). Full suite green, no test deleted or weakened.Collision check
forgejo_list_prsonHomelab/pi-extensionsat start: one open PR, #68 (HOMELAB-2321,plane/HOMELAB-2321-poll-loop-guard), whose diff ispi-swarm-suite/src/core/index.tsonly. Nothing open touchedforgejo/landing.tsordocs/forgejo.md.Scope
Three files:
forgejo/landing.ts,docs/forgejo.md,forgejo/tests/landing-gradle-dangerous.test.ts. No merge, no review, no deployment performed by this worker — the PR is here for review and landing by someone else.Model honesty: this worker ran pinned to
omniroute/deepseek-flash; I attest to that configuration and to the commands whose output is quoted above, and to nothing I did not run.Verdict: APPROVED
model: omniroute/deepseek-flash
head:
913f803713session: 01a1a5a7-9537-7446-a658-2500c1ca69d6
What I reviewed
Exact head
913f80371310144e2d3d1deba54de9e9465462f2(branchplane/HOMELAB-2612-gradle-danger, basemainfb1838001619e903868816ecbf4bbc575307573c), read in the worktree/home/aaron/repos/pi-extensions/.worktrees/HOMELAB-2612-gradle-danger, which is at that sha with a cleangit status.git diff fb18380..913f803 --statis three files, +272/−1:docs/forgejo.md(+23/−1),forgejo/landing.ts(+10),forgejo/tests/landing-gradle-dangerous.test.ts(+240, new).The whole
landing.tsdelta is ten added lines insideDEFAULT_LANDING_POLICY.dangerousPathsplus their comment; no other line of the file moved, so the lock's legs 1, 2, 4 and 5 are untouched code and leg 3 only sees two more entries in the list it already iterated.CI is green at this exact head: run #232, all 14 job contexts
success(forgejo,forgejo-ci,plane,pr-title,redact,mcp-bridge,nats-intercom,pi-friction,pi-otel-telemetry,pi-swarm-suite,ask-user-question,refocus,skill-args,todo); run #233 (python) alsopass, same sha913f803713.The live instance is real, and the fixture is the real file set
I read
Homelab/ghostty-in-the-shellPR #135 through the API rather than trusting the PR body:GET /pulls/135/filesreturns exactlydocs/deps.md,gradle/libs.versions.toml,gradle/verification-metadata.xml,push/build.gradle.kts— the same four paths the test fixture uses (the fixture lists them in a different order; classification is order-independent, and only the recordeddangerousMatchesordering follows input order, which the test pins to its own fixture order — cosmetic, noted below).That PR's own body states it is dangerous class because it changes
gradle/verification-metadata.xml, and the ticket's report is that the census classed it normal — i.e. it landed on the single-verdict routine path. The gap the ticket names is therefore a real classification gap, not a hypothetical.Pattern semantics — run, not inferred
pathMatches()atforgejo/landing.ts:1084escapes., then rewrites**/to(?:.*/)?(zero or more segments, via sentinels so the single-*rewrite cannot eat it),*to[^/]*, and anchors with^…$. So**/gradle/libs.versions.tomlcompiles to^(?:.*/)?gradle/libs\.versions\.toml$.I loaded the real
pathMatchesfrom that worktree undernode --experimental-strip-typesand ran a matrix through it. Matches:gradle/libs.versions.toml,gradle/verification-metadata.xml,app/gradle/...,android/app/gradle/...,./gradle/...,./app/gradle/...,../gradle/libs.versions.toml,app/../gradle/libs.versions.toml.Correct non-matches:
gradle/libs.versions.toml.md,docs/design/gradle/libs.versions.toml.md,docs/design/gradle/verification-metadata.xml.md,gradle/verification-metadata.xml.bak,xgradle/libs.versions.toml,gradle/libsXversions.toml,gradle/libs.versions.toml/,gradle//libs.versions.toml.So the
$anchor is what stops a file that merely names the path, the zero-or-more-segment reading really does cover root and nested module with one entry, and a..-bearing path cannot be used to step around the pattern. The zero-segment case is the one that matters most for the ticket: the vanilla root layout is covered, not only nested modules.End-to-end, driven by me (not by the PR's test file)
I built
LandingLockInputmyself againstevaluateLandingLockand ran PR #135's file list:dangerous-class change with an incomplete review panel,dangerous paths touched: gradle/verification-metadata.xml, gradle/libs.versions.toml,1 independent review session(s) (sess-me-0001) — dangerous class needs two approvals from two independent sessions.1 independent review session(s).ok=truewith the notedangerous class: two independent sessions on the head.push/build.gradle.kts+docs/deps.mdwith ONE session ⇒ok=true, notenot dangerous class: one exact-head verdict is sufficient.That is the ticket's Verify, in both directions, on the real lock.
Interactions
FORGEJO_DANGEROUS_PATHSstill REPLACES the list: undersecrets/**, PR #135's files classify{dangerous: false, matches: []}andsecrets/db.yamlstill matches — the operator opt-out is intact and the default is not silently re-added.jesus/...andsendrast/...(including a URL-shaped ref with different casing) the refusal lines readreview panel policy: legacy owner-scoped panel, i.e. the two-declared-models/one-premium rule the 05:10Z ruling protects.Homelab/ghostty-in-the-shellresolves to ownerhomelab, which is not inlegacyPanelOwners, so the modern two-session rule is the one that refuses — the test pins that boundary correctly.the changed-file list is unavailable) — the new entries do not open a hole there.DEFAULT_LANDING_POLICY.dangerousPathsis now 33 entries and contains both new patterns exactly once.Over-match hunt
Realistic false positives I could construct are vendored or fixture copies that carry the exact path:
third_party/vendor/gradle/libs.versions.tomlanddocs/fixtures/gradle/libs.versions.tomlboth MATCH, because they are literally the path. Cost of that is one extra reviewer, never a safety hole, and I looked for live instances:pi-extensions(the whole tree), thehomelabcheckout, andghostty-in-the-shell— no repo other than ghostty-in-the-shell contains either file at all, so there is no current false positive in the fleet.Correctly left alone by running them:
push/build.gradle.kts,build.gradle.kts,settings.gradle.kts,gradle/wrapper/gradle-wrapper.properties,gradle/wrapper/gradle-wrapper.jar,gradle/gradle.properties,buildSrc/src/main/kotlin/Deps.kt,docs/deps.md,docs/gradle-notes.md.Case:
GRADLE/libs.versions.tomlandgradle/Libs.versions.tomldo not match, which is correct — Gradle looks for exactlygradle/libs.versions.toml, and git paths are case-sensitive.The negative controls in the test file are real assertions on
classifyChangedPaths, not tautologies: they assert{dangerous:false, matches:[]}per path and again for the whole routine set, and the red log shows those three controls were the only assertions PASSING before the fix — exactly what a control is for.Under-match hunt — what a path these patterns miss can still do
I ran each of these through the real matcher; none match, and each is a genuine dependency-integrity act that therefore stays routine:
gradle/wrapper/gradle-wrapper.properties(itsdistributionUrl/distributionSha256Sumrepoint the whole Gradle distribution),settings.gradle.kts+build.gradle.kts(plugin and dependency coordinates),buildSrc/src/main/kotlin/Deps.kt,gradle/dependency-locks/x.lockfile,gradle.properties, and a catalogue at a non-default location (libs.versions.toml,catalog/deps.toml) whichversionCatalogs { … from(files(...)) }permits.This is scope, not a defect in what shipped: the ticket asks for the two default files, the PR body states the exclusion deliberately (a false positive pushes ordinary PRs onto the two-session path), and nothing here regresses — those paths were routine before and are routine after. I recommend a follow-up ticket for
distributionUrl/distributionSha256Sumin wrapper properties rather than widening this PR.One pre-existing bound, unchanged by this PR: the changed-file listing is capped at 10 pages × 50 = 500 paths (
api.ts listPRFiles,verdict-cli.ts paged), so a PR over 500 files could place this metadata past page 10 for every dangerous pattern alike. Not introduced here and not a reason to hold this change.Tests
In
/home/aaron/repos/pi-extensions/.worktrees/HOMELAB-2612-gradle-danger/forgejo:npx tsc --noEmitexit 0, andnpx vitest rungivesTest Files 20 passed (20),Tests 570 passed (570)— matching the expected 20 files / 570 tests, including the newtests/landing-gradle-dangerous.test.ts(11 tests).The pre-fix log
working-files/homelab-2518/HOMELAB-2612-red.logis a genuine red: 8 failing assertions with real assertion diffs (expected false to be true,expected { dangerous: false, matches: [] } to deeply equal { dangerous: true, … }) — not import errors, not compile failures — against basefb18380beforelanding.tswas touched, with the 3 negative controls passing. The green log matches what I reproduced.No existing test asserted the length or exact contents of the default list, so nothing needed re-pointing; the only other
dangerousPathsassertions in the tree are the two["secrets/**"]override assertions and thetoContain("**/rbac/**")control, all still green.Docs
docs/forgejo.mdadds dependency integrity to the leg-4 dangerous-class sentence with both patterns spelled out, plus a paragraph on what the catalogue selects and what the metadata trusts, the #135 precedent, why the patterns lead with**/, and why build scripts, wrapper properties and Gradle prose stay routine. Every claim there matches what I ran: the zero-or-more-segment reading, and the anchored non-match fordocs/design/gradle/libs.versions.toml.md. I found no overclaim — in particular the docs describe the two patterns only, and so does the code.Observations — none of these is a hold, and none asks for a change to this head
dangerous paths touched: …, so nothing is hidden from an operator; adding a phrase there is a tidy-up, not a correction./home/aaron/.pi/agent/extensions/forgejo/landing.tscontains zero occurrences of these patterns, so the class takes effect only at deploy — which is the ticket's ownOVERSEER: READY TO DEPLOYstep, and it means nobody should read the census as already enforcing this.matchesto fixture order.Hygiene
Read-only throughout: no code edit, no merge, no deploy, no writes outside my scratch directory, nothing written under
~/.pi/agent/extensions, and no Plane or GITS write —GET /issues/135/commentson ghostty-in-the-shell returns 0 comments, so the evidence read left no trace on that PR.Dispatched as one of the two independent sessions this dangerous-class change requires; the second seat reviews concurrently and I neither read nor waited for it.
I approve this head.
Verdict: APPROVED
model: omniroute/deepseek-flash
head:
913f803713session: 01a1a5a7-94f1-7446-a658-24feb471d212
Seat 1 of the two independent review sessions this dangerous-class PR requires. Read-only review. I re-derived every claim below from the code at the exact head, not from the PR body or the test's word.
Scope read at the head
git diff fb1838001619e903868816ecbf4bbc575307573c..913f80371310144e2d3d1deba54de9e9465462f2is three files:forgejo/landing.ts(+10, -0),docs/forgejo.md(+23/-1),forgejo/tests/landing-gradle-dangerous.test.ts(new, 240 lines). The base isorigin/main's tip, so nothing else can be hiding in the range.landing.tsis purely additive: the only change is two string literals plus their comment inside theDEFAULT_LANDING_POLICY.dangerousPathsarray literal, at indices 25 and 26. No leg body, no helper, nopathMatches, nosessionPanel, noisLegacyPanelRepo, noresolveLandingPolicyline was touched — legs 1, 2, 4, 5 are unchanged by construction, not by inspection only.The pattern semantics, verified by running the real
pathMatchesI did not trust the test's matrix; I built my own against the head's own
pathMatches, imported fromlanding.ts(29 asserted rows, 0 mismatches, plus 12 exploratory probes).**/compiles to(?:.*/)?— zero or more segments — and the whole pattern is wrapped in^…$, with.escaped by theescstep, so:**/gradle/libs.versions.toml**/gradle/verification-metadata.xmlgradle/libs.versions.tomlgradle/verification-metadata.xml./gradle/libs.versions.toml./gradle/verification-metadata.xmlapp/gradle/libs.versions.tomlapp/gradle/verification-metadata.xmlandroid/app/gradle/libs.versions.tomlandroid/app/gradle/verification-metadata.xmla/b/c/d/gradle/libs.versions.tomlgradle/libsXversions.tomlgradle/libs.versions.tomlxgradle/libs.versions.toml.bakgradle/libs.versions.toml/childdocs/design/gradle/libs.versions.toml.mddocs/design/gradle/verification-metadata.xml.mdgradlex/libs.versions.toml,gradle-old/…,build/libs.versions.tomlgradle/wrapper/gradle-wrapper.properties,.jarbuild.gradle.kts,push/build.gradle.kts,settings.gradle.ktsdocs/deps.md,docs/gradle-notes.mdEach pattern matches only its own file: verified both per-pattern (the off-diagonal rows) and at the classifier (below). Root,
./-root and nested all match, as claimed. The$anchor is what carries the.md/.bak/.tomlxexclusions, and it holds:docs/design/gradle/libs.versions.toml.mdis genuinely out, so the doc sentence that names it is true rather than aspirational.False positives — I looked, and found none that is harmful
Over-classification here costs one extra review session; it does not block a landing, because the dangerous class is satisfiable (proved below). So the bar for "realistic false positive" is a legitimate ordinary PR pushed onto the two-session path. The exploratory probes that do match, and why each is acceptable, not a blocker:
src/gradle/libs.versions.toml,android/app/gradle/…,vendor/acme/gradle/libs.versions.toml— a nested or vendored version catalogue. A vendored catalogue that a build resolves is still a file that selects coordinates and versions; the rule catching it is the rule working.docs/gradle/libs.versions.toml— a doc directory literally namedgradleholding a file of exactly that name. Needs an implausible layout to bite.forgejo/tests/fixtures/gradle/libs.versions.toml— a test fixture copy. This is the only one I would call a genuine (if cheap) false positive: a future PR adding a fixture catalogue to a tool that parses them would be classed dangerous. Not a blocker: the diff shape is still "a version catalogue landing in a repository", slowing it by one session is the conservative direction, andFORGEJO_DANGEROUS_PATHSremains an operator escape hatch. Naming it so it is a known cost, not a surprise.Things that are correctly not caught and would have been the damaging false positives:
build.gradle.kts(in a real Android/iOS repo, every module has one),gradle/wrapper/*, and prose. I confirmed against the live repo the fixture comes from:Homelab/ghostty-in-the-shellhas exactly onegradle/directory, containinglibs.versions.toml,verification-metadata.xmlandwrapper— so the wrapper stays routine in the repo where this class fires most.End-to-end: the consequence is real
I drove
evaluateLandingLockfrom my own harness with the head's code, independent of the PR's test file, on PR #135's exact changed-file list (gradle/verification-metadata.xml,gradle/libs.versions.toml,push/build.gradle.kts,docs/deps.md— I confirmed that list against the Forgejo API forHomelab/ghostty-in-the-shell#135, it is verbatim, and GitHub/Forgejo reports no fifth file):model:andsession:→ refused,dangerous-class change with an incomplete review panel,dangerous paths touched: gradle/verification-metadata.xml, gradle/libs.versions.toml, policy line "two independent review sessions",1 independent review session(s). Route: leg 3, not a CI or verdict miss.1 independent review session(s)and collapsed on the reviewer key. One session is one approval.dangerous class: two independent sessions on the head (… session=… session=…). Both onomniroute/deepseek-flashstill permits, which is the HOMELAB-2659 rule and not this PR's business.push/build.gradle.kts,docs/deps.md) → permitted,not dangerous class: one exact-head verdict is sufficient. So the class refuses, not the fixture.reviewerIdentities→ still refused as non-evidence; an unavailable changed-file list → still fails closed on the new patterns' account.The classifier on #135's list returns
matches= exactly the two Gradle files, no duplication (classifyChangedPathsbreaks after the first matching pattern per file).Override and the legacy owner panel
FORGEJO_DANGEROUS_PATHS=secrets/**→resolveLandingPolicyreturns["secrets/**"]exactly; #135's list is then routine, and dangerous against the default policy. So replacement still works and the defaults are not silently merged, sorted or re-appended. The default array itself is unmutated byresolveLandingPolicy(it is returned by reference only when the env var is unset), contains no duplicate, and the two new entries sit inside the literal, not in a post-processing step.["**/gradle/libs.versions.toml", "**/gradle/verification-metadata.xml"]confirmed at indices 25/26 of a 33-entry list.Sendrast/sendrast-webwith a dangerous Gradle file →isLegacyPanelRepotrue, and the refusal names the legacy owner-scoped panel ("two-declared-models/one-premium"), demanding two models and a premium seat, not two sessions. Verified four ways: two sessions on one model → refused on models/premium; two models without a premium → refused on premium; two models with one premium → permitted, notedangerous class (legacy owner panel): two models on the head. Same forjesus/…, and for the URL-shaped repoRefhttps://forgejo.aaron.reynoza.org/Sendrast/sendrast-web.git(owner parsing holds). The Gradle patterns therefore classify the change but never override the panel: they cannot impose the two-session rule on those owners.Sendrast/sendrast-webhas nogradle/directory at all (404 oncontents/gradle). Worth stating rather than implying it was measured live.Tests and the red log
npx tsc --noEmitclean;npx vitest run→ 20 files / 570 tests, all pass, exactly the expected totals. Worktreegit statusclean afterwards.working-files/homelab-2518/HOMELAB-2612-red.logis genuine: 8 failures, and I checked each assertion — they assert the old behaviour (classifier false, both patterns absent from a 29-entry default, one verdict permitting,classifyChangedPaths(PR_135_FILES).dangerousfalse at the foot of the override test). The 3 that already passed before the change are the negative controls and the routine counterpart — they had to pass before, and they still pass. No test was weakened, skipped or deleted; no existing test asserted the default list's length or exact contents, so nothing needed re-pointing and nothing was.Docs
docs/forgejo.mdadds the two patterns to the enumerated leg-4 list and one paragraph in the section's existing voice: what a catalogue selects, whatverification-metadata.xmltrusts, the #135 precedent, why**/rather than two entries, and why build scripts/wrapper/prose deliberately stay routine. Every factual claim in it is one I checked above, including thedocs/design/gradle/libs.versions.toml.mdexclusion. No overclaim: it does not say the list is exhaustive, and the "same class of act as editing an RBAC rule" framing is an analogy, not a rule statement.Non-blocking observations (not defects in this diff, and not reasons to refuse it)
dangerous paths touched:line one row above it, which does name both files. Cosmetic drift for a later docs pass.gradleand the filename to be exactly one of these two. A build can point a catalogue elsewhere (versionCatalogs { … from(files("gradle/deps.toml")) }) or moveverification-metadata.xml, andbuild.gradle.kts— which can declare the same coordinates inline and more — is deliberately and reasonably left routine. This is strictly weaker than the tradeoff the PR already argued, so it cannot be a requirement this PR failed to meet.SESSION_DECL_RE(\bsession\s*[:=]\s*\?([\w.:-]{6,})) lets an **empty**session:value swallow the next word on the following line — my probe producedsession: Reviewedfrom a blank declaration followed by prose. I found it while probing and it changed no verdict in my matrix (an empty declaration still refuses), but it is a hole in the "a verdict with nosession:` refuses" sentence that a separate ticket could close.Hygiene
Probe scripts were written under
/tmpand nowhere else; the worktree is clean at913f803; no write to~/.pi/agent/extensions; no Plane or GITS write — the only outbound reads were the Forgejo API for this PR and for #135's file list, as read-only evidence. No token in argv. Nothing posted on anyone's behalf; no review, comment or merge beyond this one verdict.Verdict
APPROVED, seat 1 of 2. The two patterns do what they claim at every depth the ticket names, the classifier is suffix-anchored enough that the damaging false positives (
build.gradle.kts, wrapper, prose) stay routine, the end-to-end refusal with one session and permission with two are real in the code and not only in the test, the override still replaces, and the legacy owner panel still governsjesus/Sendrastunchanged. No blocker.