From d540f908ed494f7b3e7c0b33f04b1de14e5d41d9 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Thu, 9 Jul 2026 01:23:15 -0700 Subject: [PATCH] fix(review): gate self-host vision by author opt-in --- src/queue/processors.ts | 9 +++--- test/unit/visual-vision-wiring.test.ts | 42 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 3f91c38da8..6be489f4df 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -8255,10 +8255,11 @@ export async function runVisualVisionForAdvisory( model: args.settings.aiReviewModel ?? storedVisionKey.model, } : null; - // Self-host local vision (#4335): a dedicated ollama+VLM binding lets vision run WITHOUT a maintainer - // BYOK key -- see evaluateVisualVisionGate's header for why only an HTTP-capable provider (BYOK or this) - // can see the screenshots at all. - const selfHostVisionAvailable = Boolean(env.AI_VISION); + // Self-host local vision (#4335) still consumes operator resources, so mirror the AI-spend gate used by + // the other self-host review paths: confirmed contributors only unless the repo explicitly opts in to all + // authors. BYOK remains checked above because it also requires a confirmed contributor-owned repo key. + const selfHostVisionAllowed = args.confirmedContributor || args.settings.aiReviewAllAuthors; + const selfHostVisionAvailable = selfHostVisionAllowed && Boolean(env.AI_VISION); const visionGate = evaluateVisualVisionGate({ routes: args.routes, reputationSignal: visionReputation.signal, diff --git a/test/unit/visual-vision-wiring.test.ts b/test/unit/visual-vision-wiring.test.ts index dac4053505..9e944fa498 100644 --- a/test/unit/visual-vision-wiring.test.ts +++ b/test/unit/visual-vision-wiring.test.ts @@ -438,6 +438,48 @@ describe("runVisualVisionForAdvisory: self-host local vision provider (#4335)", ]); }); + it("does not let an unconfirmed contributor spend self-host vision resources unless all-authors is enabled", async () => { + const runMock = vi.fn(async () => ({ response: findingsResponse([{ path: "/app", body: "should not run" }]) })); + const env = byokEnv(); + (env as unknown as { AI_VISION: unknown }).AI_VISION = { run: runMock }; + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + const adv = findingsHolder(); + await runVisualVisionForAdvisory(env, { + repoFullName, + pr, + author: "alice", + confirmedContributor: false, + settings: byokSettings({ aiReviewByok: false, aiReviewAllAuthors: false }), + advisory: adv, + routes: selfHostVisionRoutes(), + }); + expect(runMock).not.toHaveBeenCalled(); + expect(fetchMock).not.toHaveBeenCalled(); + expect(adv.findings).toEqual([]); + }); + + it("allows self-host vision for an unconfirmed contributor when all-authors is explicitly enabled", async () => { + const runMock = vi.fn(async () => ({ + response: findingsResponse([{ path: "/app", body: "All-authors opt-in covers this self-host call." }]), + })); + const env = byokEnv(); + (env as unknown as { AI_VISION: unknown }).AI_VISION = { run: runMock }; + stubShots(); + const adv = findingsHolder(); + await runVisualVisionForAdvisory(env, { + repoFullName, + pr, + author: "alice", + confirmedContributor: false, + settings: byokSettings({ aiReviewByok: false, aiReviewAllAuthors: true }), + advisory: adv, + routes: selfHostVisionRoutes(), + }); + expect(runMock).toHaveBeenCalledTimes(1); + expect(adv.findings[0]).toMatchObject({ detail: "All-authors opt-in covers this self-host call." }); + }); + it("prefers a configured BYOK key over env.AI_VISION when both are available", async () => { const env = byokEnv(); await upsertRepositoryAiKey(env, { repoFullName, provider: "anthropic", key: "sk-ant-vision-key", model: null });