From 642693d1198e85c643407bf093d5ea65d8924eb1 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:17:35 -0700 Subject: [PATCH] fix(ai-review): restrict BYOK to confirmed contributors --- src/queue/processors.ts | 4 ++-- test/unit/ai-review-advisory.test.ts | 29 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 4bb96957e0..cb9bdd43a7 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -1168,11 +1168,11 @@ export async function runAiReviewForAdvisory( const packAllowsAnyAuthorBlockingReview = args.settings.gatePack === "oss-anti-slop" && args.settings.aiReviewMode === "block"; if (args.settings.aiReviewMode === "off" || (!args.confirmedContributor && !packAllowsAnyAuthorBlockingReview) || !args.advisory.headSha) return undefined; try { - // BYOK: decrypt the maintainer's provider key only when opted in. Falls back to free Workers AI when + // BYOK: decrypt the maintainer's provider key only for confirmed contributors when opted in. Falls back to free Workers AI when // no key is configured or the encryption secret is unavailable (getDecryptedRepositoryAiKey → null). // Apply config-as-code provider/model: a declared provider must match the stored key's provider (else // skip BYOK → Workers-AI fallback); a declared model overrides the stored/default model. - const storedKey = args.settings.aiReviewByok ? await getDecryptedRepositoryAiKey(env, args.repoFullName) : null; + const storedKey = args.confirmedContributor && args.settings.aiReviewByok ? await getDecryptedRepositoryAiKey(env, args.repoFullName) : null; const providerKey = storedKey && (!args.settings.aiReviewProvider || args.settings.aiReviewProvider === storedKey.provider) ? { provider: storedKey.provider, key: storedKey.key, model: args.settings.aiReviewModel ?? storedKey.model } diff --git a/test/unit/ai-review-advisory.test.ts b/test/unit/ai-review-advisory.test.ts index 04994bed1a..8d21615602 100644 --- a/test/unit/ai-review-advisory.test.ts +++ b/test/unit/ai-review-advisory.test.ts @@ -156,6 +156,35 @@ describe("runAiReviewForAdvisory", () => { expect(result).toBeUndefined(); }); + it("does not use the maintainer's BYOK key for non-confirmed oss-anti-slop blocking reviews", async () => { + const run = vi.fn(async () => ({ response: defectJson() })); + const env = createTestEnv({ + AI: { run } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + TOKEN_ENCRYPTION_SECRET: "advisory-test-encryption-secret-32bytes", + }); + await upsertRepositoryAiKey(env, { repoFullName: "acme/widgets", provider: "anthropic", key: "sk-ant-byok-key-9999", model: null }); + const fetchMock = vi.fn(async () => new Response(JSON.stringify({ content: [{ type: "text", text: notesOnlyJson() }] }), { status: 200 })); + vi.stubGlobal("fetch", fetchMock); + const adv = advisory(); + + const result = await runAiReviewForAdvisory(env, { + settings: { aiReviewMode: "block", gatePack: "oss-anti-slop", aiReviewByok: true } as RepositorySettings, + advisory: adv, + repoFullName: "acme/widgets", + pr, + author: "alice", + confirmedContributor: false, + }); + + expect(result?.notes).toContain("Likely crash."); + expect(adv.findings.map((f) => f.code)).toEqual(["ai_consensus_defect"]); + expect(fetchMock).not.toHaveBeenCalled(); + expect(run).toHaveBeenCalled(); + }); + it("uses the maintainer's BYOK provider key when aiReviewByok is on and a key is configured", async () => { const env = createTestEnv({ AI: { run: async () => ({ response: notesOnlyJson() }) } as unknown as Ai,