From fd1425b11268422446a711536f1b723db83099f5 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Sun, 12 Jul 2026 06:57:44 -0700 Subject: [PATCH 1/2] fix(miner): bound rejection policy document reads --- .../gittensory-miner/lib/rejection-signal.js | 44 ++++++++++-- test/unit/miner-rejection-signal.test.ts | 68 +++++++++++++++++++ 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/packages/gittensory-miner/lib/rejection-signal.js b/packages/gittensory-miner/lib/rejection-signal.js index 3ebfec8ad..d05124d76 100644 --- a/packages/gittensory-miner/lib/rejection-signal.js +++ b/packages/gittensory-miner/lib/rejection-signal.js @@ -17,6 +17,7 @@ import { resolveAiPolicyVerdict } from "@jsonbored/gittensory-engine"; // not assume a false result here means "no rejection signal of any kind." const DEFAULT_RAW_CONTENT_BASE_URL = "https://raw.githubusercontent.com"; +const MAX_POLICY_DOC_BYTES = 128 * 1024; function parseRepoFullName(repoFullName) { if (typeof repoFullName !== "string") return null; @@ -33,13 +34,46 @@ function normalizeOptions(options = {}) { }; } +async function readBoundedPolicyDoc(response) { + const contentLength = response.headers?.get?.("content-length"); + if (contentLength !== undefined && contentLength !== null) { + const parsedLength = Number.parseInt(contentLength, 10); + if (Number.isFinite(parsedLength) && parsedLength > MAX_POLICY_DOC_BYTES) return null; + } + + if (!response.body?.getReader) { + const text = await response.text(); + return typeof text === "string" && Buffer.byteLength(text, "utf8") <= MAX_POLICY_DOC_BYTES ? text : null; + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let totalBytes = 0; + let text = ""; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + totalBytes += value.byteLength; + if (totalBytes > MAX_POLICY_DOC_BYTES) { + await reader.cancel(); + return null; + } + text += decoder.decode(value, { stream: true }); + } + text += decoder.decode(); + return text; + } finally { + reader.releaseLock(); + } +} + async function fetchPolicyDoc(target, path, resolved) { const url = `${resolved.rawContentBaseUrl}/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}/HEAD/${path}`; try { const response = await resolved.fetchImpl(url, { method: "GET", headers: { accept: "application/json", "user-agent": "loopover-miner" } }); if (!response.ok) return null; - const text = await response.text(); - return typeof text === "string" ? text : null; + return await readBoundedPolicyDoc(response); } catch { return null; } @@ -59,10 +93,8 @@ export async function resolveRejectionSignaled(repoFullName, options = {}) { if (!target) return false; const resolved = normalizeOptions(options); - const [aiUsage, contributing] = await Promise.all([ - fetchPolicyDoc(target, "AI-USAGE.md", resolved), - fetchPolicyDoc(target, "CONTRIBUTING.md", resolved), - ]); + const aiUsage = await fetchPolicyDoc(target, "AI-USAGE.md", resolved); + const contributing = aiUsage && aiUsage.trim() ? null : await fetchPolicyDoc(target, "CONTRIBUTING.md", resolved); const verdict = resolveAiPolicyVerdict({ aiUsage, contributing }); return !verdict.allowed; diff --git a/test/unit/miner-rejection-signal.test.ts b/test/unit/miner-rejection-signal.test.ts index f604a5b52..4502fad6c 100644 --- a/test/unit/miner-rejection-signal.test.ts +++ b/test/unit/miner-rejection-signal.test.ts @@ -57,6 +57,74 @@ describe("resolveRejectionSignaled (#5132)", () => { expect(result).toBe(true); }); + it("does not fetch CONTRIBUTING.md when a non-empty AI-USAGE.md decides the policy", async () => { + const fetchImpl = vi.fn(async (url: string) => { + if (url.includes("AI-USAGE.md")) return textResponse("No AI-generated pull requests, please."); + return textResponse("Do not download me"); + }); + + const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); + + expect(result).toBe(true); + expect(fetchImpl).toHaveBeenCalledTimes(1); + expect(fetchImpl.mock.calls[0]?.[0]).toContain("AI-USAGE.md"); + }); + + it("treats an oversized policy document as absent without reading its body", async () => { + const text = vi.fn(async () => "No AI-generated pull requests, please."); + const fetchImpl = routedFetch({ + "AI-USAGE.md": () => ({ + ok: true, + status: 200, + headers: new Headers({ "content-length": String(129 * 1024) }), + json: async (): Promise => { + throw new Error("json() is unused by resolveRejectionSignaled"); + }, + text, + }), + "CONTRIBUTING.md": () => textResponse("Welcome, contributors!"), + }); + + const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); + + expect(result).toBe(false); + expect(text).not.toHaveBeenCalled(); + }); + + it("cancels a streamed policy document once it exceeds the byte limit", async () => { + let canceled = false; + const chunk = new Uint8Array(65 * 1024); + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(chunk); + controller.enqueue(chunk); + }, + cancel() { + canceled = true; + }, + }); + const fetchImpl = routedFetch({ + "AI-USAGE.md": () => ({ + ok: true, + status: 200, + headers: new Headers(), + body: stream, + json: async (): Promise => { + throw new Error("json() is unused by resolveRejectionSignaled"); + }, + text: async () => { + throw new Error("streaming responses should not call text()"); + }, + }), + "CONTRIBUTING.md": () => textResponse("Welcome, contributors!"), + }); + + const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); + + expect(result).toBe(false); + expect(canceled).toBe(true); + }); + it("fails open to false when both docs 404", async () => { const fetchImpl = routedFetch({}); const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); From 84371bdfbc9519408a817673cb118ef32ce247fd Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sun, 12 Jul 2026 14:43:32 -0700 Subject: [PATCH 2/2] test(miner): close patch-coverage gaps in the bounded policy-doc reader Covers the streaming reader's normal-completion path (never exercised -- existing tests only hit the non-stream fallback and the overflow-cancel branch), plus the non-numeric content-length and oversized-non-streamed-body branches. --- test/unit/miner-rejection-signal.test.ts | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/unit/miner-rejection-signal.test.ts b/test/unit/miner-rejection-signal.test.ts index 4502fad6c..f1d2f6340 100644 --- a/test/unit/miner-rejection-signal.test.ts +++ b/test/unit/miner-rejection-signal.test.ts @@ -91,6 +91,46 @@ describe("resolveRejectionSignaled (#5132)", () => { expect(text).not.toHaveBeenCalled(); }); + it("ignores a non-numeric content-length header and falls through to reading the body", async () => { + const fetchImpl = routedFetch({ + "AI-USAGE.md": () => ({ + ok: true, + status: 200, + headers: new Headers({ "content-length": "not-a-number" }), + json: async (): Promise => { + throw new Error("json() is unused by resolveRejectionSignaled"); + }, + text: async () => "No AI-generated pull requests, please.", + }), + "CONTRIBUTING.md": () => textResponse("Welcome, contributors!"), + }); + + const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); + + expect(result).toBe(true); + }); + + it("treats an oversized non-streamed policy document as absent", async () => { + const oversizedText = "a".repeat(129 * 1024); + const fetchImpl = routedFetch({ + "AI-USAGE.md": () => ({ + ok: true, + status: 200, + headers: new Headers(), + json: async (): Promise => { + throw new Error("json() is unused by resolveRejectionSignaled"); + }, + text: async () => oversizedText, + }), + "CONTRIBUTING.md": () => textResponse("Do not submit AI-generated code."), + }); + + const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); + + // AI-USAGE.md is treated as absent (oversized), so the verdict falls through to CONTRIBUTING.md's ban. + expect(result).toBe(true); + }); + it("cancels a streamed policy document once it exceeds the byte limit", async () => { let canceled = false; const chunk = new Uint8Array(65 * 1024); @@ -125,6 +165,36 @@ describe("resolveRejectionSignaled (#5132)", () => { expect(canceled).toBe(true); }); + it("reads a streamed policy document to completion when it stays within the byte limit", async () => { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode("No AI-generated ")); + controller.enqueue(encoder.encode("pull requests, please.")); + controller.close(); + }, + }); + const fetchImpl = routedFetch({ + "AI-USAGE.md": () => ({ + ok: true, + status: 200, + headers: new Headers(), + body: stream, + json: async (): Promise => { + throw new Error("json() is unused by resolveRejectionSignaled"); + }, + text: async () => { + throw new Error("streaming responses should not call text()"); + }, + }), + "CONTRIBUTING.md": () => textResponse("Welcome, contributors!"), + }); + + const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); + + expect(result).toBe(true); + }); + it("fails open to false when both docs 404", async () => { const fetchImpl = routedFetch({}); const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl });